]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSDeviceStateUploadTests.m
Security-59306.140.5.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)testDeviceStateUploadGoodSOSOnly {
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.mockSOSAdapter.selfPeer.peerID, "peer ID matches what we gave it");
73 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
74
75 XCTAssertNil(record[SecCKRecordOctagonPeerID], "octagon peer ID should be missing");
76 XCTAssertNil(record[SecCKRecordOctagonStatus], "octagon status should be missing");
77
78 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
79
80 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
81 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
82 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
83 return YES;
84 } else {
85 return NO;
86 }
87 }
88 runAfterModification:nil];
89
90 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
91
92 OCMVerifyAllWithDelay(self.mockDatabase, 20);
93 }
94
95 - (void)testDeviceStateUploadRateLimited {
96 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
97
98 [self startCKKSSubsystem];
99 [self.keychainView waitForKeyHierarchyReadiness];
100
101 __weak __typeof(self) weakSelf = self;
102 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
103 deletedRecordTypeCounts:nil
104 zoneID:self.keychainZoneID
105 checkModifiedRecord: ^BOOL (CKRecord* record){
106 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
107 // Check that all the things matches
108 __strong __typeof(weakSelf) strongSelf = weakSelf;
109 XCTAssertNotNil(strongSelf, "self exists");
110
111 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
112 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
113
114 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
115 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
116 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
117
118 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.mockSOSAdapter.selfPeer.peerID, "peer ID matches what we gave it");
119 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
120
121 XCTAssertNil(record[SecCKRecordOctagonPeerID], "octagon peer ID should be missing");
122 XCTAssertNil(record[SecCKRecordOctagonStatus], "octagon status should be missing");
123
124 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
125
126 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
127 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
128 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
129 return YES;
130 } else {
131 return NO;
132 }
133 }
134 runAfterModification:nil];
135
136 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
137 OCMVerifyAllWithDelay(self.mockDatabase, 20);
138 [op waitUntilFinished];
139
140 // Check that an immediate rate-limited retry doesn't upload anything
141 op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
142 [op waitUntilFinished];
143
144 // But not rate-limiting works just fine!
145 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
146 deletedRecordTypeCounts:nil
147 zoneID:self.keychainZoneID
148 checkModifiedRecord:nil
149 runAfterModification:nil];
150 op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
151 OCMVerifyAllWithDelay(self.mockDatabase, 20);
152 [op waitUntilFinished];
153
154 // And now, if the update is old enough, that'll work too
155 [self.keychainView dispatchSync:^bool {
156 NSError* error = nil;
157 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:self.accountStateTracker.ckdeviceID zoneID:self.keychainZoneID error:&error];
158 XCTAssertNil(error, "No error fetching device state entry");
159 XCTAssertNotNil(cdse, "Fetched device state entry");
160
161 CKRecord* record = cdse.storedCKRecord;
162
163 NSDate* m = record.modificationDate;
164 XCTAssertNotNil(m, "Have modification date");
165
166 // Four days ago!
167 NSDateComponents* offset = [[NSDateComponents alloc] init];
168 [offset setHour:-4 * 24];
169 NSDate* m2 = [[NSCalendar currentCalendar] dateByAddingComponents:offset toDate:m options:0];
170
171 XCTAssertNotNil(m2, "Made modification date");
172
173 record.modificationDate = m2;
174 [cdse setStoredCKRecord:record];
175
176 [cdse saveToDatabase:&error];
177 XCTAssertNil(error, "No error saving device state entry");
178
179 return true;
180 }];
181
182 // And now the rate-limiting doesn't get in the way
183 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
184 deletedRecordTypeCounts:nil
185 zoneID:self.keychainZoneID
186 checkModifiedRecord:nil
187 runAfterModification:nil];
188 op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
189 OCMVerifyAllWithDelay(self.mockDatabase, 20);
190 [op waitUntilFinished];
191 }
192
193 - (void)testDeviceStateDoNotUploadIfNoDeviceID {
194 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
195
196 [self startCKKSSubsystem];
197 [self.keychainView waitForKeyHierarchyReadiness];
198
199 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: @1}
200 deletedRecordTypeCounts:nil
201 zoneID:self.keychainZoneID
202 checkModifiedRecord: ^BOOL (CKRecord* record) {
203 return [record.recordType isEqualToString: SecCKRecordDeviceStateType];
204 }
205 runAfterModification:nil];
206
207 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
208 OCMVerifyAllWithDelay(self.mockDatabase, 20);
209 [op waitUntilFinished];
210
211 // Device ID goes away
212 NSString* oldDeviceID = self.accountStateTracker.ckdeviceID;
213 self.accountStateTracker.ckdeviceID = nil;
214
215 [self.keychainView dispatchSync:^bool {
216 NSError* error = nil;
217 CKKSDeviceStateEntry* cdse = [CKKSDeviceStateEntry fromDatabase:oldDeviceID zoneID:self.keychainZoneID error:&error];
218 XCTAssertNil(error, "No error fetching device state entry");
219 XCTAssertNotNil(cdse, "Fetched device state entry");
220
221 CKRecord* record = cdse.storedCKRecord;
222
223 NSDate* m = record.modificationDate;
224 XCTAssertNotNil(m, "Have modification date");
225
226 return true;
227 }];
228
229 // It shouldn't try to upload a new CDSE; there's no device ID
230 op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
231 [op waitUntilFinished];
232
233 // And add a new keychain item, and expect it to sync, but without a device state
234 [self expectCKModifyRecords:@{SecCKRecordItemType: @1,
235 SecCKRecordCurrentKeyType: @1,
236 }
237 deletedRecordTypeCounts:@{}
238 zoneID:self.keychainZoneID
239 checkModifiedRecord: ^BOOL (CKRecord* record){
240 return YES;
241 }
242 runAfterModification:nil];
243
244 [self addGenericPassword: @"data" account: @"account-delete-me"];
245 OCMVerifyAllWithDelay(self.mockDatabase, 20);
246 }
247
248 // Note that CKKS shouldn't even be functioning in SA, but pretend that it is
249 - (void)testDeviceStateDoNotUploadIfSAAccount {
250 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
251
252 [self startCKKSSubsystem];
253 [self.keychainView waitForKeyHierarchyReadiness];
254
255 [self expectCKModifyRecords:@{SecCKRecordDeviceStateType: @1}
256 deletedRecordTypeCounts:nil
257 zoneID:self.keychainZoneID
258 checkModifiedRecord: ^BOOL (CKRecord* record) {
259 return [record.recordType isEqualToString: SecCKRecordDeviceStateType];
260 }
261 runAfterModification:nil];
262
263 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
264 OCMVerifyAllWithDelay(self.mockDatabase, 20);
265 [op waitUntilFinished];
266
267 // The account downgrades, I guess?
268
269 self.fakeHSA2AccountStatus = CKKSAccountStatusNoAccount;
270 [self.accountStateTracker setHSA2iCloudAccountStatus:self.fakeHSA2AccountStatus];
271
272 // It shouldn't try to upload a new CDSE; the account is SA
273 op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
274 [op waitUntilFinished];
275
276 // And add a new keychain item, and expect it to sync, but without a device state
277 [self expectCKModifyRecords:@{SecCKRecordItemType: @1,
278 SecCKRecordCurrentKeyType: @1,
279 }
280 deletedRecordTypeCounts:@{}
281 zoneID:self.keychainZoneID
282 checkModifiedRecord: ^BOOL (CKRecord* record){
283 return YES;
284 }
285 runAfterModification:nil];
286
287 [self addGenericPassword: @"data" account: @"account-delete-me"];
288 OCMVerifyAllWithDelay(self.mockDatabase, 20);
289 }
290
291 - (void)testDeviceStateUploadRateLimitedAfterNormalUpload {
292 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
293
294 [self startCKKSSubsystem];
295 [self.keychainView waitForKeyHierarchyReadiness];
296
297 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
298 [self addGenericPassword:@"password" account:@"account-delete-me"];
299 OCMVerifyAllWithDelay(self.mockDatabase, 20);
300
301 // Check that an immediate rate-limited retry doesn't upload anything
302 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:true waitForKeyHierarchyInitialization:2*NSEC_PER_SEC ckoperationGroup:nil];
303 [op waitUntilFinished];
304 }
305
306 - (void)testDeviceStateUploadWaitsForKeyHierarchyReady {
307 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
308
309 // Ask to wait for quite a while if we don't become ready
310 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:20*NSEC_PER_SEC ckoperationGroup:nil];
311
312 __weak __typeof(self) weakSelf = self;
313 // Expect a ready upload
314 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
315 deletedRecordTypeCounts:nil
316 zoneID:self.keychainZoneID
317 checkModifiedRecord: ^BOOL (CKRecord* record){
318 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
319 __strong __typeof(weakSelf) strongSelf = weakSelf;
320 XCTAssertNotNil(strongSelf, "self exists");
321
322 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
323 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
324
325 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
326 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
327 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
328
329 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.mockSOSAdapter.selfPeer.peerID, "peer ID matches what we gave it");
330 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
331
332 XCTAssertNil(record[SecCKRecordOctagonPeerID], "octagon peer ID should be missing");
333 XCTAssertNil(record[SecCKRecordOctagonStatus], "octagon status should be missing");
334
335 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
336
337 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
338 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
339 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
340 return YES;
341 } else {
342 return NO;
343 }
344 }
345 runAfterModification:nil];
346
347 // And allow the key state to progress
348 [self startCKKSSubsystem];
349 OCMVerifyAllWithDelay(self.mockDatabase, 20);
350 }
351
352 - (void)testDeviceStateUploadWaitsForKeyHierarchyWaitForTLK {
353 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
354 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
355 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
356
357 // Ask to wait for the key state to enter a state if we don't become ready
358 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:20*NSEC_PER_SEC ckoperationGroup:nil];
359
360 __weak __typeof(self) weakSelf = self;
361 // Expect a waitfortlk upload
362 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
363 deletedRecordTypeCounts:nil
364 zoneID:self.keychainZoneID
365 checkModifiedRecord: ^BOOL (CKRecord* record){
366 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
367 __strong __typeof(weakSelf) strongSelf = weakSelf;
368 XCTAssertNotNil(strongSelf, "self exists");
369
370 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
371 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
372
373 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
374 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
375 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
376
377 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.mockSOSAdapter.selfPeer.peerID, "peer ID should matche what we gave it");
378 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device should be in circle");
379
380 XCTAssertNil(record[SecCKRecordOctagonPeerID], "octagon peer ID should be missing");
381 XCTAssertNil(record[SecCKRecordOctagonStatus], "octagon status should be missing");
382
383 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device should be in waitfortlk");
384
385 XCTAssertNil([record[SecCKRecordCurrentTLK] recordID].recordName, "Should have no TLK uuid");
386 XCTAssertNil([record[SecCKRecordCurrentClassA] recordID].recordName, "Should have no class A uuid");
387 XCTAssertNil([record[SecCKRecordCurrentClassC] recordID].recordName, "Should have no class C uuid");
388 return YES;
389 } else {
390 return NO;
391 }
392 }
393 runAfterModification:nil];
394
395 // And allow the key state to progress
396 [self startCKKSSubsystem];
397 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
398 OCMVerifyAllWithDelay(self.mockDatabase, 20);
399 }
400
401 - (void)testDeviceStateReceive {
402 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
403
404 ZoneKeys* zoneKeys = self.keys[self.keychainZoneID];
405 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", self.keychainZoneID);
406
407 [self startCKKSSubsystem];
408 [self.keychainView waitForKeyHierarchyReadiness];
409
410 NSDate* date = [[NSCalendar currentCalendar] startOfDayForDate:[NSDate date]];
411 CKKSDeviceStateEntry* cdse = [[CKKSDeviceStateEntry alloc] initForDevice:@"otherdevice"
412 osVersion:@"fake-version"
413 lastUnlockTime:date
414 octagonPeerID:nil
415 octagonStatus:nil
416 circlePeerID:@"asdfasdf"
417 circleStatus:kSOSCCInCircle
418 keyState:SecCKKSZoneKeyStateReady
419 currentTLKUUID:zoneKeys.tlk.uuid
420 currentClassAUUID:zoneKeys.classA.uuid
421 currentClassCUUID:zoneKeys.classC.uuid
422 zoneID:self.keychainZoneID
423 encodedCKRecord:nil];
424 CKRecord* record = [cdse CKRecordWithZoneID:self.keychainZoneID];
425 [self.keychainZone addToZone:record];
426
427 CKKSDeviceStateEntry* oldcdse = [[CKKSDeviceStateEntry alloc] initForDevice:@"olderotherdevice"
428 osVersion:nil // old-style, no OSVersion or lastUnlockTime
429 lastUnlockTime:nil
430 octagonPeerID:nil
431 octagonStatus:nil
432 circlePeerID:@"olderasdfasdf"
433 circleStatus:kSOSCCInCircle
434 keyState:SecCKKSZoneKeyStateReady
435 currentTLKUUID:zoneKeys.tlk.uuid
436 currentClassAUUID:zoneKeys.classA.uuid
437 currentClassCUUID:zoneKeys.classC.uuid
438 zoneID:self.keychainZoneID
439 encodedCKRecord:nil];
440 [self.keychainZone addToZone:[oldcdse CKRecordWithZoneID:self.keychainZoneID]];
441
442 CKKSDeviceStateEntry* octagonOnly = [[CKKSDeviceStateEntry alloc] initForDevice:@"octagon-only"
443 osVersion:@"octagon-version"
444 lastUnlockTime:date
445 octagonPeerID:@"octagon-peer-ID"
446 octagonStatus:[[OTCliqueStatusWrapper alloc] initWithStatus:CliqueStatusNotIn]
447 circlePeerID:nil
448 circleStatus:kSOSCCError
449 keyState:SecCKKSZoneKeyStateReady
450 currentTLKUUID:zoneKeys.tlk.uuid
451 currentClassAUUID:zoneKeys.classA.uuid
452 currentClassCUUID:zoneKeys.classC.uuid
453 zoneID:self.keychainZoneID
454 encodedCKRecord:nil];
455 [self.keychainZone addToZone:[octagonOnly CKRecordWithZoneID:self.keychainZoneID]];
456
457 // Trigger a notification (with hilariously fake data)
458 [self.keychainView notifyZoneChange:nil];
459 [self.keychainView waitForFetchAndIncomingQueueProcessing];
460
461 [self.keychainView dispatchSync: ^bool {
462 NSError* error = nil;
463 NSArray<CKKSDeviceStateEntry*>* cdses = [CKKSDeviceStateEntry allInZone:self.keychainZoneID error:&error];
464 XCTAssertNil(error, "No error fetching CDSEs");
465 XCTAssertNotNil(cdses, "An array of CDSEs was returned");
466 XCTAssert(cdses.count >= 1u, "At least one CDSE came back");
467
468 CKKSDeviceStateEntry* item = nil;
469 CKKSDeviceStateEntry* olderotherdevice = nil;
470 CKKSDeviceStateEntry* octagondevice = nil;
471 for(CKKSDeviceStateEntry* dbcdse in cdses) {
472 if([dbcdse.device isEqualToString:@"otherdevice"]) {
473 item = dbcdse;
474 } else if([dbcdse.device isEqualToString:@"olderotherdevice"]) {
475 olderotherdevice = dbcdse;
476 } else if([dbcdse.device isEqualToString:@"octagon-only"]) {
477 octagondevice = dbcdse;
478 }
479 }
480 XCTAssertNotNil(item, "Found a cdse for otherdevice");
481
482 XCTAssertEqualObjects(cdse, item, "Saved item matches pre-cloudkit item");
483
484 XCTAssertEqualObjects(item.osVersion, @"fake-version", "correct osVersion");
485 XCTAssertEqualObjects(item.lastUnlockTime, date, "correct date");
486 XCTAssertEqualObjects(item.circlePeerID, @"asdfasdf", "correct peer id");
487 XCTAssertEqualObjects(item.keyState, SecCKKSZoneKeyStateReady, "correct key state");
488 XCTAssertEqualObjects(item.currentTLKUUID, zoneKeys.tlk.uuid, "correct tlk uuid");
489 XCTAssertEqualObjects(item.currentClassAUUID, zoneKeys.classA.uuid, "correct classA uuid");
490 XCTAssertEqualObjects(item.currentClassCUUID, zoneKeys.classC.uuid, "correct classC uuid");
491 XCTAssertNil(item.octagonPeerID, "should have no octagon peerID");
492 XCTAssertNil(item.octagonStatus, "should have no octagon status");
493
494
495 XCTAssertNotNil(olderotherdevice, "Should have found a cdse for olderotherdevice");
496 XCTAssertEqualObjects(oldcdse, olderotherdevice, "Saved item should match pre-cloudkit item");
497
498 XCTAssertNil(olderotherdevice.osVersion, "osVersion should be nil");
499 XCTAssertNil(olderotherdevice.lastUnlockTime, "lastUnlockTime should be nil");
500 XCTAssertEqualObjects(olderotherdevice.circlePeerID, @"olderasdfasdf", "correct peer id");
501 XCTAssertEqualObjects(olderotherdevice.keyState, SecCKKSZoneKeyStateReady, "correct key state");
502 XCTAssertEqualObjects(olderotherdevice.currentTLKUUID, zoneKeys.tlk.uuid, "correct tlk uuid");
503 XCTAssertEqualObjects(olderotherdevice.currentClassAUUID, zoneKeys.classA.uuid, "correct classA uuid");
504 XCTAssertEqualObjects(olderotherdevice.currentClassCUUID, zoneKeys.classC.uuid, "correct classC uuid");
505 XCTAssertNil(olderotherdevice.octagonPeerID, "should have no octagon peerID");
506 XCTAssertNil(olderotherdevice.octagonStatus, "should have no octagon status");
507
508
509 XCTAssertNotNil(octagondevice, "Should have found a cdse for octagondevice");
510 XCTAssertEqualObjects(octagonOnly, octagondevice, "Saved item should match pre-cloudkit item");
511 XCTAssertEqualObjects(octagondevice.osVersion, @"octagon-version", "osVersion should be right");
512 XCTAssertEqualObjects(octagondevice.lastUnlockTime, date, "correct date");
513 XCTAssertEqualObjects(octagondevice.octagonPeerID, @"octagon-peer-ID", "correct octagon peer id");
514 XCTAssertNotNil(octagondevice.octagonStatus, "should have an octagon status");
515 XCTAssertEqual(octagondevice.octagonStatus.status, CliqueStatusNotIn, "correct octagon status");
516 XCTAssertEqual(octagondevice.circleStatus, kSOSCCError, "correct SOS circle state");
517 XCTAssertNil(octagondevice.circlePeerID, "correct peer id");
518 XCTAssertEqualObjects(octagondevice.keyState, SecCKKSZoneKeyStateReady, "correct key state");
519 XCTAssertEqualObjects(octagondevice.currentTLKUUID, zoneKeys.tlk.uuid, "correct tlk uuid");
520 XCTAssertEqualObjects(octagondevice.currentClassAUUID, zoneKeys.classA.uuid, "correct classA uuid");
521 XCTAssertEqualObjects(octagondevice.currentClassCUUID, zoneKeys.classC.uuid, "correct classC uuid");
522
523 return false;
524 }];
525
526 OCMVerifyAllWithDelay(self.mockDatabase, 20);
527 }
528
529 - (void)testDeviceStateUploadBadKeyState {
530 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
531 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
532 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
533
534 [self startCKKSSubsystem];
535 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
536 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
537
538 __weak __typeof(self) weakSelf = self;
539 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
540 deletedRecordTypeCounts:nil
541 zoneID:self.keychainZoneID
542 checkModifiedRecord: ^BOOL (CKRecord* record){
543 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
544 // Check that all the things matches
545 __strong __typeof(weakSelf) strongSelf = weakSelf;
546 XCTAssertNotNil(strongSelf, "self exists");
547
548 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
549 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
550 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
551
552 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.mockSOSAdapter.selfPeer.peerID, "peer ID matches what we gave it");
553 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
554 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
555
556 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
557 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
558 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
559 return YES;
560 } else {
561 return NO;
562 }
563 }
564 runAfterModification:nil];
565
566 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
567
568 OCMVerifyAllWithDelay(self.mockDatabase, 20);
569 }
570
571 - (void)testDeviceStateUploadWaitForUnlockKeyState {
572 // Starts with everything in keychain, but locked
573 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
574 [self saveTLKMaterialToKeychain:self.keychainZoneID];
575 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
576
577 NSDateComponents *dateComponents = [[NSDateComponents alloc] init];
578 [dateComponents setDay:-3];
579 NSDate* threeDaysAgo = [[NSCalendar currentCalendar] dateByAddingComponents:dateComponents toDate:[NSDate date] options:0];
580
581 self.aksLockState = true;
582 [self.lockStateTracker recheck];
583 self.lockStateTracker.lastUnlockedTime = threeDaysAgo;
584 XCTAssertTrue([self.utcCalendar isDate:self.lockStateTracker.lastUnlockTime
585 equalToDate:threeDaysAgo
586 toUnitGranularity:NSCalendarUnitSecond],
587 "last unlock date (%@) similar to threeDaysAgo (%@)", self.lockStateTracker.lastUnlockTime, threeDaysAgo);
588
589 [self startCKKSSubsystem];
590 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForUnlock] wait:20*NSEC_PER_SEC], "CKKS entered waitforunlock");
591
592 __weak __typeof(self) weakSelf = self;
593 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
594 deletedRecordTypeCounts:nil
595 zoneID:self.keychainZoneID
596 checkModifiedRecord: ^BOOL (CKRecord* record){
597 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
598 // Check that all the things matches
599 __strong __typeof(weakSelf) strongSelf = weakSelf;
600 XCTAssertNotNil(strongSelf, "self exists");
601
602 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
603 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:threeDaysAgo toUnitGranularity:NSCalendarUnitDay],
604 "last unlock date (%@) similar to three days ago (%@)", record[SecCKSRecordLastUnlockTime], threeDaysAgo);
605
606 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.mockSOSAdapter.selfPeer.peerID, "peer ID matches what we gave it");
607 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
608 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForUnlock), "Device is in waitforunlock");
609
610 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
611 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
612 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
613 return YES;
614 } else {
615 return NO;
616 }
617 }
618 runAfterModification:nil];
619
620 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
621
622 OCMVerifyAllWithDelay(self.mockDatabase, 20);
623 }
624
625 - (void)testDeviceStateUploadBadKeyStateAfterRestart {
626 // This test has stuff in CloudKit, but no TLKs. It should become very sad.
627 [self putFakeKeyHierarchyInCloudKit: self.keychainZoneID];
628 [self putFakeDeviceStatusInCloudKit:self.keychainZoneID];
629
630 [self startCKKSSubsystem];
631 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
632 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
633
634 // And restart CKKS...
635 self.keychainView = [[CKKSViewManager manager] restartZone: self.keychainZoneID.zoneName];
636 [self beginSOSTrustedViewOperation:self.keychainView];
637 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTLK] wait:20*NSEC_PER_SEC], "CKKS entered waitfortlk");
638 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateWaitForTLK, "CKKS entered waitfortlk");
639
640 __weak __typeof(self) weakSelf = self;
641 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
642 deletedRecordTypeCounts:nil
643 zoneID:self.keychainZoneID
644 checkModifiedRecord: ^BOOL (CKRecord* record){
645 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
646 // Check that all the things matches
647 __strong __typeof(weakSelf) strongSelf = weakSelf;
648 XCTAssertNotNil(strongSelf, "self exists");
649
650 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
651 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
652 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
653
654 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.mockSOSAdapter.selfPeer.peerID, "peer ID matches what we gave it");
655 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
656 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTLK), "Device is in waitfortlk");
657
658 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
659 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
660 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
661 return YES;
662 } else {
663 return NO;
664 }
665 }
666 runAfterModification:nil];
667
668 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
669
670 OCMVerifyAllWithDelay(self.mockDatabase, 20);
671 }
672
673
674 - (void)testDeviceStateUploadBadCircleState {
675 self.mockSOSAdapter.circleStatus = kSOSCCNotInCircle;
676 [self.accountStateTracker notifyCircleStatusChangeAndWaitForSignal];
677
678 // This test has stuff in CloudKit, but no TLKs.
679 // It should NOT reset the CK zone.
680 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
681 self.zones[self.keychainZoneID].flag = true;
682
683 [self startCKKSSubsystem];
684
685 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateWaitForTrust] wait:20*NSEC_PER_SEC], "CKKS entered waitfortrust");
686
687 __weak __typeof(self) weakSelf = self;
688 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
689 deletedRecordTypeCounts:nil
690 zoneID:self.keychainZoneID
691 checkModifiedRecord: ^BOOL (CKRecord* record){
692 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
693 // Check that all the things matches
694 __strong __typeof(weakSelf) strongSelf = weakSelf;
695 XCTAssertNotNil(strongSelf, "self exists");
696
697 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
698 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
699 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
700
701 XCTAssertNil(record[SecCKRecordCirclePeerID], "no peer ID if device is not in circle");
702 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCNotInCircle], "device is not in circle");
703 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateWaitForTrust), "Device is in keystate:waitfortrust");
704
705 XCTAssertNil(record[SecCKRecordCurrentTLK] , "No TLK");
706 XCTAssertNil(record[SecCKRecordCurrentClassA], "No class A key");
707 XCTAssertNil(record[SecCKRecordCurrentClassC], "No class C key");
708 return YES;
709 } else {
710 return NO;
711 }
712 }
713 runAfterModification:nil];
714
715 CKKSUpdateDeviceStateOperation* op = [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:500*NSEC_PER_MSEC ckoperationGroup:nil];
716 OCMVerifyAllWithDelay(self.mockDatabase, 20);
717
718 [op waitUntilFinished];
719 XCTAssertNil(op.error, "No error uploading 'out of circle' device state");
720
721 FakeCKZone* keychainZone = self.zones[self.keychainZoneID];
722 XCTAssertNotNil(keychainZone, "Should still have a keychain zone");
723 XCTAssertTrue(keychainZone.flag, "keychain zone should not have been recreated");
724 }
725
726 - (void)testDeviceStateUploadWithTardyNetworkAfterRestart {
727 // Test starts with a key hierarchy in cloudkit and the TLK having arrived
728 [self putFakeKeyHierarchyInCloudKit:self.keychainZoneID];
729 [self saveTLKMaterialToKeychain:self.keychainZoneID];
730 [self expectCKKSTLKSelfShareUpload:self.keychainZoneID];
731
732 [self holdCloudKitFetches];
733
734 [self startCKKSSubsystem];
735
736 // we should be stuck in fetch
737 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateFetch] wait:20*NSEC_PER_SEC], "Key state should become fetch");
738
739 __weak __typeof(self) weakSelf = self;
740 [self expectCKModifyRecords: @{SecCKRecordDeviceStateType: [NSNumber numberWithInt:1]}
741 deletedRecordTypeCounts:nil
742 zoneID:self.keychainZoneID
743 checkModifiedRecord: ^BOOL (CKRecord* record){
744 if([record.recordType isEqualToString: SecCKRecordDeviceStateType]) {
745 // Check that all the things matches
746 __strong __typeof(weakSelf) strongSelf = weakSelf;
747 XCTAssertNotNil(strongSelf, "self exists");
748
749 ZoneKeys* zoneKeys = strongSelf.keys[strongSelf.keychainZoneID];
750 XCTAssertNotNil(zoneKeys, "Have zone keys for %@", strongSelf.keychainZoneID);
751
752 XCTAssertEqualObjects(record[SecCKSRecordOSVersionKey], SecCKKSHostOSVersion(), "os version string should match current OS version");
753 XCTAssertTrue([self.utcCalendar isDate:record[SecCKSRecordLastUnlockTime] equalToDate:[NSDate date] toUnitGranularity:NSCalendarUnitDay],
754 "last unlock date (%@) similar to Now (%@)", record[SecCKSRecordLastUnlockTime], [NSDate date]);
755
756 XCTAssertEqualObjects(record[SecCKRecordCirclePeerID], strongSelf.mockSOSAdapter.selfPeer.peerID, "peer ID matches what we gave it");
757 XCTAssertEqualObjects(record[SecCKRecordCircleStatus], [NSNumber numberWithInt:kSOSCCInCircle], "device is in circle");
758 XCTAssertEqualObjects(record[SecCKRecordKeyState], CKKSZoneKeyToNumber(SecCKKSZoneKeyStateReady), "Device is in ready");
759
760 XCTAssertEqualObjects([record[SecCKRecordCurrentTLK] recordID].recordName, zoneKeys.tlk.uuid, "Correct TLK uuid");
761 XCTAssertEqualObjects([record[SecCKRecordCurrentClassA] recordID].recordName, zoneKeys.classA.uuid, "Correct class A uuid");
762 XCTAssertEqualObjects([record[SecCKRecordCurrentClassC] recordID].recordName, zoneKeys.classC.uuid, "Correct class C uuid");
763 return YES;
764 } else {
765 return NO;
766 }
767 }
768 runAfterModification:nil];
769
770
771 [self.keychainView updateDeviceState:false waitForKeyHierarchyInitialization:8*NSEC_PER_SEC ckoperationGroup:nil];
772
773 XCTAssertEqualObjects(self.keychainView.keyHierarchyState, SecCKKSZoneKeyStateFetch, "CKKS re-entered fetch");
774 [self releaseCloudKitFetchHold];
775
776 OCMVerifyAllWithDelay(self.mockDatabase, 20);
777 }
778
779
780
781 @end
782
783 #endif // OCTAGON