2 * Copyright (c) 2018 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
30 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
31 #import "keychain/ckks/tests/CloudKitKeychainSyncingMockXCTest.h"
32 #import "keychain/ckks/CKKS.h"
33 #import "keychain/ckks/CKKSViewManager.h"
35 #import "keychain/ckks/tests/MockCloudKit.h"
36 #import "keychain/ckks/tests/CKKSTests.h"
39 @interface CKKSLockStateTracker ()
40 @property (nullable) NSDate* lastUnlockedTime;
44 @interface CloudKitKeychainSyncingDeviceStateUploadTests : CloudKitKeychainSyncingTestsBase
47 @implementation CloudKitKeychainSyncingDeviceStateUploadTests
49 - (void)testDeviceStateUploadGood {
50 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
52 [self startCKKSSubsystem];
53 [self.keychainView waitForKeyHierarchyReadiness];
55 __weak __typeof(self) weakSelf = self;
56 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
57 deletedRecordTypeCounts:nil
58 zoneID:self.keychainZoneID
59 checkModifiedRecord: ^BOOL (CKRecord* record){
60 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
61 // Check that all the things matches
62 __strong __typeof(weakSelf) strongSelf = weakSelf;
63 XCTAssertNotNil(strongSelf, "self exists");
65 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
66 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
68 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
69 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
70 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
72 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
73 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
74 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
76 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
77 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
78 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
84 runAfterModification:nil];
86 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
88 OCMVerifyAllWithDelay(self.mockDatabase, 8);
91 - (void)testDeviceStateUploadRateLimited {
92 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
94 [self startCKKSSubsystem];
95 [self.keychainView waitForKeyHierarchyReadiness];
97 __weak __typeof(self) weakSelf = self;
98 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
99 deletedRecordTypeCounts:nil
100 zoneID:self.keychainZoneID
101 checkModifiedRecord: ^BOOL (CKRecord* record){
102 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
103 // Check that all the things matches
104 __strong __typeof(weakSelf) strongSelf = weakSelf;
105 XCTAssertNotNil(strongSelf, "self exists");
107 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
108 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
110 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
111 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
112 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
114 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
115 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
116 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
118 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
119 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
120 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
126 runAfterModification:nil];
128 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
129 OCMVerifyAllWithDelay(self.mockDatabase, 8);
130 [op waitUntilFinished];
132 // Check that an immediate rate-limited retry doesn't upload anything
133 op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
134 [op waitUntilFinished];
136 // But not rate-limiting works just fine!
137 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
138 deletedRecordTypeCounts:nil
139 zoneID:self.keychainZoneID
140 checkModifiedRecord:nil
141 runAfterModification:nil];
142 op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
143 OCMVerifyAllWithDelay(self.mockDatabase, 8);
144 [op waitUntilFinished];
146 // And now, if the update is old enough, that'll work too
147 [self.keychainView dispatchSync:^bool {
148 NSError* error = nil;
149 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:self.accountStateTracker.ckdeviceID zoneID:self.keychainZoneID error:&error];
150 XCTAssertNil(error, "No error fetching device state entry");
151 XCTAssertNotNil(cdse, "Fetched device state entry");
153 CKRecord* record = cdse.storedCKRecord;
155 NSDate* m = record.modificationDate;
156 XCTAssertNotNil(m, "Have modification date");
159 NSDateComponents* offset = [[NSDateComponents alloc] init];
160 [offset setHour:-4 * 24];
161 NSDate* m2 = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:m options:0];
163 XCTAssertNotNil(m2, "Made modification date");
165 record.modificationDate = m2;
166 [cdse setStoredCKRecord:record];
168 [cdse saveToDatabase:&error];
169 XCTAssertNil(error, "No error saving device state entry");
174 // And now the rate-limiting doesn't get in the way
175 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
176 deletedRecordTypeCounts:nil
177 zoneID:self.keychainZoneID
178 checkModifiedRecord:nil
179 runAfterModification:nil];
180 op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
181 OCMVerifyAllWithDelay(self.mockDatabase, 12);
182 [op waitUntilFinished];
185 - (void)testDeviceStateUploadRateLimitedAfterNormalUpload {
186 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
188 [self startCKKSSubsystem];
189 [self.keychainView waitForKeyHierarchyReadiness];
191 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
192 [self addGenericPassword:@"password" account:@"account-delete-me"];
193 OCMVerifyAllWithDelay(self.mockDatabase, 8);
195 // Check that an immediate rate-limited retry doesn't upload anything
196 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
197 [op waitUntilFinished];
200 - (void)testDeviceStateUploadWaitsForKeyHierarchyReady {
201 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
203 // Ask to wait for quite a while if we don't become ready
204 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:20*NSEC_PER_SEC ckoperationGroup:nil];
206 __weak __typeof(self) weakSelf = self;
207 // Expect a ready upload
208 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
209 deletedRecordTypeCounts:nil
210 zoneID:self.keychainZoneID
211 checkModifiedRecord: ^BOOL (CKRecord* record){
212 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
213 __strong __typeof(weakSelf) strongSelf = weakSelf;
214 XCTAssertNotNil(strongSelf, "self exists");
216 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
217 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
219 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
220 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
221 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
223 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
224 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
225 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
227 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
228 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
229 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
235 runAfterModification:nil];
237 // And allow the key state to progress
238 [self startCKKSSubsystem];
239 OCMVerifyAllWithDelay(self.mockDatabase, 8);
242 - (void)testDeviceStateUploadWaitsForKeyHierarchyWaitForTLK {
243 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
244 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
245 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
247 // Ask to wait for the key state to enter a state if we don't become ready
248 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:20*NSEC_PER_SEC ckoperationGroup:nil];
250 __weak __typeof(self) weakSelf = self;
251 // Expect a waitfortlk upload
252 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
253 deletedRecordTypeCounts:nil
254 zoneID:self.keychainZoneID
255 checkModifiedRecord: ^BOOL (CKRecord* record){
256 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
257 __strong __typeof(weakSelf) strongSelf = weakSelf;
258 XCTAssertNotNil(strongSelf, "self exists");
260 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
261 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
263 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
264 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
265 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
267 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID should matche what we gave it");
268 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device should be in circle");
269 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device should be in waitfortlk");
271 XCTAssertNil([record[SecCKRecordCurrentTLK] recordID].recordName, "Should have no TLK uuid");
272 XCTAssertNil([record[SecCKRecordCurrentClassA] recordID].recordName, "Should have no class A uuid");
273 XCTAssertNil([record[SecCKRecordCurrentClassC] recordID].recordName, "Should have no class C uuid");
279 runAfterModification:nil];
281 // And allow the key state to progress
282 [self startCKKSSubsystem];
283 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
284 OCMVerifyAllWithDelay(self.mockDatabase, 8);
287 - (void)testDeviceStateReceive {
288 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
290 ZoneKeys* zoneKeys = self.keys[self.keychainZoneID];
291 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", self.keychainZoneID);
293 [self startCKKSSubsystem];
294 [self.keychainView waitForKeyHierarchyReadiness];
296 NSDate* date = [[NSCalendar currentCalendar] startOfDayForDate:[NSDate date]];
297 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:@"otherdevice"
298 osVersion:@"fake-version"
300 circlePeerID:@"asdfasdf"
301 circleStatus:kSOSCCInCircle
302 keyState:SecCKKSZoneKeyStateReady
303 currentTLKUUID:zoneKeys.tlk.uuid
304 currentClassAUUID:zoneKeys.classA.uuid
305 currentClassCUUID:zoneKeys.classC.uuid
306 zoneID:self.keychainZoneID
307 encodedCKRecord:nil];
308 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
309 [self.keychainZone addToZone:record];
311 CKKSDeviceStateEntry* oldcdse = [[CKKSDeviceStateEntry alloc] initForDevice:@"olderotherdevice"
312 osVersion:nil // old-style, no OSVersion or lastUnlockTime
314 circlePeerID:@"olderasdfasdf"
315 circleStatus:kSOSCCInCircle
316 keyState:SecCKKSZoneKeyStateReady
317 currentTLKUUID:zoneKeys.tlk.uuid
318 currentClassAUUID:zoneKeys.classA.uuid
319 currentClassCUUID:zoneKeys.classC.uuid
320 zoneID:self.keychainZoneID
321 encodedCKRecord:nil];
322 [self.keychainZone addToZone:[oldcdse CKRecordWithZoneID:self.keychainZoneID]];
324 // Trigger a notification (with hilariously fake data)
325 [self.keychainView notifyZoneChange:nil];
326 [self.keychainView waitForFetchAndIncomingQueueProcessing];
328 [self.keychainView dispatchSync: ^bool {
329 NSError* error = nil;
330 NSArray<CKKSDeviceStateEntry*>* cdses = [CKKSDeviceStateEntry allInZone:self.keychainZoneID error:&error];
331 XCTAssertNil(error, "No error fetching CDSEs");
332 XCTAssertNotNil(cdses, "An array of CDSEs was returned");
333 XCTAssert(cdses.count >= 1u, "At least one CDSE came back");
335 CKKSDeviceStateEntry* item = nil;
336 CKKSDeviceStateEntry* olderotherdevice = nil;
337 for(CKKSDeviceStateEntry* dbcdse in cdses) {
338 if([dbcdse.device isEqualToString:@"otherdevice"]) {
340 } else if([dbcdse.device isEqualToString:@"olderotherdevice"]) {
341 olderotherdevice = dbcdse;
344 XCTAssertNotNil(item, "Found a cdse for otherdevice");
346 XCTAssertEqualObjects(cdse, item, "Saved item matches pre-cloudkit item");
348 XCTAssertEqualObjects(item.osVersion, @"fake-version", "correct osVersion");
349 XCTAssertEqualObjects(item.lastUnlockTime, date, "correct date");
350 XCTAssertEqualObjects(item.circlePeerID, @"asdfasdf", "correct peer id");
351 XCTAssertEqualObjects(item.keyState, SecCKKSZoneKeyStateReady, "correct key state");
352 XCTAssertEqualObjects(item.currentTLKUUID, zoneKeys.tlk.uuid, "correct tlk uuid");
353 XCTAssertEqualObjects(item.currentClassAUUID, zoneKeys.classA.uuid, "correct classA uuid");
354 XCTAssertEqualObjects(item.currentClassCUUID, zoneKeys.classC.uuid, "correct classC uuid");
357 XCTAssertNotNil(olderotherdevice, "Should have found a cdse for olderotherdevice");
358 XCTAssertEqualObjects(oldcdse, olderotherdevice, "Saved item should match pre-cloudkit item");
360 XCTAssertNil(olderotherdevice.osVersion, "osVersion should be nil");
361 XCTAssertNil(olderotherdevice.lastUnlockTime, "lastUnlockTime should be nil");
362 XCTAssertEqualObjects(olderotherdevice.circlePeerID, @"olderasdfasdf", "correct peer id");
363 XCTAssertEqualObjects(olderotherdevice.keyState, SecCKKSZoneKeyStateReady, "correct key state");
364 XCTAssertEqualObjects(olderotherdevice.currentTLKUUID, zoneKeys.tlk.uuid, "correct tlk uuid");
365 XCTAssertEqualObjects(olderotherdevice.currentClassAUUID, zoneKeys.classA.uuid, "correct classA uuid");
366 XCTAssertEqualObjects(olderotherdevice.currentClassCUUID, zoneKeys.classC.uuid, "correct classC uuid");
371 OCMVerifyAllWithDelay(self.mockDatabase, 8);
374 - (void)testDeviceStateUploadBadKeyState {
375 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
376 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
377 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
379 [self startCKKSSubsystem];
380 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
381 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
383 __weak __typeof(self) weakSelf = self;
384 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
385 deletedRecordTypeCounts:nil
386 zoneID:self.keychainZoneID
387 checkModifiedRecord: ^BOOL (CKRecord* record){
388 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
389 // Check that all the things matches
390 __strong __typeof(weakSelf) strongSelf = weakSelf;
391 XCTAssertNotNil(strongSelf, "self exists");
393 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
394 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
395 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
397 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
398 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
399 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
401 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
402 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
403 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
409 runAfterModification:nil];
411 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
413 OCMVerifyAllWithDelay(self.mockDatabase, 8);
416 - (void)testDeviceStateUploadWaitForUnlockKeyState {
417 // Starts with everything in keychain, but locked
418 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
419 [self saveTLKMaterialToKeychain:self.keychainZoneID];
420 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
422 NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
423 [dateComponents setDay:-3];
424 NSDate* threeDaysAgo = [[NSCalendar currentCalendar] dateByAddingComponents:dateComponents toDate:[NSDate date] options:0];
426 self.aksLockState = true;
427 [self.lockStateTracker recheck];
428 self.lockStateTracker.lastUnlockedTime = threeDaysAgo;
429 XCTAssertTrue([self.utcCalendar isDate:self.lockStateTracker.lastUnlockTime
430 equalToDate:threeDaysAgo
431 toUnitGranularity:NSCalendarUnitSecond],
432 "last unlock date (%@) similar to threeDaysAgo (%@)", self.lockStateTracker.lastUnlockTime, threeDaysAgo);
434 [self startCKKSSubsystem];
435 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:8*NSEC_PER_SEC], "CKKS entered waitforunlock");
437 __weak __typeof(self) weakSelf = self;
438 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
439 deletedRecordTypeCounts:nil
440 zoneID:self.keychainZoneID
441 checkModifiedRecord: ^BOOL (CKRecord* record){
442 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
443 // Check that all the things matches
444 __strong __typeof(weakSelf) strongSelf = weakSelf;
445 XCTAssertNotNil(strongSelf, "self exists");
447 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
448 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:threeDaysAgo toUnitGranularity:NSCalendarUnitDay],
449 "last unlock date (%@) similar to three days ago (%@)", record[SecCKSRecordLastUnlockTime], threeDaysAgo);
451 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
452 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
453 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForUnlock), "Device is in waitforunlock");
455 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
456 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
457 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
463 runAfterModification:nil];
465 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
467 OCMVerifyAllWithDelay(self.mockDatabase, 8);
470 - (void)testDeviceStateUploadBadKeyStateAfterRestart {
471 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
472 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
473 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
475 [self startCKKSSubsystem];
476 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
477 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
479 // And restart CKKS...
480 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
481 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:8*NSEC_PER_SEC], "CKKS entered waitfortlk");
482 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
484 __weak __typeof(self) weakSelf = self;
485 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
486 deletedRecordTypeCounts:nil
487 zoneID:self.keychainZoneID
488 checkModifiedRecord: ^BOOL (CKRecord* record){
489 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
490 // Check that all the things matches
491 __strong __typeof(weakSelf) strongSelf = weakSelf;
492 XCTAssertNotNil(strongSelf, "self exists");
494 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
495 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
496 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
498 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
499 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
500 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
502 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
503 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
504 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
510 runAfterModification:nil];
512 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
514 OCMVerifyAllWithDelay(self.mockDatabase, 8);
518 - (void)testDeviceStateUploadBadCircleState {
519 self.circleStatus = kSOSCCNotInCircle;
520 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
522 // This test has stuff in CloudKit, but no TLKs.
523 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
525 [self startCKKSSubsystem];
527 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateLoggedOut] wait:8*NSEC_PER_SEC], "CKKS entered logged out");
528 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateLoggedOut, "CKKS thinks it's logged out");
530 __weak __typeof(self) weakSelf = self;
531 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
532 deletedRecordTypeCounts:nil
533 zoneID:self.keychainZoneID
534 checkModifiedRecord: ^BOOL (CKRecord* record){
535 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
536 // Check that all the things matches
537 __strong __typeof(weakSelf) strongSelf = weakSelf;
538 XCTAssertNotNil(strongSelf, "self exists");
540 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
541 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
542 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
544 XCTAssertNil(record[SecCKRecordCirclePeerID], "no peer ID if device is not in circle");
545 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCNotInCircle], "device is not in circle");
546 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateLoggedOut), "Device is in keystate:loggedout");
548 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
549 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
550 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
556 runAfterModification:nil];
558 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
559 OCMVerifyAllWithDelay(self.mockDatabase, 8);
561 [op waitUntilFinished];
562 XCTAssertNil(op.error, "No error uploading 'out of circle' device state");
565 - (void)testDeviceStateUploadWithTardyNetworkAfterRestart {
566 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
567 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
568 [self saveTLKMaterialToKeychain:self.keychainZoneID];
569 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
571 [self holdCloudKitFetches];
573 [self startCKKSSubsystem];
575 // we should be stuck in fetch
576 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:8*NSEC_PER_SEC], "Key state should become fetch");
578 __weak __typeof(self) weakSelf = self;
579 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
580 deletedRecordTypeCounts:nil
581 zoneID:self.keychainZoneID
582 checkModifiedRecord: ^BOOL (CKRecord* record){
583 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
584 // Check that all the things matches
585 __strong __typeof(weakSelf) strongSelf = weakSelf;
586 XCTAssertNotNil(strongSelf, "self exists");
588 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
589 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
591 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
592 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
593 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
595 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.circlePeerID, "peer ID matches what we gave it");
596 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
597 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
599 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
600 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
601 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
607 runAfterModification:nil];
610 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:8*NSEC_PER_SEC ckoperationGroup:nil];
612 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateFetch, "CKKS re-entered fetch");
613 [self releaseCloudKitFetchHold];
615 OCMVerifyAllWithDelay(self.mockDatabase, 16);