From 3f0f0d49a9b6c2c6d459239f5926d59314cdeacf Mon Sep 17 00:00:00 2001 From: Apple Date: Sun, 4 Feb 2018 13:56:53 +0000 Subject: [PATCH] Security-58286.41.2.tar.gz --- OSX/authd/engine.c | 10 +- .../SOSAccountTrustClassic+Identity.m | 4 + .../SecureObjectSync/SOSExports.exp-in | 4 + .../SecureObjectSync/SOSFullPeerInfo.h | 1 + .../SecureObjectSync/SOSFullPeerInfo.m | 13 + OSX/sec/Security/SecCertificate.c | 123 +++ OSX/sec/Security/SecCertificateInternal.h | 4 + OSX/sec/Security/SecExports.exp-in | 3 + OSX/sec/Security/SecFramework.c | 4 + OSX/sec/ipc/server_xpc.m | 109 +-- OSX/sec/securityd/SecItemDb.c | 4 + OSX/sec/securityd/nameconstraints.c | 193 +++-- Security.xcodeproj/project.pbxproj | 18 + keychain/ckks/CKKS.h | 119 ++- keychain/ckks/CKKS.m | 31 +- keychain/ckks/CKKSAPSReceiver.h | 16 +- keychain/ckks/CKKSAPSReceiver.m | 2 +- keychain/ckks/CKKSCKAccountStateTracker.h | 49 +- keychain/ckks/CKKSCKAccountStateTracker.m | 94 ++- keychain/ckks/CKKSCondition.h | 13 +- keychain/ckks/CKKSCondition.m | 10 +- keychain/ckks/CKKSControl.h | 24 +- keychain/ckks/CKKSControlProtocol.h | 1 + keychain/ckks/CKKSCurrentItemPointer.h | 22 +- keychain/ckks/CKKSCurrentKeyPointer.h | 19 +- keychain/ckks/CKKSDeviceStateEntry.h | 17 +- .../CKKSFetchAllRecordZoneChangesOperation.h | 8 +- .../CKKSFetchAllRecordZoneChangesOperation.m | 20 +- keychain/ckks/CKKSFixups.h | 17 +- keychain/ckks/CKKSFixups.m | 78 +- keychain/ckks/CKKSGroupOperation.h | 13 +- keychain/ckks/CKKSGroupOperation.m | 10 +- keychain/ckks/CKKSHealKeyHierarchyOperation.h | 3 +- keychain/ckks/CKKSHealKeyHierarchyOperation.m | 19 +- keychain/ckks/CKKSHealTLKSharesOperation.h | 5 +- keychain/ckks/CKKSHealTLKSharesOperation.m | 6 +- keychain/ckks/CKKSIncomingQueueEntry.h | 37 +- keychain/ckks/CKKSIncomingQueueOperation.h | 6 +- keychain/ckks/CKKSIncomingQueueOperation.m | 51 +- keychain/ckks/CKKSItem.h | 111 ++- keychain/ckks/CKKSItem.m | 37 +- keychain/ckks/CKKSItemEncrypter.h | 43 +- keychain/ckks/CKKSKey.h | 148 ++-- keychain/ckks/CKKSKey.m | 342 +++++--- keychain/ckks/CKKSKeychainView.h | 212 ++--- keychain/ckks/CKKSKeychainView.m | 772 ++++++++++-------- keychain/ckks/CKKSLocalSynchronizeOperation.h | 47 ++ keychain/ckks/CKKSLocalSynchronizeOperation.m | 197 +++++ keychain/ckks/CKKSLockStateTracker.h | 10 +- keychain/ckks/CKKSManifest.h | 22 +- keychain/ckks/CKKSManifest.m | 21 +- keychain/ckks/CKKSManifestLeafRecord.h | 8 +- keychain/ckks/CKKSMirrorEntry.h | 20 +- keychain/ckks/CKKSNearFutureScheduler.h | 35 +- keychain/ckks/CKKSNearFutureScheduler.m | 29 +- keychain/ckks/CKKSNewTLKOperation.h | 3 +- keychain/ckks/CKKSNewTLKOperation.m | 35 +- keychain/ckks/CKKSNotifier.h | 11 +- keychain/ckks/CKKSNotifier.m | 4 + keychain/ckks/CKKSOutgoingQueueEntry.h | 48 +- keychain/ckks/CKKSOutgoingQueueOperation.h | 2 +- keychain/ckks/CKKSOutgoingQueueOperation.m | 65 +- keychain/ckks/CKKSPeer.h | 25 +- .../ckks/CKKSProcessReceivedKeysOperation.m | 8 +- keychain/ckks/CKKSRateLimiter.h | 19 +- keychain/ckks/CKKSRateLimiter.m | 2 +- keychain/ckks/CKKSRecordHolder.h | 35 +- .../CKKSReencryptOutgoingItemsOperation.h | 5 +- .../CKKSReencryptOutgoingItemsOperation.m | 5 +- keychain/ckks/CKKSResultOperation.h | 16 +- keychain/ckks/CKKSResultOperation.m | 29 +- keychain/ckks/CKKSSIV.h | 37 +- keychain/ckks/CKKSSQLDatabaseObject.h | 122 +-- keychain/ckks/CKKSSQLDatabaseObject.m | 3 +- keychain/ckks/CKKSScanLocalItemsOperation.h | 2 +- keychain/ckks/CKKSScanLocalItemsOperation.m | 15 +- keychain/ckks/CKKSSynchronizeOperation.h | 3 +- keychain/ckks/CKKSTLKShare.h | 70 +- .../CKKSUpdateCurrentItemPointerOperation.h | 5 +- .../CKKSUpdateCurrentItemPointerOperation.m | 8 +- .../ckks/CKKSUpdateDeviceStateOperation.h | 10 +- keychain/ckks/CKKSViewManager.h | 113 +-- keychain/ckks/CKKSViewManager.m | 156 ++-- keychain/ckks/CKKSZone.h | 78 +- keychain/ckks/CKKSZone.m | 257 +++--- keychain/ckks/CKKSZoneChangeFetcher.h | 19 +- keychain/ckks/CKKSZoneStateEntry.h | 22 +- keychain/ckks/CloudKitCategories.h | 19 +- keychain/ckks/CloudKitDependencies.h | 105 ++- keychain/ckks/NSOperationCategories.h | 11 +- keychain/ckks/NSOperationCategories.m | 2 +- keychain/ckks/RateLimiter.h | 27 +- keychain/ckks/RateLimiter.m | 2 +- keychain/ckks/tests/AutoreleaseTest.c | 99 +++ keychain/ckks/tests/AutoreleaseTest.h | 30 + .../ckks/tests/CKKSAESSIVEncryptionTests.m | 53 ++ keychain/ckks/tests/CKKSAPSReceiverTests.m | 12 +- keychain/ckks/tests/CKKSCloudKitTests.m | 14 +- keychain/ckks/tests/CKKSConditionTests.m | 14 + keychain/ckks/tests/CKKSLoggerTests.m | 133 +-- keychain/ckks/tests/CKKSManifestTests.m | 7 +- .../ckks/tests/CKKSNearFutureSchedulerTests.m | 227 ++++- keychain/ckks/tests/CKKSSOSTests.m | 57 +- keychain/ckks/tests/CKKSSQLTests.m | 4 +- .../tests/CKKSServerValidationRecoveryTests.m | 19 +- .../tests/CKKSTLKSharingEncryptionTests.m | 13 +- keychain/ckks/tests/CKKSTLKSharingTests.m | 101 ++- keychain/ckks/tests/CKKSTests+API.h | 34 +- keychain/ckks/tests/CKKSTests+API.m | 80 +- .../ckks/tests/CKKSTests+CurrentPointerAPI.m | 114 ++- keychain/ckks/tests/CKKSTests.h | 19 +- keychain/ckks/tests/CKKSTests.m | 460 +++++++++-- .../tests/CloudKitKeychainSyncingFixupTests.m | 93 ++- .../tests/CloudKitKeychainSyncingMockXCTest.h | 94 ++- .../tests/CloudKitKeychainSyncingMockXCTest.m | 211 ++--- keychain/ckks/tests/CloudKitMockXCTest.h | 103 +-- keychain/ckks/tests/CloudKitMockXCTest.m | 87 +- keychain/ckks/tests/MockCloudKit.h | 47 +- keychain/ckks/tests/MockCloudKit.m | 11 + 119 files changed, 4435 insertions(+), 2333 deletions(-) create mode 100644 keychain/ckks/CKKSLocalSynchronizeOperation.h create mode 100644 keychain/ckks/CKKSLocalSynchronizeOperation.m create mode 100644 keychain/ckks/tests/AutoreleaseTest.c create mode 100644 keychain/ckks/tests/AutoreleaseTest.h diff --git a/OSX/authd/engine.c b/OSX/authd/engine.c index 08780ce1..7991cff5 100644 --- a/OSX/authd/engine.c +++ b/OSX/authd/engine.c @@ -482,6 +482,12 @@ _evaluate_mechanisms(engine_t engine, CFArrayRef mechanisms) 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 @@ -1387,7 +1393,7 @@ OSStatus engine_authorize(engine_t engine, auth_rights_t rights, auth_items_t en 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); @@ -1804,7 +1810,7 @@ CFTypeRef engine_copy_context(engine_t engine, auth_items_t source) 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; diff --git a/OSX/sec/SOSCircle/SecureObjectSync/SOSAccountTrustClassic+Identity.m b/OSX/sec/SOSCircle/SecureObjectSync/SOSAccountTrustClassic+Identity.m index 18515919..6c8a90fe 100644 --- a/OSX/sec/SOSCircle/SecureObjectSync/SOSAccountTrustClassic+Identity.m +++ b/OSX/sec/SOSCircle/SecureObjectSync/SOSAccountTrustClassic+Identity.m @@ -72,6 +72,10 @@ NSString* octagonKeyName; SecKeyRef publicKey; + if (SOSFullPeerInfoHaveOctagonKeys(self.fullPeerInfo)) { + return; + } + bool changedSelf = false; CFErrorRef copyError = NULL; diff --git a/OSX/sec/SOSCircle/SecureObjectSync/SOSExports.exp-in b/OSX/sec/SOSCircle/SecureObjectSync/SOSExports.exp-in index 239c1b94..1434f14d 100644 --- a/OSX/sec/SOSCircle/SecureObjectSync/SOSExports.exp-in +++ b/OSX/sec/SOSCircle/SecureObjectSync/SOSExports.exp-in @@ -275,6 +275,10 @@ _SOSCircleRequestAdmission _SOSCircleAcceptRequest _SOSCircleHasPeer +_SOSFullPeerInfoCopyOctagonSigningKey +_SOSFullPeerInfoCopyOctagonEncryptionKey +_SOSFullPeerInfoCopyOctagonPublicEncryptionKey +_SOSFullPeerInfoCopyOctagonPublicSigningKey _SOSPiggyBackBlobCreateFromData _SOSPiggyBackBlobCopyEncodedData diff --git a/OSX/sec/SOSCircle/SecureObjectSync/SOSFullPeerInfo.h b/OSX/sec/SOSCircle/SecureObjectSync/SOSFullPeerInfo.h index f6d269fb..d969cf45 100644 --- a/OSX/sec/SOSCircle/SecureObjectSync/SOSFullPeerInfo.h +++ b/OSX/sec/SOSCircle/SecureObjectSync/SOSFullPeerInfo.h @@ -60,6 +60,7 @@ SecKeyRef SOSFullPeerInfoCopyOctagonPublicSigningKey(SOSFullPeerInfoRef fullPeer 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); diff --git a/OSX/sec/SOSCircle/SecureObjectSync/SOSFullPeerInfo.m b/OSX/sec/SOSCircle/SecureObjectSync/SOSFullPeerInfo.m index 1a5e5a65..d698f2d5 100644 --- a/OSX/sec/SOSCircle/SecureObjectSync/SOSFullPeerInfo.m +++ b/OSX/sec/SOSCircle/SecureObjectSync/SOSFullPeerInfo.m @@ -682,6 +682,19 @@ SecKeyRef SOSFullPeerInfoCopyOctagonEncryptionKey(SOSFullPeerInfoRef fullPeer, C 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 // diff --git a/OSX/sec/Security/SecCertificate.c b/OSX/sec/Security/SecCertificate.c index 97e853b1..007c8008 100644 --- a/OSX/sec/Security/SecCertificate.c +++ b/OSX/sec/Security/SecCertificate.c @@ -4317,6 +4317,59 @@ const DERItem * SecCertificateGetSubjectAltName(SecCertificateRef certificate) { 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; @@ -4349,6 +4402,38 @@ CFArrayRef SecCertificateCopyIPAddresses(SecCertificateRef certificate) { 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; @@ -4468,6 +4553,32 @@ static OSStatus appendDNSNamesFromX501Name(void *context, const DERItem *type, 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. */ @@ -4568,6 +4679,18 @@ OSStatus SecCertificateCopyEmailAddresses(SecCertificateRef certificate, CFArray 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; diff --git a/OSX/sec/Security/SecCertificateInternal.h b/OSX/sec/Security/SecCertificateInternal.h index 948d523c..9bb87ba3 100644 --- a/OSX/sec/Security/SecCertificateInternal.h +++ b/OSX/sec/Security/SecCertificateInternal.h @@ -181,6 +181,10 @@ bool SecCertificateIsOidString(CFStringRef oid); DERItem *SecCertificateGetExtensionValue(SecCertificateRef certificate, CFTypeRef oid); +CFArrayRef SecCertificateCopyDNSNamesFromSubject(SecCertificateRef certificate); +CFArrayRef SecCertificateCopyIPAddressesFromSubject(SecCertificateRef certificate); +CFArrayRef SecCertificateCopyRFC822NamesFromSubject(SecCertificateRef certificate); + __END_DECLS #endif /* !_SECURITY_SECCERTIFICATEINTERNAL_H_ */ diff --git a/OSX/sec/Security/SecExports.exp-in b/OSX/sec/Security/SecExports.exp-in index d0ea3926..af8c6c75 100644 --- a/OSX/sec/Security/SecExports.exp-in +++ b/OSX/sec/Security/SecExports.exp-in @@ -506,12 +506,14 @@ _SecCertificateCopyCommonNames _SecCertificateCopyCompanyName _SecCertificateCopyCountry _SecCertificateCopyDNSNames +_SecCertificateCopyDNSNamesFromSubject _SecCertificateCopyData _SecCertificateCopyEmailAddresses _SecCertificateCopyEscrowRoots _SecCertificateCopyExtendedKeyUsage _SecCertificateCopyiAPAuthCapabilities _SecCertificateCopyIPAddresses +_SecCertificateCopyIPAddressesFromSubject _SecCertificateCopyiPhoneDeviceCAChain _SecCertificateCopyIssuerSHA1Digest _SecCertificateCopyIssuerSequence @@ -528,6 +530,7 @@ _SecCertificateCopyProperties _SecCertificateCopyPublicKey _SecCertificateCopyPublicKeySHA1Digest _SecCertificateCopyRFC822Names +_SecCertificateCopyRFC822NamesFromSubject _SecCertificateCopySerialNumber _SecCertificateCopySerialNumberData _SecCertificateCopySHA256Digest diff --git a/OSX/sec/Security/SecFramework.c b/OSX/sec/Security/SecFramework.c index 3a895015..83d7782c 100644 --- a/OSX/sec/Security/SecFramework.c +++ b/OSX/sec/Security/SecFramework.c @@ -54,7 +54,11 @@ /* 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)); diff --git a/OSX/sec/ipc/server_xpc.m b/OSX/sec/ipc/server_xpc.m index b4cf8019..1c7a42cd 100644 --- a/OSX/sec/ipc/server_xpc.m +++ b/OSX/sec/ipc/server_xpc.m @@ -145,73 +145,76 @@ __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); diff --git a/OSX/sec/securityd/SecItemDb.c b/OSX/sec/securityd/SecItemDb.c index 8a30763c..5ebca2b1 100644 --- a/OSX/sec/securityd/SecItemDb.c +++ b/OSX/sec/securityd/SecItemDb.c @@ -1899,6 +1899,10 @@ bool SecServerImportKeychainInPlist(SecDbConnectionRef dbt, SecurityClient *clie 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); diff --git a/OSX/sec/securityd/nameconstraints.c b/OSX/sec/securityd/nameconstraints.c index 11755234..e841a7ae 100644 --- a/OSX/sec/securityd/nameconstraints.c +++ b/OSX/sec/securityd/nameconstraints.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Apple Inc. All Rights Reserved. + * Copyright (c) 2015-2017 Apple Inc. All Rights Reserved. * * @APPLE_LICENSE_HEADER_START@ * @@ -283,6 +283,7 @@ typedef struct { 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) { @@ -329,7 +330,7 @@ static OSStatus nc_compare_subtree(void *context, SecCEGeneralNameType gnType, c 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; @@ -359,21 +360,72 @@ out: 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) { @@ -383,23 +435,70 @@ static void nc_compare_RFC822Name_to_subtrees(const void *value, void *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) { @@ -414,17 +513,8 @@ static OSStatus nc_compare_subjectAltName_to_subtrees(void *context, SecCEGenera 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; } @@ -435,7 +525,6 @@ static OSStatus nc_compare_subjectAltName_to_subtrees(void *context, SecCEGenera 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, @@ -445,25 +534,20 @@ OSStatus SecNameContraintsMatchSubtrees(SecCertificateRef certificate, CFArrayRe /* 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); @@ -500,7 +584,6 @@ OSStatus SecNameContraintsMatchSubtrees(SecCertificateRef certificate, CFArrayRe out: CFReleaseNull(subject); - CFReleaseNull(rfc822Names); return status; } diff --git a/Security.xcodeproj/project.pbxproj b/Security.xcodeproj/project.pbxproj index 1195965e..d86582c7 100644 --- a/Security.xcodeproj/project.pbxproj +++ b/Security.xcodeproj/project.pbxproj @@ -2215,6 +2215,10 @@ 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 */; }; @@ -3301,6 +3305,7 @@ 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 */; }; @@ -9825,6 +9830,8 @@ DC3A4B601D91EAC500E46D4A /* com.apple.CodeSigningHelper.sb */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = com.apple.CodeSigningHelper.sb; sourceTree = ""; }; DC3A4B621D91EAC500E46D4A /* main.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = main.cpp; sourceTree = ""; }; 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 = ""; }; + DC3D748B1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CKKSLocalSynchronizeOperation.m; sourceTree = ""; }; DC4269031E82EDAC002B7110 /* SecItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SecItem.m; sourceTree = ""; }; DC4269061E82FBDF002B7110 /* server_security_helpers.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = server_security_helpers.c; sourceTree = ""; }; DC4269071E82FBDF002B7110 /* server_security_helpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = server_security_helpers.h; sourceTree = ""; }; @@ -10567,6 +10574,8 @@ 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 = ""; }; 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 = ""; }; 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 = ""; }; + DCB5022C1FDA155D008F8E4F /* AutoreleaseTest.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = AutoreleaseTest.c; sourceTree = ""; }; + DCB502321FDA155E008F8E4F /* AutoreleaseTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AutoreleaseTest.h; sourceTree = ""; }; DCB5D9391E4A9A3400BE22AB /* CKKSSynchronizeOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CKKSSynchronizeOperation.h; sourceTree = ""; }; DCB5D93A1E4A9A3400BE22AB /* CKKSSynchronizeOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CKKSSynchronizeOperation.m; sourceTree = ""; }; DCBDB3B01E57C67500B61300 /* CKKSKeychainView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CKKSKeychainView.h; sourceTree = ""; }; @@ -15634,6 +15643,8 @@ DC3502B61E0208BE00BC0587 /* Tests (Local) */ = { isa = PBXGroup; children = ( + DCB5022C1FDA155D008F8E4F /* AutoreleaseTest.c */, + DCB502321FDA155E008F8E4F /* AutoreleaseTest.h */, 471024D91E79CB6D00844C09 /* CKKSTests.h */, DC3502B71E0208BE00BC0587 /* CKKSTests.m */, DC6593D21ED8DBCE00C19462 /* CKKSTests+API.h */, @@ -18365,6 +18376,8 @@ DCFE1C501F1825F7007640C8 /* CKKSUpdateDeviceStateOperation.m */, DCAD9B421F8D939C00C5E2AE /* CKKSFixups.h */, DCAD9B431F8D939C00C5E2AE /* CKKSFixups.m */, + DC3D748A1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.h */, + DC3D748B1FD2217900AC57DA /* CKKSLocalSynchronizeOperation.m */, ); name = Operations; sourceTree = ""; @@ -20465,6 +20478,7 @@ 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 */, @@ -20519,6 +20533,7 @@ 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 */, @@ -26552,6 +26567,7 @@ 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 */, @@ -26619,6 +26635,7 @@ 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 */, @@ -26684,6 +26701,7 @@ 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 */, diff --git a/keychain/ckks/CKKS.h b/keychain/ckks/CKKS.h index 95f1e88e..3b6a8545 100644 --- a/keychain/ckks/CKKS.h +++ b/keychain/ckks/CKKS.h @@ -24,14 +24,20 @@ #ifndef CKKS_h #define CKKS_h +#include #include -#include #include -#include +#include #include #ifdef __OBJC__ #import +NS_ASSUME_NONNULL_BEGIN +#else +CF_ASSUME_NONNULL_BEGIN +#endif + +#ifdef __OBJC__ typedef NS_ENUM(NSUInteger, SecCKKSItemEncryptionVersion) { CKKSItemEncryptionVersionNone = 0, // No encryption present @@ -55,7 +61,7 @@ extern CKKSItemState* const SecCKKSStateUnauthenticated; 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 @@ -94,8 +100,8 @@ extern NSString* const SecCKRecordServerWasCurrent; /* 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. @@ -108,13 +114,13 @@ extern NSString* const SecCKRecordEpoch; 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; @@ -157,6 +163,9 @@ extern CKKSZoneKeyState* const SecCKKSZoneKeyStateInitializing; 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. @@ -207,7 +216,7 @@ extern NSString* const CKKSServerExtensionErrorDomain; #define SecCKKSOutgoingQueueItemsAtOnce 100 #define SecCKKSIncomingQueueItemsAtOnce 10 -#endif // OBJ-C +#endif // OBJ-C /* C functions to interact with CKKS */ void SecCKKSInitialize(SecDbRef db); @@ -219,6 +228,9 @@ void SecCKKS24hrNotification(void); // 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); @@ -235,10 +247,6 @@ bool SecCKKSEnforceManifests(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); @@ -255,8 +263,7 @@ bool SecCKKSTestDisableKeyNotifications(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) { @@ -305,40 +312,68 @@ typedef CF_ENUM(CFIndex, CKKSServerExtensionErrorCode) { //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 */ diff --git a/keychain/ckks/CKKS.m b/keychain/ckks/CKKS.m index d5da4e83..d0d130b6 100644 --- a/keychain/ckks/CKKS.m +++ b/keychain/ckks/CKKS.m @@ -117,6 +117,7 @@ NSString* const SecCKRecordManifestLeafDERKey = @"der"; NSString* const SecCKRecordManifestLeafDigestKey = @"digest"; CKKSZoneKeyState* const SecCKKSZoneKeyStateReady = (CKKSZoneKeyState*) @"ready"; +CKKSZoneKeyState* const SecCKKSZoneKeyStateReadyPendingUnlock = (CKKSZoneKeyState*) @"readypendingunlock"; CKKSZoneKeyState* const SecCKKSZoneKeyStateError = (CKKSZoneKeyState*) @"error"; CKKSZoneKeyState* const SecCKKSZoneKeyStateCancelled = (CKKSZoneKeyState*) @"cancelled"; @@ -154,6 +155,7 @@ NSDictionary* CKKSZoneKeyStateMap(void) { SecCKKSZoneKeyStateHealTLKShares: @12U, SecCKKSZoneKeyStateHealTLKSharesFailed:@13U, SecCKKSZoneKeyStateWaitForFixupOperation:@14U, + SecCKKSZoneKeyStateReadyPendingUnlock: @15U, }; }); return map; @@ -279,8 +281,10 @@ bool SecCKKSSetEnforceManifests(bool value) { 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 @@ -292,17 +296,7 @@ bool SecCKKSShareTLKs(void) { }); 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; @@ -445,3 +439,16 @@ void CKKSRegisterSyncStatusCallback(CFStringRef cfuuid, SecBoolCFErrorCallback c [[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 +} diff --git a/keychain/ckks/CKKSAPSReceiver.h b/keychain/ckks/CKKSAPSReceiver.h index 9a876a91..e9788bc9 100644 --- a/keychain/ckks/CKKSAPSReceiver.h +++ b/keychain/ckks/CKKSAPSReceiver.h @@ -24,13 +24,15 @@ #import #if OCTAGON -#import #import -#import "keychain/ckks/CloudKitDependencies.h" +#import #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 @@ -39,13 +41,12 @@ // class dependencies (for injection) @property (readonly) Class apsConnectionClass; - -@property id apsConnection; +@property (nullable) id apsConnection; + (instancetype)receiverForEnvironment:(NSString*)environmentName namedDelegatePort:(NSString*)namedDelegatePort apsConnectionClass:(Class)apsConnectionClass; -- (CKKSCondition*)register:(id)receiver forZoneID:(CKRecordZoneID *)zoneID; +- (CKKSCondition*)registerReceiver:(id)receiver forZoneID:(CKRecordZoneID*)zoneID; // Test support: - (instancetype)initWithEnvironmentName:(NSString*)environmentName @@ -56,4 +57,5 @@ @end -#endif // OCTAGON +NS_ASSUME_NONNULL_END +#endif // OCTAGON diff --git a/keychain/ckks/CKKSAPSReceiver.m b/keychain/ckks/CKKSAPSReceiver.m index 31bd8625..c8bc2158 100644 --- a/keychain/ckks/CKKSAPSReceiver.m +++ b/keychain/ckks/CKKSAPSReceiver.m @@ -99,7 +99,7 @@ return self; } -- (CKKSCondition*)register:(id)receiver forZoneID:(CKRecordZoneID *)zoneID { +- (CKKSCondition*)registerReceiver:(id)receiver forZoneID:(CKRecordZoneID *)zoneID { CKKSCondition* finished = [[CKKSCondition alloc] init]; __weak __typeof(self) weakSelf = self; diff --git a/keychain/ckks/CKKSCKAccountStateTracker.h b/keychain/ckks/CKKSCKAccountStateTracker.h index 49d8de91..19370999 100644 --- a/keychain/ckks/CKKSCKAccountStateTracker.h +++ b/keychain/ckks/CKKSCKAccountStateTracker.h @@ -24,11 +24,13 @@ #import #if OCTAGON -#import #import -#import "keychain/ckks/CloudKitDependencies.h" -#import "keychain/ckks/CKKSCondition.h" +#import #include +#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. @@ -42,48 +44,51 @@ 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) nsnotificationCenterClass; +- (instancetype)init:(CKContainer*)container nsnotificationCenterClass:(Class)nsnotificationCenterClass; --(CKKSAccountStatus)currentCKAccountStatusAndNotifyOnChange: (id) listener; +- (dispatch_semaphore_t)notifyOnAccountStatusChange:(id)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 diff --git a/keychain/ckks/CKKSCKAccountStateTracker.m b/keychain/ckks/CKKSCKAccountStateTracker.m index e29784cc..9c340e19 100644 --- a/keychain/ckks/CKKSCKAccountStateTracker.m +++ b/keychain/ckks/CKKSCKAccountStateTracker.m @@ -67,7 +67,7 @@ _firstCKAccountFetch = false; _firstSOSCircleFetch = false; - _finishedInitialCalls = [[CKKSCondition alloc] init]; + _finishedInitialDispatches = [[CKKSCondition alloc] init]; _ckdeviceIDInitialized = [[CKKSCondition alloc] init]; id notificationCenter = [self.nsnotificationCenterClass defaultCenter]; @@ -92,7 +92,7 @@ } [strongSelf notifyCKAccountStatusChange:nil]; [strongSelf notifyCircleChange:nil]; - [strongSelf.finishedInitialCalls fulfill]; + [strongSelf.finishedInitialDispatches fulfill]; }); } return self; @@ -119,13 +119,11 @@ return [self descriptionInternal: [super description]]; } --(CKKSAccountStatus)currentCKAccountStatusAndNotifyOnChange: (id) listener { - - __block CKKSAccountStatus status = CKKSAccountStatusUnknown; - - dispatch_sync(self.queue, ^{ - status = self.currentComputedAccountStatus; +-(dispatch_semaphore_t)notifyOnAccountStatusChange:(id)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 value; @@ -140,9 +138,30 @@ 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 { @@ -262,7 +281,7 @@ } } --(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) { @@ -328,6 +347,12 @@ 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."); @@ -340,13 +365,7 @@ // Queue up the changes for each listener. while ((dq = [enumerator nextObject])) { id 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, ^{ @@ -354,6 +373,18 @@ }); } +-(void)_onqueueDeliverCurrentState:(id)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); } @@ -362,6 +393,35 @@ 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 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; diff --git a/keychain/ckks/CKKSCondition.h b/keychain/ckks/CKKSCondition.h index ff6cf694..ebb780ac 100644 --- a/keychain/ckks/CKKSCondition.h +++ b/keychain/ckks/CKKSCondition.h @@ -24,6 +24,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + /* * CKKSCondition is for implementing one-shot condition variables, * based on libdispatch semaphores (which might use condition variables underneath). @@ -31,14 +33,19 @@ @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 diff --git a/keychain/ckks/CKKSCondition.m b/keychain/ckks/CKKSCondition.m index 998c3485..ebef4ef2 100644 --- a/keychain/ckks/CKKSCondition.m +++ b/keychain/ckks/CKKSCondition.m @@ -25,20 +25,28 @@ @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 { diff --git a/keychain/ckks/CKKSControl.h b/keychain/ckks/CKKSControl.h index 29172c6d..32aedf63 100644 --- a/keychain/ckks/CKKSControl.h +++ b/keychain/ckks/CKKSControl.h @@ -26,18 +26,21 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface CKKSControl : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithConnection:(NSXPCConnection*)connection; -- (void)rpcStatus: (NSString*)viewName reply:(void(^)(NSArray* 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* _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 *,NSError*))reply; - (void)rpcGetAnalyticsSysdiagnoseWithReply:(void (^)(NSString* sysdiagnose, NSError* error))reply; @@ -45,10 +48,11 @@ - (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__ diff --git a/keychain/ckks/CKKSControlProtocol.h b/keychain/ckks/CKKSControlProtocol.h index ce241101..2492a3c6 100644 --- a/keychain/ckks/CKKSControlProtocol.h +++ b/keychain/ckks/CKKSControlProtocol.h @@ -28,6 +28,7 @@ - (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* result, NSError* error)) reply; - (void)rpcFetchAndProcessChanges:(NSString*)viewName reply: (void(^)(NSError* result)) reply; - (void)rpcFetchAndProcessClassAChanges:(NSString*)viewName reply: (void(^)(NSError* result)) reply; diff --git a/keychain/ckks/CKKSCurrentItemPointer.h b/keychain/ckks/CKKSCurrentItemPointer.h index 1008cf0e..afc5b61a 100644 --- a/keychain/ckks/CKKSCurrentItemPointer.h +++ b/keychain/ckks/CKKSCurrentItemPointer.h @@ -39,14 +39,20 @@ 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*)remoteItemPointers: (CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *)error; -+ (bool) deleteAll:(CKRecordZoneID*)zoneID error:(NSError * __autoreleasing *) error; -+ (NSArray*)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*)remoteItemPointers:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; ++ (bool)deleteAll:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; ++ (NSArray*)allInZone:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; @end diff --git a/keychain/ckks/CKKSCurrentKeyPointer.h b/keychain/ckks/CKKSCurrentKeyPointer.h index b55efcfb..99bfb5e8 100644 --- a/keychain/ckks/CKKSCurrentKeyPointer.h +++ b/keychain/ckks/CKKSCurrentKeyPointer.h @@ -39,15 +39,18 @@ - (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*)all:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; -+ (bool) deleteAll:(CKRecordZoneID*) zoneID error: (NSError * __autoreleasing *) error; ++ (NSArray*)all:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; ++ (bool)deleteAll:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; @end @@ -62,8 +65,8 @@ @property NSArray* tlkShares; --(instancetype)init; --(instancetype)initForZone:(CKRecordZoneID*)zoneID; +- (instancetype)init; +- (instancetype)initForZone:(CKRecordZoneID*)zoneID; @end #endif diff --git a/keychain/ckks/CKKSDeviceStateEntry.h b/keychain/ckks/CKKSDeviceStateEntry.h index 8634310f..cd3ac7cc 100644 --- a/keychain/ckks/CKKSDeviceStateEntry.h +++ b/keychain/ckks/CKKSDeviceStateEntry.h @@ -26,13 +26,13 @@ #if OCTAGON -#include #include +#include #import #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 @@ -54,10 +54,10 @@ @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*)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*)allInZone:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initForDevice:(NSString*)device @@ -71,6 +71,5 @@ encodedCKRecord:(NSData*)encodedrecord; @end -#endif // OCTAGON -#endif /* CKKSDeviceStateEntry_h */ - +#endif // OCTAGON +#endif /* CKKSDeviceStateEntry_h */ diff --git a/keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h b/keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h index 1e8a356f..f9459459 100644 --- a/keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h +++ b/keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.h @@ -24,28 +24,30 @@ #import #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* modifications; @property NSMutableDictionary* 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 diff --git a/keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.m b/keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.m index bf98cb51..528ba34e 100644 --- a/keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.m +++ b/keychain/ckks/CKKSFetchAllRecordZoneChangesOperation.m @@ -33,12 +33,14 @@ #import "keychain/ckks/CKKSMirrorEntry.h" #import "keychain/ckks/CKKSManifest.h" #import "keychain/ckks/CKKSManifestLeafRecord.h" +#import "CKKSPowerCollection.h" #include @interface CKKSFetchAllRecordZoneChangesOperation() @property CKDatabaseOperation* fetchRecordZoneChangesOperation; @property CKOperationGroup* ckoperationGroup; +@property (assign) NSUInteger fetchedItems; @end @implementation CKKSFetchAllRecordZoneChangesOperation @@ -91,7 +93,7 @@ } } - if (![ckks _onQueueUpdateLatestManifestWithError:&error]) { + if (![ckks _onqueueUpdateLatestManifestWithError:&error]) { self.error = error; ckkserror("ckksfetch", ckks, "failed to get latest manifest"); } @@ -117,18 +119,22 @@ NSError* error = nil; if(self.resync) { ckksnotice("ckksresync", ckks, "Comparing local UUIDs against the CloudKit list"); - NSMutableArray* uuids = [[CKKSMirrorEntry allUUIDs: &error] mutableCopy]; + NSMutableArray* 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]; @@ -140,7 +146,6 @@ - (void)groupStart { __weak __typeof(self) weakSelf = self; - CKKSKeychainView* ckks = self.ckks; if(!ckks) { ckkserror("ckksresync", ckks, "no CKKS object"); @@ -200,6 +205,7 @@ // 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) { @@ -215,6 +221,7 @@ // 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 @@ -343,6 +350,9 @@ strongSelf.error = operationError; } + //[CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventFetchAllChanges zone:ckks.zoneName count:strongSelf.fetchedItems]; + + // Trigger the fake 'we're done' operation. [strongSelf runBeforeGroupFinished: recordZoneChangesCompletedOperation]; }; diff --git a/keychain/ckks/CKKSFixups.h b/keychain/ckks/CKKSFixups.h index 2346dbac..3db29c4a 100644 --- a/keychain/ckks/CKKSFixups.h +++ b/keychain/ckks/CKKSFixups.h @@ -24,9 +24,9 @@ #if OCTAGON #import -#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. @@ -36,8 +36,9 @@ typedef NS_ENUM(NSUInteger, CKKSFixup) { CKKSFixupNever, CKKSFixupRefetchCurrentItemPointers, CKKSFixupFetchTLKShares, + CKKSFixupLocalReload, }; -#define CKKSCurrentFixupNumber (SecCKKSShareTLKs() ? CKKSFixupFetchTLKShares : CKKSFixupRefetchCurrentItemPointers) +#define CKKSCurrentFixupNumber (CKKSFixupLocalReload) @interface CKKSFixups : NSObject +(CKKSGroupOperation*)fixup:(CKKSFixup)lastfixup for:(CKKSKeychainView*)keychainView; @@ -46,12 +47,18 @@ typedef NS_ENUM(NSUInteger, CKKSFixup) { // 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 diff --git a/keychain/ckks/CKKSFixups.m b/keychain/ckks/CKKSFixups.m index ee99c72e..97de7c4b 100644 --- a/keychain/ckks/CKKSFixups.m +++ b/keychain/ckks/CKKSFixups.m @@ -39,17 +39,17 @@ 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]; @@ -57,6 +57,15 @@ 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 @@ -268,5 +277,68 @@ } @end +#pragma mark - CKKSFixupLocalReloadOperation + +@interface CKKSFixupLocalReloadOperation () +@property CKOperationGroup* group; +@end + +// In 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:@"", 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 diff --git a/keychain/ckks/CKKSGroupOperation.h b/keychain/ckks/CKKSGroupOperation.h index f0caf654..d157762c 100644 --- a/keychain/ckks/CKKSGroupOperation.h +++ b/keychain/ckks/CKKSGroupOperation.h @@ -21,15 +21,14 @@ * @APPLE_LICENSE_HEADER_END@ */ - -#ifndef CKKSGroupOperation_h -#define CKKSGroupOperation_h +#if OCTAGON #import #include #import "keychain/ckks/CKKSResultOperation.h" -@interface CKKSGroupOperation : CKKSResultOperation { +@interface CKKSGroupOperation : CKKSResultOperation +{ BOOL executing; BOOL finished; } @@ -41,8 +40,8 @@ // 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 diff --git a/keychain/ckks/CKKSGroupOperation.m b/keychain/ckks/CKKSGroupOperation.m index 727a12f2..1ea9ea85 100644 --- a/keychain/ckks/CKKSGroupOperation.m +++ b/keychain/ckks/CKKSGroupOperation.m @@ -21,6 +21,8 @@ * @APPLE_LICENSE_HEADER_END@ */ +#if OCTAGON + #import "CKKSGroupOperation.h" #include @@ -250,11 +252,6 @@ } - (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]; @@ -293,3 +290,6 @@ } @end + +#endif // OCTAGON + diff --git a/keychain/ckks/CKKSHealKeyHierarchyOperation.h b/keychain/ckks/CKKSHealKeyHierarchyOperation.h index 4f74072d..496f833b 100644 --- a/keychain/ckks/CKKSHealKeyHierarchyOperation.h +++ b/keychain/ckks/CKKSHealKeyHierarchyOperation.h @@ -36,5 +36,4 @@ @end -#endif // OCTAGON - +#endif // OCTAGON diff --git a/keychain/ckks/CKKSHealKeyHierarchyOperation.m b/keychain/ckks/CKKSHealKeyHierarchyOperation.m index 52df80c4..130da74e 100644 --- a/keychain/ckks/CKKSHealKeyHierarchyOperation.m +++ b/keychain/ckks/CKKSHealKeyHierarchyOperation.m @@ -225,6 +225,7 @@ [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; } @@ -237,6 +238,7 @@ [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; } @@ -384,17 +386,22 @@ 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; } @@ -416,18 +423,18 @@ [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; diff --git a/keychain/ckks/CKKSHealTLKSharesOperation.h b/keychain/ckks/CKKSHealTLKSharesOperation.h index 5d1e2955..4786d6ea 100644 --- a/keychain/ckks/CKKSHealTLKSharesOperation.h +++ b/keychain/ckks/CKKSHealTLKSharesOperation.h @@ -32,8 +32,7 @@ @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 diff --git a/keychain/ckks/CKKSHealTLKSharesOperation.m b/keychain/ckks/CKKSHealTLKSharesOperation.m index 97d0cb2d..be883bd4 100644 --- a/keychain/ckks/CKKSHealTLKSharesOperation.m +++ b/keychain/ckks/CKKSHealTLKSharesOperation.m @@ -30,6 +30,8 @@ #import "keychain/ckks/CKKSGroupOperation.h" #import "keychain/ckks/CKKSTLKShare.h" +#import "CKKSPowerCollection.h" + @interface CKKSHealTLKSharesOperation () @property NSBlockOperation* cloudkitModifyOperationFinished; @property CKOperationGroup* ckoperationGroup; @@ -87,12 +89,14 @@ 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); diff --git a/keychain/ckks/CKKSIncomingQueueEntry.h b/keychain/ckks/CKKSIncomingQueueEntry.h index e4787234..1b3971f8 100644 --- a/keychain/ckks/CKKSIncomingQueueEntry.h +++ b/keychain/ckks/CKKSIncomingQueueEntry.h @@ -23,42 +23,37 @@ #if OCTAGON -#import "CKKSSQLDatabaseObject.h" +#import +#include +#include #import "CKKSItem.h" #import "CKKSMirrorEntry.h" -#include -#include - -#ifndef CKKSIncomingQueueEntry_h -#define CKKSIncomingQueueEntry_h - +#import "CKKSSQLDatabaseObject.h" -#import +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*)fetch:(ssize_t)n - startingAtUUID:(NSString*)uuid - state:(NSString*)state - zoneID:(CKRecordZoneID*)zoneID - error: (NSError * __autoreleasing *) error; ++ (NSArray* _Nullable)fetch:(ssize_t)n + startingAtUUID:(NSString*)uuid + state:(NSString*)state + zoneID:(CKRecordZoneID*)zoneID + error:(NSError* __autoreleasing*)error; -+ (NSDictionary*)countsByState:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; ++ (NSDictionary*)countsByState:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; @end +NS_ASSUME_NONNULL_END #endif -#endif /* CKKSIncomingQueueEntry_h */ diff --git a/keychain/ckks/CKKSIncomingQueueOperation.h b/keychain/ckks/CKKSIncomingQueueOperation.h index dd2a8a12..ff8a6f09 100644 --- a/keychain/ckks/CKKSIncomingQueueOperation.h +++ b/keychain/ckks/CKKSIncomingQueueOperation.h @@ -34,10 +34,12 @@ // 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 diff --git a/keychain/ckks/CKKSIncomingQueueOperation.m b/keychain/ckks/CKKSIncomingQueueOperation.m index dd192ba1..e2e7ecde 100644 --- a/keychain/ckks/CKKSIncomingQueueOperation.m +++ b/keychain/ckks/CKKSIncomingQueueOperation.m @@ -29,6 +29,7 @@ #import "CKKSKey.h" #import "CKKSManifest.h" #import "CKKSAnalyticsLogger.h" +#import "CKKSPowerCollection.h" #import "keychain/ckks/CKKSCurrentItemPointer.h" #include @@ -105,14 +106,10 @@ 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; @@ -141,8 +138,6 @@ 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) { @@ -181,6 +176,7 @@ ckkserror("ckksincoming", ckks, "Couldn't decrypt IQE %@ for some reason: %@", iqe, error); self.error = error; } + self.errorItemsProcessed += 1; continue; } @@ -207,6 +203,7 @@ 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; } @@ -220,19 +217,14 @@ 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]]; @@ -243,18 +235,12 @@ 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 @@ -266,6 +252,8 @@ 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; } } @@ -382,7 +370,6 @@ // Iterate through all incoming queue entries a chunk at a time (for peak memory concerns) NSArray * queueEntries = nil; NSString* lastMaxUUID = nil; - NSUInteger processedItems = 0; while(queueEntries == nil || queueEntries.count == SecCKKSIncomingQueueItemsAtOnce) { if(self.cancelled) { ckksnotice("ckksincoming", ckks, "CKKSIncomingQueueOperation cancelled, quitting"); @@ -407,11 +394,12 @@ 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) { @@ -420,7 +408,7 @@ } // 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* newCIPs = [CKKSCurrentItemPointer remoteItemPointers:ckks.zoneID error:&error]; if(error || !newCIPs) { @@ -449,12 +437,18 @@ 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]; } }; @@ -574,6 +568,9 @@ if(error) { ckkserror("ckksincoming", ckks, "couldn't delete CKKSIncomingQueueEntry: %@", error); self.error = error; + self.errorItemsProcessed += 1; + } else { + self.successfulItemsProcessed += 1; } if(moddate) { @@ -592,6 +589,8 @@ ckkserror("ckksincoming", ckks, "Couldn't save errored IQE to database: %@", error); self.error = error; } + + self.errorItemsProcessed += 1; } } @@ -657,10 +656,14 @@ 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; } } diff --git a/keychain/ckks/CKKSItem.h b/keychain/ckks/CKKSItem.h index a0aeb067..6cd0088d 100644 --- a/keychain/ckks/CKKSItem.h +++ b/keychain/ckks/CKKSItem.h @@ -23,97 +23,92 @@ #if OCTAGON +#import +#include +#include #import "keychain/ckks/CKKS.h" -#import "keychain/ckks/CKKSSQLDatabaseObject.h" #import "keychain/ckks/CKKSRecordHolder.h" -#include -#include - -#ifndef CKKSItem_h -#define CKKSItem_h +#import "keychain/ckks/CKKSSQLDatabaseObject.h" -#import +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*)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*)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*) allUUIDs: (NSError * __autoreleasing *) error; +// Convenience function: get all UUIDs of this type on this particular zone ++ (NSArray*)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 */ diff --git a/keychain/ckks/CKKSItem.m b/keychain/ckks/CKKSItem.m index c883c189..2b9dde4e 100644 --- a/keychain/ckks/CKKSItem.m +++ b/keychain/ckks/CKKSItem.m @@ -33,6 +33,7 @@ #include #include +#include #import #import @@ -199,11 +200,35 @@ plaintextPCSServiceIdentifier: (NSNumber*) pcsServiceIdentifier 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 { @@ -478,11 +503,11 @@ plaintextPCSServiceIdentifier: (NSNumber*) pcsServiceIdentifier @implementation CKKSSQLDatabaseObject (CKKSZoneExtras) -+ (NSArray*) allUUIDs: (NSError * __autoreleasing *) error { ++ (NSArray*)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 diff --git a/keychain/ckks/CKKSItemEncrypter.h b/keychain/ckks/CKKSItemEncrypter.h index dc103ef5..aa477698 100644 --- a/keychain/ckks/CKKSItemEncrypter.h +++ b/keychain/ckks/CKKSItemEncrypter.h @@ -21,13 +21,10 @@ * @APPLE_LICENSE_HEADER_END@ */ -#ifndef CKKSItemEncrypter_h -#define CKKSItemEncrypter_h +#if OCTAGON #include -#if OCTAGON - @class CKKSItem; @class CKKSMirrorEntry; @class CKKSKey; @@ -35,28 +32,32 @@ @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*) ad error: (NSError * __autoreleasing *) error; -+ (NSDictionary*) decryptDictionary: (NSData*) encitem key: (CKKSAESSIVKey*) key authenticatedData: (NSDictionary*) ad error: (NSError * __autoreleasing *) error; ++ (NSData* _Nullable)encryptDictionary:(NSDictionary*)dict + key:(CKKSAESSIVKey*)key + authenticatedData:(NSDictionary* _Nullable)ad + error:(NSError* _Nullable __autoreleasing* _Nullable)error; ++ (NSDictionary* _Nullable)decryptDictionary:(NSData*)encitem + key:(CKKSAESSIVKey*)key + authenticatedData:(NSDictionary* _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 diff --git a/keychain/ckks/CKKSKey.h b/keychain/ckks/CKKSKey.h index 5fe452bf..ae5842fd 100644 --- a/keychain/ckks/CKKSKey.h +++ b/keychain/ckks/CKKSKey.h @@ -28,8 +28,8 @@ #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 @@ -40,110 +40,120 @@ @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*) selfWrappedKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; ++ (NSArray*)selfWrappedKeys:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; -+ (instancetype)currentKeyForClass: (CKKSKeyClass*) keyclass zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; -+ (NSArray*)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*)currentKeysForClass:(CKKSKeyClass*)keyclass + state:(CKKSProcessedState*)state + zoneID:(CKRecordZoneID*)zoneID + error:(NSError* __autoreleasing*)error; -+ (NSArray*)allKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; -+ (NSArray*)remoteKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; -+ (NSArray*)localKeys: (CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; ++ (NSArray*)allKeys:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; ++ (NSArray*)remoteKeys:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; ++ (NSArray*)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*) ad error: (NSError * __autoreleasing *) error; -- (NSData*)decryptData: (NSData*) ciphertext authenticatedData: (NSDictionary*) ad error: (NSError * __autoreleasing *) error; +- (NSData*)encryptData:(NSData*)plaintext + authenticatedData:(NSDictionary*)ad + error:(NSError* __autoreleasing*)error; +- (NSData*)decryptData:(NSData*)ciphertext + authenticatedData:(NSDictionary*)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*)countsByClass:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; ++ (NSDictionary*)countsByClass:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; @end #endif diff --git a/keychain/ckks/CKKSKey.m b/keychain/ckks/CKKSKey.m index 231f2c7c..fc10d37a 100644 --- a/keychain/ckks/CKKSKey.m +++ b/keychain/ckks/CKKSKey.m @@ -326,118 +326,151 @@ } - (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, @@ -445,6 +478,7 @@ (id)kSecAttrDescription: key.keyclass, (id)kSecAttrAccount: key.uuid, (id)kSecAttrServer: key.zoneID.zoneName, + (id)kSecAttrPath: key.parentKeyUUID, (id)kSecReturnAttributes: @YES, (id)kSecReturnData: @YES, } mutableCopy]; @@ -454,37 +488,71 @@ 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 = [@{ @@ -499,56 +567,52 @@ (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; } @@ -770,6 +834,44 @@ 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 { diff --git a/keychain/ckks/CKKSKeychainView.h b/keychain/ckks/CKKSKeychainView.h index f10395e0..b7fe760f 100644 --- a/keychain/ckks/CKKSKeychainView.h +++ b/keychain/ckks/CKKSKeychainView.h @@ -21,47 +21,39 @@ * @APPLE_LICENSE_HEADER_END@ */ - -#ifndef CKKSKeychainView_h -#define CKKSKeychainView_h - +#if OCTAGON #import #include -#if OCTAGON -#import "keychain/ckks/CloudKitDependencies.h" #import "keychain/ckks/CKKSAPSReceiver.h" #import "keychain/ckks/CKKSLockStateTracker.h" -#endif +#import "keychain/ckks/CloudKitDependencies.h" -#include #include +#include #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; @@ -72,27 +64,32 @@ @class CKKSOutgoingQueueEntry; @class CKKSZoneChangeFetcher; -@interface CKKSKeychainView : CKKSZone { +@interface CKKSKeychainView : CKKSZone +{ 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* keyHierarchyConditions; @@ -102,22 +99,24 @@ @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; @@ -129,60 +128,59 @@ @property (nonatomic, readonly) NSSet>* currentTrustedPeers; @property (nonatomic, readonly) NSError* currentTrustedPeersError; -- (instancetype)initWithContainer: (CKContainer*) container - zoneName: (NSString*) zoneName - accountTracker:(CKKSCKAccountStateTracker*) accountTracker - lockStateTracker:(CKKSLockStateTracker*) lockStateTracker - savedTLKNotifier:(CKKSNearFutureScheduler*) savedTLKNotifier - peerProvider:(id)peerProvider - fetchRecordZoneChangesOperationClass: (Class) fetchRecordZoneChangesOperationClass - fetchRecordsOperationClass: (Class)fetchRecordsOperationClass - queryOperationClass:(Class)queryOperationClass - modifySubscriptionsOperationClass: (Class) modifySubscriptionsOperationClass - modifyRecordZonesOperationClass: (Class) modifyRecordZonesOperationClass - apsConnectionClass: (Class) apsConnectionClass - notifierClass: (Class) notifierClass; +- (instancetype)initWithContainer:(CKContainer*)container + zoneName:(NSString*)zoneName + accountTracker:(CKKSCKAccountStateTracker*)accountTracker + lockStateTracker:(CKKSLockStateTracker*)lockStateTracker + savedTLKNotifier:(CKKSNearFutureScheduler*)savedTLKNotifier + peerProvider:(id)peerProvider + fetchRecordZoneChangesOperationClass:(Class)fetchRecordZoneChangesOperationClass + fetchRecordsOperationClass:(Class)fetchRecordsOperationClass + queryOperationClass:(Class)queryOperationClass + modifySubscriptionsOperationClass:(Class)modifySubscriptionsOperationClass + modifyRecordZonesOperationClass:(Class)modifyRecordZonesOperationClass + apsConnectionClass:(Class)apsConnectionClass + notifierClass:(Class)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; @@ -191,9 +189,10 @@ // 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; @@ -209,8 +208,8 @@ // 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 */ @@ -225,35 +224,39 @@ - (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*)savedRecords; +- (bool)_onqueueCKWriteFailed:(NSError*)ckerror attemptedRecordsChanged:(NSDictionary*)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>*)_onqueueFindPeersMissingShare:(CKKSKey*)key error:(NSError* __autoreleasing*)error; +- (NSSet>* _Nullable)_onqueueFindPeersMissingShare:(CKKSKey*)key error:(NSError* __autoreleasing*)error; // For this key, share it to all trusted peers who don't have it yet -- (NSSet*)_onqueueCreateMissingKeyShares:(CKKSKey*)key error:(NSError* __autoreleasing*)error; +- (NSSet* _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* incomingQueueOperations; @@ -261,16 +264,15 @@ @property CKKSScanLocalItemsOperation* initialScanOperation; // Returns the current state of this view --(NSDictionary*)status; +- (NSDictionary*)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 +@interface CKKSKeychainView : NSObject +{ + NSString* _containerName; +} +@end +#endif // OCTAGON diff --git a/keychain/ckks/CKKSKeychainView.m b/keychain/ckks/CKKSKeychainView.m index 1906df76..2b9260ff 100644 --- a/keychain/ckks/CKKSKeychainView.m +++ b/keychain/ckks/CKKSKeychainView.m @@ -53,7 +53,7 @@ #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" @@ -64,6 +64,7 @@ #import "keychain/ckks/CloudKitCategories.h" #import "keychain/ckks/CKKSTLKShare.h" #import "keychain/ckks/CKKSHealTLKSharesOperation.h" +#import "keychain/ckks/CKKSLocalSynchronizeOperation.h" #include #include @@ -89,12 +90,18 @@ @property CKKSNearFutureScheduler* initializeScheduler; +// Slows down all outgoing queue operations +@property CKKSNearFutureScheduler* outgoingQueueOperationScheduler; + @property CKKSResultOperation* processIncomingQueueAfterNextUnlockOperation; @property NSMutableDictionary* pendingSyncCallbacks; @property id 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; @@ -132,6 +139,9 @@ 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]; @@ -173,9 +183,12 @@ _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 @@ -186,16 +199,23 @@ [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 { @@ -223,25 +243,15 @@ 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; }]; } @@ -255,7 +265,7 @@ _keyHierarchyError = nil; } - - (void)_onqueueInitializeZone { + - (void)_onqueueHandleCKLogin { if(!SecCKKSIsEnabled()) { ckksnotice("ckks", self, "Skipping CloudKit initialization due to disabled CKKS"); return; @@ -265,7 +275,10 @@ __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"); @@ -306,6 +319,7 @@ // 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. @@ -328,6 +342,10 @@ // 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. @@ -356,6 +374,14 @@ } 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) { @@ -384,16 +410,19 @@ 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]; } @@ -408,20 +437,9 @@ 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]; } @@ -538,8 +556,6 @@ return true; }]; - [strongSelf resetSetup]; - if(error) { ckksnotice("ckksreset", strongSelf, "Local reset finished with error %@", error); strongOp.error = error; @@ -547,7 +563,7 @@ 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:^{}]; @@ -569,11 +585,6 @@ } - (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]; @@ -604,7 +615,7 @@ 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:^{}]; @@ -682,9 +693,15 @@ } 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; @@ -722,7 +739,12 @@ 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; } @@ -795,10 +817,11 @@ } 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. @@ -826,28 +849,32 @@ 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 @@ -857,28 +884,12 @@ 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 @@ -896,12 +907,9 @@ 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 { @@ -911,20 +919,21 @@ [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. @@ -934,25 +943,22 @@ // 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) { @@ -961,72 +967,100 @@ [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) { @@ -1035,6 +1069,8 @@ } } } + ckksnotice("ckkskey", self, "Advancing to key state: %@", state); + self.keyHierarchyState = state; } // For this key, who doesn't yet have a CKKSTLKShare for it? @@ -1043,7 +1079,8 @@ - (NSSet>*)_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]; } @@ -1052,14 +1089,14 @@ 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>* peersMissingShares = [NSMutableSet set]; @@ -1127,10 +1164,6 @@ - (NSSet*)_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) { @@ -1196,6 +1229,7 @@ } NSError* localerror = nil; + bool probablyOkIfUnlocked = false; // keychain being locked is not a fatal error here [set.tlk loadKeyMaterialFromKeychain:&localerror]; @@ -1207,6 +1241,7 @@ return SecCKKSZoneKeyStateUnhealthy; } else if(localerror) { ckkserror("ckkskey", self, "Soft error loading TLK(%@), maybe locked: %@", set.tlk, localerror); + probablyOkIfUnlocked = true; } localerror = nil; @@ -1220,6 +1255,7 @@ return SecCKKSZoneKeyStateUnhealthy; } else if(localerror) { ckkserror("ckkskey", self, "Soft error loading classA key(%@), maybe locked: %@", set.classA, localerror); + probablyOkIfUnlocked = true; } localerror = nil; @@ -1262,30 +1298,33 @@ 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>* 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>* 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 { @@ -1360,16 +1399,9 @@ __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; } @@ -1407,7 +1439,7 @@ 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; } @@ -1426,11 +1458,6 @@ 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]; @@ -1447,6 +1474,7 @@ 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 @@ -1457,24 +1485,13 @@ } // 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]; @@ -1703,7 +1720,7 @@ return false; } - ckksnotice("ckkscurrent", strongSelf, "Retrieved current item pointer: %@", cip); + ckksinfo("ckkscurrent", strongSelf, "Retrieved current item pointer: %@", cip); complete(cip.currentItemUUID, NULL); return true; }]; @@ -1719,7 +1736,7 @@ 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 { @@ -1793,11 +1810,6 @@ } - (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]]; @@ -1814,6 +1826,9 @@ // 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; } } @@ -1821,6 +1836,8 @@ 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]; @@ -1853,11 +1870,6 @@ } - (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"); @@ -1884,10 +1896,6 @@ - (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. @@ -1910,6 +1918,7 @@ } 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"; @@ -1922,8 +1931,8 @@ [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; } @@ -2019,16 +2028,17 @@ } - (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]; } @@ -2252,8 +2262,7 @@ } 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); } } @@ -2267,7 +2276,7 @@ // 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; @@ -2344,10 +2353,26 @@ } } - // 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; @@ -2493,6 +2518,43 @@ } } +- (bool)_onqueueResetAllInflightOQE:(NSError**)error { + NSError* localError = nil; + + while(true) { + NSArray * 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); @@ -2511,6 +2573,32 @@ 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]; @@ -2535,10 +2623,10 @@ } 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) { @@ -2547,7 +2635,7 @@ return localerror == nil; } -- (bool)_onQueueUpdateLatestManifestWithError:(NSError**)error +- (bool)_onqueueUpdateLatestManifestWithError:(NSError**)error { dispatch_assert_queue(self.queue); CKKSManifest* manifest = [CKKSManifest latestTrustedManifestForZone:self.zoneName error:error]; @@ -2565,84 +2653,79 @@ // 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* 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* 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]) { @@ -2662,44 +2745,35 @@ }); } -// 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); } } @@ -2738,41 +2812,75 @@ #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 { @@ -2806,7 +2914,7 @@ 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]; } }]; @@ -2834,15 +2942,12 @@ 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); } }]; @@ -2888,11 +2993,6 @@ } - (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; @@ -2921,9 +3021,6 @@ } [self.incomingQueueOperations removeAllObjects]; - // Don't send any more notifications, either - _notifierClass = nil; - [super cancelAllOperations]; [self dispatchSync:^bool{ @@ -2932,6 +3029,13 @@ }]; } +- (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") @@ -2975,7 +3079,7 @@ @"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), diff --git a/keychain/ckks/CKKSLocalSynchronizeOperation.h b/keychain/ckks/CKKSLocalSynchronizeOperation.h new file mode 100644 index 00000000..65e3d559 --- /dev/null +++ b/keychain/ckks/CKKSLocalSynchronizeOperation.h @@ -0,0 +1,47 @@ +/* + * 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 +#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 + diff --git a/keychain/ckks/CKKSLocalSynchronizeOperation.m b/keychain/ckks/CKKSLocalSynchronizeOperation.m new file mode 100644 index 00000000..5d14ba1c --- /dev/null +++ b/keychain/ckks/CKKSLocalSynchronizeOperation.m @@ -0,0 +1,197 @@ +/* + * 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* 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 + diff --git a/keychain/ckks/CKKSLockStateTracker.h b/keychain/ckks/CKKSLockStateTracker.h index f2d62dde..0780a2a7 100644 --- a/keychain/ckks/CKKSLockStateTracker.h +++ b/keychain/ckks/CKKSLockStateTracker.h @@ -28,16 +28,16 @@ @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 diff --git a/keychain/ckks/CKKSManifest.h b/keychain/ckks/CKKSManifest.h index 249f722f..139614b6 100644 --- a/keychain/ckks/CKKSManifest.h +++ b/keychain/ckks/CKKSManifest.h @@ -23,9 +23,9 @@ #if OCTAGON -#import "CKKSRecordHolder.h" #import #import +#import "CKKSRecordHolder.h" NS_ASSUME_NONNULL_BEGIN @@ -76,9 +76,14 @@ extern NSString* const CKKSManifestGenCountKey; @interface CKKSEgoManifest : CKKSManifest + (nullable CKKSEgoManifest*)tryCurrentEgoManifestForZone:(NSString*)zone; -+ (nullable instancetype)newManifestForZone:(NSString*)zone withItems:(NSArray*)items peerManifestIDs:(NSArray*)peerManifestIDs currentItems:(NSDictionary*)currentItems error:(NSError**)error; - -- (void)updateWithNewOrChangedRecords:(NSArray*)newOrChangedRecords deletedRecordIDs:(NSArray*)deletedRecordIDs; ++ (nullable instancetype)newManifestForZone:(NSString*)zone + withItems:(NSArray*)items + peerManifestIDs:(NSArray*)peerManifestIDs + currentItems:(NSDictionary*)currentItems + error:(NSError**)error; + +- (void)updateWithNewOrChangedRecords:(NSArray*)newOrChangedRecords + deletedRecordIDs:(NSArray*)deletedRecordIDs; - (void)setCurrentItemUUID:(NSString*)newCurrentItemUUID forIdentifier:(NSString*)currentPointerIdentifier; - (NSArray*)allCKRecordsWithZoneID:(CKRecordZoneID*)zoneID; @@ -98,13 +103,18 @@ extern NSString* const CKKSManifestGenCountKey; @interface CKKSEgoManifest (UnitTesting) -+ (nullable instancetype)newFakeManifestForZone:(NSString*)zone withItemRecords:(NSArray*)itemRecords currentItems:(NSDictionary*)currentItems signerID:(NSString*)signerID keyPair:(SFECKeyPair*)keyPair error:(NSError**)error; ++ (nullable instancetype)newFakeManifestForZone:(NSString*)zone + withItemRecords:(NSArray*)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; diff --git a/keychain/ckks/CKKSManifest.m b/keychain/ckks/CKKSManifest.m index ada18403..66587a72 100644 --- a/keychain/ckks/CKKSManifest.m +++ b/keychain/ckks/CKKSManifest.m @@ -28,7 +28,6 @@ #import "CKKS.h" #import "CKKSItem.h" #import "CKKSCurrentItemPointer.h" -#import "CKKSAnalyticsLogger.h" #import "utilities/der_plist.h" #import #import @@ -282,7 +281,7 @@ static NSUInteger LeafBucketIndexForUUID(NSString* uuid) { 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); @@ -522,14 +521,12 @@ static NSUInteger LeafBucketIndexForUUID(NSString* uuid) 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; } @@ -546,7 +543,6 @@ static NSUInteger LeafBucketIndexForUUID(NSString* uuid) 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]; @@ -562,12 +558,8 @@ static NSUInteger LeafBucketIndexForUUID(NSString* uuid) 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; } @@ -739,7 +731,6 @@ static NSUInteger LeafBucketIndexForUUID(NSString* uuid) NSData* signatureDERData = [self derDataFromSignatureDict:self.signatures error:nil]; if (!signatureDERData) { - [[CKKSAnalyticsLogger logger] logHardFailureForEventNamed:@"CKKSManifestUpdateRecord" withAttributes:@{CKKSManifestZoneKey : zoneID.zoneName, CKKSManifestSignerIDKey : _signerID, CKKSManifestGenCountKey : @(_generationCount)}]; return record; } @@ -756,7 +747,6 @@ static NSUInteger LeafBucketIndexForUUID(NSString* uuid) record[key] = futureField; }]; - [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"CKKSManifestUpdateRecord"]; return record; } @@ -834,13 +824,6 @@ static NSUInteger LeafBucketIndexForUUID(NSString* uuid) } } - if (verified) { - [[CKKSAnalyticsLogger logger] logSuccessForEventNamed:@"CKKSManifestValidateSelf"]; - } - else { - [[CKKSAnalyticsLogger logger] logSoftFailureForEventNamed:@"CKKSManifestValidateSelf" withAttributes:@{CKKSManifestZoneKey : _zoneName, CKKSManifestSignerIDKey : _signerID, CKKSManifestGenerationCountKey : @(_generationCount)}]; - } - return verified; } diff --git a/keychain/ckks/CKKSManifestLeafRecord.h b/keychain/ckks/CKKSManifestLeafRecord.h index 3e69d18b..4a10f75f 100644 --- a/keychain/ckks/CKKSManifestLeafRecord.h +++ b/keychain/ckks/CKKSManifestLeafRecord.h @@ -23,8 +23,8 @@ #if OCTAGON -#import "CKKSRecordHolder.h" #import +#import "CKKSRecordHolder.h" @class CKRecord; @class CKKSItem; @@ -34,13 +34,13 @@ NS_ASSUME_NONNULL_BEGIN @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* recordDigestDict; // keyed by record UUID +@property (nonatomic, readonly) NSDictionary* recordDigestDict; // keyed by record UUID @end diff --git a/keychain/ckks/CKKSMirrorEntry.h b/keychain/ckks/CKKSMirrorEntry.h index 9b1238dc..3b2a8dc1 100644 --- a/keychain/ckks/CKKSMirrorEntry.h +++ b/keychain/ckks/CKKSMirrorEntry.h @@ -23,10 +23,10 @@ #if OCTAGON -#import "CKKSSQLDatabaseObject.h" -#import "CKKSItem.h" -#include #include +#include +#import "CKKSItem.h" +#import "CKKSSQLDatabaseObject.h" #ifndef CKKSMirrorEntry_h #define CKKSMirrorEntry_h @@ -42,15 +42,15 @@ @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*)countsByParentKey:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; ++ (NSDictionary*)countsByParentKey:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; @end diff --git a/keychain/ckks/CKKSNearFutureScheduler.h b/keychain/ckks/CKKSNearFutureScheduler.h index fa7bac49..b8670e01 100644 --- a/keychain/ckks/CKKSNearFutureScheduler.h +++ b/keychain/ckks/CKKSNearFutureScheduler.h @@ -23,6 +23,7 @@ #import #import +NS_ASSUME_NONNULL_BEGIN /* * The CKKSNearFutureScheduler is intended to rate-limit an operation. When @@ -35,25 +36,31 @@ @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 diff --git a/keychain/ckks/CKKSNearFutureScheduler.m b/keychain/ckks/CKKSNearFutureScheduler.m index 3a2f049d..5d1ed527 100644 --- a/keychain/ckks/CKKSNearFutureScheduler.m +++ b/keychain/ckks/CKKSNearFutureScheduler.m @@ -23,6 +23,7 @@ #import "CKKSNearFutureScheduler.h" #import "CKKSCondition.h" +#import "keychain/ckks/NSOperationCategories.h" #include @interface CKKSNearFutureScheduler () @@ -30,6 +31,9 @@ @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. @@ -43,16 +47,16 @@ @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; @@ -60,17 +64,24 @@ _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) { @@ -86,7 +97,7 @@ // 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; } @@ -103,11 +114,17 @@ 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. diff --git a/keychain/ckks/CKKSNewTLKOperation.h b/keychain/ckks/CKKSNewTLKOperation.h index 918dd147..f75052e7 100644 --- a/keychain/ckks/CKKSNewTLKOperation.h +++ b/keychain/ckks/CKKSNewTLKOperation.h @@ -36,5 +36,4 @@ @end -#endif // OCTAGON - +#endif // OCTAGON diff --git a/keychain/ckks/CKKSNewTLKOperation.m b/keychain/ckks/CKKSNewTLKOperation.m index bdccb802..b9d09a86 100644 --- a/keychain/ckks/CKKSNewTLKOperation.m +++ b/keychain/ckks/CKKSNewTLKOperation.m @@ -89,6 +89,29 @@ 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"); @@ -207,14 +230,12 @@ // Generate the TLK sharing records for all trusted peers NSMutableSet* tlkShares = [NSMutableSet set]; - if(SecCKKSShareTLKs()) { - for(id 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 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 diff --git a/keychain/ckks/CKKSNotifier.h b/keychain/ckks/CKKSNotifier.h index 85324ce4..64b550fd 100644 --- a/keychain/ckks/CKKSNotifier.h +++ b/keychain/ckks/CKKSNotifier.h @@ -21,15 +21,22 @@ * @APPLE_LICENSE_HEADER_END@ */ +#if OCTAGON #import +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 -+(void)post:(NSString*) notification; ++ (void)post:(NSString*)notification; @end + +NS_ASSUME_NONNULL_END + +#endif //OCTAGON diff --git a/keychain/ckks/CKKSNotifier.m b/keychain/ckks/CKKSNotifier.m index 916a4675..2591a046 100644 --- a/keychain/ckks/CKKSNotifier.m +++ b/keychain/ckks/CKKSNotifier.m @@ -21,6 +21,8 @@ * @APPLE_LICENSE_HEADER_END@ */ +#if OCTAGON + #import "CKKSNotifier.h" #import #import @@ -35,3 +37,5 @@ } @end + +#endif // OCTAGON diff --git a/keychain/ckks/CKKSOutgoingQueueEntry.h b/keychain/ckks/CKKSOutgoingQueueEntry.h index f8d1516f..aadeb473 100644 --- a/keychain/ckks/CKKSOutgoingQueueEntry.h +++ b/keychain/ckks/CKKSOutgoingQueueEntry.h @@ -21,11 +21,11 @@ * @APPLE_LICENSE_HEADER_END@ */ -#import "CKKSSQLDatabaseObject.h" +#include +#include #import "CKKSItem.h" #import "CKKSMirrorEntry.h" -#include -#include +#import "CKKSSQLDatabaseObject.h" #ifndef CKKSOutgoingQueueEntry_h #define CKKSOutgoingQueueEntry_h @@ -38,28 +38,42 @@ @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*) fetch:(ssize_t) n state: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; -+ (NSArray*) allInState: (NSString*) state zoneID:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; ++ (NSArray*)fetch:(ssize_t)n + state:(NSString*)state + zoneID:(CKRecordZoneID*)zoneID + error:(NSError* __autoreleasing*)error; ++ (NSArray*)allInState:(NSString*)state + zoneID:(CKRecordZoneID*)zoneID + error:(NSError* __autoreleasing*)error; -+ (NSDictionary*)countsByState:(CKRecordZoneID*)zoneID error: (NSError * __autoreleasing *) error; ++ (NSDictionary*)countsByState:(CKRecordZoneID*)zoneID error:(NSError* __autoreleasing*)error; @end diff --git a/keychain/ckks/CKKSOutgoingQueueOperation.h b/keychain/ckks/CKKSOutgoingQueueOperation.h index aeeed762..32348f45 100644 --- a/keychain/ckks/CKKSOutgoingQueueOperation.h +++ b/keychain/ckks/CKKSOutgoingQueueOperation.h @@ -41,4 +41,4 @@ - (instancetype)initWithCKKSKeychainView:(CKKSKeychainView*)ckks ckoperationGroup:(CKOperationGroup*)ckoperationGroup; @end -#endif // OCTAGON +#endif // OCTAGON diff --git a/keychain/ckks/CKKSOutgoingQueueOperation.m b/keychain/ckks/CKKSOutgoingQueueOperation.m index 96fbe10e..ea8a9540 100644 --- a/keychain/ckks/CKKSOutgoingQueueOperation.m +++ b/keychain/ckks/CKKSOutgoingQueueOperation.m @@ -29,7 +29,7 @@ #import "CKKSOutgoingQueueEntry.h" #import "CKKSReencryptOutgoingItemsOperation.h" #import "CKKSManifest.h" -#import "CKKSAnalyticsLogger.h" +#import "CKKSAnalyticsLogger.h" #include #include @@ -87,6 +87,7 @@ NSError* error = nil; + // We only actually care about queue items in the 'new' state NSArray * queueEntries = [CKKSOutgoingQueueEntry fetch:SecCKKSOutgoingQueueItemsAtOnce state: SecCKKSStateNew zoneID:ckks.zoneID error:&error]; @@ -96,6 +97,8 @@ return false; } + //[CKKSPowerCollection CKKSPowerEvent:kCKKSPowerEventOutgoingQueue zone:ckks.zoneName count:[queueEntries count]]; + ckksinfo("ckksoutgoing", ckks, "processing outgoing queue: %@", queueEntries); NSMutableDictionary* recordsToSave = [[NSMutableDictionary alloc] init]; @@ -161,7 +164,9 @@ } } 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) { @@ -260,10 +265,17 @@ 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]; @@ -278,7 +290,7 @@ 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' @@ -298,26 +310,14 @@ // 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); @@ -326,6 +326,10 @@ } } } + } 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; @@ -373,7 +377,7 @@ } } 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); } @@ -404,7 +408,13 @@ 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; }]; @@ -445,13 +455,14 @@ 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); } @@ -495,7 +506,7 @@ } -- (void)_onqueueModifyAllRecordsAsReencrypt: (NSArray*) recordIDs { +- (void)_onqueueModifyAllRecords:(NSArray*)recordIDs as:(CKKSItemState*)state { CKKSKeychainView* ckks = self.ckks; if(!ckks) { ckkserror("ckksoutgoing", ckks, "no CKKS object"); @@ -514,16 +525,18 @@ // 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; diff --git a/keychain/ckks/CKKSPeer.h b/keychain/ckks/CKKSPeer.h index dd9f9394..9c6dc3be 100644 --- a/keychain/ckks/CKKSPeer.h +++ b/keychain/ckks/CKKSPeer.h @@ -26,13 +26,14 @@ #import #import +NS_ASSUME_NONNULL_BEGIN // ==== Peer protocols ==== @protocol CKKSPeer @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)peer; @@ -47,8 +48,8 @@ @interface CKKSSelves : NSObject @property id currentSelf; -@property NSSet>* allSelves; -- (instancetype)initWithCurrent:(id)selfPeer allSelves:(NSSet>*)allSelves; +@property (nullable) NSSet>* allSelves; +- (instancetype)initWithCurrent:(id)selfPeer allSelves:(NSSet>* _Nullable)allSelves; @end // ==== Peer handler protocols ==== @@ -56,8 +57,8 @@ @protocol CKKSPeerUpdateListener; @protocol CKKSPeerProvider -- (CKKSSelves*)fetchSelfPeers:(NSError* __autoreleasing *)error; -- (NSSet>*)fetchTrustedPeers:(NSError* __autoreleasing *)error; +- (CKKSSelves* _Nullable)fetchSelfPeers:(NSError* _Nullable __autoreleasing* _Nullable)error; +- (NSSet>* _Nullable)fetchTrustedPeers:(NSError* _Nullable __autoreleasing* _Nullable)error; // Trusted peers should include self peers - (void)registerForPeerChangeUpdates:(id)listener; @@ -69,16 +70,15 @@ - (void)trustedPeerSetChanged; @end - // These should be replaced by Octagon peers, when those exist @interface CKKSSOSPeer : NSObject @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 @@ -94,4 +94,5 @@ signingKey:(SFECKeyPair*)signingKey; @end -#endif // OCTAGON +NS_ASSUME_NONNULL_END +#endif // OCTAGON diff --git a/keychain/ckks/CKKSProcessReceivedKeysOperation.m b/keychain/ckks/CKKSProcessReceivedKeysOperation.m index d05c8695..98f9ba06 100644 --- a/keychain/ckks/CKKSProcessReceivedKeysOperation.m +++ b/keychain/ckks/CKKSProcessReceivedKeysOperation.m @@ -145,7 +145,13 @@ } 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; } } diff --git a/keychain/ckks/CKKSRateLimiter.h b/keychain/ckks/CKKSRateLimiter.h index 8dc8f2f7..5d290773 100644 --- a/keychain/ckks/CKKSRateLimiter.h +++ b/keychain/ckks/CKKSRateLimiter.h @@ -21,17 +21,16 @@ * @APPLE_LICENSE_HEADER_END@ */ -#ifndef RateLimiter_h -#define RateLimiter_h - #if OCTAGON #import #import "CKKSOutgoingQueueEntry.h" +NS_ASSUME_NONNULL_BEGIN + @interface CKKSRateLimiter : NSObject -@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. @@ -44,19 +43,19 @@ * 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 diff --git a/keychain/ckks/CKKSRateLimiter.m b/keychain/ckks/CKKSRateLimiter.m index 5f2aae70..fc16cef7 100644 --- a/keychain/ckks/CKKSRateLimiter.m +++ b/keychain/ckks/CKKSRateLimiter.m @@ -41,7 +41,7 @@ typedef NS_ENUM(int, BucketType) { }; @interface CKKSRateLimiter() -@property (readwrite, nonnull) NSDictionary *config; +@property (readwrite) NSDictionary *config; @property NSMutableDictionary *buckets; @property NSDate *overloadUntil; #if !TARGET_OS_BRIDGE diff --git a/keychain/ckks/CKKSRecordHolder.h b/keychain/ckks/CKKSRecordHolder.h index df625342..81e24714 100644 --- a/keychain/ckks/CKKSRecordHolder.h +++ b/keychain/ckks/CKKSRecordHolder.h @@ -23,38 +23,37 @@ #if OCTAGON -#include +#import #include - -#ifndef CKKSRecordHolder_h -#define CKKSRecordHolder_h - +#include #import "keychain/ckks/CKKSSQLDatabaseObject.h" -#import + +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 */ diff --git a/keychain/ckks/CKKSReencryptOutgoingItemsOperation.h b/keychain/ckks/CKKSReencryptOutgoingItemsOperation.h index 4a163ebd..6cd17a24 100644 --- a/keychain/ckks/CKKSReencryptOutgoingItemsOperation.h +++ b/keychain/ckks/CKKSReencryptOutgoingItemsOperation.h @@ -22,8 +22,8 @@ */ #if OCTAGON -#import #import +#import #import "keychain/ckks/CKKSGroupOperation.h" @class CKKSKeychainView; @@ -37,5 +37,4 @@ @end -#endif // OCTAGON - +#endif // OCTAGON diff --git a/keychain/ckks/CKKSReencryptOutgoingItemsOperation.m b/keychain/ckks/CKKSReencryptOutgoingItemsOperation.m index 44956f0f..3b975a66 100644 --- a/keychain/ckks/CKKSReencryptOutgoingItemsOperation.m +++ b/keychain/ckks/CKKSReencryptOutgoingItemsOperation.m @@ -28,6 +28,7 @@ #import "keychain/ckks/CKKSOutgoingQueueEntry.h" #import "keychain/ckks/CKKSReencryptOutgoingItemsOperation.h" #import "keychain/ckks/CKKSItemEncrypter.h" +#import "keychain/ckks/CloudKitCategories.h" #if OCTAGON @@ -91,7 +92,7 @@ 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) { @@ -103,7 +104,7 @@ continue; } - ckksinfo("ckksreencrypt", ckks, "Reencrypting item %@", oqe); + ckksnotice("ckksreencrypt", ckks, "Reencrypting item %@", oqe); NSDictionary* item = [CKKSItemEncrypter decryptItemToDictionary: oqe.item error:&error]; if(error) { diff --git a/keychain/ckks/CKKSResultOperation.h b/keychain/ckks/CKKSResultOperation.h index 20fc548d..b387ba5b 100644 --- a/keychain/ckks/CKKSResultOperation.h +++ b/keychain/ckks/CKKSResultOperation.h @@ -21,8 +21,7 @@ * @APPLE_LICENSE_HEADER_END@ */ -#ifndef CKKSResultOperation_h -#define CKKSResultOperation_h +#if OCTAGON #import #import @@ -44,7 +43,7 @@ enum { // 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. @@ -57,16 +56,17 @@ enum { - (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*) operations; +- (bool)allSuccessful:(NSArray*)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 diff --git a/keychain/ckks/CKKSResultOperation.m b/keychain/ckks/CKKSResultOperation.m index 0adc5114..f92f957f 100644 --- a/keychain/ckks/CKKSResultOperation.m +++ b/keychain/ckks/CKKSResultOperation.m @@ -21,8 +21,12 @@ * @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 @interface CKKSResultOperation() @@ -110,7 +114,9 @@ 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]; } @@ -144,12 +150,17 @@ 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* 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) { @@ -164,7 +175,7 @@ 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; } @@ -181,4 +192,18 @@ 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 diff --git a/keychain/ckks/CKKSSIV.h b/keychain/ckks/CKKSSIV.h index 5b1b2687..db1cc3a9 100644 --- a/keychain/ckks/CKKSSIV.h +++ b/keychain/ckks/CKKSSIV.h @@ -27,40 +27,45 @@ // For AES-SIV 512. -#define CKKSKeySize (512/8) -#define CKKSWrappedKeySize (CKKSKeySize+16) +#define CKKSKeySize (512 / 8) +#define CKKSWrappedKeySize (CKKSKeySize + 16) -@interface CKKSBaseAESSIVKey : NSObject { - @package - uint8_t key[CKKSWrappedKeySize]; // subclasses can use less than the whole buffer, and set key to be precise +@interface CKKSBaseAESSIVKey : NSObject +{ + @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*) ad error: (NSError * __autoreleasing *) error; -- (NSData*)decryptData: (NSData*) ciphertext authenticatedData: (NSDictionary*) ad error: (NSError * __autoreleasing *) error; +- (NSData*)encryptData:(NSData*)plaintext + authenticatedData:(NSDictionary*)ad + error:(NSError* __autoreleasing*)error; +- (NSData*)decryptData:(NSData*)ciphertext + authenticatedData:(NSDictionary*)ad + error:(NSError* __autoreleasing*)error; @end -#endif // OCTAGON +#endif // OCTAGON diff --git a/keychain/ckks/CKKSSQLDatabaseObject.h b/keychain/ckks/CKKSSQLDatabaseObject.h index d016e35d..efa8c45c 100644 --- a/keychain/ckks/CKKSSQLDatabaseObject.h +++ b/keychain/ckks/CKKSSQLDatabaseObject.h @@ -21,80 +21,106 @@ * @APPLE_LICENSE_HEADER_END@ */ -#ifndef DatabaseObject_h -#define DatabaseObject_h - -#include #include +#include -#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 { +NS_ASSUME_NONNULL_BEGIN -} +@interface CKKSSQLDatabaseObject : NSObject -@property (copy) NSDictionary* originalSelfWhereClause; +@property (copy) NSDictionary* 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*) sqlColumns; ++ (NSArray*)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*) sqlValues; +- (NSDictionary*)sqlValues; // Return a set of key-value pairs that will uniquely find This Row in the table -- (NSDictionary*) whereClauseToFindSelf; +- (NSDictionary*)whereClauseToFindSelf; -- (instancetype)copyWithZone:(NSZone *)zone; +//- (instancetype)copyWithZone:(NSZone* _Nullable)zone; @end // Helper class to use with where clauses @@ -102,9 +128,9 @@ @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 diff --git a/keychain/ckks/CKKSSQLDatabaseObject.m b/keychain/ckks/CKKSSQLDatabaseObject.m index ed859d2d..8e6ab086 100644 --- a/keychain/ckks/CKKSSQLDatabaseObject.m +++ b/keychain/ckks/CKKSSQLDatabaseObject.m @@ -25,6 +25,7 @@ #import "CKKSSQLDatabaseObject.h" #include +#import "keychain/ckks/CKKS.h" #import "CKKSKeychainView.h" @implementation CKKSSQLDatabaseObject @@ -328,7 +329,7 @@ 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] diff --git a/keychain/ckks/CKKSScanLocalItemsOperation.h b/keychain/ckks/CKKSScanLocalItemsOperation.h index cd02e822..b1b7754d 100644 --- a/keychain/ckks/CKKSScanLocalItemsOperation.h +++ b/keychain/ckks/CKKSScanLocalItemsOperation.h @@ -41,4 +41,4 @@ @end -#endif // OCTAGON +#endif // OCTAGON diff --git a/keychain/ckks/CKKSScanLocalItemsOperation.m b/keychain/ckks/CKKSScanLocalItemsOperation.m index 065d06a0..cc42a1d5 100644 --- a/keychain/ckks/CKKSScanLocalItemsOperation.m +++ b/keychain/ckks/CKKSScanLocalItemsOperation.m @@ -33,6 +33,8 @@ #import "keychain/ckks/CKKSViewManager.h" #import "keychain/ckks/CKKSManifest.h" +#import "CKKSPowerCollection.h" + #include #include #include @@ -40,6 +42,7 @@ @interface CKKSScanLocalItemsOperation () @property CKOperationGroup* ckoperationGroup; +@property (assign) NSUInteger processsedItems; @end @implementation CKKSScanLocalItemsOperation @@ -78,7 +81,7 @@ __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++) { @@ -113,6 +116,8 @@ 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. @@ -181,7 +186,7 @@ // 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; @@ -237,7 +242,9 @@ 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]; @@ -265,6 +272,8 @@ [ckks processOutgoingQueue:self.ckoperationGroup]; } + ckksnotice("ckksscan", ckks, "Completed scan"); + ckks.droppedItems = false; return true; }]; } diff --git a/keychain/ckks/CKKSSynchronizeOperation.h b/keychain/ckks/CKKSSynchronizeOperation.h index 70534d0e..cb6cbd6b 100644 --- a/keychain/ckks/CKKSSynchronizeOperation.h +++ b/keychain/ckks/CKKSSynchronizeOperation.h @@ -36,5 +36,4 @@ @end -#endif // OCTAGON - +#endif // OCTAGON diff --git a/keychain/ckks/CKKSTLKShare.h b/keychain/ckks/CKKSTLKShare.h index 2ef3b854..239e492a 100644 --- a/keychain/ckks/CKKSTLKShare.h +++ b/keychain/ckks/CKKSTLKShare.h @@ -30,12 +30,14 @@ #import "keychain/ckks/CKKSKey.h" #import "keychain/ckks/CKKSPeer.h" -#import #import +#import + +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 @@ -53,53 +55,49 @@ typedef NS_ENUM(NSUInteger, SecCKKSTLKShareVersion) { @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)recoverer - trustedPeers:(NSSet>*)peers - error:(NSError* __autoreleasing *)error; +- (CKKSKey* _Nullable)recoverTLK:(id)recoverer trustedPeers:(NSSet>*)peers error:(NSError**)error; -+ (CKKSTLKShare*)share:(CKKSKey*)key - as:(id)sender - to:(id)receiver - epoch:(NSInteger)epoch - poisoned:(NSInteger)poisoned - error:(NSError* __autoreleasing *)error; ++ (CKKSTLKShare* _Nullable)share:(CKKSKey*)key + as:(id)sender + to:(id)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*)allFor:(NSString*)receiverPeerID keyUUID:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID - error:(NSError * __autoreleasing *)error; -+ (NSArray*)allForUUID:(NSString*)uuid - zoneID:(CKRecordZoneID*)zoneID - error:(NSError * __autoreleasing *)error; -+ (NSArray*)allInZone:(CKRecordZoneID*)zoneID - error:(NSError * __autoreleasing *)error; -+ (instancetype)tryFromDatabaseFromCKRecordID:(CKRecordID*)recordID - error:(NSError * __autoreleasing *)error; + error:(NSError* __autoreleasing*)error; ++ (NSArray*)allForUUID:(NSString*)uuid zoneID:(CKRecordZoneID*)zoneID error:(NSError**)error; ++ (NSArray*)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)localPeer error:(NSError * __autoreleasing *)error; -- (NSData*)signRecord:(SFECKeyPair*)signingKey error:(NSError* __autoreleasing *)error; -- (bool)verifySignature:(NSData*)signature verifyingPeer:(id)peer error:(NSError* __autoreleasing *)error; +- (CKKSKey* _Nullable)unwrapUsing:(id)localPeer error:(NSError**)error; +- (NSData* _Nullable)signRecord:(SFECKeyPair*)signingKey error:(NSError**)error; +- (bool)verifySignature:(NSData*)signature verifyingPeer:(id)peer error:(NSError**)error; - (NSData*)dataForSigning; @end -#endif // OCTAGON +NS_ASSUME_NONNULL_END + +#endif // OCTAGON diff --git a/keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h b/keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h index 6ce5057c..3cdbdd92 100644 --- a/keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h +++ b/keychain/ckks/CKKSUpdateCurrentItemPointerOperation.h @@ -30,7 +30,7 @@ @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 @@ -38,5 +38,4 @@ ckoperationGroup:(CKOperationGroup*)ckoperationGroup; @end -#endif // OCTAGON - +#endif // OCTAGON diff --git a/keychain/ckks/CKKSUpdateCurrentItemPointerOperation.m b/keychain/ckks/CKKSUpdateCurrentItemPointerOperation.m index 1d144717..39a2d886 100644 --- a/keychain/ckks/CKKSUpdateCurrentItemPointerOperation.m +++ b/keychain/ckks/CKKSUpdateCurrentItemPointerOperation.m @@ -128,8 +128,8 @@ } // 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 @@ -215,8 +215,8 @@ 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]; diff --git a/keychain/ckks/CKKSUpdateDeviceStateOperation.h b/keychain/ckks/CKKSUpdateDeviceStateOperation.h index 0d82e7a7..59507bb5 100644 --- a/keychain/ckks/CKKSUpdateDeviceStateOperation.h +++ b/keychain/ckks/CKKSUpdateDeviceStateOperation.h @@ -23,15 +23,17 @@ #if OCTAGON -#import "keychain/ckks/CKKSGroupOperation.h" -#import "keychain/ckks/CKKSDeviceStateEntry.h" #import +#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 diff --git a/keychain/ckks/CKKSViewManager.h b/keychain/ckks/CKKSViewManager.h index 8b071b0d..06f24aa7 100644 --- a/keychain/ckks/CKKSViewManager.h +++ b/keychain/ckks/CKKSViewManager.h @@ -21,27 +21,27 @@ * @APPLE_LICENSE_HEADER_END@ */ + #import -#include -#import "keychain/ckks/CKKS.h" -#import "keychain/ckks/CKKSControlProtocol.h" #if OCTAGON -#import "keychain/ckks/CloudKitDependencies.h" + +#include +#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 @property CKContainer* container; @@ -54,66 +54,64 @@ @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) fetchRecordZoneChangesOperationClass - fetchRecordsOperationClass: (Class)fetchRecordsOperationClass - queryOperationClass:(Class)queryOperationClass - modifySubscriptionsOperationClass: (Class) modifySubscriptionsOperationClass - modifyRecordZonesOperationClass: (Class) modifyRecordZonesOperationClass - apsConnectionClass: (Class) apsConnectionClass - nsnotificationCenterClass: (Class) nsnotificationCenterClass - notifierClass: (Class) notifierClass - setupHold:(NSOperation*) setupHold; +- (instancetype)initCloudKitWithContainerName:(NSString*)containerName usePCS:(bool)usePCS; +- (instancetype)initWithContainerName:(NSString*)containerName + usePCS:(bool)usePCS + fetchRecordZoneChangesOperationClass:(Class)fetchRecordZoneChangesOperationClass + fetchRecordsOperationClass:(Class)fetchRecordsOperationClass + queryOperationClass:(Class)queryOperationClass + modifySubscriptionsOperationClass:(Class)modifySubscriptionsOperationClass + modifyRecordZonesOperationClass:(Class)modifyRecordZonesOperationClass + apsConnectionClass:(Class)apsConnectionClass + nsnotificationCenterClass:(Class)nsnotificationCenterClass + notifierClass:(Class)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*)activeTLKs; +- (NSDictionary*)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; @@ -122,18 +120,23 @@ - (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>*)fetchTrustedPeers:(NSError* __autoreleasing *)error; +- (CKKSSelves* _Nullable)fetchSelfPeers:(NSError* __autoreleasing*)error; +- (NSSet>* _Nullable)fetchTrustedPeers:(NSError* __autoreleasing*)error; - (void)sendSelfPeerChangedUpdate; - (void)sendTrustedPeerSetChangedUpdate; -#endif // OCTAGON @end +NS_ASSUME_NONNULL_END + +#else +@interface CKKSViewManager : NSObject +@end +#endif // OCTAGON diff --git a/keychain/ckks/CKKSViewManager.m b/keychain/ckks/CKKSViewManager.m index 436c0d5a..1e197fd9 100644 --- a/keychain/ckks/CKKSViewManager.m +++ b/keychain/ckks/CKKSViewManager.m @@ -30,7 +30,6 @@ #import "keychain/ckks/CKKSNotifier.h" #import "keychain/ckks/CKKSCondition.h" #import "keychain/ckks/CloudKitCategories.h" -#import "CKKSAnalyticsLogger.h" #import "SecEntitlements.h" @@ -51,6 +50,8 @@ #import #import + +#import "CKKSAnalyticsLogger.h" #endif @interface CKKSViewManager () @@ -90,21 +91,20 @@ 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) fetchRecordZoneChangesOperationClass fetchRecordsOperationClass: (Class)fetchRecordsOperationClass - queryOperationClass:(Class)queryOperationClass + queryOperationClass: (Class)queryOperationClass modifySubscriptionsOperationClass: (Class) modifySubscriptionsOperationClass modifyRecordZonesOperationClass: (Class) modifyRecordZonesOperationClass apsConnectionClass: (Class) apsConnectionClass nsnotificationCenterClass: (Class) nsnotificationCenterClass notifierClass: (Class) notifierClass - setupHold: (NSOperation*) setupHold { +{ if(self = [super init]) { _fetchRecordZoneChangesOperationClass = fetchRecordZoneChangesOperationClass; _fetchRecordsOperationClass = fetchRecordsOperationClass; @@ -127,7 +127,6 @@ _views = [[NSMutableDictionary alloc] init]; _pendingSyncCallbacks = [[NSMutableDictionary alloc] init]; - _zoneStartupDependency = setupHold; _initializeNewZones = false; _completedSecCKKSInitialize = [[CKKSCondition alloc] init]; @@ -278,10 +277,6 @@ dispatch_once_t globalZoneStateQueueOnce; apsConnectionClass: self.apsConnectionClass notifierClass: self.notifierClass]; - if(self.zoneStartupDependency) { - [self.views[viewName].zoneSetupOperation addDependency: self.zoneStartupDependency]; - } - if(self.initializeNewZones) { [self.views[viewName] initializeZone]; } @@ -560,28 +555,40 @@ dispatch_once_t globalZoneStateQueueOnce; reply(@{}); } -- (void)rpcResetLocal:(NSString*)viewName reply: (void(^)(NSError* result)) reply { +- (NSArray*)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:^{}]; @@ -603,26 +610,12 @@ dispatch_once_t globalZoneStateQueueOnce; } - (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:^{}]; @@ -645,38 +638,24 @@ dispatch_once_t globalZoneStateQueueOnce; } - (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]; @@ -688,6 +667,34 @@ dispatch_once_t globalZoneStateQueueOnce; 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* result, NSError* error)) reply { NSMutableArray* a = [[NSMutableArray alloc] init]; @@ -958,7 +965,12 @@ dispatch_once_t globalZoneStateQueueOnce; 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; diff --git a/keychain/ckks/CKKSZone.h b/keychain/ckks/CKKSZone.h index a46deabb..3e4b94ee 100644 --- a/keychain/ckks/CKKSZone.h +++ b/keychain/ckks/CKKSZone.h @@ -21,39 +21,33 @@ * @APPLE_LICENSE_HEADER_END@ */ -#ifndef CKKSZone_h -#define CKKSZone_h - #import #if OCTAGON -#import "keychain/ckks/CloudKitDependencies.h" #import "keychain/ckks/CKKSCKAccountStateTracker.h" -#endif +#import "keychain/ckks/CloudKitDependencies.h" -#if OCTAGON -@interface CKKSZone : NSObject { +NS_ASSUME_NONNULL_BEGIN + +@interface CKKSZone : NSObject +{ 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; @@ -74,39 +68,49 @@ @property dispatch_queue_t queue; -- (instancetype)initWithContainer: (CKContainer*) container - zoneName: (NSString*) zoneName - accountTracker:(CKKSCKAccountStateTracker*) tracker - fetchRecordZoneChangesOperationClass: (Class) fetchRecordZoneChangesOperationClass - fetchRecordsOperationClass: (Class)fetchRecordsOperationClass - queryOperationClass:(Class)queryOperationClass - modifySubscriptionsOperationClass: (Class) modifySubscriptionsOperationClass - modifyRecordZonesOperationClass: (Class) modifyRecordZonesOperationClass - apsConnectionClass: (Class) apsConnectionClass; - +- (instancetype)initWithContainer:(CKContainer*)container + zoneName:(NSString*)zoneName + accountTracker:(CKKSCKAccountStateTracker*)tracker + fetchRecordZoneChangesOperationClass:(Class)fetchRecordZoneChangesOperationClass + fetchRecordsOperationClass:(Class)fetchRecordsOperationClass + queryOperationClass:(Class)queryOperationClass + modifySubscriptionsOperationClass:(Class)modifySubscriptionsOperationClass + modifyRecordZonesOperationClass:(Class)modifyRecordZonesOperationClass + apsConnectionClass:(Class)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 @@ -116,15 +120,19 @@ - (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 diff --git a/keychain/ckks/CKKSZone.m b/keychain/ckks/CKKSZone.m index eee5cdb4..d1006737 100644 --- a/keychain/ckks/CKKSZone.m +++ b/keychain/ckks/CKKSZone.m @@ -30,7 +30,6 @@ #import "keychain/ckks/CKKSCKAccountStateTracker.h" #import #import -#endif #import "CKKSKeychainView.h" #import "CKKSZone.h" @@ -38,24 +37,22 @@ #include @interface CKKSZone() -#if OCTAGON @property CKDatabaseOperation* zoneCreationOperation; @property CKDatabaseOperation* zoneDeletionOperation; @property CKDatabaseOperation* zoneSubscriptionOperation; -@property bool acceptingNewOperations; @property NSOperationQueue* operationQueue; @property NSOperation* accountLoggedInDependency; @property NSHashTable* accountOperations; -#endif + +// Make writable +@property bool halted; @end @implementation CKKSZone -#if OCTAGON - - (instancetype)initWithContainer: (CKContainer*) container zoneName: (NSString*) zoneName accountTracker:(CKKSCKAccountStateTracker*) tracker @@ -71,12 +68,18 @@ _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]; @@ -89,33 +92,15 @@ _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; @@ -131,86 +116,75 @@ } --(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; @@ -221,42 +195,23 @@ 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; @@ -265,17 +220,10 @@ 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; @@ -290,7 +238,7 @@ CKKSAPSReceiver* aps = [CKKSAPSReceiver receiverForEnvironment:apsPushEnvString namedDelegatePort:SecCKKSAPSNamedPort apsConnectionClass:strongSelf.apsConnectionClass]; - [aps register:strongSelf forZoneID:strongSelf.zoneID]; + [aps registerReceiver:strongSelf forZoneID:strongSelf.zoneID]; } }]; @@ -330,10 +278,10 @@ 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) { @@ -384,25 +332,23 @@ } 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; } @@ -453,7 +399,6 @@ } 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. @@ -465,7 +410,7 @@ // 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; @@ -477,16 +422,19 @@ } - (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; } @@ -516,6 +464,11 @@ } - (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]; @@ -524,6 +477,11 @@ // 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; } @@ -532,6 +490,11 @@ // 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"); @@ -539,9 +502,17 @@ }); } +- (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 */ diff --git a/keychain/ckks/CKKSZoneChangeFetcher.h b/keychain/ckks/CKKSZoneChangeFetcher.h index 41d4d4ff..f03db22a 100644 --- a/keychain/ckks/CKKSZoneChangeFetcher.h +++ b/keychain/ckks/CKKSZoneChangeFetcher.h @@ -21,9 +21,12 @@ * @APPLE_LICENSE_HEADER_END@ */ +#if OCTAGON +#import #import +#import "keychain/ckks/CKKSResultOperation.h" -#if OCTAGON +NS_ASSUME_NONNULL_BEGIN /* Fetch Reasons */ @protocol SecCKKSFetchBecause @@ -39,7 +42,7 @@ extern CKKSFetchBecause* const CKKSFetchBecauseKeyHierarchy; extern CKKSFetchBecause* const CKKSFetchBecauseTesting; @protocol CKKSChangeFetcherErrorOracle -- (bool) isFatalCKFetchError: (NSError*) error; +- (bool)isFatalCKFetchError:(NSError*)error; @end /* @@ -49,12 +52,10 @@ extern CKKSFetchBecause* const CKKSFetchBecauseTesting; */ @class CKKSKeychainView; -#import "keychain/ckks/CKKSGroupOperation.h" -#import @interface CKKSZoneChangeFetcher : NSObject -@property (weak) CKKSKeychainView* ckks; +@property (nullable, weak) CKKSKeychainView* ckks; @property CKRecordZoneID* zoneID; - (instancetype)init NS_UNAVAILABLE; @@ -64,12 +65,10 @@ extern CKKSFetchBecause* const CKKSFetchBecauseTesting; - (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 - - diff --git a/keychain/ckks/CKKSZoneStateEntry.h b/keychain/ckks/CKKSZoneStateEntry.h index 274d3dc9..e66f16bf 100644 --- a/keychain/ckks/CKKSZoneStateEntry.h +++ b/keychain/ckks/CKKSZoneStateEntry.h @@ -21,9 +21,9 @@ * @APPLE_LICENSE_HEADER_END@ */ -#import "CKKSSQLDatabaseObject.h" -#include #include +#include +#import "CKKSSQLDatabaseObject.h" #ifndef CKKSZoneStateEntry_h #define CKKSZoneStateEntry_h @@ -46,14 +46,12 @@ @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; @@ -62,10 +60,10 @@ @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 @@ -75,10 +73,10 @@ 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 diff --git a/keychain/ckks/CloudKitCategories.h b/keychain/ckks/CloudKitCategories.h index 25d0a499..9ba1d850 100644 --- a/keychain/ckks/CloudKitCategories.h +++ b/keychain/ckks/CloudKitCategories.h @@ -23,29 +23,36 @@ #if OCTAGON -#import #import #import +#import + +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 diff --git a/keychain/ckks/CloudKitDependencies.h b/keychain/ckks/CloudKitDependencies.h index 39fb4a2c..ae8f9e3a 100644 --- a/keychain/ckks/CloudKitDependencies.h +++ b/keychain/ckks/CloudKitDependencies.h @@ -24,80 +24,95 @@ #ifndef CloudKitDependencies_h #define CloudKitDependencies_h -#import -#import #import +#import +#import NS_ASSUME_NONNULL_BEGIN /* CKModifyRecordZonesOperation */ @protocol CKKSModifyRecordZonesOperation + (instancetype)alloc; -- (instancetype)initWithRecordZonesToSave:(nullable NSArray *)recordZonesToSave recordZoneIDsToDelete:(nullable NSArray *)recordZoneIDsToDelete; +- (instancetype)initWithRecordZonesToSave:(nullable NSArray*)recordZonesToSave + recordZoneIDsToDelete:(nullable NSArray*)recordZoneIDsToDelete; -@property (nonatomic, strong, nullable) CKDatabase *database; -@property (nonatomic, copy, nullable) NSArray *recordZonesToSave; -@property (nonatomic, copy, nullable) NSArray *recordZoneIDsToDelete; +@property (nonatomic, strong, nullable) CKDatabase* database; +@property (nonatomic, copy, nullable) NSArray* recordZonesToSave; +@property (nonatomic, copy, nullable) NSArray* recordZoneIDsToDelete; @property NSOperationQueuePriority queuePriority; @property NSQualityOfService qualityOfService; -@property (nonatomic, copy, nullable) void (^modifyRecordZonesCompletionBlock)(NSArray * _Nullable savedRecordZones, NSArray * _Nullable deletedRecordZoneIDs, NSError * _Nullable operationError); +@property (nonatomic, copy, nullable) void (^modifyRecordZonesCompletionBlock) + (NSArray* _Nullable savedRecordZones, NSArray* _Nullable deletedRecordZoneIDs, NSError* _Nullable operationError); @end -@interface CKModifyRecordZonesOperation (SecCKKSModifyRecordZonesOperation) ; +@interface CKModifyRecordZonesOperation (SecCKKSModifyRecordZonesOperation) +; @end /* CKModifySubscriptionsOperation */ @protocol CKKSModifySubscriptionsOperation + (instancetype)alloc; -- (instancetype)initWithSubscriptionsToSave:(nullable NSArray *)subscriptionsToSave subscriptionIDsToDelete:(nullable NSArray *)subscriptionIDsToDelete; +- (instancetype)initWithSubscriptionsToSave:(nullable NSArray*)subscriptionsToSave + subscriptionIDsToDelete:(nullable NSArray*)subscriptionIDsToDelete; -@property (nonatomic, strong, nullable) CKDatabase *database; -@property (nonatomic, copy, nullable) NSArray *subscriptionsToSave; -@property (nonatomic, copy, nullable) NSArray *subscriptionIDsToDelete; +@property (nonatomic, strong, nullable) CKDatabase* database; +@property (nonatomic, copy, nullable) NSArray* subscriptionsToSave; +@property (nonatomic, copy, nullable) NSArray* 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 * _Nullable savedSubscriptions, NSArray * _Nullable deletedSubscriptionIDs, NSError * _Nullable operationError); +@property (nonatomic, copy, nullable) void (^modifySubscriptionsCompletionBlock) + (NSArray* _Nullable savedSubscriptions, NSArray* _Nullable deletedSubscriptionIDs, NSError* _Nullable operationError); @end -@interface CKModifySubscriptionsOperation (SecCKKSModifySubscriptionsOperation) ; +@interface CKModifySubscriptionsOperation (SecCKKSModifySubscriptionsOperation) +; @end /* CKFetchRecordZoneChangesOperation */ @protocol CKKSFetchRecordZoneChangesOperation + (instancetype)alloc; -- (instancetype)initWithRecordZoneIDs:(NSArray *)recordZoneIDs optionsByRecordZoneID:(nullable NSDictionary *)optionsByRecordZoneID; +- (instancetype)initWithRecordZoneIDs:(NSArray*)recordZoneIDs + optionsByRecordZoneID:(nullable NSDictionary*)optionsByRecordZoneID; -@property (nonatomic, copy, nullable) NSArray *recordZoneIDs; -@property (nonatomic, copy, nullable) NSDictionary *optionsByRecordZoneID; +@property (nonatomic, copy, nullable) NSArray* recordZoneIDs; +@property (nonatomic, copy, nullable) NSDictionary* 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 () ; +@interface CKFetchRecordZoneChangesOperation () +; @end /* CKFetchRecordsOperation */ @protocol CKKSFetchRecordsOperation + (instancetype)alloc; - (instancetype)init; -- (instancetype)initWithRecordIDs:(NSArray *)recordIDs; +- (instancetype)initWithRecordIDs:(NSArray*)recordIDs; -@property (nonatomic, copy, nullable) NSArray *recordIDs; -@property (nonatomic, copy, nullable) NSArray *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 * _Nullable recordsByRecordID, NSError * _Nullable operationError); +@property (nonatomic, copy, nullable) NSArray* recordIDs; +@property (nonatomic, copy, nullable) NSArray* 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* _Nullable recordsByRecordID, NSError* _Nullable operationError); @end @interface CKFetchRecordsOperation () @@ -107,18 +122,18 @@ NS_ASSUME_NONNULL_BEGIN @protocol CKKSQueryOperation + (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 *desiredKeys; +@property (nonatomic, copy, nullable) NSArray* 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 () @@ -127,14 +142,16 @@ NS_ASSUME_NONNULL_BEGIN /* APSConnection */ @protocol CKKSAPSConnection + (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 delegate; @end -@interface APSConnection (SecCKKSAPSConnection) ; +@interface APSConnection (SecCKKSAPSConnection) @end /* NSNotificationCenter */ @@ -148,13 +165,13 @@ NS_ASSUME_NONNULL_BEGIN /* Since CKDatabase doesn't share any types with NSOperationQueue, tell the type system about addOperation */ @protocol CKKSOperationQueue -- (void)addOperation:(NSOperation *)operation; +- (void)addOperation:(NSOperation*)operation; @end -@interface CKDatabase () ; +@interface CKDatabase () @end -@interface NSOperationQueue () ; +@interface NSOperationQueue () @end NS_ASSUME_NONNULL_END diff --git a/keychain/ckks/NSOperationCategories.h b/keychain/ckks/NSOperationCategories.h index 1255446f..3672768b 100644 --- a/keychain/ckks/NSOperationCategories.h +++ b/keychain/ckks/NSOperationCategories.h @@ -31,19 +31,18 @@ - (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 - diff --git a/keychain/ckks/NSOperationCategories.m b/keychain/ckks/NSOperationCategories.m index 8d9725df..da565e2c 100644 --- a/keychain/ckks/NSOperationCategories.m +++ b/keychain/ckks/NSOperationCategories.m @@ -77,7 +77,7 @@ 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) { diff --git a/keychain/ckks/RateLimiter.h b/keychain/ckks/RateLimiter.h index 7e986c6f..da51fc15 100644 --- a/keychain/ckks/RateLimiter.h +++ b/keychain/ckks/RateLimiter.h @@ -21,29 +21,28 @@ * @APPLE_LICENSE_HEADER_END@ */ -#ifndef RateLimiter_h -#define RateLimiter_h - #import +NS_ASSUME_NONNULL_BEGIN + @interface RateLimiter : NSObject -@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; /*! @@ -57,10 +56,10 @@ typedef NS_ENUM(NSInteger, RateLimiterBadness) { * 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: @@ -68,7 +67,7 @@ typedef NS_ENUM(NSInteger, RateLimiterBadness) { @end -#endif /* RateLimiter_h */ +NS_ASSUME_NONNULL_END /* Annotated example plist diff --git a/keychain/ckks/RateLimiter.m b/keychain/ckks/RateLimiter.m index 810651bc..fbbc5ec7 100644 --- a/keychain/ckks/RateLimiter.m +++ b/keychain/ckks/RateLimiter.m @@ -35,7 +35,7 @@ #endif @interface RateLimiter() -@property (readwrite, nonatomic, nonnull) NSDictionary *config; +@property (readwrite, nonatomic) NSDictionary *config; @property (nonatomic) NSArray *> *groups; @property (nonatomic) NSDate *lastJudgment; @property (nonatomic) NSDate *overloadUntil; diff --git a/keychain/ckks/tests/AutoreleaseTest.c b/keychain/ckks/tests/AutoreleaseTest.c new file mode 100644 index 00000000..796ef505 --- /dev/null +++ b/keychain/ckks/tests/AutoreleaseTest.c @@ -0,0 +1,99 @@ +/* + * 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 +#include +#include +#include + +#include +#include + +#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; +} diff --git a/keychain/ckks/tests/AutoreleaseTest.h b/keychain/ckks/tests/AutoreleaseTest.h new file mode 100644 index 00000000..9ef88f1d --- /dev/null +++ b/keychain/ckks/tests/AutoreleaseTest.h @@ -0,0 +1,30 @@ +/* + * 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); diff --git a/keychain/ckks/tests/CKKSAESSIVEncryptionTests.m b/keychain/ckks/tests/CKKSAESSIVEncryptionTests.m index e1cecaf3..570c4c8c 100644 --- a/keychain/ckks/tests/CKKSAESSIVEncryptionTests.m +++ b/keychain/ckks/tests/CKKSAESSIVEncryptionTests.m @@ -248,6 +248,56 @@ 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]; @@ -690,11 +740,14 @@ 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*)data key:(CKKSKey *)key { diff --git a/keychain/ckks/tests/CKKSAPSReceiverTests.m b/keychain/ckks/tests/CKKSAPSReceiverTests.m index 4b8fb997..6daee17d 100644 --- a/keychain/ckks/tests/CKKSAPSReceiverTests.m +++ b/keychain/ckks/tests/CKKSAPSReceiverTests.m @@ -30,6 +30,8 @@ #import "keychain/ckks/tests/MockCloudKit.h" #import "keychain/ckks/CKKSCondition.h" +#if OCTAGON + @interface CKKSAPSNotificationReceiver : NSObject @property XCTestExpectation* expectation; @property void (^block)(CKRecordZoneNotification* notification); @@ -122,7 +124,7 @@ 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"); @@ -157,8 +159,8 @@ 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"); @@ -185,10 +187,12 @@ 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 diff --git a/keychain/ckks/tests/CKKSCloudKitTests.m b/keychain/ckks/tests/CKKSCloudKitTests.m index 007d5eac..f0ad55f8 100644 --- a/keychain/ckks/tests/CKKSCloudKitTests.m +++ b/keychain/ckks/tests/CKKSCloudKitTests.m @@ -44,7 +44,6 @@ @interface CKKSCloudKitTests : XCTestCase @property NSOperationQueue *operationQueue; -@property NSBlockOperation *ckksHoldOperation; @property CKContainer *container; @property CKDatabase *database; @property CKKSKeychainView *kcv; @@ -83,10 +82,6 @@ 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 @@ -97,8 +92,7 @@ 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 @@ -116,7 +110,6 @@ 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 { @@ -153,10 +146,7 @@ } - (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 { diff --git a/keychain/ckks/tests/CKKSConditionTests.m b/keychain/ckks/tests/CKKSConditionTests.m index 65f06415..091500d1 100644 --- a/keychain/ckks/tests/CKKSConditionTests.m +++ b/keychain/ckks/tests/CKKSConditionTests.m @@ -80,4 +80,18 @@ [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 diff --git a/keychain/ckks/tests/CKKSLoggerTests.m b/keychain/ckks/tests/CKKSLoggerTests.m index 9c0afc62..0ef3b1e5 100644 --- a/keychain/ckks/tests/CKKSLoggerTests.m +++ b/keychain/ckks/tests/CKKSLoggerTests.m @@ -27,6 +27,8 @@ #import #import +#if OCTAGON + static NSString* tablePath = nil; @interface SQLiteTests : XCTestCase @@ -126,83 +128,10 @@ static NSString* tablePath = nil; @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); - }); -} - -// 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 { @@ -223,40 +152,6 @@ static NSString* tablePath = nil; 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); @@ -273,22 +168,6 @@ static NSString* tablePath = nil; } } -- (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 diff --git a/keychain/ckks/tests/CKKSManifestTests.m b/keychain/ckks/tests/CKKSManifestTests.m index 3a2e1688..2728f7c6 100644 --- a/keychain/ckks/tests/CKKSManifestTests.m +++ b/keychain/ckks/tests/CKKSManifestTests.m @@ -97,11 +97,8 @@ [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"]; diff --git a/keychain/ckks/tests/CKKSNearFutureSchedulerTests.m b/keychain/ckks/tests/CKKSNearFutureSchedulerTests.m index eff091fc..84de78f7 100644 --- a/keychain/ckks/tests/CKKSNearFutureSchedulerTests.m +++ b/keychain/ckks/tests/CKKSNearFutureSchedulerTests.m @@ -24,22 +24,27 @@ #include #import #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:^{ @@ -51,7 +56,7 @@ [self waitForExpectationsWithTimeout:1 handler:nil]; } -- (void)testOneShotDelay { +- (void)testBlockOneShotDelay { XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"]; toofastexpectation.inverted = YES; @@ -71,7 +76,7 @@ [self waitForExpectations: @[expectation] timeout:1]; } -- (void)testOneShotManyTrigger { +- (void)testBlockOneShotManyTrigger { XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"]; toofastexpectation.inverted = YES; @@ -105,7 +110,7 @@ } -- (void)testMultiShot { +- (void)testBlockMultiShot { XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"]; first.assertForOverFulfill = NO; @@ -120,20 +125,20 @@ [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; @@ -145,7 +150,7 @@ 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]; @@ -153,16 +158,16 @@ [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"]; @@ -170,7 +175,7 @@ [self waitForExpectations: @[waitmore] timeout: 0.2]; } -- (void)testCancel { +- (void)testBlockCancel { XCTestExpectation *cancelexpectation = [self expectationWithDescription:@"FutureScheduler fired (after cancel)"]; cancelexpectation.inverted = YES; @@ -185,7 +190,7 @@ [self waitForExpectations: @[cancelexpectation] timeout:0.2]; } -- (void)testDelayedNoShot { +- (void)testBlockDelayedNoShot { XCTestExpectation *toofastexpectation = [self expectationWithDescription:@"FutureScheduler fired (too soon)"]; toofastexpectation.inverted = YES; @@ -199,7 +204,7 @@ [self waitForExpectations: @[toofastexpectation] timeout:0.1]; } -- (void)testDelayedOneShot { +- (void)testBlockDelayedOneShot { XCTestExpectation *first = [self expectationWithDescription:@"FutureScheduler fired (one)"]; first.assertForOverFulfill = NO; @@ -215,10 +220,10 @@ [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; @@ -237,7 +242,7 @@ }]; [scheduler trigger]; - [self waitForExpectations: @[first] timeout:0.2]; + [self waitForExpectations: @[first] timeout:0.5]; [scheduler waitUntil: 150*NSEC_PER_MSEC]; [scheduler trigger]; @@ -246,4 +251,186 @@ [self waitForExpectations: @[second] timeout:0.3]; } +#pragma mark - Operation-based tests + +- (NSOperation*)operationFulfillingExpectations:(NSArray*)expectations { + return [NSBlockOperation named:@"test" withBlock:^{ + for(XCTestExpectation* e in expectations) { + [e fulfill]; + } + }]; +} + +- (void)addOperationFulfillingExpectations:(NSArray*)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 diff --git a/keychain/ckks/tests/CKKSSOSTests.m b/keychain/ckks/tests/CKKSSOSTests.m index 0538f670..819bbf44 100644 --- a/keychain/ckks/tests/CKKSSOSTests.m +++ b/keychain/ckks/tests/CKKSSOSTests.m @@ -49,9 +49,12 @@ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" #include +#include #include #include #include +#pragma clang diagnostic pop + #include #include #pragma clang diagnostic pop @@ -109,30 +112,35 @@ 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 { @@ -146,23 +154,23 @@ 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; @@ -178,11 +186,9 @@ } -(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 { @@ -288,6 +294,10 @@ [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]; @@ -358,7 +368,6 @@ tempPath = [[[[NSFileManager defaultManager] temporaryDirectory] URLByAppendingPathComponent:@"PiggyPacket"] path]; }); - NSLog(@"using temp path: %@", tempPath); return tempPath; } @@ -366,6 +375,9 @@ [self putFakeKeyHierachiesInCloudKit]; [self saveTLKsToKeychain]; + for(CKRecordZoneID* zoneID in self.ckksZones) { + [self expectCKKSTLKSelfShareUpload:zoneID]; + } [self startCKKSSubsystem]; [self waitForKeyHierarchyReadinesses]; @@ -448,9 +460,6 @@ NSArray* sortedTLKs = SOSAccountSortTLKS(tlks); XCTAssertNotNil(sortedTLKs, "sortedTLKs not set"); - NSLog(@"TLKs: %@", tlks); - NSLog(@"sortedTLKs: %@", sortedTLKs); - NSArray *expectedOrder = @[ @"11111111", @"22222222", @"33333333", @"44444444", @"55555555"]; [sortedTLKs enumerateObjectsUsingBlock:^(NSDictionary *tlk, NSUInteger idx, BOOL * _Nonnull stop) { NSString *uuid = tlk[@"acct"]; @@ -467,11 +476,15 @@ 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); @@ -554,11 +567,15 @@ [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]; diff --git a/keychain/ckks/tests/CKKSSQLTests.m b/keychain/ckks/tests/CKKSSQLTests.m index d3fa83b4..408c0772 100644 --- a/keychain/ckks/tests/CKKSSQLTests.m +++ b/keychain/ckks/tests/CKKSSQLTests.m @@ -24,6 +24,8 @@ #if OCTAGON #import +#import +#import #import "CloudKitMockXCTest.h" #import "keychain/ckks/CKKS.h" @@ -307,7 +309,7 @@ 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" ) ); diff --git a/keychain/ckks/tests/CKKSServerValidationRecoveryTests.m b/keychain/ckks/tests/CKKSServerValidationRecoveryTests.m index 4dc08d1e..ad867df6 100644 --- a/keychain/ckks/tests/CKKSServerValidationRecoveryTests.m +++ b/keychain/ckks/tests/CKKSServerValidationRecoveryTests.m @@ -61,8 +61,8 @@ 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 @@ -99,8 +99,8 @@ 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 @@ -122,6 +122,7 @@ // 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]; @@ -144,8 +145,8 @@ [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); @@ -165,6 +166,7 @@ // 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; @@ -201,8 +203,9 @@ }]; // 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); @@ -227,6 +230,7 @@ CKReference* oldClassCKey = self.keychainZone.currentDatabase[classCPointerRecordID][SecCKRecordParentKeyRefKey]; [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID]; + [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self saveTLKMaterialToKeychain:self.keychainZoneID]; // Spin up CKKS subsystem. @@ -237,6 +241,7 @@ 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]; diff --git a/keychain/ckks/tests/CKKSTLKSharingEncryptionTests.m b/keychain/ckks/tests/CKKSTLKSharingEncryptionTests.m index a61d4997..665b5e04 100644 --- a/keychain/ckks/tests/CKKSTLKSharingEncryptionTests.m +++ b/keychain/ckks/tests/CKKSTLKSharingEncryptionTests.m @@ -178,26 +178,26 @@ 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; } @@ -282,16 +282,11 @@ [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 diff --git a/keychain/ckks/tests/CKKSTLKSharingTests.m b/keychain/ckks/tests/CKKSTLKSharingTests.m index 71ce090d..2639a5a9 100644 --- a/keychain/ckks/tests/CKKSTLKSharingTests.m +++ b/keychain/ckks/tests/CKKSTLKSharingTests.m @@ -50,7 +50,6 @@ - (void)setUp { [super setUp]; - SecCKKSSetShareTLKs(true); self.remotePeer1 = [[CKKSSOSSelfPeer alloc] initWithSOSPeerID:@"remote-peer1" encryptionKey:[[SFECKeyPair alloc] initRandomKeyPairWithSpecifier:[[SFECKeySpecifier alloc] initWithCurve:SFEllipticCurveNistp384]] @@ -75,8 +74,6 @@ self.untrustedPeer = nil; [super tearDown]; - - SecCKKSSetShareTLKs(false); } - (void)testAcceptExistingTLKSharedKeyHierarchy { @@ -292,7 +289,6 @@ // 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]; @@ -324,17 +320,34 @@ } - (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* 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]; @@ -489,7 +502,7 @@ // 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 @@ -531,6 +544,74 @@ [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... } diff --git a/keychain/ckks/tests/CKKSTests+API.h b/keychain/ckks/tests/CKKSTests+API.h index 50687728..42262a68 100644 --- a/keychain/ckks/tests/CKKSTests+API.h +++ b/keychain/ckks/tests/CKKSTests+API.h @@ -25,25 +25,29 @@ #import #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 diff --git a/keychain/ckks/tests/CKKSTests+API.m b/keychain/ckks/tests/CKKSTests+API.m index 2cf4f790..48af3b78 100644 --- a/keychain/ckks/tests/CKKSTests+API.m +++ b/keychain/ckks/tests/CKKSTests+API.m @@ -164,9 +164,9 @@ [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]; @@ -180,6 +180,7 @@ (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"]; @@ -201,6 +202,35 @@ 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", @@ -219,6 +249,10 @@ [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]; } @@ -226,11 +260,13 @@ // 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 = [@{ @@ -291,11 +327,10 @@ } - (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]; @@ -318,6 +353,7 @@ (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"); @@ -336,9 +372,9 @@ 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"); @@ -351,7 +387,7 @@ [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]; @@ -374,6 +410,7 @@ (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"); @@ -419,9 +456,9 @@ 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"); @@ -458,6 +495,7 @@ (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"); @@ -466,8 +504,8 @@ [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"); @@ -644,6 +682,7 @@ -(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. @@ -685,13 +724,15 @@ [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{ @@ -705,7 +746,7 @@ 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); }]; @@ -718,6 +759,7 @@ }]; // 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]; @@ -735,6 +777,7 @@ -(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. @@ -792,7 +835,7 @@ // 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"]]; @@ -880,10 +923,6 @@ // 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]; @@ -935,12 +974,15 @@ }]; [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''"); @@ -953,6 +995,8 @@ }]; [self waitForExpectations:@[callbackOccurs] timeout:5.0]; + + OCMVerifyAllWithDelay(self.mockDatabase, 8); } @end diff --git a/keychain/ckks/tests/CKKSTests+CurrentPointerAPI.m b/keychain/ckks/tests/CKKSTests+CurrentPointerAPI.m index fe852f6d..4f2aff99 100644 --- a/keychain/ckks/tests/CKKSTests+CurrentPointerAPI.m +++ b/keychain/ckks/tests/CKKSTests+CurrentPointerAPI.m @@ -38,6 +38,7 @@ #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" @@ -59,18 +60,20 @@ }); [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]; } @@ -84,6 +87,7 @@ [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; @@ -153,6 +157,7 @@ // 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", @@ -162,6 +167,7 @@ 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]; @@ -906,6 +912,110 @@ 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 diff --git a/keychain/ckks/tests/CKKSTests.h b/keychain/ckks/tests/CKKSTests.h index 8fe3be4f..87f694b5 100644 --- a/keychain/ckks/tests/CKKSTests.h +++ b/keychain/ckks/tests/CKKSTests.h @@ -22,28 +22,30 @@ */ #import -#import #import +#import #include -#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 @@ -51,3 +53,4 @@ @interface CloudKitKeychainSyncingTests : CloudKitKeychainSyncingTestsBase @end +NS_ASSUME_NONNULL_END diff --git a/keychain/ckks/tests/CKKSTests.m b/keychain/ckks/tests/CKKSTests.m index d0d7b3b0..c1893a40 100644 --- a/keychain/ckks/tests/CKKSTests.m +++ b/keychain/ckks/tests/CKKSTests.m @@ -30,6 +30,8 @@ #import #include +#include +#include #import "keychain/ckks/tests/CloudKitMockXCTest.h" #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h" @@ -75,6 +77,8 @@ 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"); @@ -96,7 +100,7 @@ NSDictionary* status = [self.keychainView status]; (void)status; - [self.keychainView cancelAllOperations]; + [self.keychainView halt]; [self.keychainView waitUntilAllOperationsAreFinished]; self.keychainView = nil; @@ -119,6 +123,13 @@ #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. @@ -126,6 +137,7 @@ [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"]; @@ -171,6 +183,7 @@ - (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]; @@ -264,9 +277,9 @@ // 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: ^{ @@ -286,8 +299,6 @@ 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]]; @@ -375,6 +386,68 @@ [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. @@ -660,6 +733,7 @@ [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. @@ -681,7 +755,9 @@ __block NSString* itemUUID = nil; [self.keychainView dispatchSync:^bool { NSError* error = nil; - NSArray* uuids = [CKKSOutgoingQueueEntry allUUIDs:&error]; + NSArray* 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]; @@ -881,9 +957,7 @@ [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]; @@ -900,6 +974,53 @@ 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; @@ -908,9 +1029,7 @@ [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]; @@ -921,6 +1040,7 @@ [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. @@ -930,6 +1050,28 @@ 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]; @@ -945,8 +1087,7 @@ 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. @@ -988,12 +1129,13 @@ // 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); @@ -1021,19 +1163,16 @@ // 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"]]; @@ -1041,19 +1180,15 @@ 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) @@ -1072,11 +1207,14 @@ 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 @@ -1104,10 +1242,11 @@ - (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]; @@ -1124,9 +1263,9 @@ [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]]; @@ -1150,9 +1289,31 @@ [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. @@ -1172,6 +1333,7 @@ [self waitForCKModifications]; [self rollFakeKeyHierarchyInCloudKit:self.keychainZoneID]; + [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self saveTLKMaterialToKeychain:self.keychainZoneID]; // Trigger a notification @@ -1223,6 +1385,7 @@ - (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]; @@ -1250,6 +1413,7 @@ [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); @@ -1292,6 +1456,7 @@ // 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]; @@ -1316,7 +1481,7 @@ 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... @@ -1339,10 +1504,20 @@ 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... @@ -1379,12 +1554,10 @@ // 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. @@ -1400,6 +1573,7 @@ // 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"]; @@ -1420,7 +1594,8 @@ // 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 @@ -1549,6 +1724,94 @@ 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. @@ -1630,8 +1893,8 @@ [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]; @@ -1639,7 +1902,7 @@ 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 { @@ -1734,6 +1997,12 @@ [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"); @@ -1760,9 +2029,14 @@ [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"); @@ -1816,6 +2090,7 @@ OCMVerifyAllWithDelay(self.mockDatabase, 8); // Now the TLKs arrive from the other device... + [self expectCKKSTLKSelfShareUpload:self.keychainZoneID]; [self saveTLKMaterialToKeychainSimulatingSOS:self.keychainZoneID]; [self.keychainView waitForKeyHierarchyReadiness]; @@ -1844,7 +2119,7 @@ [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]; @@ -1867,9 +2142,9 @@ [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); } @@ -1897,11 +2172,12 @@ 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 @@ -2048,10 +2324,10 @@ // 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); } @@ -2069,6 +2345,7 @@ [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 @@ -2105,6 +2382,7 @@ [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 @@ -2121,6 +2399,7 @@ [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 @@ -2155,6 +2434,7 @@ - (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 @@ -2233,6 +2513,7 @@ [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 @@ -2271,6 +2552,7 @@ [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 @@ -2319,6 +2601,7 @@ [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 @@ -2446,6 +2729,9 @@ [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); @@ -2463,6 +2749,9 @@ 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]; @@ -2479,6 +2768,8 @@ [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]; @@ -2497,7 +2788,8 @@ [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 @@ -2516,6 +2808,9 @@ 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... @@ -2528,7 +2823,8 @@ [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 @@ -2547,6 +2843,9 @@ 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); } @@ -2556,10 +2855,10 @@ 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"]; @@ -2591,7 +2890,7 @@ [self.keychainView waitUntilAllOperationsAreFinished]; [self waitForCKModifications]; - [self.keychainView cancelAllOperations]; + [self.keychainView halt]; [partialKVMock stopMocking]; } @@ -2615,7 +2914,7 @@ 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"); @@ -2653,7 +2952,7 @@ 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"); @@ -2742,6 +3041,9 @@ - (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]} @@ -2755,7 +3057,7 @@ 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"); @@ -2769,10 +3071,7 @@ } 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); } @@ -2849,7 +3148,7 @@ __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"); @@ -2891,7 +3190,7 @@ __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"); @@ -2920,8 +3219,6 @@ [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"); @@ -2956,6 +3253,25 @@ 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"); diff --git a/keychain/ckks/tests/CloudKitKeychainSyncingFixupTests.m b/keychain/ckks/tests/CloudKitKeychainSyncingFixupTests.m index 33004bf7..a8f82b94 100644 --- a/keychain/ckks/tests/CloudKitKeychainSyncingFixupTests.m +++ b/keychain/ckks/tests/CloudKitKeychainSyncingFixupTests.m @@ -74,7 +74,7 @@ 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]; @@ -122,7 +122,7 @@ [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 @@ -151,9 +151,7 @@ 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"; @@ -209,7 +207,6 @@ } - (void)testFixupFetchAllTLKShareRecords { - SecCKKSSetShareTLKs(true); // In 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. @@ -223,7 +220,7 @@ 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 @@ -256,9 +253,9 @@ 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); @@ -273,8 +270,84 @@ XCTAssertNotNil(localshare, "Should be able to find a new TLKShare record in database"); return true; }]; +} + +- (void)testFixupLocalReload { + // In 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 diff --git a/keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h b/keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h index 65a39adc..b6992f58 100644 --- a/keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h +++ b/keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h @@ -24,6 +24,9 @@ #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; @@ -42,82 +45,93 @@ @property CKKSControl* ckksControl; -@property id mockCKKSKey; +@property (nullable) id mockCKKSKey; -@property id currentSelfPeer; -@property NSMutableSet>* currentPeers; +@property (nullable) id currentSelfPeer; +@property (nullable) NSMutableSet>* currentPeers; -@property NSMutableDictionary* keys; +@property NSMutableSet* ckksZones; +@property (nullable) NSMutableDictionary* 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*) SOSPiggyICloudIdentities; +- (NSMutableArray*)SOSPiggyICloudIdentities; - (void)putTLKShareInCloudKit:(CKKSKey*)key from:(CKKSSOSSelfPeer*)sharingPeer to:(id)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 diff --git a/keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.m b/keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.m index 1aaeecce..63ba5409 100644 --- a/keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.m +++ b/keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.m @@ -95,25 +95,28 @@ 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" @@ -189,16 +192,25 @@ 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]; @@ -409,7 +421,9 @@ static CFDictionaryRef SOSCreatePeerGestaltFromName(CFStringRef name) - (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"); } @@ -488,87 +502,88 @@ static CFDictionaryRef SOSCreatePeerGestaltFromName(CFStringRef name) } - (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*)expectedRecordTypeCounts + deletedRecordTypeCounts:(NSDictionary*)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* 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* 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 { @@ -806,7 +821,7 @@ static CFDictionaryRef SOSCreatePeerGestaltFromName(CFStringRef name) 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 @@ -818,7 +833,13 @@ static CFDictionaryRef SOSCreatePeerGestaltFromName(CFStringRef name) 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; } @@ -943,6 +964,10 @@ static CFDictionaryRef SOSCreatePeerGestaltFromName(CFStringRef name) }]; } +- (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", diff --git a/keychain/ckks/tests/CloudKitMockXCTest.h b/keychain/ckks/tests/CloudKitMockXCTest.h index ea3519f0..59dba40a 100644 --- a/keychain/ckks/tests/CloudKitMockXCTest.h +++ b/keychain/ckks/tests/CloudKitMockXCTest.h @@ -21,14 +21,16 @@ * @APPLE_LICENSE_HEADER_END@ */ -#import #import #import +#import #import -#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; @@ -39,19 +41,18 @@ @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; @@ -61,57 +62,59 @@ @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* zones; +@property (nullable) NSMutableDictionary* 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*) expectedRecordTypeCounts - deletedRecordTypeCounts:(NSDictionary*) expectedDeletedRecordTypeCounts - zoneID:(CKRecordZoneID*) zoneID - checkModifiedRecord:(BOOL (^)(CKRecord*)) checkRecord - runAfterModification:(void (^) ())afterModification; +- (void)expectCKModifyRecords:(NSDictionary*)expectedRecordTypeCounts + deletedRecordTypeCounts:(NSDictionary* _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; @@ -123,10 +126,10 @@ // 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; @@ -134,9 +137,6 @@ // 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; @@ -144,22 +144,23 @@ - (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*)failedRecords; +- (void)rejectWrite:(CKModifyRecordsOperation*)op failedRecords:(NSMutableDictionary*)failedRecords; -#endif // OCTAGON @end + +NS_ASSUME_NONNULL_END diff --git a/keychain/ckks/tests/CloudKitMockXCTest.m b/keychain/ckks/tests/CloudKitMockXCTest.m index e0af138d..56a59241 100644 --- a/keychain/ckks/tests/CloudKitMockXCTest.m +++ b/keychain/ckks/tests/CloudKitMockXCTest.m @@ -83,6 +83,10 @@ - (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); @@ -95,6 +99,7 @@ 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); @@ -102,18 +107,21 @@ 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"); }]; @@ -161,7 +169,7 @@ 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)) { @@ -246,16 +254,6 @@ 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 @@ -267,8 +265,7 @@ 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]); @@ -278,10 +275,7 @@ [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); @@ -291,6 +285,14 @@ 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; } @@ -380,16 +382,6 @@ - (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 { @@ -544,8 +536,10 @@ 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; } @@ -638,12 +632,11 @@ [zone deleteCKRecordIDFromZone: recordID]; } - op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil); - if(afterModification) { afterModification(); } + op.modifyRecordsCompletionBlock(savedRecords, op.recordIDsToDelete, nil); op.isFinished = YES; } }]; @@ -840,20 +833,28 @@ } - (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]; @@ -888,13 +889,15 @@ [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(); } diff --git a/keychain/ckks/tests/MockCloudKit.h b/keychain/ckks/tests/MockCloudKit.h index 9b96a083..5bc654fd 100644 --- a/keychain/ckks/tests/MockCloudKit.h +++ b/keychain/ckks/tests/MockCloudKit.h @@ -22,11 +22,11 @@ */ -#import #import +#import -#import "keychain/ckks/CloudKitDependencies.h" #import "keychain/ckks/CKKSNotifier.h" +#import "keychain/ckks/CloudKitDependencies.h" NS_ASSUME_NONNULL_BEGIN @@ -38,21 +38,20 @@ typedef NSMutableDictionary FakeCKDatabase; @interface FakeCKModifyRecordZonesOperation : NSBlockOperation @property (nullable) NSError* creationError; -@property (nonatomic, nullable) NSMutableArray* recordZonesSaved; -@property (nonatomic, nullable) NSMutableArray* recordZoneIDsDeleted; -+(FakeCKDatabase*) ckdb; +@property (nonatomic, nullable) NSMutableArray* recordZonesSaved; +@property (nonatomic, nullable) NSMutableArray* recordZoneIDsDeleted; ++ (FakeCKDatabase*)ckdb; @end @interface FakeCKModifySubscriptionsOperation : NSBlockOperation @property (nullable) NSError* subscriptionError; -@property (nonatomic, nullable) NSMutableArray *subscriptionsSaved; -@property (nonatomic, nullable) NSMutableArray *subscriptionIDsDeleted; -+(FakeCKDatabase*) ckdb; +@property (nonatomic, nullable) NSMutableArray* subscriptionsSaved; +@property (nonatomic, nullable) NSMutableArray* subscriptionIDsDeleted; ++ (FakeCKDatabase*)ckdb; @end - @interface FakeCKFetchRecordZoneChangesOperation : NSOperation -+(FakeCKDatabase*) ckdb; ++ (FakeCKDatabase*)ckdb; @property (nullable) void (^blockAfterFetch)(); @end @@ -64,24 +63,21 @@ typedef NSMutableDictionary FakeCKDatabase; + (FakeCKDatabase*)ckdb; @end - @interface FakeAPSConnection : NSObject @end - -@interface FakeNSNotificationCenter : NSObject +@interface FakeNSNotificationCenter : NSObject + (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* currentDatabase; -@property NSMutableDictionary*>* pastDatabases; -@property bool flag; // used however you'd like in a test +@property NSMutableDictionary*>* 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; @@ -92,25 +88,28 @@ typedef NSMutableDictionary FakeCKDatabase; // 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 diff --git a/keychain/ckks/tests/MockCloudKit.m b/keychain/ckks/tests/MockCloudKit.m index 8a06f416..abaada93 100644 --- a/keychain/ckks/tests/MockCloudKit.m +++ b/keychain/ckks/tests/MockCloudKit.m @@ -525,6 +525,9 @@ - (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 { @@ -566,6 +569,14 @@ [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 -- 2.47.2