]> git.saurik.com Git - apple/security.git/blob - keychain/ckks/tests/CKKSTests+Coalesce.m
Security-59754.41.1.tar.gz
[apple/security.git] / keychain / ckks / tests / CKKSTests+Coalesce.m
1 /*
2 * Copyright (c) 2016 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 "keychain/ckks/CKKS.h"
27 #import "keychain/ckks/tests/CKKSTests.h"
28 #import "keychain/ckks/tests/CloudKitMockXCTest.h"
29
30 // Break abstraction.
31 @interface CKKSKeychainView(test)
32 @property NSOperationQueue* operationQueue;
33 @end
34
35 @implementation CloudKitKeychainSyncingTests (CoalesceTests)
36 // These tests check that, if CKKS doesn't start processing an item before a new update comes in,
37 // each case is properly handled.
38
39 - (void)testCoalesceAddModifyItem {
40 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
41
42 NSString* account = @"account-delete-me";
43
44 [self addGenericPassword: @"data" account: account];
45 [self updateGenericPassword: @"otherdata" account:account];
46
47 // We expect a single record to be uploaded.
48 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
49
50 [self startCKKSSubsystem];
51 OCMVerifyAllWithDelay(self.mockDatabase, 20);
52 }
53
54 - (void)testCoalesceAddModifyModifyItem {
55 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
56
57 NSString* account = @"account-delete-me";
58
59 [self addGenericPassword: @"data" account: account];
60 [self updateGenericPassword: @"otherdata" account:account];
61 [self updateGenericPassword: @"again" account:account];
62
63 // We expect a single record to be uploaded.
64 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
65
66 [self startCKKSSubsystem];
67 OCMVerifyAllWithDelay(self.mockDatabase, 20);
68 }
69
70 - (void)testCoalesceAddModifyDeleteItem {
71 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
72
73 NSString* account = @"account-delete-me";
74
75 [self addGenericPassword: @"data" account: account];
76 [self updateGenericPassword: @"otherdata" account:account];
77 [self deleteGenericPassword: account];
78
79 // We expect no uploads.
80 [self startCKKSSubsystem];
81 [self.keychainView waitUntilAllOperationsAreFinished];
82 OCMVerifyAllWithDelay(self.mockDatabase, 20);
83 }
84
85 - (void)testCoalesceDeleteAddItem {
86 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID]; // Make life easy for this test.
87
88 NSString* account = @"account-delete-me";
89
90 [self addGenericPassword: @"data" account: account];
91
92 // We expect a single record to be uploaded.
93 [self expectCKModifyItemRecords: 1 currentKeyPointerRecords: 1 zoneID:self.keychainZoneID];
94 [self startCKKSSubsystem];
95 OCMVerifyAllWithDelay(self.mockDatabase, 20);
96 [self waitForCKModifications];
97
98 // Okay, now the delete/add. Note that this is not a coalescing operation, since the new item
99 // has different contents. (This test used to upload the item to a different UUID, but no longer).
100
101 self.keychainView.holdOutgoingQueueOperation = [CKKSResultOperation named:@"hold-outgoing-queue"
102 withBlock:^{}];
103
104 [self deleteGenericPassword: account];
105 [self addGenericPassword: @"data_new_contents" account: account];
106
107 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
108 checkItem:[self checkPasswordBlock:self.keychainZoneID account:account password:@"data_new_contents"]];
109
110 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
111 OCMVerifyAllWithDelay(self.mockDatabase, 20);
112 }
113
114 - (void)testCoalesceReceiveModifyWhileDeletingItem {
115 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
116
117 NSString* account = @"account-delete-me";
118
119 [self addGenericPassword:@"data" account:account];
120
121 // We expect a single record to be uploaded.
122 __block CKRecord* itemRecord = nil;
123 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
124 checkItem:^BOOL(CKRecord * _Nonnull record) {
125 itemRecord = record;
126 return YES;
127 }];
128
129 [self startCKKSSubsystem];
130 OCMVerifyAllWithDelay(self.mockDatabase, 20);
131 [self waitForCKModifications];
132
133 // Now, we receive a modification from CK, but then delete the item locally before processing the IQE.
134
135 XCTAssertNotNil(itemRecord, "Should have a record for the uploaded item");
136 NSMutableDictionary* contents = [[self decryptRecord:itemRecord] mutableCopy];
137 contents[@"v_Data"] = [@"updated" dataUsingEncoding:NSUTF8StringEncoding];
138
139 CKRecord* recordUpdate = [self newRecord:itemRecord.recordID withNewItemData:contents];
140 [self.keychainZone addCKRecordToZone:recordUpdate];
141
142 self.keychainView.holdIncomingQueueOperation = [NSBlockOperation blockOperationWithBlock:^{}];
143
144 // Ensure we wait for the whole fetch
145 NSOperation* fetchOp = [self.keychainView.zoneChangeFetcher requestSuccessfulFetch:CKKSFetchBecauseTesting];
146 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
147
148 [fetchOp waitUntilFinished];
149
150 // now, delete the item
151 [self expectCKDeleteItemRecords:1 zoneID:self.keychainZoneID];
152 [self deleteGenericPassword:account];
153
154 [self.operationQueue addOperation:self.keychainView.holdIncomingQueueOperation];
155
156 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
157 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
158 OCMVerifyAllWithDelay(self.mockDatabase, 20);
159
160 // And the item shouldn't be present, since it was deleted via API after the item was fetched
161 [self findGenericPassword:account expecting:errSecItemNotFound];
162 }
163
164 - (void)testCoalesceReceiveDeleteWhileModifyingItem {
165 [self createAndSaveFakeKeyHierarchy: self.keychainZoneID];
166
167 NSString* account = @"account-delete-me";
168
169 [self startCKKSSubsystem];
170 XCTAssertEqual(0, [self.keychainView.keyHierarchyConditions[SecCKKSZoneKeyStateReady] wait:20*NSEC_PER_SEC], @"key state should enter 'ready'");
171
172 __block CKRecord* itemRecord = nil;
173 [self expectCKModifyItemRecords:1 currentKeyPointerRecords:1 zoneID:self.keychainZoneID
174 checkItem:^BOOL(CKRecord * _Nonnull record) {
175 itemRecord = record;
176 return YES;
177 }];
178
179 [self addGenericPassword:@"data" account:account];
180
181 OCMVerifyAllWithDelay(self.mockDatabase, 20);
182 [self waitForCKModifications];
183
184 // Ensure we fetch again, to prime the delete (due to insufficient mock CK)
185 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
186 [self.keychainView waitForFetchAndIncomingQueueProcessing];
187
188 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
189 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
190
191 // Now, we receive a delete from CK, but after we modify the item locally
192 self.keychainView.holdOutgoingQueueOperation = [NSBlockOperation blockOperationWithBlock:^{}];
193
194 XCTAssertNotNil(itemRecord, "Should have an item record from the upload");
195 [self.keychainZone deleteCKRecordIDFromZone:itemRecord.recordID];
196 [self updateGenericPassword:@"new-password" account:account];
197
198 [self.injectedManager.zoneChangeFetcher notifyZoneChange:nil];
199 [self.keychainView waitForFetchAndIncomingQueueProcessing];
200 [self findGenericPassword:account expecting:errSecItemNotFound];
201
202 [self.operationQueue addOperation:self.keychainView.holdOutgoingQueueOperation];
203
204 [self.keychainView waitForOperationsOfClass:[CKKSIncomingQueueOperation class]];
205 [self.keychainView waitForOperationsOfClass:[CKKSOutgoingQueueOperation class]];
206 OCMVerifyAllWithDelay(self.mockDatabase, 20);
207
208 // And the item shouldn't be present, since it was deleted via CK after the API change
209 [self findGenericPassword:account expecting:errSecItemNotFound];
210 }
211
212 @end
213
214 #endif