// // TrustedPeersHelperUnitTests.swift // TrustedPeersHelperUnitTests // // Created by Ben Williamson on 5/1/18. // import CoreData import XCTest let testDSID = "123456789" let signingKey_384 = Data(base64Encoded: "BOQbPoiBnzuA0Cgc2QegjKGJqDtpkRenHwAxkYKJH1xELdaoIh8ifSch8sl18tpBYVUpEfdxz2ZSKif+dx7UPfu8WeTtpHkqm3M+9PTjr/KNNJCSR1PQNB5Jh+sRiQ+cpJnoTzm+IZSIukylamAcL3eA0nMUM0Zc2u4TijrbTgVND22WzSirUkwSK3mA/prk9A==") let encryptionKey_384 = Data(base64Encoded: "BE1RuazBWmSEx0XVGhobbrdSE6fRQOrUrYEQnBkGl4zJq9GCeRoYvbuWNYFcOH0ijCRz9pYILsTn3ajT1OknlvcKmuQ7SeoGWzk9cBZzT5bBEwozn2gZxn80DQoDkmejywlH3D0/cuV6Bxexu5KMAFGqg6eN6th4sQABL5EuI9zKPuxHStM/b9B1LyqcnRKQHA==") let symmetricKey_384 = Data(base64Encoded: "MfHje3Y/mWV0q+grjwZ4VxuqB7OreYHLxYkeeCiNjjY=") class TrustedPeersHelperUnitTests: XCTestCase { var tmpPath: String! var tmpURL: URL! var cuttlefish: FakeCuttlefishServer! var manateeKeySet: CKKSKeychainBackedKeySet! override static func setUp() { super.setUp() SecTapToRadar.disableTTRsEntirely() // Turn on NO_SERVER stuff securityd_init_local_spi() SecCKKSDisable() } override func setUp() { super.setUp() let testName = self.name.components(separatedBy: CharacterSet(charactersIn: " ]"))[1] cuttlefish = FakeCuttlefishServer(nil, ckZones: [:], ckksZoneKeys: [:]) // Make a new fake keychain tmpPath = String(format: "/tmp/%@-%X", testName, arc4random()) tmpURL = URL(fileURLWithPath: tmpPath, isDirectory: true) do { try FileManager.default.createDirectory(atPath: String(format: "%@/Library/Keychains", tmpPath), withIntermediateDirectories: true, attributes: nil) SetCustomHomeURLString(tmpPath as CFString) SecKeychainDbReset(nil) } catch { XCTFail("setUp failed: \(error)") } // Actually load the database. kc_with_dbt(true, nil) { _ in false } // Now that the keychain is alive, perform test setup do { self.manateeKeySet = try self.makeFakeKeyHierarchy(zoneID: CKRecordZone.ID(zoneName: "Manatee")) } catch { XCTFail("Creation of fake key hierarchies failed: \(error)") } } override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. cuttlefish = nil super.tearDown() } func makeFakeKeyHierarchy(zoneID: CKRecordZone.ID) throws -> CKKSKeychainBackedKeySet { // Remember, these keys come into TPH having round-tripped through an NSEncoding let tlk = try CKKSKeychainBackedKey.randomKeyWrapped(bySelf: zoneID) let classA = try CKKSKeychainBackedKey.randomKeyWrapped(byParent: tlk, keyclass: SecCKKSKeyClassA) let classC = try CKKSKeychainBackedKey.randomKeyWrapped(byParent: tlk, keyclass: SecCKKSKeyClassC) XCTAssertNoThrow(try tlk.saveMaterialToKeychain(), "Should be able to save TLK to keychain") XCTAssertNoThrow(try classA.saveMaterialToKeychain(), "Should be able to save classA key to keychain") XCTAssertNoThrow(try classC.saveMaterialToKeychain(), "Should be able to save classC key to keychain") let tlkData = try NSKeyedArchiver.archivedData(withRootObject: tlk, requiringSecureCoding: true) let classAData = try NSKeyedArchiver.archivedData(withRootObject: classA, requiringSecureCoding: true) let classCData = try NSKeyedArchiver.archivedData(withRootObject: classC, requiringSecureCoding: true) let decodedTLK = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [CKKSKeychainBackedKey.classForKeyedUnarchiver()], from: tlkData) as! CKKSKeychainBackedKey let decodedClassA = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [CKKSKeychainBackedKey.classForKeyedUnarchiver()], from: classAData) as! CKKSKeychainBackedKey let decodedClassC = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [CKKSKeychainBackedKey.classForKeyedUnarchiver()], from: classCData) as! CKKSKeychainBackedKey return CKKSKeychainBackedKeySet(tlk: decodedTLK, classA: decodedClassA, classC: decodedClassC, newUpload: false) } func assertTLKShareFor(peerID: String, keyUUID: String, zoneID: CKRecordZone.ID) { let matches = self.cuttlefish.state.tlkShares[zoneID]?.filter { tlkShare in tlkShare.receiver == peerID && tlkShare.keyUuid == keyUUID } XCTAssertEqual(matches?.count ?? 0, 1, "Should have one tlk share matching \(peerID) and \(keyUUID)") } func assertNoTLKShareFor(peerID: String, keyUUID: String, zoneID: CKRecordZone.ID) { let matches = self.cuttlefish.state.tlkShares[zoneID]?.filter { tlkShare in tlkShare.receiver == peerID && tlkShare.keyUuid == keyUUID } XCTAssertEqual(matches?.count ?? 0, 0, "Should have no tlk share matching \(peerID) and \(keyUUID)") } func assertTrusts(context: Container, peerIDs: [String]) { let state = context.getStateSync(test: self) guard let egoPeerID = state.egoPeerID else { XCTFail("context should have an ego peer ID") return } guard let dynamicInfo = state.peers[egoPeerID]?.dynamicInfo else { XCTFail("No dynamicInfo for ego peer") return } _ = peerIDs.map { XCTAssertTrue(dynamicInfo.includedPeerIDs.contains($0), "Peer should trust \($0)") XCTAssertFalse(dynamicInfo.excludedPeerIDs.contains($0), "Peer should not distrust \($0)") } } func assertDistrusts(context: Container, peerIDs: [String]) { let state = context.getStateSync(test: self) guard let egoPeerID = state.egoPeerID else { XCTFail("context should have an ego peer ID") return } guard let dynamicInfo = state.peers[egoPeerID]?.dynamicInfo else { XCTFail("No dynamicInfo for ego peer") return } _ = peerIDs.map { XCTAssertFalse(dynamicInfo.includedPeerIDs.contains($0), "Peer should not trust \($0)") XCTAssertTrue(dynamicInfo.excludedPeerIDs.contains($0), "Peer should distrust \($0)") } } func tmpStoreDescription(name: String) -> NSPersistentStoreDescription { let tmpStoreURL = URL(fileURLWithPath: name, relativeTo: tmpURL) return NSPersistentStoreDescription(url: tmpStoreURL) } func establish(reload: Bool, store: NSPersistentStoreDescription) throws -> (Container, String) { return try self.establish(reload: reload, contextID: OTDefaultContext, store: store) } func establish(reload: Bool, contextID: String, store: NSPersistentStoreDescription) throws -> (Container, String) { var container = try Container(name: ContainerName(container: "test", context: contextID), persistentStoreDescription: store, cuttlefish: cuttlefish) XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb", "ccc"]), "should be able to set allowed machine IDs") let (peerID, permanentInfo, permanentInfoSig, _, _, error) = container.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = container.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == peerID } .isEmpty, "should have a bottle for peer") let secret = container.loadSecretSync(test: self, label: peerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNotNil(peerID) XCTAssertNotNil(permanentInfo) XCTAssertNotNil(permanentInfoSig) XCTAssertNil(error) _ = container.dumpSync(test: self) if (reload) { do { container = try Container(name: ContainerName(container: "test", context: contextID), persistentStoreDescription: store, cuttlefish: cuttlefish) } catch { XCTFail() } } let (peerID2, _, error2) = container.establishSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error2) XCTAssertNotNil(peerID2) _ = container.dumpSync(test: self) return (container, peerID!) } func testEstablishWithReload() throws { let description = tmpStoreDescription(name: "container.db") let (_, peerID) = try establish(reload: true, store: description) assertTLKShareFor(peerID: peerID, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) } func testEstablishNoReload() throws { let description = tmpStoreDescription(name: "container.db") _ = try establish(reload: false, store: description) } func testEstablishNotOnAllowListErrors() throws { let description = tmpStoreDescription(name: "container.db") let container = try Container(name: ContainerName(container: "test", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let (peerID, permanentInfo, permanentInfoSig, _, _, error) = container.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = container.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == peerID } .isEmpty, "should have a bottle for peer") let secret = container.loadSecretSync(test: self, label: peerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNotNil(peerID) XCTAssertNotNil(permanentInfo) XCTAssertNotNil(permanentInfoSig) XCTAssertNil(error) // Note that an empty machine ID list means "all are allowed", so an establish now will succeed // Now set up a machine ID list that positively does not have our peer XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa"]), "should be able to set allowed machine IDs") let (peerID3, _, error3) = container.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: []) XCTAssertNotNil(peerID3, "Should get a peer when you establish a now allow-listed peer") XCTAssertNil(error3, "Should not get an error when you establish a now allow-listed peer") } func joinByVoucher(sponsor: Container, containerID: String, machineID: String, machineIDs: Set, store: NSPersistentStoreDescription) throws -> (Container, String) { let c = try Container(name: ContainerName(container: containerID, context: OTDefaultContext), persistentStoreDescription: store, cuttlefish: cuttlefish) XCTAssertNil(c.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs), "Should be able to set machine IDs") print("preparing \(containerID)") let (peerID, permanentInfo, permanentInfoSig, stableInfo, stableInfoSig, error) = c.prepareSync(test: self, epoch: 1, machineID: machineID, bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") XCTAssertNil(error) XCTAssertNotNil(peerID) XCTAssertNotNil(permanentInfo) XCTAssertNotNil(permanentInfoSig) XCTAssertNotNil(stableInfo) XCTAssertNotNil(stableInfoSig) do { assertNoTLKShareFor(peerID: peerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("\(sponsor) vouches for \(containerID)") let (voucherData, voucherSig, vouchError) = sponsor.vouchSync(test: self, peerID: peerID!, permanentInfo: permanentInfo!, permanentInfoSig: permanentInfoSig!, stableInfo: stableInfo!, stableInfoSig: stableInfoSig!, ckksKeys: [self.manateeKeySet]) XCTAssertNil(vouchError) XCTAssertNotNil(voucherData) XCTAssertNotNil(voucherSig) // As part of the join, the sponsor should have uploaded a tlk share assertTLKShareFor(peerID: peerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("\(containerID) joins") let (joinedPeerID, _, joinError) = c.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [], tlkShares: []) XCTAssertNil(joinError) XCTAssertEqual(joinedPeerID, peerID!) } return (c, peerID!) } func testJoin() throws { let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerC = try Container(name: ContainerName(container: "c", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb", "ccc"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerC.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) } print("preparing B") let (bPeerID, bPermanentInfo, bPermanentInfoSig, bStableInfo, bStableInfoSig, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) XCTAssertNotNil(bPeerID) XCTAssertNotNil(bPermanentInfo) XCTAssertNotNil(bPermanentInfoSig) do { assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("A vouches for B, but doesn't provide any TLKShares") let (_, _, errorVouchingWithoutTLKs) = containerA.vouchSync(test: self, peerID: bPeerID!, permanentInfo: bPermanentInfo!, permanentInfoSig: bPermanentInfoSig!, stableInfo: bStableInfo!, stableInfoSig: bStableInfoSig!, ckksKeys: []) XCTAssertNil(errorVouchingWithoutTLKs, "Should be no error vouching without uploading TLKShares") assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("A vouches for B, but doesn't only has provisional TLKs at the time") let provisionalManateeKeySet = try self.makeFakeKeyHierarchy(zoneID: CKRecordZone.ID(zoneName: "Manatee")) provisionalManateeKeySet.newUpload = true let (_, _, errorVouchingWithProvisionalTLKs) = containerA.vouchSync(test: self, peerID: bPeerID!, permanentInfo: bPermanentInfo!, permanentInfoSig: bPermanentInfoSig!, stableInfo: bStableInfo!, stableInfoSig: bStableInfoSig!, ckksKeys: [provisionalManateeKeySet]) XCTAssertNil(errorVouchingWithProvisionalTLKs, "Should be no error vouching without uploading TLKShares for a non-existent key") assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("A vouches for B") let (voucherData, voucherSig, error3) = containerA.vouchSync(test: self, peerID: bPeerID!, permanentInfo: bPermanentInfo!, permanentInfoSig: bPermanentInfoSig!, stableInfo: bStableInfo!, stableInfoSig: bStableInfoSig!, ckksKeys: [self.manateeKeySet]) XCTAssertNil(error3) XCTAssertNotNil(voucherData) XCTAssertNotNil(voucherSig) // As part of the vouch, A should have uploaded a tlkshare for B assertTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("B joins") let (peerID, _, error) = containerB.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [], tlkShares: []) XCTAssertNil(error) XCTAssertEqual(peerID, bPeerID!) } _ = containerA.dumpSync(test: self) _ = containerB.dumpSync(test: self) _ = containerC.dumpSync(test: self) print("preparing C") let (cPeerID, cPermanentInfo, cPermanentInfoSig, cStableInfo, cStableInfoSig, error4) = containerC.prepareSync(test: self, epoch: 1, machineID: "ccc", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerC.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == cPeerID } .isEmpty, "should have a bottle for peer") let secret = containerC.loadSecretSync(test: self, label: cPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error4) XCTAssertNotNil(cPeerID) XCTAssertNotNil(cPermanentInfo) XCTAssertNotNil(cPermanentInfoSig) do { // C, when it joins, will create a new CKKS zone. It should also upload TLKShares for A and B. let provisionalEngramKeySet = try self.makeFakeKeyHierarchy(zoneID: CKRecordZone.ID(zoneName: "Engram")) provisionalEngramKeySet.newUpload = true assertNoTLKShareFor(peerID: cPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) assertNoTLKShareFor(peerID: cPeerID!, keyUUID: provisionalEngramKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Engram")) print("B vouches for C") let (voucherData, voucherSig, error) = containerB.vouchSync(test: self, peerID: cPeerID!, permanentInfo: cPermanentInfo!, permanentInfoSig: cPermanentInfoSig!, stableInfo: cStableInfo!, stableInfoSig: cStableInfoSig!, ckksKeys: [self.manateeKeySet]) XCTAssertNil(error) XCTAssertNotNil(voucherData) XCTAssertNotNil(voucherSig) assertTLKShareFor(peerID: cPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("C joins") let (peerID, _, error2) = containerC.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [self.manateeKeySet, provisionalEngramKeySet], tlkShares: []) XCTAssertNil(error2) XCTAssertEqual(peerID, cPeerID!) assertTLKShareFor(peerID: cPeerID!, keyUUID: provisionalEngramKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Engram")) assertTLKShareFor(peerID: aPeerID!, keyUUID: provisionalEngramKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Engram")) assertTLKShareFor(peerID: bPeerID!, keyUUID: provisionalEngramKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Engram")) } print("A updates") do { let (_, error) = containerA.updateSync(test: self) XCTAssertNil(error) } do { let state = containerA.getStateSync(test: self) let a = state.peers[aPeerID!]! XCTAssertTrue(a.dynamicInfo!.includedPeerIDs.contains(cPeerID!)) } _ = containerA.dumpSync(test: self) _ = containerB.dumpSync(test: self) _ = containerC.dumpSync(test: self) } func testJoinWithoutAllowListErrors() throws { let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let (peerID, permanentInfo, permanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == peerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: peerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error, "Should not have an error after preparing A") XCTAssertNotNil(peerID, "Should have a peer ID after preparing A") XCTAssertNotNil(permanentInfo, "Should have a permanent info after preparing A") XCTAssertNotNil(permanentInfoSig, "Should have a signature after preparing A") XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa"]), "should be able to set allowed machine IDs") let (peerID2, _, error2) = containerA.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: []) XCTAssertNotNil(peerID2, "Should get a peer when you establish a now allow-listed peer") XCTAssertNil(error2, "Should not get an error when you establish a now allow-listed peer") print("preparing B") let (bPeerID, bPermanentInfo, bPermanentInfoSig, bStableInfo, bStableInfoSig, errorPrepareB) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == peerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: peerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(errorPrepareB, "Should not have an error after preparing B") XCTAssertNotNil(bPeerID, "Should have a peer ID after preparing B") XCTAssertNotNil(bPermanentInfo, "Should have a permanent info after preparing B") XCTAssertNotNil(bPermanentInfoSig, "Should have a signature after preparing B") XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa"]), "should be able to set allowed machine IDs on container B") do { print("A vouches for B") let (voucherData, voucherSig, error3) = containerA.vouchSync(test: self, peerID: bPeerID!, permanentInfo: bPermanentInfo!, permanentInfoSig: bPermanentInfoSig!, stableInfo: bStableInfo!, stableInfoSig: bStableInfoSig!, ckksKeys: []) XCTAssertNil(error3, "Should be no error vouching for B") XCTAssertNotNil(voucherData, "Should have a voucher from A") XCTAssertNotNil(voucherSig, "Should have a signature from A") print("B joins") let (peerID, _, error) = containerB.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [], tlkShares: []) XCTAssertNotNil(error, "Should have an error joining with an unapproved machine ID") XCTAssertNil(peerID, "Should not receive a peer ID joining with an unapproved machine ID") } } func testReset() throws { let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { assertNoTLKShareFor(peerID: aPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) assertTLKShareFor(peerID: aPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) } print("reset A") do { let error = containerA.resetSync(test: self) XCTAssertNil(error) } do { let (dict, error) = containerA.dumpSync(test: self) XCTAssertNil(error) XCTAssertNotNil(dict) let peers: Array = dict!["peers"] as! Array XCTAssertEqual(0, peers.count) } } func testResetLocal() throws { let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error, "Should be no error preparing an identity") XCTAssertNotNil(aPeerID, "Should have a peer ID after preparing") XCTAssertNotNil(aPermanentInfo, "Should have a permanentInfo after preparing") XCTAssertNotNil(aPermanentInfoSig, "Should have a permanentInfoSign after preparing") do { let (dict, error) = containerA.dumpSync(test: self) XCTAssertNil(error, "Should be no error dumping") XCTAssertNotNil(dict, "Should receive a dump dictionary") let selfInfo: [AnyHashable: Any]? = dict!["self"] as! [AnyHashable: Any]? XCTAssertNotNil(selfInfo, "Should have a self dictionary") let selfPeer: String? = selfInfo!["peerID"] as! String? XCTAssertNotNil(selfPeer, "self peer should be part of the dump") } do { let error = containerA.localResetSync(test: self) XCTAssertNil(error, "local-reset shouldn't error") } do { let (dict, error) = containerA.dumpSync(test: self) XCTAssertNil(error, "Should be no error dumping") XCTAssertNotNil(dict, "Should receive a dump dictionary") let selfInfo: [AnyHashable: Any]? = dict!["self"] as! [AnyHashable: Any]? XCTAssertNotNil(selfInfo, "Should have a self dictionary") let selfPeer: String? = selfInfo!["peerID"] as! String? XCTAssertNil(selfPeer, "self peer should not be part of the dump") } } func testReplayAttack() throws { let description = tmpStoreDescription(name: "container.db") var containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerC = try Container(name: ContainerName(container: "c", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]))) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]))) XCTAssertNil(containerC.setAllowedMachineIDsSync(test: self, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]))) print("preparing") let (peerID, _, _, _, _, _) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == peerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: peerID!) XCTAssertNotNil(secret, "secret should not be nil") } let (bPeerID, bPermanentInfo, bPermanentInfoSig, bStableInfo, bStableInfoSig, _) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") } let (cPeerID, cPermanentInfo, cPermanentInfoSig, cStableInfo, cStableInfoSig, _) = containerC.prepareSync(test: self, epoch: 1, machineID: "ccc", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerC.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == cPeerID } .isEmpty, "should have a bottle for peer") let secret = containerC.loadSecretSync(test: self, label: cPeerID!) XCTAssertNotNil(secret, "secret should not be nil") } print("establishing A") _ = containerA.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: []) do { print("A vouches for B") let (voucherData, voucherSig, _) = containerA.vouchSync(test: self, peerID: bPeerID!, permanentInfo: bPermanentInfo!, permanentInfoSig: bPermanentInfoSig!, stableInfo: bStableInfo!, stableInfoSig: bStableInfoSig!, ckksKeys: []) print("B joins") _ = containerB.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [], tlkShares: []) } print("A updates") _ = containerA.updateSync(test: self) let earlyClock: TPCounter do { let state = containerA.getStateSync(test: self) let b = state.peers[bPeerID!]! earlyClock = b.dynamicInfo!.clock } // Take a snapshot let snapshot = cuttlefish.state do { print("B vouches for C") let (voucherData, voucherSig, _) = containerB.vouchSync(test: self, peerID: cPeerID!, permanentInfo: cPermanentInfo!, permanentInfoSig: cPermanentInfoSig!, stableInfo: cStableInfo!, stableInfoSig: cStableInfoSig!, ckksKeys: []) print("C joins") _ = containerC.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [], tlkShares: []) } print("B updates") _ = containerB.updateSync(test: self) print("A updates") _ = containerA.updateSync(test: self) let lateClock: TPCounter do { let state = containerA.getStateSync(test: self) let b = state.peers[bPeerID!]! lateClock = b.dynamicInfo!.clock XCTAssertTrue(earlyClock < lateClock) } print("Reverting cuttlefish to the snapshot") cuttlefish.state = snapshot cuttlefish.makeSnapshot() print("A updates, fetching the old snapshot from cuttlefish") _ = containerA.updateSync(test: self) print("Reload A. Now we see whether it persisted the replayed snapshot in the previous step.") containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) do { let state = containerA.getStateSync(test: self) let b = state.peers[bPeerID!]! XCTAssertEqual(lateClock, b.dynamicInfo!.clock) } } // TODO: need a real configurable mock cuttlefish func testFetchPolicyDocuments() throws { // 1 is known locally via builtin, 3 is not but is known to cuttlefish let policies = [ 1: ("SHA256:TLXrcQmY4ue3oP5pCX1pwsi9BF8cKfohlJBilCroeBs=", "CAESDgoGaVBob25lEgRmdWxsEgwKBGlQYWQSBGZ1bGwSCwoDTWFjEgRmdWxsEgwKBGlNYWMSBGZ1bGwSDQoHQXBwbGVUVhICdHYS" + "DgoFV2F0Y2gSBXdhdGNoGhEKCVBDU0VzY3JvdxIEZnVsbBoXCgRXaUZpEgRmdWxsEgJ0dhIFd2F0Y2gaGQoRU2FmYXJpQ3JlZGl0" + "Q2FyZHMSBGZ1bGwiDAoEZnVsbBIEZnVsbCIUCgV3YXRjaBIEZnVsbBIFd2F0Y2giDgoCdHYSBGZ1bGwSAnR2"), 3: ("SHA256:JZzazSuHXrUhiOfSgElsg6vYKpnvvEPVpciR8FewRWg=", "CAMSDgoGaVBob25lEgRmdWxsEgwKBGlQYWQSBGZ1bGwSCwoDTWFjEgRmdWxsEgwKBGlNYWMSBGZ1bGwSDQoHQXBwbGVUVhICdHYSDgoFV2F0Y2gSBXdhdGNoEhcKDkF1ZGlvQWNjZXNzb3J5EgVhdWRpbxocCg1EZXZpY2VQYWlyaW5nEgRmdWxsEgV3YXRjaBoXCghBcHBsZVBheRIEZnVsbBIFd2F0Y2gaJAoVUHJvdGVjdGVkQ2xvdWRTdG9yYWdlEgRmdWxsEgV3YXRjaBoXCghCYWNrc3RvcBIEZnVsbBIFd2F0Y2gaGQoKQXV0b1VubG9jaxIEZnVsbBIFd2F0Y2gaHwoQU2VjdXJlT2JqZWN0U3luYxIEZnVsbBIFd2F0Y2gaIAoRU2FmYXJpQ3JlZGl0Q2FyZHMSBGZ1bGwSBXdhdGNoGhMKBEhvbWUSBGZ1bGwSBXdhdGNoGh4KD1NhZmFyaVBhc3N3b3JkcxIEZnVsbBIFd2F0Y2gaGwoMQXBwbGljYXRpb25zEgRmdWxsEgV3YXRjaBoVCgZFbmdyYW0SBGZ1bGwSBXdhdGNoGi0KE0xpbWl0ZWRQZWVyc0FsbG93ZWQSBGZ1bGwSBXdhdGNoEgJ0dhIFYXVkaW8aFgoHTWFuYXRlZRIEZnVsbBIFd2F0Y2gaHgoEV2lGaRIEZnVsbBIFd2F0Y2gSAnR2EgVhdWRpbxoVCgZIZWFsdGgSBGZ1bGwSBXdhdGNoIhMKBGZ1bGwSBGZ1bGwSBXdhdGNoIhsKBWF1ZGlvEgRmdWxsEgV3YXRjaBIFYXVkaW8iFAoFd2F0Y2gSBGZ1bGwSBXdhdGNoIhUKAnR2EgRmdWxsEgV3YXRjaBICdHYyIgoWAAQiEgIEdndodAoKXkFwcGxlUGF5JBIIQXBwbGVQYXkyJgoYAAQiFAIEdndodAoMXkF1dG9VbmxvY2skEgpBdXRvVW5sb2NrMh4KFAAEIhACBHZ3aHQKCF5FbmdyYW0kEgZFbmdyYW0yHgoUAAQiEAIEdndodAoIXkhlYWx0aCQSBkhlYWx0aDIaChIABCIOAgR2d2h0CgZeSG9tZSQSBEhvbWUyIAoVAAQiEQIEdndodAoJXk1hbmF0ZWUkEgdNYW5hdGVlMjgKIQAEIh0CBHZ3aHQKFV5MaW1pdGVkUGVlcnNBbGxvd2VkJBITTGltaXRlZFBlZXJzQWxsb3dlZDJdClAAAhIeAAQiGgIEdndodAoSXkNvbnRpbnVpdHlVbmxvY2skEhUABCIRAgR2d2h0CgleSG9tZUtpdCQSFQAEIhECBHZ3aHQKCV5BcHBsZVRWJBIJTm90U3luY2VkMisKGwAEIhcCBGFncnAKD15bMC05QS1aXXsxMH1cLhIMQXBwbGljYXRpb25zMsUBCrABAAISNAABChMABCIPAgVjbGFzcwoGXmdlbnAkChsABCIXAgRhZ3JwCg9eY29tLmFwcGxlLnNiZCQSPQABChMABCIPAgVjbGFzcwoGXmtleXMkCiQABCIgAgRhZ3JwChheY29tLmFwcGxlLnNlY3VyaXR5LnNvcyQSGQAEIhUCBHZ3aHQKDV5CYWNrdXBCYWdWMCQSHAAEIhgCBHZ3aHQKEF5pQ2xvdWRJZGVudGl0eSQSEFNlY3VyZU9iamVjdFN5bmMyYwpbAAISEgAEIg4CBHZ3aHQKBl5XaUZpJBJDAAEKEwAEIg8CBWNsYXNzCgZeZ2VucCQKEwAEIg8CBGFncnAKB15hcHBsZSQKFQAEIhECBHN2Y2UKCV5BaXJQb3J0JBIEV2lGaTLbAgrBAgACEhkABCIVAgR2d2h0Cg1eUENTQ2xvdWRLaXQkEhcABCITAgR2d2h0CgteUENTRXNjcm93JBIUAAQiEAIEdndodAoIXlBDU0ZERSQSGQAEIhUCBHZ3aHQKDV5QQ1NGZWxkc3BhciQSGQAEIhUCBHZ3aHQKDV5QQ1NNYWlsRHJvcCQSGgAEIhYCBHZ3aHQKDl5QQ1NNYXN0ZXJLZXkkEhYABCISAgR2d2h0CgpeUENTTm90ZXMkEhcABCITAgR2d2h0CgteUENTUGhvdG9zJBIYAAQiFAIEdndodAoMXlBDU1NoYXJpbmckEh0ABCIZAgR2d2h0ChFeUENTaUNsb3VkQmFja3VwJBIcAAQiGAIEdndodAoQXlBDU2lDbG91ZERyaXZlJBIZAAQiFQIEdndodAoNXlBDU2lNZXNzYWdlJBIVUHJvdGVjdGVkQ2xvdWRTdG9yYWdlMkAKKwAEIicCBGFncnAKH15jb20uYXBwbGUuc2FmYXJpLmNyZWRpdC1jYXJkcyQSEVNhZmFyaUNyZWRpdENhcmRzMjQKIQAEIh0CBGFncnAKFV5jb20uYXBwbGUuY2ZuZXR3b3JrJBIPU2FmYXJpUGFzc3dvcmRzMm0KXAACEh4ABCIaAgR2d2h0ChJeQWNjZXNzb3J5UGFpcmluZyQSGgAEIhYCBHZ3aHQKDl5OYW5vUmVnaXN0cnkkEhwABCIYAgR2d2h0ChBeV2F0Y2hNaWdyYXRpb24kEg1EZXZpY2VQYWlyaW5nMi0KIQAEIh0CBGFncnAKFV5jb20uYXBwbGUuY2ZuZXR3b3JrJBIIQmFja3N0b3A="), ] let (request1, data1) = policies[1]! let (request3, data3) = policies[3]! let description = tmpStoreDescription(name: "container.db") let container = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) // nothing let (response1, error1) = container.fetchPolicyDocumentsSync(test: self, keys: [:]) XCTAssertNil(error1, "No error querying for an empty list") XCTAssertEqual(response1, [:], "Received empty dictionary") // local stuff let (response2, error2) = container.fetchPolicyDocumentsSync(test: self, keys: [1: request1]) XCTAssertNil(error2, "No error getting locally known policy document") XCTAssertEqual(response2?.count, 1, "Got one response for request for one locally known policy") XCTAssertEqual(response2?[1]?[0], request1, "retrieved hash matches request hash") XCTAssertEqual(response2?[1]?[1], data1, "retrieved data matches known data") // fetch remote let (response3, error3) = container.fetchPolicyDocumentsSync(test: self, keys: [1: request1, 3: request3]) XCTAssertNil(error3, "No error fetching local + remote policy") XCTAssertEqual(response3?.count, 2, "Got two responses for local+remote policy request") XCTAssertEqual(response3?[1]?[0], request1, "retrieved hash matches local request hash") XCTAssertEqual(response3?[1]?[1], data1, "retrieved data matches local known data") XCTAssertEqual(response3?[3]?[0], request3, "retrieved hash matches remote request hash") XCTAssertEqual(response3?[3]?[1], data3, "retrieved data matches remote known data") // invalid version let (response4, error4) = container.fetchPolicyDocumentsSync(test: self, keys: [9000: "not a hash"]) XCTAssertNil(response4, "No response for wrong [version: hash] combination") XCTAssertNotNil(error4, "Expected error fetching invalid policy version") // valid + invalid let (response5, error5) = container.fetchPolicyDocumentsSync(test: self, keys: [9000: "not a hash", 1: request1, 3: request3, ]) XCTAssertNil(response5, "No response for valid + unknown [version: hash] combination") XCTAssertNotNil(error5, "Expected error fetching valid + invalid policy version") } func testEscrowKeys() throws { XCTAssertThrowsError(try EscrowKeys.retrieveEscrowKeysFromKeychain(label: "hash"), "retrieveEscrowKeysFromKeychain should throw error") XCTAssertThrowsError(try EscrowKeys.findEscrowKeysForLabel(label: "hash"), "findEscrowKeysForLabel should throw error") let secretString = "i'm a secret!" XCTAssertNotNil(secretString, "secretString should not be nil") let secretData: Data? = secretString.data(using: .utf8) XCTAssertNotNil(secretData, "secretData should not be nil") let keys = try EscrowKeys(secret: secretData!, bottleSalt: "123456789") XCTAssertNotNil(keys, "keys should not be nil") XCTAssertNotNil(keys.secret, "secret should not be nil") XCTAssertNotNil(keys.bottleSalt, "bottleSalt should not be nil") XCTAssertNotNil(keys.encryptionKey, "encryptionKey should not be nil") XCTAssertNotNil(keys.signingKey, "signingKey should not be nil") XCTAssertNotNil(keys.symmetricKey, "symmetricKey should not be nil") let hash = try EscrowKeys.hashEscrowedSigningPublicKey(keyData: keys.signingKey.publicKey().spki()) XCTAssertNotNil(hash, "hash should not be nil") let result = try EscrowKeys.storeEscrowedSigningKeyPair(keyData: keys.signingKey.keyData, label: "Signing Key") XCTAssertTrue(result, "result should be true") let escrowKey = try EscrowKeys.retrieveEscrowKeysFromKeychain(label: hash) XCTAssertNotNil(escrowKey, "escrowKey should not be nil") let (signingKey, encryptionKey, symmetricKey) = try EscrowKeys.findEscrowKeysForLabel(label: hash) XCTAssertNotNil(signingKey, "signingKey should not be nil") XCTAssertNotNil(encryptionKey, "encryptionKey should not be nil") XCTAssertNotNil(symmetricKey, "symmetricKey should not be nil") } func testEscrowKeyTestVectors() { let secretString = "I'm a secretI'm a secretI'm a secretI'm a secretI'm a secretI'm a secret" let secret = secretString.data(using: .utf8) do { let testv1 = try EscrowKeys.generateEscrowKey(keyType: escrowKeyType.kOTEscrowKeySigning, masterSecret: secret!, bottleSalt: testDSID) XCTAssertEqual(testv1, signingKey_384, "signing keys should match") let testv2 = try EscrowKeys.generateEscrowKey(keyType: escrowKeyType.kOTEscrowKeyEncryption, masterSecret: secret!, bottleSalt: testDSID) XCTAssertEqual(testv2, encryptionKey_384, "encryption keys should match") let testv3 = try EscrowKeys.generateEscrowKey(keyType: escrowKeyType.kOTEscrowKeySymmetric, masterSecret: secret!, bottleSalt: testDSID) XCTAssertEqual(testv3, symmetricKey_384, "symmetric keys should match") let newSecretString = "I'm f secretI'm a secretI'm a secretI'm a secretI'm a secretI'm a secret" let newSecret = newSecretString.data(using: .utf8) let testv4 = try EscrowKeys.generateEscrowKey(keyType: escrowKeyType.kOTEscrowKeySigning, masterSecret: newSecret!, bottleSalt: testDSID) XCTAssertNotEqual(testv4, signingKey_384, "signing keys should not match") let testv5 = try EscrowKeys.generateEscrowKey(keyType: escrowKeyType.kOTEscrowKeyEncryption, masterSecret: newSecret!, bottleSalt: testDSID) XCTAssertNotEqual(testv5, encryptionKey_384, "encryption keys should not match") let testv6 = try EscrowKeys.generateEscrowKey(keyType: escrowKeyType.kOTEscrowKeySymmetric, masterSecret: newSecret!, bottleSalt: testDSID) XCTAssertNotEqual(testv6, symmetricKey_384, "symmetric keys should not match") } catch { XCTFail("error testing escrow key test vectors \(error)") } } func testJoiningWithBottle() throws { var bottleA: BottleMO var entropy: Data let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { var state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") bottleA = state.bottles.removeFirst() let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { assertNoTLKShareFor(peerID: aPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) assertTLKShareFor(peerID: aPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) } do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) entropy = secret! XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } _ = containerB.updateSync(test: self) print("preparing B") let (bPeerID, _, _, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) do { print("B prepares to join via bottle") let (voucherData, voucherSig, error3) = containerB.vouchWithBottleSync(test: self, b: bottleA.bottleID!, entropy: entropy, bottleSalt: "123456789", tlkShares: []) XCTAssertNil(error3) XCTAssertNotNil(voucherData) XCTAssertNotNil(voucherSig) // Before B joins, there should be no TLKShares for B assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("B joins") let (peerID, _, error) = containerB.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [self.manateeKeySet], tlkShares: []) XCTAssertNil(error) XCTAssertEqual(peerID, bPeerID!) // But afterward, it has one! assertTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) } } func testJoiningWithBottleAndEmptyBottleSalt() throws { var bottleA: BottleMO var entropy: Data let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { var state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") bottleA = state.bottles.removeFirst() let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { assertNoTLKShareFor(peerID: aPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) assertTLKShareFor(peerID: aPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) } do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) entropy = secret! XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } _ = containerB.updateSync(test: self) print("preparing B") let (bPeerID, _, _, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) do { print("B prepares to join via bottle") let (voucherData, voucherSig, error3) = containerB.vouchWithBottleSync(test: self, b: bottleA.bottleID!, entropy: entropy, bottleSalt: "123456789", tlkShares: []) XCTAssertNil(error3) XCTAssertNotNil(voucherData) XCTAssertNotNil(voucherSig) // Before B joins, there should be no TLKShares for B assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("B joins") let (peerID, _, error) = containerB.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [self.manateeKeySet], tlkShares: []) XCTAssertNil(error) XCTAssertEqual(peerID, bPeerID!) // But afterward, it has one! assertTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) } } func testJoiningWithWrongEscrowRecordForBottle() throws { var entropy: Data let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) } do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) entropy = secret! XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } _ = containerB.updateSync(test: self) print("preparing B") let (bPeerID, _, _, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) do { print("B joins via bottle") let (voucherData, voucherSig, error3) = containerB.vouchWithBottleSync(test: self, b: "wrong escrow record", entropy: entropy, bottleSalt: "123456789", tlkShares: []) XCTAssertNotNil(error3) XCTAssertNil(voucherData) XCTAssertNil(voucherSig) } } func testJoiningWithWrongBottle() throws { var bottleB: BottleMO var entropy: Data let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) } do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) entropy = secret! XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } print("preparing B") let (bPeerID, _, _, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { var state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") bottleB = state.bottles.removeFirst() let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) do { print("B joins via bottle") let (voucherData, voucherSig, error3) = containerB.vouchWithBottleSync(test: self, b: bottleB.bottleID!, entropy: entropy, bottleSalt: "123456789", tlkShares: []) XCTAssertNotNil(error3) XCTAssertNil(voucherData) XCTAssertNil(voucherSig) } } func testJoiningWithBottleAndBadSalt() throws { var bottleA: BottleMO var entropy: Data let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { var state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) bottleA = state.bottles.removeFirst() XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) } do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) entropy = secret! XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } _ = containerB.updateSync(test: self) print("preparing B") let (bPeerID, _, _, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) do { print("B joins via bottle") let (voucherData, voucherSig, error3) = containerB.vouchWithBottleSync(test: self, b: bottleA.bottleID!, entropy: entropy, bottleSalt: "987654321", tlkShares: []) XCTAssertNotNil(error3) XCTAssertNil(voucherData) XCTAssertNil(voucherSig) } } func testJoiningWithBottleAndBadSecret() throws { var bottleA: BottleMO let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { var state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) bottleA = state.bottles.removeFirst() XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) } do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } _ = containerB.updateSync(test: self) print("preparing B") let (bPeerID, _, _, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) do { print("B joins via bottle") let (voucherData, voucherSig, error3) = containerB.vouchWithBottleSync(test: self, b: bottleA.bottleID!, entropy: Data(count: Int(OTMasterSecretLength)), bottleSalt: "123456789", tlkShares: []) XCTAssertNotNil(error3) XCTAssertNil(voucherData) XCTAssertNil(voucherSig) } } func testJoiningWithNoFetchAllBottles() throws { var bottleA: BottleMO var entropy: Data let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { var state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) entropy = secret! bottleA = state.bottles.removeFirst() XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) } do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } print("preparing B") let (bPeerID, _, _, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) do { print("B joins via bottle") // And the first container fetches again, which should succeed let cuttlefishError = NSError(domain: CuttlefishErrorDomain, code: CuttlefishErrorCode.changeTokenExpired.rawValue, userInfo: nil) let ckError = NSError(domain: CKInternalErrorDomain, code: CKInternalErrorCode.errorInternalPluginError.rawValue, userInfo: [NSUnderlyingErrorKey: cuttlefishError]) self.cuttlefish.fetchViableBottlesError.append(ckError) let (voucherData, voucherSig, error3) = containerB.vouchWithBottleSync(test: self, b: bottleA.bottleID!, entropy: entropy, bottleSalt: "123456789", tlkShares: []) XCTAssertNotNil(error3) XCTAssertNil(voucherData) XCTAssertNil(voucherSig) } } func testJoinByPreapproval() throws { let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("preparing B") let (bPeerID, bPermanentInfo, bPermanentInfoSig, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) XCTAssertNotNil(bPeerID) XCTAssertNotNil(bPermanentInfo) XCTAssertNotNil(bPermanentInfoSig) // Now, A establishes preapproving B // Note: secd is responsible for passing in TLKShares to these preapproved keys in sosTLKShares let bPermanentInfoParsed = TPPeerPermanentInfo(peerID: bPeerID!, data: bPermanentInfo!, sig: bPermanentInfoSig!, keyFactory: TPECPublicKeyFactory()) XCTAssertNotNil(bPermanentInfoParsed, "Should have parsed B's permanent info") print("establishing A") do { let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: [], preapprovedKeys: [bPermanentInfoParsed!.signingPubKey.spki()]) XCTAssertNil(error) XCTAssertNotNil(peerID) } do { assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("B joins by preapproval, and uploads all TLKShares that it has") let (bJoinedPeerID, _, bJoinedError) = containerB.preapprovedJoinSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: []) XCTAssertNil(bJoinedError, "Should be no error joining by preapproval") XCTAssertNotNil(bJoinedPeerID, "Should have a peer ID out of join") assertTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) } _ = containerA.dumpSync(test: self) _ = containerB.dumpSync(test: self) } func testDepart() throws { let description = tmpStoreDescription(name: "container.db") let (container, peerID) = try establish(reload: false, store: description) XCTAssertNil(container.departByDistrustingSelfSync(test: self), "Should be no error distrusting self") assertDistrusts(context: container, peerIDs: [peerID]) } func testDistrustPeers() throws { let store = tmpStoreDescription(name: "container.db") let (c, peerID1) = try establish(reload: false, store: store) let (c2, peerID2) = try joinByVoucher(sponsor: c, containerID: "second", machineID: "bbb", machineIDs: ["aaa", "bbb", "ccc"], store: store) let (c3, peerID3) = try joinByVoucher(sponsor: c, containerID: "third", machineID: "ccc", machineIDs: ["aaa", "bbb", "ccc"], store: store) let (_, cUpdateError) = c.updateSync(test: self) XCTAssertNil(cUpdateError, "Should be able to update first container") assertTrusts(context: c, peerIDs: [peerID1, peerID2, peerID3]) // You can't distrust yourself via peerID. XCTAssertNotNil(c.distrustSync(test: self, peerIDs: Set([peerID1, peerID2, peerID3])), "Should error trying to distrust yourself via peer ID") assertTrusts(context: c, peerIDs: [peerID1, peerID2, peerID3]) // Passing in nonsense should error too XCTAssertNotNil(c.distrustSync(test: self, peerIDs: Set(["not a real peer ID"])), "Should error when passing in unknown peer IDs") // Now, distrust both peers. XCTAssertNil(c.distrustSync(test: self, peerIDs: Set([peerID2, peerID3])), "Should be no error distrusting peers") assertDistrusts(context: c, peerIDs: [peerID2, peerID3]) // peers should accept their fates let (_, c2UpdateError) = c2.updateSync(test: self) XCTAssertNil(c2UpdateError, "Should be able to update second container") assertDistrusts(context: c2, peerIDs: [peerID2]) let (_, c3UpdateError) = c3.updateSync(test: self) XCTAssertNil(c3UpdateError, "Should be able to update third container") assertDistrusts(context: c3, peerIDs: [peerID3]) } func testFetchWithBadChangeToken() throws { let (c, peerID1) = try establish(reload: false, store: tmpStoreDescription(name: "container.db")) // But all that goes away, and a new peer establishes self.cuttlefish.state = FakeCuttlefishServer.State() let (_, peerID2) = try establish(reload: false, contextID: "second", store: tmpStoreDescription(name: "container-peer2.db")) // And the first container fetches again, which should succeed let cuttlefishError = NSError(domain: CuttlefishErrorDomain, code: CuttlefishErrorCode.changeTokenExpired.rawValue, userInfo: nil) let ckError = NSError(domain: CKInternalErrorDomain, code: CKInternalErrorCode.errorInternalPluginError.rawValue, userInfo: [NSUnderlyingErrorKey: cuttlefishError]) self.cuttlefish.nextFetchErrors.append(ckError) _ = c.updateSync(test: self) // and c's model should only include peerID2 c.moc.performAndWait { let modelPeers = c.model.allPeerIDs() XCTAssertEqual(modelPeers.count, 1, "Model should have one peer") XCTAssert(modelPeers.contains(peerID2), "Model should contain peer 2") XCTAssertFalse(modelPeers.contains(peerID1), "Model should no longer container peer 1 (ego peer)") } } func testFetchEscrowContents() throws { let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let (entropyA, bottleIDA, spkiA, errorA) = containerA.fetchEscrowContentsSync(test: self) XCTAssertNotNil(errorA, "Should be an error fetching escrow contents") XCTAssertEqual(errorA.debugDescription, "Optional(TrustedPeersHelperUnitTests.ContainerError.noPreparedIdentity)", "error should be no prepared identity") XCTAssertNil(entropyA, "Should not have some entropy to bottle") XCTAssertNil(bottleIDA, "Should not have a bottleID") XCTAssertNil(spkiA, "Should not have an SPKI") let (c, peerID) = try establish(reload: false, store: description) XCTAssertNotNil(peerID, "establish should return a peer id") let (entropy, bottleID, spki, error) = c.fetchEscrowContentsSync(test: self) XCTAssertNil(error, "Should be no error fetching escrow contents") XCTAssertNotNil(entropy, "Should have some entropy to bottle") XCTAssertNotNil(bottleID, "Should have a bottleID") XCTAssertNotNil(spki, "Should have an SPKI") } func testBottles() { do { let peerSigningKey = _SFECKeyPair.init(randomKeyPairWith: _SFECKeySpecifier.init(curve: SFEllipticCurve.nistp384))! let peerEncryptionKey = _SFECKeyPair.init(randomKeyPairWith: _SFECKeySpecifier.init(curve: SFEllipticCurve.nistp384))! let bottle = try BottledPeer(peerID: "peerID", bottleID: UUID().uuidString, peerSigningKey: peerSigningKey, peerEncryptionKey: peerEncryptionKey, bottleSalt: "123456789") let keys = bottle.escrowKeys XCTAssertNotNil(keys, "keys should not be nil") XCTAssertNotNil(bottle, "bottle should not be nil") XCTAssertNotNil(bottle.escrowSigningPublicKeyHash(), "escrowSigningPublicKeyHash should not be nil") XCTAssertThrowsError(try BottledPeer.verifyBottleSignature(data: bottle.contents, signature: bottle.signatureUsingPeerKey, pubKey: keys.signingKey.publicKey() as! _SFECPublicKey)) XCTAssertThrowsError(try BottledPeer.verifyBottleSignature(data: bottle.contents, signature: bottle.signatureUsingEscrowKey, pubKey: peerSigningKey.publicKey() as! _SFECPublicKey)) XCTAssertNotNil(BottledPeer.signingOperation(), "signing operation should not be nil") let verifyBottleEscrowSignature = try BottledPeer.verifyBottleSignature(data: bottle.contents, signature: bottle.signatureUsingEscrowKey, pubKey: keys.signingKey.publicKey() as! _SFECPublicKey) XCTAssertNotNil(verifyBottleEscrowSignature, "verifyBottleEscrowSignature should not be nil") let verifyBottlePeerSignature = try BottledPeer.verifyBottleSignature(data: bottle.contents, signature: bottle.signatureUsingPeerKey, pubKey: peerSigningKey.publicKey() as! _SFECPublicKey) XCTAssertNotNil(verifyBottlePeerSignature, "verifyBottlePeerSignature should not be nil") let deserializedBottle = try BottledPeer(contents: bottle.contents, secret: bottle.secret, bottleSalt: "123456789", signatureUsingEscrow: bottle.signatureUsingEscrowKey, signatureUsingPeerKey: bottle.signatureUsingPeerKey) XCTAssertNotNil(deserializedBottle, "deserializedBottle should not be nil") XCTAssertEqual(deserializedBottle.contents, bottle.contents, "bottle data should be equal") } catch { XCTFail("error testing bottles \(error)") } } func testFetchBottles() throws { let store = tmpStoreDescription(name: "container.db") let (c, _) = try establish(reload: false, store: store) let (bottles, _, fetchError) = c.fetchViableBottlesSync(test: self) XCTAssertNil(fetchError, "should be no error fetching viable bottles") XCTAssertNotNil(bottles, "should have fetched some bottles") XCTAssertEqual(bottles!.count, 1, "should have fetched one bottle") do { let state = c.getStateSync(test: self) XCTAssertEqual(state.bottles.count, 1, "first container should have a bottle for peer") } let c2 = try Container(name: ContainerName(container: "test", context: "newcomer"), persistentStoreDescription: store, cuttlefish: self.cuttlefish) do { let state = c2.getStateSync(test: self) XCTAssertEqual(state.bottles.count, 0, "before fetch, second container should not have any stored bottles") } let (c2bottles, _, c2FetchError) = c2.fetchViableBottlesSync(test: self) XCTAssertNil(c2FetchError, "should be no error fetching viable bottles") XCTAssertNotNil(c2bottles, "should have fetched some bottles") XCTAssertEqual(c2bottles!.count, 1, "should have fetched one bottle") do { let state = c2.getStateSync(test: self) XCTAssertEqual(state.bottles.count, 1, "after fetch, second container should have one stored bottles") } } func testTrustStatus() throws { let store = tmpStoreDescription(name: "container.db") let preC = try Container(name: ContainerName(container: "preC", context: "preCContext"), persistentStoreDescription: store, cuttlefish: self.cuttlefish) let (preEgoStatus, precStatusError) = preC.trustStatusSync(test: self) XCTAssertNil(precStatusError, "No error fetching status") XCTAssertEqual(preEgoStatus.egoStatus, .unknown, "Before establish, trust status should be 'unknown'") XCTAssertNil(preEgoStatus.egoPeerID, "should not have a peer ID") XCTAssertEqual(preEgoStatus.numberOfPeersInOctagon, 0, "should not see number of peers") XCTAssertFalse(preEgoStatus.isExcluded, "should be excluded") let (c, _) = try establish(reload: false, store: store) let (cEgoStatus, cStatusError) = c.trustStatusSync(test: self) XCTAssertNil(cStatusError, "Should be no error fetching trust status directly after establish") XCTAssertEqual(cEgoStatus.egoStatus, [.fullyReciprocated, .selfTrust], "After establish, should be fully reciprocated") XCTAssertNotNil(cEgoStatus.egoPeerID, "should have a peer ID") XCTAssertEqual(cEgoStatus.numberOfPeersInOctagon, 1, "should be 1 peer") XCTAssertFalse(cEgoStatus.isExcluded, "should not be excluded") let c2 = try Container(name: ContainerName(container: "differentContainer", context: "a different context"), persistentStoreDescription: store, cuttlefish: self.cuttlefish) let (egoStatus, statusError) = c2.trustStatusSync(test: self) XCTAssertNil(statusError, "No error fetching status") XCTAssertEqual(egoStatus.egoStatus, .excluded, "After establish, other container should be 'excluded'") XCTAssertNil(egoStatus.egoPeerID, "should not have a peerID") XCTAssertEqual(egoStatus.numberOfPeersInOctagon, 1, "should be 1 peer") XCTAssertTrue(egoStatus.isExcluded, "should not be excluded") } func testTrustStatusWhenMissingIdentityKeys() throws { let store = tmpStoreDescription(name: "container.db") let (c, _) = try establish(reload: false, store: store) let (cEgoStatus, cStatusError) = c.trustStatusSync(test: self) XCTAssertNil(cStatusError, "Should be no error fetching trust status directly after establish") XCTAssertEqual(cEgoStatus.egoStatus, [.fullyReciprocated, .selfTrust], "After establish, should be fully reciprocated") XCTAssertNotNil(cEgoStatus.egoPeerID, "should have a peer ID") XCTAssertEqual(cEgoStatus.numberOfPeersInOctagon, 1, "should be 1 peer") XCTAssertFalse(cEgoStatus.isExcluded, "should not be excluded") let result = try removeEgoKeysSync(peerID: cEgoStatus.egoPeerID!) XCTAssertTrue(result, "result should be true") let (distrustedStatus, distrustedError) = c.trustStatusSync(test: self) XCTAssertNotNil(distrustedError, "error should not be nil") XCTAssertEqual(distrustedStatus.egoStatus, [.excluded], "trust status should be excluded") XCTAssertTrue(distrustedStatus.isExcluded, "should be excluded") } func testSetRecoveryKey() throws { let store = tmpStoreDescription(name: "container.db") let c = try Container(name: ContainerName(container: "c", context: "context"), persistentStoreDescription: store, cuttlefish: self.cuttlefish) let machineIDs = Set(["aaa", "bbb", "ccc"]) XCTAssertNil(c.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing peer A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = c.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = c.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = c.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { let (peerID, _, error) = c.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: nil) XCTAssertNil(error) XCTAssertNotNil(peerID) } let recoveryKey = SecRKCreateRecoveryKeyString(nil) XCTAssertNotNil(recoveryKey, "recoveryKey should not be nil") let (setRecoveryError) = c.setRecoveryKeySync(test: self, recoveryKey: recoveryKey!, recoverySalt: "altDSID", ckksKeys: []) XCTAssertNil(setRecoveryError, "error should be nil") } func roundTripThroughSetValueTransformer(set: Set) { let t = SetValueTransformer() let transformedSet = t.transformedValue(set) as? Data XCTAssertNotNil(transformedSet, "SVT should return some data") let recoveredSet = t.reverseTransformedValue(transformedSet) as? Set XCTAssertNotNil(recoveredSet, "SVT should return some recovered set") XCTAssertEqual(set, recoveredSet, "Recovered set should be the same as original") } func testSetValueTransformer() { roundTripThroughSetValueTransformer(set: Set([])) roundTripThroughSetValueTransformer(set: Set(["asdf"])) roundTripThroughSetValueTransformer(set: Set(["asdf", "three", "test"])) } func testGetRepairSuggestion() throws { let store = tmpStoreDescription(name: "container.db") let c = try Container(name: ContainerName(container: "c", context: "context"), persistentStoreDescription: store, cuttlefish: self.cuttlefish) let machineIDs = Set(["aaa", "bbb", "ccc"]) XCTAssertNil(c.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing peer A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = c.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = c.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = c.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { let (peerID, _, error) = c.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: nil) XCTAssertNil(error) XCTAssertNotNil(peerID) } let (repairAccount, repairEscrow, resetOctagon, healthError) = c.requestHealthCheckSync(requiresEscrowCheck: true, test: self) XCTAssertEqual(repairAccount, false, "") XCTAssertEqual(repairEscrow, false, "") XCTAssertEqual(resetOctagon, false, "") XCTAssertNil(healthError) } func testFetchChangesFailDuringVouchWithBottle() throws { var bottleA: BottleMO var entropy: Data let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, _, _, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { var state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") bottleA = state.bottles.removeFirst() let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) print("establishing A") do { assertNoTLKShareFor(peerID: aPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: [], preapprovedKeys: []) XCTAssertNil(error) XCTAssertNotNil(peerID) assertTLKShareFor(peerID: aPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) } do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) entropy = secret! XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } _ = containerB.updateSync(test: self) print("preparing B") let (bPeerID, _, _, _, _, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) do { print("B prepares to join via bottle") let (voucherData, voucherSig, error3) = containerB.vouchWithBottleSync(test: self, b: bottleA.bottleID!, entropy: entropy, bottleSalt: "123456789", tlkShares: []) XCTAssertNil(error3) XCTAssertNotNil(voucherData) XCTAssertNotNil(voucherSig) // And the first container fetches again, which should succeed let cuttlefishError = NSError(domain: CuttlefishErrorDomain, code: CuttlefishErrorCode.changeTokenExpired.rawValue, userInfo: nil) let ckError = NSError(domain: CKInternalErrorDomain, code: CKInternalErrorCode.errorInternalPluginError.rawValue, userInfo: [NSUnderlyingErrorKey: cuttlefishError]) self.cuttlefish.nextFetchErrors.append(ckError) // Before B joins, there should be no TLKShares for B assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("B joins") let (peerID, _, error) = containerB.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [self.manateeKeySet], tlkShares: []) XCTAssertNotNil(error) XCTAssertNil(peerID) } } func testDistrustedPeerRecoveryKeyNotSet() throws { let description = tmpStoreDescription(name: "container.db") let containerA = try Container(name: ContainerName(container: "a", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let containerB = try Container(name: ContainerName(container: "b", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let machineIDs = Set(["aaa", "bbb"]) XCTAssertNil(containerA.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) XCTAssertNil(containerB.setAllowedMachineIDsSync(test: self, allowedMachineIDs: machineIDs)) print("preparing peer A") let (aPeerID, aPermanentInfo, aPermanentInfoSig, aStableInfo, aStableInfoSig, error) = containerA.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerA.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == aPeerID } .isEmpty, "should have a bottle for peer") let secret = containerA.loadSecretSync(test: self, label: aPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error) XCTAssertNotNil(aPeerID) XCTAssertNotNil(aPermanentInfo) XCTAssertNotNil(aPermanentInfoSig) XCTAssertNotNil(aStableInfo) XCTAssertNotNil(aStableInfoSig) print("establishing A") do { let (peerID, _, error) = containerA.establishSync(test: self, ckksKeys: [], tlkShares: [], preapprovedKeys: nil) XCTAssertNil(error) XCTAssertNotNil(peerID) } print("preparing B") let (bPeerID, bPermanentInfo, bPermanentInfoSig, bStableInfo, bStableInfoSig, error2) = containerB.prepareSync(test: self, epoch: 1, machineID: "bbb", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") do { let state = containerB.getStateSync(test: self) XCTAssertFalse( state.bottles.filter { $0.peerID == bPeerID } .isEmpty, "should have a bottle for peer") let secret = containerB.loadSecretSync(test: self, label: bPeerID!) XCTAssertNotNil(secret, "secret should not be nil") XCTAssertNil(error, "error should be nil") } XCTAssertNil(error2) XCTAssertNotNil(bPeerID) XCTAssertNotNil(bPermanentInfo) XCTAssertNotNil(bPermanentInfoSig) do { assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("A vouches for B, but doesn't provide any TLKShares") let (_, _, errorVouchingWithoutTLKs) = containerA.vouchSync(test: self, peerID: bPeerID!, permanentInfo: bPermanentInfo!, permanentInfoSig: bPermanentInfoSig!, stableInfo: bStableInfo!, stableInfoSig: bStableInfoSig!, ckksKeys: []) XCTAssertNil(errorVouchingWithoutTLKs, "Should be no error vouching without uploading TLKShares") assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("A vouches for B, but doesn't only has provisional TLKs at the time") let provisionalManateeKeySet = try self.makeFakeKeyHierarchy(zoneID: CKRecordZone.ID(zoneName: "Manatee")) provisionalManateeKeySet.newUpload = true let (_, _, errorVouchingWithProvisionalTLKs) = containerA.vouchSync(test: self, peerID: bPeerID!, permanentInfo: bPermanentInfo!, permanentInfoSig: bPermanentInfoSig!, stableInfo: bStableInfo!, stableInfoSig: bStableInfoSig!, ckksKeys: [provisionalManateeKeySet]) XCTAssertNil(errorVouchingWithProvisionalTLKs, "Should be no error vouching without uploading TLKShares for a non-existent key") assertNoTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("A vouches for B") let (voucherData, voucherSig, error3) = containerA.vouchSync(test: self, peerID: bPeerID!, permanentInfo: bPermanentInfo!, permanentInfoSig: bPermanentInfoSig!, stableInfo: bStableInfo!, stableInfoSig: bStableInfoSig!, ckksKeys: [self.manateeKeySet]) XCTAssertNil(error3) XCTAssertNotNil(voucherData) XCTAssertNotNil(voucherSig) // As part of the vouch, A should have uploaded a tlkshare for B assertTLKShareFor(peerID: bPeerID!, keyUUID: self.manateeKeySet.tlk.uuid, zoneID: CKRecordZone.ID(zoneName: "Manatee")) print("B joins") let (peerID, _, error) = containerB.joinSync(test: self, voucherData: voucherData!, voucherSig: voucherSig!, ckksKeys: [], tlkShares: []) XCTAssertNil(error) XCTAssertEqual(peerID, bPeerID!) } print("A updates") do { let (_, error) = containerA.updateSync(test: self) XCTAssertNil(error) } print("B updates") do { let (_, error) = containerB.updateSync(test: self) XCTAssertNil(error) } // Now, A distrusts B. XCTAssertNil(containerA.distrustSync(test: self, peerIDs: Set([bPeerID!])), "Should be no error distrusting peers") assertDistrusts(context: containerA, peerIDs: [bPeerID!]) let recoveryKey = SecRKCreateRecoveryKeyString(nil) XCTAssertNotNil(recoveryKey, "recoveryKey should not be nil") let (setRecoveryError) = containerB.setRecoveryKeySync(test: self, recoveryKey: recoveryKey!, recoverySalt: "altDSID", ckksKeys: []) XCTAssertNil(setRecoveryError, "error should be nil") print("A updates") do { let (_, error) = containerA.updateSync(test: self) XCTAssertNil(error) } print("B updates") do { let (_, error) = containerB.updateSync(test: self) XCTAssertNil(error) } do { let (dict, error) = containerA.dumpSync(test: self) XCTAssertNil(error, "Should be no error dumping") XCTAssertNotNil(dict, "Should receive a dump dictionary") let selfInfo: [AnyHashable: Any]? = dict!["self"] as! [AnyHashable: Any]? XCTAssertNotNil(selfInfo, "Should have a self dictionary") let stableInfo: [AnyHashable: Any]? = selfInfo!["stableInfo"] as! [AnyHashable: Any]? XCTAssertNotNil(stableInfo, "stableInfo should not be nil") let recoverySigningPublicKey: Data? = stableInfo!["recovery_signing_public_key"] as! Data? XCTAssertNil(recoverySigningPublicKey, "recoverySigningPublicKey should be nil") let recoveryEncryptionPublicKey: Data? = stableInfo!["recovery_encryption_public_key"] as! Data? XCTAssertNil(recoveryEncryptionPublicKey, "recoveryEncryptionPublicKey should be nil") } } func assert(container: Container, allowedMachineIDs: Set, disallowedMachineIDs: Set, unknownMachineIDs: Set = Set(), persistentStore: NSPersistentStoreDescription, cuttlefish: FakeCuttlefishServer) throws { let midList = container.onqueueCurrentMIDList() XCTAssertEqual(midList.machineIDs(in: .allowed), allowedMachineIDs, "List of allowed machine IDs should match") XCTAssertEqual(midList.machineIDs(in: .disallowed), disallowedMachineIDs, "List of disallowed machine IDs should match") XCTAssertEqual(midList.machineIDs(in: .unknown), unknownMachineIDs, "List of unknown machine IDs should match") // if we reload the container, does it still match? let reloadedContainer = try Container(name: ContainerName(container: "test", context: OTDefaultContext), persistentStoreDescription: persistentStore, cuttlefish: cuttlefish) let reloadedMidList = reloadedContainer.onqueueCurrentMIDList() XCTAssertEqual(reloadedMidList.machineIDs(in: .allowed), allowedMachineIDs, "List of allowed machine IDs on a reloaded container should match") XCTAssertEqual(reloadedMidList.machineIDs(in: .disallowed), disallowedMachineIDs, "List of disallowed machine IDs on a reloaded container should match") XCTAssertEqual(reloadedMidList.machineIDs(in: .unknown), unknownMachineIDs, "List of unknown machine IDs on a reloaded container should match") } func testAllowListManipulation() throws { let description = tmpStoreDescription(name: "container.db") let container = try Container(name: ContainerName(container: "test", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) let (peerID, permanentInfo, permanentInfoSig, _, _, error) = container.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") XCTAssertNil(error) XCTAssertNotNil(peerID) XCTAssertNotNil(permanentInfo) XCTAssertNotNil(permanentInfoSig) try self.assert(container: container, allowedMachineIDs: [], disallowedMachineIDs: [], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb", "ccc"]), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]), disallowedMachineIDs: [], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb"]), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb"]), disallowedMachineIDs: Set(["ccc"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") XCTAssertNil(container.addAllowedMachineIDsSync(test: self, machineIDs: ["zzz", "kkk"]), "should be able to add allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "zzz", "kkk"]), disallowedMachineIDs: Set(["ccc"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // Receivng a 'remove' push should send the MIDs to the 'unknown' list XCTAssertNil(container.removeAllowedMachineIDsSync(test: self, machineIDs: ["bbb", "fff"]), "should be able to remove allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "zzz", "kkk"]), disallowedMachineIDs: Set(["ccc"]), unknownMachineIDs: Set(["bbb", "fff"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertTrue(container.onqueueFullIDMSListWouldBeHelpful(), "Container should think it could use an IDMS list set: there's machine IDs pending removal") // once they're unknown, a full list set will make them disallowed XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "zzz", "kkk"]), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "zzz", "kkk"]), disallowedMachineIDs: Set(["bbb", "ccc", "fff"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // Resetting the list to what it is doesn't change the list XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "zzz", "kkk"], listDifference: false), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "zzz", "kkk"]), disallowedMachineIDs: Set(["bbb", "ccc", "fff"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // But changing it to something completely new does XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["xxx", "mmm"]), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["xxx", "mmm"]), disallowedMachineIDs: Set(["aaa", "zzz", "kkk", "bbb", "ccc", "fff"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // And, readding a previously disallowed machine ID works too XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["xxx", "mmm", "aaa"]), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["xxx", "mmm", "aaa"]), disallowedMachineIDs: Set(["zzz", "kkk", "bbb", "ccc", "fff"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // A update() before establish() doesn't change the list, since it isn't actually changing anything let (_, updateError) = container.updateSync(test: self) XCTAssertNil(updateError, "Should not be an error updating the container without first establishing") try self.assert(container: container, allowedMachineIDs: Set(["xxx", "mmm", "aaa"]), disallowedMachineIDs: Set(["zzz", "kkk", "bbb", "ccc", "fff"]), persistentStore: description, cuttlefish: self.cuttlefish) let (_, _, establishError) = container.establishSync(test: self, ckksKeys: [self.manateeKeySet], tlkShares: [], preapprovedKeys: []) XCTAssertNil(establishError, "Should be able to establish() with no error") try self.assert(container: container, allowedMachineIDs: Set(["xxx", "mmm", "aaa"]), disallowedMachineIDs: Set(["zzz", "kkk", "bbb", "ccc", "fff"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // But a successful update() does remove all disallowed machine IDs, as they're no longer relevant let (_, updateError2) = container.updateSync(test: self) XCTAssertNil(updateError2, "Should not be an error updating the container after establishing") try self.assert(container: container, allowedMachineIDs: Set(["xxx", "mmm", "aaa"]), disallowedMachineIDs: Set([]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") } func testAllowListManipulationWithAddsAndRemoves() throws { let description = tmpStoreDescription(name: "container.db") let container = try Container(name: ContainerName(container: "test", context: OTDefaultContext), persistentStoreDescription: description, cuttlefish: cuttlefish) try self.assert(container: container, allowedMachineIDs: [], disallowedMachineIDs: [], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb", "ccc"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]), disallowedMachineIDs: [], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // Now, an 'add' comes in for some peers XCTAssertNil(container.addAllowedMachineIDsSync(test: self, machineIDs: ["ddd", "eee"]), "should be able to receive an add push") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ccc", "ddd", "eee"]), disallowedMachineIDs: [], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // But, the next time we ask IDMS, they still haven't made it to the full list, and in fact, C has disappeared. XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ddd", "eee"]), disallowedMachineIDs: Set(["ccc"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // And a remove comes in for E. It becomes 'unknown' XCTAssertNil(container.removeAllowedMachineIDsSync(test: self, machineIDs: ["eee"]), "should be able to receive an add push") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ddd"]), disallowedMachineIDs: Set(["ccc"]), unknownMachineIDs: Set(["eee"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertTrue(container.onqueueFullIDMSListWouldBeHelpful(), "Container should think it could use an IDMS list set: there's machine IDs pending removal") // and a list set after the remove confirms the removal XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ddd"]), disallowedMachineIDs: Set(["ccc", "eee"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // Then a new list set includes D! Hurray IDMS. Note that this is not a "list change", because the list doesn't actually change XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb", "ddd"], listDifference: false), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ddd"]), disallowedMachineIDs: Set(["ccc", "eee"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // And another list set no longer includes D, so it should now be disallowed XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb"]), disallowedMachineIDs: Set(["ccc", "ddd", "eee"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // And just to check the 48 hour boundary... XCTAssertNil(container.addAllowedMachineIDsSync(test: self, machineIDs: ["xxx"]), "should be able to receive an add push") XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb"], listDifference: false), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "xxx"]), disallowedMachineIDs: Set(["ccc", "ddd", "eee"]), persistentStore: description, cuttlefish: self.cuttlefish) container.moc.performAndWait { let knownMachineMOs = container.containerMO.machines as? Set ?? Set() knownMachineMOs.forEach { if $0.machineID == "xxx" { $0.modified = Date(timeIntervalSinceNow: -60*60*72) } } try! container.moc.save() } XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // Setting the list again should kick out X, since it was 'added' too long ago XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb"]), disallowedMachineIDs: Set(["ccc", "ddd", "eee", "xxx"]), persistentStore: description, cuttlefish: self.cuttlefish) } func testAllowSetUpgrade() throws { let containerName = ContainerName(container: "test", context: OTDefaultContext) let description = tmpStoreDescription(name: "container.db") let url = Bundle(for: type(of: self)).url(forResource: "TrustedPeersHelper", withExtension: "momd")! let mom = getOrMakeModel(url: url) let persistentContainer = NSPersistentContainer(name: "TrustedPeersHelper", managedObjectModel: mom) persistentContainer.persistentStoreDescriptions = [description] persistentContainer.loadPersistentStores { _, error in XCTAssertNil(error, "Should be no error loading persistent stores") } let moc = persistentContainer.newBackgroundContext() moc.performAndWait { let containerMO = ContainerMO(context: moc) containerMO.name = containerName.asSingleString() containerMO.allowedMachineIDs = Set(["aaa", "bbb", "ccc"]) as NSSet XCTAssertNoThrow(try! moc.save()) } // Now TPH boots up with a preexisting model let container = try Container(name: containerName, persistentStoreDescription: description, cuttlefish: cuttlefish) let (peerID, permanentInfo, permanentInfoSig, _, _, error) = container.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") XCTAssertNil(error) XCTAssertNotNil(peerID) XCTAssertNotNil(permanentInfo) XCTAssertNotNil(permanentInfoSig) try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]), disallowedMachineIDs: [], persistentStore: description, cuttlefish: self.cuttlefish) // Setting a new list should work fine XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb", "ddd"]), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ddd"]), disallowedMachineIDs: ["ccc"], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertEqual(container.containerMO.allowedMachineIDs, Set() as NSSet, "Set of allowed machine IDs should now be empty") } func testAllowStatusUpgrade() throws { let containerName = ContainerName(container: "test", context: OTDefaultContext) let description = tmpStoreDescription(name: "container.db") let url = Bundle(for: type(of: self)).url(forResource: "TrustedPeersHelper", withExtension: "momd")! let mom = getOrMakeModel(url: url) let persistentContainer = NSPersistentContainer(name: "TrustedPeersHelper", managedObjectModel: mom) persistentContainer.persistentStoreDescriptions = [description] persistentContainer.loadPersistentStores { _, error in XCTAssertNil(error, "Should be no error loading persistent stores") } let moc = persistentContainer.newBackgroundContext() moc.performAndWait { let containerMO = ContainerMO(context: moc) containerMO.name = containerName.asSingleString() let machine = MachineMO(context: moc) machine.allowed = true machine.modified = Date() machine.machineID = "aaa" containerMO.addToMachines(machine) let machineB = MachineMO(context: moc) machineB.allowed = false machineB.modified = Date() machineB.machineID = "bbb" containerMO.addToMachines(machineB) try! moc.save() } // Now TPH boots up with a preexisting model let container = try Container(name: containerName, persistentStoreDescription: description, cuttlefish: cuttlefish) try self.assert(container: container, allowedMachineIDs: Set(["aaa"]), disallowedMachineIDs: ["bbb"], persistentStore: description, cuttlefish: self.cuttlefish) // Setting a new list should work fine XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "ddd"]), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "ddd"]), disallowedMachineIDs: ["bbb"], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertEqual(container.containerMO.allowedMachineIDs, Set() as NSSet, "Set of allowed machine IDs should now be empty") } func testMachineIDListSetWithUnknownMachineIDs() throws { let description = tmpStoreDescription(name: "container.db") let (container, _) = try establish(reload: false, store: description) container.moc.performAndWait { let knownMachineMOs = container.containerMO.machines as? Set ?? Set() knownMachineMOs.forEach { container.containerMO.removeFromMachines($0) } try! container.moc.save() } // and set the machine ID list to something that doesn't include 'aaa' XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["bbb", "ccc"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["bbb", "ccc"]), disallowedMachineIDs: [], unknownMachineIDs: Set(["aaa"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // But check that it exists, and set its modification date to a while ago for an upcoming test container.moc.performAndWait { let knownMachineMOs = container.containerMO.machines as? Set ?? Set() let aaaMOs = knownMachineMOs.filter { $0.machineID == "aaa" } XCTAssert(aaaMOs.count == 1, "Should have one machine MO for aaa") let aaaMO = aaaMOs.first! XCTAssertEqual(aaaMO.status, Int64(TPMachineIDStatus.unknown.rawValue), "Status of aaa MO should be 'unknown'") XCTAssertFalse(aaaMO.allowed, "allowed should no longer be a used field") aaaMO.modified = Date(timeIntervalSinceNow: -60) try! container.moc.save() } // With it 'modified' only 60s ago, we shouldn't want a list fetch XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // Setting it again is fine... XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["bbb", "ccc"], listDifference: false), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["bbb", "ccc"]), disallowedMachineIDs: [], unknownMachineIDs: Set(["aaa"]), persistentStore: description, cuttlefish: self.cuttlefish) // And doesn't reset the modified date on the record container.moc.performAndWait { let knownMachineMOs = container.containerMO.machines as? Set ?? Set() let aaaMOs = knownMachineMOs.filter { $0.machineID == "aaa" } XCTAssert(aaaMOs.count == 1, "Should have one machine MO for aaa") let aaaMO = aaaMOs.first! XCTAssertEqual(aaaMO.status, Int64(TPMachineIDStatus.unknown.rawValue), "Status of aaa MO should be 'unknown'") XCTAssertFalse(aaaMO.allowed, "allowed should no longer be a used field") XCTAssertLessThan(aaaMO.modified!, Date(timeIntervalSinceNow: -5), "Modification date of record should still be its previously on-disk value") } // And can be promoted to 'allowed' XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb"]), disallowedMachineIDs: ["ccc"], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") } func testMachineIDListSetDisallowedOldUnknownMachineIDs() throws { let description = tmpStoreDescription(name: "container.db") let (container, _) = try establish(reload: false, store: description) container.moc.performAndWait { let knownMachineMOs = container.containerMO.machines as? Set ?? Set() knownMachineMOs.forEach { container.containerMO.removeFromMachines($0) } try! container.moc.save() } // and set the machine ID list to something that doesn't include 'aaa' XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["bbb", "ccc"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["bbb", "ccc"]), disallowedMachineIDs: [], unknownMachineIDs: Set(["aaa"]), persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // But an entry for "aaa" should exist, as a peer in the model claims it as their MID container.moc.performAndWait { let knownMachineMOs = container.containerMO.machines as? Set ?? Set() let aaaMOs = knownMachineMOs.filter { $0.machineID == "aaa" } XCTAssertEqual(aaaMOs.count, 1, "Should have one machine MO for aaa") let aaaMO = aaaMOs.first! XCTAssertEqual(aaaMO.status, Int64(TPMachineIDStatus.unknown.rawValue), "Status of aaa MO should be 'unknown'") XCTAssertFalse(aaaMO.allowed, "allowed should no longer be a used field") // Pretend that aaa was added 49 hours ago aaaMO.modified = Date(timeIntervalSinceNow: -60*60*49) try! container.moc.save() } XCTAssertTrue(container.onqueueFullIDMSListWouldBeHelpful(), "Container should think it could use an IDMS list set: there's machine IDs pending removal") // And, setting the list again should disallow aaa, since it is so old // Note that this _should_ return a list difference, since A is promoted to disallowed XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["bbb", "ccc"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["bbb", "ccc"]), disallowedMachineIDs: ["aaa"], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // Setting ths list again has no change XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["bbb", "ccc"], listDifference: false), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["bbb", "ccc"]), disallowedMachineIDs: ["aaa"], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") // But A can appear again, no problem. XCTAssertNil(container.setAllowedMachineIDsSync(test: self, allowedMachineIDs: ["aaa", "bbb", "ccc"], listDifference: true), "should be able to set allowed machine IDs") try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]), disallowedMachineIDs: [], persistentStore: description, cuttlefish: self.cuttlefish) XCTAssertFalse(container.onqueueFullIDMSListWouldBeHelpful(), "Container shouldn't think it could use an IDMS list set") } func testMachineIDListHandlingWithPeers() throws { let description = tmpStoreDescription(name: "container.db") let (container, peerID1) = try establish(reload: false, store: description) try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]), disallowedMachineIDs: [], unknownMachineIDs: Set(), persistentStore: description, cuttlefish: self.cuttlefish) let unknownMachineID = "not-on-list" let (_, peerID2) = try self.joinByVoucher(sponsor: container, containerID: "second", machineID: unknownMachineID, machineIDs: Set([unknownMachineID, "aaa", "bbb", "ccc"]), store: description) // And the first container accepts the join... let (_, cUpdateError) = container.updateSync(test: self) XCTAssertNil(cUpdateError, "Should be able to update first container") assertTrusts(context: container, peerIDs: [peerID1, peerID2]) try self.assert(container: container, allowedMachineIDs: Set(["aaa", "bbb", "ccc"]), disallowedMachineIDs: [], unknownMachineIDs: Set([unknownMachineID]), persistentStore: description, cuttlefish: self.cuttlefish) } func testContainerAndModelConsistency() throws{ let preTestContainerName = ContainerName(container: "testToCreatePrepareData", context: "context") let description = tmpStoreDescription(name: "container.db") let containerTest = try Container(name: preTestContainerName, persistentStoreDescription: description, cuttlefish: cuttlefish) let (peerID, permanentInfo, permanentInfoSig, stableInfo, stableInfoSig, error) = containerTest.prepareSync(test: self, epoch: 1, machineID: "aaa", bottleSalt: "123456789", bottleID: UUID().uuidString, modelID: "iPhone1,1") XCTAssertNil(error) XCTAssertNotNil(peerID) XCTAssertNotNil(permanentInfo) XCTAssertNotNil(permanentInfoSig) XCTAssertNotNil(stableInfo) XCTAssertNotNil(stableInfoSig) let containerName = ContainerName(container: "test", context: OTDefaultContext) let url = Bundle(for: type(of: self)).url(forResource: "TrustedPeersHelper", withExtension: "momd")! let mom = getOrMakeModel(url: url) let persistentContainer = NSPersistentContainer(name: "TrustedPeersHelper", managedObjectModel: mom) persistentContainer.persistentStoreDescriptions = [description] persistentContainer.loadPersistentStores { _, error in XCTAssertNil(error, "Should be no error loading persistent stores") } let moc = persistentContainer.newBackgroundContext() moc.performAndWait { let containerMO = ContainerMO(context: moc) containerMO.name = containerName.asSingleString() containerMO.allowedMachineIDs = Set(["aaa"]) as NSSet containerMO.egoPeerID = peerID containerMO.egoPeerPermanentInfo = permanentInfo containerMO.egoPeerPermanentInfoSig = permanentInfoSig containerMO.egoPeerStableInfoSig = stableInfoSig containerMO.egoPeerStableInfo = stableInfo let containerEgoStableInfo = TPPeerStableInfo(data: stableInfo!, sig: stableInfoSig!) do{ let peerKeys: OctagonSelfPeerKeys = try loadEgoKeysSync(peerID: containerMO.egoPeerID!) let info3: TPPeerStableInfo = TPPeerStableInfo(clock: containerEgoStableInfo!.clock + 2, policyVersion:containerEgoStableInfo!.policyVersion, policyHash:containerEgoStableInfo!.policyHash, policySecrets:containerEgoStableInfo!.policySecrets, deviceName:containerEgoStableInfo!.deviceName, serialNumber:containerEgoStableInfo!.serialNumber, osVersion:containerEgoStableInfo!.osVersion, signing:peerKeys.signingKey, recoverySigningPubKey:containerEgoStableInfo!.recoverySigningPublicKey, recoveryEncryptionPubKey:containerEgoStableInfo!.recoveryEncryptionPublicKey, error:nil) //setting the containerMO's ego stable info to an old clock containerMO.egoPeerStableInfo = containerEgoStableInfo!.data containerMO.egoPeerStableInfoSig = containerEgoStableInfo!.sig //now we are adding the ego stable info with a clock of 3 to the list of peers let peer = PeerMO(context: moc) peer.peerID = peerID peer.permanentInfo = permanentInfo peer.permanentInfoSig = permanentInfoSig peer.stableInfo = info3.data peer.stableInfoSig = info3.sig peer.isEgoPeer = true peer.container = containerMO containerMO.addToPeers(peer) //at this point the containerMO's egoStableInfo should have a clock of 1 //the saved ego peer in the peer list has a clock of 3 } catch { XCTFail("load ego keys failed: \(error)") } XCTAssertNoThrow(try! moc.save()) } // Now TPH boots up with a preexisting model let container = try Container(name: containerName, persistentStoreDescription: description, cuttlefish: cuttlefish) let stableInfoAfterBoot = TPPeerStableInfo(data: container.containerMO.egoPeerStableInfo!, sig: container.containerMO.egoPeerStableInfoSig!) XCTAssertNotNil(stableInfoAfterBoot) //after boot the clock should be updated to the one that was saved in the model XCTAssertEqual(stableInfoAfterBoot!.clock, 3, "clock should be updated to 3") } }