]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSDeviceStateUploadTests.m
Security-58286.60.28.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSDeviceStateUploadTests.m
1 /*
2 * Copyright (c) 2018 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
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
11 * file.
12 *
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.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24 #if OCTAGON
25
26 #import <CloudKit/CloudKit.h>
27 #import <XCTest/XCTest.h>
28 #import <OCMock/OCMock.h>
29
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"
34
35 #import "keychain/ckks/tests/MockCloudKit.h"
36 #import "keychain/ckks/tests/CKKSTests.h"
37
38 // break abstraction
39 @interface CKKSLockStateTracker ()
40 @property (nullable) NSDate* lastUnlockedTime;
41 @end
42
43
44 @interface CloudKitKeychainSyncingDeviceStateUploadTests : CloudKitKeychainSyncingTestsBase
45 @end
46
47 @implementation CloudKitKeychainSyncingDeviceStateUploadTests
48
49 - (void)testDeviceStateUploadGood {
50 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
51
52 [self startCKKSSubsystem];
53 [self.keychainView waitForKeyHierarchyReadiness];
54
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");
64
65 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
66 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
67
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]);
71
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");
75
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");
79 return YES;
80 } else {
81 return NO;
82 }
83 }
84 runAfterModification:nil];
85
86 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
87
88 OCMVerifyAllWithDelay(self.mockDatabase, 8);
89 }
90
91 - (void)testDeviceStateUploadRateLimited {
92 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
93
94 [self startCKKSSubsystem];
95 [self.keychainView waitForKeyHierarchyReadiness];
96
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");
106
107 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
108 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
109
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]);
113
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");
117
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");
121 return YES;
122 } else {
123 return NO;
124 }
125 }
126 runAfterModification:nil];
127
128 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
129 OCMVerifyAllWithDelay(self.mockDatabase, 8);
130 [op waitUntilFinished];
131
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];
135
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];
145
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");
152
153 CKRecord* record = cdse.storedCKRecord;
154
155 NSDate* m = record.modificationDate;
156 XCTAssertNotNil(m, "Have modification date");
157
158 // Four days ago!
159 NSDateComponents* offset = [[NSDateComponents alloc] init];
160 [offset setHour:-4 * 24];
161 NSDate* m2 = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:m options:0];
162
163 XCTAssertNotNil(m2, "Made modification date");
164
165 record.modificationDate = m2;
166 [cdse setStoredCKRecord:record];
167
168 [cdse saveToDatabase:&error];
169 XCTAssertNil(error, "No error saving device state entry");
170
171 return true;
172 }];
173
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];
183 }
184
185 - (void)testDeviceStateUploadRateLimitedAfterNormalUpload {
186 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
187
188 [self startCKKSSubsystem];
189 [self.keychainView waitForKeyHierarchyReadiness];
190
191 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
192 [self addGenericPassword:@"password" account:@"account-delete-me"];
193 OCMVerifyAllWithDelay(self.mockDatabase, 8);
194
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];
198 }
199
200 - (void)testDeviceStateUploadWaitsForKeyHierarchyReady {
201 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
202
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];
205
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");
215
216 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
217 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
218
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]);
222
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");
226
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");
230 return YES;
231 } else {
232 return NO;
233 }
234 }
235 runAfterModification:nil];
236
237 // And allow the key state to progress
238 [self startCKKSSubsystem];
239 OCMVerifyAllWithDelay(self.mockDatabase, 8);
240 }
241
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];
246
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];
249
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");
259
260 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
261 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
262
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]);
266
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");
270
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");
274 return YES;
275 } else {
276 return NO;
277 }
278 }
279 runAfterModification:nil];
280
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);
285 }
286
287 - (void)testDeviceStateReceive {
288 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
289
290 ZoneKeys* zoneKeys = self.keys[self.keychainZoneID];
291 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", self.keychainZoneID);
292
293 [self startCKKSSubsystem];
294 [self.keychainView waitForKeyHierarchyReadiness];
295
296 NSDate* date = [[NSCalendar currentCalendar] startOfDayForDate:[NSDate date]];
297 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:@"otherdevice"
298 osVersion:@"fake-version"
299 lastUnlockTime:date
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];
310
311 CKKSDeviceStateEntry* oldcdse = [[CKKSDeviceStateEntry alloc] initForDevice:@"olderotherdevice"
312 osVersion:nil // old-style, no OSVersion or lastUnlockTime
313 lastUnlockTime:nil
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]];
323
324 // Trigger a notification (with hilariously fake data)
325 [self.keychainView notifyZoneChange:nil];
326 [self.keychainView waitForFetchAndIncomingQueueProcessing];
327
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");
334
335 CKKSDeviceStateEntry* item = nil;
336 CKKSDeviceStateEntry* olderotherdevice = nil;
337 for(CKKSDeviceStateEntry* dbcdse in cdses) {
338 if([dbcdse.device isEqualToString:@"otherdevice"]) {
339 item = dbcdse;
340 } else if([dbcdse.device isEqualToString:@"olderotherdevice"]) {
341 olderotherdevice = dbcdse;
342 }
343 }
344 XCTAssertNotNil(item, "Found a cdse for otherdevice");
345
346 XCTAssertEqualObjects(cdse, item, "Saved item matches pre-cloudkit item");
347
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");
355
356
357 XCTAssertNotNil(olderotherdevice, "Should have found a cdse for olderotherdevice");
358 XCTAssertEqualObjects(oldcdse, olderotherdevice, "Saved item should match pre-cloudkit item");
359
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");
367
368 return false;
369 }];
370
371 OCMVerifyAllWithDelay(self.mockDatabase, 8);
372 }
373
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];
378
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");
382
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");
392
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]);
396
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");
400
401 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
402 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
403 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
404 return YES;
405 } else {
406 return NO;
407 }
408 }
409 runAfterModification:nil];
410
411 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
412
413 OCMVerifyAllWithDelay(self.mockDatabase, 8);
414 }
415
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];
421
422 NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
423 [dateComponents setDay:-3];
424 NSDate* threeDaysAgo = [[NSCalendar currentCalendar] dateByAddingComponents:dateComponents toDate:[NSDate date] options:0];
425
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);
433
434 [self startCKKSSubsystem];
435 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:8*NSEC_PER_SEC], "CKKS entered waitforunlock");
436
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");
446
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);
450
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");
454
455 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
456 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
457 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
458 return YES;
459 } else {
460 return NO;
461 }
462 }
463 runAfterModification:nil];
464
465 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
466
467 OCMVerifyAllWithDelay(self.mockDatabase, 8);
468 }
469
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];
474
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");
478
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");
483
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");
493
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]);
497
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");
501
502 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
503 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
504 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
505 return YES;
506 } else {
507 return NO;
508 }
509 }
510 runAfterModification:nil];
511
512 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
513
514 OCMVerifyAllWithDelay(self.mockDatabase, 8);
515 }
516
517
518 - (void)testDeviceStateUploadBadCircleState {
519 self.circleStatus = kSOSCCNotInCircle;
520 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
521
522 // This test has stuff in CloudKit, but no TLKs.
523 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
524
525 [self startCKKSSubsystem];
526
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");
529
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");
539
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]);
543
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");
547
548 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
549 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
550 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
551 return YES;
552 } else {
553 return NO;
554 }
555 }
556 runAfterModification:nil];
557
558 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
559 OCMVerifyAllWithDelay(self.mockDatabase, 8);
560
561 [op waitUntilFinished];
562 XCTAssertNil(op.error, "No error uploading 'out of circle' device state");
563 }
564
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];
570
571 [self holdCloudKitFetches];
572
573 [self startCKKSSubsystem];
574
575 // we should be stuck in fetch
576 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:8*NSEC_PER_SEC], "Key state should become fetch");
577
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");
587
588 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
589 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
590
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]);
594
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");
598
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");
602 return YES;
603 } else {
604 return NO;
605 }
606 }
607 runAfterModification:nil];
608
609
610 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:8*NSEC_PER_SEC ckoperationGroup:nil];
611
612 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateFetch, "CKKS re-entered fetch");
613 [self releaseCloudKitFetchHold];
614
615 OCMVerifyAllWithDelay(self.mockDatabase, 16);
616 }
617
618
619
620 @end
621
622 #endif // OCTAGON