]> git.saurik.com Git - apple/security.git/blob - keychain/ot/tests/octagon/OctagonTests+EscrowRecovery.swift
64a4006fb7df4359ab2a6869c681bc31af835ffd
[apple/security.git] / keychain / ot / tests / octagon / OctagonTests+EscrowRecovery.swift
1 #if OCTAGON
2
3 @objcMembers class OctagonEscrowRecoveryTests: OctagonTestsBase {
4 override func setUp() {
5 super.setUp()
6 }
7
8 func testJoinWithBottle() throws {
9 let initiatorContextID = "initiator-context-id"
10 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
11
12 bottlerContext.startOctagonStateMachine()
13 let ckacctinfo = CKAccountInfo()
14 ckacctinfo.accountStatus = .available
15 ckacctinfo.hasValidCredentials = true
16 ckacctinfo.accountPartition = .production
17
18 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
19 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
20
21 let clique: OTClique
22 let bottlerotcliqueContext = OTConfigurationContext()
23 bottlerotcliqueContext.context = initiatorContextID
24 bottlerotcliqueContext.dsid = "1234"
25 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
26 bottlerotcliqueContext.otControl = self.otControl
27 do {
28 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
29 XCTAssertNotNil(clique, "Clique should not be nil")
30 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
31 } catch {
32 XCTFail("Shouldn't have errored making new friends: \(error)")
33 throw error
34 }
35
36 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
37 self.assertConsidersSelfTrusted(context: bottlerContext)
38
39 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
40 XCTAssertNotNil(entropy, "entropy should not be nil")
41
42 // Fake that this peer also created some TLKShares for itself
43 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
44 try self.putSelfTLKShareInCloudKit(context: bottlerContext, zoneID: self.manateeZoneID)
45
46 let bottle = self.fakeCuttlefishServer.state.bottles[0]
47
48 self.cuttlefishContext.startOctagonStateMachine()
49 self.startCKAccountStatusMock()
50 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
51
52 // Try to enforce that that CKKS doesn't know about the key hierarchy until Octagon asks it
53 self.holdCloudKitFetches()
54
55 // Note: CKKS will want to upload a TLKShare for its self
56 self.expectCKModifyKeyRecords(0, currentKeyPointerRecords: 0, tlkShareRecords: 1, zoneID: self.manateeZoneID)
57
58 // Before you call joinWithBottle, you need to call fetchViableBottles.
59 let fetchViableExpectation = self.expectation(description: "fetchViableBottles callback occurs")
60 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
61 XCTAssertNil(error, "should be no error fetching viable bottles")
62 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
63 fetchViableExpectation.fulfill()
64 }
65 self.wait(for: [fetchViableExpectation], timeout: 10)
66
67 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
68 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID) { error in
69 XCTAssertNil(error, "error should be nil")
70 joinWithBottleExpectation.fulfill()
71 }
72
73 sleep(1)
74 self.releaseCloudKitFetchHold()
75
76 self.wait(for: [joinWithBottleExpectation], timeout: 100)
77
78 let dumpCallback = self.expectation(description: "dumpCallback callback occurs")
79 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) {
80 dump, _ in
81 XCTAssertNotNil(dump, "dump should not be nil")
82 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
83 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
84 let dynamicInfo = egoSelf!["dynamicInfo"] as? Dictionary<String, AnyObject>
85 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
86 let included = dynamicInfo!["included"] as? Array<String>
87 XCTAssertNotNil(included, "included should not be nil")
88 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
89 dumpCallback.fulfill()
90 }
91 self.wait(for: [dumpCallback], timeout: 10)
92
93 self.verifyDatabaseMocks()
94 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
95 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
96 }
97
98 func testBottleRestoreEntersOctagonReady() throws {
99 self.startCKAccountStatusMock()
100
101 let initiatorContextID = "initiator-context-id"
102 self.cuttlefishContext.startOctagonStateMachine()
103 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
104
105 let clique: OTClique
106 do {
107 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
108 XCTAssertNotNil(clique, "Clique should not be nil")
109 } catch {
110 XCTFail("Shouldn't have errored making new friends: \(error)")
111 throw error
112 }
113
114 self.verifyDatabaseMocks()
115
116 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
117 XCTAssertNotNil(entropy, "entropy should not be nil")
118
119 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
120 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
121
122 let bottle = self.fakeCuttlefishServer.state.bottles[0]
123
124 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
125
126 initiatorContext.startOctagonStateMachine()
127 let restoreExpectation = self.expectation(description: "restore returns")
128
129 self.manager!.restore(OTCKContainerName, contextID: initiatorContextID, bottleSalt: self.otcliqueContext.altDSID, entropy: entropy!, bottleID: bottle.bottleID) { error in
130 XCTAssertNil(error, "error should be nil")
131 restoreExpectation.fulfill()
132 }
133 self.wait(for: [restoreExpectation], timeout: 10)
134
135 self.assertEnters(context: initiatorContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
136
137 let initiatorDumpCallback = self.expectation(description: "initiatorDumpCallback callback occurs")
138 self.tphClient.dump(withContainer: OTCKContainerName, context: initiatorContextID) {
139 dump, _ in
140 XCTAssertNotNil(dump, "dump should not be nil")
141 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
142 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
143 let dynamicInfo = egoSelf!["dynamicInfo"] as? Dictionary<String, AnyObject>
144 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
145 let included = dynamicInfo!["included"] as? Array<String>
146 XCTAssertNotNil(included, "included should not be nil")
147 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
148
149 initiatorDumpCallback.fulfill()
150 }
151 self.wait(for: [initiatorDumpCallback], timeout: 10)
152 }
153
154 func testJoinWithBottleWithCKKSConflict() throws {
155 let initiatorContextID = "initiator-context-id"
156 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
157
158 bottlerContext.startOctagonStateMachine()
159 let ckacctinfo = CKAccountInfo()
160 ckacctinfo.accountStatus = .available
161 ckacctinfo.hasValidCredentials = true
162 ckacctinfo.accountPartition = .production
163
164 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
165 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
166
167 let clique: OTClique
168 let bottlerotcliqueContext = OTConfigurationContext()
169 bottlerotcliqueContext.context = initiatorContextID
170 bottlerotcliqueContext.dsid = "1234"
171 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
172 bottlerotcliqueContext.otControl = self.otControl
173 do {
174 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
175 XCTAssertNotNil(clique, "Clique should not be nil")
176 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
177 } catch {
178 XCTFail("Shouldn't have errored making new friends: \(error)")
179 throw error
180 }
181
182 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
183 self.assertConsidersSelfTrusted(context: bottlerContext)
184
185 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
186 XCTAssertNotNil(entropy, "entropy should not be nil")
187
188 let bottle = self.fakeCuttlefishServer.state.bottles[0]
189
190 // During the join, there's a CKKS key race
191 self.silentFetchesAllowed = false
192 self.expectCKFetchAndRun(beforeFinished: {
193 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
194 self.putFakeDeviceStatus(inCloudKit: self.manateeZoneID)
195 self.silentFetchesAllowed = true
196 })
197
198 self.cuttlefishContext.startOctagonStateMachine()
199 self.startCKAccountStatusMock()
200 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
201
202 // Before you call joinWithBottle, you need to call fetchViableBottles.
203 let fetchViableExpectation = self.expectation(description: "fetchViableBottles callback occurs")
204 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
205 XCTAssertNil(error, "should be no error fetching viable bottles")
206 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
207 fetchViableExpectation.fulfill()
208 }
209 self.wait(for: [fetchViableExpectation], timeout: 10)
210
211 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
212 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID) { error in
213 XCTAssertNil(error, "error should be nil")
214 joinWithBottleExpectation.fulfill()
215 }
216
217 self.wait(for: [joinWithBottleExpectation], timeout: 100)
218
219 self.verifyDatabaseMocks()
220 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
221 assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTLK, within: 10 * NSEC_PER_SEC)
222 }
223
224 func testBottleRestoreWithSameMachineID() throws {
225 self.startCKAccountStatusMock()
226
227 self.cuttlefishContext.startOctagonStateMachine()
228 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
229
230 let clique: OTClique
231 do {
232 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
233 XCTAssertNotNil(clique, "Clique should not be nil")
234 } catch {
235 XCTFail("Shouldn't have errored making new friends: \(error)")
236 throw error
237 }
238
239 self.verifyDatabaseMocks()
240
241 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
242 XCTAssertNotNil(entropy, "entropy should not be nil")
243
244 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
245 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
246
247 let bottle = self.fakeCuttlefishServer.state.bottles[0]
248
249 // some other peer should restore from this bottle
250 let differentDevice = self.makeInitiatorContext(contextID: "differenDevice")
251 let differentRestoreExpectation = self.expectation(description: "different restore returns")
252 differentDevice.startOctagonStateMachine()
253 differentDevice.join(withBottle: bottle.bottleID,
254 entropy: entropy!,
255 bottleSalt: self.otcliqueContext.altDSID) { error in
256 XCTAssertNil(error, "error should be nil")
257 differentRestoreExpectation.fulfill()
258 }
259 self.wait(for: [differentRestoreExpectation], timeout: 10)
260
261 self.assertTrusts(context: differentDevice, includedPeerIDCount: 2, excludedPeerIDCount: 0)
262
263 // The first peer will upload TLKs for the new peer
264 self.assertAllCKKSViewsUpload(tlkShares: 1)
265 self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
266 self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
267 self.verifyDatabaseMocks()
268
269 self.assertTrusts(context: self.cuttlefishContext, includedPeerIDCount: 2, excludedPeerIDCount: 0)
270
271 // Explicitly use the same authkit parameters as the original peer when restoring this time
272 let restoreContext = self.makeInitiatorContext(contextID: "restoreContext", authKitAdapter: self.mockAuthKit)
273
274 restoreContext.startOctagonStateMachine()
275
276 let restoreExpectation = self.expectation(description: "restore returns")
277 restoreContext.join(withBottle: bottle.bottleID,
278 entropy: entropy!,
279 bottleSalt: self.otcliqueContext.altDSID) { error in
280 XCTAssertNil(error, "error should be nil")
281 restoreExpectation.fulfill()
282 }
283 self.wait(for: [restoreExpectation], timeout: 10)
284
285 self.assertEnters(context: restoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
286 self.assertConsidersSelfTrusted(context: restoreContext)
287
288 // The restore context should exclude its sponsor
289 self.assertTrusts(context: restoreContext, includedPeerIDCount: 2, excludedPeerIDCount: 1)
290
291 // Then, the remote peer should trust the new peer and exclude the original
292 self.sendContainerChangeWaitForFetchForStates(context: differentDevice, states: [OctagonStateReadyUpdated, OctagonStateReady])
293 self.assertTrusts(context: differentDevice, includedPeerIDCount: 2, excludedPeerIDCount: 1)
294
295 // Then, if by some strange miracle the original peer is still around, it should bail (as it's now untrusted)
296 self.sendContainerChangeWaitForFetchForStates(context: self.cuttlefishContext, states: [OctagonStateUntrusted])
297 self.assertConsidersSelfUntrusted(context: self.cuttlefishContext)
298 self.assertTrusts(context: self.cuttlefishContext, includedPeerIDCount: 0, excludedPeerIDCount: 1)
299 }
300
301 func testRestoreSPIFromPiggybackingState() throws {
302 self.startCKAccountStatusMock()
303
304 let initiatorContextID = "initiator-context-id"
305 self.cuttlefishContext.startOctagonStateMachine()
306 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
307
308 let clique: OTClique
309 do {
310 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
311 XCTAssertNotNil(clique, "Clique should not be nil")
312 } catch {
313 XCTFail("Shouldn't have errored making new friends: \(error)")
314 throw error
315 }
316 self.verifyDatabaseMocks()
317
318 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
319 XCTAssertNotNil(entropy, "entropy should not be nil")
320
321 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
322 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
323
324 let bottle = self.fakeCuttlefishServer.state.bottles[0]
325
326 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
327
328 initiatorContext.startOctagonStateMachine()
329
330 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
331
332 let restoreExpectation = self.expectation(description: "restore returns")
333
334 self.manager!.restore(OTCKContainerName, contextID: initiatorContextID, bottleSalt: self.otcliqueContext.altDSID, entropy: entropy!, bottleID: bottle.bottleID) { error in
335 XCTAssertNil(error, "error should be nil")
336 restoreExpectation.fulfill()
337 }
338 self.wait(for: [restoreExpectation], timeout: 10)
339
340 let initiatorDumpCallback = self.expectation(description: "initiatorDumpCallback callback occurs")
341 self.tphClient.dump(withContainer: OTCKContainerName, context: initiatorContextID) {
342 dump, _ in
343 XCTAssertNotNil(dump, "dump should not be nil")
344 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
345 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
346 let dynamicInfo = egoSelf!["dynamicInfo"] as? Dictionary<String, AnyObject>
347 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
348 let included = dynamicInfo!["included"] as? Array<String>
349 XCTAssertNotNil(included, "included should not be nil")
350 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
351
352 initiatorDumpCallback.fulfill()
353 }
354 self.wait(for: [initiatorDumpCallback], timeout: 10)
355 }
356
357 func testRestoreBadBottleIDFails() throws {
358 self.startCKAccountStatusMock()
359
360 let initiatorContextID = "initiator-context-id"
361 self.cuttlefishContext.startOctagonStateMachine()
362 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
363
364 let clique: OTClique
365 do {
366 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
367 XCTAssertNotNil(clique, "Clique should not be nil")
368 } catch {
369 XCTFail("Shouldn't have errored making new friends: \(error)")
370 throw error
371 }
372
373 self.verifyDatabaseMocks()
374
375 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
376 XCTAssertNotNil(entropy, "entropy should not be nil")
377
378 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
379 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
380
381 _ = self.fakeCuttlefishServer.state.bottles[0]
382
383 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
384
385 initiatorContext.startOctagonStateMachine()
386
387 let restoreExpectation = self.expectation(description: "restore returns")
388
389 self.manager!.restore(OTCKContainerName, contextID: initiatorContextID, bottleSalt: self.otcliqueContext.altDSID, entropy: entropy!, bottleID: "bad escrow record ID") { error in
390 XCTAssertNotNil(error, "error should not be nil")
391 restoreExpectation.fulfill()
392 }
393 self.wait(for: [restoreExpectation], timeout: 10)
394 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
395
396 let acceptorDumpCallback = self.expectation(description: "acceptorDumpCallback callback occurs")
397 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) {
398 dump, _ in
399 XCTAssertNotNil(dump, "dump should not be nil")
400 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
401 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
402 let dynamicInfo = egoSelf!["dynamicInfo"] as? Dictionary<String, AnyObject>
403 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
404 let included = dynamicInfo!["included"] as? Array<String>
405 XCTAssertNotNil(included, "included should not be nil")
406 XCTAssertEqual(included!.count, 1, "should be 1 peer ids")
407
408 acceptorDumpCallback.fulfill()
409 }
410 self.wait(for: [acceptorDumpCallback], timeout: 10)
411 }
412
413 func testRestoreOptimalBottleIDs() throws {
414 self.startCKAccountStatusMock()
415
416 let clique: OTClique
417 do {
418 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
419 XCTAssertNotNil(clique, "Clique should not be nil")
420 } catch {
421 XCTFail("Shouldn't have errored making new friends: \(error)")
422 throw error
423 }
424
425 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
426 XCTAssertNotNil(entropy, "entropy should not be nil")
427
428 var bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
429 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
430 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 1, "preferredBottleIDs should have 1 bottle")
431 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
432
433 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
434 self.verifyDatabaseMocks()
435 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
436
437 let bottle = self.fakeCuttlefishServer.state.bottles[0]
438
439 let initiatorContext = self.manager.context(forContainerName: OTCKContainerName,
440 contextID: "restoreContext",
441 sosAdapter: OTSOSMissingAdapter(),
442 authKitAdapter: self.mockAuthKit2,
443 lockStateTracker: self.lockStateTracker,
444 accountStateTracker: self.accountStateTracker,
445 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
446
447 initiatorContext.startOctagonStateMachine()
448 let newOTCliqueContext = OTConfigurationContext()
449 newOTCliqueContext.context = "restoreContext"
450 newOTCliqueContext.dsid = self.otcliqueContext.dsid
451 newOTCliqueContext.altDSID = self.otcliqueContext.altDSID
452 newOTCliqueContext.otControl = self.otcliqueContext.otControl
453 newOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottle.bottleID, entropy: entropy!)
454
455 let newClique: OTClique
456 do {
457 newClique = try OTClique.performEscrowRecovery(withContextData: newOTCliqueContext, escrowArguments: [:])
458 XCTAssertNotNil(newClique, "newClique should not be nil")
459 } catch {
460 XCTFail("Shouldn't have errored recovering: \(error)")
461 throw error
462 }
463
464 // We will upload a new TLK for the new peer
465 self.assertAllCKKSViewsUpload(tlkShares: 1)
466 self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
467 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
468
469 self.sendContainerChangeWaitForFetch(context: initiatorContext)
470 bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
471 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
472 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 2, "preferredBottleIDs should have 2 bottle")
473 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
474 }
475
476 func testRestoreFromEscrowContents() throws {
477 self.startCKAccountStatusMock()
478
479 let clique: OTClique
480 do {
481 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
482 XCTAssertNotNil(clique, "Clique should not be nil")
483 } catch {
484 XCTFail("Shouldn't have errored making new friends: \(error)")
485 throw error
486 }
487
488 var bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
489 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
490 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 1, "preferredBottleIDs should have 1 bottle")
491 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
492
493 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
494 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
495 self.verifyDatabaseMocks()
496
497 var entropy = Data()
498 var bottledID: String = ""
499
500 let fetchEscrowContentsExpectation = self.expectation(description: "fetchEscrowContentsExpectation returns")
501 clique.fetchEscrowContents { e, b, s, _ in
502 XCTAssertNotNil(e, "entropy should not be nil")
503 XCTAssertNotNil(b, "bottleID should not be nil")
504 XCTAssertNotNil(s, "signingPublicKey should not be nil")
505 entropy = e!
506 bottledID = b!
507 fetchEscrowContentsExpectation.fulfill()
508 }
509
510 self.wait(for: [fetchEscrowContentsExpectation], timeout: 10)
511
512 let initiatorContext = self.manager.context(forContainerName: OTCKContainerName,
513 contextID: "restoreContext",
514 sosAdapter: OTSOSMissingAdapter(),
515 authKitAdapter: self.mockAuthKit2,
516 lockStateTracker: self.lockStateTracker,
517 accountStateTracker: self.accountStateTracker,
518 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
519
520 initiatorContext.startOctagonStateMachine()
521 let newOTCliqueContext = OTConfigurationContext()
522 newOTCliqueContext.context = "restoreContext"
523 newOTCliqueContext.dsid = self.otcliqueContext.dsid
524 newOTCliqueContext.altDSID = self.otcliqueContext.altDSID
525 newOTCliqueContext.otControl = self.otcliqueContext.otControl
526 newOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottledID, entropy: entropy)
527
528 let newClique: OTClique
529 do {
530 newClique = try OTClique.performEscrowRecovery(withContextData: newOTCliqueContext, escrowArguments: [:])
531 XCTAssertNotNil(newClique, "newClique should not be nil")
532 } catch {
533 XCTFail("Shouldn't have errored recovering: \(error)")
534 throw error
535 }
536
537 // We will upload a new TLK for the new peer
538 self.assertAllCKKSViewsUpload(tlkShares: 1)
539 self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
540 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
541
542 self.sendContainerChangeWaitForFetch(context: initiatorContext)
543
544 bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
545 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
546 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 2, "preferredBottleIDs should have 2 bottle")
547 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
548
549 let dumpExpectation = self.expectation(description: "dump callback occurs")
550 self.tphClient.dump(withContainer: self.cuttlefishContext.containerName, context: self.cuttlefishContext.contextID) {
551 dump, error in
552 XCTAssertNil(error, "Should be no error dumping data")
553 XCTAssertNotNil(dump, "dump should not be nil")
554 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
555 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
556 let peerID = egoSelf!["peerID"] as? String
557 XCTAssertNotNil(peerID, "peerID should not be nil")
558
559 dumpExpectation.fulfill()
560 }
561 self.wait(for: [dumpExpectation], timeout: 10)
562
563 self.otControlCLI.status(OTCKContainerName,
564 context: newOTCliqueContext.context!,
565 json: false)
566 }
567
568 func testFetchEmptyOptimalBottleList () throws {
569 self.startCKAccountStatusMock()
570
571 let bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
572 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 0, "should be 0 preferred bottle ids")
573 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "should be 0 partialRecoveryBottleIDs")
574 }
575
576 func testFetchOptimalBottlesAfterFailedRestore() throws {
577 self.startCKAccountStatusMock()
578
579 self.otcliqueContext.sbd = OTMockSecureBackup(bottleID: "bottle ID", entropy: Data(count: 72))
580
581 XCTAssertThrowsError(try OTClique.performEscrowRecovery(withContextData: self.otcliqueContext, escrowArguments: [:]))
582
583 let bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
584 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
585 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 0, "preferredBottleIDs should have 0 bottle")
586 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
587 }
588
589 func testMakeNewFriendsAndFetchEscrowContents () throws {
590 self.startCKAccountStatusMock()
591
592 let clique: OTClique
593 do {
594 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
595 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
596 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
597
598 let fetchEscrowContentsException = self.expectation(description: "update returns")
599
600 clique.fetchEscrowContents { e, b, s, _ in
601 XCTAssertNotNil(e, "entropy should not be nil")
602 XCTAssertNotNil(b, "bottleID should not be nil")
603 XCTAssertNotNil(s, "signingPublicKey should not be nil")
604 fetchEscrowContentsException.fulfill()
605 }
606 self.wait(for: [fetchEscrowContentsException], timeout: 10)
607
608 } catch {
609 XCTFail("failed to reset clique: \(error)")
610 }
611
612 self.verifyDatabaseMocks()
613 }
614
615 func testFetchEscrowContentsBeforeIdentityExists() {
616 self.startCKAccountStatusMock()
617
618 let initiatorContext = self.manager.context(forContainerName: OTCKContainerName, contextID: "initiator")
619 let fetchEscrowContentsException = self.expectation(description: "update returns")
620
621 initiatorContext.fetchEscrowContents { entropy, bottleID, signingPubKey, error in
622 XCTAssertNil(entropy, "entropy should be nil")
623 XCTAssertNil(bottleID, "bottleID should be nil")
624 XCTAssertNil(signingPubKey, "signingPublicKey should be nil")
625 XCTAssertNotNil(error, "error should not be nil")
626
627 fetchEscrowContentsException.fulfill()
628 }
629 self.wait(for: [fetchEscrowContentsException], timeout: 10)
630 }
631
632 func testFetchEscrowContentsChecksEntitlement() throws {
633 self.startCKAccountStatusMock()
634
635 let contextName = OTDefaultContext
636 let containerName = OTCKContainerName
637
638 // First, fail due to not having any data
639 let fetchEscrowContentsExpectation = self.expectation(description: "fetchEscrowContentsExpectation returns")
640 self.otControl.fetchEscrowContents(containerName, contextID: contextName) { entropy, bottle, signingPublicKey, error in
641 XCTAssertNotNil(error, "error should not be nil")
642 //XCTAssertNotEqual(error.code, errSecMissingEntitlement, "Error should not be 'missing entitlement'")
643 XCTAssertNil(entropy, "entropy should be nil")
644 XCTAssertNil(bottle, "bottleID should be nil")
645 XCTAssertNil(signingPublicKey, "signingPublicKey should be nil")
646 fetchEscrowContentsExpectation.fulfill()
647 }
648 self.wait(for: [fetchEscrowContentsExpectation], timeout: 10)
649
650 // Now, fail due to the client not having an entitlement
651 self.otControlEntitlementBearer.entitlements.removeAll()
652 let failFetchEscrowContentsExpectation = self.expectation(description: "fetchEscrowContentsExpectation returns")
653 self.otControl.fetchEscrowContents(containerName, contextID: contextName) { entropy, bottle, signingPublicKey, error in
654 XCTAssertNotNil(error, "error should not be nil")
655 switch error {
656 case .some(let error as NSError):
657 XCTAssertEqual(error.domain, NSOSStatusErrorDomain, "Error should be an OS status")
658 XCTAssertEqual(error.code, Int(errSecMissingEntitlement), "Error should be 'missing entitlement'")
659 default:
660 XCTFail("Unable to turn error into NSError: \(String(describing: error))")
661 }
662
663 XCTAssertNil(entropy, "entropy should not be nil")
664 XCTAssertNil(bottle, "bottleID should not be nil")
665 XCTAssertNil(signingPublicKey, "signingPublicKey should not be nil")
666 failFetchEscrowContentsExpectation.fulfill()
667 }
668 self.wait(for: [failFetchEscrowContentsExpectation], timeout: 10)
669 }
670
671 func testJoinWithBottleFailCaseBottleDoesntExist() throws {
672 self.startCKAccountStatusMock()
673
674 let initiatorContextID = "initiator-context-id"
675 self.cuttlefishContext.startOctagonStateMachine()
676 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
677
678 let clique: OTClique
679 do {
680 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
681 XCTAssertNotNil(clique, "Clique should not be nil")
682 } catch {
683 XCTFail("Shouldn't have errored making new friends: \(error)")
684 throw error
685 }
686
687 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
688 XCTAssertNotNil(entropy, "entropy should not be nil")
689
690 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
691 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
692 self.verifyDatabaseMocks()
693
694 let bottle = self.fakeCuttlefishServer.state.bottles[0]
695 self.fakeCuttlefishServer.state.bottles.removeAll()
696
697 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
698
699 initiatorContext.startOctagonStateMachine()
700
701 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
702 initiatorContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID) { error in
703 XCTAssertNotNil(error, "error should not be nil")
704 joinWithBottleExpectation.fulfill()
705 }
706 self.wait(for: [joinWithBottleExpectation], timeout: 10)
707 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
708 }
709
710 func testJoinWithBottleFailCaseBadEscrowRecord() throws {
711 self.startCKAccountStatusMock()
712
713 let initiatorContextID = "initiator-context-id"
714
715 self.cuttlefishContext.startOctagonStateMachine()
716 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
717
718 let clique: OTClique
719 do {
720 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
721 XCTAssertNotNil(clique, "Clique should not be nil")
722 } catch {
723 XCTFail("Shouldn't have errored making new friends: \(error)")
724 throw error
725 }
726
727 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
728 XCTAssertNotNil(entropy, "entropy should not be nil")
729
730 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
731 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
732 self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
733 self.verifyDatabaseMocks()
734
735 _ = self.fakeCuttlefishServer.state.bottles[0]
736
737 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
738
739 initiatorContext.startOctagonStateMachine()
740
741 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
742 initiatorContext.join(withBottle: "sos peer id", entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID) { error in
743 XCTAssertNotNil(error, "error should be nil")
744 joinWithBottleExpectation.fulfill()
745 }
746 self.wait(for: [joinWithBottleExpectation], timeout: 10)
747 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
748 }
749
750 func testJoinWithBottleFailCaseBadEntropy() throws {
751 self.startCKAccountStatusMock()
752
753 let initiatorContextID = "initiator-context-id"
754 self.cuttlefishContext.startOctagonStateMachine()
755 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
756
757 let clique: OTClique
758 do {
759 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
760 XCTAssertNotNil(clique, "Clique should not be nil")
761 } catch {
762 XCTFail("Shouldn't have errored making new friends: \(error)")
763 throw error
764 }
765
766 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
767 XCTAssertNotNil(entropy, "entropy should not be nil")
768
769 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
770 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
771 self.verifyDatabaseMocks()
772
773 let bottle = self.fakeCuttlefishServer.state.bottles[0]
774
775 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
776
777 initiatorContext.startOctagonStateMachine()
778
779 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
780
781 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
782 initiatorContext.join(withBottle: bottle.bottleID, entropy: Data(count: 72), bottleSalt: self.otcliqueContext.altDSID) { error in
783 XCTAssertNotNil(error, "error should not be nil, when entropy is missing")
784 joinWithBottleExpectation.fulfill()
785 }
786 self.wait(for: [joinWithBottleExpectation], timeout: 10)
787 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
788 }
789
790 func testJoinWithBottleFailCaseBadBottleSalt() throws {
791 self.startCKAccountStatusMock()
792
793 let initiatorContextID = "initiator-context-id"
794 self.cuttlefishContext.startOctagonStateMachine()
795 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
796
797 let clique: OTClique
798 do {
799 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
800 XCTAssertNotNil(clique, "Clique should not be nil")
801 } catch {
802 XCTFail("Shouldn't have errored making new friends: \(error)")
803 throw error
804 }
805
806 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
807 XCTAssertNotNil(entropy, "entropy should not be nil")
808
809 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
810 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
811 self.verifyDatabaseMocks()
812
813 let bottle = self.fakeCuttlefishServer.state.bottles[0]
814
815 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
816
817 initiatorContext.startOctagonStateMachine()
818
819 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
820
821 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
822 initiatorContext.join(withBottle: bottle.bottleID, entropy: Data(count: 72), bottleSalt: "123456789") { error in
823 XCTAssertNotNil(error, "error should not be nil with bad entropy and salt")
824 joinWithBottleExpectation.fulfill()
825 }
826 self.wait(for: [joinWithBottleExpectation], timeout: 10)
827 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
828 }
829
830 func testRecoverFromDeviceNotOnMachineIDList() throws {
831 self.startCKAccountStatusMock()
832
833 let clique: OTClique
834 do {
835 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
836 XCTAssertNotNil(clique, "Clique should not be nil")
837 } catch {
838 XCTFail("Shouldn't have errored making new friends: \(error)")
839 throw error
840 }
841
842 let firstPeerID = clique.cliqueMemberIdentifier
843 XCTAssertNotNil(firstPeerID, "Clique should have a member identifier")
844 let entropy = try self.loadSecret(label: firstPeerID!)
845 XCTAssertNotNil(entropy, "entropy should not be nil")
846
847 let bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
848 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
849 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 1, "preferredBottleIDs should have 1 bottle")
850 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
851
852 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
853 self.verifyDatabaseMocks()
854 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
855
856 let bottle = self.fakeCuttlefishServer.state.bottles[0]
857
858 // To get into the state we need, we need to introduce peer B and C. C should then distrust A, whose bottle it used
859 // B shouldn't have an opinion of C.
860
861 let bNewOTCliqueContext = OTConfigurationContext()
862 bNewOTCliqueContext.context = "restoreB"
863 bNewOTCliqueContext.dsid = self.otcliqueContext.dsid
864 bNewOTCliqueContext.altDSID = self.otcliqueContext.altDSID
865 bNewOTCliqueContext.otControl = self.otcliqueContext.otControl
866 bNewOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottle.bottleID, entropy: entropy!)
867
868 let deviceBmockAuthKit = OTMockAuthKitAdapter(altDSID: self.otcliqueContext.altDSID,
869 machineID: "b-machine-id",
870 otherDevices: [self.mockAuthKit.currentMachineID])
871
872 let bRestoreContext = self.manager.context(forContainerName: OTCKContainerName,
873 contextID: bNewOTCliqueContext.context!,
874 sosAdapter: OTSOSMissingAdapter(),
875 authKitAdapter: deviceBmockAuthKit,
876 lockStateTracker: self.lockStateTracker,
877 accountStateTracker: self.accountStateTracker,
878 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
879 bRestoreContext.startOctagonStateMachine()
880 let bNewClique: OTClique
881 do {
882 bNewClique = try OTClique.performEscrowRecovery(withContextData: bNewOTCliqueContext, escrowArguments: [:])
883 XCTAssertNotNil(bNewClique, "bNewClique should not be nil")
884 } catch {
885 XCTFail("Shouldn't have errored recovering: \(error)")
886 throw error
887 }
888 self.assertEnters(context: bRestoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
889
890 // And introduce C, which will kick out A
891 // During the next sign in, the machine ID list has changed to just the new one
892 let restoremockAuthKit = OTMockAuthKitAdapter(altDSID: self.otcliqueContext.altDSID,
893 machineID: "c-machine-id",
894 otherDevices: [self.mockAuthKit.currentMachineID, deviceBmockAuthKit.currentMachineID])
895 let restoreContext = self.manager.context(forContainerName: OTCKContainerName,
896 contextID: "restoreContext",
897 sosAdapter: OTSOSMissingAdapter(),
898 authKitAdapter: restoremockAuthKit,
899 lockStateTracker: self.lockStateTracker,
900 accountStateTracker: self.accountStateTracker,
901 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
902
903 restoreContext.startOctagonStateMachine()
904 let newOTCliqueContext = OTConfigurationContext()
905 newOTCliqueContext.context = "restoreContext"
906 newOTCliqueContext.dsid = self.otcliqueContext.dsid
907 newOTCliqueContext.altDSID = self.otcliqueContext.altDSID
908 newOTCliqueContext.otControl = self.otcliqueContext.otControl
909 newOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottle.bottleID, entropy: entropy!)
910
911 let newClique: OTClique
912 do {
913 newClique = try OTClique.performEscrowRecovery(withContextData: newOTCliqueContext, escrowArguments: [:])
914 XCTAssertNotNil(newClique, "newClique should not be nil")
915 } catch {
916 XCTFail("Shouldn't have errored recovering: \(error)")
917 throw error
918 }
919
920 self.assertEnters(context: restoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
921 self.verifyDatabaseMocks()
922 self.assertConsidersSelfTrusted(context: restoreContext)
923
924 let restoreDumpCallback = self.expectation(description: "acceptorDumpCallback callback occurs")
925 self.tphClient.dump(withContainer: OTCKContainerName, context: newOTCliqueContext.context!) {
926 dump, _ in
927 XCTAssertNotNil(dump, "dump should not be nil")
928 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
929 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
930 let dynamicInfo = egoSelf!["dynamicInfo"] as? Dictionary<String, AnyObject>
931 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
932 let included = dynamicInfo!["included"] as? Array<String>
933 XCTAssertNotNil(included, "included should not be nil")
934 XCTAssertEqual(included!.count, 3, "should be 3 peer id included")
935
936 restoreDumpCallback.fulfill()
937 }
938 self.wait(for: [restoreDumpCallback], timeout: 10)
939
940 // Now, exclude peer A's machine ID
941 restoremockAuthKit.otherDevices = [deviceBmockAuthKit.currentMachineID]
942
943 // Peer C should upload a new trust status
944 let updateTrustExpectation = self.expectation(description: "updateTrust")
945 self.fakeCuttlefishServer.updateListener = { request in
946 XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
947 let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
948 sig: request.dynamicInfoAndSig.sig)
949 XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")
950
951 XCTAssertEqual(newDynamicInfo!.excludedPeerIDs.count, 1, "Should have a single excluded peer")
952 updateTrustExpectation.fulfill()
953 return nil
954 }
955
956 restoreContext.incompleteNotificationOfMachineIDListChange()
957 self.wait(for: [updateTrustExpectation], timeout: 10)
958
959 self.assertEnters(context: restoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
960 self.verifyDatabaseMocks()
961 self.assertConsidersSelfTrusted(context: restoreContext)
962 }
963
964 func testCachedBottleFetch() throws {
965 let initiatorContextID = "initiator-context-id"
966 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
967
968 bottlerContext.startOctagonStateMachine()
969 let ckacctinfo = CKAccountInfo()
970 ckacctinfo.accountStatus = .available
971 ckacctinfo.hasValidCredentials = true
972 ckacctinfo.accountPartition = .production
973
974 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
975 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
976
977 let clique: OTClique
978 let bottlerotcliqueContext = OTConfigurationContext()
979 bottlerotcliqueContext.context = initiatorContextID
980 bottlerotcliqueContext.dsid = "1234"
981 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
982 bottlerotcliqueContext.otControl = self.otControl
983 do {
984 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
985 XCTAssertNotNil(clique, "Clique should not be nil")
986 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
987 } catch {
988 XCTFail("Shouldn't have errored making new friends: \(error)")
989 throw error
990 }
991
992 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
993 self.assertConsidersSelfTrusted(context: bottlerContext)
994
995 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
996 XCTAssertNotNil(entropy, "entropy should not be nil")
997
998 // Fake that this peer also created some TLKShares for itself
999 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
1000 try self.putSelfTLKShareInCloudKit(context: bottlerContext, zoneID: self.manateeZoneID)
1001
1002 let bottle = self.fakeCuttlefishServer.state.bottles[0]
1003
1004 self.cuttlefishContext.startOctagonStateMachine()
1005 self.startCKAccountStatusMock()
1006 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1007
1008 // Try to enforce that that CKKS doesn't know about the key hierarchy until Octagon asks it
1009 self.holdCloudKitFetches()
1010
1011 // Note: CKKS will want to upload a TLKShare for its self
1012 self.expectCKModifyKeyRecords(0, currentKeyPointerRecords: 0, tlkShareRecords: 1, zoneID: self.manateeZoneID)
1013
1014 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
1015 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID) { error in
1016 XCTAssertNil(error, "error should be nil")
1017 joinWithBottleExpectation.fulfill()
1018 }
1019
1020 sleep(1)
1021 self.releaseCloudKitFetchHold()
1022
1023 self.wait(for: [joinWithBottleExpectation], timeout: 100)
1024
1025 let dumpCallback = self.expectation(description: "dumpCallback callback occurs")
1026 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) {
1027 dump, _ in
1028 XCTAssertNotNil(dump, "dump should not be nil")
1029 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
1030 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
1031 let dynamicInfo = egoSelf!["dynamicInfo"] as? Dictionary<String, AnyObject>
1032 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
1033 let included = dynamicInfo!["included"] as? Array<String>
1034 XCTAssertNotNil(included, "included should not be nil")
1035 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
1036 dumpCallback.fulfill()
1037 }
1038 self.wait(for: [dumpCallback], timeout: 10)
1039
1040 self.verifyDatabaseMocks()
1041 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1042 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
1043
1044 //now call fetchviablebottles, we should get the uncached version
1045 let fetchUnCachedViableBottlesExpectation = self.expectation(description: "fetch UnCached ViableBottles")
1046
1047 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1048 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1049 fetchUnCachedViableBottlesExpectation.fulfill()
1050 return nil
1051 }
1052 let FetchAllViableBottles = self.expectation(description: "FetchAllViableBottles callback occurs")
1053
1054 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1055 XCTAssertNil(error, "should be no error fetching viable bottles")
1056 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1057 FetchAllViableBottles.fulfill()
1058 }
1059 self.wait(for: [FetchAllViableBottles], timeout: 10)
1060 self.wait(for: [fetchUnCachedViableBottlesExpectation], timeout: 10)
1061
1062 let fetchViableExpectation = self.expectation(description: "fetchViableBottles callback occurs")
1063 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1064 XCTAssertNil(error, "should be no error fetching viable bottles")
1065 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1066 fetchViableExpectation.fulfill()
1067 }
1068 self.wait(for: [fetchViableExpectation], timeout: 10)
1069
1070 //now call fetchviablebottles, we should get the cached version
1071 let fetchViableBottlesExpectation = self.expectation(description: "fetch Cached ViableBottles")
1072 fetchViableBottlesExpectation.isInverted = true
1073
1074 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1075 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1076 fetchViableBottlesExpectation.fulfill()
1077 return nil
1078 }
1079 let fetchExpectation = self.expectation(description: "fetchExpectation callback occurs")
1080
1081 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1082 XCTAssertNil(error, "should be no error fetching viable bottles")
1083 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1084 fetchExpectation.fulfill()
1085 }
1086 self.wait(for: [fetchExpectation], timeout: 10)
1087 self.wait(for: [fetchViableBottlesExpectation], timeout: 10)
1088 }
1089
1090 func testViableBottleCachingAfterJoin() throws {
1091 let initiatorContextID = "initiator-context-id"
1092 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
1093
1094 bottlerContext.startOctagonStateMachine()
1095 let ckacctinfo = CKAccountInfo()
1096 ckacctinfo.accountStatus = .available
1097 ckacctinfo.hasValidCredentials = true
1098 ckacctinfo.accountPartition = .production
1099
1100 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
1101 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1102
1103 let clique: OTClique
1104 let bottlerotcliqueContext = OTConfigurationContext()
1105 bottlerotcliqueContext.context = initiatorContextID
1106 bottlerotcliqueContext.dsid = "1234"
1107 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
1108 bottlerotcliqueContext.otControl = self.otControl
1109 do {
1110 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
1111 XCTAssertNotNil(clique, "Clique should not be nil")
1112 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
1113 } catch {
1114 XCTFail("Shouldn't have errored making new friends: \(error)")
1115 throw error
1116 }
1117
1118 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1119 self.assertConsidersSelfTrusted(context: bottlerContext)
1120
1121 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
1122 XCTAssertNotNil(entropy, "entropy should not be nil")
1123
1124 // Fake that this peer also created some TLKShares for itself
1125 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
1126 try self.putSelfTLKShareInCloudKit(context: bottlerContext, zoneID: self.manateeZoneID)
1127
1128 let bottle = self.fakeCuttlefishServer.state.bottles[0]
1129
1130 self.cuttlefishContext.startOctagonStateMachine()
1131 self.startCKAccountStatusMock()
1132 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1133
1134 // Try to enforce that that CKKS doesn't know about the key hierarchy until Octagon asks it
1135 self.holdCloudKitFetches()
1136
1137 // Note: CKKS will want to upload a TLKShare for its self
1138 self.expectCKModifyKeyRecords(0, currentKeyPointerRecords: 0, tlkShareRecords: 1, zoneID: self.manateeZoneID)
1139
1140 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
1141 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID) { error in
1142 XCTAssertNil(error, "error should be nil")
1143 joinWithBottleExpectation.fulfill()
1144 }
1145
1146 sleep(1)
1147 self.releaseCloudKitFetchHold()
1148
1149 self.wait(for: [joinWithBottleExpectation], timeout: 100)
1150
1151 let dumpCallback = self.expectation(description: "dumpCallback callback occurs")
1152 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) {
1153 dump, _ in
1154 XCTAssertNotNil(dump, "dump should not be nil")
1155 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
1156 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
1157 let dynamicInfo = egoSelf!["dynamicInfo"] as? Dictionary<String, AnyObject>
1158 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
1159 let included = dynamicInfo!["included"] as? Array<String>
1160 XCTAssertNotNil(included, "included should not be nil")
1161 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
1162 dumpCallback.fulfill()
1163 }
1164 self.wait(for: [dumpCallback], timeout: 10)
1165
1166 self.verifyDatabaseMocks()
1167 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1168 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
1169
1170 //now call fetchviablebottles, we should get the uncached version
1171 let fetchUnCachedViableBottlesExpectation = self.expectation(description: "fetch UnCached ViableBottles")
1172
1173 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1174 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1175 fetchUnCachedViableBottlesExpectation.fulfill()
1176 return nil
1177 }
1178 let FetchAllViableBottles = self.expectation(description: "FetchAllViableBottles callback occurs")
1179
1180 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1181 XCTAssertNil(error, "should be no error fetching viable bottles")
1182 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1183 FetchAllViableBottles.fulfill()
1184 }
1185 self.wait(for: [FetchAllViableBottles], timeout: 10)
1186 self.wait(for: [fetchUnCachedViableBottlesExpectation], timeout: 10)
1187
1188 let fetchViableExpectation = self.expectation(description: "fetchViableBottles callback occurs")
1189 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1190 XCTAssertNil(error, "should be no error fetching viable bottles")
1191 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1192 fetchViableExpectation.fulfill()
1193 }
1194 self.wait(for: [fetchViableExpectation], timeout: 10)
1195
1196 //now call fetchviablebottles, we should get the cached version
1197 let fetchViableBottlesExpectation = self.expectation(description: "fetch Cached ViableBottles")
1198 fetchViableBottlesExpectation.isInverted = true
1199
1200 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1201 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1202 fetchViableBottlesExpectation.fulfill()
1203 return nil
1204 }
1205 let fetchExpectation = self.expectation(description: "fetchExpectation callback occurs")
1206
1207 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1208 XCTAssertNil(error, "should be no error fetching viable bottles")
1209 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1210 fetchExpectation.fulfill()
1211 }
1212 self.wait(for: [fetchExpectation], timeout: 10)
1213 self.wait(for: [fetchViableBottlesExpectation], timeout: 10)
1214 }
1215
1216 func testViableBottleReturns1Bottle() throws {
1217 let initiatorContextID = "initiator-context-id"
1218 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
1219
1220 bottlerContext.startOctagonStateMachine()
1221 let ckacctinfo = CKAccountInfo()
1222 ckacctinfo.accountStatus = .available
1223 ckacctinfo.hasValidCredentials = true
1224 ckacctinfo.accountPartition = .production
1225
1226 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
1227 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1228
1229 let clique: OTClique
1230 let bottlerotcliqueContext = OTConfigurationContext()
1231 bottlerotcliqueContext.context = initiatorContextID
1232 bottlerotcliqueContext.dsid = "1234"
1233 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
1234 bottlerotcliqueContext.otControl = self.otControl
1235 do {
1236 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
1237 XCTAssertNotNil(clique, "Clique should not be nil")
1238 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
1239 } catch {
1240 XCTFail("Shouldn't have errored making new friends: \(error)")
1241 throw error
1242 }
1243
1244 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1245 self.assertConsidersSelfTrusted(context: bottlerContext)
1246
1247 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
1248 XCTAssertNotNil(entropy, "entropy should not be nil")
1249
1250 // Fake that this peer also created some TLKShares for itself
1251 self.putFakeKeyHierarchy(inCloudKit: self.manateeZoneID)
1252 try self.putSelfTLKShareInCloudKit(context: bottlerContext, zoneID: self.manateeZoneID)
1253
1254 let bottle = self.fakeCuttlefishServer.state.bottles[0]
1255
1256 self.cuttlefishContext.startOctagonStateMachine()
1257 self.startCKAccountStatusMock()
1258 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1259
1260 // Try to enforce that that CKKS doesn't know about the key hierarchy until Octagon asks it
1261 self.holdCloudKitFetches()
1262
1263 // Note: CKKS will want to upload a TLKShare for its self
1264 self.expectCKModifyKeyRecords(0, currentKeyPointerRecords: 0, tlkShareRecords: 1, zoneID: self.manateeZoneID)
1265
1266 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
1267 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID) { error in
1268 XCTAssertNil(error, "error should be nil")
1269 joinWithBottleExpectation.fulfill()
1270 }
1271
1272 sleep(1)
1273 self.releaseCloudKitFetchHold()
1274
1275 self.wait(for: [joinWithBottleExpectation], timeout: 100)
1276
1277 var egoPeerID: String?
1278
1279 let dumpCallback = self.expectation(description: "dumpCallback callback occurs")
1280 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) {
1281 dump, _ in
1282 XCTAssertNotNil(dump, "dump should not be nil")
1283 let egoSelf = dump!["self"] as? Dictionary<String, AnyObject>
1284 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
1285 egoPeerID = egoSelf!["peerID"] as? String
1286 let dynamicInfo = egoSelf!["dynamicInfo"] as? Dictionary<String, AnyObject>
1287 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
1288 let included = dynamicInfo!["included"] as? Array<String>
1289 XCTAssertNotNil(included, "included should not be nil")
1290 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
1291 dumpCallback.fulfill()
1292 }
1293 self.wait(for: [dumpCallback], timeout: 10)
1294
1295 self.verifyDatabaseMocks()
1296 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1297 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
1298
1299 let bottles: [Bottle] = self.fakeCuttlefishServer.state.bottles
1300 var bottleToExclude: String?
1301 bottles.forEach { bottle in
1302 if bottle.peerID == egoPeerID {
1303 bottleToExclude = bottle.bottleID
1304 }
1305 }
1306
1307 XCTAssertNotNil(bottleToExclude, "bottleToExclude should not be nil")
1308
1309 //now call fetchviablebottles, we should get the uncached version
1310 var fetchUnCachedViableBottlesExpectation = self.expectation(description: "fetch UnCached ViableBottles")
1311
1312 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1313 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1314 fetchUnCachedViableBottlesExpectation.fulfill()
1315 return nil
1316 }
1317 self.fakeCuttlefishServer.fetchViableBottlesDontReturnBottleWithID = bottleToExclude
1318 var FetchAllViableBottles = self.expectation(description: "FetchAllViableBottles callback occurs")
1319
1320 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1321 XCTAssertNil(error, "should be no error fetching viable bottles")
1322 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1323 FetchAllViableBottles.fulfill()
1324 }
1325 self.wait(for: [FetchAllViableBottles], timeout: 10)
1326 self.wait(for: [fetchUnCachedViableBottlesExpectation], timeout: 10)
1327
1328 //now call fetchviablebottles, we should get the uncached version
1329 fetchUnCachedViableBottlesExpectation = self.expectation(description: "fetch UnCached ViableBottles")
1330
1331 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1332 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1333 fetchUnCachedViableBottlesExpectation.fulfill()
1334 return nil
1335 }
1336
1337 self.fakeCuttlefishServer.fetchViableBottlesDontReturnBottleWithID = bottleToExclude
1338
1339 FetchAllViableBottles = self.expectation(description: "FetchAllViableBottles callback occurs")
1340
1341 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1342 XCTAssertNil(error, "should be no error fetching viable bottles")
1343 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1344 FetchAllViableBottles.fulfill()
1345 }
1346 self.wait(for: [FetchAllViableBottles], timeout: 10)
1347 self.wait(for: [fetchUnCachedViableBottlesExpectation], timeout: 10)
1348 }
1349 }
1350
1351 #endif