if (engine->la_context) {
// sheet variant in progress
if (strcmp(mechanism_get_string(mech), "builtin:authenticate") == 0) {
+ // set the UID the same way as SecurityAgent would
+ if (auth_items_exist(engine->context, "sheet-uid")) {
+ os_log_debug(AUTHD_LOG, "engine: setting sheet UID %d to the context", auth_items_get_uint(engine->context, "sheet-uid"));
+ auth_items_set_uint(engine->context, "uid", auth_items_get_uint(engine->context, "sheet-uid"));
+ }
+
// find out if sheet just provided credentials or did real authentication
// if password is provided or PAM service name exists, it means authd has to evaluate credentials
// otherwise we need to check la_result
auth_items_set_string(engine->context, kAuthorizationEnvironmentUsername, user);
struct passwd *pwd = getpwnam(user);
require(pwd, done);
- auth_items_set_int(engine->context, AGENT_CONTEXT_UID, pwd->pw_uid);
+ auth_items_set_uint(engine->context, "sheet-uid", pwd->pw_uid);
// move sheet-specific items from hints to context
const char *service = auth_items_get_string(engine->hints, AGENT_CONTEXT_AP_PAM_SERVICE_NAME);
bool engine_acquire_sheet_data(engine_t engine)
{
- uid_t uid = auth_items_get_int(engine->context, AGENT_CONTEXT_UID);
+ uid_t uid = auth_items_get_int(engine->context, "sheet-uid");
if (!uid)
return false;
NSString* octagonKeyName;
SecKeyRef publicKey;
+ if (SOSFullPeerInfoHaveOctagonKeys(self.fullPeerInfo)) {
+ return;
+ }
+
bool changedSelf = false;
CFErrorRef copyError = NULL;
_SOSCircleAcceptRequest
_SOSCircleHasPeer
+_SOSFullPeerInfoCopyOctagonSigningKey
+_SOSFullPeerInfoCopyOctagonEncryptionKey
+_SOSFullPeerInfoCopyOctagonPublicEncryptionKey
+_SOSFullPeerInfoCopyOctagonPublicSigningKey
_SOSPiggyBackBlobCreateFromData
_SOSPiggyBackBlobCopyEncodedData
SecKeyRef SOSFullPeerInfoCopyOctagonPublicEncryptionKey(SOSFullPeerInfoRef fullPeer, CFErrorRef* error);
SecKeyRef SOSFullPeerInfoCopyOctagonSigningKey(SOSFullPeerInfoRef fullPeer, CFErrorRef* error);
SecKeyRef SOSFullPeerInfoCopyOctagonEncryptionKey(SOSFullPeerInfoRef fullPeer, CFErrorRef* error);
+bool SOSFullPeerInfoHaveOctagonKeys(SOSFullPeerInfoRef fullPeer);
bool SOSFullPeerInfoPurgePersistentKey(SOSFullPeerInfoRef peer, CFErrorRef* error);
return SOSFullPeerInfoCopyMatchingOctagonEncryptionPrivateKey(fullPeer, error);
}
+bool SOSFullPeerInfoHaveOctagonKeys(SOSFullPeerInfoRef fullPeer)
+{
+ SOSPeerInfoRef pi = SOSFullPeerInfoGetPeerInfo(fullPeer);
+ if (pi == NULL) {
+ return false;
+ }
+
+ return
+ SOSPeerInfoHasOctagonSigningPubKey(pi) &&
+ SOSPeerInfoHasOctagonEncryptionPubKey(pi);
+}
+
+
//
// MARK: Encode and decode
//
return &certificate->_subjectAltName->extnValue;
}
+static bool convertIPAddress(CFStringRef name, CFDataRef *dataIP) {
+ /* IPv4: 4 octets in decimal separated by dots. We don't support matching IPv6 already. */
+ bool result = false;
+ /* Check size */
+ if (CFStringGetLength(name) < 7 || /* min size is #.#.#.# */
+ CFStringGetLength(name) > 15) { /* max size is ###.###.###.### */
+ return false;
+ }
+
+ CFCharacterSetRef decimals = CFCharacterSetCreateWithCharactersInString(NULL, CFSTR("0123456789."));
+ CFCharacterSetRef nonDecimals = CFCharacterSetCreateInvertedSet(NULL, decimals);
+ CFMutableDataRef data = CFDataCreateMutable(NULL, 0);
+ CFArrayRef parts = CFStringCreateArrayBySeparatingStrings(NULL, name, CFSTR("."));
+
+ /* Check character set */
+ if (CFStringFindCharacterFromSet(name, nonDecimals,
+ CFRangeMake(0, CFStringGetLength(name)),
+ kCFCompareForcedOrdering, NULL)) {
+ goto out;
+ }
+
+ /* Check number of labels */
+ if (CFArrayGetCount(parts) != 4) {
+ goto out;
+ }
+
+ /* Check each label and convert */
+ CFIndex i, count = CFArrayGetCount(parts);
+ for (i = 0; i < count; i++) {
+ CFStringRef octet = CFArrayGetValueAtIndex(parts, i);
+ char *cString = CFStringToCString(octet);
+ uint32_t value = atoi(cString);
+ free(cString);
+ if (value > 255) {
+ goto out;
+ } else {
+ uint8_t byte = value;
+ CFDataAppendBytes(data, &byte, 1);
+ }
+ }
+ result = true;
+ if (dataIP) {
+ *dataIP = CFRetain(data);
+ }
+
+out:
+ CFReleaseNull(data);
+ CFReleaseNull(parts);
+ CFReleaseNull(decimals);
+ CFReleaseNull(nonDecimals);
+ return result;
+}
+
static OSStatus appendIPAddressesFromGeneralNames(void *context,
SecCEGeneralNameType gnType, const DERItem *generalName) {
CFMutableArrayRef ipAddresses = (CFMutableArrayRef)context;
return ipAddresses;
}
+static OSStatus appendIPAddressesFromX501Name(void *context, const DERItem *type,
+ const DERItem *value, CFIndex rdnIX) {
+ CFMutableArrayRef addrs = (CFMutableArrayRef)context;
+ if (DEROidCompare(type, &oidCommonName)) {
+ CFStringRef string = copyDERThingDescription(kCFAllocatorDefault,
+ value, true);
+ if (string) {
+ CFDataRef data = NULL;
+ if (convertIPAddress(string, &data)) {
+ CFArrayAppendValue(addrs, data);
+ CFReleaseNull(data);
+ }
+ CFRelease(string);
+ } else {
+ return errSecInvalidCertificate;
+ }
+ }
+ return errSecSuccess;
+}
+
+CFArrayRef SecCertificateCopyIPAddressesFromSubject(SecCertificateRef certificate) {
+ CFMutableArrayRef addrs = CFArrayCreateMutable(kCFAllocatorDefault,
+ 0, &kCFTypeArrayCallBacks);
+ OSStatus status = parseX501NameContent(&certificate->_subject, addrs,
+ appendIPAddressesFromX501Name);
+ if (status || CFArrayGetCount(addrs) == 0) {
+ CFReleaseNull(addrs);
+ return NULL;
+ }
+ return addrs;
+}
+
static OSStatus appendDNSNamesFromGeneralNames(void *context, SecCEGeneralNameType gnType,
const DERItem *generalName) {
CFMutableArrayRef dnsNames = (CFMutableArrayRef)context;
return errSecSuccess;
}
+CFArrayRef SecCertificateCopyDNSNamesFromSubject(SecCertificateRef certificate) {
+ CFMutableArrayRef dnsNames = CFArrayCreateMutable(kCFAllocatorDefault,
+ 0, &kCFTypeArrayCallBacks);
+ OSStatus status = parseX501NameContent(&certificate->_subject, dnsNames,
+ appendDNSNamesFromX501Name);
+ if (status || CFArrayGetCount(dnsNames) == 0) {
+ CFReleaseNull(dnsNames);
+ return NULL;
+ }
+
+ /* appendDNSNamesFromX501Name allows IP addresses, we don't want those for this function */
+ __block CFMutableArrayRef result = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
+ CFArrayForEach(dnsNames, ^(const void *value) {
+ CFStringRef name = (CFStringRef)value;
+ if (!convertIPAddress(name, NULL)) {
+ CFArrayAppendValue(result, name);
+ }
+ });
+ CFReleaseNull(dnsNames);
+ if (CFArrayGetCount(result) == 0) {
+ CFReleaseNull(result);
+ }
+
+ return result;
+}
+
/* Not everything returned by this function is going to be a proper DNS name,
we also return the certificates common name entries from the subject,
assuming they look like dns names as specified in RFC 1035. */
return errSecSuccess;
}
+CFArrayRef SecCertificateCopyRFC822NamesFromSubject(SecCertificateRef certificate) {
+ CFMutableArrayRef rfc822Names = CFArrayCreateMutable(kCFAllocatorDefault,
+ 0, &kCFTypeArrayCallBacks);
+ OSStatus status = parseX501NameContent(&certificate->_subject, rfc822Names,
+ appendRFC822NamesFromX501Name);
+ if (status || CFArrayGetCount(rfc822Names) == 0) {
+ CFRelease(rfc822Names);
+ rfc822Names = NULL;
+ }
+ return rfc822Names;
+}
+
static OSStatus appendCommonNamesFromX501Name(void *context,
const DERItem *type, const DERItem *value, CFIndex rdnIX) {
CFMutableArrayRef commonNames = (CFMutableArrayRef)context;
DERItem *SecCertificateGetExtensionValue(SecCertificateRef certificate, CFTypeRef oid);
+CFArrayRef SecCertificateCopyDNSNamesFromSubject(SecCertificateRef certificate);
+CFArrayRef SecCertificateCopyIPAddressesFromSubject(SecCertificateRef certificate);
+CFArrayRef SecCertificateCopyRFC822NamesFromSubject(SecCertificateRef certificate);
+
__END_DECLS
#endif /* !_SECURITY_SECCERTIFICATEINTERNAL_H_ */
_SecCertificateCopyCompanyName
_SecCertificateCopyCountry
_SecCertificateCopyDNSNames
+_SecCertificateCopyDNSNamesFromSubject
_SecCertificateCopyData
_SecCertificateCopyEmailAddresses
_SecCertificateCopyEscrowRoots
_SecCertificateCopyExtendedKeyUsage
_SecCertificateCopyiAPAuthCapabilities
_SecCertificateCopyIPAddresses
+_SecCertificateCopyIPAddressesFromSubject
_SecCertificateCopyiPhoneDeviceCAChain
_SecCertificateCopyIssuerSHA1Digest
_SecCertificateCopyIssuerSequence
_SecCertificateCopyPublicKey
_SecCertificateCopyPublicKeySHA1Digest
_SecCertificateCopyRFC822Names
+_SecCertificateCopyRFC822NamesFromSubject
_SecCertificateCopySerialNumber
_SecCertificateCopySerialNumberData
_SecCertificateCopySHA256Digest
/* Security.framework's bundle id. */
+#if TARGET_OS_IPHONE
static CFStringRef kSecFrameworkBundleID = CFSTR("com.apple.Security");
+#else
+static CFStringRef kSecFrameworkBundleID = CFSTR("com.apple.security");
+#endif
CFGiblisGetSingleton(CFBundleRef, SecFrameworkGetBundle, bundle, ^{
*bundle = CFRetainSafe(CFBundleGetBundleWithIdentifier(kSecFrameworkBundleID));
__block SecDbItemRef oldItem = NULL;
bool ok = kc_with_dbt(false, &cferror, ^bool (SecDbConnectionRef dbt) {
- Query *q = query_create_with_limit( (__bridge CFDictionaryRef) @{
- (__bridge NSString *)kSecValuePersistentRef : newItemPersistentRef,
- (__bridge NSString *)kSecAttrAccessGroup : accessGroup,
- },
- NULL,
- 1,
- &cferror);
- if(cferror) {
- secerror("couldn't create query for new item pref: %@", cferror);
- return false;
- }
-
- if(!SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
- newItem = CFRetainSafe(item);
- })) {
- query_destroy(q, NULL);
- secerror("couldn't run query for new item pref: %@", cferror);
- return false;
- }
-
- if(!query_destroy(q, &cferror)) {
- secerror("couldn't destroy query for new item pref: %@", cferror);
- return false;
- };
-
- if(oldCurrentItemPersistentRef) {
- q = query_create_with_limit( (__bridge CFDictionaryRef) @{
- (__bridge NSString *)kSecValuePersistentRef : oldCurrentItemPersistentRef,
- (__bridge NSString *)kSecAttrAccessGroup : accessGroup,
- },
- NULL,
- 1,
- &cferror);
+ // Use a DB transaction to gain synchronization with all CKKS zones.
+ return kc_transaction_type(dbt, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, ^bool {
+ Query *q = query_create_with_limit( (__bridge CFDictionaryRef) @{
+ (__bridge NSString *)kSecValuePersistentRef : newItemPersistentRef,
+ (__bridge NSString *)kSecAttrAccessGroup : accessGroup,
+ },
+ NULL,
+ 1,
+ &cferror);
if(cferror) {
- secerror("couldn't create query: %@", cferror);
+ secerror("couldn't create query for new item pref: %@", cferror);
return false;
}
if(!SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
- oldItem = CFRetainSafe(item);
+ newItem = CFRetainSafe(item);
})) {
query_destroy(q, NULL);
- secerror("couldn't run query for old item pref: %@", cferror);
+ secerror("couldn't run query for new item pref: %@", cferror);
return false;
}
if(!query_destroy(q, &cferror)) {
- secerror("couldn't destroy query for old item pref: %@", cferror);
+ secerror("couldn't destroy query for new item pref: %@", cferror);
return false;
};
- }
- CKKSViewManager* manager = [CKKSViewManager manager];
- if(!manager) {
- secerror("SecItemSetCurrentItemAcrossAllDevices: no view manager?");
- cferror = (CFErrorRef) CFBridgingRetain([NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey: @"No view manager, cannot forward request"}]);
- return false;
- }
- [manager setCurrentItemForAccessGroup:newItem
- hash:newItemSHA1
- accessGroup:accessGroup
- identifier:identifier
- viewHint:viewHint
- replacing:oldItem
- hash:oldItemSHA1
- complete:complete];
- return true;
+ if(oldCurrentItemPersistentRef) {
+ q = query_create_with_limit( (__bridge CFDictionaryRef) @{
+ (__bridge NSString *)kSecValuePersistentRef : oldCurrentItemPersistentRef,
+ (__bridge NSString *)kSecAttrAccessGroup : accessGroup,
+ },
+ NULL,
+ 1,
+ &cferror);
+ if(cferror) {
+ secerror("couldn't create query: %@", cferror);
+ return false;
+ }
+
+ if(!SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
+ oldItem = CFRetainSafe(item);
+ })) {
+ query_destroy(q, NULL);
+ secerror("couldn't run query for old item pref: %@", cferror);
+ return false;
+ }
+
+ if(!query_destroy(q, &cferror)) {
+ secerror("couldn't destroy query for old item pref: %@", cferror);
+ return false;
+ };
+ }
+
+ CKKSViewManager* manager = [CKKSViewManager manager];
+ if(!manager) {
+ secerror("SecItemSetCurrentItemAcrossAllDevices: no view manager?");
+ cferror = (CFErrorRef) CFBridgingRetain([NSError errorWithDomain:@"securityd" code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey: @"No view manager, cannot forward request"}]);
+ return false;
+ }
+ [manager setCurrentItemForAccessGroup:newItem
+ hash:newItemSHA1
+ accessGroup:accessGroup
+ identifier:identifier
+ viewHint:viewHint
+ replacing:oldItem
+ hash:oldItemSHA1
+ complete:complete];
+ return true;
+ });
});
CFReleaseNull(newItem);
ok = false;
}
+ // If CKKS had spun up, it's very likely that we just deleted its data.
+ // Tell it to perform a local resync.
+ SecCKKSPerformLocalResync();
+
errOut:
CFReleaseSafe(sys_bound);
CFReleaseSafe(keybaguuid);
/*
- * Copyright (c) 2015 Apple Inc. All Rights Reserved.
+ * Copyright (c) 2015-2017 Apple Inc. All Rights Reserved.
*
* @APPLE_LICENSE_HEADER_START@
*
typedef struct {
const CFArrayRef subtrees;
match_t *match;
+ bool permit;
} nc_san_match_context_t;
static OSStatus nc_compare_subtree(void *context, SecCEGeneralNameType gnType, const DERItem *generalName) {
static void nc_decode_and_compare_subtree(const void *value, void *context) {
CFDataRef subtree = value;
nc_match_context_t *match_context = context;
- if(subtree) {
+ if (subtree) {
/* convert subtree to DERItem */
const DERItem general_name = { (unsigned char *)CFDataGetBytePtr(subtree), CFDataGetLength(subtree) };
DERDecodedInfo general_name_content;
return true;
}
-static void nc_compare_subject_to_subtrees(CFDataRef subject, CFArrayRef subtrees, match_t *match) {
- /* An empty subject name is considered not present */
- if (isEmptySubject(subject)) {
+/*
+ * We update match structs as follows:
+ * 'present' is true if there's any subtree of the same type as any Subject/SAN
+ * 'match' is false if the subtree(s) and Subject(s)/SAN(s) don't match.
+ * Note: the state of 'match' is meaningless without 'present' also being true.
+ */
+static void update_match(bool permit, match_t *input_match, match_t *output_match) {
+ if (!input_match || !output_match) {
return;
}
-
- CFIndex num_trees = CFArrayGetCount(subtrees);
- CFRange range = { 0, num_trees };
- const DERItem subject_der = { (unsigned char *)CFDataGetBytePtr(subject), CFDataGetLength(subject) };
- nc_match_context_t context = {GNT_DirectoryName, &subject_der, match};
- CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &context);
+ if (input_match->present) {
+ output_match->present = true;
+ if (permit) {
+ output_match->isMatch &= input_match->isMatch;
+ } else {
+ output_match->isMatch |= input_match->isMatch;
+ }
+ }
+}
+
+static void nc_compare_DNSName_to_subtrees(const void *value, void *context) {
+ CFStringRef dnsName = (CFStringRef)value;
+ char *dnsNameString = NULL;
+ nc_san_match_context_t *san_context = context;
+ CFArrayRef subtrees = NULL;
+ if (san_context) {
+ subtrees = san_context->subtrees;
+ }
+ if (subtrees) {
+ CFIndex num_trees = CFArrayGetCount(subtrees);
+ CFRange range = { 0, num_trees };
+ match_t match = { false, false };
+ dnsNameString = CFStringToCString(dnsName);
+ if (!dnsNameString) { return; }
+ const DERItem name = { (unsigned char *)dnsNameString,
+ CFStringGetLength(dnsName) };
+ nc_match_context_t match_context = {GNT_DNSName, &name, &match};
+ CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &match_context);
+ free(dnsNameString);
+
+ update_match(san_context->permit, &match, san_context->match);
+ }
+}
+
+static void nc_compare_IPAddress_to_subtrees(const void *value, void *context) {
+ CFDataRef ipAddr = (CFDataRef)value;
+ nc_san_match_context_t *san_context = context;
+ CFArrayRef subtrees = NULL;
+ if (san_context) {
+ subtrees = san_context->subtrees;
+ }
+ if (subtrees) {
+ CFIndex num_trees = CFArrayGetCount(subtrees);
+ CFRange range = { 0, num_trees };
+ match_t match = { false, false };
+ const DERItem addr = { (unsigned char *)CFDataGetBytePtr(ipAddr), CFDataGetLength(ipAddr) };
+ nc_match_context_t match_context = {GNT_IPAddress, &addr, &match};
+ CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &match_context);
+
+ update_match(san_context->permit, &match, san_context->match);
+ }
}
static void nc_compare_RFC822Name_to_subtrees(const void *value, void *context) {
- CFStringRef rfc822Name = value;
+ CFStringRef rfc822Name = (CFStringRef)value;
+ char *rfc822NameString = NULL;
nc_san_match_context_t *san_context = context;
CFArrayRef subtrees = NULL;
if (san_context) {
CFIndex num_trees = CFArrayGetCount(subtrees);
CFRange range = { 0, num_trees };
match_t match = { false, false };
- const DERItem addr = { (unsigned char *)CFStringGetCStringPtr(rfc822Name, kCFStringEncodingUTF8),
+ rfc822NameString = CFStringToCString(rfc822Name);
+ if (!rfc822NameString) { return; }
+ const DERItem addr = { (unsigned char *)rfc822NameString,
CFStringGetLength(rfc822Name) };
nc_match_context_t match_context = {GNT_RFC822Name, &addr, &match};
CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &match_context);
-
- /*
- * We set the SAN context match struct as follows:
- * 'present' is true if there's any subtree of the same type as any SAN
- * 'match' is false if the present type(s) is/are not supported or the subtree(s) and SAN(s) don't match.
- * Note: the state of 'match' is meaningless without 'present' also being true.
- */
- if (match.present && san_context->match) {
- san_context->match->present = true;
- san_context->match->isMatch &= match.isMatch;
- }
+ free(rfc822NameString);
+
+ update_match(san_context->permit, &match, san_context->match);
+
+ }
+}
+
+static void nc_compare_subject_to_subtrees(SecCertificateRef certificate, CFArrayRef subtrees,
+ bool permit, match_t *match) {
+ CFDataRef subject = SecCertificateCopySubjectSequence(certificate);
+ /* An empty subject name is considered not present */
+ if (!subject || isEmptySubject(subject)) {
+ CFReleaseNull(subject);
+ return;
}
+ /* Compare X.500 distinguished name constraints */
+ CFIndex num_trees = CFArrayGetCount(subtrees);
+ CFRange range = { 0, num_trees };
+ match_t x500_match = { false, false };
+ const DERItem subject_der = { (unsigned char *)CFDataGetBytePtr(subject), CFDataGetLength(subject) };
+ nc_match_context_t context = {GNT_DirectoryName, &subject_der, &x500_match};
+ CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &context);
+ CFReleaseNull(subject);
+ update_match(permit, &x500_match, match);
+
+ /* Compare DNSName constraints */
+ match_t dns_match = { false, permit };
+ CFArrayRef dnsNames = SecCertificateCopyDNSNamesFromSubject(certificate);
+ if (dnsNames) {
+ CFRange dnsRange = { 0, CFArrayGetCount(dnsNames) };
+ nc_san_match_context_t dnsContext = { subtrees, &dns_match, permit };
+ CFArrayApplyFunction(dnsNames, dnsRange, nc_compare_DNSName_to_subtrees, &dnsContext);
+ }
+ CFReleaseNull(dnsNames);
+ update_match(permit, &dns_match, match);
+
+ /* Compare IPAddresss constraints */
+ match_t ip_match = { false, permit };
+ CFArrayRef ipAddresses = SecCertificateCopyIPAddressesFromSubject(certificate);
+ if (ipAddresses) {
+ CFRange ipRange = { 0, CFArrayGetCount(ipAddresses) };
+ nc_san_match_context_t ipContext = { subtrees, &ip_match, permit };
+ CFArrayApplyFunction(ipAddresses, ipRange, nc_compare_IPAddress_to_subtrees, &ipContext);
+ }
+ CFReleaseNull(ipAddresses);
+ update_match(permit, &ip_match, match);
+
+ /* Compare RFC822 constraints to subject email address */
+ match_t email_match = { false, permit };
+ CFArrayRef rfc822Names = SecCertificateCopyRFC822NamesFromSubject(certificate);
+ if (rfc822Names) {
+ CFRange emailRange = { 0, CFArrayGetCount(rfc822Names) };
+ nc_san_match_context_t emailContext = { subtrees, &email_match, permit };
+ CFArrayApplyFunction(rfc822Names, emailRange, nc_compare_RFC822Name_to_subtrees, &emailContext);
+ }
+ CFReleaseNull(rfc822Names);
+ update_match(permit, &email_match, match);
}
static OSStatus nc_compare_subjectAltName_to_subtrees(void *context, SecCEGeneralNameType gnType, const DERItem *generalName) {
match_t match = { false, false };
nc_match_context_t match_context = {gnType, generalName, &match};
CFArrayApplyFunction(subtrees, range, nc_decode_and_compare_subtree, &match_context);
-
- /*
- * We set the SAN context match struct as follows:
- * 'present' is true if there's any subtree of the same type as any SAN
- * 'match' is false if the present type(s) is/are not supported or the subtree(s) and SAN(s) don't match.
- * Note: the state of 'match' is meaningless without 'present' also being true.
- */
- if (match.present && san_context->match) {
- san_context->match->present = true;
- san_context->match->isMatch &= match.isMatch;
- }
+
+ update_match(san_context->permit, &match, san_context->match);
return errSecSuccess;
}
OSStatus SecNameContraintsMatchSubtrees(SecCertificateRef certificate, CFArrayRef subtrees, bool *matched, bool permit) {
CFDataRef subject = NULL;
OSStatus status = errSecSuccess;
- CFArrayRef rfc822Names = NULL;
require_action_quiet(subject = SecCertificateCopySubjectSequence(certificate),
out,
/* Reject certificates with neither Subject Name nor SubjectAltName */
require_action_quiet(!isEmptySubject(subject) || subjectAltNames, out, status = errSecInvalidCertificate);
- /* Verify that the subject name is within any of the subtrees for X.500 distinguished names */
- match_t subject_match = { false, false };
- nc_compare_subject_to_subtrees(subject,subtrees,&subject_match);
-
- match_t san_match = { false, true };
- nc_san_match_context_t san_context = {subtrees, &san_match};
+ /* Verify that the subject name is within all of the subtrees */
+ match_t subject_match = { false, permit };
+ nc_compare_subject_to_subtrees(certificate, subtrees, permit, &subject_match);
+
+ /* permit tells us whether to start with true or false. If we are looking at permitted
+ * subtrees, we are going to "and" the matching results because all present types must match
+ * to permit. For excluded subtrees, we are going to "or" the matching results because
+ * any matching present types causes exclusion. */
+ match_t san_match = { false, permit };
+ nc_san_match_context_t san_context = {subtrees, &san_match, permit};
- /* If there are no subjectAltNames, then determine if there's a matching emailAddress in the Subject */
- if (!subjectAltNames) {
- rfc822Names = SecCertificateCopyRFC822Names(certificate);
- /* If there's also no emailAddress field then subject match is enough. */
- if (rfc822Names) {
- CFRange range = { 0 , CFArrayGetCount(rfc822Names) };
- CFArrayApplyFunction(rfc822Names, range, nc_compare_RFC822Name_to_subtrees, &san_context);
- }
- }
- else {
- /* And verify that each of the alternative names in the subjectAltName extension (critical or non-critical)
- * is within any of the subtrees for that name type. */
+ /* And verify that each of the alternative names in the subjectAltName extension (critical or non-critical)
+ * is within any of the subtrees for that name type. */
+ if (subjectAltNames) {
status = SecCertificateParseGeneralNames(subjectAltNames,
&san_context,
nc_compare_subjectAltName_to_subtrees);
out:
CFReleaseNull(subject);
- CFReleaseNull(rfc822Names);
return status;
}
DC3C7AB91D838C8D00F6A832 /* oids.h in Headers */ = {isa = PBXBuildFile; fileRef = DC1785421D778A7400B50D50 /* oids.h */; settings = {ATTRIBUTES = (Private, ); }; };
DC3C7ABA1D838C9F00F6A832 /* sslTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = DC1786FB1D778F3C00B50D50 /* sslTypes.h */; settings = {ATTRIBUTES = (Private, ); }; };
DC3C7C901D83957F00F6A832 /* NSFileHandle+Formatting.m in Sources */ = {isa = PBXBuildFile; fileRef = E78A9AD91D34959200006B5B /* NSFileHandle+Formatting.m */; };
+ DC3D748C1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = DC3D748A1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h */; };
+ DC3D748D1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = DC3D748A1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h */; };
+ DC3D748E1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = DC3D748B1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m */; };
+ DC3D748F1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = DC3D748B1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m */; };
DC4268F61E82036F002B7110 /* server_endpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6ACC401E81DF9400125DC5 /* server_endpoint.m */; };
DC4268FC1E820370002B7110 /* server_endpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6ACC401E81DF9400125DC5 /* server_endpoint.m */; };
DC4268FE1E820371002B7110 /* server_endpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = DC6ACC401E81DF9400125DC5 /* server_endpoint.m */; };
DCB344A51D8A35270054D16E /* si-20-sectrust-provisioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DCB344701D8A35270054D16E /* si-20-sectrust-provisioning.h */; };
DCB344A61D8A35270054D16E /* si-33-keychain-backup.c in Sources */ = {isa = PBXBuildFile; fileRef = DCB344711D8A35270054D16E /* si-33-keychain-backup.c */; };
DCB344A71D8A35270054D16E /* si-34-one-true-keychain.c in Sources */ = {isa = PBXBuildFile; fileRef = DCB344721D8A35270054D16E /* si-34-one-true-keychain.c */; };
+ DCB502331FDA156B008F8E4F /* AutoreleaseTest.c in Sources */ = {isa = PBXBuildFile; fileRef = DCB5022C1FDA155D008F8E4F /* AutoreleaseTest.c */; };
DCB515DE1ED3CF86001F1152 /* SecurityFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCE4E7C01D7A463E00AFB96E /* SecurityFoundation.framework */; };
DCB515DF1ED3CF95001F1152 /* SecurityFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DCE4E7C01D7A463E00AFB96E /* SecurityFoundation.framework */; };
DCB515E01ED3D111001F1152 /* client.c in Sources */ = {isa = PBXBuildFile; fileRef = 7908507F0CA87CF00083CC4D /* client.c */; };
DC3A4B601D91EAC500E46D4A /* com.apple.CodeSigningHelper.sb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = com.apple.CodeSigningHelper.sb; sourceTree = "<group>"; };
DC3A4B621D91EAC500E46D4A /* main.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = main.cpp; sourceTree = "<group>"; };
DC3A81D41D99D567000C7419 /* libcoretls_cfhelpers.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libcoretls_cfhelpers.dylib; path = usr/lib/libcoretls_cfhelpers.dylib; sourceTree = SDKROOT; };
+ DC3D748A1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CKKSLocalSynchronizeOperation.h; sourceTree = "<group>"; };
+ DC3D748B1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CKKSLocalSynchronizeOperation.m; sourceTree = "<group>"; };
DC4269031E82EDAC002B7110 /* SecItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SecItem.m; sourceTree = "<group>"; };
DC4269061E82FBDF002B7110 /* server_security_helpers.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = server_security_helpers.c; sourceTree = "<group>"; };
DC4269071E82FBDF002B7110 /* server_security_helpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = server_security_helpers.h; sourceTree = "<group>"; };
DCB344701D8A35270054D16E /* si-20-sectrust-provisioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "si-20-sectrust-provisioning.h"; path = "regressions/si-20-sectrust-provisioning.h"; sourceTree = "<group>"; };
DCB344711D8A35270054D16E /* si-33-keychain-backup.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "si-33-keychain-backup.c"; path = "regressions/si-33-keychain-backup.c"; sourceTree = "<group>"; };
DCB344721D8A35270054D16E /* si-34-one-true-keychain.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = "si-34-one-true-keychain.c"; path = "regressions/si-34-one-true-keychain.c"; sourceTree = "<group>"; };
+ DCB5022C1FDA155D008F8E4F /* AutoreleaseTest.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = AutoreleaseTest.c; sourceTree = "<group>"; };
+ DCB502321FDA155E008F8E4F /* AutoreleaseTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoreleaseTest.h; sourceTree = "<group>"; };
DCB5D9391E4A9A3400BE22AB /* CKKSSynchronizeOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CKKSSynchronizeOperation.h; sourceTree = "<group>"; };
DCB5D93A1E4A9A3400BE22AB /* CKKSSynchronizeOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CKKSSynchronizeOperation.m; sourceTree = "<group>"; };
DCBDB3B01E57C67500B61300 /* CKKSKeychainView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CKKSKeychainView.h; sourceTree = "<group>"; };
DC3502B61E0208BE00BC0587 /* Tests (Local) */ = {
isa = PBXGroup;
children = (
+ DCB5022C1FDA155D008F8E4F /* AutoreleaseTest.c */,
+ DCB502321FDA155E008F8E4F /* AutoreleaseTest.h */,
471024D91E79CB6D00844C09 /* CKKSTests.h */,
DC3502B71E0208BE00BC0587 /* CKKSTests.m */,
DC6593D21ED8DBCE00C19462 /* CKKSTests+API.h */,
DCFE1C501F1825F7007640C8 /* CKKSUpdateDeviceStateOperation.m */,
DCAD9B421F8D939C00C5E2AE /* CKKSFixups.h */,
DCAD9B431F8D939C00C5E2AE /* CKKSFixups.m */,
+ DC3D748A1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h */,
+ DC3D748B1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m */,
);
name = Operations;
sourceTree = "<group>";
DCAD9B451F8D939C00C5E2AE /* CKKSFixups.h in Headers */,
DC9C95B51F79CFD1000D19E5 /* CKKSControl.h in Headers */,
DC222C6F1E034D1F00B09171 /* SecKeybagSupport.h in Headers */,
+ DC3D748D1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h in Headers */,
DC222C701E034D1F00B09171 /* iCloudTrace.h in Headers */,
DCEA5D861E2F14810089CF55 /* CKKSAPSReceiver.h in Headers */,
DC222C711E034D1F00B09171 /* CKKSOutgoingQueueEntry.h in Headers */,
DCAD9B441F8D939C00C5E2AE /* CKKSFixups.h in Headers */,
DC9C95B41F79CFD1000D19E5 /* CKKSControl.h in Headers */,
DC52E7EA1D80BE9500B0A59C /* SecItemSchema.h in Headers */,
+ DC3D748C1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h in Headers */,
DC52E7E91D80BE8D00B0A59C /* SecKeybagSupport.h in Headers */,
DCD662F51E329B6800188186 /* CKKSNewTLKOperation.h in Headers */,
DC52E7EB1D80BE9B00B0A59C /* iCloudTrace.h in Headers */,
DC222C431E034D1F00B09171 /* SecItemBackupServer.c in Sources */,
DCE278E01ED789EF0083B485 /* CKKSCurrentItemPointer.m in Sources */,
DC222C441E034D1F00B09171 /* SecItemDataSource.c in Sources */,
+ DC3D748F1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m in Sources */,
526965D31E6E284500627F9D /* AsymKeybagBackup.m in Sources */,
DCFE1C541F1825F7007640C8 /* CKKSUpdateDeviceStateOperation.m in Sources */,
DCD6C4B51EC5302500414FEE /* CKKSNearFutureScheduler.m in Sources */,
4771ECD91F17CE5100840998 /* SFAnalyticsLogger.m in Sources */,
4771ECCE1F17CD2100840998 /* SFObjCType.m in Sources */,
4771ECCC1F17CD0E00840998 /* SFSQLite.m in Sources */,
+ DCB502331FDA156B008F8E4F /* AutoreleaseTest.c in Sources */,
4771ECCD1F17CD0E00840998 /* SFSQLiteStatement.m in Sources */,
DCD6C4B71EC5319600414FEE /* CKKSNearFutureSchedulerTests.m in Sources */,
DC08D1C41E64FA8C006237DA /* CloudKitKeychainSyncingMockXCTest.m in Sources */,
6C588D801EAA20AB00D7E322 /* RateLimiter.m in Sources */,
DC15F7681E67A6F6003B9A40 /* CKKSHealKeyHierarchyOperation.m in Sources */,
DCE278DF1ED789EF0083B485 /* CKKSCurrentItemPointer.m in Sources */,
+ DC3D748E1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m in Sources */,
DCA4D1FF1E552DD50056214F /* CKKSCurrentKeyPointer.m in Sources */,
DCFE1C531F1825F7007640C8 /* CKKSUpdateDeviceStateOperation.m in Sources */,
DCD6C4B41EC5302500414FEE /* CKKSNearFutureScheduler.m in Sources */,
#ifndef CKKS_h
#define CKKS_h
+#include <dispatch/dispatch.h>
#include <ipc/securityd_client.h>
-#include <utilities/SecDb.h>
#include <utilities/SecCFWrappers.h>
-#include <dispatch/dispatch.h>
+#include <utilities/SecDb.h>
#include <xpc/xpc.h>
#ifdef __OBJC__
#import <Foundation/Foundation.h>
+NS_ASSUME_NONNULL_BEGIN
+#else
+CF_ASSUME_NONNULL_BEGIN
+#endif
+
+#ifdef __OBJC__
typedef NS_ENUM(NSUInteger, SecCKKSItemEncryptionVersion) {
CKKSItemEncryptionVersionNone = 0, // No encryption present
extern CKKSItemState* const SecCKKSStateInFlight;
extern CKKSItemState* const SecCKKSStateReencrypt;
extern CKKSItemState* const SecCKKSStateError;
-extern CKKSItemState* const SecCKKSStateDeleted; // meta-state: please delete this item!
+extern CKKSItemState* const SecCKKSStateDeleted; // meta-state: please delete this item!
/* Processed States */
@protocol SecCKKSProcessedState
/* Intermediate Key CKRecord Keys */
extern NSString* const SecCKRecordIntermediateKeyType;
extern NSString* const SecCKRecordKeyClassKey;
-//extern NSString* const SecCKRecordWrappedKeyKey;
-//extern NSString* const SecCKRecordParentKeyRefKey;
+//extern NSString* const SecCKRecordWrappedKeyKey;
+//extern NSString* const SecCKRecordParentKeyRefKey;
/* TLK Share CKRecord Keys */
// These are a bit special; they can't use the record ID as information without parsing.
extern NSString* const SecCKRecordPoisoned;
extern NSString* const SecCKRecordSignature;
extern NSString* const SecCKRecordVersion;
-//extern NSString* const SecCKRecordParentKeyRefKey; // reference to the key contained by this record
-//extern NSString* const SecCKRecordWrappedKeyKey; // key material
+//extern NSString* const SecCKRecordParentKeyRefKey; // reference to the key contained by this record
+//extern NSString* const SecCKRecordWrappedKeyKey; // key material
/* Current Key CKRecord Keys */
extern NSString* const SecCKRecordCurrentKeyType;
// The key class will be the record name.
-//extern NSString* const SecCKRecordParentKeyRefKey; <-- represent the current key for this key class
+//extern NSString* const SecCKRecordParentKeyRefKey; <-- represent the current key for this key class
/* Current Item CKRecord Keys */
extern NSString* const SecCKRecordCurrentItemType;
extern CKKSZoneKeyState* const SecCKKSZoneKeyStateInitialized;
// Everything is ready and waiting for input.
extern CKKSZoneKeyState* const SecCKKSZoneKeyStateReady;
+// We're presumably ready, but we'd like to do one or two more checks after we unlock.
+extern CKKSZoneKeyState* const SecCKKSZoneKeyStateReadyPendingUnlock;
+
// A Fetch has just been completed which includes some new keys to process
extern CKKSZoneKeyState* const SecCKKSZoneKeyStateFetchComplete;
// We'd really like a full refetch.
#define SecCKKSOutgoingQueueItemsAtOnce 100
#define SecCKKSIncomingQueueItemsAtOnce 10
-#endif // OBJ-C
+#endif // OBJ-C
/* C functions to interact with CKKS */
void SecCKKSInitialize(SecDbRef db);
// Register this callback to receive a call when the item with this UUID next successfully (or unsuccessfully) exits the outgoing queue.
void CKKSRegisterSyncStatusCallback(CFStringRef cfuuid, SecBoolCFErrorCallback callback);
+// Tells CKKS that the local keychain was reset, and that it should respond accordingly
+void SecCKKSPerformLocalResync(void);
+
// Returns true if CloudKit keychain syncing should occur
bool SecCKKSIsEnabled(void);
bool SecCKKSEnableEnforceManifests(void);
bool SecCKKSSetEnforceManifests(bool value);
-bool SecCKKSShareTLKs(void);
-bool SecCKKSEnableShareTLKs(void);
-bool SecCKKSSetShareTLKs(bool value);
-
// Testing support
bool SecCKKSTestsEnabled(void);
bool SecCKKSTestsEnable(void);
void SecCKKSTestSetDisableKeyNotifications(bool set);
-XPC_RETURNS_RETAINED xpc_endpoint_t
-SecServerCreateCKKSEndpoint(void);
+XPC_RETURNS_RETAINED _Nullable xpc_endpoint_t SecServerCreateCKKSEndpoint(void);
// TODO: handle errors better
typedef CF_ENUM(CFIndex, CKKSErrorCode) {
//CKKSServerInvalidCurrentItem = 17,
};
-#define SecTranslateError(nserrorptr, cferror) \
- if(nserrorptr) { \
- *nserrorptr = (__bridge_transfer NSError*) cferror; \
- } else { \
- CFReleaseNull(cferror); \
+#define SecTranslateError(nserrorptr, cferror) \
+ if(nserrorptr) { \
+ *nserrorptr = (__bridge_transfer NSError*)cferror; \
+ } else { \
+ CFReleaseNull(cferror); \
}
// Very similar to the secerror, secnotice, and secinfo macros in debugging.h, but add zoneNames
-#define ckkserrorwithzonename(scope, zoneName, format, ...) { os_log(secLogObjForScope("SecError"), scope "-%@: " format, (zoneName ? zoneName : @"unknown"), ## __VA_ARGS__); }
-#define ckksnoticewithzonename(scope, zoneName, format, ...) { os_log(secLogObjForCFScope((__bridge CFStringRef)[@(scope "-") stringByAppendingString: (zoneName ? zoneName : @"unknown")]), format, ## __VA_ARGS__); }
-#define ckksinfowithzonename(scope, zoneName, format, ...) { os_log_debug(secLogObjForCFScope((__bridge CFStringRef)[@(scope "-") stringByAppendingString: (zoneName ? zoneName : @"unknown")]), format, ## __VA_ARGS__); }
-
-#define ckkserror(scope, zoneNameHaver, format, ...) \
-{ NSString* znh = zoneNameHaver.zoneName; \
- ckkserrorwithzonename(scope, znh, format, ## __VA_ARGS__) \
-}
-#define ckksnotice(scope, zoneNameHaver, format, ...) \
-{ NSString* znh = zoneNameHaver.zoneName; \
- ckksnoticewithzonename(scope, znh, format, ## __VA_ARGS__) \
-}
-#define ckksinfo(scope, zoneNameHaver, format, ...) \
-{ NSString* znh = zoneNameHaver.zoneName; \
- ckksinfowithzonename(scope, znh, format, ## __VA_ARGS__) \
-}
+#define ckkserrorwithzonename(scope, zoneName, format, ...) \
+ { \
+ os_log(secLogObjForScope("SecError"), scope "-%@: " format, (zoneName ? zoneName : @"unknown"), ##__VA_ARGS__); \
+ }
+#define ckksnoticewithzonename(scope, zoneName, format, ...) \
+ { \
+ os_log(secLogObjForCFScope((__bridge CFStringRef)[@(scope "-") stringByAppendingString:(zoneName ? zoneName : @"unknown")]), \
+ format, \
+ ##__VA_ARGS__); \
+ }
+#define ckksinfowithzonename(scope, zoneName, format, ...) \
+ { \
+ os_log_debug(secLogObjForCFScope((__bridge CFStringRef)[@(scope "-") stringByAppendingString:(zoneName ? zoneName : @"unknown")]), \
+ format, \
+ ##__VA_ARGS__); \
+ }
+
+#define ckkserror(scope, zoneNameHaver, format, ...) \
+ { \
+ NSString* znh = zoneNameHaver.zoneName; \
+ ckkserrorwithzonename(scope, znh, format, ##__VA_ARGS__) \
+ }
+#define ckksnotice(scope, zoneNameHaver, format, ...) \
+ { \
+ NSString* znh = zoneNameHaver.zoneName; \
+ ckksnoticewithzonename(scope, znh, format, ##__VA_ARGS__) \
+ }
+#define ckksinfo(scope, zoneNameHaver, format, ...) \
+ { \
+ NSString* znh = zoneNameHaver.zoneName; \
+ ckksinfowithzonename(scope, znh, format, ##__VA_ARGS__) \
+ }
#undef ckksdebug
#if !defined(NDEBUG)
-#define ckksdebugwithzonename(scope, zoneName, format, ...) { os_log_debug(secLogObjForCFScope((__bridge CFStringRef)[@(scope "-") stringByAppendingString: (zoneName ? zoneName : @"unknown")]), format, ## __VA_ARGS__); }
-#define ckksdebug(scope, zoneNameHaver, format, ...) \
-{ NSString* znh = zoneNameHaver.zoneName; \
- ckksdebugwithzonename(scope, znh, format, ## __VA_ARGS__) \
-}
+#define ckksdebugwithzonename(scope, zoneName, format, ...) \
+ { \
+ os_log_debug(secLogObjForCFScope((__bridge CFStringRef)[@(scope "-") stringByAppendingString:(zoneName ? zoneName : @"unknown")]), \
+ format, \
+ ##__VA_ARGS__); \
+ }
+#define ckksdebug(scope, zoneNameHaver, format, ...) \
+ { \
+ NSString* znh = zoneNameHaver.zoneName; \
+ ckksdebugwithzonename(scope, znh, format, ##__VA_ARGS__) \
+ }
+#else
+#define ckksdebug(scope, ...) /* nothing */
+#endif
+
+#ifdef __OBJC__
+NS_ASSUME_NONNULL_END
#else
-#define ckksdebug(scope,...) /* nothing */
+CF_ASSUME_NONNULL_END
#endif
#endif /* CKKS_h */
NSString* const SecCKRecordManifestLeafDigestKey = @"digest";
CKKSZoneKeyState* const SecCKKSZoneKeyStateReady = (CKKSZoneKeyState*) @"ready";
+CKKSZoneKeyState* const SecCKKSZoneKeyStateReadyPendingUnlock = (CKKSZoneKeyState*) @"readypendingunlock";
CKKSZoneKeyState* const SecCKKSZoneKeyStateError = (CKKSZoneKeyState*) @"error";
CKKSZoneKeyState* const SecCKKSZoneKeyStateCancelled = (CKKSZoneKeyState*) @"cancelled";
SecCKKSZoneKeyStateHealTLKShares: @12U,
SecCKKSZoneKeyStateHealTLKSharesFailed:@13U,
SecCKKSZoneKeyStateWaitForFixupOperation:@14U,
+ SecCKKSZoneKeyStateReadyPendingUnlock: @15U,
};
});
return map;
return CKKSEnforceManifests;
}
-static bool CKKSShareTLKs = true;
+// Here's a mechanism for CKKS feature flags with default values from NSUserDefaults:
+/*static bool CKKSShareTLKs = true;
bool SecCKKSShareTLKs(void) {
+ return true;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Use the default value as above, or apply the preferences value if it exists
});
return CKKSShareTLKs;
-}
-bool SecCKKSEnableShareTLKs(void) {
- return SecCKKSSetShareTLKs(true);
-}
-bool SecCKKSSetShareTLKs(bool value) {
- // Call this to do the dispatch_once first
- SecCKKSShareTLKs();
-
- CKKSShareTLKs = value;
- return CKKSShareTLKs;
-}
+}*/
// Feature flags to twiddle behavior for tests
static bool CKKSDisableAutomaticUUID = false;
[[CKKSViewManager manager] registerSyncStatusCallback: (__bridge NSString*) cfuuid callback:nscallback];
#endif
}
+
+void SecCKKSPerformLocalResync() {
+#if OCTAGON
+ secnotice("ckks", "Local keychain was reset; performing local resync");
+ [[CKKSViewManager manager] rpcResyncLocal:nil reply:^(NSError *result) {
+ if(result) {
+ secnotice("ckks", "Local keychain reset resync finished with an error: %@", result);
+ } else {
+ secnotice("ckks", "Local keychain reset resync finished successfully");
+ }
+ }];
+#endif
+}
#import <Foundation/Foundation.h>
#if OCTAGON
-#import <CloudKit/CloudKit.h>
#import <ApplePushService/ApplePushService.h>
-#import "keychain/ckks/CloudKitDependencies.h"
+#import <CloudKit/CloudKit.h>
#import "keychain/ckks/CKKSCondition.h"
+#import "keychain/ckks/CloudKitDependencies.h"
+
+NS_ASSUME_NONNULL_BEGIN
@protocol CKKSZoneUpdateReceiver
-- (void)notifyZoneChange: (CKRecordZoneNotification*) notification;
+- (void)notifyZoneChange:(CKRecordZoneNotification* _Nullable)notification;
@end
@interface CKKSAPSReceiver : NSObject <APSConnectionDelegate>
// class dependencies (for injection)
@property (readonly) Class<CKKSAPSConnection> apsConnectionClass;
-
-@property id<CKKSAPSConnection> apsConnection;
+@property (nullable) id<CKKSAPSConnection> apsConnection;
+ (instancetype)receiverForEnvironment:(NSString*)environmentName
namedDelegatePort:(NSString*)namedDelegatePort
apsConnectionClass:(Class<CKKSAPSConnection>)apsConnectionClass;
-- (CKKSCondition*)register:(id<CKKSZoneUpdateReceiver>)receiver forZoneID:(CKRecordZoneID *)zoneID;
+- (CKKSCondition*)registerReceiver:(id<CKKSZoneUpdateReceiver>)receiver forZoneID:(CKRecordZoneID*)zoneID;
// Test support:
- (instancetype)initWithEnvironmentName:(NSString*)environmentName
@end
-#endif // OCTAGON
+NS_ASSUME_NONNULL_END
+#endif // OCTAGON
return self;
}
-- (CKKSCondition*)register:(id<CKKSZoneUpdateReceiver>)receiver forZoneID:(CKRecordZoneID *)zoneID {
+- (CKKSCondition*)registerReceiver:(id<CKKSZoneUpdateReceiver>)receiver forZoneID:(CKRecordZoneID *)zoneID {
CKKSCondition* finished = [[CKKSCondition alloc] init];
__weak __typeof(self) weakSelf = self;
#import <Foundation/Foundation.h>
#if OCTAGON
-#import <CloudKit/CloudKit.h>
#import <CloudKit/CKContainer_Private.h>
-#import "keychain/ckks/CloudKitDependencies.h"
-#import "keychain/ckks/CKKSCondition.h"
+#import <CloudKit/CloudKit.h>
#include <Security/SecureObjectSync/SOSCloudCircle.h>
+#import "keychain/ckks/CKKSCondition.h"
+#import "keychain/ckks/CloudKitDependencies.h"
+
+NS_ASSUME_NONNULL_BEGIN
/*
* Implements a 'debouncer' to store the current CK account and circle state, and receive updates to it.
typedef NS_ENUM(NSInteger, CKKSAccountStatus) {
/* Set at initialization. This means we haven't figured out what the account state is. */
- CKKSAccountStatusUnknown = 0,
+ CKKSAccountStatusUnknown = 0,
/* We have an iCloud account and are in-circle */
- CKKSAccountStatusAvailable = 1,
+ CKKSAccountStatusAvailable = 1,
/* No iCloud account is logged in on this device, or we're out of circle */
- CKKSAccountStatusNoAccount = 3,
+ CKKSAccountStatusNoAccount = 3,
};
@protocol CKKSAccountStateListener
--(void)ckAccountStatusChange: (CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus;
+- (void)ckAccountStatusChange:(CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus;
@end
@interface CKKSCKAccountStateTracker : NSObject
-@property CKKSCondition* finishedInitialCalls;
+@property CKKSCondition* finishedInitialDispatches;
// If you use these, please be aware they could change out from under you at any time
-@property CKAccountInfo* currentCKAccountInfo;
+@property (nullable) CKAccountInfo* currentCKAccountInfo;
@property SOSCCStatus currentCircleStatus;
// Fetched and memoized from CloudKit; we can't afford deadlocks with their callbacks
-@property (copy) NSString* ckdeviceID;
-@property NSError* ckdeviceIDError;
-@property CKKSCondition* ckdeviceIDInitialized;
+@property (nullable, copy) NSString* ckdeviceID;
+@property (nullable) NSError* ckdeviceIDError;
+@property CKKSCondition* ckdeviceIDInitialized;
// Fetched and memoized from the Account when we're in-circle; our threading model is strange
-@property NSString* accountCirclePeerID;
-@property NSError* accountCirclePeerIDError;
+@property (nullable) NSString* accountCirclePeerID;
+@property (nullable) NSError* accountCirclePeerIDError;
@property CKKSCondition* accountCirclePeerIDInitialized;
--(instancetype)init: (CKContainer*) container nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass;
+- (instancetype)init:(CKContainer*)container nsnotificationCenterClass:(Class<CKKSNSNotificationCenter>)nsnotificationCenterClass;
--(CKKSAccountStatus)currentCKAccountStatusAndNotifyOnChange: (id<CKKSAccountStateListener>) listener;
+- (dispatch_semaphore_t)notifyOnAccountStatusChange:(id<CKKSAccountStateListener>)listener;
// Methods useful for testing:
// Call this to simulate a notification (and pause the calling thread until all notifications are delivered)
--(void)notifyCKAccountStatusChangeAndWaitForSignal;
--(void)notifyCircleStatusChangeAndWaitForSignal;
+- (void)notifyCKAccountStatusChangeAndWaitForSignal;
+- (void)notifyCircleStatusChangeAndWaitForSignal;
+
+- (dispatch_group_t _Nullable)checkForAllDeliveries;
-+(SOSCCStatus)getCircleStatus;
-+(void)fetchCirclePeerID:(void (^)(NSString* peerID, NSError* error))callback;
-+(NSString*)stringFromAccountStatus: (CKKSAccountStatus) status;
++ (SOSCCStatus)getCircleStatus;
++ (void)fetchCirclePeerID:(void (^)(NSString* _Nullable peerID, NSError* _Nullable error))callback;
++ (NSString*)stringFromAccountStatus:(CKKSAccountStatus)status;
@end
-#endif // OCTAGON
+NS_ASSUME_NONNULL_END
+#endif // OCTAGON
_firstCKAccountFetch = false;
_firstSOSCircleFetch = false;
- _finishedInitialCalls = [[CKKSCondition alloc] init];
+ _finishedInitialDispatches = [[CKKSCondition alloc] init];
_ckdeviceIDInitialized = [[CKKSCondition alloc] init];
id<CKKSNSNotificationCenter> notificationCenter = [self.nsnotificationCenterClass defaultCenter];
}
[strongSelf notifyCKAccountStatusChange:nil];
[strongSelf notifyCircleChange:nil];
- [strongSelf.finishedInitialCalls fulfill];
+ [strongSelf.finishedInitialDispatches fulfill];
});
}
return self;
return [self descriptionInternal: [super description]];
}
--(CKKSAccountStatus)currentCKAccountStatusAndNotifyOnChange: (id<CKKSAccountStateListener>) listener {
-
- __block CKKSAccountStatus status = CKKSAccountStatusUnknown;
-
- dispatch_sync(self.queue, ^{
- status = self.currentComputedAccountStatus;
+-(dispatch_semaphore_t)notifyOnAccountStatusChange:(id<CKKSAccountStateListener>)listener {
+ // signals when we've successfully delivered the first account status
+ dispatch_semaphore_t finishedSema = dispatch_semaphore_create(0);
+ dispatch_async(self.queue, ^{
bool alreadyRegisteredListener = false;
NSEnumerator *enumerator = [self.changeListeners objectEnumerator];
id<CKKSAccountStateListener> value;
dispatch_queue_t objQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL);
[self.changeListeners setObject: listener forKey: objQueue];
+
+ // If we know the current account status, let this listener know
+ if(self.currentComputedAccountStatus != CKKSAccountStatusUnknown) {
+
+ dispatch_group_t g = dispatch_group_create();
+ if(!g) {
+ secnotice("ckksaccount", "Unable to get dispatch group.");
+ return;
+ }
+
+ [self _onqueueDeliverCurrentState:listener listenerQueue:objQueue oldStatus:CKKSAccountStatusUnknown group:g];
+
+ dispatch_group_notify(g, self.queue, ^{
+ dispatch_semaphore_signal(finishedSema);
+ });
+ } else {
+ dispatch_semaphore_signal(finishedSema);
+ }
+ } else {
+ dispatch_semaphore_signal(finishedSema);
}
});
- return status;
+
+ return finishedSema;
}
- (dispatch_semaphore_t)notifyCKAccountStatusChange:(__unused id)object {
}
}
--(void)_onqueueUpdateAccountState: (CKAccountInfo*) ckAccountInfo circle: (SOSCCStatus) sosccstatus deliveredSemaphore: (dispatch_semaphore_t) finishedSema {
+-(void)_onqueueUpdateAccountState:(CKAccountInfo*)ckAccountInfo circle:(SOSCCStatus)sosccstatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
dispatch_assert_queue(self.queue);
if([self.currentCKAccountInfo isEqual: ckAccountInfo] && self.currentCircleStatus == sosccstatus) {
self.currentCKAccountInfo,
SOSCCGetStatusDescription(self.currentCircleStatus));
+ [self _onqueueDeliverStateChanges:oldComputedStatus deliveredSemaphore:finishedSema];
+}
+
+-(void)_onqueueDeliverStateChanges:(CKKSAccountStatus)oldStatus deliveredSemaphore:(dispatch_semaphore_t)finishedSema {
+ dispatch_assert_queue(self.queue);
+
dispatch_group_t g = dispatch_group_create();
if(!g) {
secnotice("ckksaccount", "Unable to get dispatch group.");
// Queue up the changes for each listener.
while ((dq = [enumerator nextObject])) {
id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
- __weak __typeof(listener) weakListener = listener;
-
- if(listener) {
- dispatch_group_async(g, dq, ^{
- [weakListener ckAccountStatusChange: oldComputedStatus to: self.currentComputedAccountStatus];
- });
- }
+ [self _onqueueDeliverCurrentState:listener listenerQueue:dq oldStatus:oldStatus group:g];
}
dispatch_group_notify(g, self.queue, ^{
});
}
+-(void)_onqueueDeliverCurrentState:(id<CKKSAccountStateListener>)listener listenerQueue:(dispatch_queue_t)listenerQueue oldStatus:(CKKSAccountStatus)oldStatus group:(dispatch_group_t)g {
+ dispatch_assert_queue(self.queue);
+
+ __weak __typeof(listener) weakListener = listener;
+
+ if(listener) {
+ dispatch_group_async(g, listenerQueue, ^{
+ [weakListener ckAccountStatusChange:oldStatus to:self.currentComputedAccountStatus];
+ });
+ }
+}
+
-(void)notifyCKAccountStatusChangeAndWaitForSignal {
dispatch_semaphore_wait([self notifyCKAccountStatusChange: nil], DISPATCH_TIME_FOREVER);
}
dispatch_semaphore_wait([self notifyCircleChange: nil], DISPATCH_TIME_FOREVER);
}
+-(dispatch_group_t)checkForAllDeliveries {
+
+ dispatch_group_t g = dispatch_group_create();
+ if(!g) {
+ secnotice("ckksaccount", "Unable to get dispatch group.");
+ return nil;
+ }
+
+ dispatch_sync(self.queue, ^{
+ NSEnumerator *enumerator = [self.changeListeners keyEnumerator];
+ dispatch_queue_t dq;
+
+ // Queue up the changes for each listener.
+ while ((dq = [enumerator nextObject])) {
+ id<CKKSAccountStateListener> listener = [self.changeListeners objectForKey: dq];
+
+ secinfo("ckksaccountblock", "Starting blocking for listener %@", listener);
+ __weak __typeof(listener) weakListener = listener;
+ dispatch_group_async(g, dq, ^{
+ __strong __typeof(listener) strongListener = weakListener;
+ // Do nothing in particular. It's just important that this block runs.
+ secinfo("ckksaccountblock", "Done blocking for listener %@", strongListener);
+ });
+ }
+ });
+
+ return g;
+}
+
// This is its own function to allow OCMock to swoop in and replace the result during testing.
+(SOSCCStatus)getCircleStatus {
CFErrorRef cferror = NULL;
#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>
+NS_ASSUME_NONNULL_BEGIN
+
/*
* CKKSCondition is for implementing one-shot condition variables,
* based on libdispatch semaphores (which might use condition variables underneath).
@interface CKKSCondition : NSObject
--(instancetype)init;
+- (instancetype)init;
+
+// Fulfilling this condition will also fulfill the chained condition.
+// Fulfilling the chained condition will do nothing to this condition.
+- (instancetype)initToChain:(CKKSCondition* _Nullable)chain;
/* Fulfills the condition. Can only be called once per CKKSCondition. */
--(void)fulfill;
+- (void)fulfill;
/* Wait for the the condition to be fulfilled. The returned result behaves exactly like
libdispatch's dispatch_semaphore_wait. */
--(long)wait:(uint64_t)timeout;
+- (long)wait:(uint64_t)timeout;
@end
+NS_ASSUME_NONNULL_END
@interface CKKSCondition ()
@property dispatch_semaphore_t semaphore;
+@property CKKSCondition* chain;
@end
@implementation CKKSCondition
--(instancetype)init
+-(instancetype)init {
+ return [self initToChain:nil];
+}
+
+-(instancetype)initToChain:(CKKSCondition*)chain
{
if((self = [super init])) {
_semaphore = dispatch_semaphore_create(0);
+ _chain = chain;
}
return self;
}
-(void)fulfill {
dispatch_semaphore_signal(self.semaphore);
+ [self.chain fulfill];
+ self.chain = nil; // break the retain, since that condition is filled
}
-(long)wait:(uint64_t)timeout {
#import <Foundation/Foundation.h>
+NS_ASSUME_NONNULL_BEGIN
+
@interface CKKSControl : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConnection:(NSXPCConnection*)connection;
-- (void)rpcStatus: (NSString*)viewName reply:(void(^)(NSArray<NSDictionary*>* result, NSError* error)) reply;
-- (void)rpcResetLocal: (NSString*)viewName reply:(void(^)(NSError* error))reply;
-- (void)rpcResetCloudKit: (NSString*)viewName reply:(void(^)(NSError* error))reply;
-- (void)rpcResync: (NSString*)viewName reply:(void(^)(NSError* error))reply;
-- (void)rpcFetchAndProcessChanges: (NSString*)viewName reply:(void(^)(NSError* error))reply;
-- (void)rpcFetchAndProcessClassAChanges: (NSString*)viewName reply:(void(^)(NSError* error))reply;
-- (void)rpcPushOutgoingChanges: (NSString*)viewName reply:(void(^)(NSError* error))reply;
+- (void)rpcStatus:(NSString* _Nullable)viewName
+ reply:(void (^)(NSArray<NSDictionary*>* _Nullable result, NSError* _Nullable error))reply;
+- (void)rpcResetLocal:(NSString* _Nullable)viewName reply:(void (^)(NSError* _Nullable error))reply;
+- (void)rpcResetCloudKit:(NSString* _Nullable)viewName reply:(void (^)(NSError* _Nullable error))reply;
+- (void)rpcResync:(NSString* _Nullable)viewName reply:(void (^)(NSError* _Nullable error))reply;
+- (void)rpcFetchAndProcessChanges:(NSString* _Nullable)viewName reply:(void (^)(NSError* _Nullable error))reply;
+- (void)rpcFetchAndProcessClassAChanges:(NSString* _Nullable)viewName reply:(void (^)(NSError* _Nullable error))reply;
+- (void)rpcPushOutgoingChanges:(NSString* _Nullable)viewName reply:(void (^)(NSError* _Nullable error))reply;
- (void)rpcPerformanceCounters: (void(^)(NSDictionary <NSString *,NSNumber *> *,NSError*))reply;
- (void)rpcGetAnalyticsSysdiagnoseWithReply:(void (^)(NSString* sysdiagnose, NSError* error))reply;
- (void)rpcForceUploadAnalyticsWithReply: (void (^)(BOOL success, NSError* error))reply;
// convenience wrapper for rpcStatus:reply:
-- (void)rpcTLKMissing: (NSString*)viewName reply:(void(^)(bool missing))reply;
+- (void)rpcTLKMissing:(NSString* _Nullable)viewName reply:(void (^)(bool missing))reply;
-+ (CKKSControl*)controlObject:(NSError* __autoreleasing *)error;
++ (CKKSControl* _Nullable)controlObject:(NSError* _Nullable __autoreleasing* _Nullable)error;
@end
-#endif // __OBJC__
+NS_ASSUME_NONNULL_END
+#endif // __OBJC__
- (void)rpcResetLocal: (NSString*)viewName reply: (void(^)(NSError* result)) reply;
- (void)rpcResetCloudKit: (NSString*)viewName reply: (void(^)(NSError* result)) reply;
- (void)rpcResync:(NSString*)viewName reply: (void(^)(NSError* result)) reply;
+- (void)rpcResyncLocal:(NSString*)viewName reply:(void(^)(NSError* result))reply;
- (void)rpcStatus:(NSString*)viewName reply: (void(^)(NSArray<NSDictionary*>* result, NSError* error)) reply;
- (void)rpcFetchAndProcessChanges:(NSString*)viewName reply: (void(^)(NSError* result)) reply;
- (void)rpcFetchAndProcessClassAChanges:(NSString*)viewName reply: (void(^)(NSError* result)) reply;
currentItemUUID:(NSString*)currentItemUUID
state:(CKKSProcessedState*)state
zoneID:(CKRecordZoneID*)zoneID
- encodedCKRecord:(NSData*) encodedrecord;
-
-+ (instancetype) fromDatabase:(NSString*)identifier state:(CKKSProcessedState*)state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabase:(NSString*)identifier state:(CKKSProcessedState*)state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-
-+ (NSArray<CKKSCurrentItemPointer*>*)remoteItemPointers: (CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error;
-+ (bool) deleteAll:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *) error;
-+ (NSArray<CKKSCurrentItemPointer*>*)allInZone:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error;
+ encodedCKRecord:(NSData*)encodedrecord;
+
++ (instancetype)fromDatabase:(NSString*)identifier
+ state:(CKKSProcessedState*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabase:(NSString*)identifier
+ state:(CKKSProcessedState*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
+
++ (NSArray<CKKSCurrentItemPointer*>*)remoteItemPointers:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (bool)deleteAll:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (NSArray<CKKSCurrentItemPointer*>*)allInZone:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
@end
- (instancetype)initForClass:(CKKSKeyClass*)keyclass
currentKeyUUID:(NSString*)currentKeyUUID
zoneID:(CKRecordZoneID*)zoneID
- encodedCKRecord: (NSData*) encodedrecord;
+ encodedCKRecord:(NSData*)encodedrecord;
-+ (instancetype) fromDatabase: (CKKSKeyClass*) keyclass zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabase: (CKKSKeyClass*) keyclass zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype)fromDatabase:(CKKSKeyClass*)keyclass zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabase:(CKKSKeyClass*)keyclass zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
-+ (instancetype) forKeyClass: (CKKSKeyClass*) keyclass withKeyUUID: (NSString*) keyUUID zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype)forKeyClass:(CKKSKeyClass*)keyclass
+ withKeyUUID:(NSString*)keyUUID
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
-+ (NSArray<CKKSCurrentKeyPointer*>*)all:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (bool) deleteAll:(CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error;
++ (NSArray<CKKSCurrentKeyPointer*>*)all:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (bool)deleteAll:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
@end
@property NSArray<CKKSTLKShare*>* tlkShares;
--(instancetype)init;
--(instancetype)initForZone:(CKRecordZoneID*)zoneID;
+- (instancetype)init;
+- (instancetype)initForZone:(CKRecordZoneID*)zoneID;
@end
#endif
#if OCTAGON
-#include <utilities/SecDb.h>
#include <securityd/SecDbItem.h>
+#include <utilities/SecDb.h>
#import <CloudKit/CloudKit.h>
#import "keychain/ckks/CKKS.h"
-#import "keychain/ckks/CKKSSQLDatabaseObject.h"
#import "keychain/ckks/CKKSRecordHolder.h"
+#import "keychain/ckks/CKKSSQLDatabaseObject.h"
/*
* This is the backing class for "device state" records: each device in an iCloud account copies
@property NSString* currentClassAUUID;
@property NSString* currentClassCUUID;
-+ (instancetype)fromDatabase:(NSString*)device zoneID:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error;
-+ (instancetype)tryFromDatabase:(NSString*)device zoneID:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error;
-+ (instancetype)tryFromDatabaseFromCKRecordID:(CKRecordID*)recordID error:(NSError * __autoreleasing *)error;
-+ (NSArray<CKKSDeviceStateEntry*>*)allInZone:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error;
++ (instancetype)fromDatabase:(NSString*)device zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabase:(NSString*)device zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabaseFromCKRecordID:(CKRecordID*)recordID error:(NSError* __autoreleasing*)error;
++ (NSArray<CKKSDeviceStateEntry*>*)allInZone:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initForDevice:(NSString*)device
encodedCKRecord:(NSData*)encodedrecord;
@end
-#endif // OCTAGON
-#endif /* CKKSDeviceStateEntry_h */
-
+#endif // OCTAGON
+#endif /* CKKSDeviceStateEntry_h */
#import <Foundation/Foundation.h>
#if OCTAGON
-
@class CKKSKeychainView;
#import "keychain/ckks/CKKSGroupOperation.h"
+NS_ASSUME_NONNULL_BEGIN
+
@interface CKKSFetchAllRecordZoneChangesOperation : CKKSGroupOperation
// Set this to true before starting this operation if you'd like resync behavior:
// Fetching everything currently in CloudKit and comparing to local copy
@property bool resync;
-@property (weak) CKKSKeychainView* ckks;
+@property (nullable, weak) CKKSKeychainView* ckks;
@property CKRecordZoneID* zoneID;
@property NSMutableDictionary<CKRecordID*, CKRecord*>* modifications;
@property NSMutableDictionary<CKRecordID*, NSString*>* deletions;
-@property CKServerChangeToken* serverChangeToken;
+@property (nullable) CKServerChangeToken* serverChangeToken;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
@end
+NS_ASSUME_NONNULL_END
#endif
#import "keychain/ckks/CKKSMirrorEntry.h"
#import "keychain/ckks/CKKSManifest.h"
#import "keychain/ckks/CKKSManifestLeafRecord.h"
+#import "CKKSPowerCollection.h"
#include <securityd/SecItemServer.h>
@interface CKKSFetchAllRecordZoneChangesOperation()
@property CKDatabaseOperation<CKKSFetchRecordZoneChangesOperation>* fetchRecordZoneChangesOperation;
@property CKOperationGroup* ckoperationGroup;
+@property (assign) NSUInteger fetchedItems;
@end
@implementation CKKSFetchAllRecordZoneChangesOperation
}
}
- if (![ckks _onQueueUpdateLatestManifestWithError:&error]) {
+ if (![ckks _onqueueUpdateLatestManifestWithError:&error]) {
self.error = error;
ckkserror("ckksfetch", ckks, "failed to get latest manifest");
}
NSError* error = nil;
if(self.resync) {
ckksnotice("ckksresync", ckks, "Comparing local UUIDs against the CloudKit list");
- NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs: &error] mutableCopy];
+ NSMutableArray<NSString*>* uuids = [[CKKSMirrorEntry allUUIDs:ckks.zoneID error:&error] mutableCopy];
for(NSString* uuid in uuids) {
if([self.modifications objectForKey: [[CKRecordID alloc] initWithRecordName: uuid zoneID: ckks.zoneID]]) {
- ckksdebug("ckksresync", ckks, "UUID %@ is still in CloudKit; carry on.", uuid);
+ ckksnotice("ckksresync", ckks, "UUID %@ is still in CloudKit; carry on.", uuid);
} else {
- CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
+ CKKSMirrorEntry* ckme = [CKKSMirrorEntry tryFromDatabase:uuid zoneID:ckks.zoneID error: &error];
if(error != nil) {
ckkserror("ckksresync", ckks, "Couldn't read an item from the database, but it used to be there: %@ %@", uuid, error);
self.error = error;
continue;
}
+ if(!ckme) {
+ ckkserror("ckksresync", ckks, "Couldn't read ckme(%@) from database; continuing", uuid);
+ continue;
+ }
ckkserror("ckksresync", ckks, "BUG: Local item %@ not found in CloudKit, deleting", uuid);
[ckks _onqueueCKRecordDeleted:ckme.item.storedCKRecord.recordID recordType:ckme.item.storedCKRecord.recordType resync:self.resync];
- (void)groupStart {
__weak __typeof(self) weakSelf = self;
-
CKKSKeychainView* ckks = self.ckks;
if(!ckks) {
ckkserror("ckksresync", ckks, "no CKKS object");
// Add this to the modifications, and remove it from the deletions
[strongSelf.modifications setObject: record forKey: record.recordID];
[strongSelf.deletions removeObjectForKey: record.recordID];
+ strongSelf.fetchedItems++;
};
self.fetchRecordZoneChangesOperation.recordWithIDWasDeletedBlock = ^(CKRecordID *recordID, NSString *recordType) {
// Add to the deletions, and remove any pending modifications
[strongSelf.modifications removeObjectForKey: recordID];
[strongSelf.deletions setObject: recordType forKey: recordID];
+ strongSelf.fetchedItems++;
};
// This class only supports fetching from a single zone, so we can ignore recordZoneID
strongSelf.error = operationError;
}
+ //[CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventFetchAllChanges zone:ckks.zoneName count:strongSelf.fetchedItems];
+
+
// Trigger the fake 'we're done' operation.
[strongSelf runBeforeGroupFinished: recordZoneChangesCompletedOperation];
};
#if OCTAGON
#import <Foundation/Foundation.h>
-#import "keychain/ckks/CKKSResultOperation.h"
#import "keychain/ckks/CKKSGroupOperation.h"
#import "keychain/ckks/CKKSKeychainView.h"
+#import "keychain/ckks/CKKSResultOperation.h"
// Sometimes things go wrong.
// Sometimes you have to clean up after your past self.
CKKSFixupNever,
CKKSFixupRefetchCurrentItemPointers,
CKKSFixupFetchTLKShares,
+ CKKSFixupLocalReload,
};
-#define CKKSCurrentFixupNumber (SecCKKSShareTLKs() ? CKKSFixupFetchTLKShares : CKKSFixupRefetchCurrentItemPointers)
+#define CKKSCurrentFixupNumber (CKKSFixupLocalReload)
@interface CKKSFixups : NSObject
+(CKKSGroupOperation*)fixup:(CKKSFixup)lastfixup for:(CKKSKeychainView*)keychainView;
// Fixup declarations. You probably don't need to look at these
@interface CKKSFixupRefetchAllCurrentItemPointers : CKKSGroupOperation
@property (weak) CKKSKeychainView* ckks;
-- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)keychainView ckoperationGroup:(CKOperationGroup *)ckoperationGroup;
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)keychainView ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
@end
@interface CKKSFixupFetchAllTLKShares : CKKSGroupOperation
@property (weak) CKKSKeychainView* ckks;
-- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)keychainView ckoperationGroup:(CKOperationGroup *)ckoperationGroup;
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)keychainView ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
+@end
+
+@interface CKKSFixupLocalReloadOperation : CKKSGroupOperation
+@property (weak) CKKSKeychainView* ckks;
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)keychainView
+ ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
@end
-#endif // OCTAGON
+#endif // OCTAGON
CKKSGroupOperation* fixups = [[CKKSGroupOperation alloc] init];
fixups.name = @"ckks-fixups";
- CKKSResultOperation* previousOp = nil;
+ CKKSResultOperation* previousOp = keychainView.holdFixupOperation;
if(lastfixup < CKKSFixupRefetchCurrentItemPointers) {
CKKSResultOperation* refetch = [[CKKSFixupRefetchAllCurrentItemPointers alloc] initWithCKKSKeychainView:keychainView
ckoperationGroup:fixupCKGroup];
- [refetch addNullableSuccessDependency:previousOp];
+ [refetch addNullableDependency:previousOp];
[fixups runBeforeGroupFinished:refetch];
previousOp = refetch;
}
- if(SecCKKSShareTLKs() && lastfixup < CKKSFixupFetchTLKShares) {
+ if(lastfixup < CKKSFixupFetchTLKShares) {
CKKSResultOperation* fetchShares = [[CKKSFixupFetchAllTLKShares alloc] initWithCKKSKeychainView:keychainView
ckoperationGroup:fixupCKGroup];
[fetchShares addNullableSuccessDependency:previousOp];
previousOp = fetchShares;
}
+ if(lastfixup < CKKSFixupLocalReload) {
+ CKKSResultOperation* localSync = [[CKKSFixupLocalReloadOperation alloc] initWithCKKSKeychainView:keychainView
+ ckoperationGroup:fixupCKGroup];
+ [localSync addNullableSuccessDependency:previousOp];
+ [fixups runBeforeGroupFinished:localSync];
+ previousOp = localSync;
+ }
+
+ (void)previousOp;
return fixups;
}
@end
}
@end
+#pragma mark - CKKSFixupLocalReloadOperation
+
+@interface CKKSFixupLocalReloadOperation ()
+@property CKOperationGroup* group;
+@end
+
+// In <rdar://problem/35540228> Server Generated CloudKit "Manatee Identity Lost"
+// items could be deleted from the local keychain after CKKS believed they were already synced, and therefore wouldn't resync
+// Perform a local resync operation
+@implementation CKKSFixupLocalReloadOperation
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)keychainView
+ ckoperationGroup:(CKOperationGroup *)ckoperationGroup
+{
+ if((self = [super init])) {
+ _ckks = keychainView;
+ _group = ckoperationGroup;
+ }
+ return self;
+}
+
+- (NSString*)description {
+ return [NSString stringWithFormat:@"<CKKSFixup:LocalReload (%@)>", self.ckks];
+}
+- (void)groupStart {
+ CKKSKeychainView* ckks = self.ckks;
+ __weak __typeof(self) weakSelf = self;
+ if(!ckks) {
+ ckkserror("ckksfixup", ckks, "no CKKS object");
+ self.error = [NSError errorWithDomain:CKKSErrorDomain code:errSecInternalError userInfo:@{NSLocalizedDescriptionKey: @"no CKKS object"}];
+ return;
+ }
+
+ CKKSResultOperation* reload = [[CKKSReloadAllItemsOperation alloc] initWithCKKSKeychainView:ckks];
+ [self runBeforeGroupFinished:reload];
+
+ CKKSResultOperation* cleanup = [CKKSResultOperation named:@"local-reload-cleanup" withBlock:^{
+ __strong __typeof(self) strongSelf = weakSelf;
+ __strong __typeof(self.ckks) strongCKKS = strongSelf.ckks;
+ [strongCKKS dispatchSync:^bool{
+ if(reload.error) {
+ ckkserror("ckksfixup", strongCKKS, "Couldn't perform a reload: %@", reload.error);
+ strongSelf.error = reload.error;
+ return false;
+ }
+
+ ckksnotice("ckksfixup", strongCKKS, "Successfully performed a reload fixup");
+
+ NSError* localerror = nil;
+ CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry fromDatabase:strongCKKS.zoneName error:&localerror];
+ ckse.lastFixup = CKKSFixupLocalReload;
+ [ckse saveToDatabase:&localerror];
+ if(localerror) {
+ ckkserror("ckksfixup", strongCKKS, "Couldn't save CKKSZoneStateEntry(%@): %@", ckse, localerror);
+ } else {
+ ckksnotice("ckksfixup", strongCKKS, "Updated zone fixup state to CKKSFixupLocalReload");
+ }
+ return true;
+ }];
+ }];
+ [cleanup addNullableDependency:reload];
+ [self runBeforeGroupFinished:cleanup];
+}
+@end
#endif // OCTAGON
* @APPLE_LICENSE_HEADER_END@
*/
-
-#ifndef CKKSGroupOperation_h
-#define CKKSGroupOperation_h
+#if OCTAGON
#import <Foundation/Foundation.h>
#include <dispatch/dispatch.h>
#import "keychain/ckks/CKKSResultOperation.h"
-@interface CKKSGroupOperation : CKKSResultOperation {
+@interface CKKSGroupOperation : CKKSResultOperation
+{
BOOL executing;
BOOL finished;
}
// For subclasses: override this to execute at Group operation start time
- (void)groupStart;
-- (void)runBeforeGroupFinished: (NSOperation*) suboperation;
-- (void)dependOnBeforeGroupFinished: (NSOperation*) suboperation;
+- (void)runBeforeGroupFinished:(NSOperation*)suboperation;
+- (void)dependOnBeforeGroupFinished:(NSOperation*)suboperation;
@end
-#endif // CKKSGroupOperation_h
+#endif // OCTAGON
* @APPLE_LICENSE_HEADER_END@
*/
+#if OCTAGON
+
#import "CKKSGroupOperation.h"
#include <utilities/debugging.h>
}
- (void)runBeforeGroupFinished: (NSOperation*) suboperation {
- if([self isCancelled]) {
- // Cancelled operations can't add anything.
- secnotice("ckksgroup", "Not adding operation to cancelled group %@", self);
- return;
- }
// op must wait for this operation to start
[suboperation addDependency: self.startOperation];
}
@end
+
+#endif // OCTAGON
+
@end
-#endif // OCTAGON
-
+#endif // OCTAGON
[ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateError withError:[NSError errorWithDomain: @"securityd" code:0 userInfo:@{NSLocalizedDescriptionKey: @"couldn't create new classA key", NSUnderlyingErrorKey: error}]];
}
+ keyset.classA = newClassAKey;
keyset.currentClassAPointer.currentKeyUUID = newClassAKey.uuid;
changedCurrentClassA = true;
}
[ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateError withError:[NSError errorWithDomain: @"securityd" code:0 userInfo:@{NSLocalizedDescriptionKey: @"couldn't create new classC key", NSUnderlyingErrorKey: error}]];
}
+ keyset.classC = newClassCKey;
keyset.currentClassCPointer.currentKeyUUID = newClassCKey.uuid;
changedCurrentClassC = true;
}
return true;
}
- [keyset.tlk loadKeyMaterialFromKeychain:&error];
+ // Check if CKKS can recover this TLK.
+ [ckks _onqueueWithAccountKeysCheckTLK:keyset.tlk error:&error];
if(error && [ckks.lockStateTracker isLockedError:error]) {
- ckksnotice("ckkskey", ckks, "Failed to load TLK from keychain, keybag is locked. Entering WaitForUnlock: %@", error);
+ ckksnotice("ckkskey", ckks, "Failed to load TLK from keychain, keybag is locked. Entering waitforunlock: %@", error);
[ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForUnlock withError:nil];
return false;
} else if(error) {
- ckkserror("ckksheal", ckks, "No TLK in keychain, triggering move to bad state: %@", error);
+ ckkserror("ckksheal", ckks, "CKKS wasn't sure about TLK, triggering move to bad state: %@", error);
[ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateWaitForTLK withError: nil];
return false;
}
+ if(![self ensureKeyPresent:keyset.tlk]) {
+ return false;
+ }
+
if(![self ensureKeyPresent:keyset.classA]) {
return false;
}
[key loadKeyMaterialFromKeychain:&error];
if(error) {
- ckkserror("ckksheal", ckks, "Couldn't load classC key from keychain. Attempting recovery: %@", error);
+ ckkserror("ckksheal", ckks, "Couldn't load key(%@) from keychain. Attempting recovery: %@", key, error);
error = nil;
[key unwrapViaKeyHierarchy: &error];
if(error) {
- ckkserror("ckksheal", ckks, "Couldn't unwrap class C key using key hierarchy. Keys are broken, quitting: %@", error);
+ ckkserror("ckksheal", ckks, "Couldn't unwrap key(%@) using key hierarchy. Keys are broken, quitting: %@", key, error);
[ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
self.error = error;
return false;
}
[key saveKeyMaterialToKeychain:&error];
if(error) {
- ckkserror("ckksheal", ckks, "Couldn't save class C key to keychain: %@", error);
+ ckkserror("ckksheal", ckks, "Couldn't save key(%@) to keychain: %@", key, error);
[ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: error];
self.error = error;
return false;
@property (weak) CKKSKeychainView* ckks;
- (instancetype)init NS_UNAVAILABLE;
-- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks
- ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
@end
-#endif // OCTAGON
+#endif // OCTAGON
#import "keychain/ckks/CKKSGroupOperation.h"
#import "keychain/ckks/CKKSTLKShare.h"
+#import "CKKSPowerCollection.h"
+
@interface CKKSHealTLKSharesOperation ()
@property NSBlockOperation* cloudkitModifyOperationFinished;
@property CKOperationGroup* ckoperationGroup;
ckksnotice("ckksshare", ckks, "Key set is %@", keyset);
}
+ //[CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventTLKShareProcessing zone:ckks.zoneName];
+
// Okay! Perform the checks.
if(![keyset.tlk loadKeyMaterialFromKeychain:&error] || error) {
// Well, that's no good. We can't share a TLK we don't have.
if([ckks.lockStateTracker isLockedError: error]) {
ckkserror("ckksshare", ckks, "Keychain is locked: can't fix shares yet: %@", error);
- [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForUnlock withError:nil];
+ [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateReadyPendingUnlock withError:nil];
} else {
// TODO go to waitfortlk
ckkserror("ckksshare", ckks, "couldn't load current tlk from keychain: %@", error);
#if OCTAGON
-#import "CKKSSQLDatabaseObject.h"
+#import <CloudKit/CloudKit.h>
+#include <securityd/SecDbItem.h>
+#include <utilities/SecDb.h>
#import "CKKSItem.h"
#import "CKKSMirrorEntry.h"
-#include <utilities/SecDb.h>
-#include <securityd/SecDbItem.h>
-
-#ifndef CKKSIncomingQueueEntry_h
-#define CKKSIncomingQueueEntry_h
-
+#import "CKKSSQLDatabaseObject.h"
-#import <CloudKit/CloudKit.h>
+NS_ASSUME_NONNULL_BEGIN
@interface CKKSIncomingQueueEntry : CKKSSQLDatabaseObject
@property CKKSItem* item;
-@property NSString* uuid; // through-access to underlying item
+@property NSString* uuid; // through-access to underlying item
@property NSString* action;
@property NSString* state;
-- (instancetype) initWithCKKSItem:(CKKSItem*) ckme
- action:(NSString*) action
- state:(NSString*) state;
+- (instancetype)initWithCKKSItem:(CKKSItem*)ckme action:(NSString*)action state:(NSString*)state;
-+ (instancetype) fromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype _Nullable)fromDatabase:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (instancetype _Nullable)tryFromDatabase:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
-+ (NSArray<CKKSIncomingQueueEntry*>*)fetch:(ssize_t)n
- startingAtUUID:(NSString*)uuid
- state:(NSString*)state
- zoneID:(CKRecordZoneID*)zoneID
- error: (NSError * __autoreleasing *) error;
++ (NSArray<CKKSIncomingQueueEntry*>* _Nullable)fetch:(ssize_t)n
+ startingAtUUID:(NSString*)uuid
+ state:(NSString*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
-+ (NSDictionary<NSString*,NSNumber*>*)countsByState:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (NSDictionary<NSString*, NSNumber*>*)countsByState:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
@end
+NS_ASSUME_NONNULL_END
#endif
-#endif /* CKKSIncomingQueueEntry_h */
// should error if it can't process class A items due to the keychain being locked.
@property bool errorOnClassAFailure;
+@property size_t successfulItemsProcessed;
+@property size_t errorItemsProcessed;
+
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks errorOnClassAFailure:(bool)errorOnClassAFailure;
@end
-#endif // OCTAGON
-
+#endif // OCTAGON
#import "CKKSKey.h"
#import "CKKSManifest.h"
#import "CKKSAnalyticsLogger.h"
+#import "CKKSPowerCollection.h"
#import "keychain/ckks/CKKSCurrentItemPointer.h"
#include <securityd/SecItemServer.h>
if ([CKKSManifest shouldSyncManifests]) {
if (![manifest validateCurrentItem:p withError:&error]) {
ckkserror("ckksincoming", ckks, "Unable to validate current item pointer (%@) against manifest (%@)", p, manifest);
- [[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"CKKSManifestCurrentItemPointerValidation" withAttributes:@{CKKSManifestZoneKey : ckks.zoneID.zoneName, CKKSManifestSignerIDKey : manifest.signerID ?: @"no signer", CKKSManifestGenCountKey : @(manifest.generationCount)}];
if ([CKKSManifest shouldEnforceManifests]) {
return false;
}
}
- else {
- [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"CKKSManifestCurrentItemPointerValidation"];
- }
}
p.state = SecCKKSProcessedStateLocal;
NSMutableArray* newOrChangedRecords = [[NSMutableArray alloc] init];
NSMutableArray* deletedRecordIDs = [[NSMutableArray alloc] init];
- NSInteger manifestGenerationCount = manifest.generationCount;
- NSString* manifestSignerID = manifest.signerID ?: @"no signer";
for(id entry in queueEntries) {
if(self.cancelled) {
ckkserror("ckksincoming", ckks, "Couldn't decrypt IQE %@ for some reason: %@", iqe, error);
self.error = error;
}
+ self.errorItemsProcessed += 1;
continue;
}
code:errSecInternalError
userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Item did not have a reasonable class: %@", classStr]}];
ckkserror("ckksincoming", ckks, "Synced item seems wrong: %@", self.error);
+ self.errorItemsProcessed += 1;
continue;
}
ckkserror("ckksincoming", ckks, "Couldn't save errored IQE to database: %@", error);
self.error = error;
}
+ self.errorItemsProcessed += 1;
continue;
}
if([iqe.action isEqualToString: SecCKKSActionAdd] || [iqe.action isEqualToString: SecCKKSActionModify]) {
BOOL requireManifestValidation = [CKKSManifest shouldEnforceManifests];
BOOL manifestValidatesItem = [manifest validateItem:iqe.item withError:&error];
- if (manifestValidatesItem) {
- [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"CKKSManifestValidateItemAdd"];
- }
- else {
- [[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"CKKSManifestValidateItemAdd" withAttributes:@{CKKSManifestZoneKey : ckks.zoneID.zoneName, CKKSManifestSignerIDKey : manifestSignerID, CKKSManifestGenCountKey : @(manifestGenerationCount)}];
- }
-
+
if (!requireManifestValidation || manifestValidatesItem) {
[self _onqueueHandleIQEChange: iqe attributes:attributes class:classP];
[newOrChangedRecords addObject:[iqe.item CKRecordWithZoneID:ckks.zoneID]];
ckkserror("ckksincoming", ckks, "failed to save incoming item back to database in unauthenticated state with error: %@", error);
return false;
}
-
+ self.errorItemsProcessed += 1;
continue;
}
} else if ([iqe.action isEqualToString: SecCKKSActionDelete]) {
BOOL requireManifestValidation = [CKKSManifest shouldEnforceManifests];
BOOL manifestValidatesDelete = ![manifest itemUUIDExistsInManifest:iqe.uuid];
- if (manifestValidatesDelete) {
- [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"CKKSManifestValidateItemDelete"];
- }
- else {
- [[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"CKKSManifestValidateItemDelete" withAttributes:@{CKKSManifestZoneKey : ckks.zoneID.zoneName, CKKSManifestSignerIDKey : manifestSignerID, CKKSManifestGenCountKey : @(manifestGenerationCount)}];
- }
if (!requireManifestValidation || manifestValidatesDelete) {
// if the item does not exist in the latest manifest, we're good to delete it
ckkserror("ckksincoming", ckks, "could not validate incoming item deletion against manifest");
if (![self _onqueueUpdateIQE:iqe withState:SecCKKSStateUnauthenticated error:&error]) {
ckkserror("ckksincoming", ckks, "failed to save incoming item deletion back to database in unauthenticated state with error: %@", error);
+
+ self.errorItemsProcessed += 1;
return false;
}
}
// Iterate through all incoming queue entries a chunk at a time (for peak memory concerns)
NSArray<CKKSIncomingQueueEntry*> * queueEntries = nil;
NSString* lastMaxUUID = nil;
- NSUInteger processedItems = 0;
while(queueEntries == nil || queueEntries.count == SecCKKSIncomingQueueItemsAtOnce) {
if(self.cancelled) {
ckksnotice("ckksincoming", ckks, "CKKSIncomingQueueOperation cancelled, quitting");
break;
}
+ //[CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventOutgoingQueue zone:ckks.zoneName count:[queueEntries count]];
+
if (![self processQueueEntries:queueEntries withManifest:ckks.latestManifest egoManifest:ckks.egoManifest]) {
ckksnotice("ckksincoming", ckks, "processQueueEntries didn't complete successfully");
return false;
}
- processedItems += [queueEntries count];
// Find the highest UUID for the next fetch.
for(CKKSIncomingQueueEntry* iqe in queueEntries) {
}
// Process other queues: CKKSCurrentItemPointers
- ckksnotice("ckksincoming", ckks, "Processed %lu items in incoming queue", processedItems);
+ ckksnotice("ckksincoming", ckks, "Processed %lu items in incoming queue (%lu errors)", self.successfulItemsProcessed, self.errorItemsProcessed);
NSArray<CKKSCurrentItemPointer*>* newCIPs = [CKKSCurrentItemPointer remoteItemPointers:ckks.zoneID error:&error];
if(error || !newCIPs) {
return;
}
+ CKKSAnalyticsLogger* logger = [CKKSAnalyticsLogger logger];
+
if (!strongSelf.error) {
- CKKSAnalyticsLogger* logger = [CKKSAnalyticsLogger logger];
[logger logSuccessForEvent:CKKSEventProcessIncomingQueueClassC inView:ckks];
if (!strongSelf.pendingClassAEntries) {
[logger logSuccessForEvent:CKKSEventProcessIncomingQueueClassA inView:ckks];
}
+ } else {
+ [logger logRecoverableError:strongSelf.error
+ forEvent:CKKSEventProcessIncomingQueueClassA
+ inView:strongSelf.ckks
+ withAttributes:NULL];
}
};
if(error) {
ckkserror("ckksincoming", ckks, "couldn't delete CKKSIncomingQueueEntry: %@", error);
self.error = error;
+ self.errorItemsProcessed += 1;
+ } else {
+ self.successfulItemsProcessed += 1;
}
if(moddate) {
ckkserror("ckksincoming", ckks, "Couldn't save errored IQE to database: %@", error);
self.error = error;
}
+
+ self.errorItemsProcessed += 1;
}
}
if(error) {
ckkserror("ckksincoming", ckks, "couldn't delete CKKSIncomingQueueEntry: %@", error);
self.error = error;
+ self.errorItemsProcessed += 1;
+ } else {
+ self.successfulItemsProcessed += 1;
}
} else {
ckkserror("ckksincoming", ckks, "IQE not correctly processed, but why? %@ %@", error, cferror);
self.error = error;
+ self.errorItemsProcessed += 1;
}
}
#if OCTAGON
+#import <CloudKit/CloudKit.h>
+#include <securityd/SecDbItem.h>
+#include <utilities/SecDb.h>
#import "keychain/ckks/CKKS.h"
-#import "keychain/ckks/CKKSSQLDatabaseObject.h"
#import "keychain/ckks/CKKSRecordHolder.h"
-#include <utilities/SecDb.h>
-#include <securityd/SecDbItem.h>
-
-#ifndef CKKSItem_h
-#define CKKSItem_h
+#import "keychain/ckks/CKKSSQLDatabaseObject.h"
-#import <CloudKit/CloudKit.h>
+NS_ASSUME_NONNULL_BEGIN
@class CKKSWrappedAESSIVKey;
-
// Helper base class that includes UUIDs and key information
-@interface CKKSItem : CKKSCKRecordHolder {
-
-}
+@interface CKKSItem : CKKSCKRecordHolder
@property (copy) NSString* uuid;
@property (copy) NSString* parentKeyUUID;
-@property (copy) NSData* encitem;
+@property (nullable, copy) NSData* encitem;
-@property (getter=base64Item, setter=setBase64Item:) NSString* base64encitem;
+@property (nullable, getter=base64Item, setter=setBase64Item:) NSString* base64encitem;
-@property (copy) CKKSWrappedAESSIVKey* wrappedkey;
+@property (nullable, copy) CKKSWrappedAESSIVKey* wrappedkey;
@property NSUInteger generationCount;
@property enum SecCKKSItemEncryptionVersion encver;
-@property NSNumber* plaintextPCSServiceIdentifier;
-@property NSData* plaintextPCSPublicKey;
-@property NSData* plaintextPCSPublicIdentity;
+@property (nullable) NSNumber* plaintextPCSServiceIdentifier;
+@property (nullable) NSData* plaintextPCSPublicKey;
+@property (nullable) NSData* plaintextPCSPublicIdentity;
-// Used for item encryption and decryption. Attempts to be future-compatible for new CloudKit record fields with an optional olditem field, which may contain a CK record. Any fields in that record that we don't understand will be added to the authenticated data dictionary.
-- (NSDictionary<NSString*, NSData*>*)makeAuthenticatedDataDictionaryUpdatingCKKSItem:(CKKSItem*) olditem encryptionVersion:(SecCKKSItemEncryptionVersion)encversion;
+// Used for item encryption and decryption. Attempts to be future-compatible for new CloudKit record fields with an optional
+// olditem field, which may contain a CK record. Any fields in that record that we don't understand will be added to the authenticated data dictionary.
+- (NSDictionary<NSString*, NSData*>*)makeAuthenticatedDataDictionaryUpdatingCKKSItem:(CKKSItem* _Nullable)olditem
+ encryptionVersion:(SecCKKSItemEncryptionVersion)encversion;
-- (instancetype) initWithCKRecord: (CKRecord*) record;
-- (instancetype) initCopyingCKKSItem: (CKKSItem*) item;
+- (instancetype)initWithCKRecord:(CKRecord*)record;
+- (instancetype)initCopyingCKKSItem:(CKKSItem*)item;
// Use this one if you really don't have any more information
-- (instancetype) initWithUUID: (NSString*) uuid
- parentKeyUUID: (NSString*) parentKeyUUID
- zoneID: (CKRecordZoneID*) zoneID;
+- (instancetype)initWithUUID:(NSString*)uuid parentKeyUUID:(NSString*)parentKeyUUID zoneID:(CKRecordZoneID*)zoneID;
// Use this one if you don't have a CKRecord yet
-- (instancetype) initWithUUID: (NSString*) uuid
- parentKeyUUID: (NSString*) parentKeyUUID
- zoneID: (CKRecordZoneID*) zoneID
- encItem: (NSData*) encitem
- wrappedkey: (CKKSWrappedAESSIVKey*) wrappedkey
- generationCount: (NSUInteger) genCount
- encver: (NSUInteger) encver;
-
-- (instancetype) initWithUUID: (NSString*) uuid
- parentKeyUUID: (NSString*) parentKeyUUID
- zoneID: (CKRecordZoneID*)zoneID
- encodedCKRecord: (NSData*) encodedrecord
- encItem: (NSData*) encitem
- wrappedkey: (CKKSWrappedAESSIVKey*) wrappedkey
- generationCount: (NSUInteger) genCount
- encver: (NSUInteger) encver;
-
-- (instancetype) initWithUUID: (NSString*) uuid
- parentKeyUUID: (NSString*) parentKeyUUID
- zoneID: (CKRecordZoneID*)zoneID
- encodedCKRecord: (NSData*) encodedrecord
- encItem: (NSData*) encitem
- wrappedkey: (CKKSWrappedAESSIVKey*) wrappedkey
- generationCount: (NSUInteger) genCount
- encver: (NSUInteger) encver
-plaintextPCSServiceIdentifier: (NSNumber*) pcsServiceIdentifier
- plaintextPCSPublicKey: (NSData*) pcsPublicKey
- plaintextPCSPublicIdentity: (NSData*) pcsPublicIdentity;
+- (instancetype)initWithUUID:(NSString*)uuid
+ parentKeyUUID:(NSString*)parentKeyUUID
+ zoneID:(CKRecordZoneID*)zoneID
+ encItem:(NSData* _Nullable)encitem
+ wrappedkey:(CKKSWrappedAESSIVKey* _Nullable)wrappedkey
+ generationCount:(NSUInteger)genCount
+ encver:(NSUInteger)encver;
+
+- (instancetype)initWithUUID:(NSString*)uuid
+ parentKeyUUID:(NSString*)parentKeyUUID
+ zoneID:(CKRecordZoneID*)zoneID
+ encodedCKRecord:(NSData* _Nullable)encodedrecord
+ encItem:(NSData* _Nullable)encitem
+ wrappedkey:(CKKSWrappedAESSIVKey* _Nullable)wrappedkey
+ generationCount:(NSUInteger)genCount
+ encver:(NSUInteger)encver;
+
+- (instancetype)initWithUUID:(NSString*)uuid
+ parentKeyUUID:(NSString*)parentKeyUUID
+ zoneID:(CKRecordZoneID*)zoneID
+ encodedCKRecord:(NSData* _Nullable)encodedrecord
+ encItem:(NSData* _Nullable)encitem
+ wrappedkey:(CKKSWrappedAESSIVKey* _Nullable)wrappedkey
+ generationCount:(NSUInteger)genCount
+ encver:(NSUInteger)encver
+ plaintextPCSServiceIdentifier:(NSNumber* _Nullable)pcsServiceIdentifier
+ plaintextPCSPublicKey:(NSData* _Nullable)pcsPublicKey
+ plaintextPCSPublicIdentity:(NSData* _Nullable)pcsPublicIdentity;
// Convenience function: set the upload version for this record to be the current OS version
-+ (void)setOSVersionInRecord: (CKRecord*) record;
++ (void)setOSVersionInRecord:(CKRecord*)record;
@end
@interface CKKSSQLDatabaseObject (CKKSZoneExtras)
-// Convenience function: get all UUIDs of this type
-+ (NSArray<NSString*>*) allUUIDs: (NSError * __autoreleasing *) error;
+// Convenience function: get all UUIDs of this type on this particular zone
++ (NSArray<NSString*>*)allUUIDs:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error;
// Convenience function: get all objects in this particular zone
-+ (NSArray*) all:(CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error;
++ (NSArray*)all:(CKRecordZoneID*)zoneID error:(NSError* _Nullable __autoreleasing* _Nullable)error;
// Convenience function: delete all records of this type with this zoneID
-+ (bool) deleteAll:(CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error;
++ (bool)deleteAll:(CKRecordZoneID*)zoneID error:(NSError* _Nullable __autoreleasing* _Nullable)error;
@end
+NS_ASSUME_NONNULL_END
#endif
-#endif /* CKKSItem_H */
#include <securityd/SecDbItem.h>
#include <securityd/SecItemSchema.h>
+#include <sys/sysctl.h>
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
NSString* platform = "unknown";
#warning No PLATFORM defined; why?
#endif
- NSString* osversion = [[NSProcessInfo processInfo]operatingSystemVersionString];
- // subtly improve osversion (but it's okay if that does nothing)
- NSString* finalversion = [platform stringByAppendingString: [osversion stringByReplacingOccurrencesOfString:@"Version" withString:@""]];
- record[SecCKRecordHostOSVersionKey] = finalversion;
+ NSString* osversion = nil;
+
+ // If we can get the build information from sysctl, use it.
+ char release[256];
+ size_t releasesize = sizeof(release);
+ bool haveSysctlInfo = true;
+ haveSysctlInfo &= (0 == sysctlbyname("kern.osrelease", release, &releasesize, NULL, 0));
+
+ char version[256];
+ size_t versionsize = sizeof(version);
+ haveSysctlInfo &= (0 == sysctlbyname("kern.osversion", version, &versionsize, NULL, 0));
+
+ if(haveSysctlInfo) {
+ // Null-terminate for extra safety
+ release[sizeof(release)-1] = '\0';
+ version[sizeof(version)-1] = '\0';
+ osversion = [NSString stringWithFormat:@"%s (%s)", release, version];
+ }
+
+ if(!osversion) {
+ // Otherwise, use the not-really-supported fallback.
+ osversion = [[NSProcessInfo processInfo] operatingSystemVersionString];
+
+ // subtly improve osversion (but it's okay if that does nothing)
+ osversion = [osversion stringByReplacingOccurrencesOfString:@"Version" withString:@""];
+ }
+
+ record[SecCKRecordHostOSVersionKey] = [NSString stringWithFormat:@"%@ %@", platform, osversion];
}
- (CKRecord*) updateCKRecord: (CKRecord*) record zoneID: (CKRecordZoneID*) zoneID {
@implementation CKKSSQLDatabaseObject (CKKSZoneExtras)
-+ (NSArray<NSString*>*) allUUIDs: (NSError * __autoreleasing *) error {
++ (NSArray<NSString*>*)allUUIDs:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error {
__block NSMutableArray* uuids = [[NSMutableArray alloc] init];
[CKKSSQLDatabaseObject queryDatabaseTable: [self sqlTable]
- where: nil
+ where:@{@"ckzone": CKKSNilToNSNull(zoneID.zoneName)}
columns: @[@"UUID"]
groupBy: nil
orderBy:nil
* @APPLE_LICENSE_HEADER_END@
*/
-#ifndef CKKSItemEncrypter_h
-#define CKKSItemEncrypter_h
+#if OCTAGON
#include <securityd/SecDbItem.h>
-#if OCTAGON
-
@class CKKSItem;
@class CKKSMirrorEntry;
@class CKKSKey;
@class CKKSAESSIVKey;
@class CKRecordZoneID;
-#define CKKS_PADDING_MARK_BYTE 0x80
+NS_ASSUME_NONNULL_BEGIN
-@interface CKKSItemEncrypter : NSObject {
+#define CKKS_PADDING_MARK_BYTE 0x80
-}
+@interface CKKSItemEncrypter : NSObject
-+(CKKSItem*)encryptCKKSItem:(CKKSItem*)baseitem
- dataDictionary:(NSDictionary *)dict
- updatingCKKSItem:(CKKSItem*)olditem
- parentkey:(CKKSKey *)parentkey
- error:(NSError * __autoreleasing *) error;
++ (CKKSItem* _Nullable)encryptCKKSItem:(CKKSItem*)baseitem
+ dataDictionary:(NSDictionary*)dict
+ updatingCKKSItem:(CKKSItem* _Nullable)olditem
+ parentkey:(CKKSKey*)parentkey
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
-+ (NSDictionary*) decryptItemToDictionary: (CKKSItem*) item error: (NSError * __autoreleasing *) error;
++ (NSDictionary* _Nullable)decryptItemToDictionary:(CKKSItem*)item error:(NSError* _Nullable __autoreleasing* _Nullable)error;
-+ (NSData*) encryptDictionary: (NSDictionary*) dict key: (CKKSAESSIVKey*) key authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error;
-+ (NSDictionary*) decryptDictionary: (NSData*) encitem key: (CKKSAESSIVKey*) key authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error;
++ (NSData* _Nullable)encryptDictionary:(NSDictionary*)dict
+ key:(CKKSAESSIVKey*)key
+ authenticatedData:(NSDictionary<NSString*, NSData*>* _Nullable)ad
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
++ (NSDictionary* _Nullable)decryptDictionary:(NSData*)encitem
+ key:(CKKSAESSIVKey*)key
+ authenticatedData:(NSDictionary<NSString*, NSData*>* _Nullable)ad
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
-+ (NSData *)padData:(NSData *)input blockSize:(NSUInteger)blockSize additionalBlock:(BOOL)extra;
-+ (NSData *)removePaddingFromData:(NSData *)input;
++ (NSData*)padData:(NSData*)input blockSize:(NSUInteger)blockSize additionalBlock:(BOOL)extra;
++ (NSData* _Nullable)removePaddingFromData:(NSData*)input;
@end
-#endif // OCTAGON
-
-#endif /* CKKSItemEncrypter_h */
-
+NS_ASSUME_NONNULL_END
+#endif // OCTAGON
#import "keychain/ckks/CKKSItem.h"
#import "keychain/ckks/CKKSSIV.h"
-#import "keychain/ckks/proto/source/CKKSSerializedKey.h"
#import "keychain/ckks/CKKSPeer.h"
+#import "keychain/ckks/proto/source/CKKSSerializedKey.h"
@interface CKKSKey : CKKSItem
@property bool currentkey;
// Fetches and attempts to unwrap this key for use
-+ (instancetype) loadKeyWithUUID: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype)loadKeyWithUUID:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
// Creates new random keys, in the parent's zone
-+ (instancetype) randomKeyWrappedByParent: (CKKSKey*) parentKey error: (NSError * __autoreleasing *) error;
-+ (instancetype) randomKeyWrappedByParent: (CKKSKey*) parentKey keyclass:(CKKSKeyClass*)keyclass error: (NSError * __autoreleasing *) error;
++ (instancetype)randomKeyWrappedByParent:(CKKSKey*)parentKey error:(NSError* __autoreleasing*)error;
++ (instancetype)randomKeyWrappedByParent:(CKKSKey*)parentKey
+ keyclass:(CKKSKeyClass*)keyclass
+ error:(NSError* __autoreleasing*)error;
// Creates a new random key that wraps itself
-+ (instancetype)randomKeyWrappedBySelf: (CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype)randomKeyWrappedBySelf:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
/* Helper functions for persisting key material in the keychain */
-- (bool)saveKeyMaterialToKeychain: (NSError * __autoreleasing *) error;
-- (bool)saveKeyMaterialToKeychain: (bool)stashTLK error:(NSError * __autoreleasing *) error; // call this to not stash a non-syncable TLK, if that's what you want
-
-- (bool)loadKeyMaterialFromKeychain: (NSError * __autoreleasing *) error;
-- (bool)deleteKeyMaterialFromKeychain: (NSError * __autoreleasing *) error;
-+ (NSString*)isItemKeyForKeychainView: (SecDbItemRef) item;
+- (bool)saveKeyMaterialToKeychain:(NSError* __autoreleasing*)error;
+- (bool)saveKeyMaterialToKeychain:(bool)stashTLK
+ error:(NSError* __autoreleasing*)error; // call this to not stash a non-syncable TLK, if that's what you want
+
+- (bool)loadKeyMaterialFromKeychain:(NSError* __autoreleasing*)error;
+- (bool)deleteKeyMaterialFromKeychain:(NSError* __autoreleasing*)error;
++ (NSString*)isItemKeyForKeychainView:(SecDbItemRef)item;
// Class methods to help tests
-+ (bool)saveKeyMaterialToKeychain:(CKKSKey*)key stashTLK:(bool)stashTLK error:(NSError * __autoreleasing *) error;
-+ (NSData*)loadKeyMaterialFromKeychain:(CKKSKey*)key resave:(bool*)resavePtr error:(NSError* __autoreleasing *) error;
++ (NSDictionary*)setKeyMaterialInKeychain:(NSDictionary*)query error:(NSError* __autoreleasing*)error;
++ (NSDictionary*)queryKeyMaterialInKeychain:(NSDictionary*)query error:(NSError* __autoreleasing*)error;
-+ (instancetype)keyFromKeychain: (NSString*) uuid
- parentKeyUUID: (NSString*) parentKeyUUID
- keyclass: (CKKSKeyClass*)keyclass
- state: (CKKSProcessedState*) state
- zoneID: (CKRecordZoneID*) zoneID
- encodedCKRecord: (NSData*) encodedrecord
- currentkey: (NSInteger) currentkey
- error: (NSError * __autoreleasing *) error;
++ (instancetype)keyFromKeychain:(NSString*)uuid
+ parentKeyUUID:(NSString*)parentKeyUUID
+ keyclass:(CKKSKeyClass*)keyclass
+ state:(CKKSProcessedState*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ encodedCKRecord:(NSData*)encodedrecord
+ currentkey:(NSInteger)currentkey
+ error:(NSError* __autoreleasing*)error;
-+ (instancetype) fromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabaseAnyState: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype)fromDatabase:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabase:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabaseAnyState:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
-+ (NSArray<CKKSKey*>*) selfWrappedKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (NSArray<CKKSKey*>*)selfWrappedKeys:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
-+ (instancetype)currentKeyForClass: (CKKSKeyClass*) keyclass zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (NSArray<CKKSKey*>*)currentKeysForClass: (CKKSKeyClass*) keyclass state:(CKKSProcessedState*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype)currentKeyForClass:(CKKSKeyClass*)keyclass zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (NSArray<CKKSKey*>*)currentKeysForClass:(CKKSKeyClass*)keyclass
+ state:(CKKSProcessedState*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
-+ (NSArray<CKKSKey*>*)allKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (NSArray<CKKSKey*>*)remoteKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (NSArray<CKKSKey*>*)localKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (NSArray<CKKSKey*>*)allKeys:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (NSArray<CKKSKey*>*)remoteKeys:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (NSArray<CKKSKey*>*)localKeys:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
-- (bool)saveToDatabaseAsOnlyCurrentKeyForClassAndState: (NSError * __autoreleasing *) error;
+- (bool)saveToDatabaseAsOnlyCurrentKeyForClassAndState:(NSError* __autoreleasing*)error;
- (instancetype)init NS_UNAVAILABLE;
-- (instancetype) initSelfWrappedWithAESKey: (CKKSAESSIVKey*) aeskey
- uuid: (NSString*) uuid
- keyclass: (CKKSKeyClass*)keyclass
- state: (CKKSProcessedState*) state
- zoneID: (CKRecordZoneID*) zoneID
- encodedCKRecord: (NSData*) encodedrecord
- currentkey: (NSInteger) currentkey;
-
-- (instancetype) initWrappedBy: (CKKSKey*) wrappingKey
- AESKey: (CKKSAESSIVKey*) aeskey
- uuid: (NSString*) uuid
- keyclass: (CKKSKeyClass*)keyclass
- state: (CKKSProcessedState*) state
- zoneID: (CKRecordZoneID*) zoneID
- encodedCKRecord: (NSData*) encodedrecord
- currentkey: (NSInteger) currentkey;
-
-- (instancetype) initWithWrappedAESKey: (CKKSWrappedAESSIVKey*) wrappedaeskey
- uuid: (NSString*) uuid
- parentKeyUUID: (NSString*) parentKeyUUID
- keyclass: (CKKSKeyClass*)keyclass
- state: (CKKSProcessedState*) state
- zoneID: (CKRecordZoneID*) zoneID
- encodedCKRecord: (NSData*) encodedrecord
- currentkey: (NSInteger) currentkey;
+- (instancetype)initSelfWrappedWithAESKey:(CKKSAESSIVKey*)aeskey
+ uuid:(NSString*)uuid
+ keyclass:(CKKSKeyClass*)keyclass
+ state:(CKKSProcessedState*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ encodedCKRecord:(NSData*)encodedrecord
+ currentkey:(NSInteger)currentkey;
+
+- (instancetype)initWrappedBy:(CKKSKey*)wrappingKey
+ AESKey:(CKKSAESSIVKey*)aeskey
+ uuid:(NSString*)uuid
+ keyclass:(CKKSKeyClass*)keyclass
+ state:(CKKSProcessedState*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ encodedCKRecord:(NSData*)encodedrecord
+ currentkey:(NSInteger)currentkey;
+
+- (instancetype)initWithWrappedAESKey:(CKKSWrappedAESSIVKey*)wrappedaeskey
+ uuid:(NSString*)uuid
+ parentKeyUUID:(NSString*)parentKeyUUID
+ keyclass:(CKKSKeyClass*)keyclass
+ state:(CKKSProcessedState*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ encodedCKRecord:(NSData*)encodedrecord
+ currentkey:(NSInteger)currentkey;
/* Returns true if we believe this key wraps itself. */
- (bool)wrapsSelf;
- (void)zeroKeys;
-- (CKKSKey*)topKeyInAnyState: (NSError * __autoreleasing *) error;
+- (CKKSKey*)topKeyInAnyState:(NSError* __autoreleasing*)error;
// Attempts checks if the AES key is already loaded, or attempts to load it from the keychain. Returns false if it fails.
-- (CKKSAESSIVKey*)ensureKeyLoaded: (NSError * __autoreleasing *) error;
+- (CKKSAESSIVKey*)ensureKeyLoaded:(NSError* __autoreleasing*)error;
// Attempts to unwrap this key via unwrapping its wrapping keys via the key hierarchy.
-- (CKKSAESSIVKey*)unwrapViaKeyHierarchy: (NSError * __autoreleasing *) error;
+- (CKKSAESSIVKey*)unwrapViaKeyHierarchy:(NSError* __autoreleasing*)error;
// On a self-wrapped key, determine if this AES-SIV key is the self-wrapped key.
// If it is, save the key as this CKKSKey's unwrapped key.
-- (bool)trySelfWrappedKeyCandidate:(CKKSAESSIVKey*)candidate error:(NSError * __autoreleasing *) error;
+- (bool)trySelfWrappedKeyCandidate:(CKKSAESSIVKey*)candidate error:(NSError* __autoreleasing*)error;
-- (CKKSWrappedAESSIVKey*)wrapAESKey: (CKKSAESSIVKey*) keyToWrap error: (NSError * __autoreleasing *) error;
-- (CKKSAESSIVKey*)unwrapAESKey: (CKKSWrappedAESSIVKey*) keyToUnwrap error: (NSError * __autoreleasing *) error;
+- (CKKSWrappedAESSIVKey*)wrapAESKey:(CKKSAESSIVKey*)keyToWrap error:(NSError* __autoreleasing*)error;
+- (CKKSAESSIVKey*)unwrapAESKey:(CKKSWrappedAESSIVKey*)keyToUnwrap error:(NSError* __autoreleasing*)error;
-- (bool)wrapUnder: (CKKSKey*) wrappingKey error: (NSError * __autoreleasing *) error;
-- (bool)unwrapSelfWithAESKey: (CKKSAESSIVKey*) unwrappingKey error: (NSError * __autoreleasing *) error;
+- (bool)wrapUnder:(CKKSKey*)wrappingKey error:(NSError* __autoreleasing*)error;
+- (bool)unwrapSelfWithAESKey:(CKKSAESSIVKey*)unwrappingKey error:(NSError* __autoreleasing*)error;
-- (NSData*)encryptData: (NSData*) plaintext authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error;
-- (NSData*)decryptData: (NSData*) ciphertext authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error;
+- (NSData*)encryptData:(NSData*)plaintext
+ authenticatedData:(NSDictionary<NSString*, NSData*>*)ad
+ error:(NSError* __autoreleasing*)error;
+- (NSData*)decryptData:(NSData*)ciphertext
+ authenticatedData:(NSDictionary<NSString*, NSData*>*)ad
+ error:(NSError* __autoreleasing*)error;
-- (NSData*)serializeAsProtobuf:(NSError* __autoreleasing *)error;
-+ (CKKSKey*)loadFromProtobuf:(NSData*)data error:(NSError* __autoreleasing *)error;
+- (NSData*)serializeAsProtobuf:(NSError* __autoreleasing*)error;
++ (CKKSKey*)loadFromProtobuf:(NSData*)data error:(NSError* __autoreleasing*)error;
-+ (NSDictionary<NSString*,NSNumber*>*)countsByClass:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (NSDictionary<NSString*, NSNumber*>*)countsByClass:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
@end
#endif
}
- (bool)saveKeyMaterialToKeychain: (bool)stashTLK error:(NSError * __autoreleasing *) error {
- return [CKKSKey saveKeyMaterialToKeychain:self stashTLK:stashTLK error:error];
-}
-
-+(bool)saveKeyMaterialToKeychain:(CKKSKey*)key stashTLK:(bool)stashTLK error:(NSError * __autoreleasing *) error {
// Note that we only store the key class, view, UUID, parentKeyUUID, and key material in the keychain
// Any other metadata must be stored elsewhere and filled in at load time.
- if(![key ensureKeyLoaded:error]) {
+ if(![self ensureKeyLoaded:error]) {
// No key material, nothing to save to keychain.
return false;
}
// iOS keychains can't store symmetric keys, so we're reduced to storing this key as a password
- NSData* keydata = [[[NSData alloc] initWithBytes:key.aessivkey->key length:key.aessivkey->size] base64EncodedDataWithOptions:0];
+ NSData* keydata = [[[NSData alloc] initWithBytes:self.aessivkey->key length:self.aessivkey->size] base64EncodedDataWithOptions:0];
NSMutableDictionary* query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrAccessGroup: @"com.apple.security.ckks",
- (id)kSecAttrDescription: key.keyclass,
- (id)kSecAttrServer: key.zoneID.zoneName,
- (id)kSecAttrAccount: key.uuid,
- (id)kSecAttrPath: key.parentKeyUUID,
+ (id)kSecAttrDescription: self.keyclass,
+ (id)kSecAttrServer: self.zoneID.zoneName,
+ (id)kSecAttrAccount: self.uuid,
+ (id)kSecAttrPath: self.parentKeyUUID,
(id)kSecAttrIsInvisible: @YES,
(id)kSecValueData : keydata,
} mutableCopy];
// Only TLKs are synchronizable. Other keyclasses must synchronize via key hierarchy.
- if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
+ if([self.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
// Use PCS-MasterKey view so they'll be initial-synced under SOS.
query[(id)kSecAttrSyncViewHint] = (id)kSecAttrViewHintPCSMasterKey;
query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue;
}
// Class C keys are accessible after first unlock; TLKs and Class A keys are accessible only when unlocked
- if([key.keyclass isEqualToString: SecCKKSKeyClassC]) {
+ if([self.keyclass isEqualToString: SecCKKSKeyClassC]) {
query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleAfterFirstUnlock;
} else {
query[(id)kSecAttrAccessible] = (id)kSecAttrAccessibleWhenUnlocked;
}
- OSStatus status = SecItemAdd((__bridge CFDictionaryRef) query, NULL);
-
- if(status == errSecDuplicateItem) {
- // Sure, okay, fine, we'll update.
- error = nil;
- NSMutableDictionary* update = [@{
- (id)kSecValueData: keydata,
- (id)kSecAttrPath: key.parentKeyUUID,
- } mutableCopy];
- query[(id)kSecValueData] = nil;
- query[(id)kSecAttrPath] = nil;
-
- // Udpate the view-hint, too
- if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
- update[(id)kSecAttrSyncViewHint] = (id)kSecAttrViewHintPCSMasterKey;
- query[(id)kSecAttrSyncViewHint] = nil;
- }
-
- status = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef)update);
- }
+ NSError* localError = nil;
+ [CKKSKey setKeyMaterialInKeychain:query error:&localError];
- if(status && error) {
+ if(localError && error) {
*error = [NSError errorWithDomain:@"securityd"
- code:status
- userInfo:@{NSLocalizedDescriptionKey:
- [NSString stringWithFormat:@"Couldn't save %@ to keychain: %d", self, (int)status]}];
+ code:localError.code
+ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Couldn't save %@ to keychain: %d", self, (int)localError.code],
+ NSUnderlyingErrorKey: localError,
+ }];
}
// TLKs are synchronizable. Stash them nonsyncably nearby.
// Don't report errors here.
- if(stashTLK && [key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
+ if(stashTLK && [self.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrAccessGroup: @"com.apple.security.ckks",
- (id)kSecAttrDescription: [key.keyclass stringByAppendingString: @"-nonsync"],
- (id)kSecAttrServer: key.zoneID.zoneName,
- (id)kSecAttrAccount: key.uuid,
- (id)kSecAttrPath: key.parentKeyUUID,
+ (id)kSecAttrDescription: [self.keyclass stringByAppendingString: @"-nonsync"],
+ (id)kSecAttrServer: self.zoneID.zoneName,
+ (id)kSecAttrAccount: self.uuid,
+ (id)kSecAttrPath: self.parentKeyUUID,
(id)kSecAttrIsInvisible: @YES,
(id)kSecValueData : keydata,
} mutableCopy];
query[(id)kSecAttrSynchronizable] = (id)kCFBooleanFalse;
- OSStatus stashstatus = SecItemAdd((__bridge CFDictionaryRef) query, NULL);
- if(stashstatus != errSecSuccess) {
- if(stashstatus == errSecDuplicateItem) {
- // Sure, okay, fine, we'll update.
- error = nil;
- NSDictionary* update = @{
- (id)kSecValueData: keydata,
- (id)kSecAttrPath: key.parentKeyUUID,
- };
- query[(id)kSecValueData] = nil;
- query[(id)kSecAttrPath] = nil;
-
- stashstatus = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef)update);
- }
+ NSError* stashError = nil;
+ [CKKSKey setKeyMaterialInKeychain:query error:&localError];
- if(stashstatus != errSecSuccess) {
- secerror("ckkskey: Couldn't stash %@ to keychain: %d", self, (int)stashstatus);
- }
+ if(stashError) {
+ secerror("ckkskey: Couldn't stash %@ to keychain: %@", self, stashError);
+ }
+ }
+
+ return localError == nil;
+}
+
++ (NSDictionary*)setKeyMaterialInKeychain:(NSDictionary*)query error:(NSError* __autoreleasing *)error {
+ CFTypeRef result = NULL;
+ OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, &result);
+
+ NSError* localerror = nil;
+
+ // Did SecItemAdd fall over due to an existing item?
+ if(status == errSecDuplicateItem) {
+ // Add every primary key attribute to this find dictionary
+ NSMutableDictionary* findQuery = [[NSMutableDictionary alloc] init];
+ findQuery[(id)kSecClass] = query[(id)kSecClass];
+ findQuery[(id)kSecAttrSynchronizable] = query[(id)kSecAttrSynchronizable];
+ findQuery[(id)kSecAttrSyncViewHint] = query[(id)kSecAttrSyncViewHint];
+ findQuery[(id)kSecAttrAccessGroup] = query[(id)kSecAttrAccessGroup];
+ findQuery[(id)kSecAttrAccount] = query[(id)kSecAttrAccount];
+ findQuery[(id)kSecAttrServer] = query[(id)kSecAttrServer];
+ findQuery[(id)kSecAttrPath] = query[(id)kSecAttrPath];
+
+ NSMutableDictionary* updateQuery = [query mutableCopy];
+ updateQuery[(id)kSecClass] = nil;
+
+ status = SecItemUpdate((__bridge CFDictionaryRef)findQuery, (__bridge CFDictionaryRef)updateQuery);
+
+ if(status) {
+ localerror = [NSError errorWithDomain:@"securityd"
+ code:status
+ description:[NSString stringWithFormat:@"SecItemUpdate: %d", (int)status]];
+ }
+ } else {
+ localerror = [NSError errorWithDomain:@"securityd"
+ code:status
+ description: [NSString stringWithFormat:@"SecItemAdd: %d", (int)status]];
+ }
+
+ if(status) {
+ CFReleaseNull(result);
+
+ if(error) {
+ *error = localerror;
}
+ return false;
}
- return status == 0;
+ NSDictionary* resultDict = CFBridgingRelease(result);
+ return resultDict;
}
-+ (NSData*)loadKeyMaterialFromKeychain:(CKKSKey*)key resave:(bool*)resavePtr error:(NSError* __autoreleasing *)error {
++ (NSDictionary*)queryKeyMaterialInKeychain:(NSDictionary*)query error:(NSError* __autoreleasing *)error {
+ CFTypeRef result = NULL;
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
+
+ if(status) {
+ CFReleaseNull(result);
+
+ if(error) {
+ *error = [NSError errorWithDomain:@"securityd"
+ code:status
+ userInfo:@{NSLocalizedDescriptionKey:
+ [NSString stringWithFormat:@"SecItemCopyMatching: %d", (int)status]}];
+ }
+ return false;
+ }
+
+ NSDictionary* resultDict = CFBridgingRelease(result);
+ return resultDict;
+}
+
++ (NSDictionary*)fetchKeyMaterialItemFromKeychain:(CKKSKey*)key resave:(bool*)resavePtr error:(NSError* __autoreleasing *)error {
NSMutableDictionary* query = [@{
(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrNoLegacy : @YES,
(id)kSecAttrDescription: key.keyclass,
(id)kSecAttrAccount: key.uuid,
(id)kSecAttrServer: key.zoneID.zoneName,
+ (id)kSecAttrPath: key.parentKeyUUID,
(id)kSecReturnAttributes: @YES,
(id)kSecReturnData: @YES,
} mutableCopy];
query[(id)kSecAttrSynchronizable] = (id)kCFBooleanTrue;
}
- CFTypeRef result = NULL;
- OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &result);
+ NSError* localError = nil;
+ NSDictionary* result = [self queryKeyMaterialInKeychain:query error:&localError];
+ NSError* originalError = localError;
- if(status == errSecItemNotFound) {
- CFReleaseNull(result);
+ // If we found the item or errored in some interesting way, return.
+ if(localError == nil) {
+ return result;
+ }
+ if(localError && localError.code != errSecItemNotFound) {
+ if(error) {
+ *error = [NSError errorWithDomain:@"securityd"
+ code:localError.code
+ userInfo:@{NSLocalizedDescriptionKey:
+ [NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", key, (int)localError.code],
+ NSUnderlyingErrorKey: localError,
+ }];
+ }
+ return result;
+ }
+ localError = nil;
+
+ if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
//didn't find a regular tlk? how about a piggy?
- if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
- query = [@{
- (id)kSecClass : (id)kSecClassInternetPassword,
- (id)kSecAttrNoLegacy : @YES,
- (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
- (id)kSecAttrDescription: @"tlk-piggy",
- (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
- (id)kSecAttrAccount: [NSString stringWithFormat: @"%@-piggy", key.uuid],
- (id)kSecAttrServer: key.zoneID.zoneName,
- (id)kSecReturnAttributes: @YES,
- (id)kSecReturnData: @YES,
- (id)kSecMatchLimit: (id)kSecMatchLimitOne,
- } mutableCopy];
- status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &result);
- if(status == errSecSuccess){
- secnotice("ckkskey", "loaded a piggy TLK (%@)", key.uuid);
-
- if(resavePtr) {
- *resavePtr = true;
- }
+ query = [@{
+ (id)kSecClass : (id)kSecClassInternetPassword,
+ (id)kSecAttrNoLegacy : @YES,
+ (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
+ (id)kSecAttrDescription: [key.keyclass stringByAppendingString: @"-piggy"],
+ (id)kSecAttrSynchronizable : (id)kSecAttrSynchronizableAny,
+ (id)kSecAttrAccount: [NSString stringWithFormat: @"%@-piggy", key.uuid],
+ (id)kSecAttrServer: key.zoneID.zoneName,
+ (id)kSecReturnAttributes: @YES,
+ (id)kSecReturnData: @YES,
+ (id)kSecMatchLimit: (id)kSecMatchLimitOne,
+ } mutableCopy];
+
+ result = [self queryKeyMaterialInKeychain:query error:&localError];
+ if(localError == nil) {
+ secnotice("ckkskey", "loaded a piggy TLK (%@)", key.uuid);
+
+ if(resavePtr) {
+ *resavePtr = true;
}
+
+ return result;
+ }
+
+ if(localError && localError.code != errSecItemNotFound) {
+ if(error) {
+ *error = [NSError errorWithDomain:@"securityd"
+ code:localError.code
+ userInfo:@{NSLocalizedDescriptionKey:
+ [NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", key, (int)localError.code],
+ NSUnderlyingErrorKey: localError,
+ }];
+ }
+ return nil;
}
}
- if(status == errSecItemNotFound && [key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
- CFReleaseNull(result);
+
+ localError = nil;
+
+ // Try to load a stashed TLK
+ if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
+ localError = nil;
// Try to look for the non-syncable stashed tlk and resurrect it.
query = [@{
(id)kSecAttrSynchronizable: @NO,
} mutableCopy];
- status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &result);
- if(status == errSecSuccess) {
+ result = [self queryKeyMaterialInKeychain:query error:&localError];
+ if(localError == nil) {
secnotice("ckkskey", "loaded a stashed TLK (%@)", key.uuid);
if(resavePtr) {
*resavePtr = true;
}
- }
- }
- if(status){ //still can't find it!
- if(error) {
- *error = [NSError errorWithDomain:@"securityd"
- code:status
- userInfo:@{NSLocalizedDescriptionKey:
- [NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", self, (int)status]}];
+ return result;
}
- return false;
- }
-
- // Determine if we should fix up any attributes on this item...
- NSDictionary* resultDict = CFBridgingRelease(result);
- // We created some TLKs with no ViewHint. Fix it.
- if([key.keyclass isEqualToString: SecCKKSKeyClassTLK]) {
- NSString* viewHint = resultDict[(id)kSecAttrSyncViewHint];
- if(!viewHint) {
- ckksnotice("ckkskey", key.zoneID, "Fixing up non-viewhinted TLK %@", self);
- query[(id)kSecReturnAttributes] = nil;
- query[(id)kSecReturnData] = nil;
-
- NSDictionary* update = @{(id)kSecAttrSyncViewHint: (id)kSecAttrViewHintPCSMasterKey};
-
- status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)update);
- if(status) {
- // Don't report error upwards; this is an optimization fixup.
- secerror("ckkskey: Couldn't update viewhint on existing TLK %@", self);
+ if(localError && localError.code != errSecItemNotFound) {
+ if(error) {
+ *error = [NSError errorWithDomain:@"securityd"
+ code:localError.code
+ userInfo:@{NSLocalizedDescriptionKey:
+ [NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", key, (int)localError.code],
+ NSUnderlyingErrorKey: localError,
+ }];
}
+ return nil;
}
}
- // Okay, back to the real purpose of this function: extract the CFData currently in the results dictionary
- NSData* b64keymaterial = resultDict[(id)kSecValueData];
- NSData* keymaterial = [[NSData alloc] initWithBase64EncodedData:b64keymaterial options:0];
- return keymaterial;
+ // We didn't early-return. Use whatever error the original fetch produced.
+ if(error && originalError) {
+ *error = [NSError errorWithDomain:@"securityd"
+ code:originalError.code
+ userInfo:@{NSLocalizedDescriptionKey:
+ [NSString stringWithFormat:@"Couldn't load %@ from keychain: %d", key, (int)originalError.code],
+ NSUnderlyingErrorKey: originalError,
+ }];
+ }
+
+ return result;
}
- (bool)loadKeyMaterialFromKeychain: (NSError * __autoreleasing *) error {
bool resave = false;
- NSData* keymaterial = [CKKSKey loadKeyMaterialFromKeychain:self resave:&resave error:error];
+ NSDictionary* result = [CKKSKey fetchKeyMaterialItemFromKeychain:self resave:&resave error:error];
+ if(!result) {
+ return false;
+ }
+
+ NSData* b64keymaterial = result[(id)kSecValueData];
+ NSData* keymaterial = [[NSData alloc] initWithBase64EncodedData:b64keymaterial options:0];
if(!keymaterial) {
return false;
}
return record;
}
+- (bool)matchesCKRecord:(CKRecord*)record {
+ if(![record.recordType isEqual: SecCKRecordIntermediateKeyType]) {
+ return false;
+ }
+
+ if(![record.recordID.recordName isEqualToString: self.uuid]) {
+ secinfo("ckkskey", "UUID does not match");
+ return false;
+ }
+
+ // For the parent key ref, ensure that if it's nil, we wrap ourself
+ if(record[SecCKRecordParentKeyRefKey] == nil) {
+ if(![self wrapsSelf]) {
+ secinfo("ckkskey", "wrapping key reference (self-wrapped) does not match");
+ return false;
+ }
+
+ } else {
+ if(![[[record[SecCKRecordParentKeyRefKey] recordID] recordName] isEqualToString: self.parentKeyUUID]) {
+ secinfo("ckkskey", "wrapping key reference (non-self-wrapped) does not match");
+ return false;
+ }
+ }
+
+ if(![record[SecCKRecordKeyClassKey] isEqual: self.keyclass]) {
+ secinfo("ckkskey", "key class does not match");
+ return false;
+ }
+
+ if(![record[SecCKRecordWrappedKeyKey] isEqual: [self.wrappedkey base64WrappedKey]]) {
+ secinfo("ckkskey", "wrapped key does not match");
+ return false;
+ }
+
+ return true;
+}
+
+
#pragma mark - Utility
- (NSString*)description {
* @APPLE_LICENSE_HEADER_END@
*/
-
-#ifndef CKKSKeychainView_h
-#define CKKSKeychainView_h
-
+#if OCTAGON
#import <Foundation/Foundation.h>
#include <dispatch/dispatch.h>
-#if OCTAGON
-#import "keychain/ckks/CloudKitDependencies.h"
#import "keychain/ckks/CKKSAPSReceiver.h"
#import "keychain/ckks/CKKSLockStateTracker.h"
-#endif
+#import "keychain/ckks/CloudKitDependencies.h"
-#include <utilities/SecDb.h>
#include <securityd/SecDbItem.h>
+#include <utilities/SecDb.h>
#import "keychain/ckks/CKKS.h"
+#import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
+#import "keychain/ckks/CKKSGroupOperation.h"
#import "keychain/ckks/CKKSIncomingQueueOperation.h"
-#import "keychain/ckks/CKKSOutgoingQueueOperation.h"
#import "keychain/ckks/CKKSNearFutureScheduler.h"
#import "keychain/ckks/CKKSNewTLKOperation.h"
+#import "keychain/ckks/CKKSNotifier.h"
+#import "keychain/ckks/CKKSOutgoingQueueOperation.h"
+#import "keychain/ckks/CKKSPeer.h"
#import "keychain/ckks/CKKSProcessReceivedKeysOperation.h"
#import "keychain/ckks/CKKSReencryptOutgoingItemsOperation.h"
-#import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
#import "keychain/ckks/CKKSScanLocalItemsOperation.h"
+#import "keychain/ckks/CKKSTLKShare.h"
#import "keychain/ckks/CKKSUpdateDeviceStateOperation.h"
-#import "keychain/ckks/CKKSGroupOperation.h"
#import "keychain/ckks/CKKSZone.h"
#import "keychain/ckks/CKKSZoneChangeFetcher.h"
-#import "keychain/ckks/CKKSNotifier.h"
-#import "keychain/ckks/CKKSPeer.h"
-#import "keychain/ckks/CKKSTLKShare.h"
+#import "keychain/ckks/CKKSSynchronizeOperation.h"
+#import "keychain/ckks/CKKSLocalSynchronizeOperation.h"
#include "CKKS.h"
-# if !OCTAGON
-@interface CKKSKeychainView : NSObject {
- NSString* _containerName;
-}
-@end
-#else // OCTAGON
+NS_ASSUME_NONNULL_BEGIN
@class CKKSKey;
@class CKKSAESSIVKey;
@class CKKSOutgoingQueueEntry;
@class CKKSZoneChangeFetcher;
-@interface CKKSKeychainView : CKKSZone <CKKSZoneUpdateReceiver,
- CKKSChangeFetcherErrorOracle,
- CKKSPeerUpdateListener> {
+@interface CKKSKeychainView : CKKSZone <CKKSZoneUpdateReceiver, CKKSChangeFetcherErrorOracle, CKKSPeerUpdateListener>
+{
CKKSZoneKeyState* _keyHierarchyState;
}
+@property CKKSCondition* loggedIn;
+@property CKKSCondition* loggedOut;
+
@property CKKSLockStateTracker* lockStateTracker;
@property CKKSZoneKeyState* keyHierarchyState;
-@property NSError* keyHierarchyError;
-@property CKOperationGroup* keyHierarchyOperationGroup;
-@property NSOperation* keyStateMachineOperation;
+@property (nullable) NSError* keyHierarchyError;
+@property (nullable) CKOperationGroup* keyHierarchyOperationGroup;
+@property (nullable) NSOperation* keyStateMachineOperation;
// If the key hierarchy isn't coming together, it might be because we're out of sync with cloudkit.
// Use this to track if we've completed a full refetch, so fix-up operations can be done.
@property bool keyStateMachineRefetched;
-@property CKKSEgoManifest* egoManifest;
-@property CKKSManifest* latestManifest;
-@property CKKSResultOperation* keyStateReadyDependency;
+@property (nullable) CKKSEgoManifest* egoManifest;
+@property (nullable) CKKSManifest* latestManifest;
+@property (nullable) CKKSResultOperation* keyStateReadyDependency;
-@property (readonly) NSString *lastActiveTLKUUID;
+// True if we believe there's any items in the keychain which haven't been brought up in CKKS yet
+@property bool droppedItems;
+
+@property (readonly) NSString* lastActiveTLKUUID;
// Full of condition variables, if you'd like to try to wait until the key hierarchy is in some state
@property NSMutableDictionary<CKKSZoneKeyState*, CKKSCondition*>* keyHierarchyConditions;
@property (weak) CKKSNearFutureScheduler* savedTLKNotifier;
// Differs from the zonesetupoperation: zoneSetup is only for CK modifications, viewSetup handles local db changes too
-@property CKKSGroupOperation* viewSetupOperation;
+@property CKKSResultOperation* viewSetupOperation;
/* Used for debugging: just what happened last time we ran this? */
-@property CKKSIncomingQueueOperation* lastIncomingQueueOperation;
-@property CKKSNewTLKOperation* lastNewTLKOperation;
-@property CKKSOutgoingQueueOperation* lastOutgoingQueueOperation;
-@property CKKSProcessReceivedKeysOperation* lastProcessReceivedKeysOperation;
+@property CKKSIncomingQueueOperation* lastIncomingQueueOperation;
+@property CKKSNewTLKOperation* lastNewTLKOperation;
+@property CKKSOutgoingQueueOperation* lastOutgoingQueueOperation;
+@property CKKSProcessReceivedKeysOperation* lastProcessReceivedKeysOperation;
@property CKKSFetchAllRecordZoneChangesOperation* lastRecordZoneChangesOperation;
-@property CKKSReencryptOutgoingItemsOperation* lastReencryptOutgoingItemsOperation;
-@property CKKSScanLocalItemsOperation* lastScanLocalItemsOperation;
-@property CKKSSynchronizeOperation* lastSynchronizeOperation;
-@property CKKSResultOperation* lastFixupOperation;
+@property CKKSReencryptOutgoingItemsOperation* lastReencryptOutgoingItemsOperation;
+@property CKKSScanLocalItemsOperation* lastScanLocalItemsOperation;
+@property CKKSSynchronizeOperation* lastSynchronizeOperation;
+@property CKKSResultOperation* lastFixupOperation;
/* Used for testing: pause operation types by adding operations here */
@property NSOperation* holdReencryptOutgoingItemsOperation;
@property NSOperation* holdOutgoingQueueOperation;
+@property NSOperation* holdLocalSynchronizeOperation;
+@property CKKSResultOperation* holdFixupOperation;
/* Trigger this to tell the whole machine that this view has changed */
@property CKKSNearFutureScheduler* notifyViewChangedScheduler;
@property (nonatomic, readonly) NSSet<id<CKKSPeer>>* currentTrustedPeers;
@property (nonatomic, readonly) NSError* currentTrustedPeersError;
-- (instancetype)initWithContainer: (CKContainer*) container
- zoneName: (NSString*) zoneName
- accountTracker:(CKKSCKAccountStateTracker*) accountTracker
- lockStateTracker:(CKKSLockStateTracker*) lockStateTracker
- savedTLKNotifier:(CKKSNearFutureScheduler*) savedTLKNotifier
- peerProvider:(id<CKKSPeerProvider>)peerProvider
- fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
- fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
- queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
- modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
- modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
- apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
- notifierClass: (Class<CKKSNotifier>) notifierClass;
+- (instancetype)initWithContainer:(CKContainer*)container
+ zoneName:(NSString*)zoneName
+ accountTracker:(CKKSCKAccountStateTracker*)accountTracker
+ lockStateTracker:(CKKSLockStateTracker*)lockStateTracker
+ savedTLKNotifier:(CKKSNearFutureScheduler*)savedTLKNotifier
+ peerProvider:(id<CKKSPeerProvider>)peerProvider
+ fetchRecordZoneChangesOperationClass:(Class<CKKSFetchRecordZoneChangesOperation>)fetchRecordZoneChangesOperationClass
+ fetchRecordsOperationClass:(Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
+ queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
+ modifySubscriptionsOperationClass:(Class<CKKSModifySubscriptionsOperation>)modifySubscriptionsOperationClass
+ modifyRecordZonesOperationClass:(Class<CKKSModifyRecordZonesOperation>)modifyRecordZonesOperationClass
+ apsConnectionClass:(Class<CKKSAPSConnection>)apsConnectionClass
+ notifierClass:(Class<CKKSNotifier>)notifierClass;
/* Synchronous operations */
-- (void) handleKeychainEventDbConnection:(SecDbConnectionRef) dbconn
- added:(SecDbItemRef) added
- deleted:(SecDbItemRef) deleted
- rateLimiter:(CKKSRateLimiter*) rateLimiter
- syncCallback:(SecBoolNSErrorCallback) syncCallback;
+- (void)handleKeychainEventDbConnection:(SecDbConnectionRef)dbconn
+ added:(SecDbItemRef _Nullable)added
+ deleted:(SecDbItemRef _Nullable)deleted
+ rateLimiter:(CKKSRateLimiter*)rateLimiter
+ syncCallback:(SecBoolNSErrorCallback)syncCallback;
--(void)setCurrentItemForAccessGroup:(SecDbItemRef)newItem
- hash:(NSData*)newItemSHA1
- accessGroup:(NSString*)accessGroup
- identifier:(NSString*)identifier
- replacing:(SecDbItemRef)oldItem
- hash:(NSData*)oldItemSHA1
- complete:(void (^) (NSError* operror)) complete;
+- (void)setCurrentItemForAccessGroup:(SecDbItemRef)newItem
+ hash:(NSData*)newItemSHA1
+ accessGroup:(NSString*)accessGroup
+ identifier:(NSString*)identifier
+ replacing:(SecDbItemRef _Nullable)oldItem
+ hash:(NSData* _Nullable)oldItemSHA1
+ complete:(void (^)(NSError* operror))complete;
--(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
- identifier:(NSString*)identifier
- fetchCloudValue:(bool)fetchCloudValue
- complete:(void (^) (NSString* uuid, NSError* operror)) complete;
+- (void)getCurrentItemForAccessGroup:(NSString*)accessGroup
+ identifier:(NSString*)identifier
+ fetchCloudValue:(bool)fetchCloudValue
+ complete:(void (^)(NSString* uuid, NSError* operror))complete;
-- (bool) outgoingQueueEmpty: (NSError * __autoreleasing *) error;
+- (bool)outgoingQueueEmpty:(NSError* __autoreleasing*)error;
- (CKKSResultOperation*)waitForFetchAndIncomingQueueProcessing;
-- (void) waitForKeyHierarchyReadiness;
-- (void) cancelAllOperations;
+- (void)waitForKeyHierarchyReadiness;
+- (void)cancelAllOperations;
-- (CKKSKey*) keyForItem: (SecDbItemRef) item error: (NSError * __autoreleasing *) error;
+- (CKKSKey* _Nullable)keyForItem:(SecDbItemRef)item error:(NSError* __autoreleasing*)error;
-- (bool)_onqueueWithAccountKeysCheckTLK: (CKKSKey*) proposedTLK error: (NSError * __autoreleasing *) error;
+- (bool)_onqueueWithAccountKeysCheckTLK:(CKKSKey*)proposedTLK error:(NSError* __autoreleasing*)error;
/* Asynchronous kickoffs */
-- (void) initializeZone;
-
-- (CKKSOutgoingQueueOperation*)processOutgoingQueue:(CKOperationGroup*)ckoperationGroup;
-- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation*)after ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
+- (CKKSOutgoingQueueOperation*)processOutgoingQueue:(CKOperationGroup* _Nullable)ckoperationGroup;
+- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation* _Nullable)after
+ ckoperationGroup:(CKOperationGroup* _Nullable)ckoperationGroup;
-- (CKKSIncomingQueueOperation*) processIncomingQueue:(bool)failOnClassA;
-- (CKKSIncomingQueueOperation*) processIncomingQueue:(bool)failOnClassA after: (CKKSResultOperation*) after;
+- (CKKSIncomingQueueOperation*)processIncomingQueue:(bool)failOnClassA;
+- (CKKSIncomingQueueOperation*)processIncomingQueue:(bool)failOnClassA after:(CKKSResultOperation* _Nullable)after;
// Schedules a process queueoperation to happen after the next device unlock. This may be Immediately, if the device is unlocked.
- (void)processIncomingQueueAfterNextUnlock;
// If rateLimit is true, the operation will abort if it's updated the record in the past 3 days
- (CKKSUpdateDeviceStateOperation*)updateDeviceState:(bool)rateLimit
waitForKeyHierarchyInitialization:(uint64_t)timeout
- ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
+ ckoperationGroup:(CKOperationGroup* _Nullable)ckoperationGroup;
-- (CKKSSynchronizeOperation*) resyncWithCloud;
+- (CKKSSynchronizeOperation*)resyncWithCloud;
+- (CKKSLocalSynchronizeOperation*)resyncLocal;
- (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because;
// For our serial queue to work with how handleKeychainEventDbConnection is called from the main thread,
// every block on our queue must have a SecDBConnectionRef available to it before it begins on the queue.
// Use these helper methods to make sure those exist.
-- (void) dispatchAsync: (bool (^)(void)) block;
-- (void) dispatchSync: (bool (^)(void)) block;
+- (void)dispatchAsync:(bool (^)(void))block;
+- (void)dispatchSync:(bool (^)(void))block;
- (void)dispatchSyncWithAccountKeys:(bool (^)(void))block;
/* Synchronous operations which must be called from inside a dispatchAsync or dispatchSync block */
- (void)_onqueueKeyStateMachineRequestProcess;
// Call this from a key hierarchy operation to move the state machine, and record the results of the last move.
-- (void)_onqueueAdvanceKeyStateMachineToState: (CKKSZoneKeyState*) state withError: (NSError*) error;
+- (void)_onqueueAdvanceKeyStateMachineToState:(CKKSZoneKeyState* _Nullable)state withError:(NSError* _Nullable)error;
// Since we might have people interested in the state transitions of objects, please do those transitions via these methods
-- (bool)_onqueueChangeOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe toState: (NSString*) state error: (NSError* __autoreleasing*) error;
-- (bool)_onqueueErrorOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe itemError: (NSError*) itemError error: (NSError* __autoreleasing*) error;
+- (bool)_onqueueChangeOutgoingQueueEntry:(CKKSOutgoingQueueEntry*)oqe
+ toState:(NSString*)state
+ error:(NSError* __autoreleasing*)error;
+- (bool)_onqueueErrorOutgoingQueueEntry:(CKKSOutgoingQueueEntry*)oqe
+ itemError:(NSError*)itemError
+ error:(NSError* __autoreleasing*)error;
// Call this if you've done a write and received an error. It'll pull out any new records returned as CKErrorServerRecordChanged and pretend we received them in a fetch
//
// Note that you need to tell this function the records you wanted to save, so it can determine which record failed from its CKRecordID.
// I don't know why CKRecordIDs don't have record types, either.
-- (bool)_onqueueCKWriteFailed:(NSError*)ckerror attemptedRecordsChanged:(NSDictionary<CKRecordID*,CKRecord*>*)savedRecords;
+- (bool)_onqueueCKWriteFailed:(NSError*)ckerror attemptedRecordsChanged:(NSDictionary<CKRecordID*, CKRecord*>*)savedRecords;
-- (bool) _onqueueCKRecordChanged:(CKRecord*)record resync:(bool)resync;
-- (bool) _onqueueCKRecordDeleted:(CKRecordID*)recordID recordType:(NSString*)recordType resync:(bool)resync;
+- (bool)_onqueueCKRecordChanged:(CKRecord*)record resync:(bool)resync;
+- (bool)_onqueueCKRecordDeleted:(CKRecordID*)recordID recordType:(NSString*)recordType resync:(bool)resync;
// For this key, who doesn't yet have a CKKSTLKShare for it?
// Note that we really want a record sharing the TLK to ourselves, so this function might return
// a non-empty set even if all peers have the TLK: it wants us to make a record for ourself.
-- (NSSet<id<CKKSPeer>>*)_onqueueFindPeersMissingShare:(CKKSKey*)key error:(NSError* __autoreleasing*)error;
+- (NSSet<id<CKKSPeer>>* _Nullable)_onqueueFindPeersMissingShare:(CKKSKey*)key error:(NSError* __autoreleasing*)error;
// For this key, share it to all trusted peers who don't have it yet
-- (NSSet<CKKSTLKShare*>*)_onqueueCreateMissingKeyShares:(CKKSKey*)key error:(NSError* __autoreleasing*)error;
+- (NSSet<CKKSTLKShare*>* _Nullable)_onqueueCreateMissingKeyShares:(CKKSKey*)key error:(NSError* __autoreleasing*)error;
-- (bool)_onQueueUpdateLatestManifestWithError:(NSError**)error;
+- (bool)_onqueueUpdateLatestManifestWithError:(NSError**)error;
-- (CKKSDeviceStateEntry*)_onqueueCurrentDeviceStateEntry: (NSError* __autoreleasing*)error;
+- (CKKSDeviceStateEntry* _Nullable)_onqueueCurrentDeviceStateEntry:(NSError* __autoreleasing*)error;
// Called by the CKKSZoneChangeFetcher
-- (bool) isFatalCKFetchError: (NSError*) error;
+- (bool)isFatalCKFetchError:(NSError*)error;
// Please don't use these unless you're an Operation in this package
@property NSHashTable<CKKSIncomingQueueOperation*>* incomingQueueOperations;
@property CKKSScanLocalItemsOperation* initialScanOperation;
// Returns the current state of this view
--(NSDictionary<NSString*, NSString*>*)status;
+- (NSDictionary<NSString*, NSString*>*)status;
@end
-#endif // OCTAGON
-
-
-#define SecTranslateError(nserrorptr, cferror) \
- if(nserrorptr) { \
- *nserrorptr = (__bridge_transfer NSError*) cferror; \
- } else { \
- CFReleaseNull(cferror); \
- }
-#endif /* CKKSKeychainView_h */
+NS_ASSUME_NONNULL_END
+#else // !OCTAGON
+#import <Foundation/Foundation.h>
+@interface CKKSKeychainView : NSObject
+{
+ NSString* _containerName;
+}
+@end
+#endif // OCTAGON
#import "CKKSManifest.h"
#import "CKKSManifestLeafRecord.h"
#import "CKKSZoneChangeFetcher.h"
-#import "CKKSAnalyticsLogger.h"
+#import "CKKSAnalyticsLogger.h"
#import "keychain/ckks/CKKSDeviceStateEntry.h"
#import "keychain/ckks/CKKSNearFutureScheduler.h"
#import "keychain/ckks/CKKSCurrentItemPointer.h"
#import "keychain/ckks/CloudKitCategories.h"
#import "keychain/ckks/CKKSTLKShare.h"
#import "keychain/ckks/CKKSHealTLKSharesOperation.h"
+#import "keychain/ckks/CKKSLocalSynchronizeOperation.h"
#include <utilities/SecCFWrappers.h>
#include <utilities/SecDb.h>
@property CKKSNearFutureScheduler* initializeScheduler;
+// Slows down all outgoing queue operations
+@property CKKSNearFutureScheduler* outgoingQueueOperationScheduler;
+
@property CKKSResultOperation* processIncomingQueueAfterNextUnlockOperation;
@property NSMutableDictionary<NSString*, SecBoolNSErrorCallback>* pendingSyncCallbacks;
@property id<CKKSPeerProvider> currentPeerProvider;
+// An extra queue for semaphore-waiting-based NSOperations
+@property NSOperationQueue* waitingQueue;
+
// Make these readwrite
@property (nonatomic, readwrite) CKKSSelves* currentSelfPeers;
@property (nonatomic, readwrite) NSError* currentSelfPeersError;
apsConnectionClass:apsConnectionClass]) {
__weak __typeof(self) weakSelf = self;
+ _loggedIn = [[CKKSCondition alloc] init];
+ _loggedOut = [[CKKSCondition alloc] init];
+
_incomingQueueOperations = [NSHashTable weakObjectsHashTable];
_outgoingQueueOperations = [NSHashTable weakObjectsHashTable];
_zoneChangeFetcher = [[CKKSZoneChangeFetcher alloc] initWithCKKSKeychainView: self];
_keyStateFetchRequested = false;
_keyStateProcessRequested = false;
+ _waitingQueue = [[NSOperationQueue alloc] init];
+ _waitingQueue.maxConcurrentOperationCount = 5;
+
_keyStateReadyDependency = [self createKeyStateReadyDependency: @"Key state has become ready for the first time." ckoperationGroup:[CKOperationGroup CKKSGroupWithName:@"initial-key-state-ready-scan"]];
- dispatch_time_t initializeDelay = SecCKKSTestsEnabled() ? NSEC_PER_MSEC * 500 : NSEC_PER_SEC * 30;
+ dispatch_time_t initializeDelay = SecCKKSTestsEnabled() ? NSEC_PER_MSEC * 600 : NSEC_PER_SEC * 30;
_initializeScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-zone-initializer", self.zoneName]
initialDelay:0
continuingDelay:initializeDelay
[strongSelf maybeRestartSetup];
}];
+ dispatch_time_t initialOutgoingQueueDelay = SecCKKSTestsEnabled() ? NSEC_PER_MSEC * 200 : NSEC_PER_SEC * 1;
+ dispatch_time_t continuingOutgoingQueueDelay = SecCKKSTestsEnabled() ? NSEC_PER_MSEC * 500 : NSEC_PER_SEC * 30;
+ _outgoingQueueOperationScheduler = [[CKKSNearFutureScheduler alloc] initWithName:[NSString stringWithFormat: @"%@-outgoing-queue-scheduler", self.zoneName]
+ initialDelay:initialOutgoingQueueDelay
+ continuingDelay:continuingOutgoingQueueDelay
+ keepProcessAlive:false
+ block:^{}];
}
return self;
}
- (NSString*)description {
- return [NSString stringWithFormat:@"<%@: %@>", NSStringFromClass([self class]), self.zoneName];
+ return [NSString stringWithFormat:@"<%@: %@ (%@)>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState];
}
- (NSString*)debugDescription {
- return [NSString stringWithFormat:@"<%@: %@ %p>", NSStringFromClass([self class]), self.zoneName, self];
+ return [NSString stringWithFormat:@"<%@: %@ (%@) %p>", NSStringFromClass([self class]), self.zoneName, self.keyHierarchyState, self];
}
- (CKKSZoneKeyState*)keyHierarchyState {
return self.activeTLK;
}
-- (void) initializeZone {
- // Unfortunate, but makes retriggering easy.
- [self.initializeScheduler trigger];
-}
-
- (void)maybeRestartSetup {
[self dispatchSync: ^bool{
- if(self.setupStarted && !self.setupComplete) {
- ckksdebug("ckks", self, "setup has restarted. Ignoring timer fire");
- return false;
- }
-
- if(self.setupSuccessful) {
- ckksdebug("ckks", self, "setup has completed successfully. Ignoring timer fire");
+ if([self.viewSetupOperation isPending] || [self.viewSetupOperation isExecuting]) {
+ ckksinfo("ckks", self, "setup is in-flight. Ignoring timer fire");
return false;
}
[self resetSetup];
- [self _onqueueInitializeZone];
+ [self restartCurrentAccountStateOperation];
return true;
}];
}
_keyHierarchyError = nil;
}
- - (void)_onqueueInitializeZone {
+ - (void)_onqueueHandleCKLogin {
if(!SecCKKSIsEnabled()) {
ckksnotice("ckks", self, "Skipping CloudKit initialization due to disabled CKKS");
return;
__weak __typeof(self) weakSelf = self;
- NSBlockOperation* afterZoneSetup = [NSBlockOperation blockOperationWithBlock: ^{
+ CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
+ [self handleCKLogin:ckse.ckzonecreated zoneSubscribed:ckse.ckzonesubscribed];
+
+ self.viewSetupOperation = [CKKSResultOperation operationWithBlock: ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
if(!strongSelf) {
ckkserror("ckks", strongSelf, "received callback for released object");
// Note that CKKSZone has probably called [handleLogout]; which means we have a key hierarchy reset queued up. Error here anyway.
NSError* realReason = strongSelf.zoneCreatedError ? strongSelf.zoneCreatedError : strongSelf.zoneSubscribedError;
+ strongSelf.viewSetupOperation.error = realReason;
[strongSelf _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateError withError: realReason];
// We're supposed to be up, but something has gone wrong. Blindly retry until it works.
// We can't enter the account queue until an account exists. Before this point, we don't know if one does.
[strongSelf dispatchSyncWithAccountKeys: ^bool{
+ // Change our condition variables to reflect that we think we're logged in
+ strongSelf.loggedOut = [[CKKSCondition alloc] initToChain: strongSelf.loggedOut];
+ [strongSelf.loggedIn fulfill];
+
CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: strongSelf.zoneName];
// Check if we believe we've synced this zone before.
} else {
// Likely a restart of securityd!
+ // First off, are there any in-flight queue entries? If so, put them back into New.
+ // If they're truly in-flight, we'll "conflict" with ourselves, but that should be fine.
+ NSError* error = nil;
+ [self _onqueueResetAllInflightOQE:&error];
+ if(error) {
+ ckkserror("ckks", self, "Couldn't reset in-flight OQEs, bad behavior ahead: %@", error);
+ }
+
// Are there any fixups to run first?
strongSelf.lastFixupOperation = [CKKSFixups fixup:ckse.lastFixup for:strongSelf];
if(strongSelf.lastFixupOperation) {
initialProcess = [strongSelf processIncomingQueue:false after:strongSelf.lastFixupOperation];
}
- if(!strongSelf.egoManifest) {
- ckksnotice("ckksmanifest", strongSelf, "No ego manifest on restart; rescanning");
- strongSelf.initialScanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:strongSelf ckoperationGroup:strongSelf.keyHierarchyOperationGroup];
- strongSelf.initialScanOperation.name = @"initial-scan-operation";
- [strongSelf.initialScanOperation addNullableDependency:strongSelf.lastFixupOperation];
- [strongSelf.initialScanOperation addNullableDependency:strongSelf.lockStateTracker.unlockDependency];
- [strongSelf.initialScanOperation addDependency: initialProcess];
- [strongSelf scheduleOperation: strongSelf.initialScanOperation];
+ if([CKKSManifest shouldSyncManifests]) {
+ if (!strongSelf.egoManifest) {
+ ckksnotice("ckksmanifest", strongSelf, "No ego manifest on restart; rescanning");
+ strongSelf.initialScanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:strongSelf ckoperationGroup:strongSelf.keyHierarchyOperationGroup];
+ strongSelf.initialScanOperation.name = @"initial-scan-operation";
+ [strongSelf.initialScanOperation addNullableDependency:strongSelf.lastFixupOperation];
+ [strongSelf.initialScanOperation addNullableDependency:strongSelf.lockStateTracker.unlockDependency];
+ [strongSelf.initialScanOperation addDependency: initialProcess];
+ [strongSelf scheduleOperation: strongSelf.initialScanOperation];
+ }
}
+ // Process outgoing queue after re-start
[strongSelf processOutgoingQueueAfter:strongSelf.lastFixupOperation ckoperationGroup:strongSelf.keyHierarchyOperationGroup];
}
return true;
}];
}];
- afterZoneSetup.name = @"view-setup";
-
- CKKSZoneStateEntry* ckse = [CKKSZoneStateEntry state: self.zoneName];
- NSOperation* zoneSetupOperation = [self createSetupOperation: ckse.ckzonecreated zoneSubscribed: ckse.ckzonesubscribed];
-
- self.viewSetupOperation = [[CKKSGroupOperation alloc] init];
- self.viewSetupOperation.name = @"view-setup-group";
- if(!zoneSetupOperation.isFinished) {
- [self.viewSetupOperation runBeforeGroupFinished: zoneSetupOperation];
- }
-
- [afterZoneSetup addDependency: zoneSetupOperation];
- [self.viewSetupOperation runBeforeGroupFinished: afterZoneSetup];
+ self.viewSetupOperation.name = @"view-setup";
+ [self.viewSetupOperation addNullableDependency: self.zoneSetupOperation];
[self scheduleAccountStatusOperation: self.viewSetupOperation];
}
return true;
}];
- [strongSelf resetSetup];
-
if(error) {
ckksnotice("ckksreset", strongSelf, "Local reset finished with error %@", error);
strongOp.error = error;
if(strongSelf.accountStatus == CKKSAccountStatusAvailable) {
// Since we're logged in, we expect a reset to fix up the key hierarchy
ckksnotice("ckksreset", strongSelf, "logged in; re-initializing zone");
- [strongSelf initializeZone];
+ [self.initializeScheduler trigger];
ckksnotice("ckksreset", strongSelf, "waiting for key hierarchy to become ready");
CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-key-hierarchy" withBlock:^{}];
}
- (CKKSResultOperation*)resetCloudKitZone {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping CloudKit reset due to disabled CKKS");
- return nil;
- }
-
// On a reset, we should cancel all existing operations
[self cancelAllOperations];
CKKSResultOperation* reset = [super beginResetCloudKitZoneOperation];
if(strongSelf.accountStatus == CKKSAccountStatusAvailable) {
// Since we're logged in, we expect a reset to fix up the key hierarchy
ckksnotice("ckksreset", strongSelf, "re-initializing zone");
- [strongSelf initializeZone];
+ [self.initializeScheduler trigger];
ckksnotice("ckksreset", strongSelf, "waiting for key hierarchy to become ready");
CKKSResultOperation* waitOp = [CKKSResultOperation named:@"waiting-for-reset" withBlock:^{}];
}
ckksnotice("ckkskey", strongSelf, "%@", message);
- // While we weren't in 'ready', keychain modifications might have come in and were dropped on the floor. Find them!
- CKKSScanLocalItemsOperation* scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView: strongSelf ckoperationGroup:group];
- [strongSelf scheduleOperation: scanOperation];
+ [strongSelf dispatchSync:^bool {
+ if(strongSelf.droppedItems) {
+ // While we weren't in 'ready', keychain modifications might have come in and were dropped on the floor. Find them!
+ ckksnotice("ckkskey", strongSelf, "Launching scan operation for missed items");
+ CKKSScanLocalItemsOperation* scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView: strongSelf ckoperationGroup:group];
+ [strongSelf scheduleOperation: scanOperation];
+ }
+ return true;
+ }];
}];
keyStateReadyDependency.name = [NSString stringWithFormat: @"%@-key-state-ready", self.zoneName];
return keyStateReadyDependency;
self.keyStateProcessRequested = false;
self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-reset"];
+ NSOperation* oldKSRD = self.keyStateReadyDependency;
self.keyStateReadyDependency = [self createKeyStateReadyDependency:@"Key state has become ready for the first time (after reset)." ckoperationGroup:self.keyHierarchyOperationGroup];
+ if(oldKSRD) {
+ [oldKSRD addDependency:self.keyStateReadyDependency];
+ [self.waitingQueue addOperation:oldKSRD];
+ }
return;
}
}
if(state) {
+ ckksnotice("ckkskey", self, "Preparing to advance key hierarchy state machine from %@ to %@", self.keyHierarchyState, state);
self.keyStateMachineOperation = nil;
-
- ckksnotice("ckkskey", self, "Advancing key hierarchy state machine from %@ to %@", self.keyHierarchyState, state);
- self.keyHierarchyState = state;
+ } else {
+ ckksnotice("ckkskey", self, "Key hierarchy state machine is being poked; currently %@", self.keyHierarchyState);
+ state = self.keyHierarchyState;
}
// Many of our decisions below will be based on what keys exist. Help them out.
NSError* hierarchyError = nil;
- if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateInitializing]) {
+ if([state isEqualToString: SecCKKSZoneKeyStateInitializing]) {
if(state != nil) {
// Wait for CKKSZone to finish initialization.
ckkserror("ckkskey", self, "Asked to advance state machine (to %@) while CK zone still initializing.", state);
}
return;
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateReady]) {
+ } else if([state isEqualToString: SecCKKSZoneKeyStateReady]) {
if(self.keyStateProcessRequested || [remoteKeys count] > 0) {
// We've either received some remote keys from the last fetch, or someone has requested a reprocess.
ckksnotice("ckkskey", self, "Kicking off a key reprocess based on request:%d and remote key count %lu", self.keyStateProcessRequested, (unsigned long)[remoteKeys count]);
[self _onqueueKeyHierarchyProcess];
+ // Stay in state 'ready': this reprocess might not change anything. If it does, cleanup code elsewhere will
+ // reencode items that arrive during this ready
} else if(self.keyStateFullRefetchRequested) {
// In ready, but someone has requested a full fetch. Kick it off.
ckksnotice("ckkskey", self, "Kicking off a key refetch based on request:%d", self.keyStateFetchRequested);
[self _onqueueKeyHierarchyRefetch];
+ state = SecCKKSZoneKeyStateNeedFullRefetch;
} else if(self.keyStateFetchRequested) {
// In ready, but someone has requested a fetch. Kick it off.
ckksnotice("ckkskey", self, "Kicking off a key refetch based on request:%d", self.keyStateFetchRequested);
[self _onqueueKeyHierarchyFetch];
+ state = SecCKKSZoneKeyStateInitialized; // Don't go to 'ready', go to 'initialized', since we want to fetch again
}
// TODO: kick off a key roll if one has been requested
if(![checkedstate isEqualToString:SecCKKSZoneKeyStateReady] || hierarchyError) {
// Things is bad. Kick off a heal to fix things up.
ckksnotice("ckkskey", self, "Thought we were ready, but the key hierarchy is %@: %@", checkedstate, hierarchyError);
- self.keyHierarchyState = checkedstate;
+ state = checkedstate;
- } else {
- // In ready, nothing to do. Notify waiters and quit.
- self.keyHierarchyOperationGroup = nil;
- if(self.keyStateReadyDependency) {
- [self scheduleOperation: self.keyStateReadyDependency];
- self.keyStateReadyDependency = nil;
- }
-
- // If there are any OQEs waiting to be encrypted, launch an op to fix them
- if([outdatedOQEs count] > 0u) {
- ckksnotice("ckksreencrypt", self, "Reencrypting outgoing items as the key hierarchy is ready");
- CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
- [self scheduleOperation:op];
- }
-
- return;
}
}
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateInitialized]) {
+ } else if([state isEqualToString: SecCKKSZoneKeyStateInitialized]) {
// We're initialized and CloudKit is ready. See what needs done...
// Check if we have an existing key hierarchy
CKKSZoneKeyState* checkedstate = [self _onqueueEnsureKeyHierarchyHealth:keyset error:&hierarchyError];
if([checkedstate isEqualToString:SecCKKSZoneKeyStateReady] && !hierarchyError) {
ckksnotice("ckkskey", self, "Already have existing key hierarchy for %@; using it.", self.zoneID.zoneName);
- } else if(hierarchyError && [self.lockStateTracker isLockedError:hierarchyError]) {
- ckksnotice("ckkskey", self, "Initial scan shows key hierarchy is unavailable since keychain is locked: %@", hierarchyError);
- self.keyHierarchyState = SecCKKSZoneKeyStateWaitForUnlock;
} else {
ckksnotice("ckkskey", self, "Initial scan shows key hierarchy is %@: %@", checkedstate, hierarchyError);
- self.keyHierarchyState = checkedstate;
+ state = checkedstate;
}
} else {
[self _onqueueKeyHierarchyFetch];
}
- } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForFixupOperation]) {
+ } else if([state isEqualToString:SecCKKSZoneKeyStateWaitForFixupOperation]) {
// We should enter 'initialized' when the fixup operation completes
ckksnotice("ckkskey", self, "Waiting for the fixup operation: %@", self.lastFixupOperation);
self.keyStateMachineOperation = [NSBlockOperation named:@"key-state-after-fixup" withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
- [strongSelf dispatchSync:^bool{
+ [strongSelf dispatchSyncWithAccountKeys:^bool{
+ ckksnotice("ckkskey", self, "Fixup operation complete! Restarting key hierarchy machinery");
[strongSelf _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitialized withError:nil];
return true;
}];
}];
[self.keyStateMachineOperation addNullableDependency:self.lastFixupOperation];
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateFetchComplete]) {
+ } else if([state isEqualToString: SecCKKSZoneKeyStateFetchComplete]) {
// We've just completed a fetch of everything. Are there any remote keys?
if(remoteKeys.count > 0u) {
// Process the keys we received.
// Huh. We appear to have current key pointers, but the keys themselves don't exist. That's weird.
// Transfer to the "unhealthy" state to request a fix
ckksnotice("ckkskey", self, "We appear to have current key pointers but no keys to match them. Moving to 'unhealthy'");
- self.keyHierarchyState = SecCKKSZoneKeyStateUnhealthy;
+ state = SecCKKSZoneKeyStateUnhealthy;
} else {
// No remote keys, and the pointers look sane? Do we have an existing key hierarchy?
CKKSZoneKeyState* checkedstate = [self _onqueueEnsureKeyHierarchyHealth:keyset error:&hierarchyError];
if([checkedstate isEqualToString:SecCKKSZoneKeyStateReady] && !hierarchyError) {
ckksnotice("ckkskey", self, "After fetch, everything looks good.");
- } else if(hierarchyError && [self.lockStateTracker isLockedError:hierarchyError]) {
- ckksnotice("ckkskey", self, "After fetch, we're locked. Key hierarchy is unavailable since keychain is locked: %@", hierarchyError);
- self.keyHierarchyState = SecCKKSZoneKeyStateWaitForUnlock;
} else if(localKeys.count == 0 && remoteKeys.count == 0) {
ckksnotice("ckkskey", self, "After fetch, we don't have any key hierarchy. Making a new one: %@", hierarchyError);
self.keyStateMachineOperation = [[CKKSNewTLKOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];
} else {
- ckksnotice("ckkskey", self, "After fetch, we have an unhealthy key hierarchy. Moving to %@: %@", checkedstate, hierarchyError);
- self.keyHierarchyState = checkedstate;
+ ckksnotice("ckkskey", self, "After fetch, we have a possibly unhealthy key hierarchy. Moving to %@: %@", checkedstate, hierarchyError);
+ state = checkedstate;
}
}
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateWaitForTLK]) {
+ } else if([state isEqualToString: SecCKKSZoneKeyStateWaitForTLK]) {
// We're in a hold state: waiting for the TLK bytes to arrive.
if(self.keyStateProcessRequested) {
[self _onqueueKeyHierarchyProcess];
}
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateWaitForUnlock]) {
- // We're in a hold state: waiting for the keybag to unlock so we can process the keys again.
+ } else if([state isEqualToString: SecCKKSZoneKeyStateWaitForUnlock]) {
+ // will be handled later.
+ ckksnotice("ckkskey", self, "Requested to enter waitforunlock");
- [self _onqueueKeyHierarchyProcess];
- [self.keyStateMachineOperation addNullableDependency: self.lockStateTracker.unlockDependency];
+ } else if([state isEqualToString: SecCKKSZoneKeyStateReadyPendingUnlock]) {
+ // will be handled later.
+ ckksnotice("ckkskey", self, "Believe we're ready, but recheck after unlock");
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateBadCurrentPointers]) {
+ } else if([state isEqualToString: SecCKKSZoneKeyStateBadCurrentPointers]) {
// The current key pointers are broken, but we're not sure why.
ckksnotice("ckkskey", self, "Our current key pointers are reported broken. Attempting a fix!");
self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:self.keyHierarchyOperationGroup];
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateNewTLKsFailed]) {
+ } else if([state isEqualToString: SecCKKSZoneKeyStateNewTLKsFailed]) {
ckksnotice("ckkskey", self, "Creating new TLKs didn't work. Attempting to refetch!");
[self _onqueueKeyHierarchyFetch];
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateHealTLKSharesFailed]) {
- if(!SecCKKSShareTLKs()) {
- ckkserror("ckkskey", self, "In SecCKKSZoneKeyStateHealTLKSharesFailed, but TLK sharing is disabled.");
- }
+ } else if([state isEqualToString: SecCKKSZoneKeyStateHealTLKSharesFailed]) {
ckksnotice("ckkskey", self, "Creating new TLK shares didn't work. Attempting to refetch!");
[self _onqueueKeyHierarchyFetch];
- } else if([self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateNeedFullRefetch]) {
+ } else if([state isEqualToString: SecCKKSZoneKeyStateNeedFullRefetch]) {
ckksnotice("ckkskey", self, "Informed of request for full refetch");
[self _onqueueKeyHierarchyRefetch];
} else {
- ckkserror("ckks", self, "asked to advance state machine to unknown state: %@", self.keyHierarchyState);
+ ckkserror("ckks", self, "asked to advance state machine to unknown state: %@", state);
+ self.keyHierarchyState = state;
return;
}
- if(self.keyStateMachineOperation) {
- if(self.keyStateReadyDependency == nil || [self.keyStateReadyDependency isFinished]) {
- ckksnotice("ckkskey", self, "reloading keyStateReadyDependency due to operation %@", self.keyStateMachineOperation);
+ // Check our other states: did the above code ask for a fix up? Are we in ready?
+ if([state isEqualToString:SecCKKSZoneKeyStateUnhealthy]) {
+ ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy. Launching fix.");
+ self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
- self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-broken"];
- self.keyStateReadyDependency = [self createKeyStateReadyDependency:@"Key state has become ready again." ckoperationGroup:self.keyHierarchyOperationGroup];
- }
+ } else if([state isEqualToString:SecCKKSZoneKeyStateHealTLKShares]) {
+ ckksnotice("ckksshare", self, "Key hierarchy is okay, but not shared appropriately. Launching fix.");
+ self.keyStateMachineOperation = [[CKKSHealTLKSharesOperation alloc] initWithCKKSKeychainView:self
+ ckoperationGroup:self.keyHierarchyOperationGroup];
- [self scheduleOperation: self.keyStateMachineOperation];
- } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateWaitForTLK]) {
- ckksnotice("ckkskey", self, "Entering %@", self.keyHierarchyState);
+ } else if([state isEqualToString: SecCKKSZoneKeyStateWaitForUnlock] || [state isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) {
+ // We're in a hold state: waiting for the keybag to unlock so we can reenter the key hierarchy state machine
+ // After the next unlock, poke ourselves
+ self.keyStateMachineOperation = [NSBlockOperation named:@"key-state-after-unlock" withBlock:^{
+ __strong __typeof(self) strongSelf = weakSelf;
+ if(!strongSelf) {
+ return;
+ }
+ [strongSelf dispatchSyncWithAccountKeys:^bool{
+ [strongSelf _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateInitialized withError:nil];
+ return true;
+ }];
+ }];
+ [self.keyStateMachineOperation addNullableDependency: self.lockStateTracker.unlockDependency];
- } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateUnhealthy]) {
- ckksnotice("ckkskey", self, "Looks like the key hierarchy is unhealthy. Launching fix.");
- self.keyStateMachineOperation = [[CKKSHealKeyHierarchyOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
- [self scheduleOperation: self.keyStateMachineOperation];
+ }
- } else if([self.keyHierarchyState isEqualToString:SecCKKSZoneKeyStateHealTLKShares]) {
- if(!SecCKKSShareTLKs()) {
- // This is an invalid state, since we haven't enabled TLK fixing. Set ourselves to ready!
- ckkserror("ckksshare", self, "In SecCKKSZoneKeyStateHealTLKShares, but TLK sharing is disabled.");
- self.keyHierarchyState = SecCKKSZoneKeyStateReady;
- } else {
- ckksnotice("ckksshare", self, "Key hierarchy is okay, but not shared appropriately. Launching fix.");
- self.keyStateMachineOperation = [[CKKSHealTLKSharesOperation alloc] initWithCKKSKeychainView:self
- ckoperationGroup:self.keyHierarchyOperationGroup];
- [self scheduleOperation: self.keyStateMachineOperation];
+ // Handle the key state ready dependency
+ if([state isEqualToString:SecCKKSZoneKeyStateReady] || [state isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock]) {
+ // Ready enough!
+ if(self.keyStateReadyDependency) {
+ [self scheduleOperation: self.keyStateReadyDependency];
+ self.keyStateReadyDependency = nil;
}
+ // If there are any OQEs waiting to be encrypted, launch an op to fix them
+ if([outdatedOQEs count] > 0u) {
+ ckksnotice("ckksreencrypt", self, "Reencrypting outgoing items as the key hierarchy is ready");
+ CKKSReencryptOutgoingItemsOperation* op = [[CKKSReencryptOutgoingItemsOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:self.keyHierarchyOperationGroup];
+ [self scheduleOperation:op];
+ }
+ } else {
+ // Not in ready: we need a key state ready dependency
+ if(self.keyStateReadyDependency == nil || [self.keyStateReadyDependency isFinished]) {
+ self.keyHierarchyOperationGroup = [CKOperationGroup CKKSGroupWithName:@"key-state-broken"];
+ self.keyStateReadyDependency = [self createKeyStateReadyDependency:@"Key state has become ready again." ckoperationGroup:self.keyHierarchyOperationGroup];
+ }
+ }
+
+ // Start any operations, or log that we aren't
+ if(self.keyStateMachineOperation) {
+ [self scheduleOperation: self.keyStateMachineOperation];
+
+ } else if([state isEqualToString:SecCKKSZoneKeyStateWaitForTLK]) {
+ ckksnotice("ckkskey", self, "Entering key state %@", state);
+ } else if([state isEqualToString:SecCKKSZoneKeyStateError]) {
+ ckksnotice("ckkskey", self, "Entering key state 'error'");
} else {
// Nothing to do and not in a waiting state? Awesome; we must be in the ready state.
- if(![self.keyHierarchyState isEqual: SecCKKSZoneKeyStateReady]) {
- ckksnotice("ckkskey", self, "No action to take in state %@; we must be ready.", self.keyHierarchyState);
- self.keyHierarchyState = SecCKKSZoneKeyStateReady;
+ if(!([state isEqualToString:SecCKKSZoneKeyStateReady] || [state isEqualToString:SecCKKSZoneKeyStateReadyPendingUnlock])) {
+ ckksnotice("ckkskey", self, "No action to take in state %@; we must be ready.", state);
+ state = SecCKKSZoneKeyStateReady;
self.keyHierarchyOperationGroup = nil;
if(self.keyStateReadyDependency) {
}
}
}
+ ckksnotice("ckkskey", self, "Advancing to key state: %@", state);
+ self.keyHierarchyState = state;
}
// For this key, who doesn't yet have a CKKSTLKShare for it?
- (NSSet<id<CKKSPeer>>*)_onqueueFindPeersMissingShare:(CKKSKey*)key error:(NSError* __autoreleasing*)error {
dispatch_assert_queue(self.queue);
- if(!SecCKKSShareTLKs()) {
+ if(!key) {
+ ckkserror("ckksshare", self, "Attempting to find missing shares for nil key");
return [NSSet set];
}
if(error) {
*error = self.currentTrustedPeersError;
}
- return nil;
+ return [NSSet set];
}
if(self.currentSelfPeersError) {
ckkserror("ckksshare", self, "Couldn't find missing shares because self peers aren't available: %@", self.currentSelfPeersError);
if(error) {
*error = self.currentSelfPeersError;
}
- return nil;
+ return [NSSet set];
}
NSMutableSet<id<CKKSPeer>>* peersMissingShares = [NSMutableSet set];
- (NSSet<CKKSTLKShare*>*)_onqueueCreateMissingKeyShares:(CKKSKey*)key error:(NSError* __autoreleasing*)error {
dispatch_assert_queue(self.queue);
- if(!SecCKKSShareTLKs()) {
- return [NSSet set];
- }
-
if(self.currentTrustedPeersError) {
ckkserror("ckksshare", self, "Couldn't create missing shares because trusted peers aren't available: %@", self.currentTrustedPeersError);
if(error) {
}
NSError* localerror = nil;
+ bool probablyOkIfUnlocked = false;
// keychain being locked is not a fatal error here
[set.tlk loadKeyMaterialFromKeychain:&localerror];
return SecCKKSZoneKeyStateUnhealthy;
} else if(localerror) {
ckkserror("ckkskey", self, "Soft error loading TLK(%@), maybe locked: %@", set.tlk, localerror);
+ probablyOkIfUnlocked = true;
}
localerror = nil;
return SecCKKSZoneKeyStateUnhealthy;
} else if(localerror) {
ckkserror("ckkskey", self, "Soft error loading classA key(%@), maybe locked: %@", set.classA, localerror);
+ probablyOkIfUnlocked = true;
}
localerror = nil;
self.activeTLK = [set.tlk uuid];
// Now that we're pretty sure we have the keys, are they shared appropriately?
- if(SecCKKSShareTLKs()) {
- // Check that every trusted peer has at least one TLK share
- NSSet<id<CKKSPeer>>* missingShares = [self _onqueueFindPeersMissingShare:set.tlk error:&localerror];
- if(localerror) {
- if(error) {
- *error = localerror;
- }
- return SecCKKSZoneKeyStateError;
+ // Check that every trusted peer has at least one TLK share
+ NSSet<id<CKKSPeer>>* missingShares = [self _onqueueFindPeersMissingShare:set.tlk error:&localerror];
+ if(localerror && [self.lockStateTracker isLockedError: localerror]) {
+ ckkserror("ckkskey", self, "Couldn't find missing TLK shares due to lock state: %@", localerror);
+ probablyOkIfUnlocked = true;
+
+ } else if(localerror) {
+ if(error) {
+ *error = localerror;
}
+ ckkserror("ckkskey", self, "Error finding missing TLK shares: %@", localerror);
+ return SecCKKSZoneKeyStateError;
+ }
- if(!missingShares || missingShares.count != 0u) {
- localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSMissingTLKShare
- description:[NSString stringWithFormat:@"Missing shares for %lu peers", (unsigned long)missingShares.count]];
- if(error) {
- *error = localerror;
- }
- return SecCKKSZoneKeyStateHealTLKShares;
- } else {
- ckksnotice("ckksshare", self, "TLK (%@) is shared correctly", set.tlk);
+ if(!missingShares || missingShares.count != 0u) {
+ localerror = [NSError errorWithDomain:CKKSErrorDomain code:CKKSMissingTLKShare
+ description:[NSString stringWithFormat:@"Missing shares for %lu peers", (unsigned long)missingShares.count]];
+ if(error) {
+ *error = localerror;
}
+ return SecCKKSZoneKeyStateHealTLKShares;
+ } else {
+ ckksnotice("ckksshare", self, "TLK (%@) is shared correctly", set.tlk);
}
// Got to the bottom? Cool! All keys are present and accounted for.
- return SecCKKSZoneKeyStateReady;
+ return probablyOkIfUnlocked ? SecCKKSZoneKeyStateReadyPendingUnlock : SecCKKSZoneKeyStateReady;
}
- (void)_onqueueKeyHierarchyFetch {
__block NSError* error = nil;
- if(self.accountStatus != CKKSAccountStatusAvailable && syncCallback) {
- // We're not logged into CloudKit, and therefore don't expect this item to be synced anytime particularly soon.
- CKKSAccountStatus accountStatus = self.accountStatus;
- dispatch_async(self.queue, ^{
- syncCallback(false, [NSError errorWithDomain:@"securityd"
- code:errSecNotLoggedIn
- userInfo:@{NSLocalizedDescriptionKey:
- [NSString stringWithFormat: @"No iCloud account available(%d); item is not expected to sync", (int)accountStatus]}]);
- });
-
+ if(self.accountStatus == CKKSAccountStatusNoAccount && syncCallback) {
+ // We're positively not logged into CloudKit, and therefore don't expect this item to be synced anytime particularly soon.
+ [self callSyncCallbackWithErrorNoAccount: syncCallback];
syncCallback = nil;
}
NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(added ? added : deleted, kSecAttrAccessible);
if(! ([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked] ||
[protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock] ||
- [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlways])) {
+ [protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate])) {
ckksnotice("ckks", self, "skipping sync of device-bound(%@) item", protection);
return;
}
CFReleaseNull(cferror);
}
- if(![self.keyHierarchyState isEqualToString: SecCKKSZoneKeyStateReady]) {
- ckksnotice("ckks", self, "Key state not ready for new items; skipping");
- return true;
- }
-
CKKSOutgoingQueueEntry* oqe = nil;
if (isAdd) {
oqe = [CKKSOutgoingQueueEntry withItem: added action: SecCKKSActionAdd ckks:self error: &error];
if(error) {
ckkserror("ckks", self, "Couldn't create outgoing queue entry: %@", error);
+ self.droppedItems = true;
// If the problem is 'no UUID', launch a scan operation to find and fix it
// We don't want to fix it up here, in the closing moments of a transaction
}
// If the problem is 'couldn't load key', tell the key hierarchy state machine to fix it
- // Then, launch a scan operation to find this item and upload it
if([error.domain isEqualToString:CKKSErrorDomain] && error.code == errSecItemNotFound) {
[self _onqueueAdvanceKeyStateMachineToState: nil withError: nil];
-
- ckksnotice("ckks", self, "Launching scan operation to refind %@", added);
- CKKSScanLocalItemsOperation* scanOperation = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView: self ckoperationGroup:operationGroup];
- [scanOperation addNullableDependency:self.keyStateReadyDependency];
- [self scheduleOperation: scanOperation];
}
return true;
}
- if(!oqe) {
- ckkserror("ckks", self, "Didn't create an outgoing queue entry, but no error given.");
- return true;
- }
-
if(rateLimiter) {
NSDate* limit = nil;
NSInteger value = [rateLimiter judge:oqe at:[NSDate date] limitTime:&limit];
return false;
}
- ckksnotice("ckkscurrent", strongSelf, "Retrieved current item pointer: %@", cip);
+ ckksinfo("ckkscurrent", strongSelf, "Retrieved current item pointer: %@", cip);
complete(cip.currentItemUUID, NULL);
return true;
}];
NSString* protection = (__bridge NSString*)SecDbItemGetCachedValueWithName(item, kSecAttrAccessible);
if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleWhenUnlocked]) {
class = SecCKKSKeyClassA;
- } else if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlways] ||
+ } else if([protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAlwaysPrivate] ||
[protection isEqualToString: (__bridge NSString*)kSecAttrAccessibleAfterFirstUnlock]) {
class = SecCKKSKeyClassC;
} else {
}
- (CKKSOutgoingQueueOperation*)processOutgoingQueueAfter:(CKKSResultOperation*)after ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping processOutgoingQueue due to disabled CKKS");
- return nil;
- }
-
CKKSOutgoingQueueOperation* outgoingop =
(CKKSOutgoingQueueOperation*) [self findFirstPendingOperation:self.outgoingQueueOperations
ofClass:[CKKSOutgoingQueueOperation class]];
// Will log any pending dependencies as well
ckksnotice("ckksoutgoing", self, "Returning existing %@", outgoingop);
+
+ // Shouldn't be necessary, but can't hurt
+ [self.outgoingQueueOperationScheduler trigger];
return outgoingop;
}
}
CKKSOutgoingQueueOperation* op = [[CKKSOutgoingQueueOperation alloc] initWithCKKSKeychainView:self ckoperationGroup:ckoperationGroup];
op.name = @"outgoing-queue-operation";
[op addNullableDependency:after];
+ [op addNullableDependency:self.outgoingQueueOperationScheduler.operationDependency];
+ [self.outgoingQueueOperationScheduler trigger];
[op addNullableDependency: self.initialScanOperation];
}
- (CKKSIncomingQueueOperation*) processIncomingQueue:(bool)failOnClassA after: (CKKSResultOperation*) after {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping processIncomingQueue due to disabled CKKS");
- return nil;
- }
-
CKKSIncomingQueueOperation* incomingop = (CKKSIncomingQueueOperation*) [self findFirstPendingOperation:self.incomingQueueOperations];
if(incomingop) {
ckksinfo("ckks", self, "Skipping processIncomingQueue due to at least one pending instance");
- (CKKSUpdateDeviceStateOperation*)updateDeviceState:(bool)rateLimit
waitForKeyHierarchyInitialization:(uint64_t)timeout
ckoperationGroup:(CKOperationGroup*)ckoperationGroup {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping updateDeviceState due to disabled CKKS");
- return nil;
- }
__weak __typeof(self) weakSelf = self;
// If securityd just started, the key state might be in some transient early state. Wait a bit.
}
ckksnotice("ckksdevice", strongSelf, "Finished waiting for key hierarchy state, currently %@", strongSelf.keyHierarchyState);
}];
+ [self.waitingQueue addOperation:waitForKeyReady];
CKKSUpdateDeviceStateOperation* op = [[CKKSUpdateDeviceStateOperation alloc] initWithCKKSKeychainView:self rateLimit:rateLimit ckoperationGroup:ckoperationGroup];
op.name = @"device-state-operation";
[op linearDependenciesWithSelfFirst:self.outgoingQueueOperations];
// CKKSUpdateDeviceStateOperations are special: they should fire even if we don't believe we're in an iCloud account.
- [self scheduleAccountStatusOperation:waitForKeyReady];
- [self scheduleAccountStatusOperation:op];
+ // They also shouldn't block or be blocked by any other operation; our wait operation above will handle that
+ [self scheduleOperationWithoutDependencies:op];
return op;
}
}
- (CKKSSynchronizeOperation*) resyncWithCloud {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Skipping resyncWithCloud due to disabled CKKS");
- return nil;
- }
-
CKKSSynchronizeOperation* op = [[CKKSSynchronizeOperation alloc] initWithCKKSKeychainView: self];
[self scheduleOperation: op];
return op;
}
+- (CKKSLocalSynchronizeOperation*)resyncLocal {
+ CKKSLocalSynchronizeOperation* op = [[CKKSLocalSynchronizeOperation alloc] initWithCKKSKeychainView:self];
+ [self scheduleOperation: op];
+ return op;
+}
+
- (CKKSResultOperation*)fetchAndProcessCKChanges:(CKKSFetchBecause*)because {
return [self fetchAndProcessCKChanges:because after:nil];
}
} else if(![ckme matchesCKRecord:record]) {
ckkserror("ckksresync", self, "BUG: Local item doesn't match resynced CloudKit record: %@ %@", ckme, record);
} else {
- ckksnotice("ckksresync", self, "Already know about this item record, skipping update: %@", record);
- return;
+ ckksnotice("ckksresync", self, "Already know about this item record, updating anyway: %@", record.recordID);
}
}
// If we found an old version in the database; this might be an update
if(ckme) {
- if([ckme matchesCKRecord:record]) {
+ if([ckme matchesCKRecord:record] && !resync) {
// This is almost certainly a record we uploaded; CKFetchChanges sends them back as new records
ckksnotice("ckks", self, "CloudKit has told us of record we already know about; skipping update");
return;
}
}
- // For now, drop into the synckeys table as a 'remote' key, then ask for a rekey operation.
CKKSKey* remotekey = [[CKKSKey alloc] initWithCKRecord: record];
- // We received this from an update. Don't use, yet.
+ // Do we already know about this key?
+ CKKSKey* possibleLocalKey = [CKKSKey tryFromDatabase:remotekey.uuid zoneID:self.zoneID error:&error];
+ if(error) {
+ ckkserror("ckkskey", self, "Error findibg exsiting local key for %@: %@", remotekey, error);
+ // Go on, assuming there isn't a local key
+ } else if(possibleLocalKey && [possibleLocalKey matchesCKRecord:record]) {
+ // Okay, nothing new here. Update the CKRecord and move on.
+ // Note: If the new record doesn't match the local copy, we have to go through the whole dance below
+ possibleLocalKey.storedCKRecord = record;
+ [possibleLocalKey saveToDatabase:&error];
+
+ if(error) {
+ ckkserror("ckkskey", self, "Couldn't update existing key: %@: %@", possibleLocalKey, error);
+ }
+ return;
+ }
+
+ // Drop into the synckeys table as a 'remote' key, then ask for a rekey operation.
remotekey.state = SecCKKSProcessedStateRemote;
remotekey.currentkey = false;
}
}
+- (bool)_onqueueResetAllInflightOQE:(NSError**)error {
+ NSError* localError = nil;
+
+ while(true) {
+ NSArray<CKKSOutgoingQueueEntry*> * inflightQueueEntries = [CKKSOutgoingQueueEntry fetch:SecCKKSOutgoingQueueItemsAtOnce
+ state:SecCKKSStateInFlight
+ zoneID:self.zoneID
+ error:&localError];
+
+ if(localError != nil) {
+ ckkserror("ckks", self, "Error finding inflight outgoing queue records: %@", localError);
+ if(error) {
+ *error = localError;
+ }
+ return false;
+ }
+
+ if([inflightQueueEntries count] == 0u) {
+ break;
+ }
+
+ for(CKKSOutgoingQueueEntry* oqe in inflightQueueEntries) {
+ [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateNew error:&localError];
+
+ if(localError) {
+ ckkserror("ckks", self, "Error fixing up inflight OQE(%@): %@", oqe, localError);
+ if(error) {
+ *error = localError;
+ }
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
- (bool)_onqueueChangeOutgoingQueueEntry: (CKKSOutgoingQueueEntry*) oqe toState: (NSString*) state error: (NSError* __autoreleasing*) error {
dispatch_assert_queue(self.queue);
ckkserror("ckks", self, "Couldn't delete %@: %@", oqe, localerror);
}
+ } else if([oqe.state isEqualToString:SecCKKSStateInFlight] && [state isEqualToString:SecCKKSStateNew]) {
+ // An in-flight OQE is moving to new? See if it's been superceded
+ CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:oqe.uuid state:SecCKKSStateNew zoneID:self.zoneID error:&localerror];
+ if(localerror) {
+ ckkserror("ckksoutgoing", self, "Couldn't fetch an overwriting OQE, assuming one doesn't exist: %@", localerror);
+ newOQE = nil;
+ }
+
+ if(newOQE) {
+ ckksnotice("ckksoutgoing", self, "New modification has come in behind inflight %@; dropping failed change", oqe);
+ // recurse for that lovely code reuse
+ [self _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateDeleted error:&localerror];
+ if(localerror) {
+ ckkserror("ckksoutgoing", self, "Couldn't delete in-flight OQE: %@", localerror);
+ if(error) {
+ *error = localerror;
+ }
+ }
+ } else {
+ oqe.state = state;
+ [oqe saveToDatabase: &localerror];
+ if(localerror) {
+ ckkserror("ckks", self, "Couldn't save %@ as %@: %@", oqe, state, localerror);
+ }
+ }
+
} else {
oqe.state = state;
[oqe saveToDatabase: &localerror];
}
NSError* localerror = nil;
- oqe.state = SecCKKSStateError;
- [oqe saveToDatabase: &localerror];
+ // Now, delete the OQE: it's never coming back
+ [oqe deleteFromDatabase:&localerror];
if(localerror) {
- ckkserror("ckks", self, "Couldn't set %@ as error: %@", oqe, localerror);
+ ckkserror("ckks", self, "Couldn't delete %@ (due to error %@): %@", oqe, itemError, localerror);
}
if(error && localerror) {
return localerror == nil;
}
-- (bool)_onQueueUpdateLatestManifestWithError:(NSError**)error
+- (bool)_onqueueUpdateLatestManifestWithError:(NSError**)error
{
dispatch_assert_queue(self.queue);
CKKSManifest* manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:error];
// First, if we have a local identity, check for any TLK shares
NSError* localerror = nil;
- if(SecCKKSShareTLKs()) {
- if(![proposedTLK wrapsSelf]) {
- ckkserror("ckksshare", self, "Potential TLK %@ does not wrap self; skipping TLK share checking", proposedTLK);
- } else {
- if(!self.currentSelfPeers.currentSelf || self.currentSelfPeersError) {
- ckkserror("ckksshare", self, "Couldn't fetch self peers: %@", self.currentSelfPeersError);
- if(error) {
- *error = self.currentSelfPeersError;
- }
- return false;
+ if(![proposedTLK wrapsSelf]) {
+ ckkserror("ckksshare", self, "Potential TLK %@ does not wrap self; skipping TLK share checking", proposedTLK);
+ } else {
+ if(!self.currentSelfPeers.currentSelf || self.currentSelfPeersError) {
+ ckkserror("ckksshare", self, "Couldn't fetch self peers: %@", self.currentSelfPeersError);
+ if(error) {
+ *error = self.currentSelfPeersError;
}
+ return false;
+ }
- if(!self.currentTrustedPeers || self.currentTrustedPeersError) {
- ckkserror("ckksshare", self, "Couldn't fetch trusted peers: %@", self.currentTrustedPeersError);
- if(error) {
- *error = self.currentTrustedPeersError;
- }
- return false;
+ if(!self.currentTrustedPeers || self.currentTrustedPeersError) {
+ ckkserror("ckksshare", self, "Couldn't fetch trusted peers: %@", self.currentTrustedPeersError);
+ if(error) {
+ *error = self.currentTrustedPeersError;
}
+ return false;
+ }
- NSArray<CKKSTLKShare*>* possibleShares = [CKKSTLKShare allFor:self.currentSelfPeers.currentSelf.peerID
- keyUUID:proposedTLK.uuid
- zoneID:self.zoneID
- error:&localerror];
- if(localerror) {
- ckkserror("ckksshare", self, "Error fetching CKKSTLKShares: %@", localerror);
- }
+ NSArray<CKKSTLKShare*>* possibleShares = [CKKSTLKShare allFor:self.currentSelfPeers.currentSelf.peerID
+ keyUUID:proposedTLK.uuid
+ zoneID:self.zoneID
+ error:&localerror];
+ if(localerror) {
+ ckkserror("ckksshare", self, "Error fetching CKKSTLKShares: %@", localerror);
+ }
- if(possibleShares.count == 0) {
- ckksnotice("ckksshare", self, "No CKKSTLKShares for %@", proposedTLK);
- }
+ if(possibleShares.count == 0) {
+ ckksnotice("ckksshare", self, "No CKKSTLKShares for %@", proposedTLK);
+ }
- for(CKKSTLKShare* possibleShare in possibleShares) {
- NSError* possibleShareError = nil;
- ckksnotice("ckksshare", self, "Checking possible TLK share %@ as %@",
- possibleShare, self.currentSelfPeers.currentSelf);
+ for(CKKSTLKShare* possibleShare in possibleShares) {
+ NSError* possibleShareError = nil;
+ ckksnotice("ckksshare", self, "Checking possible TLK share %@ as %@",
+ possibleShare, self.currentSelfPeers.currentSelf);
- CKKSKey* possibleKey = [possibleShare recoverTLK:self.currentSelfPeers.currentSelf
- trustedPeers:self.currentTrustedPeers
- error:&possibleShareError];
+ CKKSKey* possibleKey = [possibleShare recoverTLK:self.currentSelfPeers.currentSelf
+ trustedPeers:self.currentTrustedPeers
+ error:&possibleShareError];
- if(possibleShareError) {
- ckkserror("ckksshare", self, "Unable to unwrap TLKShare(%@) as %@: %@",
- possibleShare, self.currentSelfPeers.currentSelf, possibleShareError);
- ckkserror("ckksshare", self, "Current trust set: %@", self.currentTrustedPeers);
- // TODO: save error
- continue;
- }
+ if(possibleShareError) {
+ ckkserror("ckksshare", self, "Unable to unwrap TLKShare(%@) as %@: %@",
+ possibleShare, self.currentSelfPeers.currentSelf, possibleShareError);
+ ckkserror("ckksshare", self, "Current trust set: %@", self.currentTrustedPeers);
+ // TODO: save error
+ continue;
+ }
- bool result = [proposedTLK trySelfWrappedKeyCandidate:possibleKey.aessivkey error:&possibleShareError];
- if(possibleShareError) {
- ckkserror("ckksshare", self, "Unwrapped TLKShare(%@) does not unwrap proposed TLK(%@) as %@: %@",
- possibleShare, proposedTLK, self.currentSelfPeers.currentSelf, possibleShareError);
- // TODO save error
- continue;
- }
+ bool result = [proposedTLK trySelfWrappedKeyCandidate:possibleKey.aessivkey error:&possibleShareError];
+ if(possibleShareError) {
+ ckkserror("ckksshare", self, "Unwrapped TLKShare(%@) does not unwrap proposed TLK(%@) as %@: %@",
+ possibleShare, proposedTLK, self.currentSelfPeers.currentSelf, possibleShareError);
+ // TODO save error
+ continue;
+ }
- if(result) {
- ckksnotice("ckksshare", self, "TLKShare(%@) unlocked TLK(%@) as %@",
- possibleShare, proposedTLK, self.currentSelfPeers.currentSelf);
-
- // The proposed TLK is trusted key material. Persist it as a "trusted" key.
- [proposedTLK saveKeyMaterialToKeychain:true error: &possibleShareError];
- if(possibleShareError) {
- ckkserror("ckksshare", self, "Couldn't store the new TLK(%@) to the keychain: %@", proposedTLK, possibleShareError);
- if(error) {
- *error = possibleShareError;
- }
- return false;
- }
+ if(result) {
+ ckksnotice("ckksshare", self, "TLKShare(%@) unlocked TLK(%@) as %@",
+ possibleShare, proposedTLK, self.currentSelfPeers.currentSelf);
- return true;
+ // The proposed TLK is trusted key material. Persist it as a "trusted" key.
+ [proposedTLK saveKeyMaterialToKeychain:true error: &possibleShareError];
+ if(possibleShareError) {
+ ckkserror("ckksshare", self, "Couldn't store the new TLK(%@) to the keychain: %@", proposedTLK, possibleShareError);
+ if(error) {
+ *error = possibleShareError;
+ }
+ return false;
}
+
+ return true;
}
}
-
- } else {
- ckksnotice("ckks", self, "No current self identity. Skipping TLK shares.")
}
if([proposedTLK loadKeyMaterialFromKeychain:error]) {
});
}
-// Use this if you have a potential database connection already
-- (void) dispatchSyncWithConnection: (SecDbConnectionRef) dbconn block: (bool (^)(void)) block {
- if(dbconn) {
- dispatch_sync(self.queue, ^{
- CFErrorRef cferror = NULL;
- kc_transaction_type(dbconn, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, block);
+- (bool)dispatchSyncWithConnection:(SecDbConnectionRef _Nonnull)dbconn block:(bool (^)(void))block {
+ CFErrorRef cferror = NULL;
- if(cferror) {
- ckkserror("ckks", self, "error doing database transaction (sync), major problems ahead: %@", cferror);
- }
+ // Take the DB transaction, then get on the local queue.
+ // In the case of exclusive DB transactions, we don't really _need_ the local queue, but, it's here for future use.
+ bool ret = kc_transaction_type(dbconn, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, ^bool{
+ __block bool ok = false;
+
+ dispatch_sync(self.queue, ^{
+ ok = block();
});
- } else {
- [self dispatchSync: block];
+
+ return ok;
+ });
+
+ if(cferror) {
+ ckkserror("ckks", self, "error doing database transaction, major problems ahead: %@", cferror);
}
+ return ret;
}
-- (void) dispatchSync: (bool (^)(void)) block {
+- (void)dispatchSync: (bool (^)(void)) block {
// important enough to block this thread. Must get a connection first, though!
- __weak __typeof(self) weakSelf = self;
-
CFErrorRef cferror = NULL;
kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
- __strong __typeof(weakSelf) strongSelf = weakSelf;
- if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
- return false;
- }
-
- __block bool ok = false;
- __block CFErrorRef cferror = NULL;
-
- dispatch_sync(strongSelf.queue, ^{
- ok = kc_transaction_type(dbt, kSecDbExclusiveRemoteCKKSTransactionType, &cferror, block);
- });
- return ok;
+ return [self dispatchSyncWithConnection:dbt block:block];
});
if(cferror) {
- ckkserror("ckks", self, "error getting database connection (sync), major problems ahead: %@", cferror);
+ ckkserror("ckks", self, "error getting database connection, major problems ahead: %@", cferror);
}
}
#pragma mark - CKKSZoneUpdateReceiver
- (void)notifyZoneChange: (CKRecordZoneNotification*) notification {
- ckksinfo("ckks", self, "hurray, got a zone change for %@ %@", self, notification);
+ ckksnotice("ckks", self, "received a zone change notification for %@ %@", self, notification);
[self fetchAndProcessCKChanges:CKKSFetchBecauseAPNS];
}
-// Must be on the queue when this is called
- (void)handleCKLogin {
- dispatch_assert_queue(self.queue);
+ ckksnotice("ckks", self, "received a notification of CK login");
- if(!self.setupStarted) {
- [self _onqueueInitializeZone];
- } else {
- ckksinfo("ckks", self, "ignoring login as setup has already started");
- }
+ __weak __typeof(self) weakSelf = self;
+ CKKSResultOperation* login = [CKKSResultOperation named:@"ckks-login" withBlock:^{
+ __strong __typeof(self) strongSelf = weakSelf;
+
+ [strongSelf dispatchSync:^bool{
+ strongSelf.accountStatus = CKKSAccountStatusAvailable;
+ [strongSelf _onqueueHandleCKLogin];
+ return true;
+ }];
+ }];
+
+ [self scheduleAccountStatusOperation:login];
}
-- (void)handleCKLogout {
- NSBlockOperation* logout = [NSBlockOperation blockOperationWithBlock: ^{
- [self dispatchSync:^bool {
- ckksnotice("ckks", self, "received a notification of CK logout for %@", self.zoneName);
- NSError* error = nil;
+- (void)superHandleCKLogout {
+ [super handleCKLogout];
+}
- [self _onqueueResetLocalData: &error];
+- (void)handleCKLogout {
+ __weak __typeof(self) weakSelf = self;
+ CKKSResultOperation* logout = [CKKSResultOperation named:@"ckks-logout" withBlock: ^{
+ __strong __typeof(self) strongSelf = weakSelf;
+ if(!strongSelf) {
+ return;
+ }
+ [strongSelf dispatchSync:^bool {
+ ckksnotice("ckks", strongSelf, "received a notification of CK logout");
+ [strongSelf superHandleCKLogout];
+ NSError* error = nil;
+ [strongSelf _onqueueResetLocalData: &error];
if(error) {
- ckkserror("ckks", self, "error while resetting local data: %@", error);
+ ckkserror("ckks", strongSelf, "error while resetting local data: %@", error);
+ }
+
+ // Tell all pending sync clients that we don't expect to ever sync
+ for(NSString* callbackUUID in strongSelf.pendingSyncCallbacks.allKeys) {
+ [strongSelf callSyncCallbackWithErrorNoAccount:strongSelf.pendingSyncCallbacks[callbackUUID]];
+ strongSelf.pendingSyncCallbacks[callbackUUID] = nil;
}
+
+ strongSelf.loggedIn = [[CKKSCondition alloc] initToChain: strongSelf.loggedIn];
+ [strongSelf.loggedOut fulfill];
+
return true;
}];
}];
- logout.name = @"cloudkit-logout";
[self scheduleAccountStatusOperation: logout];
}
+- (void)callSyncCallbackWithErrorNoAccount:(SecBoolNSErrorCallback)syncCallback {
+ CKKSAccountStatus accountStatus = self.accountStatus;
+ dispatch_async(self.queue, ^{
+ syncCallback(false, [NSError errorWithDomain:@"securityd"
+ code:errSecNotLoggedIn
+ userInfo:@{NSLocalizedDescriptionKey:
+ [NSString stringWithFormat: @"No iCloud account available(%d); item is not expected to sync", (int)accountStatus]}]);
+ });
+}
+
#pragma mark - CKKSChangeFetcherErrorOracle
- (bool) isFatalCKFetchError: (NSError*) error {
ckksnotice("ckks", strongSelf, "CloudKit-inspired local reset of %@ ended with error: %@", strongSelf.zoneID, error);
} else {
ckksnotice("ckksreset", strongSelf, "re-initializing zone %@", strongSelf.zoneID);
- [strongSelf initializeZone];
+ [self.initializeScheduler trigger];
}
}];
CKKSResultOperation* resetHandler = [CKKSResultOperation named:@"reset-handler" withBlock:^{
__strong __typeof(self) strongSelf = weakSelf;
if(!strongSelf) {
- ckkserror("ckks", strongSelf, "received callback for released object");
+ ckkserror("ckksreset", strongSelf, "received callback for released object");
return;
}
if(resetOp.error) {
- ckksnotice("ckks", strongSelf, "CloudKit-inspired zone reset of %@ ended with error: %@", strongSelf.zoneID, resetOp.error);
- } else {
- ckksnotice("ckksreset", strongSelf, "re-initializing zone %@", strongSelf.zoneID);
- [strongSelf initializeZone];
+ ckksnotice("ckksreset", strongSelf, "CloudKit-inspired zone reset of %@ ended with error: %@", strongSelf.zoneID, resetOp.error);
}
}];
}
- (CKKSResultOperation*)waitForFetchAndIncomingQueueProcessing {
- if(!SecCKKSIsEnabled()) {
- ckksinfo("ckks", self, "Due to disabled CKKS, returning fast from waitForFetchAndIncomingQueueProcessing");
- return nil;
- }
-
CKKSResultOperation* op = [self fetchAndProcessCKChanges:CKKSFetchBecauseTesting];
[op waitUntilFinished];
return op;
}
[self.incomingQueueOperations removeAllObjects];
- // Don't send any more notifications, either
- _notifierClass = nil;
-
[super cancelAllOperations];
[self dispatchSync:^bool{
}];
}
+- (void)halt {
+ [super halt];
+
+ // Don't send any more notifications, either
+ _notifierClass = nil;
+}
+
- (NSDictionary*)status {
#define stringify(obj) CKKSNilToNSNull([obj description])
#define boolstr(obj) (!!(obj) ? @"yes" : @"no")
@"lockstatetracker": stringify(self.lockStateTracker),
@"accounttracker": stringify(self.accountTracker),
@"fetcher": stringify(self.zoneChangeFetcher),
- @"setup": boolstr(self.setupComplete),
+ @"setup": boolstr([self.viewSetupOperation isFinished]),
@"zoneCreated": boolstr(self.zoneCreated),
@"zoneCreatedError": stringify(self.zoneCreatedError),
@"zoneSubscribed": boolstr(self.zoneSubscribed),
--- /dev/null
+/*
+ * Copyright (c) 2017 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+#import <Foundation/Foundation.h>
+#import "keychain/ckks/CKKSGroupOperation.h"
+
+#if OCTAGON
+
+@class CKKSKeychainView;
+
+@interface CKKSLocalSynchronizeOperation : CKKSGroupOperation
+@property (weak) CKKSKeychainView* ckks;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks;
+@end
+
+// Reload everything from the mirror table into the incoming queue
+// Does not process these items
+@interface CKKSReloadAllItemsOperation : CKKSResultOperation
+@property (weak) CKKSKeychainView* ckks;
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks;
+@end
+
+#endif // OCTAGON
+
--- /dev/null
+/*
+ * Copyright (c) 2016 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+#import "keychain/ckks/CKKSKeychainView.h"
+#import "keychain/ckks/CKKSGroupOperation.h"
+#import "keychain/ckks/CKKSLocalSynchronizeOperation.h"
+#import "keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h"
+#import "keychain/ckks/CKKSScanLocalItemsOperation.h"
+#import "keychain/ckks/CKKSMirrorEntry.h"
+#import "keychain/ckks/CloudKitCategories.h"
+
+#if OCTAGON
+
+@interface CKKSLocalSynchronizeOperation ()
+@property int32_t restartCount;
+@end
+
+@implementation CKKSLocalSynchronizeOperation
+
+- (instancetype)init {
+ return nil;
+}
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks
+{
+ if(self = [super init]) {
+ _ckks = ckks;
+ _restartCount = 0;
+
+ [self addNullableDependency:ckks.holdLocalSynchronizeOperation];
+ }
+ return self;
+}
+
+- (void)groupStart {
+ __weak __typeof(self) weakSelf = self;
+
+ /*
+ * A local synchronize is very similar to a CloudKit synchronize, but it won't cause any (non-essential)
+ * CloudKit operations to occur.
+ *
+ * 1. Finish processing the outgoing queue. You can't be in-sync with cloudkit if you have an update that hasn't propagated.
+ * 2. Process anything in the incoming queue as normal.
+ * (Note that this might require the keybag to be unlocked.)
+ *
+ * 3. Take every item in the CKMirror, and check for its existence in the local keychain. If not present, add to the incoming queue.
+ * 4. Process the incoming queue again.
+ * 5. Scan the local keychain for items which exist locally but are not in CloudKit. Upload them.
+ * 6. If there are any such items in 4, restart the sync.
+ */
+
+ CKKSKeychainView* ckks = self.ckks;
+
+ // Synchronous, on some thread. Get back on the CKKS queue for SQL thread-safety.
+ [ckks dispatchSync: ^bool{
+ if(self.cancelled) {
+ ckksnotice("ckksresync", ckks, "CKKSSynchronizeOperation cancelled, quitting");
+ return false;
+ }
+
+ //ckks.lastLocalSynchronizeOperation = self;
+
+ uint32_t steps = 5;
+
+ ckksinfo("ckksresync", ckks, "Beginning local resynchronize (attempt %u)", self.restartCount);
+
+ CKOperationGroup* operationGroup = [CKOperationGroup CKKSGroupWithName:@"ckks-resync-local"];
+
+ // Step 1
+ CKKSOutgoingQueueOperation* outgoingOp = [ckks processOutgoingQueue: operationGroup];
+ outgoingOp.name = [NSString stringWithFormat: @"resync-step%u-outgoing", self.restartCount * steps + 1];
+ [self dependOnBeforeGroupFinished:outgoingOp];
+
+ // Step 2
+ CKKSIncomingQueueOperation* incomingOp = [[CKKSIncomingQueueOperation alloc] initWithCKKSKeychainView:ckks errorOnClassAFailure:true];
+ incomingOp.name = [NSString stringWithFormat: @"resync-step%u-incoming", self.restartCount * steps + 2];
+ [incomingOp addSuccessDependency:outgoingOp];
+ [self runBeforeGroupFinished:incomingOp];
+
+ // Step 3:
+ CKKSResultOperation* reloadOp = [[CKKSReloadAllItemsOperation alloc] initWithCKKSKeychainView:ckks];
+ reloadOp.name = [NSString stringWithFormat: @"resync-step%u-reload", self.restartCount * steps + 3];
+ [self runBeforeGroupFinished:reloadOp];
+
+ // Step 4
+ CKKSIncomingQueueOperation* incomingResyncOp = [[CKKSIncomingQueueOperation alloc] initWithCKKSKeychainView:ckks errorOnClassAFailure:true];
+ incomingResyncOp.name = [NSString stringWithFormat: @"resync-step%u-incoming-again", self.restartCount * steps + 4];
+ [incomingResyncOp addSuccessDependency: reloadOp];
+ [self runBeforeGroupFinished:incomingResyncOp];
+
+ // Step 5
+ CKKSScanLocalItemsOperation* scan = [[CKKSScanLocalItemsOperation alloc] initWithCKKSKeychainView:ckks ckoperationGroup:operationGroup];
+ scan.name = [NSString stringWithFormat: @"resync-step%u-scan", self.restartCount * steps + 5];
+ [scan addSuccessDependency: incomingResyncOp];
+ [self runBeforeGroupFinished: scan];
+
+ // Step 6
+ CKKSResultOperation* restart = [[CKKSResultOperation alloc] init];
+ restart.name = [NSString stringWithFormat: @"resync-step%u-consider-restart", self.restartCount * steps + 6];
+ [restart addExecutionBlock:^{
+ __strong __typeof(weakSelf) strongSelf = weakSelf;
+ if(!strongSelf) {
+ secerror("ckksresync: received callback for released object");
+ return;
+ }
+
+ if(scan.recordsFound > 0) {
+ if(strongSelf.restartCount >= 3) {
+ // we've restarted too many times. Fail and stop.
+ ckkserror("ckksresync", ckks, "restarted synchronization too often; Failing");
+ strongSelf.error = [NSError errorWithDomain:@"securityd"
+ code:2
+ userInfo:@{NSLocalizedDescriptionKey: @"resynchronization restarted too many times; churn in database?"}];
+ } else {
+ // restart the sync operation.
+ strongSelf.restartCount += 1;
+ ckkserror("ckksresync", ckks, "restarting synchronization operation due to new local items");
+ [strongSelf groupStart];
+ }
+ }
+ }];
+
+ [restart addSuccessDependency: scan];
+ [self runBeforeGroupFinished: restart];
+
+ return true;
+ }];
+}
+
+@end;
+
+#pragma mark - CKKSReloadAllItemsOperation
+
+@implementation CKKSReloadAllItemsOperation
+
+- (instancetype)init {
+ return nil;
+}
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks
+{
+ if(self = [super init]) {
+ _ckks = ckks;
+ }
+ return self;
+}
+
+- (void)main {
+ CKKSKeychainView* strongCKKS = self.ckks;
+
+ [strongCKKS dispatchSync: ^bool{
+ NSError* error = nil;
+ NSArray<CKKSMirrorEntry*>* mirrorItems = [CKKSMirrorEntry all:strongCKKS.zoneID error:&error];
+
+ if(error) {
+ ckkserror("ckksresync", strongCKKS, "Couldn't fetch mirror items: %@", error);
+ self.error = error;
+ return false;
+ }
+
+ // Reload all entries back into the local keychain
+ // We _could_ scan for entries, but that'd be expensive
+ // In 36044942, we used to store only the CKRecord system fields in the ckrecord. To work around this, make a whole new CKRecord from the item.
+ for(CKKSMirrorEntry* ckme in mirrorItems) {
+ CKRecord* ckmeRecord = [ckme.item CKRecordWithZoneID:strongCKKS.zoneID];
+ if(!ckmeRecord) {
+ ckkserror("ckksresync", strongCKKS, "Couldn't make CKRecord for item: %@", ckme);
+ continue;
+ }
+
+ [strongCKKS _onqueueCKRecordChanged:ckmeRecord resync:true];
+ }
+
+ return true;
+ }];
+}
+@end
+#endif
+
@interface CKKSLockStateTracker : NSObject
@property NSOperation* unlockDependency;
--(instancetype)init;
+- (instancetype)init;
// Force a recheck of the keybag lock state
--(void)recheck;
+- (void)recheck;
// Check if this error code is related to keybag is locked and we should retry later
--(bool)isLockedError:(NSError *)error;
+- (bool)isLockedError:(NSError*)error;
// Ask AKS if the user's keybag is locked
-+(bool)queryAKSLocked;
++ (bool)queryAKSLocked;
@end
-#endif // OCTAGON
+#endif // OCTAGON
#if OCTAGON
-#import "CKKSRecordHolder.h"
#import <Foundation/Foundation.h>
#import <SecurityFoundation/SFKey.h>
+#import "CKKSRecordHolder.h"
NS_ASSUME_NONNULL_BEGIN
@interface CKKSEgoManifest : CKKSManifest
+ (nullable CKKSEgoManifest*)tryCurrentEgoManifestForZone:(NSString*)zone;
-+ (nullable instancetype)newManifestForZone:(NSString*)zone withItems:(NSArray<CKKSItem*>*)items peerManifestIDs:(NSArray<NSString*>*)peerManifestIDs currentItems:(NSDictionary*)currentItems error:(NSError**)error;
-
-- (void)updateWithNewOrChangedRecords:(NSArray<CKRecord*>*)newOrChangedRecords deletedRecordIDs:(NSArray<CKRecordID*>*)deletedRecordIDs;
++ (nullable instancetype)newManifestForZone:(NSString*)zone
+ withItems:(NSArray<CKKSItem*>*)items
+ peerManifestIDs:(NSArray<NSString*>*)peerManifestIDs
+ currentItems:(NSDictionary*)currentItems
+ error:(NSError**)error;
+
+- (void)updateWithNewOrChangedRecords:(NSArray<CKRecord*>*)newOrChangedRecords
+ deletedRecordIDs:(NSArray<CKRecordID*>*)deletedRecordIDs;
- (void)setCurrentItemUUID:(NSString*)newCurrentItemUUID forIdentifier:(NSString*)currentPointerIdentifier;
- (NSArray<CKRecord*>*)allCKRecordsWithZoneID:(CKRecordZoneID*)zoneID;
@interface CKKSEgoManifest (UnitTesting)
-+ (nullable instancetype)newFakeManifestForZone:(NSString*)zone withItemRecords:(NSArray<CKRecord*>*)itemRecords currentItems:(NSDictionary*)currentItems signerID:(NSString*)signerID keyPair:(SFECKeyPair*)keyPair error:(NSError**)error;
++ (nullable instancetype)newFakeManifestForZone:(NSString*)zone
+ withItemRecords:(NSArray<CKRecord*>*)itemRecords
+ currentItems:(NSDictionary*)currentItems
+ signerID:(NSString*)signerID
+ keyPair:(SFECKeyPair*)keyPair
+ error:(NSError**)error;
@end
@interface CKKSManifestInjectionPointHelper : NSObject
-@property (class) BOOL ignoreChanges; // turn to YES to have changes to the database get ignored by CKKSManifest to support negative testing
+@property (class) BOOL ignoreChanges; // turn to YES to have changes to the database get ignored by CKKSManifest to support negative testing
+ (void)registerEgoPeerID:(NSString*)egoPeerID keyPair:(SFECKeyPair*)keyPair;
#import "CKKS.h"
#import "CKKSItem.h"
#import "CKKSCurrentItemPointer.h"
-#import "CKKSAnalyticsLogger.h"
#import "utilities/der_plist.h"
#import <securityd/SOSCloudCircleServer.h>
#import <securityd/SecItemServer.h>
{
CKKSAccountInfo* accountInfo = [[CKKSAccountInfo alloc] init];
- [[CKKSEgoManifest egoHelper] performWithSigningKey:^(_SFECKeyPair* signingKey, NSError* error) {
+ [[CKKSEgoManifest egoHelper] performWithSigningKey:^(SFECKeyPair* signingKey, NSError* error) {
accountInfo.signingKey = signingKey;
if(error) {
secerror("ckksmanifest: cannot get signing key from account: %@", error);
NSString* signatureBase64String = record[SecCKRecordManifestSignaturesKey];
if (!signatureBase64String) {
ckkserror("ckksmanifest", record.recordID.zoneID, "attempt to create manifest from CKRecord that does not have signatures attached: %@", record);
- [[CKKSAnalyticsLogger logger] logHardFailureForEventNamed:@"CKKSManifestCreateFromCKRecord" withAttributes:@{CKKSManifestZoneKey : record.recordID.zoneID.zoneName}];
return nil;
}
NSData* signatureDERData = [[NSData alloc] initWithBase64EncodedString:signatureBase64String options:0];
NSDictionary* signaturesDict = [self signatureDictFromDERData:signatureDERData error:&error];
if (error) {
ckkserror("ckksmanifest", record.recordID.zoneID, "failed to initialize CKKSManifest from CKRecord because we could not form a signature dict from the record: %@", record);
- [[CKKSAnalyticsLogger logger] logHardFailureForEventNamed:@"CKKSManifestCreateFromCKRecord" withAttributes:@{CKKSManifestZoneKey : record.recordID.zoneID.zoneName}];
return nil;
}
NSString* digestBase64String = record[SecCKRecordManifestDigestValueKey];
if (!digestBase64String) {
ckkserror("ckksmanifest", record.recordID.zoneID, "attempt to create manifest from CKRecord that does not have a digest attached: %@", record);
- [[CKKSAnalyticsLogger logger] logHardFailureForEventNamed:@"CKKSManifestCreateFromCKRecord" withAttributes:@{CKKSManifestZoneKey : record.recordID.zoneID.zoneName}];
return nil;
}
NSData* digestData = [[NSData alloc] initWithBase64EncodedString:digestBase64String options:0];
signerID:record[SecCKRecordManifestSignerIDKey]
schema:schemaDict]) {
self.storedCKRecord = record;
- [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"CKKSManifestCreateFromCKRecord"];
}
- else {
- [[CKKSAnalyticsLogger logger] logHardFailureForEventNamed:@"CKKSManifestCreateFromCKRecord" withAttributes:@{CKKSManifestZoneKey : record.recordID.zoneID.zoneName}];
- }
-
+
return self;
}
NSData* signatureDERData = [self derDataFromSignatureDict:self.signatures error:nil];
if (!signatureDERData) {
- [[CKKSAnalyticsLogger logger] logHardFailureForEventNamed:@"CKKSManifestUpdateRecord" withAttributes:@{CKKSManifestZoneKey : zoneID.zoneName, CKKSManifestSignerIDKey : _signerID, CKKSManifestGenCountKey : @(_generationCount)}];
return record;
}
record[key] = futureField;
}];
- [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"CKKSManifestUpdateRecord"];
return record;
}
}
}
- if (verified) {
- [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"CKKSManifestValidateSelf"];
- }
- else {
- [[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"CKKSManifestValidateSelf" withAttributes:@{CKKSManifestZoneKey : _zoneName, CKKSManifestSignerIDKey : _signerID, CKKSManifestGenerationCountKey : @(_generationCount)}];
- }
-
return verified;
}
#if OCTAGON
-#import "CKKSRecordHolder.h"
#import <Foundation/Foundation.h>
+#import "CKKSRecordHolder.h"
@class CKRecord;
@class CKKSItem;
@interface CKKSManifestLeafRecord : CKKSCKRecordHolder
+ (BOOL)recordExistsForID:(NSString*)recordID;
-+ (instancetype)leafRecordForID:(NSString*)recordID error:(NSError* __autoreleasing *)error;
-+ (instancetype)tryLeafRecordForID:(NSString*)recordID error:(NSError* __autoreleasing *)error;
++ (instancetype)leafRecordForID:(NSString*)recordID error:(NSError* __autoreleasing*)error;
++ (instancetype)tryLeafRecordForID:(NSString*)recordID error:(NSError* __autoreleasing*)error;
+ (NSString*)leafUUIDForRecordID:(NSString*)recordID;
@property (nonatomic, readonly) NSString* uuid;
@property (nonatomic, readonly) NSData* digestValue;
-@property (nonatomic, readonly) NSDictionary<NSString*, NSData*>* recordDigestDict; // keyed by record UUID
+@property (nonatomic, readonly) NSDictionary<NSString*, NSData*>* recordDigestDict; // keyed by record UUID
@end
#if OCTAGON
-#import "CKKSSQLDatabaseObject.h"
-#import "CKKSItem.h"
-#include <utilities/SecDb.h>
#include <securityd/SecDbItem.h>
+#include <utilities/SecDb.h>
+#import "CKKSItem.h"
+#import "CKKSSQLDatabaseObject.h"
#ifndef CKKSMirrorEntry_h
#define CKKSMirrorEntry_h
@property uint64_t wasCurrent;
--(instancetype)initWithCKKSItem:(CKKSItem*)item;
--(instancetype)initWithCKRecord:(CKRecord*)record;
--(void)setFromCKRecord: (CKRecord*) record;
-- (bool)matchesCKRecord: (CKRecord*) record;
+- (instancetype)initWithCKKSItem:(CKKSItem*)item;
+- (instancetype)initWithCKRecord:(CKRecord*)record;
+- (void)setFromCKRecord:(CKRecord*)record;
+- (bool)matchesCKRecord:(CKRecord*)record;
-+ (instancetype) fromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype)fromDatabase:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabase:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
-+ (NSDictionary<NSString*,NSNumber*>*)countsByParentKey:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (NSDictionary<NSString*, NSNumber*>*)countsByParentKey:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
@end
#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>
+NS_ASSUME_NONNULL_BEGIN
/*
* The CKKSNearFutureScheduler is intended to rate-limit an operation. When
@interface CKKSNearFutureScheduler : NSObject
-@property (readonly) NSDate* nextFireTime;
-@property void (^futureOperation)(void);
+@property (nullable, readonly) NSDate* nextFireTime;
+@property void (^futureBlock)(void);
--(instancetype)initWithName:(NSString*)name
- delay:(dispatch_time_t)ns
- keepProcessAlive:(bool)keepProcessAlive
- block:(void (^)(void))futureOperation;
+// Will execute every time futureBlock is called, just after the future block.
+// Operations added in the futureBlock will receive the next operationDependency, so they won't run again until futureBlock occurs again.
+@property (nullable, readonly) NSOperation* operationDependency;
--(instancetype)initWithName:(NSString*)name
- initialDelay:(dispatch_time_t)initialDelay
- continuingDelay:(dispatch_time_t)continuingDelay
- keepProcessAlive:(bool)keepProcessAlive
- block:(void (^)(void))futureOperation;
+- (instancetype)initWithName:(NSString*)name
+ delay:(dispatch_time_t)ns
+ keepProcessAlive:(bool)keepProcessAlive
+ block:(void (^_Nonnull)(void))futureOperation;
--(void)trigger;
+- (instancetype)initWithName:(NSString*)name
+ initialDelay:(dispatch_time_t)initialDelay
+ continuingDelay:(dispatch_time_t)continuingDelay
+ keepProcessAlive:(bool)keepProcessAlive
+ block:(void (^_Nonnull)(void))futureBlock;
--(void)cancel;
+- (void)trigger;
+
+- (void)cancel;
// Don't trigger again until at least this much time has passed.
--(void)waitUntil:(uint64_t)delay;
+- (void)waitUntil:(uint64_t)delay;
@end
+
+NS_ASSUME_NONNULL_END
#import "CKKSNearFutureScheduler.h"
#import "CKKSCondition.h"
+#import "keychain/ckks/NSOperationCategories.h"
#include <os/transaction_private.h>
@interface CKKSNearFutureScheduler ()
@property dispatch_time_t initialDelay;
@property dispatch_time_t continuingDelay;
+@property NSOperation* operationDependency;
+@property (nonnull) NSOperationQueue* operationQueue;
+
@property NSDate* predictedNextFireTime;
@property bool liveRequest;
@property CKKSCondition* liveRequestReceived; // Triggered when liveRequest goes to true.
@implementation CKKSNearFutureScheduler
--(instancetype)initWithName:(NSString*)name delay:(dispatch_time_t)ns keepProcessAlive:(bool)keepProcessAlive block:(void (^)(void))futureOperation
+-(instancetype)initWithName:(NSString*)name delay:(dispatch_time_t)ns keepProcessAlive:(bool)keepProcessAlive block:(void (^)(void))futureBlock
{
- return [self initWithName:name initialDelay:ns continuingDelay:ns keepProcessAlive:keepProcessAlive block:futureOperation];
+ return [self initWithName:name initialDelay:ns continuingDelay:ns keepProcessAlive:keepProcessAlive block:futureBlock];
}
-(instancetype)initWithName:(NSString*)name
initialDelay:(dispatch_time_t)initialDelay
continuingDelay:(dispatch_time_t)continuingDelay
keepProcessAlive:(bool)keepProcessAlive
- block:(void (^)(void))futureOperation
+ block:(void (^)(void))futureBlock
{
if((self = [super init])) {
_name = name;
_queue = dispatch_queue_create([[NSString stringWithFormat:@"near-future-scheduler-%@",name] UTF8String], DISPATCH_QUEUE_SERIAL);
_initialDelay = initialDelay;
_continuingDelay = continuingDelay;
- _futureOperation = futureOperation;
+ _futureBlock = futureBlock;
_liveRequest = false;
_liveRequestReceived = [[CKKSCondition alloc] init];
_predictedNextFireTime = nil;
_keepProcessAlive = keepProcessAlive;
+
+ _operationQueue = [[NSOperationQueue alloc] init];
+ _operationDependency = [self makeOperationDependency];
}
return self;
}
+- (NSOperation*)makeOperationDependency {
+ return [NSBlockOperation named:[NSString stringWithFormat:@"nfs-%@", self.name] withBlock:^{}];
+}
+
-(NSString*)description {
NSDate* nextAt = self.nextFireTime;
if(nextAt) {
// If we have a live request, send the next fire time back. Otherwise, wait a tiny tiny bit to see if we receive a request.
if(self.liveRequest) {
return self.predictedNextFireTime;
- } else if([self.liveRequestReceived wait:100*NSEC_PER_USEC] == 0) {
+ } else if([self.liveRequestReceived wait:50*NSEC_PER_USEC] == 0) {
return self.predictedNextFireTime;
}
dispatch_assert_queue(self.queue);
if(self.liveRequest) {
- self.futureOperation();
+ // Put a new dependency in place, and save the old one for execution
+ NSOperation* dependency = self.operationDependency;
+ self.operationDependency = [self makeOperationDependency];
+
+ self.futureBlock();
self.liveRequest = false;
self.liveRequestReceived = [[CKKSCondition alloc] init];
self.transaction = nil;
+ [self.operationQueue addOperation: dependency];
+
self.predictedNextFireTime = [NSDate dateWithTimeIntervalSinceNow: (NSTimeInterval) ((double) self.continuingDelay) / (double) NSEC_PER_SEC];
} else {
// The timer has fired with no requests to call the block. Cancel it.
@end
-#endif // OCTAGON
-
+#endif // OCTAGON
ckks.lastNewTLKOperation = self;
+ if(ckks.currentSelfPeersError) {
+ if([ckks.lockStateTracker isLockedError: ckks.currentSelfPeersError]) {
+ ckkserror("ckksshare", ckks, "Can't create new TLKs: keychain is locked so self peers are unavailable: %@", ckks.currentSelfPeersError);
+ [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForUnlock withError:nil];
+ } else {
+ ckkserror("ckkstlk", ckks, "Couldn't create new TLKs because self peers aren't available: %@", ckks.currentSelfPeersError);
+ [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateNewTLKsFailed withError: ckks.currentSelfPeersError];
+ }
+ self.error = ckks.currentSelfPeersError;
+ return false;
+ }
+ if(ckks.currentTrustedPeersError) {
+ if([ckks.lockStateTracker isLockedError: ckks.currentTrustedPeersError]) {
+ ckkserror("ckksshare", ckks, "Can't create new TLKs: keychain is locked so trusted peers are unavailable: %@", ckks.currentTrustedPeersError);
+ [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateWaitForUnlock withError:nil];
+ } else {
+ ckkserror("ckkstlk", ckks, "Couldn't create new TLKs because trusted peers aren't available: %@", ckks.currentTrustedPeersError);
+ [ckks _onqueueAdvanceKeyStateMachineToState: SecCKKSZoneKeyStateNewTLKsFailed withError: ckks.currentTrustedPeersError];
+ }
+ self.error = ckks.currentTrustedPeersError;
+ return false;
+ }
+
NSError* error = nil;
ckksinfo("ckkstlk", ckks, "Generating new TLK");
// Generate the TLK sharing records for all trusted peers
NSMutableSet<CKKSTLKShare*>* tlkShares = [NSMutableSet set];
- if(SecCKKSShareTLKs()) {
- for(id<CKKSPeer> trustedPeer in ckks.currentTrustedPeers) {
- ckksnotice("ckkstlk", ckks, "Generating TLK(%@) share for %@", newTLK, trustedPeer);
- CKKSTLKShare* share = [CKKSTLKShare share:newTLK as:ckks.currentSelfPeers.currentSelf to:trustedPeer epoch:-1 poisoned:0 error:&error];
+ for(id<CKKSPeer> trustedPeer in ckks.currentTrustedPeers) {
+ ckksnotice("ckkstlk", ckks, "Generating TLK(%@) share for %@", newTLK, trustedPeer);
+ CKKSTLKShare* share = [CKKSTLKShare share:newTLK as:ckks.currentSelfPeers.currentSelf to:trustedPeer epoch:-1 poisoned:0 error:&error];
- [tlkShares addObject:share];
- [recordsToSave addObject: [share CKRecordWithZoneID: ckks.zoneID]];
- }
+ [tlkShares addObject:share];
+ [recordsToSave addObject: [share CKRecordWithZoneID: ckks.zoneID]];
}
// Use the spare operation trick to wait for the CKModifyRecordsOperation to complete
* @APPLE_LICENSE_HEADER_END@
*/
+#if OCTAGON
#import <Foundation/Foundation.h>
+NS_ASSUME_NONNULL_BEGIN
+
// There's terrible testing support for notify_post, but that's what our clients
// are listening for. Use this structure to mock out notification sending for testing.
@protocol CKKSNotifier
-+(void)post:(NSString*) notification;
++ (void)post:(NSString*)notification;
@end
@interface CKKSNotifyPostNotifier : NSObject <CKKSNotifier>
-+(void)post:(NSString*) notification;
++ (void)post:(NSString*)notification;
@end
+
+NS_ASSUME_NONNULL_END
+
+#endif //OCTAGON
* @APPLE_LICENSE_HEADER_END@
*/
+#if OCTAGON
+
#import "CKKSNotifier.h"
#import <notify.h>
#import <utilities/debugging.h>
}
@end
+
+#endif // OCTAGON
* @APPLE_LICENSE_HEADER_END@
*/
-#import "CKKSSQLDatabaseObject.h"
+#include <securityd/SecDbItem.h>
+#include <utilities/SecDb.h>
#import "CKKSItem.h"
#import "CKKSMirrorEntry.h"
-#include <utilities/SecDb.h>
-#include <securityd/SecDbItem.h>
+#import "CKKSSQLDatabaseObject.h"
#ifndef CKKSOutgoingQueueEntry_h
#define CKKSOutgoingQueueEntry_h
@interface CKKSOutgoingQueueEntry : CKKSSQLDatabaseObject
@property CKKSItem* item;
-@property NSString* uuid; // property access to underlying CKKSItem
+@property NSString* uuid; // property access to underlying CKKSItem
@property NSString* action;
@property NSString* state;
@property NSString* accessgroup;
-@property NSDate* waitUntil; // If non-null, the time at which this entry should be processed
+@property NSDate* waitUntil; // If non-null, the time at which this entry should be processed
-- (instancetype) initWithCKKSItem:(CKKSItem*) item
- action:(NSString*) action
- state:(NSString*) state
- waitUntil:(NSDate*) waitUntil
- accessGroup:(NSString*) accessgroup;
+- (instancetype)initWithCKKSItem:(CKKSItem*)item
+ action:(NSString*)action
+ state:(NSString*)state
+ waitUntil:(NSDate*)waitUntil
+ accessGroup:(NSString*)accessgroup;
-+ (instancetype) withItem: (SecDbItemRef) item action: (NSString*) action ckks:(CKKSKeychainView*)ckks error: (NSError * __autoreleasing *) error;
-+ (instancetype) fromDatabase: (NSString*) uuid state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabase: (NSString*) uuid zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabase: (NSString*) uuid state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (instancetype)withItem:(SecDbItemRef)item
+ action:(NSString*)action
+ ckks:(CKKSKeychainView*)ckks
+ error:(NSError* __autoreleasing*)error;
++ (instancetype)fromDatabase:(NSString*)uuid
+ state:(NSString*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabase:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabase:(NSString*)uuid
+ state:(NSString*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
-+ (NSArray<CKKSOutgoingQueueEntry*>*) fetch:(ssize_t) n state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
-+ (NSArray<CKKSOutgoingQueueEntry*>*) allInState: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (NSArray<CKKSOutgoingQueueEntry*>*)fetch:(ssize_t)n
+ state:(NSString*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
++ (NSArray<CKKSOutgoingQueueEntry*>*)allInState:(NSString*)state
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
-+ (NSDictionary<NSString*,NSNumber*>*)countsByState:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error;
++ (NSDictionary<NSString*, NSNumber*>*)countsByState:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error;
@end
- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
@end
-#endif // OCTAGON
+#endif // OCTAGON
#import "CKKSOutgoingQueueEntry.h"
#import "CKKSReencryptOutgoingItemsOperation.h"
#import "CKKSManifest.h"
-#import "CKKSAnalyticsLogger.h"
+#import "CKKSAnalyticsLogger.h"
#include <securityd/SecItemServer.h>
#include <securityd/SecItemDb.h>
NSError* error = nil;
+
// We only actually care about queue items in the 'new' state
NSArray<CKKSOutgoingQueueEntry*> * queueEntries = [CKKSOutgoingQueueEntry fetch:SecCKKSOutgoingQueueItemsAtOnce state: SecCKKSStateNew zoneID:ckks.zoneID error:&error];
return false;
}
+ //[CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventOutgoingQueue zone:ckks.zoneName count:[queueEntries count]];
+
ckksinfo("ckksoutgoing", ckks, "processing outgoing queue: %@", queueEntries);
NSMutableDictionary<CKRecordID*, CKRecord*>* recordsToSave = [[NSMutableDictionary alloc] init];
}
} else if ([oqe.action isEqualToString: SecCKKSActionDelete]) {
- [recordIDsToDelete addObject: [[CKRecordID alloc] initWithRecordName: oqe.item.uuid zoneID: ckks.zoneID]];
+ CKRecordID* recordIDToDelete = [[CKRecordID alloc] initWithRecordName: oqe.item.uuid zoneID: ckks.zoneID];
+ [recordIDsToDelete addObject: recordIDToDelete];
+ [oqesModified addObject: recordIDToDelete];
[ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateInFlight error:&error];
if(error) {
return;
}
+ //CKKSAnalyticsLogger* logger = [CKKSAnalyticsLogger logger];
+
[strongCKKS dispatchSync: ^bool{
if(ckerror) {
ckkserror("ckksoutgoing", strongCKKS, "error processing outgoing queue: %@", ckerror);
+ /*[logger logRecoverableError:ckerror
+ forEvent:CKKSEventProcessOutgoingQueue
+ inView:strongCKKS
+ withAttributes:NULL];*/
+
// Tell CKKS about any out-of-date records
[strongCKKS _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave];
if([recordID.recordName isEqualToString: SecCKKSKeyClassA] ||
[recordID.recordName isEqualToString: SecCKKSKeyClassC]) {
// The current key pointers have updated without our knowledge, so CloudKit failed this operation. Mark all records as 'needs reencryption' and kick that off.
- [strongSelf _onqueueModifyAllRecordsAsReencrypt: failedRecords.allKeys];
+ [strongSelf _onqueueModifyAllRecords:failedRecords.allKeys as:SecCKKSStateReencrypt];
// Note that _onqueueCKWriteFailed is responsible for kicking the key state machine, so we don't need to do it here.
// This will wait for the key hierarchy to become 'ready'
// OQEs should be placed back into the 'new' state, unless they've been overwritten by a new OQE. Other records should be ignored.
if([oqesModified containsObject:recordID]) {
- NSError* error = nil;
- CKKSOutgoingQueueEntry* inflightOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateInFlight zoneID:recordID.zoneID error:&error];
- CKKSOutgoingQueueEntry* newOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateNew zoneID:recordID.zoneID error:&error];
- if(error) {
- ckkserror("ckksoutgoing", strongCKKS, "Couldn't try to fetch an overwriting OQE: %@", error);
- }
-
- if(newOQE) {
- ckksnotice("ckksoutgoing", strongCKKS, "New modification has come in behind failed change for %@; dropping failed change", inflightOQE);
- [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateDeleted error:&error];
- if(error) {
- ckkserror("ckksoutgoing", strongCKKS, "Couldn't delete in-flight OQE: %@", error);
- }
- } else {
- [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateNew error:&error];
+ NSError* localerror = nil;
+ CKKSOutgoingQueueEntry* inflightOQE = [CKKSOutgoingQueueEntry tryFromDatabase:recordID.recordName state:SecCKKSStateInFlight zoneID:recordID.zoneID error:&localerror];
+ [strongCKKS _onqueueChangeOutgoingQueueEntry:inflightOQE toState:SecCKKSStateNew error:&localerror];
+ if(localerror) {
+ ckkserror("ckksoutgoing", strongCKKS, "Couldn't clean up outgoing queue entry: %@", localerror);
}
}
- } else if ([recordID.recordName hasPrefix:@"Manifest:-:"] || [recordID.recordName hasPrefix:@"ManifestLeafRecord:-:"]) {
- [[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"ManifestUpload" withAttributes:@{CKKSManifestZoneKey : strongCKKS.zoneID.zoneName, CKKSManifestSignerIDKey : strongCKKS.egoManifest.signerID, CKKSManifestGenCountKey : @(strongCKKS.egoManifest.generationCount)}];
} else {
// Some unknown error occurred on this record. If it's an OQE, move it to the error state.
ckkserror("ckksoutgoing", strongCKKS, "Unknown error on row: %@ %@", recordID, recordError);
}
}
}
+ } else {
+ // Some non-partial error occured. We should place all "inflight" OQEs back into the outgoing queue.
+ ckksnotice("ckks", strongCKKS, "Error is scary: putting all inflight OQEs back into state 'new'");
+ [strongSelf _onqueueModifyAllRecords:[oqesModified allObjects] as:SecCKKSStateNew];
}
strongSelf.error = error;
}
} else if ([record.recordType isEqualToString:SecCKRecordManifestType]) {
- [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"ManifestUpload"];
+
} else if (![record.recordType isEqualToString:SecCKRecordManifestLeafType]) {
ckkserror("ckksoutgoing", strongCKKS, "unknown record type in results: %@", record);
}
if(strongSelf.error) {
ckkserror("ckksoutgoing", strongCKKS, "Operation failed; rolling back: %@", strongSelf.error);
+ /*[logger logRecoverableError:strongSelf.error
+ forEvent:CKKSEventProcessOutgoingQueue
+ inView:strongCKKS
+ withAttributes:NULL];*/
return false;
+ } else {
+ //[logger logSuccessForEvent:CKKSEventProcessOutgoingQueue inView:strongCKKS];
}
return true;
}];
self.modifyRecordsOperation.savePolicy = CKRecordSaveIfServerRecordUnchanged;
self.modifyRecordsOperation.group = self.ckoperationGroup;
ckksnotice("ckksoutgoing", ckks, "Operation group is %@", self.ckoperationGroup);
+ ckksnotice("ckksoutgoing", ckks, "Beginning upload for %@ %@", recordsToSave.allValues, recordIDsToDelete);
self.modifyRecordsOperation.perRecordCompletionBlock = ^(CKRecord *record, NSError * _Nullable error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
__strong __typeof(strongSelf.ckks) blockCKKS = strongSelf.ckks;
if(!error) {
- ckksnotice("ckksoutgoing", blockCKKS, "Record upload successful for %@", record.recordID.recordName);
+ ckksnotice("ckksoutgoing", blockCKKS, "Record upload successful for %@ (%@)", record.recordID.recordName, record.recordChangeTag);
} else {
ckkserror("ckksoutgoing", blockCKKS, "error on row: %@ %@", error, record);
}
}
-- (void)_onqueueModifyAllRecordsAsReencrypt: (NSArray<CKRecordID*>*) recordIDs {
+- (void)_onqueueModifyAllRecords:(NSArray<CKRecordID*>*)recordIDs as:(CKKSItemState*)state {
CKKSKeychainView* ckks = self.ckks;
if(!ckks) {
ckkserror("ckksoutgoing", ckks, "no CKKS object");
// Nothing to do here. We need a whole key refetch and synchronize.
} else {
CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry fromDatabase:recordID.recordName state: SecCKKSStateInFlight zoneID:ckks.zoneID error:&error];
- [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:SecCKKSStateReencrypt error:&error];
+ [ckks _onqueueChangeOutgoingQueueEntry:oqe toState:state error:&error];
if(error) {
- ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as reencrypt: %@", recordID.recordName, error);
+ ckkserror("ckksoutgoing", ckks, "Couldn't set OQE %@ as %@: %@", recordID.recordName, state, error);
self.error = error;
}
count ++;
}
}
- SecADAddValueForScalarKey((__bridge CFStringRef) SecCKKSAggdItemReencryption, count);
+ if([state isEqualToString:SecCKKSStateReencrypt]) {
+ SecADAddValueForScalarKey((__bridge CFStringRef) SecCKKSAggdItemReencryption, count);
+ }
}
@end;
#import <Foundation/Foundation.h>
#import <SecurityFoundation/SFKey.h>
+NS_ASSUME_NONNULL_BEGIN
// ==== Peer protocols ====
@protocol CKKSPeer <NSObject>
@property (readonly) NSString* peerID;
-@property (readonly) SFECPublicKey* publicEncryptionKey;
-@property (readonly) SFECPublicKey* publicSigningKey;
+@property (nullable, readonly) SFECPublicKey* publicEncryptionKey;
+@property (nullable, readonly) SFECPublicKey* publicSigningKey;
// Not exactly isEqual, since this only compares peerID
- (bool)matchesPeer:(id<CKKSPeer>)peer;
@interface CKKSSelves : NSObject
@property id<CKKSSelfPeer> currentSelf;
-@property NSSet<id<CKKSSelfPeer>>* allSelves;
-- (instancetype)initWithCurrent:(id<CKKSSelfPeer>)selfPeer allSelves:(NSSet<id<CKKSSelfPeer>>*)allSelves;
+@property (nullable) NSSet<id<CKKSSelfPeer>>* allSelves;
+- (instancetype)initWithCurrent:(id<CKKSSelfPeer>)selfPeer allSelves:(NSSet<id<CKKSSelfPeer>>* _Nullable)allSelves;
@end
// ==== Peer handler protocols ====
@protocol CKKSPeerUpdateListener;
@protocol CKKSPeerProvider <NSObject>
-- (CKKSSelves*)fetchSelfPeers:(NSError* __autoreleasing *)error;
-- (NSSet<id<CKKSPeer>>*)fetchTrustedPeers:(NSError* __autoreleasing *)error;
+- (CKKSSelves* _Nullable)fetchSelfPeers:(NSError* _Nullable __autoreleasing* _Nullable)error;
+- (NSSet<id<CKKSPeer>>* _Nullable)fetchTrustedPeers:(NSError* _Nullable __autoreleasing* _Nullable)error;
// Trusted peers should include self peers
- (void)registerForPeerChangeUpdates:(id<CKKSPeerUpdateListener>)listener;
- (void)trustedPeerSetChanged;
@end
-
// These should be replaced by Octagon peers, when those exist
@interface CKKSSOSPeer : NSObject <CKKSPeer>
@property (readonly) NSString* peerID;
-@property (readonly) SFECPublicKey* publicEncryptionKey;
-@property (readonly) SFECPublicKey* publicSigningKey;
+@property (nullable, readonly) SFECPublicKey* publicEncryptionKey;
+@property (nullable, readonly) SFECPublicKey* publicSigningKey;
- (instancetype)initWithSOSPeerID:(NSString*)syncingPeerID
- encryptionPublicKey:(SFECPublicKey*)encryptionKey
- signingPublicKey:(SFECPublicKey*)signingKey;
+ encryptionPublicKey:(SFECPublicKey* _Nullable)encryptionKey
+ signingPublicKey:(SFECPublicKey* _Nullable)signingKey;
@end
@interface CKKSSOSSelfPeer : NSObject <CKKSPeer, CKKSSelfPeer>
signingKey:(SFECKeyPair*)signingKey;
@end
-#endif // OCTAGON
+NS_ASSUME_NONNULL_END
+#endif // OCTAGON
} else {
// Otherwise, something has gone horribly wrong. enter error state.
ckkserror("ckkskey", ckks, "CKKS claims %@ is not a valid TLK: %@", tlk, error);
- [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateError withError:[NSError errorWithDomain: @"securityd" code:0 userInfo:@{NSLocalizedDescriptionKey: @"invalid TLK from CloudKit", NSUnderlyingErrorKey: error}]];
+ NSError* newError = nil;
+ if(error) {
+ newError = [NSError errorWithDomain: @"securityd" code:0 userInfo:@{NSLocalizedDescriptionKey: @"invalid TLK from CloudKit", NSUnderlyingErrorKey: error}];
+ } else {
+ newError = [NSError errorWithDomain: @"securityd" code:0 userInfo:@{NSLocalizedDescriptionKey: @"invalid TLK from CloudKit (unknown error)"}];
+ }
+ [ckks _onqueueAdvanceKeyStateMachineToState:SecCKKSZoneKeyStateError withError:newError];
return true;
}
}
* @APPLE_LICENSE_HEADER_END@
*/
-#ifndef RateLimiter_h
-#define RateLimiter_h
-
#if OCTAGON
#import <Foundation/Foundation.h>
#import "CKKSOutgoingQueueEntry.h"
+NS_ASSUME_NONNULL_BEGIN
+
@interface CKKSRateLimiter : NSObject <NSSecureCoding>
-@property (readonly, nonnull) NSDictionary * config; // of NSString : NSNumber
+@property (readonly) NSDictionary* config; // of NSString : NSNumber
/*!
* @brief Find out whether outgoing items are okay to send.
* At badness 5 judge:at: has determined there is too much activity so the caller should hold off altogether. The limitTime object will indicate when
* this overloaded state will end.
*/
-- (int)judge:(CKKSOutgoingQueueEntry * _Nonnull const)entry
- at:(NSDate * _Nonnull)time
- limitTime:(NSDate * _Nonnull __autoreleasing * _Nonnull) limitTime;
+- (int)judge:(CKKSOutgoingQueueEntry* const)entry
+ at:(NSDate*)time
+ limitTime:(NSDate* _Nonnull __autoreleasing* _Nonnull)limitTime;
- (instancetype _Nullable)init;
-- (instancetype _Nullable)initWithCoder:(NSCoder * _Nullable)coder NS_DESIGNATED_INITIALIZER;
+- (instancetype _Nullable)initWithCoder:(NSCoder* _Nullable)coder NS_DESIGNATED_INITIALIZER;
- (NSUInteger)stateSize;
- (void)reset;
-- (NSString * _Nonnull)diagnostics;
+- (NSString*)diagnostics;
+ (BOOL)supportsSecureCoding;
@end
-#endif
+NS_ASSUME_NONNULL_END
#endif
};
@interface CKKSRateLimiter()
-@property (readwrite, nonnull) NSDictionary<NSString *, NSNumber *> *config;
+@property (readwrite) NSDictionary<NSString *, NSNumber *> *config;
@property NSMutableDictionary<NSString *, NSDate *> *buckets;
@property NSDate *overloadUntil;
#if !TARGET_OS_BRIDGE
#if OCTAGON
-#include <utilities/SecDb.h>
+#import <CloudKit/CloudKit.h>
#include <securityd/SecDbItem.h>
-
-#ifndef CKKSRecordHolder_h
-#define CKKSRecordHolder_h
-
+#include <utilities/SecDb.h>
#import "keychain/ckks/CKKSSQLDatabaseObject.h"
-#import <CloudKit/CloudKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
@class CKKSWrappedAESSIVKey;
// Helper class that includes a single encoded CKRecord
-@interface CKKSCKRecordHolder : CKKSSQLDatabaseObject {
-}
+@interface CKKSCKRecordHolder : CKKSSQLDatabaseObject
-- (instancetype)initWithCKRecord: (CKRecord*) record;
-- (instancetype)initWithCKRecordType: (NSString*) recordType encodedCKRecord: (NSData*) encodedCKRecord zoneID:(CKRecordZoneID*)zoneID;
+- (instancetype)initWithCKRecord:(CKRecord*)record;
+- (instancetype)initWithCKRecordType:(NSString*)recordType
+ encodedCKRecord:(NSData* _Nullable)encodedCKRecord
+ zoneID:(CKRecordZoneID*)zoneID;
@property (copy) CKRecordZoneID* zoneID;
@property (copy) NSString* ckRecordType;
-@property (copy) NSData* encodedCKRecord;
-@property (getter=storedCKRecord,setter=setStoredCKRecord:) CKRecord* storedCKRecord;
+@property (nullable, copy) NSData* encodedCKRecord;
+@property (nullable, getter=storedCKRecord, setter=setStoredCKRecord:) CKRecord* storedCKRecord;
-- (CKRecord*) CKRecordWithZoneID: (CKRecordZoneID*) zoneID;
+- (CKRecord*)CKRecordWithZoneID:(CKRecordZoneID*)zoneID;
// All of the following are virtual: you must override to use
-- (NSString*) CKRecordName;
-- (CKRecord*) updateCKRecord: (CKRecord*) record zoneID: (CKRecordZoneID*) zoneID;
-- (void) setFromCKRecord: (CKRecord*) record; // When you override this, make sure to call [setStoredCKRecord]
-- (bool) matchesCKRecord: (CKRecord*) record;
+- (NSString*)CKRecordName;
+- (CKRecord*)updateCKRecord:(CKRecord*)record zoneID:(CKRecordZoneID*)zoneID;
+- (void)setFromCKRecord:(CKRecord*)record; // When you override this, make sure to call [setStoredCKRecord]
+- (bool)matchesCKRecord:(CKRecord*)record;
@end
+NS_ASSUME_NONNULL_END
#endif
-#endif /* CKKSRecordHolder_h */
*/
#if OCTAGON
-#import <Foundation/Foundation.h>
#import <CloudKit/CloudKit.h>
+#import <Foundation/Foundation.h>
#import "keychain/ckks/CKKSGroupOperation.h"
@class CKKSKeychainView;
@end
-#endif // OCTAGON
-
+#endif // OCTAGON
#import "keychain/ckks/CKKSOutgoingQueueEntry.h"
#import "keychain/ckks/CKKSReencryptOutgoingItemsOperation.h"
#import "keychain/ckks/CKKSItemEncrypter.h"
+#import "keychain/ckks/CloudKitCategories.h"
#if OCTAGON
continue;
}
if(newOQE) {
- ckksinfo("ckksreencrypt", ckks, "Have a new OQE superceding %@ (%@), skipping", oqe, newOQE);
+ ckksnotice("ckksreencrypt", ckks, "Have a new OQE superceding %@ (%@), skipping", oqe, newOQE);
// Don't use the state transition here, either, since this item isn't really changing states
[oqe deleteFromDatabase:&error];
if(error) {
continue;
}
- ckksinfo("ckksreencrypt", ckks, "Reencrypting item %@", oqe);
+ ckksnotice("ckksreencrypt", ckks, "Reencrypting item %@", oqe);
NSDictionary* item = [CKKSItemEncrypter decryptItemToDictionary: oqe.item error:&error];
if(error) {
* @APPLE_LICENSE_HEADER_END@
*/
-#ifndef CKKSResultOperation_h
-#define CKKSResultOperation_h
+#if OCTAGON
#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>
// Very similar to addDependency, but:
// if the dependent operation has an error or is canceled, cancel this operation
-- (void)addSuccessDependency: (CKKSResultOperation*) operation;
+- (void)addSuccessDependency:(CKKSResultOperation*)operation;
- (void)addNullableSuccessDependency:(CKKSResultOperation*)operation;
// Call to check if you should run.
- (instancetype)timeout:(dispatch_time_t)timeout;
// Convenience constructor.
-+(instancetype)operationWithBlock:(void (^)(void))block;
-+(instancetype)named: (NSString*)name withBlock: (void(^)(void)) block;
++ (instancetype)operationWithBlock:(void (^)(void))block;
++ (instancetype)named:(NSString*)name withBlock:(void (^)(void))block;
++ (instancetype)named:(NSString*)name withBlockTakingSelf:(void(^)(CKKSResultOperation* op))block;
// Determine if all these operations were successful, and set this operation's result if not.
-- (bool)allSuccessful: (NSArray<CKKSResultOperation*>*) operations;
+- (bool)allSuccessful:(NSArray<CKKSResultOperation*>*)operations;
// Call this to prevent the timeout on this operation from occuring.
// Upon return, either this operation is cancelled, or the timeout will never fire.
--(void)invalidateTimeout;
+- (void)invalidateTimeout;
@end
-#endif // CKKSResultOperation_h
+#endif // OCTAGON
* @APPLE_LICENSE_HEADER_END@
*/
+#if OCTAGON
+
#import "keychain/ckks/CKKSResultOperation.h"
+#import "keychain/ckks/NSOperationCategories.h"
#import "keychain/ckks/CKKSCondition.h"
+#import "keychain/ckks/CloudKitCategories.h"
#include <utilities/debugging.h>
@interface CKKSResultOperation()
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeout), self.timeoutQueue, ^{
__strong __typeof(self) strongSelf = weakSelf;
if(strongSelf.timeoutCanOccur) {
- strongSelf.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultTimedOut userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Operation timed out waiting to start for [%@]", [self pendingDependenciesString:@""]]}];
+ strongSelf.error = [NSError errorWithDomain:CKKSResultErrorDomain
+ code:CKKSResultTimedOut
+ description:[NSString stringWithFormat:@"Operation(%@) timed out waiting to start for [%@]", [self selfname], [self pendingDependenciesString:@""]]];
strongSelf.timeoutCanOccur = false;
[strongSelf cancel];
}
bool finished = true; // all dependents must be finished
bool cancelled = false; // no dependents can be cancelled
bool failed = false; // no dependents can have failed
+ NSMutableArray<NSOperation*>* cancelledSuboperations = [NSMutableArray array];
for(CKKSResultOperation* op in operations) {
finished &= !!([op isFinished]);
cancelled |= !!([op isCancelled]);
failed |= (op.error != nil);
+ if([op isCancelled]) {
+ [cancelledSuboperations addObject:op];
+ }
+
// TODO: combine suberrors
if(op.error != nil) {
if([op.error.domain isEqual: CKKSResultErrorDomain] && op.error.code == CKKSResultSubresultError) {
result = finished && !( cancelled || failed );
if(!result && self.error == nil) {
- self.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultSubresultCancelled userInfo:nil];
+ self.error = [NSError errorWithDomain:CKKSResultErrorDomain code: CKKSResultSubresultCancelled userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Operation (%@) cancelled", cancelledSuboperations]}];
}
return result;
}
blockOp.name = name;
return blockOp;
}
+
++ (instancetype)named:(NSString*)name withBlockTakingSelf:(void(^)(CKKSResultOperation* op))block
+{
+ CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
+ __weak __typeof(op) weakOp = op;
+ [op addExecutionBlock:^{
+ __strong __typeof(op) strongOp = weakOp;
+ block(strongOp);
+ }];
+ op.name = name;
+ return op;
+}
@end
+
+#endif // OCTAGON
// For AES-SIV 512.
-#define CKKSKeySize (512/8)
-#define CKKSWrappedKeySize (CKKSKeySize+16)
+#define CKKSKeySize (512 / 8)
+#define CKKSWrappedKeySize (CKKSKeySize + 16)
-@interface CKKSBaseAESSIVKey : NSObject <NSCopying> {
- @package
- uint8_t key[CKKSWrappedKeySize]; // subclasses can use less than the whole buffer, and set key to be precise
+@interface CKKSBaseAESSIVKey : NSObject <NSCopying>
+{
+ @package
+ uint8_t key[CKKSWrappedKeySize]; // subclasses can use less than the whole buffer, and set key to be precise
size_t size;
}
- (instancetype)init;
-- (instancetype)initWithBytes:(uint8_t *)bytes len:(size_t)len;
+- (instancetype)initWithBytes:(uint8_t*)bytes len:(size_t)len;
- (void)zeroKey;
-- (instancetype)copyWithZone:(NSZone *)zone;
+- (instancetype)copyWithZone:(NSZone*)zone;
// Mostly for testing.
-- (instancetype)initWithBase64: (NSString*) base64bytes;
-- (BOOL)isEqual: (id) object;
+- (instancetype)initWithBase64:(NSString*)base64bytes;
+- (BOOL)isEqual:(id)object;
@end
@interface CKKSWrappedAESSIVKey : CKKSBaseAESSIVKey
-- (instancetype)initWithData: (NSData*) data;
+- (instancetype)initWithData:(NSData*)data;
- (NSData*)wrappedData;
-- (NSString*) base64WrappedKey;
+- (NSString*)base64WrappedKey;
@end
@interface CKKSAESSIVKey : CKKSBaseAESSIVKey
+ (instancetype)randomKey;
-- (CKKSWrappedAESSIVKey*)wrapAESKey: (CKKSAESSIVKey*) keyToWrap error: (NSError * __autoreleasing *) error;
-- (CKKSAESSIVKey*)unwrapAESKey: (CKKSWrappedAESSIVKey*) keyToUnwrap error: (NSError * __autoreleasing *) error;
+- (CKKSWrappedAESSIVKey*)wrapAESKey:(CKKSAESSIVKey*)keyToWrap error:(NSError* __autoreleasing*)error;
+- (CKKSAESSIVKey*)unwrapAESKey:(CKKSWrappedAESSIVKey*)keyToUnwrap error:(NSError* __autoreleasing*)error;
// Encrypt and decrypt data into buffers. Adds a nonce for ciphertext protection.
-- (NSData*)encryptData: (NSData*) plaintext authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error;
-- (NSData*)decryptData: (NSData*) ciphertext authenticatedData: (NSDictionary<NSString*, NSData*>*) ad error: (NSError * __autoreleasing *) error;
+- (NSData*)encryptData:(NSData*)plaintext
+ authenticatedData:(NSDictionary<NSString*, NSData*>*)ad
+ error:(NSError* __autoreleasing*)error;
+- (NSData*)decryptData:(NSData*)ciphertext
+ authenticatedData:(NSDictionary<NSString*, NSData*>*)ad
+ error:(NSError* __autoreleasing*)error;
@end
-#endif // OCTAGON
+#endif // OCTAGON
* @APPLE_LICENSE_HEADER_END@
*/
-#ifndef DatabaseObject_h
-#define DatabaseObject_h
-
-#include <utilities/SecDb.h>
#include <securityd/SecDbItem.h>
+#include <utilities/SecDb.h>
-#define CKKSNilToNSNull(obj) ({ id o = (obj); o ? o : [NSNull null]; })
-#define CKKSNSNullToNil(obj) ({ id o = (obj); ([o isEqual: [NSNull null]]) ? nil : o; })
-
-#define CKKSIsNull(x) ({ id y = (x); ((y == nil) || ([y isEqual: [NSNull null]])); })
+#define CKKSNilToNSNull(obj) \
+ ({ \
+ id o = (obj); \
+ o ? o : [NSNull null]; \
+ })
+#define CKKSNSNullToNil(obj) \
+ ({ \
+ id o = (obj); \
+ ([o isEqual:[NSNull null]]) ? nil : o; \
+ })
+
+#define CKKSIsNull(x) \
+ ({ \
+ id y = (x); \
+ ((y == nil) || ([y isEqual:[NSNull null]])); \
+ })
#define CKKSUnbase64NullableString(x) (!CKKSIsNull(x) ? [[NSData alloc] initWithBase64EncodedString:x options:0] : nil)
-@interface CKKSSQLDatabaseObject : NSObject <NSCopying> {
+NS_ASSUME_NONNULL_BEGIN
-}
+@interface CKKSSQLDatabaseObject : NSObject <NSCopying>
-@property (copy) NSDictionary<NSString*,NSString*>* originalSelfWhereClause;
+@property (copy) NSDictionary<NSString*, NSString*>* originalSelfWhereClause;
-- (bool) saveToDatabase: (NSError * __autoreleasing *) error;
-- (bool) saveToDatabaseWithConnection: (SecDbConnectionRef) conn error: (NSError * __autoreleasing *) error;
-- (bool) deleteFromDatabase: (NSError * __autoreleasing *) error;
-+ (bool) deleteAll: (NSError * __autoreleasing *) error;
+- (bool)saveToDatabase:(NSError* _Nullable __autoreleasing* _Nullable)error;
+- (bool)saveToDatabaseWithConnection:(SecDbConnectionRef _Nullable)conn
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
+- (bool)deleteFromDatabase:(NSError* _Nullable __autoreleasing* _Nullable)error;
++ (bool)deleteAll:(NSError* _Nullable __autoreleasing* _Nullable)error;
// Load the object from the database, and error if it doesn't exist
-+ (instancetype) fromDatabaseWhere: (NSDictionary*) whereDict error: (NSError * __autoreleasing *) error;
++ (instancetype _Nullable)fromDatabaseWhere:(NSDictionary*)whereDict error:(NSError* _Nullable __autoreleasing* _Nullable)error;
// Load the object from the database, and return nil if it doesn't exist
-+ (instancetype) tryFromDatabaseWhere: (NSDictionary*) whereDict error: (NSError * __autoreleasing *) error;
++ (instancetype _Nullable)tryFromDatabaseWhere:(NSDictionary*)whereDict
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
-+ (NSArray*) all: (NSError * __autoreleasing *) error;
-+ (NSArray*) allWhere: (NSDictionary*) whereDict error: (NSError * __autoreleasing *) error;
++ (NSArray*)all:(NSError* _Nullable __autoreleasing* _Nullable)error;
++ (NSArray*)allWhere:(NSDictionary* _Nullable)whereDict error:(NSError* _Nullable __autoreleasing* _Nullable)error;
// Like all() above, but with limits on how many will return
-+ (NSArray*)fetch:(size_t)count error: (NSError * __autoreleasing *) error;
-+ (NSArray*)fetch:(size_t)count where:(NSDictionary*)whereDict error: (NSError * __autoreleasing *) error;
-+ (NSArray*)fetch: (size_t)count where:(NSDictionary*)whereDict orderBy:(NSArray*) orderColumns error: (NSError * __autoreleasing *) error;
-
-
-+ (bool) saveToDatabaseTable: (NSString*) table row: (NSDictionary*) row connection: (SecDbConnectionRef) dbconn error: (NSError * __autoreleasing *) error;
-+ (bool) deleteFromTable: (NSString*) table where: (NSDictionary*) whereDict connection:(SecDbConnectionRef) dbconn error: (NSError * __autoreleasing *) error;
-
-+ (bool) queryDatabaseTable:(NSString*) table
- where:(NSDictionary*) whereDict
- columns:(NSArray*) names
- groupBy:(NSArray*) groupColumns
- orderBy:(NSArray*) orderColumns
- limit:(ssize_t)limit
- processRow:(void (^)(NSDictionary*)) processRow
- error:(NSError * __autoreleasing *) error;
-
-+ (bool)queryMaxValueForField:(NSString*)maxField inTable:(NSString*)table where:(NSDictionary*)whereDict columns:(NSArray*)names processRow:(void (^)(NSDictionary*))processRow;
++ (NSArray*)fetch:(size_t)count error:(NSError* _Nullable __autoreleasing* _Nullable)error;
++ (NSArray*)fetch:(size_t)count
+ where:(NSDictionary* _Nullable)whereDict
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
++ (NSArray*)fetch:(size_t)count
+ where:(NSDictionary* _Nullable)whereDict
+ orderBy:(NSArray* _Nullable)orderColumns
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
+
+
++ (bool)saveToDatabaseTable:(NSString*)table
+ row:(NSDictionary*)row
+ connection:(SecDbConnectionRef _Nullable)dbconn
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
++ (bool)deleteFromTable:(NSString*)table
+ where:(NSDictionary* _Nullable)whereDict
+ connection:(SecDbConnectionRef _Nullable)dbconn
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
+
++ (bool)queryDatabaseTable:(NSString*)table
+ where:(NSDictionary* _Nullable)whereDict
+ columns:(NSArray*)names
+ groupBy:(NSArray* _Nullable)groupColumns
+ orderBy:(NSArray* _Nullable)orderColumns
+ limit:(ssize_t)limit
+ processRow:(void (^)(NSDictionary*))processRow
+ error:(NSError* _Nullable __autoreleasing* _Nullable)error;
+
++ (bool)queryMaxValueForField:(NSString*)maxField
+ inTable:(NSString*)table
+ where:(NSDictionary* _Nullable)whereDict
+ columns:(NSArray*)names
+ processRow:(void (^)(NSDictionary*))processRow;
// Note: if you don't use the SQLDatabase methods of loading yourself,
// make sure you call this directly after loading.
-- (instancetype) memoizeOriginalSelfWhereClause;
+- (instancetype)memoizeOriginalSelfWhereClause;
#pragma mark - Subclasses must implement the following:
// Given a row from the database, make this object
-+ (instancetype) fromDatabaseRow: (NSDictionary*) row;
++ (instancetype _Nullable)fromDatabaseRow:(NSDictionary*)row;
// Return the columns, in order, that this row wants to fetch
-+ (NSArray<NSString*>*) sqlColumns;
++ (NSArray<NSString*>*)sqlColumns;
// Return the table name for objects of this class
-+ (NSString*) sqlTable;
++ (NSString*)sqlTable;
// Return the columns and values, in order, that this row wants to save
-- (NSDictionary<NSString*,NSString*>*) sqlValues;
+- (NSDictionary<NSString*, NSString*>*)sqlValues;
// Return a set of key-value pairs that will uniquely find This Row in the table
-- (NSDictionary<NSString*,NSString*>*) whereClauseToFindSelf;
+- (NSDictionary<NSString*, NSString*>*)whereClauseToFindSelf;
-- (instancetype)copyWithZone:(NSZone *)zone;
+//- (instancetype)copyWithZone:(NSZone* _Nullable)zone;
@end
// Helper class to use with where clauses
@interface CKKSSQLWhereObject : NSObject
@property NSString* sqlOp;
@property NSString* contents;
-- (instancetype) initWithOperation:(NSString*)op string: (NSString*) str;
-+ (instancetype) op:(NSString*)op string:(NSString*) str;
-+ (instancetype)op:(NSString*) op stringValue: (NSString*) str; // Will add single quotes around your value.
+- (instancetype)initWithOperation:(NSString*)op string:(NSString*)str;
++ (instancetype)op:(NSString*)op string:(NSString*)str;
++ (instancetype)op:(NSString*)op stringValue:(NSString*)str; // Will add single quotes around your value.
@end
-#endif /* DatabaseObject_h */
+NS_ASSUME_NONNULL_END
#import "CKKSSQLDatabaseObject.h"
#include <securityd/SecItemServer.h>
+#import "keychain/ckks/CKKS.h"
#import "CKKSKeychainView.h"
@implementation CKKSSQLDatabaseObject
return ret;
}
-+ (instancetype) tryFromDatabaseWhere: (NSDictionary*) whereDict error: (NSError * __autoreleasing *) error {
++ (instancetype _Nullable) tryFromDatabaseWhere: (NSDictionary*) whereDict error: (NSError * __autoreleasing *) error {
__block id ret = nil;
[CKKSSQLDatabaseObject queryDatabaseTable: [self sqlTable]
@end
-#endif // OCTAGON
+#endif // OCTAGON
#import "keychain/ckks/CKKSViewManager.h"
#import "keychain/ckks/CKKSManifest.h"
+#import "CKKSPowerCollection.h"
+
#include <securityd/SecItemSchema.h>
#include <securityd/SecItemServer.h>
#include <securityd/SecItemDb.h>
@interface CKKSScanLocalItemsOperation ()
@property CKOperationGroup* ckoperationGroup;
+@property (assign) NSUInteger processsedItems;
@end
@implementation CKKSScanLocalItemsOperation
__block CFErrorRef cferror = NULL;
__block NSError* error = nil;
__block bool newEntries = false;
-
+
// Must query per-class, so:
const SecDbSchema *newSchema = current_schema();
for (const SecDbClass *const *class = newSchema->classes; *class != NULL; class++) {
return SecDbItemQuery(q, NULL, dbt, &cferror, ^(SecDbItemRef item, bool *stop) {
ckksnotice("ckksscan", ckks, "scanning item: %@", item);
+ self.processsedItems += 1;
+
SecDbItemRef itemToSave = NULL;
// First check: is this a tombstone? If so, skip with prejudice.
// We don't care about the oqe state here, just that one exists
CKKSOutgoingQueueEntry* oqe = [CKKSOutgoingQueueEntry tryFromDatabase: uuid zoneID:ckks.zoneID error: &error];
if(oqe != nil) {
- ckksinfo("ckksscan", ckks, "Existing outgoing queue entry with UUID %@", uuid);
+ ckksnotice("ckksscan", ckks, "Existing outgoing queue entry with UUID %@", uuid);
// If its state is 'new', mark down that we've seen new entries that need processing
newEntries |= !![oqe.state isEqualToString: SecCKKSStateNew];
return;
continue;
}
}
-
+
+ //[CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventScanLocalItems zone:ckks.zoneName count:self.processsedItems];
+
if ([CKKSManifest shouldSyncManifests]) {
// TODO: this manifest needs to incorporate peer manifests
CKKSEgoManifest* manifest = [CKKSEgoManifest newManifestForZone:ckks.zoneName withItems:itemsForManifest peerManifestIDs:@[] currentItems:@{} error:&error];
[ckks processOutgoingQueue:self.ckoperationGroup];
}
+ ckksnotice("ckksscan", ckks, "Completed scan");
+ ckks.droppedItems = false;
return true;
}];
}
@end
-#endif // OCTAGON
-
+#endif // OCTAGON
#import "keychain/ckks/CKKSKey.h"
#import "keychain/ckks/CKKSPeer.h"
-#import <SecurityFoundation/SFKey.h>
#import <SecurityFoundation/SFEncryptionOperation.h>
+#import <SecurityFoundation/SFKey.h>
+
+NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, SecCKKSTLKShareVersion) {
SecCKKSTLKShareVersion0 = 0, // Signature is over all fields except (signature) and (receiverPublicKey)
- // Unknown fields in the CKRecord will be appended to the end, in sorted order based on column ID
+ // Unknown fields in the CKRecord will be appended to the end, in sorted order based on column ID
};
#define SecCKKSTLKShareCurrentVersion SecCKKSTLKShareVersion0
@property NSInteger epoch;
@property NSInteger poisoned;
-@property NSData* wrappedTLK;
-@property NSData* signature;
+@property (nullable) NSData* wrappedTLK;
+@property (nullable) NSData* signature;
--(instancetype)init NS_UNAVAILABLE;
+- (instancetype)init NS_UNAVAILABLE;
-- (CKKSKey*)recoverTLK:(id<CKKSSelfPeer>)recoverer
- trustedPeers:(NSSet<id<CKKSPeer>>*)peers
- error:(NSError* __autoreleasing *)error;
+- (CKKSKey* _Nullable)recoverTLK:(id<CKKSSelfPeer>)recoverer trustedPeers:(NSSet<id<CKKSPeer>>*)peers error:(NSError**)error;
-+ (CKKSTLKShare*)share:(CKKSKey*)key
- as:(id<CKKSSelfPeer>)sender
- to:(id<CKKSPeer>)receiver
- epoch:(NSInteger)epoch
- poisoned:(NSInteger)poisoned
- error:(NSError* __autoreleasing *)error;
++ (CKKSTLKShare* _Nullable)share:(CKKSKey*)key
+ as:(id<CKKSSelfPeer>)sender
+ to:(id<CKKSPeer>)receiver
+ epoch:(NSInteger)epoch
+ poisoned:(NSInteger)poisoned
+ error:(NSError**)error;
// Database loading
-+ (instancetype)fromDatabase:(NSString*)uuid
- receiverPeerID:(NSString*)receiverPeerID
- senderPeerID:(NSString*)senderPeerID
- zoneID:(CKRecordZoneID*)zoneID
- error:(NSError * __autoreleasing *)error;
-+ (instancetype)tryFromDatabase:(NSString*)uuid
- receiverPeerID:(NSString*)receiverPeerID
- senderPeerID:(NSString*)senderPeerID
- zoneID:(CKRecordZoneID*)zoneID
- error:(NSError * __autoreleasing *)error;
++ (instancetype _Nullable)fromDatabase:(NSString*)uuid
+ receiverPeerID:(NSString*)receiverPeerID
+ senderPeerID:(NSString*)senderPeerID
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError* __autoreleasing*)error;
++ (instancetype _Nullable)tryFromDatabase:(NSString*)uuid
+ receiverPeerID:(NSString*)receiverPeerID
+ senderPeerID:(NSString*)senderPeerID
+ zoneID:(CKRecordZoneID*)zoneID
+ error:(NSError**)error;
+ (NSArray<CKKSTLKShare*>*)allFor:(NSString*)receiverPeerID
keyUUID:(NSString*)uuid
zoneID:(CKRecordZoneID*)zoneID
- error:(NSError * __autoreleasing *)error;
-+ (NSArray<CKKSTLKShare*>*)allForUUID:(NSString*)uuid
- zoneID:(CKRecordZoneID*)zoneID
- error:(NSError * __autoreleasing *)error;
-+ (NSArray<CKKSTLKShare*>*)allInZone:(CKRecordZoneID*)zoneID
- error:(NSError * __autoreleasing *)error;
-+ (instancetype)tryFromDatabaseFromCKRecordID:(CKRecordID*)recordID
- error:(NSError * __autoreleasing *)error;
+ error:(NSError* __autoreleasing*)error;
++ (NSArray<CKKSTLKShare*>*)allForUUID:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError**)error;
++ (NSArray<CKKSTLKShare*>*)allInZone:(CKRecordZoneID*)zoneID error:(NSError**)error;
++ (instancetype _Nullable)tryFromDatabaseFromCKRecordID:(CKRecordID*)recordID error:(NSError**)error;
// Returns a prefix that all every CKKSTLKShare CKRecord will have
+ (NSString*)ckrecordPrefix;
// For tests
-- (CKKSKey*)unwrapUsing:(id<CKKSSelfPeer>)localPeer error:(NSError * __autoreleasing *)error;
-- (NSData*)signRecord:(SFECKeyPair*)signingKey error:(NSError* __autoreleasing *)error;
-- (bool)verifySignature:(NSData*)signature verifyingPeer:(id<CKKSPeer>)peer error:(NSError* __autoreleasing *)error;
+- (CKKSKey* _Nullable)unwrapUsing:(id<CKKSSelfPeer>)localPeer error:(NSError**)error;
+- (NSData* _Nullable)signRecord:(SFECKeyPair*)signingKey error:(NSError**)error;
+- (bool)verifySignature:(NSData*)signature verifyingPeer:(id<CKKSPeer>)peer error:(NSError**)error;
- (NSData*)dataForSigning;
@end
-#endif // OCTAGON
+NS_ASSUME_NONNULL_END
+
+#endif // OCTAGON
@property (weak) CKKSKeychainView* ckks;
- (instancetype)init NS_UNAVAILABLE;
-- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*) ckks
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks
currentPointer:(NSString*)identifier
oldItemUUID:(NSString*)oldItemUUID
oldItemHash:(NSData*)oldItemHash
ckoperationGroup:(CKOperationGroup*)ckoperationGroup;
@end
-#endif // OCTAGON
-
+#endif // OCTAGON
}
// Check if either item is currently in any sync queue, and fail if so
- NSArray* oqes = [CKKSOutgoingQueueEntry allUUIDs:&error];
- NSArray* iqes = [CKKSIncomingQueueEntry allUUIDs:&error];
+ NSArray* oqes = [CKKSOutgoingQueueEntry allUUIDs:ckks.zoneID error:&error];
+ NSArray* iqes = [CKKSIncomingQueueEntry allUUIDs:ckks.zoneID error:&error];
if([oqes containsObject:self.currentItemUUID] || [iqes containsObject:self.currentItemUUID]) {
error = [NSError errorWithDomain:CKKSErrorDomain
code:CKKSLocalItemChangePending
ckkserror("ckkscurrent", strongCKKS, "CloudKit returned an error: %@", ckerror);
strongSelf.error = ckerror;
- [ckks dispatchSync:^bool {
- return [ckks _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave];
+ [strongCKKS dispatchSync:^bool {
+ return [strongCKKS _onqueueCKWriteFailed:ckerror attemptedRecordsChanged:recordsToSave];
}];
[strongCKKS scheduleOperation: modifyComplete];
#if OCTAGON
-#import "keychain/ckks/CKKSGroupOperation.h"
-#import "keychain/ckks/CKKSDeviceStateEntry.h"
#import <Foundation/Foundation.h>
+#import "keychain/ckks/CKKSDeviceStateEntry.h"
+#import "keychain/ckks/CKKSGroupOperation.h"
@interface CKKSUpdateDeviceStateOperation : CKKSGroupOperation
@property (weak) CKKSKeychainView* ckks;
- (instancetype)init NS_UNAVAILABLE;
-- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks rateLimit:(bool)rateLimit ckoperationGroup:(CKOperationGroup*)group;
+- (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks
+ rateLimit:(bool)rateLimit
+ ckoperationGroup:(CKOperationGroup*)group;
@end
-#endif // OCTAGON
+#endif // OCTAGON
* @APPLE_LICENSE_HEADER_END@
*/
+
#import <Foundation/Foundation.h>
-#include <securityd/SecDbItem.h>
-#import "keychain/ckks/CKKS.h"
-#import "keychain/ckks/CKKSControlProtocol.h"
#if OCTAGON
-#import "keychain/ckks/CloudKitDependencies.h"
+
+#include <securityd/SecDbItem.h>
+#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSAPSReceiver.h"
#import "keychain/ckks/CKKSCKAccountStateTracker.h"
+#import "keychain/ckks/CKKSCondition.h"
+#import "keychain/ckks/CKKSControlProtocol.h"
#import "keychain/ckks/CKKSLockStateTracker.h"
-#import "keychain/ckks/CKKSRateLimiter.h"
#import "keychain/ckks/CKKSNotifier.h"
-#import "keychain/ckks/CKKSCondition.h"
#import "keychain/ckks/CKKSPeer.h"
-#endif
+#import "keychain/ckks/CKKSRateLimiter.h"
+#import "keychain/ckks/CloudKitDependencies.h"
+
+NS_ASSUME_NONNULL_BEGIN
@class CKKSKeychainView, CKKSRateLimiter;
-#if !OCTAGON
-@interface CKKSViewManager : NSObject
-#else
@interface CKKSViewManager : NSObject <CKKSControlProtocol, CKKSPeerProvider>
@property CKContainer* container;
@property CKKSRateLimiter* globalRateLimiter;
-// Set this and all newly-created zones will wait to do setup until it completes.
-// this gives you a bit more control than initializedNewZones above.
-@property NSOperation* zoneStartupDependency;
-
-- (instancetype)initCloudKitWithContainerName: (NSString*) containerName usePCS:(bool)usePCS;
-- (instancetype)initWithContainerName: (NSString*) containerName
- usePCS: (bool)usePCS
- fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
- fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
- queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
- modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
- modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
- apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
- nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass
- notifierClass: (Class<CKKSNotifier>) notifierClass
- setupHold:(NSOperation*) setupHold;
+- (instancetype)initCloudKitWithContainerName:(NSString*)containerName usePCS:(bool)usePCS;
+- (instancetype)initWithContainerName:(NSString*)containerName
+ usePCS:(bool)usePCS
+ fetchRecordZoneChangesOperationClass:(Class<CKKSFetchRecordZoneChangesOperation>)fetchRecordZoneChangesOperationClass
+ fetchRecordsOperationClass:(Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
+ queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
+ modifySubscriptionsOperationClass:(Class<CKKSModifySubscriptionsOperation>)modifySubscriptionsOperationClass
+ modifyRecordZonesOperationClass:(Class<CKKSModifyRecordZonesOperation>)modifyRecordZonesOperationClass
+ apsConnectionClass:(Class<CKKSAPSConnection>)apsConnectionClass
+ nsnotificationCenterClass:(Class<CKKSNSNotificationCenter>)nsnotificationCenterClass
+ notifierClass:(Class<CKKSNotifier>)notifierClass;
- (CKKSKeychainView*)findView:(NSString*)viewName;
- (CKKSKeychainView*)findOrCreateView:(NSString*)viewName;
+ (CKKSKeychainView*)findOrCreateView:(NSString*)viewName;
-- (void)setView: (CKKSKeychainView*) obj;
-- (void)clearView:(NSString*) viewName;
+- (void)setView:(CKKSKeychainView*)obj;
+- (void)clearView:(NSString*)viewName;
-- (NSDictionary<NSString *,NSString *>*)activeTLKs;
+- (NSDictionary<NSString*, NSString*>*)activeTLKs;
// Call this to bring zones up (and to do so automatically in the future)
- (void)initializeZones;
-- (NSString*)viewNameForItem: (SecDbItemRef) item;
+- (NSString*)viewNameForItem:(SecDbItemRef)item;
-- (void) handleKeychainEventDbConnection: (SecDbConnectionRef) dbconn source:(SecDbTransactionSource)txionSource added: (SecDbItemRef) added deleted: (SecDbItemRef) deleted;
+- (void)handleKeychainEventDbConnection:(SecDbConnectionRef)dbconn
+ source:(SecDbTransactionSource)txionSource
+ added:(SecDbItemRef _Nullable)added
+ deleted:(SecDbItemRef _Nullable)deleted;
--(void)setCurrentItemForAccessGroup:(SecDbItemRef)newItem
- hash:(NSData*)newItemSHA1
- accessGroup:(NSString*)accessGroup
- identifier:(NSString*)identifier
- viewHint:(NSString*)viewHint
- replacing:(SecDbItemRef)oldItem
- hash:(NSData*)oldItemSHA1
- complete:(void (^) (NSError* operror)) complete;
+- (void)setCurrentItemForAccessGroup:(SecDbItemRef)newItem
+ hash:(NSData*)newItemSHA1
+ accessGroup:(NSString*)accessGroup
+ identifier:(NSString*)identifier
+ viewHint:(NSString*)viewHint
+ replacing:(SecDbItemRef _Nullable)oldItem
+ hash:(NSData* _Nullable)oldItemSHA1
+ complete:(void (^)(NSError* operror))complete;
--(void)getCurrentItemForAccessGroup:(NSString*)accessGroup
- identifier:(NSString*)identifier
- viewHint:(NSString*)viewHint
- fetchCloudValue:(bool)fetchCloudValue
- complete:(void (^) (NSString* uuid, NSError* operror)) complete;
+- (void)getCurrentItemForAccessGroup:(NSString*)accessGroup
+ identifier:(NSString*)identifier
+ viewHint:(NSString*)viewHint
+ fetchCloudValue:(bool)fetchCloudValue
+ complete:(void (^)(NSString* uuid, NSError* operror))complete;
-- (NSString*)viewNameForAttributes: (NSDictionary*) item;
+- (NSString*)viewNameForAttributes:(NSDictionary*)item;
-- (void)registerSyncStatusCallback: (NSString*) uuid callback: (SecBoolNSErrorCallback) callback;
+- (void)registerSyncStatusCallback:(NSString*)uuid callback:(SecBoolNSErrorCallback)callback;
// Cancels pending operations owned by this view manager
- (void)cancelPendingOperations;
// Use these to acquire (and set) the singleton
-+ (instancetype) manager;
-+ (instancetype) resetManager: (bool) reset setTo: (CKKSViewManager*) obj;
++ (instancetype)manager;
++ (instancetype _Nullable)resetManager:(bool)reset setTo:(CKKSViewManager* _Nullable)obj;
// Called by XPC every 24 hours
--(void)xpc24HrNotification;
+- (void)xpc24HrNotification;
/* Interface to CCKS control channel */
- (xpc_endpoint_t)xpcControlEndpoint;
- (CKKSKeychainView*)restartZone:(NSString*)viewName;
// Returns the viewList for a CKKSViewManager
--(NSSet*)viewList;
+- (NSSet*)viewList;
// Notify sbd to re-backup.
--(void)notifyNewTLKsInKeychain;
--(void)syncBackupAndNotifyAboutSync;
+- (void)notifyNewTLKsInKeychain;
+- (void)syncBackupAndNotifyAboutSync;
// Fetch peers from SOS
-- (CKKSSelves*)fetchSelfPeers:(NSError* __autoreleasing *)error;
-- (NSSet<id<CKKSPeer>>*)fetchTrustedPeers:(NSError* __autoreleasing *)error;
+- (CKKSSelves* _Nullable)fetchSelfPeers:(NSError* __autoreleasing*)error;
+- (NSSet<id<CKKSPeer>>* _Nullable)fetchTrustedPeers:(NSError* __autoreleasing*)error;
- (void)sendSelfPeerChangedUpdate;
- (void)sendTrustedPeerSetChangedUpdate;
-#endif // OCTAGON
@end
+NS_ASSUME_NONNULL_END
+
+#else
+@interface CKKSViewManager : NSObject
+@end
+#endif // OCTAGON
#import "keychain/ckks/CKKSNotifier.h"
#import "keychain/ckks/CKKSCondition.h"
#import "keychain/ckks/CloudKitCategories.h"
-#import "CKKSAnalyticsLogger.h"
#import "SecEntitlements.h"
#import <SecurityFoundation/SFKey.h>
#import <SecurityFoundation/SFKey_Private.h>
+
+#import "CKKSAnalyticsLogger.h"
#endif
@interface CKKSViewManager () <NSXPCListenerDelegate>
modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
apsConnectionClass:[APSConnection class]
nsnotificationCenterClass:[NSNotificationCenter class]
- notifierClass:[CKKSNotifyPostNotifier class]
- setupHold:nil];
+ notifierClass:[CKKSNotifyPostNotifier class]];
}
- (instancetype)initWithContainerName: (NSString*) containerName
- usePCS:(bool)usePCS
+ usePCS: (bool)usePCS
fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
- queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
+ queryOperationClass: (Class<CKKSQueryOperation>)queryOperationClass
modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass
nsnotificationCenterClass: (Class<CKKSNSNotificationCenter>) nsnotificationCenterClass
notifierClass: (Class<CKKSNotifier>) notifierClass
- setupHold: (NSOperation*) setupHold {
+{
if(self = [super init]) {
_fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass;
_fetchRecordsOperationClass = fetchRecordsOperationClass;
_views = [[NSMutableDictionary alloc] init];
_pendingSyncCallbacks = [[NSMutableDictionary alloc] init];
- _zoneStartupDependency = setupHold;
_initializeNewZones = false;
_completedSecCKKSInitialize = [[CKKSCondition alloc] init];
apsConnectionClass: self.apsConnectionClass
notifierClass: self.notifierClass];
- if(self.zoneStartupDependency) {
- [self.views[viewName].zoneSetupOperation addDependency: self.zoneStartupDependency];
- }
-
if(self.initializeNewZones) {
[self.views[viewName] initializeZone];
}
reply(@{});
}
-- (void)rpcResetLocal:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
+- (NSArray<CKKSKeychainView*>*)views:(NSString*)viewName operation:(NSString*)opName error:(NSError**)error
+{
NSArray* actualViews = nil;
- if(viewName) {
- secnotice("ckksreset", "Received a local reset RPC for zone %@", viewName);
- CKKSKeychainView* view = self.views[viewName];
-
- if(!view) {
- secerror("ckks: Zone %@ does not exist!", viewName);
- reply([NSError errorWithDomain:@"securityd"
- code:kSOSCCNoSuchView
- userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}]);
- return;
- }
+ @synchronized(self.views) {
+ if(viewName) {
+ secnotice("ckks", "Received a %@ request for zone %@", opName, viewName);
+ CKKSKeychainView* view = self.views[viewName];
+
+ if(!view) {
+ if(error) {
+ *error = [NSError errorWithDomain:CKKSErrorDomain
+ code:CKKSNoSuchView
+ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}];
+ }
+ return nil;
+ }
- actualViews = @[view];
- } else {
- secnotice("ckksreset", "Received a local reset RPC for all zones");
- @synchronized(self.views) {
- // Can't safely iterate a mutable collection, so copy it.
+ actualViews = @[view];
+ } else {
+ secnotice("ckks", "Received a %@ request for all zones", opName);
actualViews = [self.views.allValues copy];
}
}
+ return actualViews;
+}
+
+- (void)rpcResetLocal:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
+ NSError* localError = nil;
+ NSArray* actualViews = [self views:viewName operation:@"local reset" error:&localError];
+ if(localError) {
+ secerror("ckks: Error getting view %@: %@", viewName, localError);
+ reply(localError);
+ return;
+ }
CKKSResultOperation* op = [CKKSResultOperation named:@"local-reset-zones-waiter" withBlock:^{}];
}
- (void)rpcResetCloudKit:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
- NSArray* actualViews = nil;
- if(viewName) {
- secnotice("ckksreset", "Received a cloudkit reset RPC for zone %@", viewName);
- CKKSKeychainView* view = self.views[viewName];
-
- if(!view) {
- secerror("ckks: Zone %@ does not exist!", viewName);
- reply([NSError errorWithDomain:@"securityd"
- code:kSOSCCNoSuchView
- userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat: @"No view for '%@'", viewName]}]);
- return;
- }
-
- actualViews = @[view];
- } else {
- secnotice("ckksreset", "Received a cloudkit reset RPC for all zones");
- @synchronized(self.views) {
- // Can't safely iterate a mutable collection, so copy it.
- actualViews = [self.views.allValues copy];
- }
+ NSError* localError = nil;
+ NSArray* actualViews = [self views:viewName operation:@"CloudKit reset" error:&localError];
+ if(localError) {
+ secerror("ckks: Error getting view %@: %@", viewName, localError);
+ reply(localError);
+ return;
}
CKKSResultOperation* op = [CKKSResultOperation named:@"cloudkit-reset-zones-waiter" withBlock:^{}];
}
- (void)rpcResync:(NSString*)viewName reply: (void(^)(NSError* result)) reply {
- secnotice("ckksresync", "Received a resync RPC for zone %@. Beginning resync...", viewName);
-
- NSArray* actualViews = nil;
- if(viewName) {
- secnotice("ckks", "Received a resync RPC for zone %@", viewName);
- CKKSKeychainView* view = self.views[viewName];
-
- if(!view) {
- secerror("ckks: Zone %@ does not exist!", viewName);
- reply(nil);
- return;
- }
-
- actualViews = @[view];
-
- } else {
- @synchronized(self.views) {
- // Can't safely iterate a mutable collection, so copy it.
- actualViews = [self.views.allValues copy];
- }
+ NSError* localError = nil;
+ NSArray* actualViews = [self views:viewName operation:@"CloudKit resync" error:&localError];
+ if(localError) {
+ secerror("ckks: Error getting view %@: %@", viewName, localError);
+ reply(localError);
+ return;
}
CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
- op.name = @"rpc-resync";
+ op.name = @"rpc-resync-cloudkit";
__weak __typeof(op) weakOp = op;
[op addExecutionBlock:^{
__strong __typeof(op) strongOp = weakOp;
- secnotice("ckks", "Ending rsync rpc with %@", strongOp.error);
+ secnotice("ckks", "Ending rsync-CloudKit rpc with %@", strongOp.error);
}];
for(CKKSKeychainView* view in actualViews) {
- ckksnotice("ckksresync", view, "Beginning resync for %@", view);
+ ckksnotice("ckksresync", view, "Beginning resync (CloudKit) for %@", view);
CKKSSynchronizeOperation* resyncOp = [view resyncWithCloud];
[op addSuccessDependency:resyncOp];
reply(CKXPCSuitableError(op.error));
}
+- (void)rpcResyncLocal:(NSString*)viewName reply:(void(^)(NSError* result))reply {
+ NSError* localError = nil;
+ NSArray* actualViews = [self views:viewName operation:@"local resync" error:&localError];
+ if(localError) {
+ secerror("ckks: Error getting view %@: %@", viewName, localError);
+ reply(localError);
+ return;
+ }
+
+ CKKSResultOperation* op = [[CKKSResultOperation alloc] init];
+ op.name = @"rpc-resync-local";
+ __weak __typeof(op) weakOp = op;
+ [op addExecutionBlock:^{
+ __strong __typeof(op) strongOp = weakOp;
+ secnotice("ckks", "Ending rsync-local rpc with %@", strongOp.error);
+ reply(CKXPCSuitableError(strongOp.error));
+ }];
+
+ for(CKKSKeychainView* view in actualViews) {
+ ckksnotice("ckksresync", view, "Beginning resync (local) for %@", view);
+
+ CKKSLocalSynchronizeOperation* resyncOp = [view resyncLocal];
+ [op addSuccessDependency:resyncOp];
+ }
+
+ [op timeout:120*NSEC_PER_SEC];
+}
+
- (void)rpcStatus: (NSString*)viewName reply: (void(^)(NSArray<NSDictionary*>* result, NSError* error)) reply {
NSMutableArray* a = [[NSMutableArray alloc] init];
SecKeyRef cfOctagonEncryptionKey = SOSPeerInfoCopyOctagonEncryptionPublicKey(sosPeerInfoRef, &cfPeerError);
if(cfPeerError) {
- secerror("ckkspeer: error fetching octagon keys for peer: %@ %@", sosPeerInfoRef, cfPeerError);
+ // Don't log non-debug for -50; it almost always just means this peer didn't have octagon keys
+ if(!(CFEqualSafe(CFErrorGetDomain(cfPeerError), kCFErrorDomainOSStatus) && (CFErrorGetCode(cfPeerError) == errSecParam))) {
+ secerror("ckkspeer: error fetching octagon keys for peer: %@ %@", sosPeerInfoRef, cfPeerError);
+ } else {
+ secinfo("ckkspeer", "Peer doesn't have Octagon keys, but this is expected: %@", cfPeerError);
+ }
} else {
SFECPublicKey* signingPublicKey = cfOctagonSigningKey ? [[SFECPublicKey alloc] initWithSecKey:cfOctagonSigningKey] : nil;
SFECPublicKey* encryptionPublicKey = cfOctagonEncryptionKey ? [[SFECPublicKey alloc] initWithSecKey:cfOctagonEncryptionKey] : nil;
* @APPLE_LICENSE_HEADER_END@
*/
-#ifndef CKKSZone_h
-#define CKKSZone_h
-
#import <Foundation/Foundation.h>
#if OCTAGON
-#import "keychain/ckks/CloudKitDependencies.h"
#import "keychain/ckks/CKKSCKAccountStateTracker.h"
-#endif
+#import "keychain/ckks/CloudKitDependencies.h"
-#if OCTAGON
-@interface CKKSZone : NSObject<CKKSZoneUpdateReceiver, CKKSAccountStateListener> {
+NS_ASSUME_NONNULL_BEGIN
+
+@interface CKKSZone : NSObject <CKKSZoneUpdateReceiver, CKKSAccountStateListener>
+{
CKContainer* _container;
CKDatabase* _database;
CKRecordZone* _zone;
}
-#else
-@interface CKKSZone : NSObject {
-}
-#endif
@property (readonly) NSString* zoneName;
-@property bool setupStarted;
-@property bool setupComplete;
@property CKKSGroupOperation* zoneSetupOperation;
@property bool zoneCreated;
@property bool zoneSubscribed;
-@property NSError* zoneCreatedError;
-@property NSError* zoneSubscribedError;
+@property (nullable) NSError* zoneCreatedError;
+@property (nullable) NSError* zoneSubscribedError;
+
+// True if this zone object has been halted. Halted zones will never recover.
+@property (readonly) bool halted;
-#if OCTAGON
@property CKKSAccountStatus accountStatus;
@property (readonly) CKContainer* container;
@property dispatch_queue_t queue;
-- (instancetype)initWithContainer: (CKContainer*) container
- zoneName: (NSString*) zoneName
- accountTracker:(CKKSCKAccountStateTracker*) tracker
- fetchRecordZoneChangesOperationClass: (Class<CKKSFetchRecordZoneChangesOperation>) fetchRecordZoneChangesOperationClass
- fetchRecordsOperationClass: (Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
- queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
- modifySubscriptionsOperationClass: (Class<CKKSModifySubscriptionsOperation>) modifySubscriptionsOperationClass
- modifyRecordZonesOperationClass: (Class<CKKSModifyRecordZonesOperation>) modifyRecordZonesOperationClass
- apsConnectionClass: (Class<CKKSAPSConnection>) apsConnectionClass;
-
+- (instancetype)initWithContainer:(CKContainer*)container
+ zoneName:(NSString*)zoneName
+ accountTracker:(CKKSCKAccountStateTracker*)tracker
+ fetchRecordZoneChangesOperationClass:(Class<CKKSFetchRecordZoneChangesOperation>)fetchRecordZoneChangesOperationClass
+ fetchRecordsOperationClass:(Class<CKKSFetchRecordsOperation>)fetchRecordsOperationClass
+ queryOperationClass:(Class<CKKSQueryOperation>)queryOperationClass
+ modifySubscriptionsOperationClass:(Class<CKKSModifySubscriptionsOperation>)modifySubscriptionsOperationClass
+ modifyRecordZonesOperationClass:(Class<CKKSModifyRecordZonesOperation>)modifyRecordZonesOperationClass
+ apsConnectionClass:(Class<CKKSAPSConnection>)apsConnectionClass;
-- (NSOperation*) createSetupOperation: (bool) zoneCreated zoneSubscribed: (bool) zoneSubscribed;
-- (CKKSResultOperation*) beginResetCloudKitZoneOperation;
+- (CKKSResultOperation* _Nullable)beginResetCloudKitZoneOperation;
// Called when CloudKit notifies us that we just logged in.
// That is, if we transition from any state to CKAccountStatusAvailable.
-// This will be called under the protection of dispatchSync
+// This will be called under the protection of dispatchSync.
+// This is a no-op; you should intercept this call and call handleCKLogin:zoneSubscribed:
+// with the appropriate state
- (void)handleCKLogin;
+// Actually start a cloudkit login. Pass in whether you believe this zone has been created and if this device has
+// subscribed to this zone on the server.
+- (NSOperation* _Nullable)handleCKLogin:(bool)zoneCreated zoneSubscribed:(bool)zoneSubscribed;
+
// Called when CloudKit notifies us that we just logged out.
// i.e. we transition from CKAccountStatusAvailable to any other state.
// This will be called under the protection of dispatchSync
- (void)handleCKLogout;
+// Call this when you're ready for this zone to kick off operations
+// based on iCloud account status
+- (void)initializeZone;
+
// Cancels all operations (no matter what they are).
- (void)cancelAllOperations;
+// Reissues the call
+- (void)restartCurrentAccountStateOperation;
// Schedules this operation for execution (if the CloudKit account exists)
-- (bool)scheduleOperation: (NSOperation*) op;
+- (bool)scheduleOperation:(NSOperation*)op;
// Use this to schedule an operation handling account status (cleaning up after logout, etc.).
-- (bool)scheduleAccountStatusOperation: (NSOperation*) op;
+- (bool)scheduleAccountStatusOperation:(NSOperation*)op;
// Schedules this operation for execution, and doesn't do any dependency magic
// This should _only_ be used if you want to run something even if the CloudKit account is logged out
- (void)waitUntilAllOperationsAreFinished;
// Use this for testing, to only wait for a certain type of operation to finish.
-- (void)waitForOperationsOfClass:(Class) operationClass;
+- (void)waitForOperationsOfClass:(Class)operationClass;
// If this object wants to do anything that needs synchronization, use this.
-- (void) dispatchSync: (bool (^)(void)) block;
+// If this object has had -halt called, this block will never fire.
+- (void)dispatchSync:(bool (^)(void))block;
+
+// Call this to halt everything this zone is doing. This object will never recover. Use for testing.
+- (void)halt;
// Call this to reset this object's setup, so you can call createSetupOperation again.
- (void)resetSetup;
-#endif
@end
-#endif /* CKKSZone_h */
+NS_ASSUME_NONNULL_END
+#endif // OCTAGON
#import "keychain/ckks/CKKSCKAccountStateTracker.h"
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
-#endif
#import "CKKSKeychainView.h"
#import "CKKSZone.h"
#include <utilities/debugging.h>
@interface CKKSZone()
-#if OCTAGON
@property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneCreationOperation;
@property CKDatabaseOperation<CKKSModifyRecordZonesOperation>* zoneDeletionOperation;
@property CKDatabaseOperation<CKKSModifySubscriptionsOperation>* zoneSubscriptionOperation;
-@property bool acceptingNewOperations;
@property NSOperationQueue* operationQueue;
@property NSOperation* accountLoggedInDependency;
@property NSHashTable<NSOperation*>* accountOperations;
-#endif
+
+// Make writable
+@property bool halted;
@end
@implementation CKKSZone
-#if OCTAGON
-
- (instancetype)initWithContainer: (CKContainer*) container
zoneName: (NSString*) zoneName
accountTracker:(CKKSCKAccountStateTracker*) tracker
_zoneName = zoneName;
_accountTracker = tracker;
+ _halted = false;
+
_database = [_container privateCloudDatabase];
_zone = [[CKRecordZone alloc] initWithZoneID: [[CKRecordZoneID alloc] initWithZoneName:zoneName ownerName:CKCurrentUserDefaultName]];
- // Every subclass must set up call beginSetup at least once.
_accountStatus = CKKSAccountStatusUnknown;
- [self resetSetup];
+
+ __weak __typeof(self) weakSelf = self;
+ self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
+ ckksnotice("ckkszone", weakSelf, "CloudKit account logged in.");
+ }];
+ self.accountLoggedInDependency.name = @"account-logged-in-dependency";
_accountOperations = [NSHashTable weakObjectsHashTable];
_queue = dispatch_queue_create([[NSString stringWithFormat:@"CKKSQueue.%@.zone.%@", container.containerIdentifier, zoneName] UTF8String], DISPATCH_QUEUE_SERIAL);
_operationQueue = [[NSOperationQueue alloc] init];
- _acceptingNewOperations = true;
}
return self;
}
-// Initialize this object so that we can call beginSetup again
-- (void)resetSetup {
- self.setupStarted = false;
- self.setupComplete = false;
-
- if([self.zoneSetupOperation isPending]) {
- // Nothing to do here: there's already an existing zoneSetupOperation
- } else {
- self.zoneSetupOperation = [[CKKSGroupOperation alloc] init];
- self.zoneSetupOperation.name = @"zone-setup-operation";
- }
-
- if([self.accountLoggedInDependency isPending]) {
- // Nothing to do here: there's already an existing accountLoggedInDependency
- } else {
- __weak __typeof(self) weakSelf = self;
- self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
- ckksnotice("ckkszone", weakSelf, "CloudKit account logged in.");
- }];
- self.accountLoggedInDependency.name = @"account-logged-in-dependency";
- }
+- (void)initializeZone {
+ [self.accountTracker notifyOnAccountStatusChange:self];
+}
+- (void)resetSetup {
self.zoneCreated = false;
self.zoneSubscribed = false;
self.zoneCreatedError = nil;
}
--(void)ckAccountStatusChange: (CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus {
-
- // dispatch this on a serial queue, so we get each transition in order
- [self dispatchSync: ^bool {
- ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
- self.zoneID.zoneName,
- [CKKSCKAccountStateTracker stringFromAccountStatus: self.accountStatus],
- [CKKSCKAccountStateTracker stringFromAccountStatus: currentStatus]);
- CKKSAccountStatus oldStatus = self.accountStatus;
- self.accountStatus = currentStatus;
-
- switch(currentStatus) {
- case CKKSAccountStatusAvailable: {
-
- ckksinfo("ckkszone", self, "logging in while setup started: %d and complete: %d", self.setupStarted, self.setupComplete);
-
- // This is only a login if we're not in the middle of setup, and the previous state was not logged in
- if(!(self.setupStarted ^ self.setupComplete) && oldStatus != CKKSAccountStatusAvailable) {
- [self resetSetup];
- [self handleCKLogin];
- }
+-(void)ckAccountStatusChange:(CKKSAccountStatus)oldStatus to:(CKKSAccountStatus)currentStatus {
+ ckksnotice("ckkszone", self, "%@ Received notification of CloudKit account status change, moving from %@ to %@",
+ self.zoneID.zoneName,
+ [CKKSCKAccountStateTracker stringFromAccountStatus: oldStatus],
+ [CKKSCKAccountStateTracker stringFromAccountStatus: currentStatus]);
- if(self.accountLoggedInDependency) {
- [self.operationQueue addOperation:self.accountLoggedInDependency];
- self.accountLoggedInDependency = nil;
- };
- }
+ __weak __typeof(self) weakSelf = self;
+ switch(currentStatus) {
+ case CKKSAccountStatusAvailable: {
+ ckksnotice("ckkszone", self, "Logged into iCloud.");
+ [self handleCKLogin];
+
+ if(self.accountLoggedInDependency) {
+ [self.operationQueue addOperation:self.accountLoggedInDependency];
+ self.accountLoggedInDependency = nil;
+ };
+ }
break;
- case CKKSAccountStatusNoAccount: {
- ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
+ case CKKSAccountStatusNoAccount: {
+ ckksnotice("ckkszone", self, "Logging out of iCloud. Shutting down.");
- self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
- ckksnotice("ckkszone", self, "CloudKit account logged in again.");
- }];
- self.accountLoggedInDependency.name = @"account-logged-in-dependency";
+ self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
+ ckksnotice("ckkszone", weakSelf, "CloudKit account logged in again.");
+ }];
+ self.accountLoggedInDependency.name = @"account-logged-in-dependency";
- [self.operationQueue cancelAllOperations];
- [self handleCKLogout];
-
- // now we're in a logged out state. Optimistically prepare for a log in!
- [self resetSetup];
- }
+ [self handleCKLogout];
+ }
break;
- case CKKSAccountStatusUnknown: {
- // We really don't expect to receive this as a notification, but, okay!
- ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
+ case CKKSAccountStatusUnknown: {
+ // We really don't expect to receive this as a notification, but, okay!
+ ckksnotice("ckkszone", self, "Account status has become undetermined. Pausing for %@", self.zoneID.zoneName);
- self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
- ckksnotice("ckkszone", self, "CloudKit account restored from 'unknown'.");
- }];
- self.accountLoggedInDependency.name = @"account-logged-in-dependency";
+ self.accountLoggedInDependency = [NSBlockOperation blockOperationWithBlock:^{
+ ckksnotice("ckkszone", weakSelf, "CloudKit account restored from 'unknown'.");
+ }];
+ self.accountLoggedInDependency.name = @"account-logged-in-dependency";
- [self.operationQueue cancelAllOperations];
- [self resetSetup];
- }
- break;
+ [self handleCKLogout];
}
+ break;
+ }
+}
- return true;
- }];
+- (void)restartCurrentAccountStateOperation {
+ __weak __typeof(self) weakSelf = self;
+ dispatch_async(self.queue, ^{
+ __strong __typeof(self) strongSelf = weakSelf;
+ ckksnotice("ckksaccount", strongSelf, "Restarting account in state %@", [CKKSCKAccountStateTracker stringFromAccountStatus:strongSelf.accountStatus]);
+ [strongSelf ckAccountStatusChange:strongSelf.accountStatus to:strongSelf.accountStatus];
+ });
}
-- (NSOperation*) createSetupOperation: (bool) zoneCreated zoneSubscribed: (bool) zoneSubscribed {
+- (NSOperation*)handleCKLogin:(bool)zoneCreated zoneSubscribed:(bool)zoneSubscribed {
if(!SecCKKSIsEnabled()) {
ckksinfo("ckkszone", self, "Skipping CloudKit registration due to disabled CKKS");
return nil;
}
// If we've already started set up, skip doing it again.
- if(self.setupStarted) {
- ckksinfo("ckkszone", self, "skipping startup: it's already started");
+ if([self.zoneSetupOperation isPending] || [self.zoneSetupOperation isExecuting]) {
+ ckksnotice("ckkszone", self, "skipping startup: it's already started");
return self.zoneSetupOperation;
}
- if(self.zoneSetupOperation == nil) {
- ckkserror("ckkszone", self, "trying to set up but the setup operation is gone; what happened?");
- return nil;
- }
+ self.zoneSetupOperation = [[CKKSGroupOperation alloc] init];
+ self.zoneSetupOperation.name = [NSString stringWithFormat:@"zone-setup-operation-%@", self.zoneName];
self.zoneCreated = zoneCreated;
self.zoneSubscribed = zoneSubscribed;
self.zoneSetupOperation.qualityOfService = NSQualityOfServiceUserInitiated;
ckksnotice("ckkszone", self, "Setting up zone %@", self.zoneName);
- self.setupStarted = true;
__weak __typeof(self) weakSelf = self;
// First, check the account status. If it's sufficient, add the necessary CloudKit operations to this operation
- NSBlockOperation* doSetup = [NSBlockOperation blockOperationWithBlock:^{
+ __weak CKKSGroupOperation* weakZoneSetupOperation = self.zoneSetupOperation;
+ [self.zoneSetupOperation runBeforeGroupFinished:[CKKSResultOperation named:[NSString stringWithFormat:@"zone-setup-%@", self.zoneName] withBlock:^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
- if(!strongSelf) {
+ __strong __typeof(self.zoneSetupOperation) zoneSetupOperation = weakZoneSetupOperation;
+ if(!strongSelf || !zoneSetupOperation) {
ckkserror("ckkszone", strongSelf, "received callback for released object");
return;
}
- __block bool ret = false;
- [strongSelf dispatchSync: ^bool {
- strongSelf.accountStatus = [strongSelf.accountTracker currentCKAccountStatusAndNotifyOnChange:strongSelf];
-
- switch(strongSelf.accountStatus) {
- case CKKSAccountStatusNoAccount:
- ckkserror("ckkszone", strongSelf, "No CloudKit account; quitting setup for %@", strongSelf.zoneID.zoneName);
- [strongSelf handleCKLogout];
- ret = true;
- break;
- case CKKSAccountStatusAvailable:
- if(strongSelf.accountLoggedInDependency) {
- [strongSelf.operationQueue addOperation: strongSelf.accountLoggedInDependency];
- strongSelf.accountLoggedInDependency = nil;
- }
- break;
- case CKKSAccountStatusUnknown:
- ckkserror("ckkszone", strongSelf, "CloudKit account status currently unknown; stopping setup for %@", strongSelf.zoneID.zoneName);
- ret = true;
- break;
- }
-
- return true;
- }];
+ if(strongSelf.accountStatus != CKKSAccountStatusAvailable) {
+ ckkserror("ckkszone", strongSelf, "Zone doesn't believe it's logged in; quitting setup");
+ return;
+ }
NSBlockOperation* setupCompleteOperation = [NSBlockOperation blockOperationWithBlock:^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
return;
}
- ckksinfo("ckkszone", strongSelf, "%@: Setup complete", strongSelf.zoneName);
- strongSelf.setupComplete = true;
+ ckksnotice("ckkszone", strongSelf, "%@: Setup complete", strongSelf.zoneName);
}];
setupCompleteOperation.name = @"zone-setup-complete-operation";
- // If we don't have an CloudKit account, don't bother continuing
- if(ret) {
- [strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
- return;
- }
-
// We have an account, so fetch the push environment and bring up APS
[strongSelf.container serverPreferredPushEnvironmentWithCompletionHandler: ^(NSString *apsPushEnvString, NSError *error) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
CKKSAPSReceiver* aps = [CKKSAPSReceiver receiverForEnvironment:apsPushEnvString
namedDelegatePort:SecCKKSAPSNamedPort
apsConnectionClass:strongSelf.apsConnectionClass];
- [aps register:strongSelf forZoneID:strongSelf.zoneID];
+ [aps registerReceiver:strongSelf forZoneID:strongSelf.zoneID];
}
}];
ckksnotice("ckkszone", strongSelf, "Adding CKKSModifyRecordZonesOperation: %@ %@", zoneCreationOperation, zoneCreationOperation.dependencies);
strongSelf.zoneCreationOperation = zoneCreationOperation;
[setupCompleteOperation addDependency: modifyRecordZonesCompleteOperation];
- [strongSelf.zoneSetupOperation runBeforeGroupFinished: zoneCreationOperation];
- [strongSelf.zoneSetupOperation dependOnBeforeGroupFinished: modifyRecordZonesCompleteOperation];
+ [zoneSetupOperation runBeforeGroupFinished: zoneCreationOperation];
+ [zoneSetupOperation dependOnBeforeGroupFinished: modifyRecordZonesCompleteOperation];
} else {
- ckksinfo("ckkszone", strongSelf, "no need to create the zone '%@'", strongSelf.zoneName);
+ ckksnotice("ckkszone", strongSelf, "no need to create the zone '%@'", strongSelf.zoneName);
}
if(!zoneSubscribed) {
}
strongSelf.zoneSubscriptionOperation = zoneSubscriptionOperation;
[setupCompleteOperation addDependency: zoneSubscriptionCompleteOperation];
- [strongSelf.zoneSetupOperation runBeforeGroupFinished:zoneSubscriptionOperation];
- [strongSelf.zoneSetupOperation dependOnBeforeGroupFinished: zoneSubscriptionCompleteOperation];
+ [zoneSetupOperation runBeforeGroupFinished:zoneSubscriptionOperation];
+ [zoneSetupOperation dependOnBeforeGroupFinished: zoneSubscriptionCompleteOperation];
} else {
- ckksinfo("ckkszone", strongSelf, "no need to create database subscription");
+ ckksnotice("ckkszone", strongSelf, "no need to create database subscription");
}
[strongSelf.zoneSetupOperation runBeforeGroupFinished:setupCompleteOperation];
- }];
- doSetup.name = @"begin-zone-setup";
-
- [self.zoneSetupOperation runBeforeGroupFinished:doSetup];
+ }]];
+ [self scheduleAccountStatusOperation:self.zoneSetupOperation];
return self.zoneSetupOperation;
}
- (CKKSResultOperation*)beginResetCloudKitZoneOperation {
if(!SecCKKSIsEnabled()) {
- ckksinfo("ckkszone", self, "Skipping CloudKit reset due to disabled CKKS");
+ ckksnotice("ckkszone", self, "Skipping CloudKit reset due to disabled CKKS");
return nil;
}
}
ckksinfo("ckkszone", strongSelf, "record zones deletion %@ completed with error: %@", deletedRecordZoneIDs, operationError);
- [strongSelf resetSetup];
if(operationError && fatalError) {
// If the error wasn't actually a problem, don't report it upward.
// If the zone creation operation is still pending, wait for it to complete before attempting zone deletion
[zoneDeletionOperation addNullableDependency: self.zoneCreationOperation];
- ckksinfo("ckkszone", self, "deleting zone with %@ %@", zoneDeletionOperation, zoneDeletionOperation.dependencies);
+ ckksnotice("ckkszone", self, "deleting zone with %@ %@", zoneDeletionOperation, zoneDeletionOperation.dependencies);
// Don't use scheduleOperation: zone deletions should be attempted even if we're "logged out"
[self.operationQueue addOperation: zoneDeletionOperation];
self.zoneDeletionOperation = zoneDeletionOperation;
}
- (void)handleCKLogin {
- ckksinfo("ckkszone", self, "received a notification of CK login, ignoring");
+ ckksinfo("ckkszone", self, "received a notification of CK login");
+ self.accountStatus = CKKSAccountStatusAvailable;
}
- (void)handleCKLogout {
- ckksinfo("ckkszone", self, "received a notification of CK logout, ignoring");
+ ckksinfo("ckkszone", self, "received a notification of CK logout");
+ self.accountStatus = CKKSAccountStatusNoAccount;
+ [self resetSetup];
}
- (bool)scheduleOperation: (NSOperation*) op {
- if(!self.acceptingNewOperations) {
- ckksdebug("ckkszone", self, "attempted to schedule an operation on a cancelled zone, ignoring");
+ if(self.halted) {
+ ckkserror("ckkszone", self, "attempted to schedule an operation on a halted zone, ignoring");
return false;
}
}
- (bool)scheduleAccountStatusOperation: (NSOperation*) op {
+ if(self.halted) {
+ ckkserror("ckkszone", self, "attempted to schedule an account operation on a halted zone, ignoring");
+ return false;
+ }
+
// Always succeed. But, account status operations should always proceed in-order.
[op linearDependencies:self.accountOperations];
[self.operationQueue addOperation: op];
// to be used rarely, if at all
- (bool)scheduleOperationWithoutDependencies:(NSOperation*)op {
+ if(self.halted) {
+ ckkserror("ckkszone", self, "attempted to schedule an non-dependent operation on a halted zone, ignoring");
+ return false;
+ }
+
[self.operationQueue addOperation: op];
return true;
}
// important enough to block this thread.
__block bool ok = false;
dispatch_sync(self.queue, ^{
+ if(self.halted) {
+ ckkserror("ckkszone", self, "CKKSZone not dispatchSyncing a block (due to being halted)");
+ return;
+ }
+
ok = block();
if(!ok) {
ckkserror("ckkszone", self, "CKKSZone block returned false");
});
}
+- (void)halt {
+ // Synchronously set the 'halted' bit
+ dispatch_sync(self.queue, ^{
+ self.halted = true;
+ });
-#endif /* OCTAGON */
-@end
+ // Bring all operations down, too
+ [self cancelAllOperations];
+}
+@end
+#endif /* OCTAGON */
* @APPLE_LICENSE_HEADER_END@
*/
+#if OCTAGON
+#import <CloudKit/CloudKit.h>
#import <Foundation/Foundation.h>
+#import "keychain/ckks/CKKSResultOperation.h"
-#if OCTAGON
+NS_ASSUME_NONNULL_BEGIN
/* Fetch Reasons */
@protocol SecCKKSFetchBecause
extern CKKSFetchBecause* const CKKSFetchBecauseTesting;
@protocol CKKSChangeFetcherErrorOracle
-- (bool) isFatalCKFetchError: (NSError*) error;
+- (bool)isFatalCKFetchError:(NSError*)error;
@end
/*
*/
@class CKKSKeychainView;
-#import "keychain/ckks/CKKSGroupOperation.h"
-#import <CloudKit/CloudKit.h>
@interface CKKSZoneChangeFetcher : NSObject
-@property (weak) CKKSKeychainView* ckks;
+@property (nullable, weak) CKKSKeychainView* ckks;
@property CKRecordZoneID* zoneID;
- (instancetype)init NS_UNAVAILABLE;
- (CKKSResultOperation*)requestSuccessfulResyncFetch:(CKKSFetchBecause*)why;
// We don't particularly care what this does, as long as it finishes
-- (void)holdFetchesUntil:(CKKSResultOperation*)holdOperation;
+- (void)holdFetchesUntil:(CKKSResultOperation* _Nullable)holdOperation;
--(void)cancel;
+- (void)cancel;
@end
-
+NS_ASSUME_NONNULL_END
#endif
-
-
* @APPLE_LICENSE_HEADER_END@
*/
-#import "CKKSSQLDatabaseObject.h"
-#include <utilities/SecDb.h>
#include <securityd/SecDbItem.h>
+#include <utilities/SecDb.h>
+#import "CKKSSQLDatabaseObject.h"
#ifndef CKKSZoneStateEntry_h
#define CKKSZoneStateEntry_h
@class CKKSRateLimiter;
-@interface CKKSZoneStateEntry : CKKSSQLDatabaseObject {
-
-}
+@interface CKKSZoneStateEntry : CKKSSQLDatabaseObject
@property NSString* ckzone;
@property bool ckzonecreated;
@property bool ckzonesubscribed;
-@property (getter=getChangeToken,setter=setChangeToken:) CKServerChangeToken* changeToken;
+@property (getter=getChangeToken, setter=setChangeToken:) CKServerChangeToken* changeToken;
@property NSData* encodedChangeToken;
@property NSDate* lastFetchTime;
@property CKKSRateLimiter* rateLimiter;
@property NSData* encodedRateLimiter;
-+ (instancetype) state: (NSString*) ckzone;
++ (instancetype)state:(NSString*)ckzone;
-+ (instancetype) fromDatabase: (NSString*) ckzone error: (NSError * __autoreleasing *) error;
-+ (instancetype) tryFromDatabase: (NSString*) ckzone error: (NSError * __autoreleasing *) error;
++ (instancetype)fromDatabase:(NSString*)ckzone error:(NSError* __autoreleasing*)error;
++ (instancetype)tryFromDatabase:(NSString*)ckzone error:(NSError* __autoreleasing*)error;
- (instancetype)initWithCKZone:(NSString*)ckzone
zoneCreated:(bool)ckzonecreated
lastFixup:(CKKSFixup)lastFixup
encodedRateLimiter:(NSData*)encodedRateLimiter;
-- (CKServerChangeToken*) getChangeToken;
-- (void) setChangeToken: (CKServerChangeToken*) token;
+- (CKServerChangeToken*)getChangeToken;
+- (void)setChangeToken:(CKServerChangeToken*)token;
-- (BOOL)isEqual: (id) object;
+- (BOOL)isEqual:(id)object;
@end
#endif
#if OCTAGON
-#import <Foundation/Foundation.h>
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
@interface CKOperationGroup (CKKS)
-+(instancetype) CKKSGroupWithName:(NSString*)name;
++ (instancetype)CKKSGroupWithName:(NSString*)name;
@end
@interface NSError (CKKS)
// More useful constructor
+ (instancetype)errorWithDomain:(NSErrorDomain)domain code:(NSInteger)code description:(NSString*)description;
-+ (instancetype)errorWithDomain:(NSErrorDomain)domain code:(NSInteger)code description:(NSString*)description underlying:(NSError*)underlying;
+
++ (instancetype)errorWithDomain:(NSErrorDomain)domain
+ code:(NSInteger)code
+ description:(NSString*)description
+ underlying:(NSError* _Nullable)underlying;
// Returns true if this is a CloudKit error where
// 1) An atomic write failed
// 2) Every single suberror is either CKErrorServerRecordChanged or CKErrorUnknownItem
--(bool) ckksIsCKErrorRecordChangedError;
+- (bool)ckksIsCKErrorRecordChangedError;
@end
// Ensure we don't print addresses
@interface CKAccountInfo (CKKS)
--(NSString*)description;
+- (NSString*)description;
@end
-#endif // OCTAGON
+NS_ASSUME_NONNULL_END
+#endif // OCTAGON
#ifndef CloudKitDependencies_h
#define CloudKitDependencies_h
-#import <Foundation/Foundation.h>
-#import <CloudKit/CloudKit.h>
#import <ApplePushService/ApplePushService.h>
+#import <CloudKit/CloudKit.h>
+#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/* CKModifyRecordZonesOperation */
@protocol CKKSModifyRecordZonesOperation <NSObject>
+ (instancetype)alloc;
-- (instancetype)initWithRecordZonesToSave:(nullable NSArray<CKRecordZone *> *)recordZonesToSave recordZoneIDsToDelete:(nullable NSArray<CKRecordZoneID *> *)recordZoneIDsToDelete;
+- (instancetype)initWithRecordZonesToSave:(nullable NSArray<CKRecordZone*>*)recordZonesToSave
+ recordZoneIDsToDelete:(nullable NSArray<CKRecordZoneID*>*)recordZoneIDsToDelete;
-@property (nonatomic, strong, nullable) CKDatabase *database;
-@property (nonatomic, copy, nullable) NSArray<CKRecordZone *> *recordZonesToSave;
-@property (nonatomic, copy, nullable) NSArray<CKRecordZoneID *> *recordZoneIDsToDelete;
+@property (nonatomic, strong, nullable) CKDatabase* database;
+@property (nonatomic, copy, nullable) NSArray<CKRecordZone*>* recordZonesToSave;
+@property (nonatomic, copy, nullable) NSArray<CKRecordZoneID*>* recordZoneIDsToDelete;
@property NSOperationQueuePriority queuePriority;
@property NSQualityOfService qualityOfService;
-@property (nonatomic, copy, nullable) void (^modifyRecordZonesCompletionBlock)(NSArray<CKRecordZone *> * _Nullable savedRecordZones, NSArray<CKRecordZoneID *> * _Nullable deletedRecordZoneIDs, NSError * _Nullable operationError);
+@property (nonatomic, copy, nullable) void (^modifyRecordZonesCompletionBlock)
+ (NSArray<CKRecordZone*>* _Nullable savedRecordZones, NSArray<CKRecordZoneID*>* _Nullable deletedRecordZoneIDs, NSError* _Nullable operationError);
@end
-@interface CKModifyRecordZonesOperation (SecCKKSModifyRecordZonesOperation) <CKKSModifyRecordZonesOperation>;
+@interface CKModifyRecordZonesOperation (SecCKKSModifyRecordZonesOperation) <CKKSModifyRecordZonesOperation>
+;
@end
/* CKModifySubscriptionsOperation */
@protocol CKKSModifySubscriptionsOperation <NSObject>
+ (instancetype)alloc;
-- (instancetype)initWithSubscriptionsToSave:(nullable NSArray<CKSubscription *> *)subscriptionsToSave subscriptionIDsToDelete:(nullable NSArray<NSString *> *)subscriptionIDsToDelete;
+- (instancetype)initWithSubscriptionsToSave:(nullable NSArray<CKSubscription*>*)subscriptionsToSave
+ subscriptionIDsToDelete:(nullable NSArray<NSString*>*)subscriptionIDsToDelete;
-@property (nonatomic, strong, nullable) CKDatabase *database;
-@property (nonatomic, copy, nullable) NSArray<CKSubscription *> *subscriptionsToSave;
-@property (nonatomic, copy, nullable) NSArray<NSString *> *subscriptionIDsToDelete;
+@property (nonatomic, strong, nullable) CKDatabase* database;
+@property (nonatomic, copy, nullable) NSArray<CKSubscription*>* subscriptionsToSave;
+@property (nonatomic, copy, nullable) NSArray<NSString*>* subscriptionIDsToDelete;
@property NSOperationQueuePriority queuePriority;
@property NSQualityOfService qualityOfService;
-@property (nonatomic, strong, nullable) CKOperationGroup *group;
+@property (nonatomic, strong, nullable) CKOperationGroup* group;
-@property (nonatomic, copy, nullable) void (^modifySubscriptionsCompletionBlock)(NSArray<CKSubscription *> * _Nullable savedSubscriptions, NSArray<NSString *> * _Nullable deletedSubscriptionIDs, NSError * _Nullable operationError);
+@property (nonatomic, copy, nullable) void (^modifySubscriptionsCompletionBlock)
+ (NSArray<CKSubscription*>* _Nullable savedSubscriptions, NSArray<NSString*>* _Nullable deletedSubscriptionIDs, NSError* _Nullable operationError);
@end
-@interface CKModifySubscriptionsOperation (SecCKKSModifySubscriptionsOperation) <CKKSModifySubscriptionsOperation>;
+@interface CKModifySubscriptionsOperation (SecCKKSModifySubscriptionsOperation) <CKKSModifySubscriptionsOperation>
+;
@end
/* CKFetchRecordZoneChangesOperation */
@protocol CKKSFetchRecordZoneChangesOperation <NSObject>
+ (instancetype)alloc;
-- (instancetype)initWithRecordZoneIDs:(NSArray<CKRecordZoneID *> *)recordZoneIDs optionsByRecordZoneID:(nullable NSDictionary<CKRecordZoneID *, CKFetchRecordZoneChangesOptions *> *)optionsByRecordZoneID;
+- (instancetype)initWithRecordZoneIDs:(NSArray<CKRecordZoneID*>*)recordZoneIDs
+ optionsByRecordZoneID:(nullable NSDictionary<CKRecordZoneID*, CKFetchRecordZoneChangesOptions*>*)optionsByRecordZoneID;
-@property (nonatomic, copy, nullable) NSArray<CKRecordZoneID *> *recordZoneIDs;
-@property (nonatomic, copy, nullable) NSDictionary<CKRecordZoneID *, CKFetchRecordZoneChangesOptions *> *optionsByRecordZoneID;
+@property (nonatomic, copy, nullable) NSArray<CKRecordZoneID*>* recordZoneIDs;
+@property (nonatomic, copy, nullable) NSDictionary<CKRecordZoneID*, CKFetchRecordZoneChangesOptions*>* optionsByRecordZoneID;
@property (nonatomic, assign) BOOL fetchAllChanges;
-@property (nonatomic, copy, nullable) void (^recordChangedBlock)(CKRecord *record);
-@property (nonatomic, copy, nullable) void (^recordWithIDWasDeletedBlock)(CKRecordID *recordID, NSString *recordType);
-@property (nonatomic, copy, nullable) void (^recordZoneChangeTokensUpdatedBlock)(CKRecordZoneID *recordZoneID, CKServerChangeToken * _Nullable serverChangeToken, NSData * _Nullable clientChangeTokenData);
-@property (nonatomic, copy, nullable) void (^recordZoneFetchCompletionBlock)(CKRecordZoneID *recordZoneID, CKServerChangeToken * _Nullable serverChangeToken, NSData * _Nullable clientChangeTokenData, BOOL moreComing, NSError * _Nullable recordZoneError);
-@property (nonatomic, copy, nullable) void (^fetchRecordZoneChangesCompletionBlock)(NSError * _Nullable operationError);
+@property (nonatomic, copy, nullable) void (^recordChangedBlock)(CKRecord* record);
+@property (nonatomic, copy, nullable) void (^recordWithIDWasDeletedBlock)(CKRecordID* recordID, NSString* recordType);
+@property (nonatomic, copy, nullable) void (^recordZoneChangeTokensUpdatedBlock)
+ (CKRecordZoneID* recordZoneID, CKServerChangeToken* _Nullable serverChangeToken, NSData* _Nullable clientChangeTokenData);
+@property (nonatomic, copy, nullable) void (^recordZoneFetchCompletionBlock)(CKRecordZoneID* recordZoneID,
+ CKServerChangeToken* _Nullable serverChangeToken,
+ NSData* _Nullable clientChangeTokenData,
+ BOOL moreComing,
+ NSError* _Nullable recordZoneError);
+@property (nonatomic, copy, nullable) void (^fetchRecordZoneChangesCompletionBlock)(NSError* _Nullable operationError);
-@property (nonatomic, strong, nullable) CKOperationGroup *group;
+@property (nonatomic, strong, nullable) CKOperationGroup* group;
@end
-@interface CKFetchRecordZoneChangesOperation () <CKKSFetchRecordZoneChangesOperation>;
+@interface CKFetchRecordZoneChangesOperation () <CKKSFetchRecordZoneChangesOperation>
+;
@end
/* CKFetchRecordsOperation */
@protocol CKKSFetchRecordsOperation <NSObject>
+ (instancetype)alloc;
- (instancetype)init;
-- (instancetype)initWithRecordIDs:(NSArray<CKRecordID *> *)recordIDs;
+- (instancetype)initWithRecordIDs:(NSArray<CKRecordID*>*)recordIDs;
-@property (nonatomic, copy, nullable) NSArray<CKRecordID *> *recordIDs;
-@property (nonatomic, copy, nullable) NSArray<NSString *> *desiredKeys;
-@property (nonatomic, copy, nullable) void (^perRecordProgressBlock)(CKRecordID *recordID, double progress);
-@property (nonatomic, copy, nullable) void (^perRecordCompletionBlock)(CKRecord * _Nullable record, CKRecordID * _Nullable recordID, NSError * _Nullable error);
-@property (nonatomic, copy, nullable) void (^fetchRecordsCompletionBlock)(NSDictionary<CKRecordID * , CKRecord *> * _Nullable recordsByRecordID, NSError * _Nullable operationError);
+@property (nonatomic, copy, nullable) NSArray<CKRecordID*>* recordIDs;
+@property (nonatomic, copy, nullable) NSArray<NSString*>* desiredKeys;
+@property (nonatomic, copy, nullable) void (^perRecordProgressBlock)(CKRecordID* recordID, double progress);
+@property (nonatomic, copy, nullable) void (^perRecordCompletionBlock)
+ (CKRecord* _Nullable record, CKRecordID* _Nullable recordID, NSError* _Nullable error);
+@property (nonatomic, copy, nullable) void (^fetchRecordsCompletionBlock)
+ (NSDictionary<CKRecordID*, CKRecord*>* _Nullable recordsByRecordID, NSError* _Nullable operationError);
@end
@interface CKFetchRecordsOperation () <CKKSFetchRecordsOperation>
@protocol CKKSQueryOperation <NSObject>
+ (instancetype)alloc;
-- (instancetype)initWithQuery:(CKQuery *)query;
+- (instancetype)initWithQuery:(CKQuery*)query;
//Not implemented: - (instancetype)initWithCursor:(CKQueryCursor *)cursor;
-@property (nonatomic, copy, nullable) CKQuery *query;
-@property (nonatomic, copy, nullable) CKQueryCursor *cursor;
+@property (nonatomic, copy, nullable) CKQuery* query;
+@property (nonatomic, copy, nullable) CKQueryCursor* cursor;
-@property (nonatomic, copy, nullable) CKRecordZoneID *zoneID;
+@property (nonatomic, copy, nullable) CKRecordZoneID* zoneID;
@property (nonatomic, assign) NSUInteger resultsLimit;
-@property (nonatomic, copy, nullable) NSArray<NSString *> *desiredKeys;
+@property (nonatomic, copy, nullable) NSArray<NSString*>* desiredKeys;
-@property (nonatomic, copy, nullable) void (^recordFetchedBlock)(CKRecord *record);
-@property (nonatomic, copy, nullable) void (^queryCompletionBlock)(CKQueryCursor * _Nullable cursor, NSError * _Nullable operationError);
+@property (nonatomic, copy, nullable) void (^recordFetchedBlock)(CKRecord* record);
+@property (nonatomic, copy, nullable) void (^queryCompletionBlock)(CKQueryCursor* _Nullable cursor, NSError* _Nullable operationError);
@end
@interface CKQueryOperation () <CKKSQueryOperation>
/* APSConnection */
@protocol CKKSAPSConnection <NSObject>
+ (instancetype)alloc;
-- (id)initWithEnvironmentName:(NSString *)environmentName namedDelegatePort:(NSString*)namedDelegatePort queue:(dispatch_queue_t)queue;
+- (id)initWithEnvironmentName:(NSString*)environmentName
+ namedDelegatePort:(NSString*)namedDelegatePort
+ queue:(dispatch_queue_t)queue;
-- (void)setEnabledTopics:(NSArray *)enabledTopics;
+- (void)setEnabledTopics:(NSArray*)enabledTopics;
@property (nonatomic, readwrite, assign) id<APSConnectionDelegate> delegate;
@end
-@interface APSConnection (SecCKKSAPSConnection) <CKKSAPSConnection>;
+@interface APSConnection (SecCKKSAPSConnection) <CKKSAPSConnection>
@end
/* NSNotificationCenter */
/* Since CKDatabase doesn't share any types with NSOperationQueue, tell the type system about addOperation */
@protocol CKKSOperationQueue <NSObject>
-- (void)addOperation:(NSOperation *)operation;
+- (void)addOperation:(NSOperation*)operation;
@end
-@interface CKDatabase () <CKKSOperationQueue>;
+@interface CKDatabase () <CKKSOperationQueue>
@end
-@interface NSOperationQueue () <CKKSOperationQueue>;
+@interface NSOperationQueue () <CKKSOperationQueue>
@end
NS_ASSUME_NONNULL_END
- (NSString*)selfname;
// If op is nonnull, op becomes a dependency of this operation
-- (void)addNullableDependency: (NSOperation*) op;
+- (void)addNullableDependency:(NSOperation*)op;
// Add all operations in this collection as dependencies, then add yourself to the collection
--(void)linearDependencies:(NSHashTable*)collection;
+- (void)linearDependencies:(NSHashTable*)collection;
// Insert yourself as high up the linearized list of dependencies as possible
--(void)linearDependenciesWithSelfFirst: (NSHashTable*) collection;
+- (void)linearDependenciesWithSelfFirst:(NSHashTable*)collection;
// Return a stringified representation of this operation's live dependencies.
--(NSString*)pendingDependenciesString:(NSString*)prefix;
+- (NSString*)pendingDependenciesString:(NSString*)prefix;
@end
@interface NSBlockOperation (CKKSUsefulConstructorOperation)
-+(instancetype)named: (NSString*)name withBlock: (void(^)(void)) block;
++ (instancetype)named:(NSString*)name withBlock:(void (^)(void))block;
@end
-
dependencies = [dependencies objectsAtIndexes: [dependencies indexesOfObjectsPassingTest: ^BOOL (id obj,
NSUInteger idx,
BOOL* stop) {
- return [obj isPending] ? YES : NO;
+ return [obj isFinished] ? NO : YES;
}]];
if(dependencies.count == 0u) {
* @APPLE_LICENSE_HEADER_END@
*/
-#ifndef RateLimiter_h
-#define RateLimiter_h
-
#import <Foundation/Foundation.h>
+NS_ASSUME_NONNULL_BEGIN
+
@interface RateLimiter : NSObject <NSSecureCoding>
-@property (readonly, nonatomic, nonnull) NSDictionary *config;
+@property (readonly, nonatomic) NSDictionary* config;
@property (readonly, nonatomic) NSUInteger stateSize;
-@property (readonly, nonatomic, nullable) NSString *assetType;
+@property (readonly, nonatomic, nullable) NSString* assetType;
typedef NS_ENUM(NSInteger, RateLimiterBadness) {
- RateLimiterBadnessClear = 0, // everything is fine, process right now
+ RateLimiterBadnessClear = 0, // everything is fine, process right now
RateLimiterBadnessCongested,
RateLimiterBadnessSeverelyCongested,
RateLimiterBadnessGridlocked,
- RateLimiterBadnessOverloaded, // everything is on fire, go away
+ RateLimiterBadnessOverloaded, // everything is on fire, go away
};
-- (instancetype _Nullable)initWithConfig:(NSDictionary * _Nonnull)config;
-- (instancetype _Nullable)initWithPlistFromURL:(NSURL * _Nonnull)url;
-- (instancetype _Nullable)initWithAssetType:(NSString * _Nonnull)type; // Not implemented yet
-- (instancetype _Nullable)initWithCoder:(NSCoder * _Nonnull)coder;
+- (instancetype _Nullable)initWithConfig:(NSDictionary*)config;
+- (instancetype _Nullable)initWithPlistFromURL:(NSURL*)url;
+- (instancetype _Nullable)initWithAssetType:(NSString*)type; // Not implemented yet
+- (instancetype _Nullable)initWithCoder:(NSCoder*)coder;
- (instancetype _Nullable)init NS_UNAVAILABLE;
/*!
* At badness 5 judge:at: has determined there is too much activity so the caller should hold off altogether. The limitTime object will indicate when
* this overloaded state will end.
*/
-- (NSInteger)judge:(id _Nonnull)obj at:(NSDate * _Nonnull)time limitTime:(NSDate * _Nullable __autoreleasing * _Nonnull)limitTime;
+- (NSInteger)judge:(id)obj at:(NSDate*)time limitTime:(NSDate* _Nonnull __autoreleasing* _Nonnull)limitTime;
- (void)reset;
-- (NSString * _Nonnull)diagnostics;
+- (NSString*)diagnostics;
+ (BOOL)supportsSecureCoding;
// TODO:
@end
-#endif /* RateLimiter_h */
+NS_ASSUME_NONNULL_END
/* Annotated example plist
#endif
@interface RateLimiter()
-@property (readwrite, nonatomic, nonnull) NSDictionary *config;
+@property (readwrite, nonatomic) NSDictionary *config;
@property (nonatomic) NSArray<NSMutableDictionary<NSString *, NSDate *> *> *groups;
@property (nonatomic) NSDate *lastJudgment;
@property (nonatomic) NSDate *overloadUntil;
--- /dev/null
+/*
+ * Copyright (c) 2017 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <dispatch/dispatch.h>
+#include <objc/objc-internal.h>
+
+#include "AutoreleaseTest.h"
+
+static void
+read_releases_pending(int fd, void (^handler)(ssize_t))
+{
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ ssize_t result = -1;
+
+ FILE *fp = fdopen(fd, "r");
+
+ char *line = NULL;
+ size_t linecap = 0;
+ ssize_t linelen;
+ while ((linelen = getline(&line, &linecap, fp)) > 0) {
+ ssize_t pending;
+
+ if (sscanf(line, "objc[%*d]: %ld releases pending", &pending) == 1) {
+ result = pending;
+ break;
+ }
+ }
+ free(line);
+
+ fclose(fp);
+
+ handler(result);
+ });
+}
+
+ssize_t
+pending_autorelease_count(void)
+{
+ __block ssize_t result = -1;
+ dispatch_semaphore_t sema;
+ int fds[2];
+ int saved_stderr;
+
+ // stderr replacement pipe
+ pipe(fds);
+ fcntl(fds[1], F_SETNOSIGPIPE, 1);
+
+ // sead asynchronously - takes ownership of fds[0]
+ sema = dispatch_semaphore_create(0);
+ read_releases_pending(fds[0], ^(ssize_t pending) {
+ result = pending;
+ dispatch_semaphore_signal(sema);
+ });
+
+ // save and replace stderr
+ saved_stderr = dup(STDERR_FILENO);
+ dup2(fds[1], STDERR_FILENO);
+ close(fds[1]);
+
+ // make objc print the current autorelease pool
+ _objc_autoreleasePoolPrint();
+
+ // restore stderr
+ dup2(saved_stderr, STDERR_FILENO);
+ close(saved_stderr);
+
+ // wait for the reader
+ dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
+#if !__has_feature(objc_arc)
+ dispatch_release(sema);
+#endif
+
+ return result;
+}
--- /dev/null
+/*
+ * Copyright (c) 2017 Apple Inc. All Rights Reserved.
+ *
+ * @APPLE_LICENSE_HEADER_START@
+ *
+ * This file contains Original Code and/or Modifications of Original Code
+ * as defined in and that are subject to the Apple Public Source License
+ * Version 2.0 (the 'License'). You may not use this file except in
+ * compliance with the License. Please obtain a copy of the License at
+ * http://www.opensource.apple.com/apsl/ and read it before using this
+ * file.
+ *
+ * The Original Code and all software distributed under the License are
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
+ * Please see the License for the specific language governing rights and
+ * limitations under the License.
+ *
+ * @APPLE_LICENSE_HEADER_END@
+ */
+
+#define TEST_API_AUTORELEASE_BEFORE(name) size_t _pending_before_##name = pending_autorelease_count()
+
+#define TEST_API_AUTORELEASE_AFTER(name) \
+ size_t _pending_after_##name = pending_autorelease_count(); \
+ XCTAssertEqual(_pending_before_##name, _pending_after_##name, "pending autoreleases unchanged (%lu->%lu)", _pending_before_##name, _pending_after_##name)
+
+ssize_t pending_autorelease_count(void);
XCTAssertNil(unwrappedKey, "unwrapped key was not returned in error case");
}
+- (void)testKeyKeychainSaving {
+ NSError* error = nil;
+ CKKSKey* tlk = [self fakeTLK:self.testZoneID];
+
+ XCTAssertTrue([tlk saveKeyMaterialToKeychain:false error:&error], "should be able to save key material to keychain (without stashing)");
+ XCTAssertNil(error, "tlk should save to database without error");
+ XCTAssertTrue([tlk loadKeyMaterialFromKeychain:&error], "Should be able to reload key material");
+ XCTAssertNil(error, "should be no error loading the tlk from the keychain");
+
+ XCTAssertTrue([tlk saveKeyMaterialToKeychain:false error:&error], "should be able to save key material to keychain (without stashing)");
+ XCTAssertNil(error, "tlk should save again to database without error");
+ XCTAssertTrue([tlk loadKeyMaterialFromKeychain:&error], "Should be able to reload key material");
+ XCTAssertNil(error, "should be no error loading the tlk from the keychain");
+
+ [tlk deleteKeyMaterialFromKeychain:&error];
+ XCTAssertNil(error, "tlk should be able to delete itself without error");
+
+ XCTAssertFalse([tlk loadKeyMaterialFromKeychain:&error], "Should not able to reload key material");
+ XCTAssertNotNil(error, "should be error loading the tlk from the keychain");
+ error = nil;
+
+ NSData* keydata = [@"asdf" dataUsingEncoding:NSUTF8StringEncoding];
+
+ // Add an item using no viewhint that will conflict with itself upon a SecItemUpdate (internal builds only)
+ NSMutableDictionary* query = [@{
+ (id)kSecClass : (id)kSecClassInternetPassword,
+ (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
+ (id)kSecAttrNoLegacy : @YES,
+ (id)kSecAttrAccessGroup: @"com.apple.security.ckks",
+ (id)kSecAttrDescription: tlk.keyclass,
+ (id)kSecAttrServer: tlk.zoneID.zoneName,
+ (id)kSecAttrAccount: tlk.uuid,
+ (id)kSecAttrPath: tlk.parentKeyUUID,
+ (id)kSecAttrIsInvisible: @YES,
+ (id)kSecValueData : keydata,
+ (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
+ } mutableCopy];
+ XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)query, NULL), "Should be able to add a conflicting item");
+
+ XCTAssertTrue([tlk saveKeyMaterialToKeychain:false error:&error], "should be able to save key material to keychain (without stashing)");
+ XCTAssertNil(error, "tlk should save to database without error");
+ XCTAssertTrue([tlk loadKeyMaterialFromKeychain:&error], "Should be able to reload key material");
+ XCTAssertNil(error, "should be no error loading the tlk from the keychain");
+
+ XCTAssertTrue([tlk saveKeyMaterialToKeychain:false error:&error], "should be able to save key material to keychain (without stashing)");
+ XCTAssertNil(error, "tlk should save again to database without error");
+ XCTAssertTrue([tlk loadKeyMaterialFromKeychain:&error], "Should be able to reload key material");
+ XCTAssertNil(error, "should be no error loading the tlk from the keychain");
+}
+
- (void)testKeyHierarchy {
NSError* error = nil;
NSData* testCKRecord = [@"nonsense" dataUsingEncoding:NSUTF8StringEncoding];
XCTAssertNil(unpadded, "Cannot remove padding where none exists");
// Feeding nil
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnonnull"
padded = [CKKSItemEncrypter padData:nil blockSize:0 additionalBlock:extra];
XCTAssertNotNil(padded, "padData always returns a data object");
XCTAssertEqual(padded.length, extra ? 2ul : 1ul, "Length of padded nil object is padding byte only--two if extra");
unpadded = [CKKSItemEncrypter removePaddingFromData:nil];
XCTAssertNil(unpadded, "Removing padding from nil is senseless");
+#pragma clang diagnostic pop
}
- (BOOL)encryptAndDecryptDictionary:(NSDictionary<NSString*, NSData*>*)data key:(CKKSKey *)key {
#import "keychain/ckks/tests/MockCloudKit.h"
#import "keychain/ckks/CKKSCondition.h"
+#if OCTAGON
+
@interface CKKSAPSNotificationReceiver : NSObject <CKKSZoneUpdateReceiver>
@property XCTestExpectation* expectation;
@property void (^block)(CKRecordZoneNotification* notification);
XCTAssertEqual(strongSelf.testZoneID, notification.recordZoneID, "Should have received a notification for the test zone");
}];
- CKKSCondition* registered = [apsr register:anr forZoneID:self.testZoneID];
+ CKKSCondition* registered = [apsr registerReceiver:anr forZoneID:self.testZoneID];
XCTAssertEqual(0, [registered wait:1*NSEC_PER_SEC], "Registration should have completed within a second");
APSIncomingMessage* message = [self messageForZoneID:self.testZoneID];
XCTAssertNotNil(message, "Should have received a APSIncomingMessage");
XCTAssertEqual(otherZoneID, notification.recordZoneID, "Should have received a notification for the test zone");
}];
- CKKSCondition* registered = [apsr register:anr forZoneID:self.testZoneID];
- CKKSCondition* registered2 = [apsr register:anr2 forZoneID:otherZoneID];
+ CKKSCondition* registered = [apsr registerReceiver:anr forZoneID:self.testZoneID];
+ CKKSCondition* registered2 = [apsr registerReceiver:anr2 forZoneID:otherZoneID];
XCTAssertEqual(0, [registered wait:1*NSEC_PER_SEC], "Registration should have completed within a second");
XCTAssertEqual(0, [registered2 wait:1*NSEC_PER_SEC], "Registration should have completed within a second");
XCTAssertNil(notification, "Should not have received a notification, since we weren't alive to receive it");
}];
- CKKSCondition* registered = [apsr register:anr forZoneID:self.testZoneID];
+ CKKSCondition* registered = [apsr registerReceiver:anr forZoneID:self.testZoneID];
XCTAssertEqual(0, [registered wait:1*NSEC_PER_SEC], "Registration should have completed within a second");
[self waitForExpectationsWithTimeout:5.0 handler:nil];
}
@end
+
+#endif
@interface CKKSCloudKitTests : XCTestCase
@property NSOperationQueue *operationQueue;
-@property NSBlockOperation *ckksHoldOperation;
@property CKContainer *container;
@property CKDatabase *database;
@property CKKSKeychainView *kcv;
SecCKKSTestSetDisableSOS(true);
self.operationQueue = [NSOperationQueue new];
- self.ckksHoldOperation = [NSBlockOperation new];
- [self.ckksHoldOperation addExecutionBlock:^{
- secnotice("ckks", "CKKS testing hold released");
- }];
CKKSViewManager* manager = [[CKKSViewManager alloc] initWithContainerName:containerName
usePCS:SecCKKSContainerUsePCS
modifyRecordZonesOperationClass:[CKModifyRecordZonesOperation class]
apsConnectionClass:[APSConnection class]
nsnotificationCenterClass:[NSNotificationCenter class]
- notifierClass:[FakeCKKSNotifier class]
- setupHold:self.ckksHoldOperation];
+ notifierClass:[FakeCKKSNotifier class]];
[CKKSViewManager resetManager:false setTo:manager];
// Make a new fake keychain
self.zoneName = @"keychain";
self.zoneID = [[CKRecordZoneID alloc] initWithZoneName:self.zoneName ownerName:CKCurrentUserDefaultName];
self.kcv = [[CKKSViewManager manager] findOrCreateView:@"keychain"];
- [self.kcv.zoneSetupOperation addDependency: self.ckksHoldOperation];
}
- (void)tearDown {
}
- (void)startCKKSSubsystem {
- if(self.ckksHoldOperation) {
- [self.operationQueue addOperation: self.ckksHoldOperation];
- self.ckksHoldOperation = nil;
- }
+ // TODO: we removed this mechanism, but haven't tested to see if these tests still succeed
}
- (NSMutableDictionary *)fetchLocalItems {
[self waitForExpectations: @[expectation] timeout:0.5];
}
+-(void)testConditionChain {
+ CKKSCondition* chained = [[CKKSCondition alloc] init];
+ CKKSCondition* c = [[CKKSCondition alloc] initToChain: chained];
+
+ XCTAssertNotEqual(0, [chained wait:50*NSEC_PER_MSEC], "waiting on chained condition without fulfilling times out");
+ XCTAssertNotEqual(0, [c wait:50*NSEC_PER_MSEC], "waiting on condition without fulfilling times out");
+
+ [c fulfill];
+ XCTAssertEqual(0, [c wait:100*NSEC_PER_MSEC], "first wait after fulfill succeeds");
+ XCTAssertEqual(0, [chained wait:100*NSEC_PER_MSEC], "first chained wait after fulfill succeeds");
+ XCTAssertEqual(0, [c wait:100*NSEC_PER_MSEC], "second wait after fulfill succeeds");
+ XCTAssertEqual(0, [chained wait:100*NSEC_PER_MSEC], "second chained wait after fulfill succeeds");
+}
+
@end
#import <Foundation/Foundation.h>
#import <XCTest/XCTest.h>
+#if OCTAGON
+
static NSString* tablePath = nil;
@interface SQLiteTests : XCTestCase
@end
-@interface CKKSAnalyticsLoggerTests : CloudKitKeychainSyncingTestsBase
+@interface CKKSAnalyticsTests : CloudKitKeychainSyncingTestsBase
@end
-@implementation CKKSAnalyticsLoggerTests
-
-- (void)testLoggingJSONGenerated
-{
- [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
-
- // We expect a single record to be uploaded.
- [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
-
- [self startCKKSSubsystem];
-
- [self addGenericPassword: @"data" account: @"account-delete-me"];
-
- OCMVerifyAllWithDelay(self.mockDatabase, 8);
-
- NSError* error = nil;
- NSData* json = [[CKKSAnalyticsLogger logger] getLoggingJSON:false error:&error];
- XCTAssertNotNil(json, @"failed to generate logging json");
- XCTAssertNil(error, @"encourntered error getting logging json: %@", error);
-
- NSDictionary* dictionary = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error];
- XCTAssertNotNil(dictionary, @"failed to generate dictionary from json data");
- XCTAssertNil(error, @"encountered error deserializing json: %@", error);
- XCTAssertTrue([dictionary isKindOfClass:[NSDictionary class]], @"did not get the class we expected from json deserialization");
-
- XCTAssertNotNil(dictionary[@"postTime"], @"Failed to get posttime");
-
- NSArray *events = dictionary[@"events"];
- XCTAssertNotNil(events, @"Failed to get events");
- XCTAssert([events isKindOfClass:[NSArray class]], @"did not get the class we expected for events");
-
-
- for (NSDictionary *event in events) {
- XCTAssert([event isKindOfClass:[NSDictionary class]], @"did not get the class we expected for events");
- XCTAssertNotNil(event[@"build"], @"Failed to get build in event");
- XCTAssertNotNil(event[@"product"], @"Failed to get product in event");
- XCTAssertNotNil(event[@"topic"], @"Failed to get topic in event");
-
- NSString *eventtype = event[@"eventType"];
- XCTAssertNotNil(eventtype, @"Failed to get eventType in eventtype");
- XCTAssert([eventtype isKindOfClass:[NSString class]], @"did not get the class we expected for events");
- if ([eventtype isEqualToString:@"ckksHealthSummary"]) {
- XCTAssertNotNil(event[@"ckdeviceID"], @"Failed to get deviceID in event");
- }
- }
-}
-
-- (void)testSplunkDefaultTopicNameExists
-{
- CKKSAnalyticsLogger* logger = [CKKSAnalyticsLogger logger];
- dispatch_sync(logger.splunkLoggingQueue, ^{
- XCTAssertNotNil(logger.splunkTopicName);
- });
-}
-
-- (void)testSplunkDefaultBagURLExists
-{
- CKKSAnalyticsLogger* logger = [CKKSAnalyticsLogger logger];
- dispatch_sync(logger.splunkLoggingQueue, ^{
- XCTAssertNotNil(logger.splunkBagURL);
- });
-}
-
-// <rdar://problem/32983193> test_KeychainCKKS | CKKSTests failed: "Failed subtests: -[CloudKitKeychainSyncingTests testSplunkUploadURLExists]" [j71ap][CoreOSTigris15Z240][bats-e-27-204-1]
-#if 0
-- (void)testSplunkUploadURLExists
-{
- CKKSAnalyticsLogger* logger = [CKKSAnalyticsLogger logger];
- dispatch_sync(logger.splunkLoggingQueue, ^{
- logger.ignoreServerDisablingMessages = YES;
- XCTAssertNotNil(logger.splunkUploadURL);
- });
-}
-#endif
+@implementation CKKSAnalyticsTests
- (void)testLastSuccessfulSyncDate
{
XCTAssertTrue(timeIntervalSinceSyncDate >= 0.0 && timeIntervalSinceSyncDate <= 15.0, "Last sync date does not look like a reasonable one");
}
-- (void)testExtraValuesToUploadToServer
-{
- [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
- [self startCKKSSubsystem];
- CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
- [self.keychainZone addToZone: ckr];
-
- // Trigger a notification (with hilariously fake data)
- [self.keychainView notifyZoneChange:nil];
-
- [[[self.keychainView waitForFetchAndIncomingQueueProcessing] completionHandlerDidRunCondition] wait:4 * NSEC_PER_SEC];
-
- NSDictionary* extraValues = [[CKKSAnalyticsLogger logger] extraValuesToUploadToServer];
- XCTAssertTrue([extraValues[@"inCircle"] boolValue]);
- XCTAssertTrue([extraValues[@"keychain-TLKs"] boolValue]);
- XCTAssertTrue([extraValues[@"keychain-inSyncA"] boolValue]);
- XCTAssertTrue([extraValues[@"keychain-inSyncC"] boolValue]);
- XCTAssertTrue([extraValues[@"keychain-IQNOE"] boolValue]);
- XCTAssertTrue([extraValues[@"keychain-OQNOE"] boolValue]);
- XCTAssertTrue([extraValues[@"keychain-inSync"] boolValue]);
-}
-
-- (void)testNilEventDoesNotCrashTheSystem
-{
- CKKSAnalyticsLogger* logger = [CKKSAnalyticsLogger logger];
- [logger logSuccessForEventNamed:nil];
-
- NSData* json = nil;
- NSError* error = nil;
- XCTAssertNoThrow(json = [logger getLoggingJSON:false error:&error]);
- XCTAssertNotNil(json, @"Failed to get JSON after logging nil event");
- XCTAssertNil(error, @"Got error when grabbing JSON after logging nil event: %@", error);
-}
-
- (void)testRaceToCreateLoggers
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
}
}
-- (void)testSysdiagnoseDump
-{
- [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
- [self startCKKSSubsystem];
- CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
- [self.keychainZone addToZone: ckr];
-
- // Trigger a notification (with hilariously fake data)
- [self.keychainView notifyZoneChange:nil];
-
- [self.keychainView waitForFetchAndIncomingQueueProcessing];
-
- NSError* error = nil;
- NSString* sysdiagnose = [[CKKSAnalyticsLogger logger] getSysdiagnoseDumpWithError:&error];
- XCTAssertNil(error, @"encountered an error grabbing CKKS analytics sysdiagnose: %@", error);
- XCTAssertTrue(sysdiagnose.length > 0, @"failed to get a sysdiagnose from CKKS analytics");
-}
-
@end
+
+#endif
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
- // If TLK sharing is enabled, CKKS will save a share for itself
- if(SecCKKSShareTLKs()) {
- [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
- }
-
+ [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
+
[self addGenericPassword:@"data" account:@"first"];
[self addGenericPassword:@"data" account:@"second"];
[self addGenericPassword:@"data" account:@"third"];
#include <dispatch/dispatch.h>
#import <XCTest/XCTest.h>
#import "keychain/ckks/CKKSNearFutureScheduler.h"
+#import "keychain/ckks/CKKSResultOperation.h"
@interface CKKSNearFutureSchedulerTests : XCTestCase
-
+@property NSOperationQueue* operationQueue;
@end
@implementation CKKSNearFutureSchedulerTests
- (void)setUp {
[super setUp];
+
+ self.operationQueue = [[NSOperationQueue alloc] init];
}
- (void)tearDown {
[super tearDown];
}
-- (void)testOneShot {
+#pragma mark - Block-based tests
+
+- (void)testBlockOneShot {
XCTestExpectation *expectation = [self expectationWithDescription:@"FutureScheduler fired"];
CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" delay:50*NSEC_PER_MSEC keepProcessAlive:true block:^{
[self waitForExpectationsWithTimeout:1 handler:nil];
}
-- (void)testOneShotDelay {
+- (void)testBlockOneShotDelay {
XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"];
toofastexpectation.inverted = YES;
[self waitForExpectations: @[expectation] timeout:1];
}
-- (void)testOneShotManyTrigger {
+- (void)testBlockOneShotManyTrigger {
XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"];
toofastexpectation.inverted = YES;
}
-- (void)testMultiShot {
+- (void)testBlockMultiShot {
XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"];
first.assertForOverFulfill = NO;
[scheduler trigger];
- [self waitForExpectations: @[first] timeout:0.2];
+ [self waitForExpectations: @[first] timeout:0.4];
[scheduler trigger];
[scheduler trigger];
[scheduler trigger];
- [self waitForExpectations: @[second] timeout:0.2];
+ [self waitForExpectations: @[second] timeout:0.4];
XCTestExpectation* waitmore = [self expectationWithDescription:@"waiting"];
waitmore.inverted = YES;
- [self waitForExpectations: @[waitmore] timeout: 0.2];
+ [self waitForExpectations: @[waitmore] timeout: 0.4];
}
-- (void)testMultiShotDelays {
+- (void)testBlockMultiShotDelays {
XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"];
first.assertForOverFulfill = NO;
second.expectedFulfillmentCount = 2;
second.assertForOverFulfill = YES;
- CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" initialDelay: 50*NSEC_PER_MSEC continuingDelay:300*NSEC_PER_MSEC keepProcessAlive:false block:^{
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" initialDelay: 50*NSEC_PER_MSEC continuingDelay:600*NSEC_PER_MSEC keepProcessAlive:false block:^{
[first fulfill];
[longdelay fulfill];
[second fulfill];
[scheduler trigger];
- [self waitForExpectations: @[first] timeout:0.2];
+ [self waitForExpectations: @[first] timeout:0.5];
[scheduler trigger];
[scheduler trigger];
[scheduler trigger];
- // longdelay should NOT be fulfilled twice in the first 0.3 seconds
- [self waitForExpectations: @[longdelay] timeout:0.2];
+ // longdelay should NOT be fulfilled twice in the first 0.9 seconds
+ [self waitForExpectations: @[longdelay] timeout:0.4];
- // But second should be fulfilled in the first 0.8 seconds
+ // But second should be fulfilled in the first 1.4 seconds
[self waitForExpectations: @[second] timeout:0.5];
XCTestExpectation* waitmore = [self expectationWithDescription:@"waiting"];
[self waitForExpectations: @[waitmore] timeout: 0.2];
}
-- (void)testCancel {
+- (void)testBlockCancel {
XCTestExpectation *cancelexpectation = [self expectationWithDescription:@"FutureScheduler fired (after cancel)"];
cancelexpectation.inverted = YES;
[self waitForExpectations: @[cancelexpectation] timeout:0.2];
}
-- (void)testDelayedNoShot {
+- (void)testBlockDelayedNoShot {
XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"];
toofastexpectation.inverted = YES;
[self waitForExpectations: @[toofastexpectation] timeout:0.1];
}
-- (void)testDelayedOneShot {
+- (void)testBlockDelayedOneShot {
XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"];
first.assertForOverFulfill = NO;
[scheduler trigger];
[self waitForExpectations: @[toofastexpectation] timeout:0.1];
- [self waitForExpectations: @[first] timeout:0.2];
+ [self waitForExpectations: @[first] timeout:0.5];
}
-- (void)testDelayedMultiShot {
+- (void)testBlockWaitedMultiShot {
XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"];
first.assertForOverFulfill = NO;
}];
[scheduler trigger];
- [self waitForExpectations: @[first] timeout:0.2];
+ [self waitForExpectations: @[first] timeout:0.5];
[scheduler waitUntil: 150*NSEC_PER_MSEC];
[scheduler trigger];
[self waitForExpectations: @[second] timeout:0.3];
}
+#pragma mark - Operation-based tests
+
+- (NSOperation*)operationFulfillingExpectations:(NSArray<XCTestExpectation*>*)expectations {
+ return [NSBlockOperation named:@"test" withBlock:^{
+ for(XCTestExpectation* e in expectations) {
+ [e fulfill];
+ }
+ }];
+}
+
+- (void)addOperationFulfillingExpectations:(NSArray<XCTestExpectation*>*)expectations scheduler:(CKKSNearFutureScheduler*)scheduler {
+ NSOperation* op = [self operationFulfillingExpectations:expectations];
+ XCTAssertNotNil(scheduler.operationDependency, "Should be an operation dependency");
+ XCTAssertTrue([scheduler.operationDependency isPending], "operation dependency shouldn't have run yet");
+ [op addDependency:scheduler.operationDependency];
+ [self.operationQueue addOperation:op];
+}
+
+- (void)testOperationOneShot {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"FutureScheduler fired"];
+
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" delay:50*NSEC_PER_MSEC keepProcessAlive:true block:^{}];
+ [self addOperationFulfillingExpectations:@[expectation] scheduler:scheduler];
+
+ [scheduler trigger];
+
+ [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
+- (void)testOperationOneShotDelay {
+ XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"];
+ toofastexpectation.inverted = YES;
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"FutureScheduler fired"];
+
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" delay: 200*NSEC_PER_MSEC keepProcessAlive:false block:^{}];
+ [self addOperationFulfillingExpectations:@[expectation,toofastexpectation] scheduler:scheduler];
+
+ [scheduler trigger];
+
+ // Make sure it waits at least 0.1 seconds
+ [self waitForExpectations: @[toofastexpectation] timeout:0.1];
+
+ // But finishes within 1.1s (total)
+ [self waitForExpectations: @[expectation] timeout:1];
+}
+
+- (void)testOperationOneShotManyTrigger {
+ XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"];
+ toofastexpectation.inverted = YES;
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"FutureScheduler fired"];
+ expectation.assertForOverFulfill = YES;
+
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" delay: 200*NSEC_PER_MSEC keepProcessAlive:true block:^{}];
+ [self addOperationFulfillingExpectations:@[expectation,toofastexpectation] scheduler:scheduler];
+
+ [scheduler trigger];
+ [scheduler trigger];
+ [scheduler trigger];
+ [scheduler trigger];
+ [scheduler trigger];
+ [scheduler trigger];
+ [scheduler trigger];
+ [scheduler trigger];
+
+ // Make sure it waits at least 0.1 seconds
+ [self waitForExpectations: @[toofastexpectation] timeout:0.1];
+
+ // But finishes within .6s (total)
+ [self waitForExpectations: @[expectation] timeout:0.5];
+
+ // Ensure we don't get called again in the next 0.3 s
+ XCTestExpectation* waitmore = [self expectationWithDescription:@"waiting"];
+ waitmore.inverted = YES;
+ [self waitForExpectations: @[waitmore] timeout: 0.3];
+}
+
+
+- (void)testOperationMultiShot {
+ XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"];
+
+ XCTestExpectation *second = [self expectationWithDescription:@"FutureScheduler fired (two)"];
+
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" delay: 100*NSEC_PER_MSEC keepProcessAlive:false block:^{}];
+
+ [self addOperationFulfillingExpectations:@[first] scheduler:scheduler];
+
+ [scheduler trigger];
+
+ [self waitForExpectations: @[first] timeout:0.2];
+
+ [self addOperationFulfillingExpectations:@[second] scheduler:scheduler];
+
+ [scheduler trigger];
+ [scheduler trigger];
+ [scheduler trigger];
+
+ [self waitForExpectations: @[second] timeout:0.2];
+
+ XCTestExpectation* waitmore = [self expectationWithDescription:@"waiting"];
+ waitmore.inverted = YES;
+ [self waitForExpectations: @[waitmore] timeout: 0.2];
+}
+
+- (void)testOperationMultiShotDelays {
+ XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"];
+
+ XCTestExpectation *longdelay = [self expectationWithDescription:@"FutureScheduler fired (long delay expectation)"];
+ longdelay.inverted = YES;
+ XCTestExpectation *second = [self expectationWithDescription:@"FutureScheduler fired (two)"];
+
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" initialDelay: 50*NSEC_PER_MSEC continuingDelay:300*NSEC_PER_MSEC keepProcessAlive:false block:^{}];
+
+ [self addOperationFulfillingExpectations:@[first] scheduler:scheduler];
+
+ [scheduler trigger];
+
+ [self waitForExpectations: @[first] timeout:0.2];
+
+ [self addOperationFulfillingExpectations:@[second,longdelay] scheduler:scheduler];
+
+ [scheduler trigger];
+ [scheduler trigger];
+ [scheduler trigger];
+
+ // longdelay shouldn't be fulfilled in the first 0.2 seconds
+ [self waitForExpectations: @[longdelay] timeout:0.2];
+
+ // But second should be fulfilled in the next 0.5 seconds
+ [self waitForExpectations: @[second] timeout:0.5];
+
+ XCTestExpectation* waitmore = [self expectationWithDescription:@"waiting"];
+ waitmore.inverted = YES;
+ [self waitForExpectations: @[waitmore] timeout: 0.2];
+}
+
+- (void)testOperationCancel {
+ XCTestExpectation *cancelexpectation = [self expectationWithDescription:@"FutureScheduler fired (after cancel)"];
+ cancelexpectation.inverted = YES;
+
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" delay: 100*NSEC_PER_MSEC keepProcessAlive:true block:^{}];
+
+ [self addOperationFulfillingExpectations:@[cancelexpectation] scheduler:scheduler];
+
+ [scheduler trigger];
+ [scheduler cancel];
+
+ // Make sure it does not fire in 0.5 s
+ [self waitForExpectations: @[cancelexpectation] timeout:0.2];
+}
+
+- (void)testOperationDelayedNoShot {
+ XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"];
+ toofastexpectation.inverted = YES;
+
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" delay: 10*NSEC_PER_MSEC keepProcessAlive:false block:^{}];
+ [self addOperationFulfillingExpectations:@[toofastexpectation] scheduler:scheduler];
+
+ // Tell the scheduler to wait, but don't trigger it. It shouldn't fire.
+ [scheduler waitUntil: 50*NSEC_PER_MSEC];
+
+ [self waitForExpectations: @[toofastexpectation] timeout:0.1];
+}
+
+- (void)testOperationDelayedOneShot {
+ XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"];
+ first.assertForOverFulfill = NO;
+
+ XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"];
+ toofastexpectation.inverted = YES;
+
+ CKKSNearFutureScheduler* scheduler = [[CKKSNearFutureScheduler alloc] initWithName: @"test" delay: 10*NSEC_PER_MSEC keepProcessAlive:false block:^{}];
+ [self addOperationFulfillingExpectations:@[first,toofastexpectation] scheduler:scheduler];
+
+ [scheduler waitUntil: 150*NSEC_PER_MSEC];
+ [scheduler trigger];
+
+ [self waitForExpectations: @[toofastexpectation] timeout:0.1];
+ [self waitForExpectations: @[first] timeout:0.5];
+}
+
@end
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#include <Security/SecureObjectSync/SOSCloudCircle.h>
+#include <Security/SecureObjectSync/SOSAccountPriv.h>
#include <Security/SecureObjectSync/SOSAccount.h>
#include <Security/SecureObjectSync/SOSInternal.h>
#include <Security/SecureObjectSync/SOSFullPeerInfo.h>
+#pragma clang diagnostic pop
+
#include <Security/SecKey.h>
#include <Security/SecKeyPriv.h>
#pragma clang diagnostic pop
self.zones[self.engramZoneID] = self.engramZone;
self.engramView = [[CKKSViewManager manager] findView:@"Engram"];
XCTAssertNotNil(self.engramView, "CKKSViewManager created the Engram view");
+ [self.ckksZones addObject:self.engramZoneID];
self.manateeZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Manatee" ownerName:CKCurrentUserDefaultName];
self.manateeZone = [[FakeCKZone alloc] initZone: self.manateeZoneID];
self.zones[self.manateeZoneID] = self.manateeZone;
self.manateeView = [[CKKSViewManager manager] findView:@"Manatee"];
XCTAssertNotNil(self.manateeView, "CKKSViewManager created the Manatee view");
+ [self.ckksZones addObject:self.manateeZoneID];
self.autoUnlockZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"AutoUnlock" ownerName:CKCurrentUserDefaultName];
self.autoUnlockZone = [[FakeCKZone alloc] initZone: self.autoUnlockZoneID];
self.zones[self.autoUnlockZoneID] = self.autoUnlockZone;
self.autoUnlockView = [[CKKSViewManager manager] findView:@"AutoUnlock"];
XCTAssertNotNil(self.autoUnlockView, "CKKSViewManager created the AutoUnlock view");
+ [self.ckksZones addObject:self.autoUnlockZoneID];
self.healthZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"Health" ownerName:CKCurrentUserDefaultName];
self.healthZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
self.zones[self.healthZoneID] = self.healthZone;
self.healthView = [[CKKSViewManager manager] findView:@"Health"];
XCTAssertNotNil(self.healthView, "CKKSViewManager created the Health view");
+ [self.ckksZones addObject:self.healthZoneID];
self.applepayZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"ApplePay" ownerName:CKCurrentUserDefaultName];
self.applepayZone = [[FakeCKZone alloc] initZone: self.healthZoneID];
self.zones[self.applepayZoneID] = self.applepayZone;
self.applepayView = [[CKKSViewManager manager] findView:@"ApplePay"];
XCTAssertNotNil(self.applepayView, "CKKSViewManager created the ApplePay view");
+ [self.ckksZones addObject:self.applepayZoneID];
}
+ (void)tearDown {
self.accountStatus = CKAccountStatusNoAccount;
[self startCKKSSubsystem];
- [self.engramView cancelAllOperations];
+ [self.engramView halt];
[self.engramView waitUntilAllOperationsAreFinished];
self.engramView = nil;
- [self.manateeView cancelAllOperations];
+ [self.manateeView halt];
[self.manateeView waitUntilAllOperationsAreFinished];
self.manateeView = nil;
- [self.autoUnlockView cancelAllOperations];
+ [self.autoUnlockView halt];
[self.autoUnlockView waitUntilAllOperationsAreFinished];
self.autoUnlockView = nil;
- [self.healthView cancelAllOperations];
+ [self.healthView halt];
[self.healthView waitUntilAllOperationsAreFinished];
self.healthView = nil;
- [self.applepayView cancelAllOperations];
+ [self.applepayView halt];
[self.applepayView waitUntilAllOperationsAreFinished];
self.applepayView = nil;
}
-(void)saveFakeKeyHierarchiesToLocalDatabase {
- [self createAndSaveFakeKeyHierarchy: self.engramZoneID];
- [self createAndSaveFakeKeyHierarchy: self.manateeZoneID];
- [self createAndSaveFakeKeyHierarchy: self.autoUnlockZoneID];
- [self createAndSaveFakeKeyHierarchy: self.healthZoneID];
- [self createAndSaveFakeKeyHierarchy: self.applepayZoneID];
+ for(CKRecordZoneID* zoneID in self.ckksZones) {
+ [self createAndSaveFakeKeyHierarchy: zoneID];
+ }
}
-(void)testAddEngramManateeItems {
[self saveFakeKeyHierarchiesToLocalDatabase]; // Make life easy for this test.
[self startCKKSSubsystem];
+ for(CKRecordZoneID* zoneID in self.ckksZones) {
+ [self expectCKKSTLKSelfShareUpload:zoneID];
+ }
+
[self waitForKeyHierarchyReadinesses];
[self findGenericPassword:@"account-delete-me" expecting:errSecItemNotFound];
tempPath = [[[[NSFileManager defaultManager] temporaryDirectory] URLByAppendingPathComponent:@"PiggyPacket"] path];
});
- NSLog(@"using temp path: %@", tempPath);
return tempPath;
}
[self putFakeKeyHierachiesInCloudKit];
[self saveTLKsToKeychain];
+ for(CKRecordZoneID* zoneID in self.ckksZones) {
+ [self expectCKKSTLKSelfShareUpload:zoneID];
+ }
[self startCKKSSubsystem];
[self waitForKeyHierarchyReadinesses];
NSArray<NSDictionary *>* sortedTLKs = SOSAccountSortTLKS(tlks);
XCTAssertNotNil(sortedTLKs, "sortedTLKs not set");
- NSLog(@"TLKs: %@", tlks);
- NSLog(@"sortedTLKs: %@", sortedTLKs);
-
NSArray<NSString *> *expectedOrder = @[ @"11111111", @"22222222", @"33333333", @"44444444", @"55555555"];
[sortedTLKs enumerateObjectsUsingBlock:^(NSDictionary *tlk, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *uuid = tlk[@"acct"];
NSDictionary* piggyTLKS = [self SOSPiggyBackCopyFromKeychain];
[self SOSPiggyBackAddToKeychain:piggyTLKS];
[self deleteTLKMaterialsFromKeychain];
-
+
+ // The CKKS subsystem should write a TLK Share for each view
+ for(CKRecordZoneID* zoneID in self.ckksZones) {
+ [self expectCKKSTLKSelfShareUpload:zoneID];
+ }
+
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
-
- // The CKKS subsystem should not try to write anything to the CloudKit database while it's accepting the keys
+
[self.manateeView waitForKeyHierarchyReadiness];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
[self startCKKSSubsystem];
// The CKKS subsystem should not try to write anything to the CloudKit database.
- sleep(1);
-
+ XCTAssertEqual(0, [self.manateeView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:400*NSEC_PER_SEC], "CKKS entered waitfortlk");
+
OCMVerifyAllWithDelay(self.mockDatabase, 8);
- // Now, save the TLK to the keychain (to simulate it coming in later via piggybacking).
+ // Now, save the TLKs to the keychain (to simulate them coming in later via piggybacking).
+ for(CKRecordZoneID* zoneID in self.ckksZones) {
+ [self expectCKKSTLKSelfShareUpload:zoneID];
+ }
+
[self SOSPiggyBackAddToKeychain:piggyData];
[self waitForKeyHierarchyReadinesses];
#if OCTAGON
#import <XCTest/XCTest.h>
+#import <Security/Security.h>
+#import <Security/SecItemPriv.h>
#import "CloudKitMockXCTest.h"
#import "keychain/ckks/CKKS.h"
attrs = CFDictionaryCreateMutable( NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks );
CFDictionarySetValue( attrs, kSecClass, kSecClassGenericPassword );
- CFDictionarySetValue( attrs, kSecAttrAccessible, kSecAttrAccessibleAlways );
+ CFDictionarySetValue( attrs, kSecAttrAccessible, kSecAttrAccessibleAlwaysPrivate );
CFDictionarySetValue( attrs, kSecAttrLabel, CFSTR( "TestLabel" ) );
CFDictionarySetValue( attrs, kSecAttrDescription, CFSTR( "TestDescription" ) );
CFDictionarySetValue( attrs, kSecAttrAccount, CFSTR( "TestAccount" ) );
self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassAPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassAKey;
self.keychainZoneKeys.currentClassAPointer.currentKeyUUID = oldClassAKey.recordID.recordName;
- // CKKS should then fix the pointers, but not update any keys
- [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 zoneID:self.keychainZoneID];
+ // CKKS should then fix the pointers and give itself a TLK share, but not update any keys
+ [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 1 zoneID:self.keychainZoneID];
// And then upload the record as normal
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey] = oldClassCKey;
self.keychainZoneKeys.currentClassCPointer.currentKeyUUID = oldClassCKey.recordID.recordName;
- // CKKS should then fix the pointers, but not update any keys
- [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 zoneID:self.keychainZoneID];
+ // CKKS should then fix the pointers and its TLK shares, but not update any keys
+ [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:1 tlkShareRecords:1 zoneID:self.keychainZoneID];
// And then upload the record as normal
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
// Test starts with a good key hierarchy in our fake CloudKit, and the TLK already arrived.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
CKReference* oldClassCKey = self.keychainZone.currentDatabase[self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID][SecCKRecordParentKeyRefKey];
[self.keychainView notifyZoneChange:nil];
- // CKKS should then fix the pointers, but not update any keys
- [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 1 tlkShareRecords: 0 zoneID:self.keychainZoneID];
+ // CKKS should then fix the pointers and give itself a new TLK share record, but not update any keys
+ [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:1 tlkShareRecords:1 zoneID:self.keychainZoneID];
[self.keychainView notifyZoneChange:nil];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
// Test starts with a good key hierarchy in our fake CloudKit, and the TLK already arrived.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
CKRecordID* classCPointerRecordID = self.keychainZoneKeys.currentClassCPointer.storedCKRecord.recordID;
}];
// CKKS should try to fix the pointers, but be rejected (since someone else has already fixed them)
- // It should not try again, because someone already fixed them
+ // It should not try to modify the pointers again, but it should give itself the new TLK
[self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self.keychainView notifyZoneChange:nil];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
CKReference* oldClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
[self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
// Spin up CKKS subsystem.
checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
[self addGenericPassword: @"data" account: @"account-delete-me"];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ [self waitForCKModifications];
// Now, break the class C pointer, but don't tell CKKS
CKReference* newClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey];
key = [share recoverTLK:self.remotePeer trustedPeers:[NSSet set] error:&error];
XCTAssertNil(key, "No key should have been extracted when no trusted peers exist");
- XCTAssertNotNil(error, "Should have produced an error when failing to extract a key");
+ XCTAssertNotNil(error, "Should have produced an error when failing to extract a key with no trusted peers");
error = nil;
key = [share recoverTLK:self.remotePeer2 trustedPeers:peers error:&error];
XCTAssertNil(key, "No key should have been extracted when using the wrong key");
- XCTAssertNotNil(error, "Should have produced an error when failing to extract a key");
+ XCTAssertNotNil(error, "Should have produced an error when failing to extract with the wrong key");
error = nil;
CKKSTLKShare* shareSignature = [share copy];
shareSignature.signature = [NSMutableData dataWithLength:shareSignature.signature.length];
key = [shareSignature recoverTLK:self.remotePeer trustedPeers:peers error:&error];
XCTAssertNil(key, "No key should have been extracted when signature fails to verify");
- XCTAssertNotNil(error, "Should have produced an error when failing to extract a key");
+ XCTAssertNotNil(error, "Should have produced an error when failing to extract a key with an invalid signature");
error = nil;
CKKSTLKShare* shareUUID = [share copy];
shareUUID.tlkUUID = [[NSUUID UUID] UUIDString];
key = [shareUUID recoverTLK:self.remotePeer trustedPeers:peers error:&error];
XCTAssertNil(key, "No key should have been extracted when uuid has changed");
- XCTAssertNotNil(error, "Should have produced an error when failing to extract a key");
+ XCTAssertNotNil(error, "Should have produced an error when failing to extract a key after uuid has changed");
error = nil;
}
[share2 saveToDatabase:&error];
XCTAssertNil(error, "No error saving share2 to database");
- /*
- * DISABLE FOR NOW:
- * a bad rebase made the implementation of this function not be avilable
- * yet
CKKSTLKShare* loadedShare2 = [CKKSTLKShare tryFromDatabaseFromCKRecordID:record.recordID error:&error];
XCTAssertNil(error, "No error loading loadedShare2 from database");
XCTAssertNotNil(loadedShare2, "Should have received a CKKSTLKShare from the database");
XCTAssert([loadedShare2 verifySignature:loadedShare2.signature verifyingPeer:self.localPeer error:&error], "Signature with extra data should verify after save/load");
- */
}
@end
- (void)setUp {
[super setUp];
- SecCKKSSetShareTLKs(true);
self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1"
encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]]
self.untrustedPeer = nil;
[super tearDown];
-
- SecCKKSSetShareTLKs(false);
}
- (void)testAcceptExistingTLKSharedKeyHierarchy {
// Test also starts with the TLK shared to all trusted peers from peer1
[self putTLKSharesInCloudKit:self.keychainZoneKeys.tlk from:self.remotePeer1 zoneID:self.keychainZoneID];
- // Because 33710924 didn't make it backwards in time, this test is fragile on Chipmunk/Cinar.
self.aksLockState = true;
[self.lockStateTracker recheck];
}
- (void)testUploadTLKSharesForExistingHierarchyOnRestart {
- // Turn off TLK sharing, and get situated
- SecCKKSSetShareTLKs(false);
-
+ // Bring up CKKS. It'll upload a few TLK Shares, but we'll delete them to get into state
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
+
+ [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
[self startCKKSSubsystem];
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:10*NSEC_PER_SEC], "Key state should become ready");
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ [self waitForCKModifications];
+
+ // Now, delete all the TLK Shares, so CKKS will upload them again
+ [self.keychainView dispatchSync:^bool {
+ NSError* error = nil;
+ [CKKSTLKShare deleteAll:self.keychainZoneID error:&error];
+ XCTAssertNil(error, "Shouldn't be an error deleting all TLKShares");
- // Turn TLK sharing back on, and restart. We expect an upload of 3 TLK shares.
- SecCKKSSetShareTLKs(true);
+ NSArray<CKRecord*>* records = [self.zones[self.keychainZoneID].currentDatabase allValues];
+ for(CKRecord* record in records) {
+ if([record.recordType isEqualToString:SecCKRecordTLKShareType]) {
+ [self.zones[self.keychainZoneID] deleteFromHistory:record.recordID];
+ }
+ }
+
+ return true;
+ }];
+
+ // Restart. We expect an upload of 3 TLK shares.
[self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:3 zoneID:self.keychainZoneID];
self.keychainView = [self.injectedManager restartZone: self.keychainZoneID.zoneName];
// The CKKS subsystem should now accept the key, and share the TLK back to itself
[self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
- XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:500*NSEC_PER_SEC], "Key state should become ready");
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
// And use it as well
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
[self.keychainView waitUntilAllOperationsAreFinished];
}
+- (void)testFillInMissingPeerSharesAfterUnlock {
+ // step 1: add a new peer; we should share the TLK with them
+ // start with no trusted peers
+ [self.currentPeers removeAllObjects];
+
+ [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
+ [self startCKKSSubsystem];
+
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
+
+ [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
+ checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
+ [self addGenericPassword: @"data" account: @"account-delete-me"];
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+
+ // Now, lock.
+ self.aksLockState = true;
+ [self.lockStateTracker recheck];
+
+ // New peer arrives! This can't actually happen (since we have to be unlocked to accept a new peer), but this will exercise CKKS
+ [self.currentPeers addObject:self.remotePeer1];
+ [self.injectedManager sendTrustedPeerSetChangedUpdate];
+
+ // CKKS should notice that it has things to do...
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
+
+ // And do them.
+ [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
+ self.aksLockState = false;
+ [self.lockStateTracker recheck];
+
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+
+ // and return to ready
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
+}
+
+- (void)testAddItemDuringNewTLKSharesOnTrustSetAddition {
+ // step 1: add a new peer; we should share the TLK with them
+ // start with no trusted peers
+ [self.currentPeers removeAllObjects];
+
+ [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
+ [self startCKKSSubsystem];
+
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ [self waitForCKModifications];
+
+ // Hold the TLK share modification
+ [self holdCloudKitModifications];
+
+ [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:self.keychainZoneID];
+ [self.currentPeers addObject:self.remotePeer1];
+ [self.injectedManager sendTrustedPeerSetChangedUpdate];
+
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+
+ // While CloudKit is hanging the write, add an item
+ [self addGenericPassword: @"data" account: @"account-delete-me"];
+
+ // After that returns, release the write. CKKS should upload the new item
+ [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
+ checkItem:[self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
+ [self releaseCloudKitModificationHold];
+
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+}
+
- (void)testSendNewTLKSharesOnTrustSetRemoval {
// Not implemented. Trust set removal demands a key roll, but let's not get ahead of ourselves...
}
#import <Foundation/Foundation.h>
#import "keychain/ckks/tests/CKKSTests.h"
+NS_ASSUME_NONNULL_BEGIN
+
@interface CloudKitKeychainSyncingTests (APITests)
-- (BOOL (^) (CKRecord*)) checkPCSFieldsBlock: (CKRecordZoneID*) zoneID
- PCSServiceIdentifier:(NSNumber*)servIdentifier
- PCSPublicKey:(NSData*)publicKey
- PCSPublicIdentity:(NSData*)publicIdentity;
+- (BOOL (^)(CKRecord*))checkPCSFieldsBlock:(CKRecordZoneID*)zoneID
+ PCSServiceIdentifier:(NSNumber*)servIdentifier
+ PCSPublicKey:(NSData*)publicKey
+ PCSPublicIdentity:(NSData*)publicIdentity;
--(NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
- data:(NSData*)data
- serviceIdentifier:(NSNumber*)serviceIdentifier
- publicKey:(NSData*)publicKey
- publicIdentity:(NSData*)publicIdentity;
+- (NSMutableDictionary*)pcsAddItemQuery:(NSString*)account
+ data:(NSData*)data
+ serviceIdentifier:(NSNumber*)serviceIdentifier
+ publicKey:(NSData*)publicKey
+ publicIdentity:(NSData*)publicIdentity;
--(NSDictionary*)pcsAddItem:(NSString*)account
- data:(NSData*)data
- serviceIdentifier:(NSNumber*)serviceIdentifier
- publicKey:(NSData*)publicKey
- publicIdentity:(NSData*)publicIdentity
- expectingSync:(bool)expectingSync;
+- (NSDictionary*)pcsAddItem:(NSString*)account
+ data:(NSData*)data
+ serviceIdentifier:(NSNumber*)serviceIdentifier
+ publicKey:(NSData*)publicKey
+ publicIdentity:(NSData*)publicIdentity
+ expectingSync:(bool)expectingSync;
@end
+
+NS_ASSUME_NONNULL_END
[self startCKKSSubsystem];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
- // Due to item UUID selection, this item will be added with UUID DD7C2F9B-B22D-3B90-C299-E3B48174BFA3.
+ // Due to item UUID selection, this item will be added with UUID 50184A35-4480-E8BA-769B-567CF72F1EC0.
// Add it to CloudKit first!
- CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3"];
+ CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"50184A35-4480-E8BA-769B-567CF72F1EC0"];
[self.keychainZone addToZone: ckr];
(id)kSecAttrAccount : @"testaccount",
(id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
(id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
+ (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // @ fake view hint for fake view
} mutableCopy];
XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
self.silentFetchesAllowed = false;
[self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.loggedOut wait:2*NSEC_PER_SEC], "CKKS should positively log out");
+
+ NSMutableDictionary* query = [@{
+ (id)kSecClass : (id)kSecClassGenericPassword,
+ (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
+ (id)kSecAttrAccessible: (id)kSecAttrAccessibleAfterFirstUnlock,
+ (id)kSecAttrAccount : @"testaccount",
+ (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
+ (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
+ } mutableCopy];
+
+ XCTestExpectation* blockExpectation = [self expectationWithDescription: @"callback occurs"];
+
+ XCTAssertEqual(errSecSuccess, _SecItemAddAndNotifyOnSync((__bridge CFDictionaryRef) query, NULL, ^(bool didSync, CFErrorRef error) {
+ XCTAssertFalse(didSync, "Item did not sync (with no iCloud account)");
+ XCTAssertNotNil((__bridge NSError*)error, "Error exists syncing item while logged out");
+
+ [blockExpectation fulfill];
+ }), @"_SecItemAddAndNotifyOnSync succeeded");
+
+ [self waitForExpectationsWithTimeout:5.0 handler:nil];
+}
+
+- (void)testAddAndNotifyOnSyncAccountStatusUnclear {
+ // Test starts with nothing in database, but CKKS hasn't been told we've logged out yet.
+ // We expect no CKKS operations.
+ self.accountStatus = CKAccountStatusNoAccount;
+ self.silentFetchesAllowed = false;
+
NSMutableDictionary* query = [@{
(id)kSecClass : (id)kSecClassGenericPassword,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
[blockExpectation fulfill];
}), @"_SecItemAddAndNotifyOnSync succeeded");
+ // And now, allow CKKS to discover we're logged out
+ [self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.loggedOut wait:2*NSEC_PER_SEC], "CKKS should positively log out");
+
[self waitForExpectationsWithTimeout:5.0 handler:nil];
}
// Test starts with a key hierarchy in cloudkit and the TLK having arrived
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
// But block CloudKit fetches (so the key hierarchy won't be ready when we add this new item)
[self holdCloudKitFetches];
[self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.loggedIn wait:2*NSEC_PER_SEC], "CKKS should log in");
[self.keychainView.viewSetupOperation waitUntilFinished];
NSMutableDictionary* query = [@{
}
- (void)testPCSUnencryptedFieldsAdd {
-
[self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
[self startCKKSSubsystem];
- [self.keychainView waitUntilAllOperationsAreFinished];
+ [self.keychainView waitForKeyHierarchyReadiness];
NSNumber* servIdentifier = @3;
NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
(id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
(id)kSecAttrPCSPlaintextPublicKey : publicKey,
(id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
+ (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
} mutableCopy];
XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], publicIdentity, "public identity exists");
// Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
- // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
+ // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
[self waitForCKModifications];
- CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
+ CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
CKRecord* record = self.keychainZone.currentDatabase[recordID];
XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
[self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
[self startCKKSSubsystem];
- [self.keychainView waitUntilAllOperationsAreFinished];
+ [self.keychainView waitForKeyHierarchyReadiness];
NSNumber* servIdentifier = @3;
NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
(id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
(id)kSecAttrPCSPlaintextPublicKey : publicKey,
(id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
+ (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // allows a CKKSScanOperation to find this item
} mutableCopy];
XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
XCTAssertEqualObjects(itemAttributes[(id)kSecAttrPCSPlaintextPublicIdentity], newPublicIdentity, "public identity exists");
// Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
- // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
+ // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
[self waitForCKModifications];
- CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
+ CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
CKRecord* record = self.keychainZone.currentDatabase[recordID];
XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
(id)kSecAttrPCSPlaintextServiceIdentifier : servIdentifier,
(id)kSecAttrPCSPlaintextPublicKey : publicKey,
(id)kSecAttrPCSPlaintextPublicIdentity : publicIdentity,
+ (id)kSecAttrSyncViewHint : self.keychainView.zoneName, // fake, for CKKSScanOperation
} mutableCopy];
XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef) query, NULL), @"SecItemAdd succeeded");
[self waitForCKModifications];
// Find the item record in CloudKit. Since we're using kSecAttrDeriveSyncIDFromItemAttributes,
- // the record ID is likely DD7C2F9B-B22D-3B90-C299-E3B48174BFA3
- CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
+ // the record ID is likely 50184A35-4480-E8BA-769B-567CF72F1EC0
+ CKRecordID* recordID = [[CKRecordID alloc] initWithRecordName: @"50184A35-4480-E8BA-769B-567CF72F1EC0" zoneID:self.keychainZoneID];
CKRecord* record = self.keychainZone.currentDatabase[recordID];
XCTAssertNotNil(record, "Found record in CloudKit at expected UUID");
-(void)testResetLocal {
// Test starts with nothing in database, but one in our fake CloudKit.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// Spin up CKKS subsystem.
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
self.silentFetchesAllowed = false;
- // Test starts with local TLK and key hierarhcy in our fake cloudkit
+ // Test starts with local TLK and key hierarchy in our fake cloudkit
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
+
NSData* changeTokenData = [[[NSUUID UUID] UUIDString] dataUsingEncoding:NSUTF8StringEncoding];
CKServerChangeToken* changeToken = [[CKServerChangeToken alloc] initWithData:changeTokenData];
[self.keychainView dispatchSync: ^bool{
dispatch_semaphore_t resetSemaphore = dispatch_semaphore_create(0);
[self.injectedManager rpcResetLocal:nil reply:^(NSError* result) {
- XCTAssertNil(result, "no error resetting cloudkit");
+ XCTAssertNil(result, "no error resetting local");
secnotice("ckks", "Received a rpcResetLocal callback");
dispatch_semaphore_signal(resetSemaphore);
}];
}];
// Now log in, and see what happens! It should re-fetch, pick up the old key hierarchy, and use it
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
self.silentFetchesAllowed = true;
self.circleStatus = kSOSCCInCircle;
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
-(void)testResetCloudKitZone {
// Test starts with nothing in database, but one in our fake CloudKit.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// Spin up CKKS subsystem.
// Now, reset everything. The outgoingOp should get cancelled.
// We expect a key hierarchy upload, and then the class C item upload
- [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords:3 zoneID:self.keychainZoneID];
+ [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
- [self.keychainView.viewSetupOperation waitUntilFinished];
- // Reset setup, since that's the most likely state to be in (33866282)
- [self.keychainView resetSetup];
-
CKRecord* ckr = [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85"];
[self.keychainZone addToZone: ckr];
}];
[self waitForExpectations:@[callbackOccurs] timeout:5.0];
+
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
- (void)testRPCTLKMissingWhenFound {
// Bring CKKS up in waitfortlk
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self startCKKSSubsystem];
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "CKKS entered 'ready''");
}];
[self waitForExpectations:@[callbackOccurs] timeout:5.0];
+
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
@end
#import "keychain/ckks/CKKSMirrorEntry.h"
#import "keychain/ckks/tests/MockCloudKit.h"
+#import "keychain/ckks/tests/AutoreleaseTest.h"
#import "keychain/ckks/tests/CKKSTests.h"
#import "keychain/ckks/tests/CKKSTests+API.h"
});
[self waitForExpectationsWithTimeout:8.0 handler:nil];
}
--(void)fetchCurrentPointerExpectingError:(bool)cached
+-(void)fetchCurrentPointerExpectingError:(bool)fetchCloudValue
{
XCTestExpectation* currentExpectation = [self expectationWithDescription: @"callback occurs"];
+ //TEST_API_AUTORELEASE_BEFORE(SecItemFetchCurrentItemAcrossAllDevices);
SecItemFetchCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
(__bridge CFStringRef)@"pcsservice",
(__bridge CFStringRef)@"keychain",
- cached,
+ fetchCloudValue,
^(CFDataRef currentPersistentRef, CFErrorRef cferror) {
XCTAssertNil((__bridge id)currentPersistentRef, "no current item exists");
XCTAssertNotNil((__bridge id)cferror, "Error exists when there's a current item");
[currentExpectation fulfill];
});
+ //TEST_API_AUTORELEASE_AFTER(SecItemFetchCurrentItemAcrossAllDevices);
[self waitForExpectationsWithTimeout:8.0 handler:nil];
}
[self startCKKSSubsystem];
[self.keychainView waitForKeyHierarchyReadiness];
+ [self.keychainView waitUntilAllOperationsAreFinished]; // ensure everything finishes before we disallow fetches
// Ensure that local queries don't hit the server.
self.silentFetchesAllowed = false;
// Ensure that setting the current pointer sends a notification
keychainChanged = [self expectChangeForView:self.keychainZoneID.zoneName];
+ //TEST_API_AUTORELEASE_BEFORE(SecItemSetCurrentItemAcrossAllDevices);
SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
(__bridge CFStringRef)@"pcsservice",
(__bridge CFStringRef)@"keychain",
XCTAssertNil(error, "No error setting current item");
[setCurrentExpectation fulfill];
});
+ //TEST_API_AUTORELEASE_AFTER(SecItemSetCurrentItemAcrossAllDevices);
OCMVerifyAllWithDelay(self.mockDatabase, 8);
[self waitForExpectations:@[keychainChanged] timeout:1];
[self waitForCKModifications];
SecResetLocalSecuritydXPCFakeEntitlements();
}
+-(void)testPCSCurrentSetConflictedItemAsCurrent {
+ SecResetLocalSecuritydXPCFakeEntitlements();
+ SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSPlaintextFields, kCFBooleanTrue);
+ SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSWriteCurrentItemPointers, kCFBooleanTrue);
+ SecAddLocalSecuritydXPCFakeEntitlement(kSecEntitlementPrivateCKKSReadCurrentItemPointers, kCFBooleanTrue);
+
+ NSNumber* servIdentifier = @3;
+ NSData* publicKey = [@"asdfasdf" dataUsingEncoding:NSUTF8StringEncoding];
+ NSData* publicIdentity = [@"somedata" dataUsingEncoding:NSUTF8StringEncoding];
+
+ [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
+ [self startCKKSSubsystem];
+
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have become ready");
+ [self.keychainView waitUntilAllOperationsAreFinished];
+
+ // Before CKKS can add the item, shove a conflicting one into CloudKit
+
+ NSError* error = nil;
+
+ NSString* account = @"testaccount";
+
+ // Create an item in CloudKit that will conflict in both UUID and primary key
+ NSData* itemdata = [[NSData alloc] initWithBase64EncodedString:@"YnBsaXN0MDDbAQIDBAUGBwgJCgsMDQ4PEBESEhMUFVZ2X0RhdGFUYWNjdFR0b21iVHN2Y2VUc2hhMVRtdXNyVGNkYXRUbWRhdFRwZG1uVGFncnBVY2xhc3NEYXNkZlt0ZXN0YWNjb3VudBAAUE8QFF7OzuEEGWTTwzzSp/rjY6ubHW2rQDNBv7zNQtQUQFJja18QF2NvbS5hcHBsZS5zZWN1cml0eS5ja2tzVGdlbnAIHyYrMDU6P0RJTlNZXmpsbYSFjpGrAAAAAAAAAQEAAAAAAAAAFgAAAAAAAAAAAAAAAAAAALA=" options:0];
+ NSMutableDictionary * item = [[NSPropertyListSerialization propertyListWithData:itemdata
+ options:0
+ format:nil
+ error:&error] mutableCopy];
+ XCTAssertNil(error, "Error should be nil parsing base64 item");
+
+ item[@"v_Data"] = [@"conflictingdata" dataUsingEncoding:NSUTF8StringEncoding];
+ CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
+ CKRecord* mismatchedRecord = [self newRecord:ckrid withNewItemData:item];
+ [self.keychainZone addToZone: mismatchedRecord];
+
+ [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
+ NSDictionary* result = [self pcsAddItem:account
+ data:[@"asdf" dataUsingEncoding:NSUTF8StringEncoding]
+ serviceIdentifier:(NSNumber*)servIdentifier
+ publicKey:(NSData*)publicKey
+ publicIdentity:(NSData*)publicIdentity
+ expectingSync:false];
+ XCTAssertNotNil(result, "Should receive result from adding item");
+
+ NSData* persistentRef = result[(id)kSecValuePersistentRef];
+ NSData* sha1 = result[(id)kSecAttrSHA1];
+
+ // Set the current pointer to the result of adding this item. This should fail.
+ XCTestExpectation* setCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
+ SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
+ (__bridge CFStringRef)@"pcsservice",
+ (__bridge CFStringRef)@"keychain",
+ (__bridge CFDataRef)persistentRef,
+ (__bridge CFDataRef)sha1, NULL, NULL, ^ (CFErrorRef cferror) {
+ XCTAssertNotNil((__bridge NSError*)cferror, "Should error setting current item to hash of item which failed to sync");
+ [setCurrentExpectation fulfill];
+ });
+
+ [self waitForExpectations:@[setCurrentExpectation] timeout:8.0];
+
+ // Reissue a fetch and find the new persistent ref and sha1 for the item at this UUID
+ [self.keychainView waitForFetchAndIncomingQueueProcessing];
+
+ // The conflicting item update should have won
+ [self checkGenericPassword:@"conflictingdata" account:account];
+
+ NSDictionary *query = @{(id)kSecClass : (id)kSecClassGenericPassword,
+ (id)kSecAttrAccessGroup : @"com.apple.security.ckks",
+ (id)kSecAttrAccount : account,
+ (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
+ (id)kSecMatchLimit : (id)kSecMatchLimitOne,
+ (id)kSecReturnAttributes: @YES,
+ (id)kSecReturnPersistentRef: @YES,
+ };
+
+ CFTypeRef cfresult = NULL;
+ XCTAssertEqual(errSecSuccess, SecItemCopyMatching((__bridge CFDictionaryRef) query, &cfresult), "Finding item %@", account);
+ NSDictionary* newResult = CFBridgingRelease(cfresult);
+ XCTAssertNotNil(newResult, "Received an item");
+
+ NSData* newPersistentRef = newResult[(id)kSecValuePersistentRef];
+ NSData* newSha1 = newResult[(id)kSecAttrSHA1];
+
+ [self expectCKModifyRecords:@{SecCKRecordCurrentItemType: [NSNumber numberWithUnsignedInteger: 1]}
+ deletedRecordTypeCounts:nil
+ zoneID:self.keychainZoneID
+ checkModifiedRecord:nil
+ runAfterModification:nil];
+
+ XCTestExpectation* newSetCurrentExpectation = [self expectationWithDescription: @"callback occurs"];
+ SecItemSetCurrentItemAcrossAllDevices((__bridge CFStringRef)@"com.apple.security.ckks",
+ (__bridge CFStringRef)@"pcsservice",
+ (__bridge CFStringRef)@"keychain",
+ (__bridge CFDataRef)newPersistentRef,
+ (__bridge CFDataRef)newSha1, NULL, NULL, ^ (CFErrorRef cferror) {
+ XCTAssertNil((__bridge NSError*)cferror, "Shouldn't error setting current item");
+ [newSetCurrentExpectation fulfill];
+ });
+
+ [self waitForExpectations:@[newSetCurrentExpectation] timeout:8.0];
+
+ SecResetLocalSecuritydXPCFakeEntitlements();
+}
+
@end
#endif // OCTAGON
*/
#import <CloudKit/CloudKit.h>
-#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
#include <Security/SecItemPriv.h>
-#import "keychain/ckks/tests/CloudKitMockXCTest.h"
-#import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
-#import "keychain/ckks/CKKSManifest.h"
#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSKeychainView.h"
+#import "keychain/ckks/CKKSManifest.h"
+#import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
+#import "keychain/ckks/tests/CloudKitMockXCTest.h"
#import "keychain/ckks/tests/MockCloudKit.h"
+NS_ASSUME_NONNULL_BEGIN
+
// 1 master manifest, 72 manifest leaf nodes = 73
// 3 keys, 3 current keys, and 1 device state entry
#define SYSTEM_DB_RECORD_COUNT (7 + ([CKKSManifest shouldSyncManifests] ? 73 : 0))
@interface CloudKitKeychainSyncingTestsBase : CloudKitKeychainSyncingMockXCTest
-@property CKRecordZoneID* keychainZoneID;
-@property CKKSKeychainView* keychainView;
-@property FakeCKZone* keychainZone;
+@property (nullable) CKRecordZoneID* keychainZoneID;
+@property (nullable) CKKSKeychainView* keychainView;
+@property (nullable) FakeCKZone* keychainZone;
-@property (readonly) ZoneKeys* keychainZoneKeys;
+@property (nullable, readonly) ZoneKeys* keychainZoneKeys;
- (ZoneKeys*)keychainZoneKeys;
@end
@interface CloudKitKeychainSyncingTests : CloudKitKeychainSyncingTestsBase
@end
+NS_ASSUME_NONNULL_END
#import <OCMock/OCMock.h>
#include <Security/SecItemPriv.h>
+#include <securityd/SecItemDb.h>
+#include <securityd/SecItemServer.h>
#import "keychain/ckks/tests/CloudKitMockXCTest.h"
#import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
self.keychainZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"keychain" ownerName:CKCurrentUserDefaultName];
self.keychainZone = [[FakeCKZone alloc] initZone: self.keychainZoneID];
+ [self.ckksZones addObject:self.keychainZoneID];
+
// Wait for the ViewManager to be brought up
XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:4*NSEC_PER_SEC], "No timeout waiting for SecCKKSInitialize");
NSDictionary* status = [self.keychainView status];
(void)status;
- [self.keychainView cancelAllOperations];
+ [self.keychainView halt];
[self.keychainView waitUntilAllOperationsAreFinished];
self.keychainView = nil;
#pragma mark - Tests
+- (void)testBringupToKeyStateReady {
+ [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
+ [self startCKKSSubsystem];
+
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:4*NSEC_PER_SEC], @"Key state should have arrived at ready");
+}
+
- (void)testAddItem {
[self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
[self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:4*NSEC_PER_SEC], @"Key state should have arrived at ready");
[self addGenericPassword: @"data" account: @"account-delete-me"];
- (void)testAddItemWithoutUUID {
// Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
[self startCKKSSubsystem];
// Right now, the write in CloudKit is pending. Make the local modification...
[self updateGenericPassword: @"otherdata" account:account];
- // And then schedule the update, but for the final version of the password
+ // And then schedule the update
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
- checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"third"]];
+ checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"otherdata"]];
// Stop the reencrypt operation from happening
self.keychainView.holdReencryptOutgoingItemsOperation = [CKKSGroupOperation named:@"reencrypt-hold" withBlock: ^{
secnotice("ckks", "releasing outgoing-queue hold");
}];
- [self updateGenericPassword: @"third" account:account];
-
// Run the reencrypt items operation to completion.
[self.operationQueue addOperation: self.keychainView.holdReencryptOutgoingItemsOperation];
[self.keychainView waitForOperationsOfClass:[CKKSReencryptOutgoingItemsOperation class]];
[self waitForCKModifications];
}
+- (void)testOutgoingQueueRecoverFromStaleInflightEntry {
+ // CKKS is restarting with an existing in-flight OQE
+ // Note that this test is incomplete, and doesn't re-add the item to the local keychain
+ [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
+ NSString* account = @"fake-account";
+
+ [self.keychainView dispatchSync:^bool {
+ NSError* error = nil;
+
+ CKRecordID* ckrid = [[CKRecordID alloc] initWithRecordName:@"DD7C2F9B-B22D-3B90-C299-E3B48174BFA3" zoneID:self.keychainZoneID];
+
+ CKKSItem* item = [self newItem:ckrid withNewItemData:[self fakeRecordDictionary:account zoneID:self.keychainZoneID] key:self.keychainZoneKeys.classC];
+ XCTAssertNotNil(item, "Should be able to create a new fake item");
+
+ CKKSOutgoingQueueEntry* oqe = [[CKKSOutgoingQueueEntry alloc] initWithCKKSItem:item action:SecCKKSActionAdd state:SecCKKSStateInFlight waitUntil:nil accessGroup:@"ckks"];
+ XCTAssertNotNil(oqe, "Should be able to create a new fake OQE");
+ [oqe saveToDatabase:&error];
+
+ XCTAssertNil(error, "Shouldn't error saving new OQE to database");
+ return true;
+ }];
+
+ // When CKKS restarts, it should find and re-upload this item
+ [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
+ checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
+
+ [self startCKKSSubsystem];
+ [self.keychainView waitForFetchAndIncomingQueueProcessing];
+
+ self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
+ [self.keychainView waitForKeyHierarchyReadiness];
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+}
+
+- (void)testOutgoingQueueRecoverFromNetworkFailure {
+ [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
+ NSString* account = @"account-delete-me";
+
+ [self startCKKSSubsystem];
+ [self holdCloudKitModifications];
+
+ // We expect a single record to be uploaded, but need to hold the operation from finishing until we can modify the item locally
+
+ NSError* greyMode = [[CKPrettyError alloc] initWithDomain:CKErrorDomain code:CKErrorNotAuthenticated userInfo:@{}];
+ [self failNextCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID blockAfterReject:nil withError:greyMode];
+
+ [self addGenericPassword: @"data" account: account];
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+
+ // And then schedule the retried update
+ [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
+ checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data"]];
+
+ // The cloudkit operation finishes, letting the next OQO proceed (and set up uploading the new item)
+ [self releaseCloudKitModificationHold];
+
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+
+ [self.keychainView waitUntilAllOperationsAreFinished];
+ [self waitForCKModifications];
+}
+
- (void)testDeleteItem {
[self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
[self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
[self startCKKSSubsystem];
+ [self.keychainView waitForKeyHierarchyReadiness];
[self.keychainView waitUntilAllOperationsAreFinished];
// Place a hold on processing the outgoing queue.
__block NSString* itemUUID = nil;
[self.keychainView dispatchSync:^bool {
NSError* error = nil;
- NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:&error];
+ NSArray<NSString*>* uuids = [CKKSOutgoingQueueEntry allUUIDs:self.keychainZoneID ?: [[CKRecordZoneID alloc] initWithZoneName:@"keychain"
+ ownerName:CKCurrentUserDefaultName]
+ error:&error];
XCTAssertNil(error, "no error fetching uuids");
XCTAssertEqual(uuids.count, 1u, "There's exactly one outgoing queue entry");
itemUUID = uuids[0];
[self startCKKSSubsystem];
// Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
- while(!([self.keychainView.keyStateMachineOperation isPending] && [self.keychainView.keyStateMachineOperation.dependencies containsObject:self.lockStateTracker.unlockDependency])) {
- sleep(0.1);
- }
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:8*NSEC_PER_SEC], @"Key state should get stuck in waitforunlock");
// After unlock, the key hierarchy should be created.
[self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
+- (void)testLockImmediatelyAfterUploadingInitialKeyHierarchy {
+
+ // Upon upload, block fetches
+ __weak __typeof(self) weakSelf = self;
+ [self expectCKModifyRecords: @{
+ SecCKRecordIntermediateKeyType: [NSNumber numberWithUnsignedInteger: 3],
+ SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: 3],
+ SecCKRecordTLKShareType: [NSNumber numberWithUnsignedInteger: 1],
+ }
+ deletedRecordTypeCounts:nil
+ zoneID:self.keychainZoneID
+ checkModifiedRecord:nil
+ runAfterModification:^{
+ __strong __typeof(self) strongSelf = weakSelf;
+ [strongSelf holdCloudKitFetches];
+ }];
+
+ [self startCKKSSubsystem];
+
+ // Should enter 'ready'
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:180*NSEC_PER_SEC], @"Key state should become 'ready'");
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+
+ // Now, lock and allow fetches again
+ self.aksLockState = true;
+ [self.lockStateTracker recheck];
+ [self releaseCloudKitFetchHold];
+
+ CKKSResultOperation* op = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
+ [op waitUntilFinished];
+
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+
+ // Wait for CKKS to shake itself out...
+ [self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
+
+ // Should be in ReadyPendingUnlock
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
+
+ // We expect a single class C record to be uploaded.
+ [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID
+ checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
+
+ [self addGenericPassword: @"data" account: @"account-delete-me"];
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+}
+
- (void)testReceiveKeyHierarchyAfterLockedStart {
// 'Lock' the keybag
self.aksLockState = true;
[self startCKKSSubsystem];
// Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
- while(!([self.keychainView.keyStateMachineOperation isPending] && [self.keychainView.keyStateMachineOperation.dependencies containsObject:self.lockStateTracker.unlockDependency])) {
- sleep(0.1);
- }
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetchComplete] wait:8*NSEC_PER_SEC], @"Key state should get stuck in fetchcomplete");
// Now, another device comes along and creates the hierarchy; we download it; and it and sends us the TLK
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self.lockStateTracker recheck];
// After unlock, the TLK arrives
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
// We expect a single class C record to be uploaded.
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
+- (void)testLoadKeyHierarchyAfterLockedStart {
+ [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
+
+ // 'Lock' the keybag
+ self.aksLockState = true;
+ [self.lockStateTracker recheck];
+
+ [self startCKKSSubsystem];
+
+ // Wait for the key hierarchy state machine to get stuck waiting for the unlock dependency. No uploads should occur.
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
+
+ self.aksLockState = false;
+ [self.lockStateTracker recheck];
+
+ // We expect a single class C record to be uploaded.
+ [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
+
+ [self addGenericPassword: @"data" account: @"account-delete-me"];
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+}
+
- (void)testUploadAndUseKeyHierarchy {
// Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
[self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
CFTypeRef item = NULL;
XCTAssertEqual(errSecItemNotFound, SecItemCopyMatching((__bridge CFDictionaryRef) query, &item), "item should not exist");
- OCMVerifyAllWithDelay(self.mockDatabase, 1);
-
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
[self waitForCKModifications];
// We expect a single class C record to be uploaded.
// Test also begins with the TLK having arrived in the local keychain (via SOS)
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
- // The CKKS subsystem should not try to write anything to the CloudKit database while it's accepting the keys
- [self.keychainView waitForKeyHierarchyReadiness];
+ // The CKKS subsystem should only upload its TLK share
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
OCMVerifyAllWithDelay(self.mockDatabase, 8);
// Test starts with nothing in database, but one in our fake CloudKit.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
- // Spin up CKKS subsystem.
[self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:5*NSEC_PER_SEC], "Key state should have become waitfortlk");
- // The CKKS subsystem should not try to write anything to the CloudKit database.
- sleep(1);
-
- OCMVerifyAllWithDelay(self.mockDatabase, 8);
-
- // Now, save the TLK to the keychain (to simulate it coming in later via SOS).
+ // Now, save the TLK to the keychain (to simulate it coming in later via SOS). We'll create a TLK share for ourselves.
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// Wait for the key hierarchy to sort itself out, to make it easier on this test; see testOnboardOldItemsWithExistingKeyHierarchy for the other test.
- [self.keychainView waitForKeyHierarchyReadiness];
+ // The CKKS subsystem should write its TLK share, but nothing else
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
// We expect a single record to be uploaded for each key class
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under class C key in hierarchy"]];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassABlock:self.keychainZoneID message:@"Object was encrypted under class A key in hierarchy"]];
- XCTAssertEqual(errSecSuccess, SecItemAdd((__bridge CFDictionaryRef)@{
- (id)kSecClass : (id)kSecClassGenericPassword,
- (id)kSecAttrAccessGroup : @"com.apple.security.sos",
- (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked,
- (id)kSecAttrAccount : @"account-class-A",
- (id)kSecAttrSynchronizable : (id)kCFBooleanTrue,
- (id)kSecValueData : (id) [@"asdf" dataUsingEncoding:NSUTF8StringEncoding],
- }, NULL), @"Adding class A item");
+ [self addGenericPassword:@"asdf"
+ account:@"account-class-A"
+ viewHint:nil
+ access:(id)kSecAttrAccessibleWhenUnlocked
+ expecting:errSecSuccess
+ message:@"Adding class A item"];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
-
-
- (void)testAcceptExistingKeyHierarchyDespiteLocked {
// Test starts with no keys in CKKS database, but one in our fake CloudKit.
// Test also begins with the TLK having arrived in the local keychain (via SOS)
OCMVerifyAllWithDelay(partialKVMock, 4);
+ // CKKS will give itself a TLK Share
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
+
// Now that all operations are complete, 'unlock' AKS
self.aksLockState = false;
[self.lockStateTracker recheck];
- [self.keychainView waitForKeyHierarchyReadiness];
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have become ready");
OCMVerifyAllWithDelay(self.mockDatabase, 4);
// Verify that there are three local keys, and three local current key records
- (void)testReceiveClassCWhileALocked {
// Test starts with a key hierarchy already existing.
[self createAndSaveFakeKeyHierarchy:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
[self.keychainView waitForFetchAndIncomingQueueProcessing];
- [self.keychainView waitForKeyHierarchyReadiness];
[self findGenericPassword:@"classCItem" expecting:errSecItemNotFound];
[self findGenericPassword:@"classAItem" expecting:errSecItemNotFound];
[self.keychainView _onqueueKeyStateMachineRequestProcess];
return true;
}];
- // And ensure we end up back in 'ready': we have the keys, we're just locked now
+ // And ensure we end up back in 'readypendingunlock': we have the keys, we're just locked now
[self.keychainView waitForOperationsOfClass:[CKKSProcessReceivedKeysOperation class]];
- XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:5*NSEC_PER_SEC], "Key state should have returned to ready");
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
[self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-F9C5-481E-98AC-5A507ACB2D85" withAccount:@"classCItem" key:self.keychainZoneKeys.classC]];
[self.keychainZone addToZone: [self createFakeRecord: self.keychainZoneID recordName:@"7B598D31-FFFF-FFFF-FFFF-5A507ACB2D85" withAccount:@"classAItem" key:self.keychainZoneKeys.classA]];
[self findGenericPassword:@"classAItem" expecting:errSecSuccess];
}
+- (void)testRestartWhileLocked {
+ [self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
+ [self startCKKSSubsystem];
+
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
+
+ // 'Lock' the keybag
+ self.aksLockState = true;
+ [self.lockStateTracker recheck];
+
+ [self.keychainView halt];
+ self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
+
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReadyPendingUnlock] wait:8*NSEC_PER_SEC], @"Key state should become 'readypendingunlock'");
+
+ self.aksLockState = false;
+ [self.lockStateTracker recheck];
+
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
+}
+
- (void)testExternalKeyRoll {
// Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
// Spin up CKKS subsystem.
[self waitForCKModifications];
[self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
// Trigger a notification
- (void)testAcceptKeyConflictAndUploadReencryptedItem {
// Test starts with no keys in database, a key hierarchy in our fake CloudKit, and the TLK safely in the local keychain.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
[self startCKKSSubsystem];
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID checkItem: [self checkClassCBlock:self.keychainZoneID message:@"Object was encrypted under rolled class C key in hierarchy"]];
// New key arrives via SOS!
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
+ [self.keychainView waitForFetchAndIncomingQueueProcessing]; // just to be sure it's fetched
// Items should upload.
[self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
-- (void)testOnboardOldItems {
+- (void)testOnboardOldItemsCreatingKeyHierarchy {
// In this test, we'll check if the CKKS subsystem will pick up a keychain item which existed before the key hierarchy, both with and without a UUID attached at item creation
// Test starts with nothing in CloudKit, and CKKS blocked. Add one item without a UUID...
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
+- (void)testOnboardOldItemsWithExistingKeyHierarchy {
+ [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
+
+ [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
+ [self addGenericPassword: @"data" account: @"account-delete-me"];
+
+ [self startCKKSSubsystem];
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+}
- (void)testOnboardOldItemsWithExistingKeyHierarchyExtantTLK {
// Test starts key hierarchy in our fake CloudKit, the TLK arrived in the local keychain, and CKKS blocked.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
// Add one item without a UUID...
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
-
- // No write yet...
- sleep(0.5);
- OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:5*NSEC_PER_SEC], "Key state should have become waitfortlk");
// Now, save the TLK to the keychain (to simulate it coming in via SOS).
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// We expect a single record to be uploaded.
// Test starts with keys in CloudKit (so we can create items later)
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychain:self.keychainZoneID];
[self addGenericPassword: @"data" account: @"first"];
// Wait for uploads to happen
OCMVerifyAllWithDelay(self.mockDatabase, 8);
[self waitForCKModifications];
- XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount, "Have 6+passwordCount objects in cloudkit");
+ // One TLK share record
+ XCTAssertEqual(self.keychainZone.currentDatabase.count, SYSTEM_DB_RECORD_COUNT+passwordCount+1, "Have 6+passwordCount objects in cloudkit");
// Now, corrupt away!
// Extract all passwordCount items for Corruption
return true;
}];
}
+- (void)testResyncLocal {
+ [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
+ [self saveTLKMaterialToKeychain:self.keychainZoneID];
+
+ [self addGenericPassword: @"data" account: @"first"];
+ [self addGenericPassword: @"data" account: @"second"];
+ NSUInteger passwordCount = 2u;
+
+ [self expectCKModifyItemRecords: passwordCount currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
+ [self startCKKSSubsystem];
+
+ // Wait for uploads to happen
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ [self waitForCKModifications];
+
+ // Local resyncs shouldn't fetch clouds.
+ self.silentFetchesAllowed = false;
+ SecCKKSDisable();
+ [self deleteGenericPassword:@"first"];
+ [self deleteGenericPassword:@"second"];
+ SecCKKSEnable();
+
+ // And they're gone!
+ [self findGenericPassword:@"first" expecting:errSecItemNotFound];
+ [self findGenericPassword:@"second" expecting:errSecItemNotFound];
+
+ CKKSLocalSynchronizeOperation* op = [self.keychainView resyncLocal];
+ [op waitUntilFinished];
+ XCTAssertNil(op.error, "Shouldn't be an error resyncing locally");
+
+ // And they're back!
+ [self checkGenericPassword: @"data" account: @"first"];
+ [self checkGenericPassword: @"data" account: @"second"];
+}
+
+- (void)testPlistRestoreResyncsLocal {
+ [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
+ [self saveTLKMaterialToKeychain:self.keychainZoneID];
+
+ [self addGenericPassword: @"data" account: @"first"];
+ [self addGenericPassword: @"data" account: @"second"];
+ NSUInteger passwordCount = 2u;
+
+ [self checkGenericPassword: @"data" account: @"first"];
+ [self checkGenericPassword: @"data" account: @"second"];
+
+ [self expectCKModifyItemRecords:passwordCount currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
+ [self startCKKSSubsystem];
+
+ // Wait for uploads to happen
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ [self waitForCKModifications];
+
+ // o no
+ // This 'restores' a plist keychain backup
+ // That will kick off a local resync in CKKS, so hold that until we're ready...
+ self.keychainView.holdLocalSynchronizeOperation = [CKKSResultOperation named:@"hold-local-synchronize" withBlock:^{}];
+
+ // Local resyncs shouldn't fetch clouds.
+ self.silentFetchesAllowed = false;
+
+ CFErrorRef cferror = NULL;
+ kc_with_dbt(true, &cferror, ^bool (SecDbConnectionRef dbt) {
+ CFErrorRef cfcferror = NULL;
+
+ bool ret = SecServerImportKeychainInPlist(dbt, SecSecurityClientGet(), KEYBAG_NONE, KEYBAG_NONE,
+ (__bridge CFDictionaryRef)@{}, kSecBackupableItemFilter, &cfcferror);
+
+ XCTAssertNil(CFBridgingRelease(cfcferror), "Shouldn't error importing a 'backup'");
+ XCTAssert(ret, "Importing a 'backup' should have succeeded");
+ return true;
+ });
+ XCTAssertNil(CFBridgingRelease(cferror), "Shouldn't error mucking about in the db");
+
+ // And they're gone!
+ [self findGenericPassword:@"first" expecting:errSecItemNotFound];
+ [self findGenericPassword:@"second" expecting:errSecItemNotFound];
+
+ // Allow the local resync to continue...
+ [self.operationQueue addOperation:self.keychainView.holdLocalSynchronizeOperation];
+ [self.keychainView waitForOperationsOfClass:[CKKSLocalSynchronizeOperation class]];
+
+ // And they're back!
+ [self checkGenericPassword: @"data" account: @"first"];
+ [self checkGenericPassword: @"data" account: @"second"];
+}
- (void)testMultipleZoneAdd {
// Test starts with nothing in database. We expect some sort of TLK/key hierarchy upload.
[self waitForCKModifications];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
- // Tear down the CKKS object and disallos fetches
- [self.keychainView cancelAllOperations];
+ // Tear down the CKKS object and disallow fetches
+ [self.keychainView halt];
self.silentFetchesAllowed = false;
self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
// Okay, cool, rad, now let's set the date to be very long ago and check that there's positively a fetch
- [self.keychainView cancelAllOperations];
+ [self.keychainView halt];
self.silentFetchesAllowed = false;
[self.keychainView dispatchSync: ^bool {
[self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
NSError* error = nil;
+
+ // Stash the TLKs.
+ [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
+ XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
+
+ // And delete the non-stashed version
[self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
[self.keychainView waitForKeyHierarchyReadiness];
// Tear down the CKKS object
- [self.keychainView cancelAllOperations];
+ [self.keychainView halt];
NSError* error = nil;
+
+ // Stash the TLKs.
+ [self.keychainZoneKeys.tlk saveKeyMaterialToKeychain:true error:&error];
+ XCTAssertNil(error, "Should have received no error stashing the new TLK in the keychain");
+
[self.keychainZoneKeys.tlk deleteKeyMaterialFromKeychain:&error];
XCTAssertNil(error, "Should have received no error deleting the new TLK from the keychain");
OCMVerifyAllWithDelay(self.mockDatabase, 8);
// Now the TLKs arrive from the other device...
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
[self.keychainView waitForKeyHierarchyReadiness];
[self startCKKSSubsystem];
// The CKKS subsystem should figure out the issue, and fix it.
- [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 0 zoneID:self.keychainZoneID];
+ [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
[self.keychainView waitForKeyHierarchyReadiness];
[self startCKKSSubsystem];
// The CKKS subsystem should figure out the issue, and fix it.
- [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 0 zoneID:self.keychainZoneID];
+ [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
- [self.keychainView waitForKeyHierarchyReadiness];
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have become ready");
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
return true;
}];
+ // The CKKS subsystem should try to write TLKs, but fail. It'll then upload a TLK share for the keys already in CloudKit
+ [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
+
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
-
- // The CKKS subsystem should try to write TLKs, but fail
- [self expectCKAtomicModifyItemRecordsUpdateFailure:self.keychainZoneID];
OCMVerifyAllWithDelay(self.mockDatabase, 16);
// CKKS should then happily use the keys in CloudKit
// Spin up CKKS subsystem.
[self startCKKSSubsystem];
- // The CKKS subsystem should figure out the issue, and fix it.
- [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 0 zoneID:self.keychainZoneID];
+ // The CKKS subsystem should figure out the issue, and fix it (while uploading itself a TLK Share)
+ [self expectCKModifyKeyRecords: 0 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
- [self.keychainView waitForKeyHierarchyReadiness];
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should have become ready");
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
[self startCKKSSubsystem];
// Now, save the TLK to the keychain (to simulate it coming in later via SOS).
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// We expect a single record to be uploaded
[self expectCKFetch];
// Now, save the TLK to the keychain (to simulate it coming in later via SOS).
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// We expect a single record to be uploaded
[self startCKKSSubsystem];
// Now, save the TLK to the keychain (to simulate it coming in later via SOS).
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// We expect a single record to be uploaded
- (void)testRecoverFromCloudKitUnknownDeviceStateRecord {
// Test starts with nothing in database, but one in our fake CloudKit.
[self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// Save a new device state record with some fake etag
[self startCKKSSubsystem];
// Now, save the TLK to the keychain (to simulate it coming in later via SOS).
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// We expect a single record to be uploaded
[self startCKKSSubsystem];
// Now, save the TLK to the keychain (to simulate it coming in later via SOS).
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// We expect a single record to be uploaded
[self startCKKSSubsystem];
// Now, save the TLK to the keychain (to simulate it coming in later via SOS).
+ [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
[self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID];
// We expect a single record to be uploaded
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
[self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.loggedOut wait:500*NSEC_PER_MSEC], "Should have been told of a 'logout' event on startup");
+ XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event shouldn't have happened");
+
[self.keychainView waitUntilAllOperationsAreFinished];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
self.circleStatus = kSOSCCInCircle;
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
+ XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
+ XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
+
OCMVerifyAllWithDelay(self.mockDatabase, 8);
[self waitForCKModifications];
[self expectCKModifyKeyRecords: 3 currentKeyPointerRecords: 3 tlkShareRecords: 1 zoneID:self.keychainZoneID];
[self startCKKSSubsystem];
+ XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
+ XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
OCMVerifyAllWithDelay(self.mockDatabase, 20);
[self waitForCKModifications];
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
// Test that there are no items in the database after logout
- [self.keychainView waitUntilAllOperationsAreFinished];
+ XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
+ XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
[self checkNoCKKSData: self.keychainView];
// There should be no further uploads, even when we save keychain items
self.circleStatus = kSOSCCInCircle;
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
+ XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
+ XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
+
OCMVerifyAllWithDelay(self.mockDatabase, 20);
// Let everything settle...
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
// Test that there are no items in the database after logout
- [self.keychainView waitUntilAllOperationsAreFinished];
+ XCTAssertEqual(0, [self.keychainView.loggedOut wait:2000*NSEC_PER_MSEC], "Should have been told of a 'logout'");
+ XCTAssertNotEqual(0, [self.keychainView.loggedIn wait:100*NSEC_PER_MSEC], "'login' event should be reset");
[self checkNoCKKSData: self.keychainView];
// There should be no further uploads, even when we save keychain items
self.circleStatus = kSOSCCInCircle;
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
+ XCTAssertEqual(0, [self.keychainView.loggedIn wait:2000*NSEC_PER_MSEC], "Should have been told of a 'login'");
+ XCTAssertNotEqual(0, [self.keychainView.loggedOut wait:100*NSEC_PER_MSEC], "'logout' event should be reset");
+
OCMVerifyAllWithDelay(self.mockDatabase, 20);
}
id partialKVMock = OCMPartialMock(self.keychainView);
OCMReject([partialKVMock handleCKLogout]);
+ // note: don't unblock the ck account state object yet...
self.circleStatus = kSOSCCInCircle;
[self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
- [self startCKKSSubsystemOnly]; // note: don't unblock the ck account state object yet...
// Add a keychain item, but make sure it doesn't upload yet.
[self addGenericPassword: @"data" account: @"account-delete-me"];
[self.keychainView waitUntilAllOperationsAreFinished];
[self waitForCKModifications];
- [self.keychainView cancelAllOperations];
+ [self.keychainView halt];
[partialKVMock stopMocking];
}
ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
- XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], @"fake-circle-id", "peer ID matches what we gave it");
+ XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
- XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], @"fake-circle-id", "peer ID matches what we gave it");
+ XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
- (void)testDeviceStateUploadWaitsForKeyHierarchy {
[self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
+ // Ask to wait for quite a while if we don't become ready
+ [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:20*NSEC_PER_SEC ckoperationGroup:nil];
+
__weak __typeof(self) weakSelf = self;
// Expect a ready upload
[self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
- XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], @"fake-circle-id", "peer ID matches what we gave it");
+ XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
}
runAfterModification:nil];
- // Ensure we'll wait for quite a while if we don't become ready
- [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:20*NSEC_PER_SEC ckoperationGroup:nil];
-
- // But don't allow the key state to progress until now
+ // And allow the key state to progress
[self startCKKSSubsystem];
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
- XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], @"fake-circle-id", "peer ID matches what we gave it");
+ XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
__strong __typeof(weakSelf) strongSelf = weakSelf;
XCTAssertNotNil(strongSelf, "self exists");
- XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], @"fake-circle-id", "peer ID matches what we gave it");
+ XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
[self startCKKSSubsystem];
- // Since CKKS should start up enough to get back into the error state and then back into initializing state, wait for that to happen.
- XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateError] wait:8*NSEC_PER_SEC], "CKKS entered error");
XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateInitializing] wait:8*NSEC_PER_SEC], "CKKS entered initializing");
XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateInitializing, "CKKS entered intializing");
XCTAssertNil(op.error, "No error uploading 'out of circle' device state");
}
+- (void)testNotStuckAfterReset {
+ [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
+
+ XCTestExpectation *operationRun = [self expectationWithDescription:@"operation run"];
+ NSOperation* op = [NSBlockOperation named:@"test" withBlock:^{
+ [operationRun fulfill];
+ }];
+
+ [op addDependency:self.keychainView.keyStateReadyDependency];
+ [self.operationQueue addOperation:op];
+
+ // And handle a spurious logout
+ [self.keychainView handleCKLogout];
+
+ [self startCKKSSubsystem];
+
+ [self waitForExpectations: @[operationRun] timeout:80];
+}
+
- (void)testCKKSControlBringup {
xpc_endpoint_t endpoint = SecServerCreateCKKSEndpoint();
XCTAssertNotNil(endpoint, "Received endpoint");
OCMVerifyAllWithDelay(self.mockDatabase, 8);
// Tear down the CKKS object
- [self.keychainView cancelAllOperations];
+ [self.keychainView halt];
self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
[self.keychainView waitForKeyHierarchyReadiness];
[self.keychainView waitForFetchAndIncomingQueueProcessing];
// Tear down the CKKS object
- [self.keychainView cancelAllOperations];
+ [self.keychainView halt];
[self.keychainView dispatchSync: ^bool {
// Edit the zone state entry to have no fixups
self.silentFetchesAllowed = false;
[self expectCKFetchByRecordID];
- if(SecCKKSShareTLKs()) {
- [self expectCKFetchByQuery]; // and one for the TLKShare fixup
- }
+ [self expectCKFetchByQuery]; // and one for the TLKShare fixup
// Change one of the CIPs while CKKS is offline
cip2.currentItemUUID = @"changed-by-cloudkit";
}
- (void)testFixupFetchAllTLKShareRecords {
- SecCKKSSetShareTLKs(true);
// In <rdar://problem/34901306> CKKSTLK: TLKShare CloudKit upload/download on TLK change, trust set addition,
// we added the TLKShare CKRecord type. Upgrading devices must fetch all such records when they come online for the first time.
OCMVerifyAllWithDelay(self.mockDatabase, 8);
// Tear down the CKKS object
- [self.keychainView cancelAllOperations];
+ [self.keychainView halt];
[self setFixupNumber:CKKSFixupRefetchCurrentItemPointers ckks:self.keychainView];
// Also, create a TLK share record that CKKS should find
self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
- XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:500*NSEC_PER_SEC], "Key state should become waitforfixup");
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:8*NSEC_PER_SEC], "Key state should become waitforfixup");
[self releaseCloudKitFetchHold];
- XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:500*NSEC_PER_SEC], "Key state should become ready");
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
OCMVerifyAllWithDelay(self.mockDatabase, 8);
XCTAssertNotNil(localshare, "Should be able to find a new TLKShare record in database");
return true;
}];
+}
+
+- (void)testFixupLocalReload {
+ // In <rdar://problem/35540228> Server Generated CloudKit "Manatee Identity Lost"
+ // items could be deleted from the local keychain after CKKS believed they were already synced, and therefore wouldn't resync
+
+ [self expectCKModifyKeyRecords:3 currentKeyPointerRecords:3 tlkShareRecords:1 zoneID:self.keychainZoneID];
+ [self startCKKSSubsystem];
+
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], @"Key state should become 'ready'");
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ [self waitForCKModifications];
+
+ [self addGenericPassword: @"data" account: @"first"];
+ [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID];
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ [self waitForCKModifications];
+
+ // Add another record to mock up early CKKS record saving
+ __block CKRecordID* secondRecordID = nil;
+ CKKSCondition* secondRecordIDFilled = [[CKKSCondition alloc] init];
+ [self addGenericPassword: @"data" account: @"second"];
+ [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID checkItem:^BOOL(CKRecord * _Nonnull record) {
+ secondRecordID = record.recordID;
+ [secondRecordIDFilled fulfill];
+ return TRUE;
+ }];
+ OCMVerifyAllWithDelay(self.mockDatabase, 8);
+ [self waitForCKModifications];
+ XCTAssertNotNil(secondRecordID, "Should have filled in secondRecordID");
+ XCTAssertEqual(0, [secondRecordIDFilled wait:8*NSEC_PER_SEC], "Should have filled in secondRecordID within enough time");
+
+ // Tear down the CKKS object
+ [self.keychainView halt];
+ [self setFixupNumber:CKKSFixupFetchTLKShares ckks:self.keychainView];
+
+ // Delete items from keychain
+ [self deleteGenericPassword:@"first"];
+
+ // Corrupt the second item's CKMirror entry to only contain system fields in the CKRecord portion (to emulate early CKKS behavior)
+ [self.keychainView dispatchSync:^bool {
+ NSError* error = nil;
+ CKKSMirrorEntry* ckme = [CKKSMirrorEntry fromDatabase:secondRecordID.recordName zoneID:self.keychainZoneID error:&error];
+ XCTAssertNil(error, "Should have no error pulling second CKKSMirrorEntry from database");
+
+ NSMutableData* data = [NSMutableData data];
+ NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
+ [ckme.item.storedCKRecord encodeSystemFieldsWithCoder:archiver];
+ [archiver finishEncoding];
+ ckme.item.encodedCKRecord = data;
+
+ [ckme saveToDatabase:&error];
+ XCTAssertNil(error, "No error saving system-fielded CKME back to database");
+ return true;
+ }];
+
+ // Now, restart CKKS, but place a hold on the fixup operation
+ self.silentFetchesAllowed = false;
+ self.accountStatus = CKAccountStatusCouldNotDetermine;
+ [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
+
+ self.keychainView = [[CKKSViewManager manager] restartZone:self.keychainZoneID.zoneName];
+ self.keychainView.holdFixupOperation = [CKKSResultOperation named:@"hold-fixup" withBlock:^{}];
+
+ self.accountStatus = CKAccountStatusAvailable;
+ [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
+
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForFixupOperation] wait:8*NSEC_PER_SEC], "Key state should become waitforfixup");
+ [self.operationQueue addOperation: self.keychainView.holdFixupOperation];
+ XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:8*NSEC_PER_SEC], "Key state should become ready");
+
+ [self.keychainView.lastFixupOperation waitUntilFinished];
+ XCTAssertNil(self.keychainView.lastFixupOperation.error, "Shouldn't have been any error performing fixup");
+
+ [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
- SecCKKSSetShareTLKs(false);
+ // And the item should be back!
+ [self checkGenericPassword: @"data" account: @"first"];
}
@end
#import "keychain/ckks/CKKS.h"
#import "keychain/ckks/CKKSControl.h"
#import "keychain/ckks/CKKSCurrentKeyPointer.h"
+#import "keychain/ckks/CKKSItem.h"
+
+NS_ASSUME_NONNULL_BEGIN
@class CKKSKey;
@class CKKSCurrentKeyPointer;
@property CKKSControl* ckksControl;
-@property id mockCKKSKey;
+@property (nullable) id mockCKKSKey;
-@property id<CKKSSelfPeer> currentSelfPeer;
-@property NSMutableSet<id<CKKSPeer>>* currentPeers;
+@property (nullable) id<CKKSSelfPeer> currentSelfPeer;
+@property (nullable) NSMutableSet<id<CKKSPeer>>* currentPeers;
-@property NSMutableDictionary<CKRecordZoneID*, ZoneKeys*>* keys;
+@property NSMutableSet<CKRecordZoneID*>* ckksZones;
+@property (nullable) NSMutableDictionary<CKRecordZoneID*, ZoneKeys*>* keys;
// Pass in an oldTLK to wrap it to the new TLK; otherwise, pass nil
-- (ZoneKeys*)createFakeKeyHierarchy: (CKRecordZoneID*)zoneID oldTLK:(CKKSKey*) oldTLK;
-- (void)saveFakeKeyHierarchyToLocalDatabase: (CKRecordZoneID*)zoneID;
-- (void)putFakeKeyHierarchyInCloudKit: (CKRecordZoneID*)zoneID;
-- (void)saveTLKMaterialToKeychain: (CKRecordZoneID*)zoneID;
-- (void)deleteTLKMaterialFromKeychain: (CKRecordZoneID*)zoneID;
-- (void)saveTLKMaterialToKeychainSimulatingSOS: (CKRecordZoneID*)zoneID;
+- (ZoneKeys*)createFakeKeyHierarchy:(CKRecordZoneID*)zoneID oldTLK:(CKKSKey* _Nullable)oldTLK;
+- (void)saveFakeKeyHierarchyToLocalDatabase:(CKRecordZoneID*)zoneID;
+- (void)putFakeKeyHierarchyInCloudKit:(CKRecordZoneID*)zoneID;
+- (void)saveTLKMaterialToKeychain:(CKRecordZoneID*)zoneID;
+- (void)deleteTLKMaterialFromKeychain:(CKRecordZoneID*)zoneID;
+- (void)saveTLKMaterialToKeychainSimulatingSOS:(CKRecordZoneID*)zoneID;
- (void)SOSPiggyBackAddToKeychain:(NSDictionary*)piggydata;
- (NSMutableDictionary*)SOSPiggyBackCopyFromKeychain;
-- (NSMutableArray<NSData *>*) SOSPiggyICloudIdentities;
+- (NSMutableArray<NSData*>*)SOSPiggyICloudIdentities;
- (void)putTLKShareInCloudKit:(CKKSKey*)key
from:(CKKSSOSSelfPeer*)sharingPeer
to:(id<CKKSPeer>)receivingPeer
zoneID:(CKRecordZoneID*)zoneID;
-- (void)putTLKSharesInCloudKit:(CKKSKey*)key
- from:(CKKSSOSSelfPeer*)sharingPeer
- zoneID:(CKRecordZoneID*)zoneID;
+- (void)putTLKSharesInCloudKit:(CKKSKey*)key from:(CKKSSOSSelfPeer*)sharingPeer zoneID:(CKRecordZoneID*)zoneID;
- (void)putSelfTLKSharesInCloudKit:(CKRecordZoneID*)zoneID;
- (void)saveTLKSharesInLocalDatabase:(CKRecordZoneID*)zoneID;
-- (void)saveClassKeyMaterialToKeychain: (CKRecordZoneID*)zoneID;
+- (void)saveClassKeyMaterialToKeychain:(CKRecordZoneID*)zoneID;
// Call this to fake out your test: all keys are created, saved in cloudkit, and saved locally (as if the key state machine had processed them)
-- (void)createAndSaveFakeKeyHierarchy: (CKRecordZoneID*)zoneID;
+- (void)createAndSaveFakeKeyHierarchy:(CKRecordZoneID*)zoneID;
-- (void)rollFakeKeyHierarchyInCloudKit: (CKRecordZoneID*)zoneID;
+- (void)rollFakeKeyHierarchyInCloudKit:(CKRecordZoneID*)zoneID;
-- (NSDictionary*)fakeRecordDictionary:(NSString*) account zoneID:(CKRecordZoneID*)zoneID;
-- (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName ;
-- (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account;
-- (CKRecord*)createFakeRecord: (CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount: (NSString*) account key:(CKKSKey*)key;
+- (NSDictionary*)fakeRecordDictionary:(NSString*)account zoneID:(CKRecordZoneID*)zoneID;
+- (CKRecord*)createFakeRecord:(CKRecordZoneID*)zoneID recordName:(NSString*)recordName;
+- (CKRecord*)createFakeRecord:(CKRecordZoneID*)zoneID recordName:(NSString*)recordName withAccount:(NSString* _Nullable)account;
+- (CKRecord*)createFakeRecord:(CKRecordZoneID*)zoneID
+ recordName:(NSString*)recordName
+ withAccount:(NSString* _Nullable)account
+ key:(CKKSKey* _Nullable)key;
-- (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary;
-- (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary key:(CKKSKey*)key;
-- (NSDictionary*)decryptRecord: (CKRecord*) record;
+- (CKKSItem*)newItem:(CKRecordID*)recordID withNewItemData:(NSDictionary*) dictionary key:(CKKSKey*)key;
+- (CKRecord*)newRecord:(CKRecordID*)recordID withNewItemData:(NSDictionary*)dictionary;
+- (CKRecord*)newRecord:(CKRecordID*)recordID withNewItemData:(NSDictionary*)dictionary key:(CKKSKey*)key;
+- (NSDictionary*)decryptRecord:(CKRecord*)record;
// Do keychain things:
-- (void)addGenericPassword: (NSString*) password account: (NSString*) account;
-- (void)addGenericPassword: (NSString*) password account: (NSString*) account viewHint:(NSString*)viewHint;
-- (void)addGenericPassword: (NSString*) password account: (NSString*) account viewHint: (NSString*) viewHint access:(NSString*)access expecting: (OSStatus) status message: (NSString*) message;
-- (void)addGenericPassword: (NSString*) password account: (NSString*) account expecting: (OSStatus) status message: (NSString*) message;
-
-- (void)updateGenericPassword: (NSString*) newPassword account: (NSString*)account;
+- (void)addGenericPassword:(NSString*)password account:(NSString*)account;
+- (void)addGenericPassword:(NSString*)password account:(NSString*)account viewHint:(NSString* _Nullable)viewHint;
+- (void)addGenericPassword:(NSString*)password
+ account:(NSString*)account
+ viewHint:(NSString* _Nullable)viewHint
+ access:(NSString*)access
+ expecting:(OSStatus)status
+ message:(NSString*)message;
+- (void)addGenericPassword:(NSString*)password account:(NSString*)account expecting:(OSStatus)status message:(NSString*)message;
+
+- (void)updateGenericPassword:(NSString*)newPassword account:(NSString*)account;
- (void)updateAccountOfGenericPassword:(NSString*)newAccount account:(NSString*)account;
-- (void)checkNoCKKSData: (CKKSKeychainView*) view;
+- (void)checkNoCKKSData:(CKKSKeychainView*)view;
-- (void)deleteGenericPassword: (NSString*) account;
+- (void)deleteGenericPassword:(NSString*)account;
-- (void)findGenericPassword: (NSString*) account expecting: (OSStatus) status;
-- (void)checkGenericPassword: (NSString*) password account: (NSString*) account;
+- (void)findGenericPassword:(NSString*)account expecting:(OSStatus)status;
+- (void)checkGenericPassword:(NSString*)password account:(NSString*)account;
- (void)createClassCItemAndWaitForUpload:(CKRecordZoneID*)zoneID account:(NSString*)account;
- (void)createClassAItemAndWaitForUpload:(CKRecordZoneID*)zoneID account:(NSString*)account;
// Pass the blocks created with these to expectCKModifyItemRecords to check if all items were encrypted with a particular class key
-- (BOOL (^) (CKRecord*)) checkClassABlock: (CKRecordZoneID*) zoneID message:(NSString*) message;
-- (BOOL (^) (CKRecord*)) checkClassCBlock: (CKRecordZoneID*) zoneID message:(NSString*) message;
+- (BOOL (^)(CKRecord*))checkClassABlock:(CKRecordZoneID*)zoneID message:(NSString*)message;
+- (BOOL (^)(CKRecord*))checkClassCBlock:(CKRecordZoneID*)zoneID message:(NSString*)message;
-- (BOOL (^) (CKRecord*)) checkPasswordBlock:(CKRecordZoneID*)zoneID
- account:(NSString*)account
- password:(NSString*)password;
+- (BOOL (^)(CKRecord*))checkPasswordBlock:(CKRecordZoneID*)zoneID account:(NSString*)account password:(NSString*)password;
- (void)checkNSyncableTLKsInKeychain:(size_t)n;
// Returns an expectation that someone will send an NSNotification that this view changed
--(XCTestExpectation*)expectChangeForView:(NSString*)view;
+- (XCTestExpectation*)expectChangeForView:(NSString*)view;
// Establish an assertion that CKKS will cause a server extension error soon.
- (void)expectCKReceiveSyncKeyHierarchyError:(CKRecordZoneID*)zoneID;
+
+// Add expectations that CKKS will upload a single TLK share
+- (void)expectCKKSTLKSelfShareUpload:(CKRecordZoneID*)zoneID;
@end
+
+NS_ASSUME_NONNULL_END
XCTAssertFalse([CKKSManifest shouldEnforceManifests], "Manifests enforcement is disabled");
[super setUp];
+ self.ckksZones = [NSMutableSet set];
self.keys = [[NSMutableDictionary alloc] init];
// Fake out whether class A keys can be loaded from the keychain.
self.mockCKKSKey = OCMClassMock([CKKSKey class]);
__weak __typeof(self) weakSelf = self;
- OCMStub([self.mockCKKSKey loadKeyMaterialFromKeychain:[OCMArg checkWithBlock:^BOOL(CKKSKey* key) {
+ BOOL (^shouldFailKeychainQuery)(NSDictionary* query) = ^BOOL(NSDictionary* query) {
__strong __typeof(self) strongSelf = weakSelf;
- return ([key.keyclass isEqualToString: SecCKKSKeyClassA] || [key.keyclass isEqualToString: SecCKKSKeyClassTLK]) && strongSelf.aksLockState;
- }]
- resave:[OCMArg anyPointer]
- error:[OCMArg anyObjectRef]]).andCall(self, @selector(handleLockLoadKeyMaterialFromKeychain:resave:error:));
+ NSString* description = query[(id)kSecAttrDescription];
+ bool isTLK = [description isEqualToString: SecCKKSKeyClassTLK] ||
+ [description isEqualToString: [SecCKKSKeyClassTLK stringByAppendingString: @"-nonsync"]] ||
+ [description isEqualToString: [SecCKKSKeyClassTLK stringByAppendingString: @"-piggy"]];
+ bool isClassA = [description isEqualToString: SecCKKSKeyClassA];
- OCMStub([self.mockCKKSKey saveKeyMaterialToKeychain:[OCMArg checkWithBlock:^BOOL(CKKSKey* key) {
- __strong __typeof(self) strongSelf = weakSelf;
- return ([key.keyclass isEqualToString: SecCKKSKeyClassA] || [key.keyclass isEqualToString: SecCKKSKeyClassTLK]) && strongSelf.aksLockState;
- }]
- stashTLK:[OCMArg anyObjectRef]
- error:[OCMArg anyObjectRef]]
- ).andCall(self, @selector(handleLockSaveKeyMaterialToKeychain:stashTLK:error:));
+ return (isTLK || isClassA) && strongSelf.aksLockState;
+ };
+
+ OCMStub([self.mockCKKSKey setKeyMaterialInKeychain:[OCMArg checkWithBlock:shouldFailKeychainQuery] error:[OCMArg anyObjectRef]]
+ ).andCall(self, @selector(handleLockSetKeyMaterialInKeychain:error:));
+
+ OCMStub([self.mockCKKSKey queryKeyMaterialInKeychain:[OCMArg checkWithBlock:shouldFailKeychainQuery] error:[OCMArg anyObjectRef]]
+ ).andCall(self, @selector(handleLockLoadKeyMaterialFromKeychain:error:));
// Fake out SOS peers
self.currentSelfPeer = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"local-peer"
OCMVerifyAllWithDelay(self.mockDatabase, 8);
}
-// Helpers to handle keychain 'locked' loading
--(bool)handleLockLoadKeyMaterialFromKeychain:(CKKSKey*)key resave:(bool*)resavePtr error:(NSError * __autoreleasing *) error {
- XCTAssertTrue(self.aksLockState, "Failing a read only when keychain is locked");
- if(error) {
- *error = [NSError errorWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil];
+// Helpers to handle 'locked' keychain loading and saving
+-(bool)handleLockLoadKeyMaterialFromKeychain:(NSDictionary*)query error:(NSError * __autoreleasing *) error {
+ // I think the behavior is: errSecItemNotFound if the item doesn't exist, otherwise errSecInteractionNotAllowed.
+ XCTAssertTrue(self.aksLockState, "Failing a read when keychain is locked");
+
+ OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
+ if(status == errSecItemNotFound) {
+ if(error) {
+ *error = [NSError errorWithDomain:@"securityd" code:status userInfo:nil];
+ }
+ } else {
+ if(error) {
+ *error = [NSError errorWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil];
+ }
}
return false;
}
--(bool)handleLockSaveKeyMaterialToKeychain:(CKKSKey*)key stashTLK:(bool)stashTLK error:(NSError * __autoreleasing *) error {
+-(bool)handleLockSetKeyMaterialInKeychain:(NSDictionary*)query error:(NSError * __autoreleasing *) error {
XCTAssertTrue(self.aksLockState, "Failing a write only when keychain is locked");
if(error) {
*error = [NSError errorWithDomain:@"securityd" code:errSecInteractionNotAllowed userInfo:nil];
- (void)saveTLKMaterialToKeychain: (CKRecordZoneID*)zoneID {
NSError* error = nil;
XCTAssertNotNil(self.keys[zoneID].tlk, "Have a TLK to save for zone %@", zoneID);
- [self.keys[zoneID].tlk saveKeyMaterialToKeychain:&error];
+
+ // Don't make the stashed local copy of the TLK
+ [self.keys[zoneID].tlk saveKeyMaterialToKeychain:false error:&error];
XCTAssertNil(error, @"Saved TLK material to keychain");
}
}
- (void)createAndSaveFakeKeyHierarchy: (CKRecordZoneID*)zoneID {
- [self saveFakeKeyHierarchyToLocalDatabase: zoneID];
+ // Put in CloudKit first, so the records on-disk will have the right change tags
[self putFakeKeyHierarchyInCloudKit: zoneID];
+ [self saveFakeKeyHierarchyToLocalDatabase: zoneID];
[self saveTLKMaterialToKeychain: zoneID];
[self saveClassKeyMaterialToKeychain: zoneID];
- if(SecCKKSShareTLKs()) {
- [self putSelfTLKSharesInCloudKit:zoneID];
- [self saveTLKSharesInLocalDatabase:zoneID];
- }
+ [self putSelfTLKSharesInCloudKit:zoneID];
+ [self saveTLKSharesInLocalDatabase:zoneID];
}
// Override our base class here:
-- (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
- currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
- tlkShareRecords:(NSUInteger)expectedTLKShareRecords
- zoneID:(CKRecordZoneID*) zoneID {
+- (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>*)expectedRecordTypeCounts
+ deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>*)expectedDeletedRecordTypeCounts
+ zoneID:(CKRecordZoneID*)zoneID
+ checkModifiedRecord:(BOOL (^)(CKRecord*))checkRecord
+ runAfterModification:(void (^) ())afterModification
+{
__weak __typeof(self) weakSelf = self;
- [self expectCKModifyRecords: @{
- SecCKRecordIntermediateKeyType: [NSNumber numberWithUnsignedInteger: expectedNumberOfRecords],
- SecCKRecordCurrentKeyType: [NSNumber numberWithUnsignedInteger: expectedCurrentKeyRecords],
- SecCKRecordTLKShareType: [NSNumber numberWithUnsignedInteger:(SecCKKSShareTLKs() ? expectedTLKShareRecords : 0)],
- }
- deletedRecordTypeCounts:nil
- zoneID:zoneID
- checkModifiedRecord:nil
- runAfterModification:^{
- __strong __typeof(weakSelf) strongSelf = weakSelf;
- XCTAssertNotNil(strongSelf, "self exists");
-
- // Reach into our cloudkit database and extract the keys
- CKRecordID* currentTLKPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassTLK zoneID:zoneID];
- CKRecordID* currentClassAPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassA zoneID:zoneID];
- CKRecordID* currentClassCPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassC zoneID:zoneID];
-
- ZoneKeys* zonekeys = strongSelf.keys[zoneID];
- if(!zonekeys) {
- zonekeys = [[ZoneKeys alloc] init];
- strongSelf.keys[zoneID] = zonekeys;
- }
-
- XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID], "Have a currentTLKPointer");
- XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID], "Have a currentClassAPointer");
- XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID], "Have a currentClassCPointer");
- XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey], "Have a currentTLKPointer parent");
- XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassAPointer parent");
- XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassCPointer parent");
- XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentTLKPointer parent UUID");
- XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassAPointer parent UUID");
- XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassCPointer parent UUID");
-
- zonekeys.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID]];
- zonekeys.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID]];
- zonekeys.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID]];
-
- XCTAssertNotNil(zonekeys.currentTLKPointer.currentKeyUUID, "Have a currentTLKPointer current UUID");
- XCTAssertNotNil(zonekeys.currentClassAPointer.currentKeyUUID, "Have a currentClassAPointer current UUID");
- XCTAssertNotNil(zonekeys.currentClassCPointer.currentKeyUUID, "Have a currentClassCPointer current UUID");
-
- CKRecordID* currentTLKID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentTLKPointer.currentKeyUUID zoneID:zoneID];
- CKRecordID* currentClassAID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassAPointer.currentKeyUUID zoneID:zoneID];
- CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassCPointer.currentKeyUUID zoneID:zoneID];
-
- zonekeys.tlk = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKID]];
- zonekeys.classA = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAID]];
- zonekeys.classC = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCID]];
-
- XCTAssertNotNil(zonekeys.tlk, "Have the current TLK");
- XCTAssertNotNil(zonekeys.classA, "Have the current Class A key");
- XCTAssertNotNil(zonekeys.classC, "Have the current Class C key");
-
- NSMutableArray<CKKSTLKShare*>* shares = [NSMutableArray array];
- for(CKRecordID* recordID in strongSelf.zones[zoneID].currentDatabase.allKeys) {
- if([recordID.recordName hasPrefix: [CKKSTLKShare ckrecordPrefix]]) {
- CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:strongSelf.zones[zoneID].currentDatabase[recordID]];
- XCTAssertNotNil(share, "Should be able to parse a CKKSTLKShare CKRecord into a CKKSTLKShare");
- [shares addObject:share];
- }
- }
- zonekeys.tlkShares = shares;
- }];
+ [super expectCKModifyRecords:expectedRecordTypeCounts
+ deletedRecordTypeCounts:expectedDeletedRecordTypeCounts
+ zoneID:zoneID
+ checkModifiedRecord:checkRecord
+ runAfterModification:^{
+ __strong __typeof(weakSelf) strongSelf = weakSelf;
+ XCTAssertNotNil(strongSelf, "self exists");
+
+ // Reach into our cloudkit database and extract the keys
+ CKRecordID* currentTLKPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassTLK zoneID:zoneID];
+ CKRecordID* currentClassAPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassA zoneID:zoneID];
+ CKRecordID* currentClassCPointerID = [[CKRecordID alloc] initWithRecordName:SecCKKSKeyClassC zoneID:zoneID];
+
+ ZoneKeys* zonekeys = strongSelf.keys[zoneID];
+ if(!zonekeys) {
+ zonekeys = [[ZoneKeys alloc] init];
+ strongSelf.keys[zoneID] = zonekeys;
+ }
+
+ XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID], "Have a currentTLKPointer");
+ XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID], "Have a currentClassAPointer");
+ XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID], "Have a currentClassCPointer");
+ XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey], "Have a currentTLKPointer parent");
+ XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassAPointer parent");
+ XCTAssertNotNil(strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey], "Have a currentClassCPointer parent");
+ XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentTLKPointer parent UUID");
+ XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassAPointer parent UUID");
+ XCTAssertNotNil([strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID][SecCKRecordParentKeyRefKey] recordID].recordName, "Have a currentClassCPointer parent UUID");
+
+ zonekeys.currentTLKPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKPointerID]];
+ zonekeys.currentClassAPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAPointerID]];
+ zonekeys.currentClassCPointer = [[CKKSCurrentKeyPointer alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCPointerID]];
+
+ XCTAssertNotNil(zonekeys.currentTLKPointer.currentKeyUUID, "Have a currentTLKPointer current UUID");
+ XCTAssertNotNil(zonekeys.currentClassAPointer.currentKeyUUID, "Have a currentClassAPointer current UUID");
+ XCTAssertNotNil(zonekeys.currentClassCPointer.currentKeyUUID, "Have a currentClassCPointer current UUID");
+
+ CKRecordID* currentTLKID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentTLKPointer.currentKeyUUID zoneID:zoneID];
+ CKRecordID* currentClassAID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassAPointer.currentKeyUUID zoneID:zoneID];
+ CKRecordID* currentClassCID = [[CKRecordID alloc] initWithRecordName:zonekeys.currentClassCPointer.currentKeyUUID zoneID:zoneID];
+
+ zonekeys.tlk = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentTLKID]];
+ zonekeys.classA = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassAID]];
+ zonekeys.classC = [[CKKSKey alloc] initWithCKRecord: strongSelf.zones[zoneID].currentDatabase[currentClassCID]];
+
+ XCTAssertNotNil(zonekeys.tlk, "Have the current TLK");
+ XCTAssertNotNil(zonekeys.classA, "Have the current Class A key");
+ XCTAssertNotNil(zonekeys.classC, "Have the current Class C key");
+
+ NSMutableArray<CKKSTLKShare*>* shares = [NSMutableArray array];
+ for(CKRecordID* recordID in strongSelf.zones[zoneID].currentDatabase.allKeys) {
+ if([recordID.recordName hasPrefix: [CKKSTLKShare ckrecordPrefix]]) {
+ CKKSTLKShare* share = [[CKKSTLKShare alloc] initWithCKRecord:strongSelf.zones[zoneID].currentDatabase[recordID]];
+ XCTAssertNotNil(share, "Should be able to parse a CKKSTLKShare CKRecord into a CKKSTLKShare");
+ [shares addObject:share];
+ }
+ }
+ zonekeys.tlkShares = shares;
+
+ if(afterModification) {
+ afterModification();
+ }
+ }];
}
- (void)expectCKReceiveSyncKeyHierarchyError:(CKRecordZoneID*)zoneID {
return [self newRecord:recordID withNewItemData:dictionary key:zonekeys.classC];
}
-- (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary key:(CKKSKey*)key {
+- (CKKSItem*)newItem:(CKRecordID*)recordID withNewItemData:(NSDictionary*)dictionary key:(CKKSKey*)key {
NSError* error = nil;
CKKSItem* cipheritem = [CKKSItemEncrypter encryptCKKSItem:[[CKKSItem alloc] initWithUUID:recordID.recordName
parentKeyUUID:key.uuid
XCTAssertNil(error, "encrypted item with class c key");
XCTAssertNotNil(cipheritem, "Have an encrypted item");
- CKRecord* ckr = [cipheritem CKRecordWithZoneID: recordID.zoneID];
+ return cipheritem;
+}
+
+- (CKRecord*)newRecord: (CKRecordID*) recordID withNewItemData:(NSDictionary*) dictionary key:(CKKSKey*)key {
+ CKKSItem* item = [self newItem:recordID withNewItemData:dictionary key:key];
+
+ CKRecord* ckr = [item CKRecordWithZoneID: recordID.zoneID];
XCTAssertNotNil(ckr, "Created a CKRecord");
return ckr;
}
}];
}
+- (void)expectCKKSTLKSelfShareUpload:(CKRecordZoneID*)zoneID {
+ [self expectCKModifyKeyRecords:0 currentKeyPointerRecords:0 tlkShareRecords:1 zoneID:zoneID];
+}
+
- (void)checkNSyncableTLKsInKeychain:(size_t)n {
NSDictionary *query = @{(id)kSecClass : (id)kSecClassInternetPassword,
(id)kSecAttrAccessGroup : @"com.apple.security.ckks",
* @APPLE_LICENSE_HEADER_END@
*/
-#import <XCTest/XCTest.h>
#import <CloudKit/CloudKit.h>
#import <CloudKit/CloudKit_Private.h>
+#import <XCTest/XCTest.h>
#import <Foundation/Foundation.h>
-#import "keychain/ckks/tests/MockCloudKit.h"
#import "keychain/ckks/CKKSCKAccountStateTracker.h"
+#import "keychain/ckks/tests/MockCloudKit.h"
+
+NS_ASSUME_NONNULL_BEGIN
@class CKKSKey;
@class CKKSCKRecordHolder;
@interface CloudKitMockXCTest : XCTestCase
-#if OCTAGON
-
@property CKRecordZoneID* testZoneID;
-@property id mockDatabase;
-@property id mockContainer;
-@property id mockFakeCKModifyRecordZonesOperation;
-@property id mockFakeCKModifySubscriptionsOperation;
-@property id mockFakeCKFetchRecordZoneChangesOperation;
-@property id mockFakeCKFetchRecordsOperation;
-@property id mockFakeCKQueryOperation;
+@property (nullable) id mockDatabase;
+@property (nullable) id mockDatabaseExceptionCatcher;
+@property (nullable) id mockContainer;
+@property (nullable) id mockFakeCKModifyRecordZonesOperation;
+@property (nullable) id mockFakeCKModifySubscriptionsOperation;
+@property (nullable) id mockFakeCKFetchRecordZoneChangesOperation;
+@property (nullable) id mockFakeCKFetchRecordsOperation;
+@property (nullable) id mockFakeCKQueryOperation;
-@property id mockAccountStateTracker;
+@property (nullable) id mockAccountStateTracker;
@property CKAccountStatus accountStatus;
@property BOOL supportsDeviceToDeviceEncryption;
@property NSString* circlePeerID;
-@property bool aksLockState; // The current 'AKS lock state'
+@property bool aksLockState; // The current 'AKS lock state'
@property (readonly) CKKSLockStateTracker* lockStateTracker;
-@property id mockLockStateTracker;
+@property (nullable) id mockLockStateTracker;
-@property NSMutableDictionary<CKRecordZoneID*, FakeCKZone*>* zones;
+@property (nullable) NSMutableDictionary<CKRecordZoneID*, FakeCKZone*>* zones;
-@property NSOperationQueue* operationQueue;
-@property NSBlockOperation* ckksHoldOperation;
-@property NSBlockOperation* ckaccountHoldOperation;
+@property (nullable) NSOperationQueue* operationQueue;
+@property (nullable) NSBlockOperation* ckaccountHoldOperation;
-@property NSBlockOperation* ckModifyHoldOperation;
-@property NSBlockOperation* ckFetchHoldOperation;
+@property (nullable) NSBlockOperation* ckModifyHoldOperation;
+@property (nullable) NSBlockOperation* ckFetchHoldOperation;
@property bool silentFetchesAllowed;
-@property id mockCKKSViewManager;
-@property CKKSViewManager* injectedManager;
+@property (nullable) id mockCKKSViewManager;
+@property (nullable) CKKSViewManager* injectedManager;
-- (CKKSKey*) fakeTLK: (CKRecordZoneID*)zoneID;
+- (CKKSKey*)fakeTLK:(CKRecordZoneID*)zoneID;
-- (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords
- currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords
- zoneID: (CKRecordZoneID*) zoneID;
-- (void)expectCKModifyItemRecords: (NSUInteger) expectedNumberOfRecords
- currentKeyPointerRecords: (NSUInteger) expectedCurrentKeyRecords
- zoneID: (CKRecordZoneID*) zoneID
- checkItem: (BOOL (^)(CKRecord*)) checkItem;
+- (void)expectCKModifyItemRecords:(NSUInteger)expectedNumberOfRecords
+ currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
+ zoneID:(CKRecordZoneID*)zoneID;
+- (void)expectCKModifyItemRecords:(NSUInteger)expectedNumberOfRecords
+ currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
+ zoneID:(CKRecordZoneID*)zoneID
+ checkItem:(BOOL (^_Nullable)(CKRecord*))checkItem;
- (void)expectCKModifyItemRecords:(NSUInteger)expectedNumberOfModifiedRecords
deletedRecords:(NSUInteger)expectedNumberOfDeletedRecords
currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
zoneID:(CKRecordZoneID*)zoneID
- checkItem:(BOOL (^)(CKRecord*))checkItem;
+ checkItem:(BOOL (^_Nullable)(CKRecord*))checkItem;
-- (void)expectCKDeleteItemRecords: (NSUInteger) expectedNumberOfRecords zoneID: (CKRecordZoneID*) zoneID;
+- (void)expectCKDeleteItemRecords:(NSUInteger)expectedNumberOfRecords zoneID:(CKRecordZoneID*)zoneID;
- (void)expectCKModifyKeyRecords:(NSUInteger)expectedNumberOfRecords
currentKeyPointerRecords:(NSUInteger)expectedCurrentKeyRecords
tlkShareRecords:(NSUInteger)expectedTLKShareRecords
zoneID:(CKRecordZoneID*)zoneID;
-- (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>*) expectedRecordTypeCounts
- deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>*) expectedDeletedRecordTypeCounts
- zoneID:(CKRecordZoneID*) zoneID
- checkModifiedRecord:(BOOL (^)(CKRecord*)) checkRecord
- runAfterModification:(void (^) ())afterModification;
+- (void)expectCKModifyRecords:(NSDictionary<NSString*, NSNumber*>*)expectedRecordTypeCounts
+ deletedRecordTypeCounts:(NSDictionary<NSString*, NSNumber*>* _Nullable)expectedDeletedRecordTypeCounts
+ zoneID:(CKRecordZoneID*)zoneID
+ checkModifiedRecord:(BOOL (^_Nullable)(CKRecord*))checkRecord
+ runAfterModification:(void (^_Nullable)())afterModification;
- (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID;
-- (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)())blockAfterReject;
-- (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID blockAfterReject: (void (^)())blockAfterReject withError:(NSError*)error;
-- (void)expectCKAtomicModifyItemRecordsUpdateFailure: (CKRecordZoneID*) zoneID;
+- (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID
+ blockAfterReject:(void (^_Nullable)())blockAfterReject;
+- (void)failNextCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID
+ blockAfterReject:(void (^_Nullable)())blockAfterReject
+ withError:(NSError* _Nullable)error;
+- (void)expectCKAtomicModifyItemRecordsUpdateFailure:(CKRecordZoneID*)zoneID;
- (void)failNextZoneCreation:(CKRecordZoneID*)zoneID;
- (void)failNextZoneCreationSilently:(CKRecordZoneID*)zoneID;
// Use this to 1) assert that a fetch occurs and 2) cause a block to run _after_ all changes have been delivered but _before_ the fetch 'completes'.
// This way, you can modify the CK zone to cause later collisions.
-- (void)expectCKFetchAndRunBeforeFinished: (void (^)())blockAfterFetch;
+- (void)expectCKFetchAndRunBeforeFinished:(void (^_Nullable)())blockAfterFetch;
// Use this to assert that a FakeCKFetchRecordsOperation occurs.
--(void)expectCKFetchByRecordID;
+- (void)expectCKFetchByRecordID;
// Use this to assert that a FakeCKQueryOperation occurs.
- (void)expectCKFetchByQuery;
// Wait until all scheduled cloudkit operations are reflected in the currentDatabase
- (void)waitForCKModifications;
-// Unblocks the CKKS subsystem only.
-- (void)startCKKSSubsystemOnly;
-
// Unblocks the CKAccount mock subsystem. Until this is called, the tests believe cloudd hasn't returned any account status yet.
- (void)startCKAccountStatusMock;
- (void)startCKKSSubsystem;
// Blocks the completion (partial or full) of CloudKit modifications
--(void)holdCloudKitModifications;
+- (void)holdCloudKitModifications;
// Unblocks the hold you've added with holdCloudKitModifications; CloudKit modifications will finish
--(void)releaseCloudKitModificationHold;
+- (void)releaseCloudKitModificationHold;
// Blocks the CloudKit fetches from beginning (similar to network latency)
--(void)holdCloudKitFetches;
+- (void)holdCloudKitFetches;
// Unblocks the hold you've added with holdCloudKitFetches; CloudKit fetches will finish
--(void)releaseCloudKitFetchHold;
+- (void)releaseCloudKitFetchHold;
// Make a CK internal server extension error with a given code and description.
- (NSError*)ckInternalServerExtensionError:(NSInteger)code description:(NSString*)desc;
// Schedule an operation for execution (and failure), with some existing record errors.
// Other records in the operation but not in failedRecords will have CKErrorBatchRequestFailed errors created.
--(void)rejectWrite:(CKModifyRecordsOperation*)op failedRecords:(NSMutableDictionary<CKRecordID*, NSError*>*)failedRecords;
+- (void)rejectWrite:(CKModifyRecordsOperation*)op failedRecords:(NSMutableDictionary<CKRecordID*, NSError*>*)failedRecords;
-#endif // OCTAGON
@end
+
+NS_ASSUME_NONNULL_END
- (void)setUp {
[super setUp];
+ NSString* testName = [self.name componentsSeparatedByString:@" "][1];
+ testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
+ secnotice("ckkstest", "Beginning test %@", testName);
+
// All tests start with the same flag set.
SecCKKSTestResetFlags();
SecCKKSTestSetDisableSOS(true);
self.zones = [[NSMutableDictionary alloc] init];
+ self.mockDatabaseExceptionCatcher = OCMStrictClassMock([CKDatabase class]);
self.mockDatabase = OCMStrictClassMock([CKDatabase class]);
self.mockContainer = OCMClassMock([CKContainer class]);
OCMStub([self.mockContainer containerWithIdentifier:[OCMArg isKindOfClass:[NSString class]]]).andReturn(self.mockContainer);
OCMStub([self.mockContainer alloc]).andReturn(self.mockContainer);
OCMStub([self.mockContainer containerIdentifier]).andReturn(SecCKKSContainerName);
OCMStub([self.mockContainer initWithContainerID: [OCMArg any] options: [OCMArg any]]).andReturn(self.mockContainer);
- OCMStub([self.mockContainer privateCloudDatabase]).andReturn(self.mockDatabase);
+ OCMStub([self.mockContainer privateCloudDatabase]).andReturn(self.mockDatabaseExceptionCatcher);
OCMStub([self.mockContainer serverPreferredPushEnvironmentWithCompletionHandler: ([OCMArg invokeBlockWithArgs:@"fake APS push string", [NSNull null], nil])]);
+ // Use two layers of mockDatabase here, so we can both add Expectations and catch the exception (instead of crash) when one fails.
+ OCMStub([self.mockDatabaseExceptionCatcher addOperation:[OCMArg any]]).andCall(self, @selector(ckdatabaseAddOperation:));
+
// If you want to change this, you'll need to update the mock
- _ckDeviceID = @"fake-cloudkit-device-id";
+ _ckDeviceID = [NSString stringWithFormat:@"fake-cloudkit-device-id-%@", testName];
OCMStub([self.mockContainer fetchCurrentDeviceIDWithCompletionHandler: ([OCMArg invokeBlockWithArgs:self.ckDeviceID, [NSNull null], nil])]);
self.accountStatus = CKAccountStatusAvailable;
self.supportsDeviceToDeviceEncryption = YES;
- // Inject a fake operation dependency into the manager object, so that the tests can perform setup and mock expectations before zone setup begins
- // Also blocks all CK account state retrieval operations (but not circle status ones)
+ // Inject a fake operation dependency so we won't respond with the CloudKit account status immediately
+ // The CKKSCKAccountStateTracker won't send any login/logout calls without that information, so this blocks all CKKS setup
self.ckaccountHoldOperation = [NSBlockOperation named:@"ckaccount-hold" withBlock:^{
secnotice("ckks", "CKKS CK account status test hold released");
}];
OCMStub([self.mockAccountStateTracker getCircleStatus]).andCall(self, @selector(circleStatus));
// If we're in circle, come up with a fake circle id. Otherwise, return an error.
- self.circlePeerID = @"fake-circle-id";
+ self.circlePeerID = [NSString stringWithFormat:@"fake-circle-id-%@", testName];
OCMStub([self.mockAccountStateTracker fetchCirclePeerID:
[OCMArg checkWithBlock:^BOOL(void (^passedBlock) (NSString* peerID,
NSError * error)) {
self.testZoneID = [[CKRecordZoneID alloc] initWithZoneName:@"testzone" ownerName:CKCurrentUserDefaultName];
- // Inject a fake operation dependency into the manager object, so that the tests can perform setup and mock expectations before zone setup begins
- // Also blocks all CK account state retrieval operations (but not circle status ones)
- self.ckksHoldOperation = [[NSBlockOperation alloc] init];
- [self.ckksHoldOperation addExecutionBlock:^{
- secnotice("ckks", "CKKS testing hold released");
- }];
- self.ckksHoldOperation.name = @"ckks-hold";
-
- //self.mockCKKSViewManagerClass = OCMClassMock([CKKSViewManager class]);
-
// We don't want to use class mocks here, because they don't play well with partial mocks
self.mockCKKSViewManager = OCMPartialMock(
[[CKKSViewManager alloc] initWithContainerName:SecCKKSContainerName
modifyRecordZonesOperationClass:[FakeCKModifyRecordZonesOperation class]
apsConnectionClass:[FakeAPSConnection class]
nsnotificationCenterClass:[FakeNSNotificationCenter class]
- notifierClass:[FakeCKKSNotifier class]
- setupHold:self.ckksHoldOperation]);
+ notifierClass:[FakeCKKSNotifier class]]);
OCMStub([self.mockCKKSViewManager viewList]).andCall(self, @selector(managedViewList));
OCMStub([self.mockCKKSViewManager syncBackupAndNotifyAboutSync]);
[CKKSViewManager resetManager:false setTo:self.injectedManager];
// Make a new fake keychain
- NSString* smallName = [self.name componentsSeparatedByString:@" "][1];
- smallName = [smallName stringByReplacingOccurrencesOfString:@"]" withString:@""];
-
- NSString* tmp_dir = [NSString stringWithFormat: @"/tmp/%@.%X", smallName, arc4random()];
+ NSString* tmp_dir = [NSString stringWithFormat: @"/tmp/%@.%X", testName, arc4random()];
[[NSFileManager defaultManager] createDirectoryAtPath:[NSString stringWithFormat: @"%@/Library/Keychains", tmp_dir] withIntermediateDirectories:YES attributes:nil error:NULL];
SetCustomHomeURLString((__bridge CFStringRef) tmp_dir);
kc_with_dbt(true, NULL, ^bool (SecDbConnectionRef dbt) { return false; });
}
+- (void)ckdatabaseAddOperation:(NSOperation*)op {
+ @try {
+ [self.mockDatabase addOperation:op];
+ } @catch (NSException *exception) {
+ XCTFail("Received an database exception: %@", exception);
+ }
+}
+
-(CKKSCKAccountStateTracker*)accountStateTracker {
return self.injectedManager.accountTracker;
}
- (void)startCKKSSubsystem {
[self startCKAccountStatusMock];
- [self startCKKSSubsystemOnly];
-}
-
-- (void)startCKKSSubsystemOnly {
- // Note: currently, based on how we're mocking up the zone creation and zone subscription operation,
- // they will 'fire' before this method is called. It's harmless, since the mocks immediately succeed
- // and return; it's just a tad confusing.
- if([self.ckksHoldOperation isPending]) {
- [self.operationQueue addOperation: self.ckksHoldOperation];
- }
}
- (void)startCKAccountStatusMock {
return NO;
}
- if([zone errorFromSavingRecord: record]) {
- secnotice("fakecloudkit", "Record zone rejected record write: %@", record);
+ NSError* recordError = [zone errorFromSavingRecord: record];
+ if(recordError) {
+ secnotice("fakecloudkit", "Record zone rejected record write: %@ %@", recordError, record);
+ XCTFail(@"Record zone rejected record write: %@ %@", recordError, record);
return NO;
}
[zone deleteCKRecordIDFromZone: recordID];
}
- op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil);
-
if(afterModification) {
afterModification();
}
+ op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil);
op.isFinished = YES;
}
}];
}
- (void)tearDown {
- // Put teardown code here. This method is called after the invocation of each test method in the class.
+ NSString* testName = [self.name componentsSeparatedByString:@" "][1];
+ testName = [testName stringByReplacingOccurrencesOfString:@"]" withString:@""];
+ secnotice("ckkstest", "Ending test %@", testName);
if(SecCKKSIsEnabled()) {
- // Ensure we don't have any blocking operations
- self.accountStatus = CKAccountStatusNoAccount;
- [self startCKKSSubsystem];
+ self.accountStatus = CKAccountStatusCouldNotDetermine;
+
+ [self.ckaccountHoldOperation cancel];
+ self.ckaccountHoldOperation = nil;
+ // Ensure we don't have any blocking operations left
+ [self.operationQueue cancelAllOperations];
[self waitForCKModifications];
XCTAssertEqual(0, [self.injectedManager.completedSecCKKSInitialize wait:2*NSEC_PER_SEC],
"Timeout did not occur waiting for SecCKKSInitialize");
// Make sure this happens before teardown.
- XCTAssertEqual(0, [self.accountStateTracker.finishedInitialCalls wait:1*NSEC_PER_SEC], "Account state tracker initialized itself");
+ XCTAssertEqual(0, [self.accountStateTracker.finishedInitialDispatches wait:1*NSEC_PER_SEC], "Account state tracker initialized itself");
+
+ dispatch_group_t accountChangesDelivered = [self.accountStateTracker checkForAllDeliveries];
+ XCTAssertEqual(0, dispatch_group_wait(accountChangesDelivered, dispatch_time(DISPATCH_TIME_NOW, 2*NSEC_PER_SEC)), "Account state tracker finished delivering everything");
}
[super tearDown];
[self.mockDatabase stopMocking];
self.mockDatabase = nil;
+ [self.mockDatabaseExceptionCatcher stopMocking];
+ self.mockDatabaseExceptionCatcher = nil;
+
[self.mockContainer stopMocking];
self.mockContainer = nil;
self.zones = nil;
+
self.operationQueue = nil;
- self.ckksHoldOperation = nil;
- self.ckaccountHoldOperation = nil;
SecCKKSTestResetFlags();
}
*/
-#import <Foundation/Foundation.h>
#import <CloudKit/CloudKit.h>
+#import <Foundation/Foundation.h>
-#import "keychain/ckks/CloudKitDependencies.h"
#import "keychain/ckks/CKKSNotifier.h"
+#import "keychain/ckks/CloudKitDependencies.h"
NS_ASSUME_NONNULL_BEGIN
@interface FakeCKModifyRecordZonesOperation : NSBlockOperation <CKKSModifyRecordZonesOperation>
@property (nullable) NSError* creationError;
-@property (nonatomic, nullable) NSMutableArray<CKRecordZone *>* recordZonesSaved;
-@property (nonatomic, nullable) NSMutableArray<CKRecordZoneID *>* recordZoneIDsDeleted;
-+(FakeCKDatabase*) ckdb;
+@property (nonatomic, nullable) NSMutableArray<CKRecordZone*>* recordZonesSaved;
+@property (nonatomic, nullable) NSMutableArray<CKRecordZoneID*>* recordZoneIDsDeleted;
++ (FakeCKDatabase*)ckdb;
@end
@interface FakeCKModifySubscriptionsOperation : NSBlockOperation <CKKSModifySubscriptionsOperation>
@property (nullable) NSError* subscriptionError;
-@property (nonatomic, nullable) NSMutableArray<CKSubscription *> *subscriptionsSaved;
-@property (nonatomic, nullable) NSMutableArray<NSString *> *subscriptionIDsDeleted;
-+(FakeCKDatabase*) ckdb;
+@property (nonatomic, nullable) NSMutableArray<CKSubscription*>* subscriptionsSaved;
+@property (nonatomic, nullable) NSMutableArray<NSString*>* subscriptionIDsDeleted;
++ (FakeCKDatabase*)ckdb;
@end
-
@interface FakeCKFetchRecordZoneChangesOperation : NSOperation <CKKSFetchRecordZoneChangesOperation>
-+(FakeCKDatabase*) ckdb;
++ (FakeCKDatabase*)ckdb;
@property (nullable) void (^blockAfterFetch)();
@end
+ (FakeCKDatabase*)ckdb;
@end
-
@interface FakeAPSConnection : NSObject <CKKSAPSConnection>
@end
-
-@interface FakeNSNotificationCenter : NSObject<CKKSNSNotificationCenter>
+@interface FakeNSNotificationCenter : NSObject <CKKSNSNotificationCenter>
+ (instancetype)defaultCenter;
- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;
@end
-
@interface FakeCKZone : NSObject
// Used while mocking: database is the contents of the current current CloudKit database, pastDatabase is the state of the world in the past at different change tokens
@property CKRecordZoneID* zoneID;
@property CKServerChangeToken* currentChangeToken;
@property NSMutableDictionary<CKRecordID*, CKRecord*>* currentDatabase;
-@property NSMutableDictionary<CKServerChangeToken*, NSMutableDictionary<CKRecordID*, CKRecord*>*>* pastDatabases;
-@property bool flag; // used however you'd like in a test
+@property NSMutableDictionary<CKServerChangeToken*, NSMutableDictionary<CKRecordID*, CKRecord*>*>* pastDatabases;
+@property bool flag; // used however you'd like in a test
// Usually nil. If set, trying to 'create' this zone should fail.
@property (nullable) NSError* creationError;
// Usually nil. If set, trying to subscribe to this zone should fail.
@property (nullable) NSError* subscriptionError;
-- (instancetype)initZone: (CKRecordZoneID*) zoneID;
+- (instancetype)initZone:(CKRecordZoneID*)zoneID;
- (void)rollChangeToken;
// Always Succeed
-- (void)addToZone: (CKKSCKRecordHolder*) item zoneID: (CKRecordZoneID*) zoneID;
-- (void)addToZone: (CKRecord*) record;
+- (void)addToZone:(CKKSCKRecordHolder*)item zoneID:(CKRecordZoneID*)zoneID;
+- (void)addToZone:(CKRecord*)record;
+
+// Removes this record from all versions of the CK database, without changing the change tag
+- (void)deleteFromHistory:(CKRecordID*)recordID;
-- (void)addCKRecordToZone: (CKRecord*) record;
-- (NSError* _Nullable)deleteCKRecordIDFromZone:(CKRecordID*) recordID;
+- (void)addCKRecordToZone:(CKRecord*)record;
+- (NSError* _Nullable)deleteCKRecordIDFromZone:(CKRecordID*)recordID;
// Sets up the next fetchChanges to fail with this error
-- (void)failNextFetchWith: (NSError*) fetchChangesError;
+- (void)failNextFetchWith:(NSError*)fetchChangesError;
// Get the next fetchChanges error. Returns NULL if the fetchChanges should succeed.
-- (NSError * _Nullable)popFetchChangesError;
+- (NSError* _Nullable)popFetchChangesError;
// Checks if this record add/modification should fail
-- (NSError * _Nullable)errorFromSavingRecord:(CKRecord*) record;
+- (NSError* _Nullable)errorFromSavingRecord:(CKRecord*)record;
@end
@interface FakeCKKSNotifier : NSObject <CKKSNotifier>
- (void)addToZone: (CKKSCKRecordHolder*) item zoneID: (CKRecordZoneID*) zoneID {
CKRecord* record = [item CKRecordWithZoneID: zoneID];
[self addToZone: record];
+
+ // Save off the etag
+ item.storedCKRecord = record;
}
- (void)addToZone: (CKRecord*) record {
[self addToZone: record];
}
+- (void)deleteFromHistory:(CKRecordID*)recordID {
+ for(NSMutableDictionary* pastDatabase in self.pastDatabases.objectEnumerator) {
+ [pastDatabase removeObjectForKey:recordID];
+ }
+ [self.currentDatabase removeObjectForKey:recordID];
+}
+
+
- (NSError*)deleteCKRecordIDFromZone:(CKRecordID*) recordID {
// todo: fail somehow