4 class OctagonEscrowRecoveryTests: OctagonTestsBase {
5 override func setUp() {
9 func testJoinWithBottle() throws {
10 let initiatorContextID = "initiator-context-id"
11 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
13 bottlerContext.startOctagonStateMachine()
14 let ckacctinfo = CKAccountInfo()
15 ckacctinfo.accountStatus = .available
16 ckacctinfo.hasValidCredentials = true
17 ckacctinfo.accountPartition = .production
19 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
20 XCTAssertNoThrow(try bottlerContext.setCDPEnabled())
21 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
24 let bottlerotcliqueContext = OTConfigurationContext()
25 bottlerotcliqueContext.context = initiatorContextID
26 bottlerotcliqueContext.dsid = "1234"
27 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
28 bottlerotcliqueContext.otControl = self.otControl
30 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
31 XCTAssertNotNil(clique, "Clique should not be nil")
32 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
34 XCTFail("Shouldn't have errored making new friends: \(error)")
38 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
39 self.assertConsidersSelfTrusted(context: bottlerContext)
41 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
42 XCTAssertNotNil(entropy, "entropy should not be nil")
44 // Fake that this peer also created some TLKShares for itself
45 self.putFakeKeyHierarchiesInCloudKit()
46 try self.putSelfTLKSharesInCloudKit(context: bottlerContext)
48 let bottle = self.fakeCuttlefishServer.state.bottles[0]
50 self.cuttlefishContext.startOctagonStateMachine()
51 self.startCKAccountStatusMock()
52 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
54 // Try to enforce that that CKKS doesn't know about the key hierarchy until Octagon asks it
55 self.holdCloudKitFetches()
57 // Note: CKKS will want to upload a TLKShare for its self
58 self.assertAllCKKSViewsUpload(tlkShares: 1)
60 // Before you call joinWithBottle, you need to call fetchViableBottles.
61 let fetchViableExpectation = self.expectation(description: "fetchViableBottles callback occurs")
62 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
63 XCTAssertNil(error, "should be no error fetching viable bottles")
64 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
65 fetchViableExpectation.fulfill()
67 self.wait(for: [fetchViableExpectation], timeout: 10)
69 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
70 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID!) { error in
71 XCTAssertNil(error, "error should be nil")
72 joinWithBottleExpectation.fulfill()
76 self.releaseCloudKitFetchHold()
78 self.wait(for: [joinWithBottleExpectation], timeout: 100)
80 let dumpCallback = self.expectation(description: "dumpCallback callback occurs")
81 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) { dump, _ in
82 XCTAssertNotNil(dump, "dump should not be nil")
83 let egoSelf = dump!["self"] as? [String: AnyObject]
84 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
85 let dynamicInfo = egoSelf!["dynamicInfo"] as? [String: AnyObject]
86 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
87 let included = dynamicInfo!["included"] as? [String]
88 XCTAssertNotNil(included, "included should not be nil")
89 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
90 dumpCallback.fulfill()
92 self.wait(for: [dumpCallback], timeout: 10)
94 self.verifyDatabaseMocks()
95 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
96 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
99 func testBottleRestoreEntersOctagonReady() throws {
100 self.startCKAccountStatusMock()
102 let initiatorContextID = "initiator-context-id"
103 self.cuttlefishContext.startOctagonStateMachine()
104 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
105 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
109 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
110 XCTAssertNotNil(clique, "Clique should not be nil")
112 XCTFail("Shouldn't have errored making new friends: \(error)")
116 self.verifyDatabaseMocks()
118 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
119 XCTAssertNotNil(entropy, "entropy should not be nil")
121 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
122 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
124 let bottle = self.fakeCuttlefishServer.state.bottles[0]
126 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
128 initiatorContext.startOctagonStateMachine()
129 let restoreExpectation = self.expectation(description: "restore returns")
131 self.manager!.restore(OTCKContainerName, contextID: initiatorContextID, bottleSalt: self.otcliqueContext.altDSID!, entropy: entropy!, bottleID: bottle.bottleID) { error in
132 XCTAssertNil(error, "error should be nil")
133 restoreExpectation.fulfill()
135 self.wait(for: [restoreExpectation], timeout: 10)
137 self.assertEnters(context: initiatorContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
139 let initiatorDumpCallback = self.expectation(description: "initiatorDumpCallback callback occurs")
140 self.tphClient.dump(withContainer: OTCKContainerName, context: initiatorContextID) { dump, _ in
141 XCTAssertNotNil(dump, "dump should not be nil")
142 let egoSelf = dump!["self"] as? [String: AnyObject]
143 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
144 let dynamicInfo = egoSelf!["dynamicInfo"] as? [String: AnyObject]
145 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
146 let included = dynamicInfo!["included"] as? [String]
147 XCTAssertNotNil(included, "included should not be nil")
148 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
150 initiatorDumpCallback.fulfill()
152 self.wait(for: [initiatorDumpCallback], timeout: 10)
155 func testJoinWithBottleWithCKKSConflict() throws {
156 let initiatorContextID = "initiator-context-id"
157 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
159 bottlerContext.startOctagonStateMachine()
160 let ckacctinfo = CKAccountInfo()
161 ckacctinfo.accountStatus = .available
162 ckacctinfo.hasValidCredentials = true
163 ckacctinfo.accountPartition = .production
165 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
166 XCTAssertNoThrow(try bottlerContext.setCDPEnabled())
167 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
170 let bottlerotcliqueContext = OTConfigurationContext()
171 bottlerotcliqueContext.context = initiatorContextID
172 bottlerotcliqueContext.dsid = "1234"
173 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
174 bottlerotcliqueContext.otControl = self.otControl
176 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
177 XCTAssertNotNil(clique, "Clique should not be nil")
178 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
180 XCTFail("Shouldn't have errored making new friends: \(error)")
184 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
185 self.assertConsidersSelfTrusted(context: bottlerContext)
187 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
188 XCTAssertNotNil(entropy, "entropy should not be nil")
190 let bottle = self.fakeCuttlefishServer.state.bottles[0]
192 // During the join, there's a CKKS key race
193 self.silentFetchesAllowed = false
194 self.expectCKFetchAndRun {
195 self.putFakeKeyHierarchiesInCloudKit()
196 self.putFakeDeviceStatusesInCloudKit()
197 self.silentFetchesAllowed = true
200 self.cuttlefishContext.startOctagonStateMachine()
201 self.startCKAccountStatusMock()
202 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
204 // Before you call joinWithBottle, you need to call fetchViableBottles.
205 let fetchViableExpectation = self.expectation(description: "fetchViableBottles callback occurs")
206 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
207 XCTAssertNil(error, "should be no error fetching viable bottles")
208 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
209 fetchViableExpectation.fulfill()
211 self.wait(for: [fetchViableExpectation], timeout: 10)
213 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
214 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID!) { error in
215 XCTAssertNil(error, "error should be nil")
216 joinWithBottleExpectation.fulfill()
219 self.wait(for: [joinWithBottleExpectation], timeout: 100)
221 self.verifyDatabaseMocks()
222 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
223 assertAllCKKSViews(enter: SecCKKSZoneKeyStateWaitForTLK, within: 10 * NSEC_PER_SEC)
226 func testBottleRestoreWithSameMachineID() throws {
227 self.startCKAccountStatusMock()
229 self.cuttlefishContext.startOctagonStateMachine()
230 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
231 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
235 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
236 XCTAssertNotNil(clique, "Clique should not be nil")
238 XCTFail("Shouldn't have errored making new friends: \(error)")
242 self.verifyDatabaseMocks()
244 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
245 XCTAssertNotNil(entropy, "entropy should not be nil")
247 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
248 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
250 let bottle = self.fakeCuttlefishServer.state.bottles[0]
252 // some other peer should restore from this bottle
253 let differentDevice = self.makeInitiatorContext(contextID: "differenDevice")
254 let differentRestoreExpectation = self.expectation(description: "different restore returns")
255 differentDevice.startOctagonStateMachine()
256 differentDevice.join(withBottle: bottle.bottleID,
258 bottleSalt: self.otcliqueContext.altDSID!) { error in
259 XCTAssertNil(error, "error should be nil")
260 differentRestoreExpectation.fulfill()
262 self.wait(for: [differentRestoreExpectation], timeout: 10)
264 self.assertTrusts(context: differentDevice, includedPeerIDCount: 2, excludedPeerIDCount: 0)
266 // The first peer will upload TLKs for the new peer
267 self.assertAllCKKSViewsUpload(tlkShares: 1)
268 self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
269 self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
270 self.verifyDatabaseMocks()
272 self.assertTrusts(context: self.cuttlefishContext, includedPeerIDCount: 2, excludedPeerIDCount: 0)
274 // Explicitly use the same authkit parameters as the original peer when restoring this time
275 let restoreContext = self.makeInitiatorContext(contextID: "restoreContext", authKitAdapter: self.mockAuthKit)
277 restoreContext.startOctagonStateMachine()
279 let restoreExpectation = self.expectation(description: "restore returns")
280 restoreContext.join(withBottle: bottle.bottleID,
282 bottleSalt: self.otcliqueContext.altDSID!) { error in
283 XCTAssertNil(error, "error should be nil")
284 restoreExpectation.fulfill()
286 self.wait(for: [restoreExpectation], timeout: 10)
288 self.assertEnters(context: restoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
289 self.assertConsidersSelfTrusted(context: restoreContext)
291 // The restore context should exclude its sponsor
292 self.assertTrusts(context: restoreContext, includedPeerIDCount: 2, excludedPeerIDCount: 1)
294 // Then, the remote peer should trust the new peer and exclude the original
295 self.sendContainerChangeWaitForFetchForStates(context: differentDevice, states: [OctagonStateReadyUpdated, OctagonStateReady])
296 self.assertTrusts(context: differentDevice, includedPeerIDCount: 2, excludedPeerIDCount: 1)
298 // Then, if by some strange miracle the original peer is still around, it should bail (as it's now untrusted)
299 self.sendContainerChangeWaitForFetchForStates(context: self.cuttlefishContext, states: [OctagonStateUntrusted])
300 self.assertConsidersSelfUntrusted(context: self.cuttlefishContext)
301 self.assertTrusts(context: self.cuttlefishContext, includedPeerIDCount: 0, excludedPeerIDCount: 1)
304 func testRestoreSPIFromPiggybackingState() throws {
305 self.startCKAccountStatusMock()
307 let initiatorContextID = "initiator-context-id"
308 self.cuttlefishContext.startOctagonStateMachine()
309 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
310 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
314 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
315 XCTAssertNotNil(clique, "Clique should not be nil")
317 XCTFail("Shouldn't have errored making new friends: \(error)")
320 self.verifyDatabaseMocks()
322 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
323 XCTAssertNotNil(entropy, "entropy should not be nil")
325 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
326 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
328 let bottle = self.fakeCuttlefishServer.state.bottles[0]
330 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
332 initiatorContext.startOctagonStateMachine()
334 XCTAssertNoThrow(try initiatorContext.setCDPEnabled())
335 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
337 let restoreExpectation = self.expectation(description: "restore returns")
339 self.manager!.restore(OTCKContainerName, contextID: initiatorContextID, bottleSalt: self.otcliqueContext.altDSID!, entropy: entropy!, bottleID: bottle.bottleID) { error in
340 XCTAssertNil(error, "error should be nil")
341 restoreExpectation.fulfill()
343 self.wait(for: [restoreExpectation], timeout: 10)
345 let initiatorDumpCallback = self.expectation(description: "initiatorDumpCallback callback occurs")
346 self.tphClient.dump(withContainer: OTCKContainerName, context: initiatorContextID) { dump, _ in
347 XCTAssertNotNil(dump, "dump should not be nil")
348 let egoSelf = dump!["self"] as? [String: AnyObject]
349 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
350 let dynamicInfo = egoSelf!["dynamicInfo"] as? [String: AnyObject]
351 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
352 let included = dynamicInfo!["included"] as? [String]
353 XCTAssertNotNil(included, "included should not be nil")
354 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
356 initiatorDumpCallback.fulfill()
358 self.wait(for: [initiatorDumpCallback], timeout: 10)
361 func testRestoreBadBottleIDFails() throws {
362 self.startCKAccountStatusMock()
364 let initiatorContextID = "initiator-context-id"
365 self.cuttlefishContext.startOctagonStateMachine()
366 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
367 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
371 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
372 XCTAssertNotNil(clique, "Clique should not be nil")
374 XCTFail("Shouldn't have errored making new friends: \(error)")
378 self.verifyDatabaseMocks()
380 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
381 XCTAssertNotNil(entropy, "entropy should not be nil")
383 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
384 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
386 _ = self.fakeCuttlefishServer.state.bottles[0]
388 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
390 initiatorContext.startOctagonStateMachine()
392 let restoreExpectation = self.expectation(description: "restore returns")
394 self.manager!.restore(OTCKContainerName, contextID: initiatorContextID, bottleSalt: self.otcliqueContext.altDSID!, entropy: entropy!, bottleID: "bad escrow record ID") { error in
395 XCTAssertNotNil(error, "error should not be nil")
396 restoreExpectation.fulfill()
398 self.wait(for: [restoreExpectation], timeout: 10)
399 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
401 let acceptorDumpCallback = self.expectation(description: "acceptorDumpCallback callback occurs")
402 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) { dump, _ in
403 XCTAssertNotNil(dump, "dump should not be nil")
404 let egoSelf = dump!["self"] as? [String: AnyObject]
405 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
406 let dynamicInfo = egoSelf!["dynamicInfo"] as? [String: AnyObject]
407 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
408 let included = dynamicInfo!["included"] as? [String]
409 XCTAssertNotNil(included, "included should not be nil")
410 XCTAssertEqual(included!.count, 1, "should be 1 peer ids")
412 acceptorDumpCallback.fulfill()
414 self.wait(for: [acceptorDumpCallback], timeout: 10)
417 func testRestoreOptimalBottleIDs() throws {
418 self.startCKAccountStatusMock()
422 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
423 XCTAssertNotNil(clique, "Clique should not be nil")
425 XCTFail("Shouldn't have errored making new friends: \(error)")
429 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
430 XCTAssertNotNil(entropy, "entropy should not be nil")
432 var bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
433 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
434 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 1, "preferredBottleIDs should have 1 bottle")
435 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
437 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
438 self.verifyDatabaseMocks()
439 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
441 let bottle = self.fakeCuttlefishServer.state.bottles[0]
443 let initiatorContext = self.manager.context(forContainerName: OTCKContainerName,
444 contextID: "restoreContext",
445 sosAdapter: OTSOSMissingAdapter(),
446 authKitAdapter: self.mockAuthKit2,
447 lockStateTracker: self.lockStateTracker,
448 accountStateTracker: self.accountStateTracker,
449 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
451 initiatorContext.startOctagonStateMachine()
452 let newOTCliqueContext = OTConfigurationContext()
453 newOTCliqueContext.context = "restoreContext"
454 newOTCliqueContext.dsid = self.otcliqueContext.dsid
455 newOTCliqueContext.altDSID = self.otcliqueContext.altDSID
456 newOTCliqueContext.otControl = self.otcliqueContext.otControl
457 newOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottle.bottleID, entropy: entropy!)
459 let newClique: OTClique
461 newClique = try OTClique.performEscrowRecovery(withContextData: newOTCliqueContext, escrowArguments: [:])
462 XCTAssertNotNil(newClique, "newClique should not be nil")
464 XCTFail("Shouldn't have errored recovering: \(error)")
468 // We will upload a new TLK for the new peer
469 self.assertAllCKKSViewsUpload(tlkShares: 1)
470 self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
471 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
473 self.sendContainerChangeWaitForFetch(context: initiatorContext)
474 bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
475 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
476 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 2, "preferredBottleIDs should have 2 bottle")
477 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
480 func testRestoreFromEscrowContents() throws {
481 self.startCKAccountStatusMock()
485 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
486 XCTAssertNotNil(clique, "Clique should not be nil")
488 XCTFail("Shouldn't have errored making new friends: \(error)")
492 var bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
493 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
494 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 1, "preferredBottleIDs should have 1 bottle")
495 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
497 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
498 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
499 self.verifyDatabaseMocks()
502 var bottledID: String = ""
504 let fetchEscrowContentsExpectation = self.expectation(description: "fetchEscrowContentsExpectation returns")
505 clique.fetchEscrowContents { e, b, s, _ in
506 XCTAssertNotNil(e, "entropy should not be nil")
507 XCTAssertNotNil(b, "bottleID should not be nil")
508 XCTAssertNotNil(s, "signingPublicKey should not be nil")
511 fetchEscrowContentsExpectation.fulfill()
514 self.wait(for: [fetchEscrowContentsExpectation], timeout: 10)
516 let initiatorContext = self.manager.context(forContainerName: OTCKContainerName,
517 contextID: "restoreContext",
518 sosAdapter: OTSOSMissingAdapter(),
519 authKitAdapter: self.mockAuthKit2,
520 lockStateTracker: self.lockStateTracker,
521 accountStateTracker: self.accountStateTracker,
522 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
524 initiatorContext.startOctagonStateMachine()
525 let newOTCliqueContext = OTConfigurationContext()
526 newOTCliqueContext.context = "restoreContext"
527 newOTCliqueContext.dsid = self.otcliqueContext.dsid
528 newOTCliqueContext.altDSID = self.otcliqueContext.altDSID
529 newOTCliqueContext.otControl = self.otcliqueContext.otControl
530 newOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottledID, entropy: entropy)
532 let newClique: OTClique
534 newClique = try OTClique.performEscrowRecovery(withContextData: newOTCliqueContext, escrowArguments: [:])
535 XCTAssertNotNil(newClique, "newClique should not be nil")
537 XCTFail("Shouldn't have errored recovering: \(error)")
541 // We will upload a new TLK for the new peer
542 self.assertAllCKKSViewsUpload(tlkShares: 1)
543 self.sendContainerChangeWaitForFetch(context: self.cuttlefishContext)
544 self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
545 self.verifyDatabaseMocks()
547 self.sendContainerChangeWaitForFetch(context: initiatorContext)
549 bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
550 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
551 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 2, "preferredBottleIDs should have 2 bottle")
552 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
554 let dumpExpectation = self.expectation(description: "dump callback occurs")
555 self.tphClient.dump(withContainer: self.cuttlefishContext.containerName, context: self.cuttlefishContext.contextID) { dump, error in
556 XCTAssertNil(error, "Should be no error dumping data")
557 XCTAssertNotNil(dump, "dump should not be nil")
558 let egoSelf = dump!["self"] as? [String: AnyObject]
559 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
560 let peerID = egoSelf!["peerID"] as? String
561 XCTAssertNotNil(peerID, "peerID should not be nil")
563 dumpExpectation.fulfill()
565 self.wait(for: [dumpExpectation], timeout: 10)
567 self.otControlCLI.status(OTCKContainerName,
568 context: newOTCliqueContext.context,
572 func testFetchEmptyOptimalBottleList () throws {
573 self.startCKAccountStatusMock()
575 let bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
576 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 0, "should be 0 preferred bottle ids")
577 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "should be 0 partialRecoveryBottleIDs")
580 func testFetchOptimalBottlesAfterFailedRestore() throws {
581 self.startCKAccountStatusMock()
583 self.otcliqueContext.sbd = OTMockSecureBackup(bottleID: "bottle ID", entropy: Data(count: 72))
585 XCTAssertThrowsError(try OTClique.performEscrowRecovery(withContextData: self.otcliqueContext, escrowArguments: [:]))
587 let bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
588 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
589 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 0, "preferredBottleIDs should have 0 bottle")
590 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
593 func testMakeNewFriendsAndFetchEscrowContents () throws {
594 self.startCKAccountStatusMock()
598 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
599 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
600 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
602 let fetchEscrowContentsException = self.expectation(description: "update returns")
604 clique.fetchEscrowContents { e, b, s, _ in
605 XCTAssertNotNil(e, "entropy should not be nil")
606 XCTAssertNotNil(b, "bottleID should not be nil")
607 XCTAssertNotNil(s, "signingPublicKey should not be nil")
608 fetchEscrowContentsException.fulfill()
610 self.wait(for: [fetchEscrowContentsException], timeout: 10)
613 XCTFail("failed to reset clique: \(error)")
616 self.verifyDatabaseMocks()
619 func testFetchEscrowContentsBeforeIdentityExists() {
620 self.startCKAccountStatusMock()
622 let initiatorContext = self.manager.context(forContainerName: OTCKContainerName, contextID: "initiator")
623 let fetchEscrowContentsException = self.expectation(description: "update returns")
625 initiatorContext.fetchEscrowContents { entropy, bottleID, signingPubKey, error in
626 XCTAssertNil(entropy, "entropy should be nil")
627 XCTAssertNil(bottleID, "bottleID should be nil")
628 XCTAssertNil(signingPubKey, "signingPublicKey should be nil")
629 XCTAssertNotNil(error, "error should not be nil")
631 fetchEscrowContentsException.fulfill()
633 self.wait(for: [fetchEscrowContentsException], timeout: 10)
636 func testFetchEscrowContentsChecksEntitlement() throws {
637 self.startCKAccountStatusMock()
639 let contextName = OTDefaultContext
640 let containerName = OTCKContainerName
642 // First, fail due to not having any data
643 let fetchEscrowContentsExpectation = self.expectation(description: "fetchEscrowContentsExpectation returns")
644 self.otControl.fetchEscrowContents(containerName, contextID: contextName) { entropy, bottle, signingPublicKey, error in
645 XCTAssertNotNil(error, "error should not be nil")
646 //XCTAssertNotEqual(error.code, errSecMissingEntitlement, "Error should not be 'missing entitlement'")
647 XCTAssertNil(entropy, "entropy should be nil")
648 XCTAssertNil(bottle, "bottleID should be nil")
649 XCTAssertNil(signingPublicKey, "signingPublicKey should be nil")
650 fetchEscrowContentsExpectation.fulfill()
652 self.wait(for: [fetchEscrowContentsExpectation], timeout: 10)
654 // Now, fail due to the client not having an entitlement
655 self.otControlEntitlementBearer.entitlements.removeAll()
656 let failFetchEscrowContentsExpectation = self.expectation(description: "fetchEscrowContentsExpectation returns")
657 self.otControl.fetchEscrowContents(containerName, contextID: contextName) { entropy, bottle, signingPublicKey, error in
658 XCTAssertNotNil(error, "error should not be nil")
660 case .some(let error as NSError):
661 XCTAssertEqual(error.domain, NSOSStatusErrorDomain, "Error should be an OS status")
662 XCTAssertEqual(error.code, Int(errSecMissingEntitlement), "Error should be 'missing entitlement'")
664 XCTFail("Unable to turn error into NSError: \(String(describing: error))")
667 XCTAssertNil(entropy, "entropy should not be nil")
668 XCTAssertNil(bottle, "bottleID should not be nil")
669 XCTAssertNil(signingPublicKey, "signingPublicKey should not be nil")
670 failFetchEscrowContentsExpectation.fulfill()
672 self.wait(for: [failFetchEscrowContentsExpectation], timeout: 10)
675 func testJoinWithBottleFailCaseBottleDoesntExist() throws {
676 self.startCKAccountStatusMock()
678 let initiatorContextID = "initiator-context-id"
679 self.cuttlefishContext.startOctagonStateMachine()
680 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
681 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
685 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
686 XCTAssertNotNil(clique, "Clique should not be nil")
688 XCTFail("Shouldn't have errored making new friends: \(error)")
692 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
693 XCTAssertNotNil(entropy, "entropy should not be nil")
695 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
696 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
697 self.verifyDatabaseMocks()
699 let bottle = self.fakeCuttlefishServer.state.bottles[0]
700 self.fakeCuttlefishServer.state.bottles.removeAll()
702 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
704 initiatorContext.startOctagonStateMachine()
706 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
707 initiatorContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID!) { error in
708 XCTAssertNotNil(error, "error should not be nil")
709 joinWithBottleExpectation.fulfill()
711 self.wait(for: [joinWithBottleExpectation], timeout: 10)
712 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
715 func testJoinWithBottleFailCaseBadEscrowRecord() throws {
716 self.startCKAccountStatusMock()
718 let initiatorContextID = "initiator-context-id"
720 self.cuttlefishContext.startOctagonStateMachine()
721 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
722 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
726 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
727 XCTAssertNotNil(clique, "Clique should not be nil")
729 XCTFail("Shouldn't have errored making new friends: \(error)")
733 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
734 XCTAssertNotNil(entropy, "entropy should not be nil")
736 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
737 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
738 self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
739 self.verifyDatabaseMocks()
741 _ = self.fakeCuttlefishServer.state.bottles[0]
743 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
745 initiatorContext.startOctagonStateMachine()
747 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
748 initiatorContext.join(withBottle: "sos peer id", entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID!) { error in
749 XCTAssertNotNil(error, "error should be nil")
750 joinWithBottleExpectation.fulfill()
752 self.wait(for: [joinWithBottleExpectation], timeout: 10)
753 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
756 func testJoinWithBottleFailCaseBadEntropy() throws {
757 self.startCKAccountStatusMock()
759 let initiatorContextID = "initiator-context-id"
760 self.cuttlefishContext.startOctagonStateMachine()
761 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
762 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
766 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
767 XCTAssertNotNil(clique, "Clique should not be nil")
769 XCTFail("Shouldn't have errored making new friends: \(error)")
773 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
774 XCTAssertNotNil(entropy, "entropy should not be nil")
776 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
777 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
778 self.verifyDatabaseMocks()
780 let bottle = self.fakeCuttlefishServer.state.bottles[0]
782 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
784 initiatorContext.startOctagonStateMachine()
786 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
788 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
789 initiatorContext.join(withBottle: bottle.bottleID, entropy: Data(count: 72), bottleSalt: self.otcliqueContext.altDSID!) { error in
790 XCTAssertNotNil(error, "error should not be nil, when entropy is missing")
791 joinWithBottleExpectation.fulfill()
793 self.wait(for: [joinWithBottleExpectation], timeout: 10)
794 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
797 func testJoinWithBottleFailCaseBadBottleSalt() throws {
798 self.startCKAccountStatusMock()
800 let initiatorContextID = "initiator-context-id"
801 self.cuttlefishContext.startOctagonStateMachine()
802 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
803 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
807 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
808 XCTAssertNotNil(clique, "Clique should not be nil")
810 XCTFail("Shouldn't have errored making new friends: \(error)")
814 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
815 XCTAssertNotNil(entropy, "entropy should not be nil")
817 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
818 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
819 self.verifyDatabaseMocks()
821 let bottle = self.fakeCuttlefishServer.state.bottles[0]
823 let initiatorContext = self.makeInitiatorContext(contextID: initiatorContextID)
825 initiatorContext.startOctagonStateMachine()
827 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
829 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
830 initiatorContext.join(withBottle: bottle.bottleID, entropy: Data(count: 72), bottleSalt: "123456789") { error in
831 XCTAssertNotNil(error, "error should not be nil with bad entropy and salt")
832 joinWithBottleExpectation.fulfill()
834 self.wait(for: [joinWithBottleExpectation], timeout: 10)
835 self.assertEnters(context: initiatorContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
838 func testRecoverFromDeviceNotOnMachineIDList() throws {
839 self.startCKAccountStatusMock()
843 clique = try OTClique.newFriends(withContextData: self.otcliqueContext, resetReason: .testGenerated)
844 XCTAssertNotNil(clique, "Clique should not be nil")
846 XCTFail("Shouldn't have errored making new friends: \(error)")
850 let firstPeerID = clique.cliqueMemberIdentifier
851 XCTAssertNotNil(firstPeerID, "Clique should have a member identifier")
852 let entropy = try self.loadSecret(label: firstPeerID!)
853 XCTAssertNotNil(entropy, "entropy should not be nil")
855 let bottleIDs = try OTClique.findOptimalBottleIDs(withContextData: self.otcliqueContext)
856 XCTAssertNotNil(bottleIDs.preferredBottleIDs, "preferredBottleIDs should not be nil")
857 XCTAssertEqual(bottleIDs.preferredBottleIDs.count, 1, "preferredBottleIDs should have 1 bottle")
858 XCTAssertEqual(bottleIDs.partialRecoveryBottleIDs.count, 0, "partialRecoveryBottleIDs should be empty")
860 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
861 self.verifyDatabaseMocks()
862 self.assertConsidersSelfTrusted(context: self.cuttlefishContext)
864 let bottle = self.fakeCuttlefishServer.state.bottles[0]
866 // To get into the state we need, we need to introduce peer B and C. C should then distrust A, whose bottle it used
867 // B shouldn't have an opinion of C.
869 let bNewOTCliqueContext = OTConfigurationContext()
870 bNewOTCliqueContext.context = "restoreB"
871 bNewOTCliqueContext.dsid = self.otcliqueContext.dsid
872 bNewOTCliqueContext.altDSID = self.otcliqueContext.altDSID
873 bNewOTCliqueContext.otControl = self.otcliqueContext.otControl
874 bNewOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottle.bottleID, entropy: entropy!)
876 let deviceBmockAuthKit = OTMockAuthKitAdapter(altDSID: self.otcliqueContext.altDSID!,
877 machineID: "b-machine-id",
878 otherDevices: [self.mockAuthKit.currentMachineID])
880 let bRestoreContext = self.manager.context(forContainerName: OTCKContainerName,
881 contextID: bNewOTCliqueContext.context,
882 sosAdapter: OTSOSMissingAdapter(),
883 authKitAdapter: deviceBmockAuthKit,
884 lockStateTracker: self.lockStateTracker,
885 accountStateTracker: self.accountStateTracker,
886 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
887 bRestoreContext.startOctagonStateMachine()
888 let bNewClique: OTClique
890 bNewClique = try OTClique.performEscrowRecovery(withContextData: bNewOTCliqueContext, escrowArguments: [:])
891 XCTAssertNotNil(bNewClique, "bNewClique should not be nil")
893 XCTFail("Shouldn't have errored recovering: \(error)")
896 self.assertEnters(context: bRestoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
898 // And introduce C, which will kick out A
899 // During the next sign in, the machine ID list has changed to just the new one
900 let restoremockAuthKit = OTMockAuthKitAdapter(altDSID: self.otcliqueContext.altDSID!,
901 machineID: "c-machine-id",
902 otherDevices: [self.mockAuthKit.currentMachineID, deviceBmockAuthKit.currentMachineID])
903 let restoreContext = self.manager.context(forContainerName: OTCKContainerName,
904 contextID: "restoreContext",
905 sosAdapter: OTSOSMissingAdapter(),
906 authKitAdapter: restoremockAuthKit,
907 lockStateTracker: self.lockStateTracker,
908 accountStateTracker: self.accountStateTracker,
909 deviceInformationAdapter: self.makeInitiatorDeviceInfoAdapter())
911 restoreContext.startOctagonStateMachine()
912 let newOTCliqueContext = OTConfigurationContext()
913 newOTCliqueContext.context = "restoreContext"
914 newOTCliqueContext.dsid = self.otcliqueContext.dsid
915 newOTCliqueContext.altDSID = self.otcliqueContext.altDSID
916 newOTCliqueContext.otControl = self.otcliqueContext.otControl
917 newOTCliqueContext.sbd = OTMockSecureBackup(bottleID: bottle.bottleID, entropy: entropy!)
919 let newClique: OTClique
921 newClique = try OTClique.performEscrowRecovery(withContextData: newOTCliqueContext, escrowArguments: [:])
922 XCTAssertNotNil(newClique, "newClique should not be nil")
924 XCTFail("Shouldn't have errored recovering: \(error)")
928 self.assertEnters(context: restoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
929 self.verifyDatabaseMocks()
930 self.assertConsidersSelfTrusted(context: restoreContext)
932 let restoreDumpCallback = self.expectation(description: "acceptorDumpCallback callback occurs")
933 self.tphClient.dump(withContainer: OTCKContainerName, context: newOTCliqueContext.context) { dump, _ in
934 XCTAssertNotNil(dump, "dump should not be nil")
935 let egoSelf = dump!["self"] as? [String: AnyObject]
936 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
937 let dynamicInfo = egoSelf!["dynamicInfo"] as? [String: AnyObject]
938 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
939 let included = dynamicInfo!["included"] as? [String]
940 XCTAssertNotNil(included, "included should not be nil")
941 XCTAssertEqual(included!.count, 3, "should be 3 peer id included")
943 restoreDumpCallback.fulfill()
945 self.wait(for: [restoreDumpCallback], timeout: 10)
947 // Now, exclude peer A's machine ID
948 restoremockAuthKit.otherDevices = [deviceBmockAuthKit.currentMachineID]
950 // Peer C should upload a new trust status
951 let updateTrustExpectation = self.expectation(description: "updateTrust")
952 self.fakeCuttlefishServer.updateListener = { request in
953 XCTAssertTrue(request.hasDynamicInfoAndSig, "updateTrust request should have a dynamic info")
954 let newDynamicInfo = TPPeerDynamicInfo(data: request.dynamicInfoAndSig.peerDynamicInfo,
955 sig: request.dynamicInfoAndSig.sig)
956 XCTAssertNotNil(newDynamicInfo, "should be able to make a dynamic info from protobuf")
958 XCTAssertEqual(newDynamicInfo!.excludedPeerIDs.count, 1, "Should have a single excluded peer")
959 updateTrustExpectation.fulfill()
963 restoreContext.incompleteNotificationOfMachineIDListChange()
964 self.wait(for: [updateTrustExpectation], timeout: 10)
966 self.assertEnters(context: restoreContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
967 self.verifyDatabaseMocks()
968 self.assertConsidersSelfTrusted(context: restoreContext)
971 func testCachedBottleFetch() throws {
972 let initiatorContextID = "initiator-context-id"
973 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
975 bottlerContext.startOctagonStateMachine()
976 let ckacctinfo = CKAccountInfo()
977 ckacctinfo.accountStatus = .available
978 ckacctinfo.hasValidCredentials = true
979 ckacctinfo.accountPartition = .production
981 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
982 XCTAssertNoThrow(try bottlerContext.setCDPEnabled())
983 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
986 let bottlerotcliqueContext = OTConfigurationContext()
987 bottlerotcliqueContext.context = initiatorContextID
988 bottlerotcliqueContext.dsid = "1234"
989 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
990 bottlerotcliqueContext.otControl = self.otControl
992 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
993 XCTAssertNotNil(clique, "Clique should not be nil")
994 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
996 XCTFail("Shouldn't have errored making new friends: \(error)")
1000 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1001 self.assertConsidersSelfTrusted(context: bottlerContext)
1003 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
1004 XCTAssertNotNil(entropy, "entropy should not be nil")
1006 // Fake that this peer also created some TLKShares for itself
1007 self.putFakeKeyHierarchiesInCloudKit()
1008 try self.putSelfTLKSharesInCloudKit(context: bottlerContext)
1010 let bottle = self.fakeCuttlefishServer.state.bottles[0]
1012 self.cuttlefishContext.startOctagonStateMachine()
1013 self.startCKAccountStatusMock()
1014 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1016 // Try to enforce that that CKKS doesn't know about the key hierarchy until Octagon asks it
1017 self.holdCloudKitFetches()
1019 // Note: CKKS will want to upload a TLKShare for its self
1020 self.assertAllCKKSViewsUpload(tlkShares: 1)
1022 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
1023 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID!) { error in
1024 XCTAssertNil(error, "error should be nil")
1025 joinWithBottleExpectation.fulfill()
1029 self.releaseCloudKitFetchHold()
1031 self.wait(for: [joinWithBottleExpectation], timeout: 100)
1033 let dumpCallback = self.expectation(description: "dumpCallback callback occurs")
1034 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) { dump, _ in
1035 XCTAssertNotNil(dump, "dump should not be nil")
1036 let egoSelf = dump!["self"] as? [String: AnyObject]
1037 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
1038 let dynamicInfo = egoSelf!["dynamicInfo"] as? [String: AnyObject]
1039 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
1040 let included = dynamicInfo!["included"] as? [String]
1041 XCTAssertNotNil(included, "included should not be nil")
1042 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
1043 dumpCallback.fulfill()
1045 self.wait(for: [dumpCallback], timeout: 10)
1047 self.verifyDatabaseMocks()
1048 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1049 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
1051 //now call fetchviablebottles, we should get the uncached version
1052 let fetchUnCachedViableBottlesExpectation = self.expectation(description: "fetch UnCached ViableBottles")
1054 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1055 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1056 fetchUnCachedViableBottlesExpectation.fulfill()
1059 let FetchAllViableBottles = self.expectation(description: "FetchAllViableBottles callback occurs")
1061 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1062 XCTAssertNil(error, "should be no error fetching viable bottles")
1063 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1064 FetchAllViableBottles.fulfill()
1066 self.wait(for: [FetchAllViableBottles], timeout: 10)
1067 self.wait(for: [fetchUnCachedViableBottlesExpectation], timeout: 10)
1069 let fetchViableExpectation = self.expectation(description: "fetchViableBottles callback occurs")
1070 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1071 XCTAssertNil(error, "should be no error fetching viable bottles")
1072 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1073 fetchViableExpectation.fulfill()
1075 self.wait(for: [fetchViableExpectation], timeout: 10)
1077 //now call fetchviablebottles, we should get the cached version
1078 let fetchViableBottlesExpectation = self.expectation(description: "fetch Cached ViableBottles")
1079 fetchViableBottlesExpectation.isInverted = true
1081 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1082 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1083 fetchViableBottlesExpectation.fulfill()
1086 let fetchExpectation = self.expectation(description: "fetchExpectation callback occurs")
1088 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1089 XCTAssertNil(error, "should be no error fetching viable bottles")
1090 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1091 fetchExpectation.fulfill()
1093 self.wait(for: [fetchExpectation], timeout: 10)
1094 self.wait(for: [fetchViableBottlesExpectation], timeout: 10)
1097 func testViableBottleCachingAfterJoin() throws {
1098 let initiatorContextID = "initiator-context-id"
1099 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
1101 bottlerContext.startOctagonStateMachine()
1102 let ckacctinfo = CKAccountInfo()
1103 ckacctinfo.accountStatus = .available
1104 ckacctinfo.hasValidCredentials = true
1105 ckacctinfo.accountPartition = .production
1107 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
1108 XCTAssertNoThrow(try bottlerContext.setCDPEnabled())
1109 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1111 let clique: OTClique
1112 let bottlerotcliqueContext = OTConfigurationContext()
1113 bottlerotcliqueContext.context = initiatorContextID
1114 bottlerotcliqueContext.dsid = "1234"
1115 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
1116 bottlerotcliqueContext.otControl = self.otControl
1118 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
1119 XCTAssertNotNil(clique, "Clique should not be nil")
1120 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
1122 XCTFail("Shouldn't have errored making new friends: \(error)")
1126 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1127 self.assertConsidersSelfTrusted(context: bottlerContext)
1129 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
1130 XCTAssertNotNil(entropy, "entropy should not be nil")
1132 // Fake that this peer also created some TLKShares for itself
1133 self.putFakeKeyHierarchiesInCloudKit()
1134 try self.putSelfTLKSharesInCloudKit(context: bottlerContext)
1136 let bottle = self.fakeCuttlefishServer.state.bottles[0]
1138 self.cuttlefishContext.startOctagonStateMachine()
1139 self.startCKAccountStatusMock()
1140 XCTAssertNoThrow(try self.cuttlefishContext.setCDPEnabled())
1141 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1143 // Try to enforce that that CKKS doesn't know about the key hierarchy until Octagon asks it
1144 self.holdCloudKitFetches()
1146 // Note: CKKS will want to upload a TLKShare for its self
1147 self.assertAllCKKSViewsUpload(tlkShares: 1)
1149 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
1150 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID!) { error in
1151 XCTAssertNil(error, "error should be nil")
1152 joinWithBottleExpectation.fulfill()
1156 self.releaseCloudKitFetchHold()
1158 self.wait(for: [joinWithBottleExpectation], timeout: 100)
1160 let dumpCallback = self.expectation(description: "dumpCallback callback occurs")
1161 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) { dump, _ in
1162 XCTAssertNotNil(dump, "dump should not be nil")
1163 let egoSelf = dump!["self"] as? [String: AnyObject]
1164 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
1165 let dynamicInfo = egoSelf!["dynamicInfo"] as? [String: AnyObject]
1166 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
1167 let included = dynamicInfo!["included"] as? [String]
1168 XCTAssertNotNil(included, "included should not be nil")
1169 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
1170 dumpCallback.fulfill()
1172 self.wait(for: [dumpCallback], timeout: 10)
1174 self.verifyDatabaseMocks()
1175 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1176 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
1178 //now call fetchviablebottles, we should get the uncached version
1179 let fetchUnCachedViableBottlesExpectation = self.expectation(description: "fetch UnCached ViableBottles")
1181 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1182 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1183 fetchUnCachedViableBottlesExpectation.fulfill()
1186 let FetchAllViableBottles = self.expectation(description: "FetchAllViableBottles callback occurs")
1188 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1189 XCTAssertNil(error, "should be no error fetching viable bottles")
1190 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1191 FetchAllViableBottles.fulfill()
1193 self.wait(for: [FetchAllViableBottles], timeout: 10)
1194 self.wait(for: [fetchUnCachedViableBottlesExpectation], timeout: 10)
1196 let fetchViableExpectation = self.expectation(description: "fetchViableBottles callback occurs")
1197 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1198 XCTAssertNil(error, "should be no error fetching viable bottles")
1199 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1200 fetchViableExpectation.fulfill()
1202 self.wait(for: [fetchViableExpectation], timeout: 10)
1204 //now call fetchviablebottles, we should get the cached version
1205 let fetchViableBottlesExpectation = self.expectation(description: "fetch Cached ViableBottles")
1206 fetchViableBottlesExpectation.isInverted = true
1208 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1209 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1210 fetchViableBottlesExpectation.fulfill()
1213 let fetchExpectation = self.expectation(description: "fetchExpectation callback occurs")
1215 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1216 XCTAssertNil(error, "should be no error fetching viable bottles")
1217 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1218 fetchExpectation.fulfill()
1220 self.wait(for: [fetchExpectation], timeout: 10)
1221 self.wait(for: [fetchViableBottlesExpectation], timeout: 10)
1224 func testViableBottleReturns1Bottle() throws {
1225 let initiatorContextID = "initiator-context-id"
1226 let bottlerContext = self.makeInitiatorContext(contextID: initiatorContextID)
1228 bottlerContext.startOctagonStateMachine()
1229 let ckacctinfo = CKAccountInfo()
1230 ckacctinfo.accountStatus = .available
1231 ckacctinfo.hasValidCredentials = true
1232 ckacctinfo.accountPartition = .production
1234 bottlerContext.cloudkitAccountStateChange(nil, to: ckacctinfo)
1235 XCTAssertNoThrow(try bottlerContext.setCDPEnabled())
1236 self.assertEnters(context: bottlerContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1238 let clique: OTClique
1239 let bottlerotcliqueContext = OTConfigurationContext()
1240 bottlerotcliqueContext.context = initiatorContextID
1241 bottlerotcliqueContext.dsid = "1234"
1242 bottlerotcliqueContext.altDSID = self.mockAuthKit.altDSID!
1243 bottlerotcliqueContext.otControl = self.otControl
1245 clique = try OTClique.newFriends(withContextData: bottlerotcliqueContext, resetReason: .testGenerated)
1246 XCTAssertNotNil(clique, "Clique should not be nil")
1247 XCTAssertNotNil(clique.cliqueMemberIdentifier, "Should have a member identifier after a clique newFriends call")
1249 XCTFail("Shouldn't have errored making new friends: \(error)")
1253 self.assertEnters(context: bottlerContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1254 self.assertConsidersSelfTrusted(context: bottlerContext)
1256 let entropy = try self.loadSecret(label: clique.cliqueMemberIdentifier!)
1257 XCTAssertNotNil(entropy, "entropy should not be nil")
1259 // Fake that this peer also created some TLKShares for itself
1260 self.putFakeKeyHierarchiesInCloudKit()
1261 try self.putSelfTLKSharesInCloudKit(context: bottlerContext)
1263 let bottle = self.fakeCuttlefishServer.state.bottles[0]
1265 self.cuttlefishContext.startOctagonStateMachine()
1266 self.startCKAccountStatusMock()
1267 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateUntrusted, within: 10 * NSEC_PER_SEC)
1269 // Try to enforce that that CKKS doesn't know about the key hierarchy until Octagon asks it
1270 self.holdCloudKitFetches()
1272 // Note: CKKS will want to upload a TLKShare for its self
1273 self.assertAllCKKSViewsUpload(tlkShares: 1)
1275 let joinWithBottleExpectation = self.expectation(description: "joinWithBottle callback occurs")
1276 self.cuttlefishContext.join(withBottle: bottle.bottleID, entropy: entropy!, bottleSalt: self.otcliqueContext.altDSID!) { error in
1277 XCTAssertNil(error, "error should be nil")
1278 joinWithBottleExpectation.fulfill()
1282 self.releaseCloudKitFetchHold()
1284 self.wait(for: [joinWithBottleExpectation], timeout: 100)
1286 var egoPeerID: String?
1288 let dumpCallback = self.expectation(description: "dumpCallback callback occurs")
1289 self.tphClient.dump(withContainer: OTCKContainerName, context: OTDefaultContext) { dump, _ in
1290 XCTAssertNotNil(dump, "dump should not be nil")
1291 let egoSelf = dump!["self"] as? [String: AnyObject]
1292 XCTAssertNotNil(egoSelf, "egoSelf should not be nil")
1293 egoPeerID = egoSelf!["peerID"] as? String
1294 let dynamicInfo = egoSelf!["dynamicInfo"] as? [String: AnyObject]
1295 XCTAssertNotNil(dynamicInfo, "dynamicInfo should not be nil")
1296 let included = dynamicInfo!["included"] as? [String]
1297 XCTAssertNotNil(included, "included should not be nil")
1298 XCTAssertEqual(included!.count, 2, "should be 2 peer ids")
1299 dumpCallback.fulfill()
1301 self.wait(for: [dumpCallback], timeout: 10)
1303 self.verifyDatabaseMocks()
1304 self.assertEnters(context: self.cuttlefishContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1305 assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
1307 let bottles: [Bottle] = self.fakeCuttlefishServer.state.bottles
1308 var bottleToExclude: String?
1309 bottles.forEach { bottle in
1310 if bottle.peerID == egoPeerID {
1311 bottleToExclude = bottle.bottleID
1315 XCTAssertNotNil(bottleToExclude, "bottleToExclude should not be nil")
1317 //now call fetchviablebottles, we should get the uncached version
1318 var fetchUnCachedViableBottlesExpectation = self.expectation(description: "fetch UnCached ViableBottles")
1320 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1321 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1322 fetchUnCachedViableBottlesExpectation.fulfill()
1325 self.fakeCuttlefishServer.fetchViableBottlesDontReturnBottleWithID = bottleToExclude
1326 var FetchAllViableBottles = self.expectation(description: "FetchAllViableBottles callback occurs")
1328 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1329 XCTAssertNil(error, "should be no error fetching viable bottles")
1330 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1331 FetchAllViableBottles.fulfill()
1333 self.wait(for: [FetchAllViableBottles], timeout: 10)
1334 self.wait(for: [fetchUnCachedViableBottlesExpectation], timeout: 10)
1336 //now call fetchviablebottles, we should get the uncached version
1337 fetchUnCachedViableBottlesExpectation = self.expectation(description: "fetch UnCached ViableBottles")
1339 self.fakeCuttlefishServer.fetchViableBottlesListener = { request in
1340 self.fakeCuttlefishServer.fetchViableBottlesListener = nil
1341 fetchUnCachedViableBottlesExpectation.fulfill()
1345 self.fakeCuttlefishServer.fetchViableBottlesDontReturnBottleWithID = bottleToExclude
1347 FetchAllViableBottles = self.expectation(description: "FetchAllViableBottles callback occurs")
1349 self.cuttlefishContext.rpcFetchAllViableBottles { viable, _, error in
1350 XCTAssertNil(error, "should be no error fetching viable bottles")
1351 XCTAssert(viable?.contains(bottle.bottleID) ?? false, "The bottle we're about to restore should be viable")
1352 FetchAllViableBottles.fulfill()
1354 self.wait(for: [FetchAllViableBottles], timeout: 10)
1355 self.wait(for: [fetchUnCachedViableBottlesExpectation], timeout: 10)
1358 func testRecoverTLKSharesSendByPeers() throws {
1359 // First, set up the world: two peers, one of which has sent TLKs tto itself and the other
1360 let noSelfSharesContext = self.makeInitiatorContext(contextID: "noShares", authKitAdapter: self.mockAuthKit2)
1361 let allSharesContext = self.makeInitiatorContext(contextID: "allShares", authKitAdapter: self.mockAuthKit3)
1363 self.startCKAccountStatusMock()
1364 let noSelfSharesPeerID = self.assertResetAndBecomeTrusted(context: noSelfSharesContext)
1365 let allSharesPeerID = self.assertJoinViaEscrowRecovery(joiningContext: allSharesContext, sponsor: noSelfSharesContext)
1367 self.sendContainerChangeWaitForFetch(context: noSelfSharesContext)
1368 self.assertEnters(context: noSelfSharesContext, state: OctagonStateReady, within: 10 * NSEC_PER_SEC)
1370 XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: noSelfSharesPeerID, opinion: .trusts, target: allSharesPeerID)),
1371 "noShares should trust allShares")
1372 XCTAssertTrue(self.fakeCuttlefishServer.assertCuttlefishState(FakeCuttlefishAssertion(peer: noSelfSharesPeerID, opinion: .trusts, target: noSelfSharesPeerID)),
1373 "No shares should trust itself")
1375 self.putFakeKeyHierarchiesInCloudKit()
1376 try self.putSelfTLKSharesInCloudKit(context: allSharesContext)
1377 try self.putAllTLKSharesInCloudKit(to: noSelfSharesContext, from: allSharesContext)
1379 try self.ckksZones.forEach { zone in
1380 XCTAssertFalse(try self.tlkShareInCloudKit(receiverPeerID: noSelfSharesPeerID, senderPeerID: noSelfSharesPeerID, zoneID: zone as! CKRecordZone.ID), "Should not have self shares for noSelfShares")
1381 XCTAssertTrue(try self.tlkShareInCloudKit(receiverPeerID: noSelfSharesPeerID, senderPeerID: allSharesPeerID, zoneID: zone as! CKRecordZone.ID), "Should have a share for noSelfShares from allShares")
1384 // Now, recover from noSelfShares
1385 self.assertAllCKKSViewsUpload(tlkShares: 1)
1386 self.assertJoinViaEscrowRecovery(joiningContext: self.cuttlefishContext, sponsor: noSelfSharesContext)
1387 // And CKKS should enter ready!
1388 self.assertAllCKKSViews(enter: SecCKKSZoneKeyStateReady, within: 10 * NSEC_PER_SEC)
1389 self.verifyDatabaseMocks()