/* * Copyright (c) 2018 Apple Inc. All Rights Reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ */ import CloudKitCode import CoreData import Foundation import os import Security import SecurityFoundation let tplogDebug = OSLog(subsystem: "com.apple.security.trustedpeers", category: "debug") let tplogTrace = OSLog(subsystem: "com.apple.security.trustedpeers", category: "trace") let egoIdentitiesAccessGroup = "com.apple.security.egoIdentities" extension ResetReason { static func from(cuttlefishResetReason: CuttlefishResetReason) -> ResetReason { switch cuttlefishResetReason { case .unknown: return ResetReason.unknown case .userInitiatedReset: return ResetReason.userInitiatedReset case .healthCheck: return ResetReason.healthCheck case .noBottleDuringEscrowRecovery: return ResetReason.noBottleDuringEscrowRecovery case .legacyJoinCircle: return ResetReason.legacyJoinCircle case .recoveryKey: return ResetReason.recoveryKey case .testGenerated: return ResetReason.testGenerated @unknown default: fatalError() } } } public enum ContainerError: Error { case unableToCreateKeyPair case noPreparedIdentity case failedToStoreIdentity case needsAuthentication case missingStableInfo case missingDynamicInfo case nonMember case invalidPermanentInfoOrSig case invalidStableInfoOrSig case invalidVoucherOrSig case sponsorNotRegistered(String) case unknownPolicyVersion(UInt64) case preparedIdentityNotOnAllowedList(String) case couldNotLoadAllowedList case noPeersPreapprovePreparedIdentity case policyDocumentDoesNotValidate case tooManyBottlesForPeer case noBottleForPeer case restoreBottleFailed case noBottlesForEscrowRecordID case bottleDoesNotContainContents case bottleDoesNotContainEscrowKeySignature case bottleDoesNotContainerPeerKeySignature case bottleDoesNotContainPeerID case failedToCreateBottledPeer case signatureVerificationFailed case bottleDoesNotContainerEscrowKeySPKI case failedToFetchEscrowContents case failedToCreateRecoveryKey case untrustedRecoveryKeys case noBottlesPresent case recoveryKeysNotEnrolled case bottleCreatingPeerNotFound case unknownCloudKitError case cloudkitResponseMissing case failedToLoadSecret(errorCode: Int) case failedToLoadSecretDueToType case failedToAssembleBottle case invalidPeerID case failedToStoreSecret(errorCode: Int) case unknownSecurityFoundationError } extension ContainerError: LocalizedError { public var errorDescription: String? { switch self { case .unableToCreateKeyPair: return "unable to create key pair" case .noPreparedIdentity: return "no prepared identity" case .failedToStoreIdentity: return "failed to stored identity" case .needsAuthentication: return "needs authentication" case .missingStableInfo: return "missing stable info" case .missingDynamicInfo: return "missing dynamic info" case .nonMember: return "non member" case .invalidPermanentInfoOrSig: return "invalid permanent info or signature" case .invalidStableInfoOrSig: return "invalid stable info or signature" case .invalidVoucherOrSig: return "invalid voucher or signature" case .sponsorNotRegistered(let s): return "sponsor not registered: \(s)" case .unknownPolicyVersion(let v): return "unknown policy version: \(v)" case .preparedIdentityNotOnAllowedList(let id): return "prepared identity (\(id)) not on allowed machineID list" case .couldNotLoadAllowedList: return "could not load allowed machineID list" case .noPeersPreapprovePreparedIdentity: return "no peers preapprove prepared identity" case .policyDocumentDoesNotValidate: return "policy document from server doesn't validate" case .tooManyBottlesForPeer: return "too many bottles exist for peer" case .noBottleForPeer: return "no bottle exists for peer" case .restoreBottleFailed: return "failed to restore bottle" case .noBottlesForEscrowRecordID: return "0 bottles exist for escrow record id" case .bottleDoesNotContainContents: return "bottle does not contain encrypted contents" case .bottleDoesNotContainEscrowKeySignature: return "bottle does not contain escrow signature" case .bottleDoesNotContainerPeerKeySignature: return "bottle does not contain peer signature" case .bottleDoesNotContainPeerID: return "bottle does not contain peer id" case .failedToCreateBottledPeer: return "failed to create a bottled peer" case .signatureVerificationFailed: return "failed to verify signature" case .bottleDoesNotContainerEscrowKeySPKI: return "bottle does not contain escrowed key spki" case .failedToFetchEscrowContents: return "failed to fetch escrow contents" case .failedToCreateRecoveryKey: return "failed to create recovery keys" case .untrustedRecoveryKeys: return "untrusted recovery keys" case .noBottlesPresent: return "no bottle present" case .recoveryKeysNotEnrolled: return "recovery key is not enrolled with octagon" case .bottleCreatingPeerNotFound: return "The peer that created the bottle was not found" case .unknownCloudKitError: return "unknown error from cloudkit" case .cloudkitResponseMissing: return "Response missing from CloudKit" case .failedToLoadSecret(errorCode: let errorCode): return "failed to load secret: \(errorCode)" case .failedToLoadSecretDueToType: return "Failed to load secret due to type mismatch (value was not dictionary)" case .failedToAssembleBottle: return "failed to assemble bottle for peer" case .invalidPeerID: return "peerID is invalid" case .failedToStoreSecret(errorCode: let errorCode): return "failed to store the secret in the keychain \(errorCode)" case .unknownSecurityFoundationError: return "SecurityFoundation returned an unknown type" } } } extension ContainerError: CustomNSError { public static var errorDomain: String { return "com.apple.security.trustedpeers.container" } public var errorCode: Int { switch self { case .unableToCreateKeyPair: return 0 case .noPreparedIdentity: return 1 case .failedToStoreIdentity: return 2 case .needsAuthentication: return 3 case .missingStableInfo: return 4 case .missingDynamicInfo: return 5 case .nonMember: return 6 case .invalidPermanentInfoOrSig: return 7 case .invalidVoucherOrSig: return 8 case .invalidStableInfoOrSig: return 9 case .sponsorNotRegistered: return 11 case .unknownPolicyVersion: return 12 case .preparedIdentityNotOnAllowedList: return 13 case .noPeersPreapprovePreparedIdentity: return 14 case .policyDocumentDoesNotValidate: return 15 // 16 was invalidPeerID and failedToAssembleBottle case .tooManyBottlesForPeer: return 17 case .noBottleForPeer: return 18 case .restoreBottleFailed: return 19 case .noBottlesForEscrowRecordID: return 20 case .bottleDoesNotContainContents: return 21 case .bottleDoesNotContainEscrowKeySignature: return 22 case .bottleDoesNotContainerPeerKeySignature: return 23 case .bottleDoesNotContainPeerID: return 24 case .failedToCreateBottledPeer: return 25 case .signatureVerificationFailed: return 26 // 27 was failedToLoadSecret* case .bottleDoesNotContainerEscrowKeySPKI: return 28 case .failedToFetchEscrowContents: return 29 case .couldNotLoadAllowedList: return 30 case .failedToCreateRecoveryKey: return 31 case .untrustedRecoveryKeys: return 32 case .noBottlesPresent: return 33 case .recoveryKeysNotEnrolled: return 34 case .bottleCreatingPeerNotFound: return 35 case .unknownCloudKitError: return 36 case .cloudkitResponseMissing: return 37 case .failedToLoadSecret: return 38 case .failedToLoadSecretDueToType: return 39 case .failedToStoreSecret: return 40 case .failedToAssembleBottle: return 41 case .invalidPeerID: return 42 case .unknownSecurityFoundationError: return 43 } } public var underlyingError: NSError? { switch self { case .failedToLoadSecret(errorCode: let errorCode): return NSError(domain: "securityd", code: errorCode) case .failedToStoreSecret(errorCode: let errorCode): return NSError(domain: "securityd", code: errorCode) default: return nil } } public var errorUserInfo: [String: Any] { var ret = [String: Any]() if let desc = self.errorDescription { ret[NSLocalizedDescriptionKey] = desc } if let underlyingError = self.underlyingError { ret[NSUnderlyingErrorKey] = underlyingError } return ret } } internal func traceError(_ error: Error?) -> String { if let error = error { return "error: \(String(describing: error))" } else { return "success" } } func saveSecret(_ secret: Data, label: String) throws { let query: [CFString: Any] = [ kSecClass: kSecClassInternetPassword, kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked, kSecUseDataProtectionKeychain: true, kSecAttrAccessGroup: "com.apple.security.octagon", kSecAttrSynchronizable: false, kSecAttrDescription: label, kSecAttrPath: label, kSecValueData: secret, ] var results: CFTypeRef? var status = SecItemAdd(query as CFDictionary, &results) if status == errSecSuccess { return } if status == errSecDuplicateItem { // Add every primary key attribute to this find dictionary var findQuery: [CFString: Any] = [:] findQuery[kSecClass] = query[kSecClass] findQuery[kSecAttrSynchronizable] = query[kSecAttrSynchronizable] findQuery[kSecAttrAccessGroup] = query[kSecAttrAccessGroup] findQuery[kSecAttrServer] = query[kSecAttrDescription] findQuery[kSecAttrPath] = query[kSecAttrPath] findQuery[kSecUseDataProtectionKeychain] = query[kSecUseDataProtectionKeychain] var updateQuery: [CFString: Any] = query updateQuery[kSecClass] = nil status = SecItemUpdate(findQuery as CFDictionary, updateQuery as CFDictionary) if status != errSecSuccess { throw ContainerError.failedToStoreSecret(errorCode: Int(status)) } } else { throw ContainerError.failedToStoreSecret(errorCode: Int(status)) } } func loadSecret(label: String) throws -> (Data?) { var secret: Data? let query: [CFString: Any] = [ kSecClass: kSecClassInternetPassword, kSecAttrAccessGroup: "com.apple.security.octagon", kSecAttrDescription: label, kSecReturnAttributes: true, kSecReturnData: true, kSecAttrSynchronizable: false, kSecMatchLimit: kSecMatchLimitOne, ] var result: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &result) if status != errSecSuccess || result == nil { throw ContainerError.failedToLoadSecret(errorCode: Int(status)) } if result != nil { if let dictionary = result as? [CFString: Any] { secret = dictionary[kSecValueData] as? Data } else { throw ContainerError.failedToLoadSecretDueToType } } return secret } func saveEgoKeyPair(_ keyPair: _SFECKeyPair, identifier: String, resultHandler: @escaping (Bool, Error?) -> Void) { let keychainManager = _SFKeychainManager.default() let signingIdentity = _SFIdentity(keyPair: keyPair) let accessibility = SFAccessibilityMakeWithMode(SFAccessibilityMode.accessibleWhenUnlocked) // class A let accessPolicy = _SFAccessPolicy(accessibility: accessibility, sharingPolicy: SFSharingPolicy.thisDeviceOnly) accessPolicy.accessGroup = egoIdentitiesAccessGroup keychainManager.setIdentity(signingIdentity, forIdentifier: identifier, accessPolicy: accessPolicy, resultHandler: resultHandler) } func loadEgoKeyPair(identifier: String, resultHandler: @escaping (_SFECKeyPair?, Error?) -> Void) { let keychainManager = _SFKeychainManager.default() // FIXME constrain to egoIdentitiesAccessGroup, keychainManager.identity(forIdentifier: identifier) { result in switch result.resultType { case .valueAvailable: resultHandler(result.value?.keyPair as? _SFECKeyPair, nil) case .needsAuthentication: resultHandler(nil, ContainerError.needsAuthentication) case .error: resultHandler(nil, result.error) @unknown default: resultHandler(nil, ContainerError.unknownSecurityFoundationError) } } } func loadEgoKeys(peerID: String, resultHandler: @escaping (OctagonSelfPeerKeys?, Error?) -> Void) { loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: peerID)) { signingKey, error in guard let signingKey = signingKey else { os_log("Unable to load signing key: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing") resultHandler(nil, error) return } loadEgoKeyPair(identifier: encryptionKeyIdentifier(peerID: peerID)) { encryptionKey, error in guard let encryptionKey = encryptionKey else { os_log("Unable to load encryption key: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing") resultHandler(nil, error) return } do { resultHandler(try OctagonSelfPeerKeys(peerID: peerID, signingKey: signingKey, encryptionKey: encryptionKey), nil) } catch { resultHandler(nil, error) } } } } func removeEgoKeysSync(peerID: String) throws -> Bool { var resultSema = DispatchSemaphore(value: 0) let keychainManager = _SFKeychainManager.default() var retresultForSigningDeletion: Bool = false var reterrorForSigningDeletion: Error? //remove signing keys keychainManager.removeItem(withIdentifier: signingKeyIdentifier(peerID: peerID)) { result, error in retresultForSigningDeletion = result reterrorForSigningDeletion = error resultSema.signal() } resultSema.wait() if let error = reterrorForSigningDeletion { throw error } if retresultForSigningDeletion == false { return retresultForSigningDeletion } // now let's do the same thing with the encryption keys resultSema = DispatchSemaphore(value: 0) var retresultForEncryptionDeletion: Bool = false var reterrorForEncryptionDeletion: Error? keychainManager.removeItem(withIdentifier: encryptionKeyIdentifier(peerID: peerID)) { result, error in retresultForEncryptionDeletion = result reterrorForEncryptionDeletion = error resultSema.signal() } resultSema.wait() if let error = reterrorForEncryptionDeletion { throw error } return retresultForEncryptionDeletion && retresultForSigningDeletion } func loadEgoKeysSync(peerID: String) throws -> OctagonSelfPeerKeys { // Gotta promote to synchronous; 'antipattern' ahoy let resultSema = DispatchSemaphore(value: 0) var result: OctagonSelfPeerKeys? var reserror: Error? loadEgoKeys(peerID: peerID) { keys, error in result = keys reserror = error resultSema.signal() } resultSema.wait() if let error = reserror { throw error } if let result = result { return result } abort() } func signingKeyIdentifier(peerID: String) -> String { return "signing-key " + peerID } func encryptionKeyIdentifier(peerID: String) -> String { return "encryption-key " + peerID } func makeTLKShares(ckksTLKs: [CKKSKeychainBackedKey]?, asPeer: CKKSSelfPeer, toPeer: CKKSPeer, epoch: Int) throws -> [TLKShare] { return try (ckksTLKs ?? []).map { tlk in os_log("Making TLKShare for %@ for key %@", log: tplogDebug, type: .default, toPeer.description, tlk) // Not being able to convert a TLK to a TLKShare is a failure, but not having a TLK is only half bad do { return TLKShare.convert(ckksTLKShare: try CKKSTLKShare(tlk, as: asPeer, to: toPeer, epoch: epoch, poisoned: 0)) } catch { let nserror = error as NSError if nserror.domain == "securityd" && nserror.code == errSecItemNotFound { os_log("No TLK contents for %@, no TLK share possible", log: tplogDebug, type: .default, tlk) return nil } else { throw error } } }.compactMap { $0 } } func extract(tlkShares: [CKKSTLKShare], peer: CKKSSelfPeer) { os_log("Attempting to recover %d TLK shares for peer %@", log: tplogDebug, type: .default, tlkShares.count, peer.peerID) for share in tlkShares { guard share.receiverPeerID == peer.peerID else { os_log("Skipping %@ (wrong peerID)", log: tplogDebug, type: .default, share) continue } do { // TODO: how should we handle peer sets here? let key = try share.recoverTLK(peer, trustedPeers: [peer as! AnyHashable], ckrecord: nil) try key.saveMaterialToKeychain() os_log("Recovered %@ (from %@)", log: tplogDebug, type: .default, key, share) } catch { os_log("Failed to recover share %@: %@", log: tplogDebug, type: .default, share, error as CVarArg) } } } struct ContainerState { var egoPeerID: String? var peers: [String: TPPeer] = [:] var vouchers: [TPVoucher] = [] var bottles = Set() var recoverySigningKey: Data? var recoveryEncryptionKey: Data? } internal struct StableChanges { let deviceName: String? let serialNumber: String? let osVersion: String? let policyVersion: UInt64? let policySecrets: [String: Data]? let recoverySigningPubKey: Data? var recoveryEncryptionPubKey: Data? } // CoreData doesn't handle creating an identical model from an identical URL. Help it out. private var nsObjectModels: [URL: NSManagedObjectModel] = [:] private let nsObjectModelsQueue = DispatchQueue(label: "com.apple.security.TrustedPeersHelper.nsObjectModels") func getOrMakeModel(url: URL) -> NSManagedObjectModel { return nsObjectModelsQueue.sync { if let model = nsObjectModels[url] { return model } let newModel = NSManagedObjectModel(contentsOf: url)! nsObjectModels[url] = newModel return newModel } } struct ContainerName: Hashable, CustomStringConvertible { let container: String let context: String func asSingleString() -> String { // Just to be nice, hide the fact that multiple contexts exist on most machines if self.context == OTDefaultContext { return self.container } else { return self.container + "-" + self.context } } var description: String { return "Container(\(self.container),\(self.context))" } } /// This maps to a Cuttlefish service backed by a CloudKit container, /// and a corresponding local Core Data persistent container. /// /// Methods may be invoked concurrently. class Container: NSObject { let name: ContainerName private let cuttlefish: CuttlefishAPIAsync // Only one request (from Client) is permitted to be in progress at a time. // That includes while waiting for network, i.e. one request must complete // before the next can begin. Otherwise two requests could in parallel be // fetching updates from Cuttlefish, with ensuing changeToken overwrites etc. // This applies for mutating requests -- requests that can only read the current // state (on the moc queue) do not need to. internal let semaphore = DispatchSemaphore(value: 1) // All Core Data access happens through moc: NSManagedObjectContext. The // moc insists on having its own queue, and all operations must happen on // that queue. internal let moc: NSManagedObjectContext // Rather than Container having its own dispatch queue, we use moc's queue // to synchronise access to our own state as well. So the following instance // variables must only be accessed within blocks executed by calling // moc.perform() or moc.performAndWait(). internal var containerMO: ContainerMO internal var model: TPModel /** Construct a Container. - Parameter name: The name the CloudKit container to which requests will be routed. The "real" container that drives CKKS etc should be named `"com.apple.security.keychain"`. Use other names for test containers such as for rawfish (rawhide-style testing for cuttlefish) - Parameter persistentStoreURL: The location the local Core Data database for this container will be stored. - Parameter cuttlefish: Interface to cuttlefish. */ init(name: ContainerName, persistentStoreDescription: NSPersistentStoreDescription, cuttlefish: CuttlefishAPIAsync) throws { var initError: Error? var containerMO: ContainerMO? var model: TPModel? // Set up Core Data stack 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 = [persistentStoreDescription] persistentContainer.loadPersistentStores { _, error in initError = error } if let initError = initError { throw initError } let moc = persistentContainer.newBackgroundContext() moc.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump moc.performAndWait { // Fetch an existing ContainerMO record if it exists, or create and save one do { let containerFetch = NSFetchRequest(entityName: "Container") containerFetch.predicate = NSPredicate(format: "name == %@", name.asSingleString()) let fetchedContainers = try moc.fetch(containerFetch) if let container = fetchedContainers.first as? ContainerMO { containerMO = container } else { containerMO = ContainerMO(context: moc) containerMO!.name = name.asSingleString() } // Perform upgrades as needed Container.onqueueUpgradeMachineIDSetToModel(container: containerMO!, moc: moc) Container.onqueueUpgradeMachineIDSetToUseStatus(container: containerMO!, moc: moc) model = Container.loadModel(from: containerMO!) Container.ensureEgoConsistency(from: containerMO!, model: model!) try moc.save() } catch { initError = error return } } if let initError = initError { throw initError } self.name = name self.moc = moc self.containerMO = containerMO! self.cuttlefish = cuttlefish self.model = model! super.init() } // Must be on containerMO's moc queue to call this internal static func loadModel(from containerMO: ContainerMO) -> TPModel { // Populate model from persistent store let model = TPModel(decrypter: Decrypter()) let keyFactory = TPECPublicKeyFactory() let peers = containerMO.peers as? Set peers?.forEach { peer in guard let permanentInfo = TPPeerPermanentInfo(peerID: peer.peerID!, data: peer.permanentInfo! as Data, sig: peer.permanentInfoSig! as Data, keyFactory: keyFactory) else { return } model.registerPeer(with: permanentInfo) if let data = peer.stableInfo, let sig = peer.stableInfoSig { if let stableInfo = TPPeerStableInfo(data: data as Data, sig: sig as Data) { do { try model.update(stableInfo, forPeerWithID: permanentInfo.peerID) } catch { os_log("loadModel unable to update stable info for peer(%@): %@", log: tplogDebug, type: .default, peer, error as CVarArg) } } else { os_log("loadModel: peer %@ has unparseable stable info", log: tplogDebug, type: .default, permanentInfo.peerID) } } else { os_log("loadModel: peer %@ has no stable info", log: tplogDebug, type: .default, permanentInfo.peerID) } if let data = peer.dynamicInfo, let sig = peer.dynamicInfoSig { if let dynamicInfo = TPPeerDynamicInfo(data: data as Data, sig: sig as Data) { do { try model.update(dynamicInfo, forPeerWithID: permanentInfo.peerID) } catch { os_log("loadModel unable to update dynamic info for peer(%@): %@", log: tplogDebug, type: .default, peer, error as CVarArg) } } else { os_log("loadModel: peer %@ has unparseable dynamic info", log: tplogDebug, type: .default, permanentInfo.peerID) } } else { os_log("loadModel: peer %@ has no dynamic info", log: tplogDebug, type: .default, permanentInfo.peerID) } peer.vouchers?.forEach { let v = $0 as! VoucherMO if let data = v.voucherInfo, let sig = v.voucherInfoSig { if let voucher = TPVoucher(infoWith: data, sig: sig) { model.register(voucher) } } } } // Register persisted policies (cached from cuttlefish) let policies = containerMO.policies as? Set policies?.forEach { policyMO in if let policyHash = policyMO.policyHash, let policyData = policyMO.policyData { if let policyDoc = TPPolicyDocument.policyDoc(withHash: policyHash, data: policyData) { model.register(policyDoc) } } } // Register built-in policies builtInPolicyDocuments().forEach { policyDoc in model.register(policyDoc) } let knownMachines = containerMO.machines as? Set ?? Set() let allowedMachineIDs = Set(knownMachines.filter { $0.status == TPMachineIDStatus.allowed.rawValue }.compactMap { $0.machineID }) let disallowedMachineIDs = Set(knownMachines.filter { $0.status == TPMachineIDStatus.disallowed.rawValue }.compactMap { $0.machineID }) os_log("loadModel: allowedMachineIDs: %@", log: tplogDebug, type: .default, allowedMachineIDs) os_log("loadModel: disallowedMachineIDs: %@", log: tplogDebug, type: .default, disallowedMachineIDs) if allowedMachineIDs.count == 0 { os_log("loadModel: no allowedMachineIDs?", log: tplogDebug, type: .default) } return model } // Must be on containerMO's moc queue to call this internal static func ensureEgoConsistency(from containerMO: ContainerMO, model: TPModel) { guard let egoPeerID = containerMO.egoPeerID, let egoStableData = containerMO.egoPeerStableInfo, let egoStableSig = containerMO.egoPeerStableInfoSig else { os_log("ensureEgoConsistency failed to find ego peer information", log: tplogDebug, type: .error) return } guard let containerEgoStableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else { os_log("ensureEgoConsistency failed to create TPPeerStableInfo from container", log: tplogDebug, type: .error) return } guard let modelStableInfo = model.getStableInfoForPeer(withID: egoPeerID) else { os_log("ensureEgoConsistency failed to create TPPeerStableInfo from model", log: tplogDebug, type: .error) return } if modelStableInfo.clock > containerEgoStableInfo.clock { containerMO.egoPeerStableInfo = modelStableInfo.data containerMO.egoPeerStableInfoSig = modelStableInfo.sig } } static func dictionaryRepresentation(bottle: BottleMO) -> [String: Any] { var dict: [String: String] = [:] dict["bottleID"] = bottle.bottleID dict["peerID"] = bottle.peerID dict["signingSPKI"] = bottle.escrowedSigningSPKI?.base64EncodedString() dict["signatureUsingPeerKey"] = bottle.signatureUsingPeerKey?.base64EncodedString() dict["signatureUsingSPKI"] = bottle.signatureUsingEscrowKey?.base64EncodedString() // ignore the bottle contents; they're mostly unreadable return dict } static func peerdictionaryRepresentation(peer: TPPeer) -> [String: Any] { var peerDict: [String: Any] = [ "permanentInfo": peer.permanentInfo.dictionaryRepresentation(), "peerID": peer.peerID, ] if let stableInfo = peer.stableInfo { peerDict["stableInfo"] = stableInfo.dictionaryRepresentation() } if let dynamicInfo = peer.dynamicInfo { peerDict["dynamicInfo"] = dynamicInfo.dictionaryRepresentation() } return peerDict } func onQueueDetermineLocalTrustStatus(reply: @escaping (TrustedPeersHelperEgoPeerStatus, Error?) -> Void) { let viablePeerCountsByModelID = self.model.viablePeerCountsByModelID() if let egoPeerID = self.containerMO.egoPeerID { var status = self.model.statusOfPeer(withID: egoPeerID) var isExcluded: Bool = (status == .excluded) loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, loadError in var returnError = loadError guard returnError == nil else { var isLocked = false if let error = (loadError as NSError?) { os_log("trust status: Unable to load ego keys: %@", log: tplogDebug, type: .default, error as CVarArg) if error.code == errSecItemNotFound && error.domain == NSOSStatusErrorDomain { os_log("trust status: Lost the ego key pair, returning 'excluded' in hopes of fixing up the identity", log: tplogDebug, type: .debug) isExcluded = true status = .excluded } else if error.code == errSecInteractionNotAllowed && error.domain == NSOSStatusErrorDomain { // we have an item, but can't read it, suppressing error isLocked = true returnError = nil } } let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: egoPeerID, status: status, viablePeerCountsByModelID: viablePeerCountsByModelID, isExcluded: isExcluded, isLocked: isLocked) reply(egoStatus, returnError) return } //ensure egoPeerKeys are populated guard egoPeerKeys != nil else { os_log("trust status: No error but Ego Peer Keys are nil", log: tplogDebug, type: .default) let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: egoPeerID, status: .excluded, viablePeerCountsByModelID: viablePeerCountsByModelID, isExcluded: true, isLocked: false) reply(egoStatus, loadError) return } let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: egoPeerID, status: status, viablePeerCountsByModelID: viablePeerCountsByModelID, isExcluded: isExcluded, isLocked: false) reply(egoStatus, nil) return } } else { // With no ego peer ID, either return 'excluded' if there are extant peers, or 'unknown' to signal no peers at all if self.model.allPeerIDs().isEmpty { os_log("No existing peers in account", log: tplogDebug, type: .debug) let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: nil, status: .unknown, viablePeerCountsByModelID: viablePeerCountsByModelID, isExcluded: false, isLocked: false) reply(egoStatus, nil) return } else { os_log("Existing peers in account, but we don't have a peer ID. We are excluded.", log: tplogDebug, type: .debug) let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: nil, status: .excluded, viablePeerCountsByModelID: viablePeerCountsByModelID, isExcluded: true, isLocked: false) reply(egoStatus, nil) return } } } func trustStatus(reply: @escaping (TrustedPeersHelperEgoPeerStatus, Error?) -> Void) { self.semaphore.wait() let reply: (TrustedPeersHelperEgoPeerStatus, Error?) -> Void = { // Suppress logging of successful replies here; it's not that useful let logType: OSLogType = $1 == nil ? .debug : .info os_log("trustStatus complete: %@ %@", log: tplogTrace, type: logType, TPPeerStatusToString($0.egoStatus), traceError($1)) self.semaphore.signal() reply($0, $1) } self.moc.performAndWait { // Knowledge of your peer status only exists if you know about other peers. If you haven't fetched, fetch. if self.containerMO.changeToken == nil { self.fetchAndPersistChanges { fetchError in guard fetchError == nil else { if let error = fetchError { os_log("Unable to fetch changes, trust status is unknown: %@", log: tplogDebug, type: .default, error as CVarArg) } let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: nil, status: .unknown, viablePeerCountsByModelID: [:], isExcluded: false, isLocked: false) reply(egoStatus, fetchError) return } self.moc.performAndWait { self.onQueueDetermineLocalTrustStatus(reply: reply) } } } else { self.onQueueDetermineLocalTrustStatus(reply: reply) } } } func fetchTrustState(reply: @escaping (TrustedPeersHelperPeerState?, [TrustedPeersHelperPeer]?, Error?) -> Void) { let reply: (TrustedPeersHelperPeerState?, [TrustedPeersHelperPeer]?, Error?) -> Void = { os_log("fetch trust state complete: %@ %@", log: tplogTrace, type: .info, String(reflecting: $0), traceError($2)) reply($0, $1, $2) } self.moc.performAndWait { if let egoPeerID = self.containerMO.egoPeerID, let egoPermData = self.containerMO.egoPeerPermanentInfo, let egoPermSig = self.containerMO.egoPeerPermanentInfoSig { let keyFactory = TPECPublicKeyFactory() guard let permanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else { os_log("fetchTrustState failed to create TPPeerPermanentInfo", log: tplogDebug, type: .error) reply(nil, nil, ContainerError.invalidPermanentInfoOrSig) return } let isPreapproved = self.model.hasPotentiallyTrustedPeerPreapprovingKey(permanentInfo.signingPubKey.spki()) os_log("fetchTrustState: ego peer is %@", log: tplogDebug, type: .default, isPreapproved ? "preapproved" : "not yet preapproved") let egoStableInfo = self.model.getStableInfoForPeer(withID: egoPeerID) let egoPeerStatus = TrustedPeersHelperPeerState(peerID: egoPeerID, isPreapproved: isPreapproved, status: self.model.statusOfPeer(withID: egoPeerID), memberChanges: false, unknownMachineIDs: self.onqueueFullIDMSListWouldBeHelpful(), osVersion: egoStableInfo?.osVersion) var tphPeers: [TrustedPeersHelperPeer] = [] if let egoPeer = self.model.peer(withID: egoPeerID) { egoPeer.trustedPeerIDs.forEach { trustedPeerID in if let peer = self.model.peer(withID: trustedPeerID) { let peerViews = try? self.model.getViewsForPeer(peer.permanentInfo, stableInfo: peer.stableInfo, inViews: Set()) tphPeers.append(TrustedPeersHelperPeer(peerID: trustedPeerID, signingSPKI: peer.permanentInfo.signingPubKey.spki(), encryptionSPKI: peer.permanentInfo.encryptionPubKey.spki(), viewList: peerViews ?? Set())) } else { os_log("No peer for trusted ID %@", log: tplogDebug, type: .default, trustedPeerID) } } } else { os_log("No ego peer in model; no trusted peers", log: tplogDebug, type: .default) } os_log("Returning trust state: %@ %@", log: tplogDebug, type: .default, egoPeerStatus, tphPeers) reply(egoPeerStatus, tphPeers, nil) } else { // With no ego peer ID, there are no trusted peers os_log("No peer ID => no trusted peers", log: tplogDebug, type: .debug) reply(TrustedPeersHelperPeerState(peerID: nil, isPreapproved: false, status: .unknown, memberChanges: false, unknownMachineIDs: false, osVersion: nil), [], nil) } } } func dump(reply: @escaping ([AnyHashable: Any]?, Error?) -> Void) { let reply: ([AnyHashable: Any]?, Error?) -> Void = { os_log("dump complete: %@", log: tplogTrace, type: .info, traceError($1)) reply($0, $1) } self.moc.performAndWait { var d: [AnyHashable: Any] = [:] if let egoPeerID = self.containerMO.egoPeerID { if let peer = self.model.peer(withID: egoPeerID) { d["self"] = Container.peerdictionaryRepresentation(peer: peer) } else { d["self"] = ["peerID": egoPeerID] } } else { d["self"] = [:] } d["peers"] = self.model.allPeers().filter { $0.peerID != self.containerMO.egoPeerID }.map { peer in Container.peerdictionaryRepresentation(peer: peer) } d["vouchers"] = self.model.allVouchers().map { $0.dictionaryRepresentation() } if let bottles = self.containerMO.bottles as? Set { d["bottles"] = bottles.map { Container.dictionaryRepresentation(bottle: $0) } } else { d["bottles"] = [] } let midList = self.onqueueCurrentMIDList() d["machineIDsAllowed"] = midList.machineIDs(in: .allowed).sorted() d["machineIDsDisallowed"] = midList.machineIDs(in: .disallowed).sorted() d["modelRecoverySigningPublicKey"] = self.model.recoverySigningPublicKey() d["modelRecoveryEncryptionPublicKey"] = self.model.recoveryEncryptionPublicKey() reply(d, nil) } } func dumpEgoPeer(reply: @escaping (String?, TPPeerPermanentInfo?, TPPeerStableInfo?, TPPeerDynamicInfo?, Error?) -> Void) { let reply: (String?, TPPeerPermanentInfo?, TPPeerStableInfo?, TPPeerDynamicInfo?, Error?) -> Void = { os_log("dumpEgoPeer complete: %@", log: tplogTrace, type: .info, traceError($4)) reply($0, $1, $2, $3, $4) } self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID else { reply(nil, nil, nil, nil, ContainerError.noPreparedIdentity) return } guard let peer = self.model.peer(withID: egoPeerID) else { reply(egoPeerID, nil, nil, nil, nil) return } reply(egoPeerID, peer.permanentInfo, peer.stableInfo, peer.dynamicInfo, nil) } } func validatePeers(request: ValidatePeersRequest, reply: @escaping ([AnyHashable: Any]?, Error?) -> Void) { self.semaphore.wait() let reply: ([AnyHashable: Any]?, Error?) -> Void = { os_log("validatePeers complete %@", log: tplogTrace, type: .info, traceError($1)) self.semaphore.signal() reply($0, $1) } self.cuttlefish.validatePeers(request) { response, error in os_log("ValidatePeers(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("validatePeers failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, error ?? ContainerError.cloudkitResponseMissing) return } var info: [AnyHashable: Any] = [:] info["health"] = response.validatorsHealth as AnyObject info["results"] = try? JSONSerialization.jsonObject(with: response.jsonUTF8Data()) reply(info, nil) } } func getViews(inViews: [String], reply: @escaping ([String]?, Error?) -> Void) { let reply: ([String]?, Error?) -> Void = { os_log("getViews complete %@", log: tplogTrace, type: .info, traceError($1)) reply($0, $1) } self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID, let egoPermData = self.containerMO.egoPeerPermanentInfo, let egoPermSig = self.containerMO.egoPeerPermanentInfoSig, let egoStableData = self.containerMO.egoPeerStableInfo, let egoStableSig = self.containerMO.egoPeerStableInfoSig else { os_log("getViews failed to find ego peer information", log: tplogDebug, type: .error) reply(nil, ContainerError.noPreparedIdentity) return } guard let stableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else { os_log("getViews failed to create TPPeerStableInfo", log: tplogDebug, type: .error) reply(nil, ContainerError.invalidStableInfoOrSig) return } let keyFactory = TPECPublicKeyFactory() guard let permanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else { os_log("getViews failed to create TPPeerPermanentInfo", log: tplogDebug, type: .error) reply(nil, ContainerError.invalidPermanentInfoOrSig) return } do { let views = try self.model.getViewsForPeer(permanentInfo, stableInfo: stableInfo, inViews: Set(inViews)) reply(Array(views), nil) } catch { reply(nil, error) return } } } func reset(resetReason: CuttlefishResetReason, reply: @escaping (Error?) -> Void) { self.semaphore.wait() let reply: (Error?) -> Void = { os_log("reset complete %@", log: tplogTrace, type: .info, traceError($0)) self.semaphore.signal() reply($0) } self.moc.performAndWait { let resetReason = ResetReason.from(cuttlefishResetReason: resetReason) let request = ResetRequest.with { $0.resetReason = resetReason } self.cuttlefish.reset(request) { response, error in os_log("Reset(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("reset failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(error ?? ContainerError.cloudkitResponseMissing) return } // Erase container's persisted state self.moc.performAndWait { self.moc.delete(self.containerMO) self.containerMO = ContainerMO(context: self.moc) self.containerMO.name = self.name.asSingleString() self.model = Container.loadModel(from: self.containerMO) do { try self.onQueuePersist(changes: response.changes) os_log("reset succeded", log: tplogDebug, type: .default) reply(nil) } catch { os_log("reset persist failed: %@", log: tplogDebug, type: .default, (error as CVarArg)) reply(error) } } } } } func localReset(reply: @escaping (Error?) -> Void) { self.semaphore.wait() let reply: (Error?) -> Void = { os_log("localReset complete %@", log: tplogTrace, type: .info, traceError($0)) self.semaphore.signal() reply($0) } self.moc.performAndWait { do { // Erase container's persisted state self.moc.delete(self.containerMO) self.containerMO = ContainerMO(context: self.moc) self.containerMO.name = self.name.asSingleString() self.model = Container.loadModel(from: self.containerMO) try self.moc.save() } catch { reply(error) return } reply(nil) } } func loadOrCreateKeyPair(privateKeyPersistentRef: Data?) throws -> _SFECKeyPair { if let privateKeyPersistentRef = privateKeyPersistentRef { return try TPHObjectiveC.fetchKeyPair(withPrivateKeyPersistentRef: privateKeyPersistentRef) } else { let keySpecifier = _SFECKeySpecifier(curve: SFEllipticCurve.nistp384) guard let keyPair = _SFECKeyPair(randomKeyPairWith: keySpecifier) else { throw ContainerError.unableToCreateKeyPair } return keyPair } } // policyVersion should only be non-nil for testing, to override prevailingPolicyVersion func prepare(epoch: UInt64, machineID: String, bottleSalt: String, bottleID: String, modelID: String, deviceName: String?, serialNumber: String, osVersion: String, policyVersion: UInt64?, policySecrets: [String: Data]?, signingPrivateKeyPersistentRef: Data?, encryptionPrivateKeyPersistentRef: Data?, reply: @escaping (String?, Data?, Data?, Data?, Data?, Error?) -> Void) { self.semaphore.wait() let reply: (String?, Data?, Data?, Data?, Data?, Error?) -> Void = { os_log("prepare complete peerID: %@ %@", log: tplogTrace, type: .info, ($0 ?? "NULL") as CVarArg, traceError($5)) self.semaphore.signal() reply($0, $1, $2, $3, $4, $5) } // Create a new peer identity with random keys, and store the keys in keychain let permanentInfo: TPPeerPermanentInfo let signingKeyPair: _SFECKeyPair let encryptionKeyPair: _SFECKeyPair do { signingKeyPair = try self.loadOrCreateKeyPair(privateKeyPersistentRef: signingPrivateKeyPersistentRef) encryptionKeyPair = try self.loadOrCreateKeyPair(privateKeyPersistentRef: encryptionPrivateKeyPersistentRef) permanentInfo = try TPPeerPermanentInfo(machineID: machineID, modelID: modelID, epoch: 1, signing: signingKeyPair, encryptionKeyPair: encryptionKeyPair, peerIDHashAlgo: TPHashAlgo.SHA256) } catch { reply(nil, nil, nil, nil, nil, error) return } let peerID = permanentInfo.peerID let bottle: BottledPeer do { bottle = try BottledPeer(peerID: peerID, bottleID: bottleID, peerSigningKey: signingKeyPair, peerEncryptionKey: encryptionKeyPair, bottleSalt: bottleSalt) _ = try saveSecret(bottle.secret, label: peerID) } catch { os_log("bottle creation failed: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, nil, nil, nil, nil, error) return } saveEgoKeyPair(signingKeyPair, identifier: signingKeyIdentifier(peerID: peerID)) { success, error in guard success else { os_log("Unable to save signing key: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing") reply(nil, nil, nil, nil, nil, error ?? ContainerError.failedToStoreIdentity) return } saveEgoKeyPair(encryptionKeyPair, identifier: encryptionKeyIdentifier(peerID: peerID)) { success, error in guard success else { os_log("Unable to save encryption key: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing") reply(nil, nil, nil, nil, nil, error ?? ContainerError.failedToStoreIdentity) return } // Save the prepared identity as containerMO.egoPeer* and its bottle self.moc.performAndWait { do { let policyVersion = policyVersion ?? prevailingPolicyVersion let policyDoc = try self.getPolicyDoc(policyVersion) let stableInfo = TPPeerStableInfo(clock: 1, policyVersion: policyDoc.policyVersion, policyHash: policyDoc.policyHash, policySecrets: policySecrets, deviceName: deviceName, serialNumber: serialNumber, osVersion: osVersion, signing: signingKeyPair, recoverySigningPubKey: nil, recoveryEncryptionPubKey: nil, error: nil) self.containerMO.egoPeerID = permanentInfo.peerID self.containerMO.egoPeerPermanentInfo = permanentInfo.data self.containerMO.egoPeerPermanentInfoSig = permanentInfo.sig self.containerMO.egoPeerStableInfo = stableInfo.data self.containerMO.egoPeerStableInfoSig = stableInfo.sig let bottleMO = BottleMO(context: self.moc) bottleMO.peerID = bottle.peerID bottleMO.bottleID = bottle.bottleID bottleMO.escrowedSigningSPKI = bottle.escrowSigningSPKI bottleMO.signatureUsingEscrowKey = bottle.signatureUsingEscrowKey bottleMO.signatureUsingPeerKey = bottle.signatureUsingPeerKey bottleMO.contents = bottle.contents self.containerMO.addToBottles(bottleMO) try self.moc.save() reply(permanentInfo.peerID, permanentInfo.data, permanentInfo.sig, stableInfo.data, stableInfo.sig, nil) } catch { reply(nil, nil, nil, nil, nil, error) } } } } } func getEgoEpoch(reply: @escaping (UInt64, Error?) -> Void) { let reply: (UInt64, Error?) -> Void = { os_log("getEgoEpoch complete: %d %@", log: tplogTrace, type: .info, $0, traceError($1)) reply($0, $1) } self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID else { reply(0, ContainerError.noPreparedIdentity) return } guard let egoPeer = self.model.peer(withID: egoPeerID) else { reply(0, ContainerError.noPreparedIdentity) return } reply(egoPeer.permanentInfo.epoch, nil) } } func establish(ckksKeys: [CKKSKeychainBackedKeySet], tlkShares: [CKKSTLKShare], preapprovedKeys: [Data]?, reply: @escaping (String?, [CKRecord], Error?) -> Void) { self.semaphore.wait() let reply: (String?, [CKRecord], Error?) -> Void = { os_log("establish complete peer: %@ %@", log: tplogTrace, type: .default, ($0 ?? "NULL") as CVarArg, traceError($2)) self.semaphore.signal() reply($0, $1, $2) } self.moc.performAndWait { self.onqueueEstablish(ckksKeys: ckksKeys, tlkShares: tlkShares, preapprovedKeys: preapprovedKeys, reply: reply) } } func onqueueTTRUntrusted() { let ttr = SecTapToRadar(tapToRadar: "Device not IDMS trusted", description: "Device not IDMS trusted", radar: "52874119") ttr.trigger() } func onqueueEstablish(ckksKeys: [CKKSKeychainBackedKeySet], tlkShares: [CKKSTLKShare], preapprovedKeys: [Data]?, reply: @escaping (String?, [CKRecord], Error?) -> Void) { // Fetch ego peer identity from local storage. guard let egoPeerID = self.containerMO.egoPeerID, let egoPermData = self.containerMO.egoPeerPermanentInfo, let egoPermSig = self.containerMO.egoPeerPermanentInfoSig, let egoStableData = self.containerMO.egoPeerStableInfo, let egoStableSig = self.containerMO.egoPeerStableInfoSig else { reply(nil, [], ContainerError.noPreparedIdentity) return } let keyFactory = TPECPublicKeyFactory() guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else { reply(nil, [], ContainerError.invalidPermanentInfoOrSig) return } guard let selfStableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else { os_log("cannot create TPPeerStableInfo", log: tplogDebug, type: .default) reply(nil, [], ContainerError.invalidStableInfoOrSig) return } guard self.onqueueMachineIDAllowedByIDMS(machineID: selfPermanentInfo.machineID) else { os_log("establish: self machineID %@ not on list", log: tplogDebug, type: .debug, selfPermanentInfo.machineID) self.onqueueTTRUntrusted() reply(nil, [], ContainerError.preparedIdentityNotOnAllowedList(selfPermanentInfo.machineID)) return } loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in guard let egoPeerKeys = egoPeerKeys else { os_log("Don't have my own peer keys; can't establish: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing") reply(nil, [], error) return } self.moc.performAndWait { let viewKeys: [ViewKeys] = ckksKeys.map(ViewKeys.convert) let allTLKShares: [TLKShare] do { let octagonShares = try makeTLKShares(ckksTLKs: ckksKeys.map { $0.tlk }, asPeer: egoPeerKeys, toPeer: egoPeerKeys, epoch: Int(selfPermanentInfo.epoch)) let sosShares = tlkShares.map { TLKShare.convert(ckksTLKShare: $0) } allTLKShares = octagonShares + sosShares } catch { os_log("Unable to make TLKShares for self: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, [], error) return } let dynamicInfo: TPPeerDynamicInfo do { dynamicInfo = try self.model.dynamicInfo(forJoiningPeerID: egoPeerID, peerPermanentInfo: selfPermanentInfo, peerStableInfo: selfStableInfo, sponsorID: nil, preapprovedKeys: preapprovedKeys, signing: egoPeerKeys.signingKey, currentMachineIDs: self.onqueueCurrentMIDList()) os_log("dynamic info: %@", log: tplogDebug, type: .default, dynamicInfo) } catch { reply(nil, [], error) return } let peer = Peer.with { $0.peerID = egoPeerID $0.permanentInfoAndSig.peerPermanentInfo = egoPermData $0.permanentInfoAndSig.sig = egoPermSig $0.stableInfoAndSig.peerStableInfo = egoStableData $0.stableInfoAndSig.sig = egoStableSig $0.dynamicInfoAndSig = SignedPeerDynamicInfo(dynamicInfo) } let bottle: Bottle do { bottle = try self.assembleBottle(egoPeerID: egoPeerID) } catch { reply(nil, [], error) return } os_log("Beginning establish for peer %@", log: tplogDebug, type: .default, egoPeerID) os_log("Establish permanentInfo: %@", log: tplogDebug, type: .debug, egoPermData.base64EncodedString()) os_log("Establish permanentInfoSig: %@", log: tplogDebug, type: .debug, egoPermSig.base64EncodedString()) os_log("Establish stableInfo: %@", log: tplogDebug, type: .debug, egoStableData.base64EncodedString()) os_log("Establish stableInfoSig: %@", log: tplogDebug, type: .debug, egoStableSig.base64EncodedString()) os_log("Establish dynamicInfo: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.peerDynamicInfo.base64EncodedString()) os_log("Establish dynamicInfoSig: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.sig.base64EncodedString()) os_log("Establish introducing %d key sets, %d tlk shares", log: tplogDebug, type: .default, viewKeys.count, allTLKShares.count) do { os_log("Establish bottle: %@", log: tplogDebug, type: .debug, try bottle.serializedData().base64EncodedString()) os_log("Establish peer: %@", log: tplogDebug, type: .debug, try peer.serializedData().base64EncodedString()) } catch { os_log("Establish unable to encode bottle/peer: %@", log: tplogDebug, type: .debug, error as CVarArg) } let request = EstablishRequest.with { $0.peer = peer $0.bottle = bottle $0.viewKeys = viewKeys $0.tlkShares = allTLKShares } self.cuttlefish.establish(request) { response, error in os_log("Establish(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") os_log("Establish: viewKeys: %@", String(describing: viewKeys)) guard let response = response, error == nil else { os_log("establish failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, [], error ?? ContainerError.cloudkitResponseMissing) return } do { os_log("Establish returned changes: %@", log: tplogDebug, type: .default, try response.changes.jsonString()) } catch { os_log("Establish returned changes, but they can't be serialized", log: tplogDebug, type: .default) } let keyHierarchyRecords = response.zoneKeyHierarchyRecords.compactMap { CKRecord($0) } do { try self.persist(changes: response.changes) guard response.changes.more == false else { os_log("establish succeeded, but more changes need fetching...", log: tplogDebug, type: .default) self.fetchAndPersistChanges { fetchError in guard fetchError == nil else { // This is an odd error condition: we might be able to fetch again and be in a good state... os_log("fetch-after-establish failed: %@", log: tplogDebug, type: .default, (fetchError as CVarArg?) ?? "no error") reply(nil, keyHierarchyRecords, fetchError) return } os_log("fetch-after-establish succeeded", log: tplogDebug, type: .default) reply(egoPeerID, keyHierarchyRecords, nil) } return } os_log("establish succeeded", log: tplogDebug, type: .default) reply(egoPeerID, keyHierarchyRecords, nil) } catch { os_log("establish handling failed: %@", log: tplogDebug, type: .default, (error as CVarArg)) reply(nil, keyHierarchyRecords, error) } } } } } func setRecoveryKey(recoveryKey: String, salt: String, ckksKeys: [CKKSKeychainBackedKeySet], reply: @escaping (Error?) -> Void) { self.semaphore.wait() let reply: (Error?) -> Void = { os_log("setRecoveryKey complete: %@", log: tplogTrace, type: .info, traceError($0)) self.semaphore.signal() reply($0) } os_log("beginning a setRecoveryKey", log: tplogDebug, type: .default) self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID else { os_log("no prepared identity, cannot set recovery key", log: tplogDebug, type: .default) reply(ContainerError.noPreparedIdentity) return } var recoveryKeys: RecoveryKey do { recoveryKeys = try RecoveryKey(recoveryKeyString: recoveryKey, recoverySalt: salt) } catch { os_log("failed to create recovery keys: %@", log: tplogDebug, type: .default, error as CVarArg) reply(ContainerError.failedToCreateRecoveryKey) return } let signingPublicKey: Data = recoveryKeys.peerKeys.signingVerificationKey.keyData let encryptionPublicKey: Data = recoveryKeys.peerKeys.encryptionVerificationKey.keyData os_log("setRecoveryKey signingPubKey: %@", log: tplogDebug, type: .debug, signingPublicKey.base64EncodedString()) os_log("setRecoveryKey encryptionPubKey: %@", log: tplogDebug, type: .debug, encryptionPublicKey.base64EncodedString()) guard let stableInfoData = self.containerMO.egoPeerStableInfo else { os_log("stableInfo does not exist", log: tplogDebug, type: .default) reply(ContainerError.nonMember) return } guard let stableInfoSig = self.containerMO.egoPeerStableInfoSig else { os_log("stableInfoSig does not exist", log: tplogDebug, type: .default) reply(ContainerError.nonMember) return } guard let permInfoData = self.containerMO.egoPeerPermanentInfo else { os_log("permanentInfo does not exist", log: tplogDebug, type: .default) reply(ContainerError.nonMember) return } guard let permInfoSig = self.containerMO.egoPeerPermanentInfoSig else { os_log("permInfoSig does not exist", log: tplogDebug, type: .default) reply(ContainerError.nonMember) return } guard let stableInfo = TPPeerStableInfo(data: stableInfoData, sig: stableInfoSig) else { os_log("cannot create TPPeerStableInfo", log: tplogDebug, type: .default) reply(ContainerError.invalidStableInfoOrSig) return } let keyFactory = TPECPublicKeyFactory() guard let permanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: permInfoData, sig: permInfoSig, keyFactory: keyFactory) else { os_log("cannot create TPPeerPermanentInfo", log: tplogDebug, type: .default) reply(ContainerError.invalidStableInfoOrSig) return } loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: egoPeerID)) { signingKeyPair, error in guard let signingKeyPair = signingKeyPair else { os_log("handle: no signing key pair: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(error) return } self.moc.performAndWait { do { let tlkShares = try makeTLKShares(ckksTLKs: ckksKeys.map { $0.tlk }, asPeer: recoveryKeys.peerKeys, toPeer: recoveryKeys.peerKeys, epoch: Int(permanentInfo.epoch)) let policyVersion = stableInfo.policyVersion let policyDoc = try self.getPolicyDoc(policyVersion) let updatedStableInfo = TPPeerStableInfo(clock: stableInfo.clock + 1, policyVersion: policyDoc.policyVersion, policyHash: policyDoc.policyHash, policySecrets: stableInfo.policySecrets, deviceName: stableInfo.deviceName, serialNumber: stableInfo.serialNumber, osVersion: stableInfo.osVersion, signing: signingKeyPair, recoverySigningPubKey: signingPublicKey, recoveryEncryptionPubKey: encryptionPublicKey, error: nil) let signedStableInfo = SignedPeerStableInfo(updatedStableInfo) let request = SetRecoveryKeyRequest.with { $0.peerID = egoPeerID $0.recoverySigningPubKey = signingPublicKey $0.recoveryEncryptionPubKey = encryptionPublicKey $0.stableInfoAndSig = signedStableInfo $0.tlkShares = tlkShares $0.changeToken = self.containerMO.changeToken ?? "" } self.cuttlefish.setRecoveryKey(request) { response, error in os_log("SetRecoveryKey(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("setRecoveryKey failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(error ?? ContainerError.cloudkitResponseMissing) return } self.moc.performAndWait { do { self.containerMO.egoPeerStableInfo = updatedStableInfo.data self.containerMO.egoPeerStableInfoSig = updatedStableInfo.sig try self.onQueuePersist(changes: response.changes) os_log("setRecoveryKey succeeded", log: tplogDebug, type: .default) reply(nil) } catch { os_log("setRecoveryKey handling failed: %@", log: tplogDebug, type: .default, (error as CVarArg)) reply(error) } } } } catch { reply(error) } } } } } func currentSetContainerBottleID(bottleMOs: Set, bottleID: String) -> (Bool) { let bmos = bottleMOs.filter { $0.bottleID == bottleID } return !bmos.isEmpty } func onqueueFindBottle(bottleID: String, reply: @escaping (BottleMO?, Error?) -> Void) { var bmo: BottleMO? var bottles: Set = [] var shouldPerformFetch = false if let containerBottles = self.containerMO.bottles as? Set { if self.currentSetContainerBottleID(bottleMOs: containerBottles, bottleID: bottleID) == false { shouldPerformFetch = true } else { bottles = containerBottles } } else { shouldPerformFetch = true } if shouldPerformFetch == true { self.fetchViableBottlesWithSemaphore { _, _, error in guard error == nil else { os_log("fetchViableBottles failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, error) return } guard let newBottles = self.containerMO.bottles as? Set else { os_log("no bottles on container: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, ContainerError.noBottlesPresent) return } guard self.currentSetContainerBottleID(bottleMOs: newBottles, bottleID: bottleID) == true else { reply(nil, ContainerError.noBottlesForEscrowRecordID) return } os_log("onqueueFindBottle found bottle: %@", log: tplogDebug, type: .default, newBottles) bottles = newBottles.filter { $0.bottleID == bottleID } if bottles.count > 1 { reply(nil, ContainerError.tooManyBottlesForPeer) return } bmo = bottles.removeFirst() reply(bmo, nil) } } else { var filteredBottles = bottles.filter { $0.bottleID == bottleID } if filteredBottles.count > 1 { reply(nil, ContainerError.tooManyBottlesForPeer) return } bmo = filteredBottles.removeFirst() reply(bmo, nil) } } func onqueueRecoverBottle(managedBottle: BottleMO, entropy: Data, bottleSalt: String) throws -> BottledPeer { guard let bottledContents = managedBottle.contents else { throw ContainerError.bottleDoesNotContainContents } guard let signatureUsingEscrowKey = managedBottle.signatureUsingEscrowKey else { throw ContainerError.bottleDoesNotContainEscrowKeySignature } guard let signatureUsingPeerKey = managedBottle.signatureUsingPeerKey else { throw ContainerError.bottleDoesNotContainerPeerKeySignature } guard let sponsorPeerID = managedBottle.peerID else { throw ContainerError.bottleDoesNotContainPeerID } //verify bottle signature using peer do { guard let sponsorPeer = self.model.peer(withID: sponsorPeerID) else { os_log("recover bottle: Unable to find peer that created the bottle", log: tplogDebug, type: .default) throw ContainerError.bottleCreatingPeerNotFound } guard let signingKey: _SFECPublicKey = sponsorPeer.permanentInfo.signingPubKey as? _SFECPublicKey else { os_log("recover bottle: Unable to create a sponsor public key", log: tplogDebug, type: .default) throw ContainerError.signatureVerificationFailed } _ = try BottledPeer.verifyBottleSignature(data: bottledContents, signature: signatureUsingPeerKey, pubKey: signingKey) } catch { os_log("Verification of bottled signature failed: %@", log: tplogDebug, type: .default, error as CVarArg) throw ContainerError.failedToCreateBottledPeer } do { return try BottledPeer(contents: bottledContents, secret: entropy, bottleSalt: bottleSalt, signatureUsingEscrow: signatureUsingEscrowKey, signatureUsingPeerKey: signatureUsingPeerKey) } catch { os_log("Creation of Bottled Peer failed with bottle salt: %@,\nAttempting with empty bottle salt", bottleSalt) do { return try BottledPeer(contents: bottledContents, secret: entropy, bottleSalt: "", signatureUsingEscrow: signatureUsingEscrowKey, signatureUsingPeerKey: signatureUsingPeerKey) } catch { os_log("Creation of Bottled Peer failed: %@", log: tplogDebug, type: .default, error as CVarArg) throw ContainerError.failedToCreateBottledPeer } } } func preflightVouchWithBottle(bottleID: String, reply: @escaping (String?, Error?) -> Void) { self.semaphore.wait() let reply: (String?, Error?) -> Void = { os_log("preflightVouchWithBottle complete: %@", log: tplogTrace, type: .info, traceError($1)) self.semaphore.signal() reply($0, $1) } self.moc.performAndWait { self.onqueueFindBottle(bottleID: bottleID) { bottleMO, error in guard let bottleMO = bottleMO else { os_log("preflightVouchWithBottle found no bottle: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "") reply(nil, error) return } reply(bottleMO.peerID, nil) } } } func vouchWithBottle(bottleID: String, entropy: Data, bottleSalt: String, tlkShares: [CKKSTLKShare], reply: @escaping (Data?, Data?, Error?) -> Void) { self.semaphore.wait() let reply: (Data?, Data?, Error?) -> Void = { os_log("vouchWithBottle complete: %@", log: tplogTrace, type: .info, traceError($2)) self.semaphore.signal() reply($0, $1, $2) } self.fetchAndPersistChanges { error in guard error == nil else { os_log("vouchWithBottle unable to fetch changes: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "") reply(nil, nil, error) return } self.onqueueFindBottle(bottleID: bottleID) { returnedBMO, error in self.moc.performAndWait { guard error == nil else { os_log("vouchWithBottle unable to find bottle for escrow record id: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "") reply(nil, nil, error) return } guard let bmo: BottleMO = returnedBMO else { os_log("vouchWithBottle bottle is nil: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "") reply(nil, nil, error) return } guard let bottledContents = bmo.contents else { reply(nil, nil, ContainerError.bottleDoesNotContainContents) return } guard let signatureUsingEscrowKey = bmo.signatureUsingEscrowKey else { reply(nil, nil, ContainerError.bottleDoesNotContainEscrowKeySignature) return } guard let signatureUsingPeerKey = bmo.signatureUsingPeerKey else { reply(nil, nil, ContainerError.bottleDoesNotContainerPeerKeySignature) return } guard let sponsorPeerID = bmo.peerID else { reply(nil, nil, ContainerError.bottleDoesNotContainPeerID) return } //verify bottle signature using peer do { guard let sponsorPeer = self.model.peer(withID: sponsorPeerID) else { os_log("vouchWithBottle: Unable to find peer that created the bottle", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.bottleCreatingPeerNotFound) return } guard let signingKey: _SFECPublicKey = sponsorPeer.permanentInfo.signingPubKey as? _SFECPublicKey else { os_log("vouchWithBottle: Unable to create a sponsor public key", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.signatureVerificationFailed) return } _ = try BottledPeer.verifyBottleSignature(data: bottledContents, signature: signatureUsingPeerKey, pubKey: signingKey) } catch { os_log("vouchWithBottle: Verification of bottled signature failed: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, nil, ContainerError.failedToCreateBottledPeer) return } //create bottled peer let bottledPeer: BottledPeer do { bottledPeer = try BottledPeer(contents: bottledContents, secret: entropy, bottleSalt: bottleSalt, signatureUsingEscrow: signatureUsingEscrowKey, signatureUsingPeerKey: signatureUsingPeerKey) } catch { os_log("Creation of Bottled Peer failed with bottle salt: %@,\nAttempting with empty bottle salt", bottleSalt) do { bottledPeer = try BottledPeer(contents: bottledContents, secret: entropy, bottleSalt: "", signatureUsingEscrow: signatureUsingEscrowKey, signatureUsingPeerKey: signatureUsingPeerKey) } catch { os_log("Creation of Bottled Peer failed: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, nil, ContainerError.failedToCreateBottledPeer) return } } os_log("Have a bottle for peer %@", log: tplogDebug, type: .default, bottledPeer.peerID) // Extract any TLKs we have been given extract(tlkShares: tlkShares, peer: bottledPeer.peerKeys) self.moc.performAndWait { // I must have an ego identity in order to vouch using bottle guard let egoPeerID = self.containerMO.egoPeerID else { os_log("As a nonmember, can't vouch for someone else", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } guard let permanentInfo = self.containerMO.egoPeerPermanentInfo else { os_log("permanentInfo does not exist", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } guard let permanentInfoSig = self.containerMO.egoPeerPermanentInfoSig else { os_log("permanentInfoSig does not exist", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } guard let stableInfo = self.containerMO.egoPeerStableInfo else { os_log("stableInfo does not exist", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } guard let stableInfoSig = self.containerMO.egoPeerStableInfoSig else { os_log("stableInfoSig does not exist", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } let keyFactory = TPECPublicKeyFactory() guard let beneficiaryPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: permanentInfo, sig: permanentInfoSig, keyFactory: keyFactory) else { os_log("Invalid permenent info or signature; can't vouch for them", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.invalidPermanentInfoOrSig) return } guard let beneficiaryStableInfo = TPPeerStableInfo(data: stableInfo, sig: stableInfoSig) else { os_log("Invalid stableinfo or signature; van't vouch for them", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.invalidStableInfoOrSig) return } do { let voucher = try self.model.createVoucher(forCandidate: beneficiaryPermanentInfo, stableInfo: beneficiaryStableInfo, withSponsorID: sponsorPeerID, reason: TPVoucherReason.restore, signing: bottledPeer.peerKeys.signingKey) reply(voucher.data, voucher.sig, nil) return } catch { os_log("Error creating voucher: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, nil, error) return } } } } } } func vouchWithRecoveryKey(recoveryKey: String, salt: String, tlkShares: [CKKSTLKShare], reply: @escaping (Data?, Data?, Error?) -> Void) { self.semaphore.wait() let reply: (Data?, Data?, Error?) -> Void = { os_log("vouchWithRecoveryKey complete: %@", log: tplogTrace, type: .info, traceError($2)) self.semaphore.signal() reply($0, $1, $2) } self.moc.performAndWait { os_log("beginning a vouchWithRecoveryKey", log: tplogDebug, type: .default) // I must have an ego identity in order to vouch using bottle guard let egoPeerID = self.containerMO.egoPeerID else { os_log("As a nonmember, can't vouch for someone else", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } guard let permanentInfo = self.containerMO.egoPeerPermanentInfo else { os_log("permanentInfo does not exist", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } guard let permanentInfoSig = self.containerMO.egoPeerPermanentInfoSig else { os_log("permanentInfoSig does not exist", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } guard let stableInfo = self.containerMO.egoPeerStableInfo else { os_log("stableInfo does not exist", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } guard let stableInfoSig = self.containerMO.egoPeerStableInfoSig else { os_log("stableInfoSig does not exist", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } let keyFactory = TPECPublicKeyFactory() guard let beneficiaryPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: permanentInfo, sig: permanentInfoSig, keyFactory: keyFactory) else { os_log("Invalid permenent info or signature; can't vouch for them", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.invalidPermanentInfoOrSig) return } guard let beneficiaryStableInfo = TPPeerStableInfo(data: stableInfo, sig: stableInfoSig) else { os_log("Invalid stableinfo or signature; van't vouch for them", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.invalidStableInfoOrSig) return } //create recovery key set var recoveryKeys: RecoveryKey do { recoveryKeys = try RecoveryKey(recoveryKeyString: recoveryKey, recoverySalt: salt) } catch { os_log("failed to create recovery keys: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, nil, ContainerError.failedToCreateRecoveryKey) return } extract(tlkShares: tlkShares, peer: recoveryKeys.peerKeys) let signingPublicKey: Data = recoveryKeys.peerKeys.signingKey.publicKey.keyData let encryptionPublicKey: Data = recoveryKeys.peerKeys.encryptionKey.publicKey.keyData os_log("vouchWithRecoveryKey signingPubKey: %@", log: tplogDebug, type: .debug, signingPublicKey.base64EncodedString()) os_log("vouchWithRecoveryKey encryptionPubKey: %@", log: tplogDebug, type: .debug, encryptionPublicKey.base64EncodedString()) guard self.model.isRecoveryKeyEnrolled() else { os_log("Recovery Key is not enrolled", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.recoveryKeysNotEnrolled) return } //find matching peer containing recovery keys guard let sponsorPeerID = self.model.peerIDThatTrustsRecoveryKeys(TPRecoveryKeyPair(signingSPKI: signingPublicKey, encryptionSPKI: encryptionPublicKey)) else { os_log("Untrusted recovery key set", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.untrustedRecoveryKeys) return } do { let voucher = try self.model.createVoucher(forCandidate: beneficiaryPermanentInfo, stableInfo: beneficiaryStableInfo, withSponsorID: sponsorPeerID, reason: TPVoucherReason.recoveryKey, signing: recoveryKeys.peerKeys.signingKey) reply(voucher.data, voucher.sig, nil) return } catch { os_log("Error creating voucher using recovery key set: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, nil, error) return } } } func vouch(peerID: String, permanentInfo: Data, permanentInfoSig: Data, stableInfo: Data, stableInfoSig: Data, ckksKeys: [CKKSKeychainBackedKeySet], reply: @escaping (Data?, Data?, Error?) -> Void) { self.semaphore.wait() let reply: (Data?, Data?, Error?) -> Void = { os_log("vouch complete: %@", log: tplogTrace, type: .info, traceError($2)) self.semaphore.signal() reply($0, $1, $2) } self.moc.performAndWait { // I must have an ego identity in order to vouch for someone else. guard let egoPeerID = self.containerMO.egoPeerID, let egoPermData = self.containerMO.egoPeerPermanentInfo, let egoPermSig = self.containerMO.egoPeerPermanentInfoSig else { os_log("As a nonmember, can't vouch for someone else", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.nonMember) return } let keyFactory = TPECPublicKeyFactory() guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else { reply(nil, nil, ContainerError.invalidPermanentInfoOrSig) return } guard let beneficiaryPermanentInfo = TPPeerPermanentInfo(peerID: peerID, data: permanentInfo, sig: permanentInfoSig, keyFactory: keyFactory) else { os_log("Invalid permenent info or signature; can't vouch for them", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.invalidPermanentInfoOrSig) return } guard let beneficiaryStableInfo = TPPeerStableInfo(data: stableInfo, sig: stableInfoSig) else { os_log("Invalid stableinfo or signature; van't vouch for them", log: tplogDebug, type: .default) reply(nil, nil, ContainerError.invalidStableInfoOrSig) return } loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in guard let egoPeerKeys = egoPeerKeys else { os_log("Don't have my own keys: can't vouch for %@: %@", log: tplogDebug, type: .default, beneficiaryPermanentInfo, (error as CVarArg?) ?? "no error") reply(nil, nil, error) return } self.moc.performAndWait { let voucher: TPVoucher do { voucher = try self.model.createVoucher(forCandidate: beneficiaryPermanentInfo, stableInfo: beneficiaryStableInfo, withSponsorID: egoPeerID, reason: TPVoucherReason.secureChannel, signing: egoPeerKeys.signingKey) } catch { os_log("Error creating voucher: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, nil, error) return } // And generate and upload any tlkShares // Note that this might not be the whole list: Octagon: Limited Peers let tlkShares: [TLKShare] do { // Note: we only want to send up TLKs for uploaded ckks zones let ckksTLKs = ckksKeys.filter { !$0.newUpload }.map { $0.tlk } tlkShares = try makeTLKShares(ckksTLKs: ckksTLKs, asPeer: egoPeerKeys, toPeer: beneficiaryPermanentInfo, epoch: Int(selfPermanentInfo.epoch)) } catch { os_log("Unable to make TLKShares for beneficiary %@: %@", log: tplogDebug, type: .default, beneficiaryPermanentInfo, error as CVarArg) reply(nil, nil, error) return } guard !tlkShares.isEmpty else { os_log("No TLKShares to upload for new peer, returning voucher", log: tplogDebug, type: .default) reply(voucher.data, voucher.sig, nil) return } self.cuttlefish.updateTrust(changeToken: self.containerMO.changeToken ?? "", peerID: egoPeerID, stableInfoAndSig: nil, dynamicInfoAndSig: nil, tlkShares: tlkShares, viewKeys: []) { response, error in guard let response = response, error == nil else { os_log("Unable to upload new tlkshares: %@", log: tplogDebug, type: .default, error as CVarArg? ?? "no error") reply(voucher.data, voucher.sig, error ?? ContainerError.cloudkitResponseMissing) return } let newKeyRecords = response.zoneKeyHierarchyRecords.map(CKRecord.init) os_log("Uploaded new tlkshares: %@", log: tplogDebug, type: .default, newKeyRecords) // We don't need to save these; CKKS will refetch them as needed reply(voucher.data, voucher.sig, nil) } } } } } func departByDistrustingSelf(reply: @escaping (Error?) -> Void) { self.semaphore.wait() let reply: (Error?) -> Void = { os_log("departByDistrustingSelf complete: %@", log: tplogTrace, type: .info, traceError($0)) self.semaphore.signal() reply($0) } self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID else { os_log("No dynamic info for self?", log: tplogDebug, type: .default) reply(ContainerError.noPreparedIdentity) return } self.onqueueDistrust(peerIDs: [egoPeerID], reply: reply) } } func distrust(peerIDs: Set, reply: @escaping (Error?) -> Void) { self.semaphore.wait() let reply: (Error?) -> Void = { os_log("distrust complete: %@", log: tplogTrace, type: .info, traceError($0)) self.semaphore.signal() reply($0) } self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID else { os_log("No dynamic info for self?", log: tplogDebug, type: .default) reply(ContainerError.noPreparedIdentity) return } guard !peerIDs.contains(egoPeerID) else { os_log("Self-distrust via peerID not allowed", log: tplogDebug, type: .default) reply(ContainerError.invalidPeerID) return } self.onqueueDistrust(peerIDs: peerIDs, reply: reply) } } func onqueueDistrust(peerIDs: Set, reply: @escaping (Error?) -> Void) { guard let egoPeerID = self.containerMO.egoPeerID else { os_log("No dynamic info for self?", log: tplogDebug, type: .default) reply(ContainerError.noPreparedIdentity) return } loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: egoPeerID)) { signingKeyPair, error in guard let signingKeyPair = signingKeyPair else { os_log("No longer have signing key pair; can't sign distrust: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "nil") reply(error) return } self.moc.performAndWait { let dynamicInfo: TPPeerDynamicInfo do { dynamicInfo = try self.model.calculateDynamicInfoForPeer(withID: egoPeerID, addingPeerIDs: nil, removingPeerIDs: Array(peerIDs), preapprovedKeys: nil, signing: signingKeyPair, currentMachineIDs: self.onqueueCurrentMIDList()) } catch { os_log("Error preparing dynamic info: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "nil") reply(error) return } let signedDynamicInfo = SignedPeerDynamicInfo(dynamicInfo) os_log("attempting distrust for %@ with: %@", log: tplogDebug, type: .default, peerIDs, dynamicInfo) let request = UpdateTrustRequest.with { $0.changeToken = self.containerMO.changeToken ?? "" $0.peerID = egoPeerID $0.dynamicInfoAndSig = signedDynamicInfo } self.cuttlefish.updateTrust(request) { response, error in os_log("UpdateTrust(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("updateTrust failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(error ?? ContainerError.cloudkitResponseMissing) return } do { try self.persist(changes: response.changes) os_log("distrust succeeded", log: tplogDebug, type: .default) reply(nil) } catch { os_log("distrust handling failed: %@", log: tplogDebug, type: .default, (error as CVarArg)) reply(error) } } } } } func fetchEscrowContents(reply: @escaping (Data?, String?, Data?, Error?) -> Void) { self.semaphore.wait() let reply: (Data?, String?, Data?, Error?) -> Void = { os_log("fetchEscrowContents complete: %@", log: tplogTrace, type: .info, traceError($3)) self.semaphore.signal() reply($0, $1, $2, $3) } os_log("beginning a fetchEscrowContents", log: tplogDebug, type: .default) self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID else { os_log("fetchEscrowContents failed", log: tplogDebug, type: .default) reply(nil, nil, nil, ContainerError.noPreparedIdentity) return } guard let bottles = self.containerMO.bottles as? Set else { os_log("fetchEscrowContents failed", log: tplogDebug, type: .default) reply(nil, nil, nil, ContainerError.noBottleForPeer) return } var bmoSet = bottles.filter { $0.peerID == egoPeerID } let bmo = bmoSet.removeFirst() let bottleID = bmo.bottleID var entropy: Data do { guard let loaded = try loadSecret(label: egoPeerID) else { os_log("fetchEscrowContents failed to load entropy", log: tplogDebug, type: .default) reply(nil, nil, nil, ContainerError.failedToFetchEscrowContents) return } entropy = loaded } catch { os_log("fetchEscrowContents failed to load entropy: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, nil, nil, error) return } guard let signingPublicKey = bmo.escrowedSigningSPKI else { os_log("fetchEscrowContents no escrow signing spki", log: tplogDebug, type: .default) reply(nil, nil, nil, ContainerError.bottleDoesNotContainerEscrowKeySPKI) return } reply(entropy, bottleID, signingPublicKey, nil) } } func fetchViableBottles(reply: @escaping ([String]?, [String]?, Error?) -> Void) { self.semaphore.wait() let reply: ([String]?, [String]?, Error?) -> Void = { os_log("fetchViableBottles complete: %@", log: tplogTrace, type: .info, traceError($2)) self.semaphore.signal() reply($0, $1, $2) } self.fetchViableBottlesWithSemaphore(reply: reply) } func onqueueCachedBottlesContainEgoPeerBottle(cachedBottles: TPCachedViableBottles) -> Bool { guard let egoPeerID = self.containerMO.egoPeerID else { os_log("bottleForEgoPeer: No identity.", log: tplogDebug, type: .default) return false } guard let bottles: Set = self.containerMO.bottles as? Set else { os_log("bottleForEgoPeer: No Bottles.", log: tplogDebug, type: .default) return false } var matchesCached: Bool = false for bottle in bottles { guard let bottleID: String = bottle.bottleID else { continue } if bottle.peerID == egoPeerID && (cachedBottles.viableBottles.contains(bottleID) || cachedBottles.partialBottles.contains(bottleID)) { matchesCached = true break } } return matchesCached } func fetchViableBottlesWithSemaphore(reply: @escaping ([String]?, [String]?, Error?) -> Void) { os_log("beginning a fetchViableBottles", log: tplogDebug, type: .default) let cachedBottles: TPCachedViableBottles = self.model.currentCachedViableBottlesSet() self.moc.performAndWait { if self.onqueueCachedBottlesContainEgoPeerBottle(cachedBottles: cachedBottles) && (cachedBottles.viableBottles.count > 0 || cachedBottles.partialBottles.count > 0) { os_log("returning from fetchViableBottles, using cached bottles", log: tplogDebug, type: .default) reply(cachedBottles.viableBottles, cachedBottles.partialBottles, nil) return } self.cuttlefish.fetchViableBottles { response, error in guard error == nil else { os_log("fetchViableBottles failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, nil, error) return } self.moc.performAndWait { guard let escrowPairs = response?.viableBottles else { os_log("fetchViableBottles returned no viable bottles: %@", log: tplogDebug, type: .default) reply([], [], nil) return } var partialPairs: [EscrowPair] = [] if let partial = response?.partialBottles { partialPairs = partial } else { os_log("fetchViableBottles returned no partially viable bottles, but that's ok: %@", log: tplogDebug, type: .default) } let viableBottleIDs = escrowPairs.compactMap { $0.bottle.bottleID } os_log("fetchViableBottles returned viable bottles: %@", log: tplogDebug, type: .default, viableBottleIDs) let partialBottleIDs = partialPairs.compactMap { $0.bottle.bottleID } os_log("fetchViableBottles returned partial bottles: %@", log: tplogDebug, type: .default, partialBottleIDs) escrowPairs.forEach { pair in let bottle = pair.bottle // Save this bottle only if we don't already have it if let existingBottles = self.containerMO.bottles as? Set { let matchingBottles: Set = existingBottles.filter { existing in existing.peerID == bottle.peerID && existing.bottleID == bottle.bottleID && existing.escrowedSigningSPKI == bottle.escrowedSigningSpki && existing.signatureUsingEscrowKey == bottle.signatureUsingEscrowKey && existing.signatureUsingPeerKey == bottle.signatureUsingPeerKey && existing.contents == bottle.contents } if !matchingBottles.isEmpty { os_log("fetchViableBottles already knows about bottle", log: tplogDebug, type: .default, bottle.bottleID) return } } let bmo = BottleMO(context: self.moc) bmo.peerID = bottle.peerID bmo.bottleID = bottle.bottleID bmo.escrowedSigningSPKI = bottle.escrowedSigningSpki bmo.signatureUsingEscrowKey = bottle.signatureUsingEscrowKey bmo.signatureUsingPeerKey = bottle.signatureUsingPeerKey bmo.contents = bottle.contents os_log("fetchViableBottles saving new bottle: %@", log: tplogDebug, type: .default, bmo) self.containerMO.addToBottles(bmo) } partialPairs.forEach { pair in let bottle = pair.bottle // Save this bottle only if we don't already have it if let existingBottles = self.containerMO.bottles as? Set { let matchingBottles: Set = existingBottles.filter { existing in existing.peerID == bottle.peerID && existing.bottleID == bottle.bottleID && existing.escrowedSigningSPKI == bottle.escrowedSigningSpki && existing.signatureUsingEscrowKey == bottle.signatureUsingEscrowKey && existing.signatureUsingPeerKey == bottle.signatureUsingPeerKey && existing.contents == bottle.contents } if !matchingBottles.isEmpty { os_log("fetchViableBottles already knows about bottle", log: tplogDebug, type: .default, bottle.bottleID) return } } let bmo = BottleMO(context: self.moc) bmo.peerID = bottle.peerID bmo.bottleID = bottle.bottleID bmo.escrowedSigningSPKI = bottle.escrowedSigningSpki bmo.signatureUsingEscrowKey = bottle.signatureUsingEscrowKey bmo.signatureUsingPeerKey = bottle.signatureUsingPeerKey bmo.contents = bottle.contents os_log("fetchViableBottles saving new bottle: %@", log: tplogDebug, type: .default, bmo) self.containerMO.addToBottles(bmo) } do { try self.moc.save() os_log("fetchViableBottles saved bottles", log: tplogDebug, type: .default) let cached = TPCachedViableBottles(viableBottles: viableBottleIDs, partialBottles: partialBottleIDs) self.model.setViableBottles(cached) reply(viableBottleIDs, partialBottleIDs, nil) } catch { os_log("fetchViableBottles unable to save bottles: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, nil, error) } } } } } func fetchPolicy(reply: @escaping (TPPolicy?, Error?) -> Void) { self.semaphore.wait() let reply: (TPPolicy?, Error?) -> Void = { os_log("fetchPolicy complete: %@", log: tplogTrace, type: .info, traceError($1)) self.semaphore.signal() reply($0, $1) } self.moc.performAndWait { var keys: [NSNumber: String] = [:] guard let stableInfoData = self.containerMO.egoPeerStableInfo, let stableInfoSig = self.containerMO.egoPeerStableInfoSig else { os_log("fetchPolicy failed to find ego peer stableinfodata/sig", log: tplogDebug, type: .error) reply(nil, ContainerError.noPreparedIdentity) return } guard let stableInfo = TPPeerStableInfo(data: stableInfoData, sig: stableInfoSig) else { os_log("fetchPolicy failed to create TPPeerStableInfo", log: tplogDebug, type: .error) reply(nil, ContainerError.invalidStableInfoOrSig) return } let policyVersionCounter = stableInfo.policyVersion let policyVersion = NSNumber(value: policyVersionCounter) keys[policyVersion] = stableInfo.policyHash if let policyDocument = self.model.policy(withVersion: policyVersionCounter) { os_log("fetchPolicy: have a local version of policy %@: %@", log: tplogDebug, type: .default, policyVersion, policyDocument) do { let policy = try policyDocument.policy(withSecrets: stableInfo.policySecrets, decrypter: Decrypter()) reply(policy, nil) return } catch { os_log("TPPolicyDocument failed: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, error) return } } self.fetchPolicyDocuments(keys: keys) { result, error in guard error == nil else { reply(nil, error) return } guard let result = result else { os_log("fetchPolicy: nil policies returned") reply(nil, ContainerError.policyDocumentDoesNotValidate) return } guard result.count == 1 else { os_log("fetchPolicy: wrong length returned") reply(nil, ContainerError.policyDocumentDoesNotValidate) return } guard let r = result[policyVersion] else { os_log("fetchPolicy: version not found") reply(nil, ContainerError.unknownPolicyVersion(policyVersion.uint64Value)) return } guard let data = r[1].data(using: .utf8) else { os_log("fetchPolicy: failed to convert data") reply(nil, ContainerError.unknownPolicyVersion(policyVersion.uint64Value)) return } guard let pd = TPPolicyDocument.policyDoc(withHash: r[0], data: data) else { os_log("fetchPolicy: pd is nil") reply(nil, ContainerError.policyDocumentDoesNotValidate) return } do { let policy = try pd.policy(withSecrets: stableInfo.policySecrets, decrypter: Decrypter()) reply(policy, nil) } catch { os_log("TPPolicyDocument: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, error) } } } } // All-or-nothing: return an error in case full list cannot be returned. // Completion handler data format: [version : [hash, data]] func fetchPolicyDocuments(keys: [NSNumber: String], reply: @escaping ([NSNumber: [String]]?, Error?) -> Void) { self.semaphore.wait() let reply: ([NSNumber: [String]]?, Error?) -> Void = { os_log("fetchPolicyDocuments complete: %@", log: tplogTrace, type: .info, traceError($1)) self.semaphore.signal() reply($0, $1) } var keys = keys var docs: [NSNumber: [String]] = [:] self.moc.performAndWait { for (version, hash) in keys { if let policydoc = try? self.getPolicyDoc(version.uint64Value), policydoc.policyHash == hash { docs[version] = [policydoc.policyHash, policydoc.protobuf.base64EncodedString()] keys[version] = nil } } } if keys.isEmpty { reply(docs, nil) return } let request = FetchPolicyDocumentsRequest.with { $0.keys = keys.map { key, value in PolicyDocumentKey.with { $0.version = key.uint64Value; $0.hash = value }} } self.cuttlefish.fetchPolicyDocuments(request) { response, error in os_log("FetchPolicyDocuments(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("FetchPolicyDocuments failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, error ?? ContainerError.cloudkitResponseMissing) return } self.moc.performAndWait { for mapEntry in response.entries { // TODO: validate the policy's signature guard let doc = TPPolicyDocument.policyDoc(withHash: mapEntry.key.hash, data: mapEntry.value) else { os_log("Can't make policy document with hash %@ and data %@", log: tplogDebug, type: .default, mapEntry.key.hash, mapEntry.value.base64EncodedString()) reply(nil, ContainerError.policyDocumentDoesNotValidate) return } guard let hash = keys[NSNumber(value: doc.policyVersion)], hash == doc.policyHash else { os_log("Requested hash %@ does not match fetched hash %@", log: tplogDebug, type: .default, keys[NSNumber(value: doc.policyVersion)] ?? "", doc.policyHash) reply(nil, ContainerError.policyDocumentDoesNotValidate) return } keys[NSNumber(value: doc.policyVersion)] = nil // Server responses should be unique, let's enforce docs[NSNumber(value: doc.policyVersion)] = [doc.policyHash, doc.protobuf.base64EncodedString()] self.model.register(doc) } do { try self.moc.save() // if this fails callers might make bad data assumptions } catch { reply(nil, error) return } if !keys.isEmpty { let (unknownVersion, _) = keys.first! reply(nil, ContainerError.unknownPolicyVersion(unknownVersion.uint64Value)) return } reply(docs, nil) } } } // Must be on moc queue to call this. // Caller is responsible for saving the moc afterwards. @discardableResult private func registerPeerMO(permanentInfo: TPPeerPermanentInfo, stableInfo: TPPeerStableInfo? = nil, dynamicInfo: TPPeerDynamicInfo? = nil, vouchers: [TPVoucher]? = nil, isEgoPeer: Bool = false) throws -> PeerMO { let peerID = permanentInfo.peerID let peer = PeerMO(context: self.moc) peer.peerID = peerID peer.permanentInfo = permanentInfo.data peer.permanentInfoSig = permanentInfo.sig peer.stableInfo = stableInfo?.data peer.stableInfoSig = stableInfo?.sig peer.dynamicInfo = dynamicInfo?.data peer.dynamicInfoSig = dynamicInfo?.sig peer.isEgoPeer = isEgoPeer self.containerMO.addToPeers(peer) self.model.registerPeer(with: permanentInfo) if let stableInfo = stableInfo { try self.model.update(stableInfo, forPeerWithID: peerID) } if let dynamicInfo = dynamicInfo { try self.model.update(dynamicInfo, forPeerWithID: peerID) } vouchers?.forEach { voucher in self.model.register(voucher) let voucherMO = VoucherMO(context: self.moc) voucherMO.voucherInfo = voucher.data voucherMO.voucherInfoSig = voucher.sig peer.addToVouchers(voucherMO) } return peer } /* Returns any new CKKS keys that need uploading, as well as any TLKShares necessary for those keys */ func makeSharesForNewKeySets(ckksKeys: [CKKSKeychainBackedKeySet], tlkShares: [CKKSTLKShare], egoPeerKeys: OctagonSelfPeerKeys, egoPeerDynamicInfo: TPPeerDynamicInfo, epoch: Int) throws -> ([ViewKeys], [TLKShare]) { let newCKKSKeys = ckksKeys.filter { $0.newUpload } let newViewKeys: [ViewKeys] = newCKKSKeys.map(ViewKeys.convert) let octagonSelfShares = try makeTLKShares(ckksTLKs: ckksKeys.map { $0.tlk }, asPeer: egoPeerKeys, toPeer: egoPeerKeys, epoch: epoch) let extraShares = tlkShares.map { TLKShare.convert(ckksTLKShare: $0) } var peerShares: [TLKShare] = [] for keyset in newCKKSKeys { do { let peerIDsWithAccess = try self.model.getPeerIDsTrustedByPeer(with: egoPeerDynamicInfo, toAccessView: keyset.tlk.zoneID.zoneName) os_log("Planning to share %@ with peers %@", log: tplogDebug, type: .default, String(describing: keyset.tlk), peerIDsWithAccess) let peers = peerIDsWithAccess.compactMap { self.model.peer(withID: $0) } let viewPeerShares = try peers.map { receivingPeer in TLKShare.convert(ckksTLKShare: try CKKSTLKShare(keyset.tlk, as: egoPeerKeys, to: receivingPeer.permanentInfo, epoch: epoch, poisoned: 0)) } peerShares = peerShares + viewPeerShares } catch { os_log("Unable to create TLKShares for keyset %@: %@", log: tplogDebug, type: .default, String(describing: keyset), error as CVarArg) } } return (newViewKeys, octagonSelfShares + peerShares + extraShares) } func onqueuePreparePeerForJoining(egoPeerID: String, peerPermanentInfo: TPPeerPermanentInfo, stableInfo: TPPeerStableInfo, sponsorID: String?, preapprovedKeys: [Data]?, vouchers: [SignedVoucher], egoPeerKeys: OctagonSelfPeerKeys) throws -> (Peer, TPPeerDynamicInfo) { let dynamicInfo = try self.model.dynamicInfo(forJoiningPeerID: egoPeerID, peerPermanentInfo: peerPermanentInfo, peerStableInfo: stableInfo, sponsorID: sponsorID, preapprovedKeys: preapprovedKeys, signing: egoPeerKeys.signingKey, currentMachineIDs: self.onqueueCurrentMIDList()) let newStableInfo = try self.createNewStableInfoIfNeeded(stableChanges: nil, egoPeerID: egoPeerID, dynamicInfo: dynamicInfo, signingKeyPair: egoPeerKeys.signingKey) let peer = Peer.with { $0.peerID = egoPeerID $0.permanentInfoAndSig = SignedPeerPermanentInfo(peerPermanentInfo) $0.stableInfoAndSig = SignedPeerStableInfo(newStableInfo ?? stableInfo) $0.dynamicInfoAndSig = SignedPeerDynamicInfo(dynamicInfo) $0.vouchers = vouchers } return (peer, dynamicInfo) } func join(voucherData: Data, voucherSig: Data, ckksKeys: [CKKSKeychainBackedKeySet], tlkShares: [CKKSTLKShare], preapprovedKeys: [Data]?, reply: @escaping (String?, [CKRecord], Error?) -> Void) { self.semaphore.wait() let reply: (String?, [CKRecord], Error?) -> Void = { os_log("join complete: %@", log: tplogTrace, type: .info, traceError($2)) self.semaphore.signal() reply($0, $1, $2) } self.fetchAndPersistChanges { error in guard error == nil else { reply(nil, [], error) return } self.moc.performAndWait { guard let voucher = TPVoucher(infoWith: voucherData, sig: voucherSig) else { reply(nil, [], ContainerError.invalidVoucherOrSig) return } guard let sponsor = self.model.peer(withID: voucher.sponsorID) else { reply(nil, [], ContainerError.sponsorNotRegistered(voucher.sponsorID)) return } // Fetch ego peer identity from local storage. guard let egoPeerID = self.containerMO.egoPeerID, let egoPermData = self.containerMO.egoPeerPermanentInfo, let egoPermSig = self.containerMO.egoPeerPermanentInfoSig, let egoStableData = self.containerMO.egoPeerStableInfo, let egoStableSig = self.containerMO.egoPeerStableInfoSig else { reply(nil, [], ContainerError.noPreparedIdentity) return } let keyFactory = TPECPublicKeyFactory() guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else { reply(nil, [], ContainerError.invalidPermanentInfoOrSig) return } guard let selfStableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else { reply(nil, [], ContainerError.invalidStableInfoOrSig) return } guard self.onqueueMachineIDAllowedByIDMS(machineID: selfPermanentInfo.machineID) else { os_log("join: self machineID %@ not on list", log: tplogDebug, type: .debug, selfPermanentInfo.machineID) self.onqueueTTRUntrusted() reply(nil, [], ContainerError.preparedIdentityNotOnAllowedList(selfPermanentInfo.machineID)) return } loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in guard let egoPeerKeys = egoPeerKeys else { os_log("Don't have my own peer keys; can't join: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing") reply(nil, [], error) return } self.moc.performAndWait { let peer: Peer let newDynamicInfo: TPPeerDynamicInfo do { (peer, newDynamicInfo) = try self.onqueuePreparePeerForJoining(egoPeerID: egoPeerID, peerPermanentInfo: selfPermanentInfo, stableInfo: selfStableInfo, sponsorID: sponsor.peerID, preapprovedKeys: preapprovedKeys, vouchers: [SignedVoucher.with { $0.voucher = voucher.data $0.sig = voucher.sig }, ], egoPeerKeys: egoPeerKeys) } catch { os_log("Unable to create peer for joining: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, [], error) return } let allTLKShares: [TLKShare] let viewKeys: [ViewKeys] do { (viewKeys, allTLKShares) = try self.makeSharesForNewKeySets(ckksKeys: ckksKeys, tlkShares: tlkShares, egoPeerKeys: egoPeerKeys, egoPeerDynamicInfo: newDynamicInfo, epoch: Int(selfPermanentInfo.epoch)) } catch { os_log("Unable to process keys before joining: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, [], error) return } do { try self.model.checkIntroduction(forCandidate: selfPermanentInfo, stableInfo: peer.stableInfoAndSig.toStableInfo(), withSponsorID: sponsor.peerID) } catch { os_log("Error checking introduction: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, [], error) return } var bottle: Bottle do { bottle = try self.assembleBottle(egoPeerID: egoPeerID) } catch { reply(nil, [], error) return } os_log("Beginning join for peer %@", log: tplogDebug, type: .default, egoPeerID) os_log("Join permanentInfo: %@", log: tplogDebug, type: .debug, egoPermData.base64EncodedString()) os_log("Join permanentInfoSig: %@", log: tplogDebug, type: .debug, egoPermSig.base64EncodedString()) os_log("Join stableInfo: %@", log: tplogDebug, type: .debug, peer.stableInfoAndSig.peerStableInfo.base64EncodedString()) os_log("Join stableInfoSig: %@", log: tplogDebug, type: .debug, peer.stableInfoAndSig.sig.base64EncodedString()) os_log("Join dynamicInfo: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.peerDynamicInfo.base64EncodedString()) os_log("Join dynamicInfoSig: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.sig.base64EncodedString()) os_log("Join vouchers: %@", log: tplogDebug, type: .debug, peer.vouchers.map { $0.voucher.base64EncodedString() }) os_log("Join voucher signatures: %@", log: tplogDebug, type: .debug, peer.vouchers.map { $0.sig.base64EncodedString() }) os_log("Uploading %d tlk shares", log: tplogDebug, type: .default, allTLKShares.count) do { os_log("Join peer: %@", log: tplogDebug, type: .debug, try peer.serializedData().base64EncodedString()) } catch { os_log("Join unable to encode peer: %@", log: tplogDebug, type: .debug, error as CVarArg) } let changeToken = self.containerMO.changeToken ?? "" let request = JoinWithVoucherRequest.with { $0.changeToken = changeToken $0.peer = peer $0.bottle = bottle $0.tlkShares = allTLKShares $0.viewKeys = viewKeys } self.cuttlefish.joinWithVoucher(request) { response, error in os_log("JoinWithVoucher(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("joinWithVoucher failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, [], error ?? ContainerError.cloudkitResponseMissing) return } self.moc.performAndWait { do { self.containerMO.egoPeerStableInfo = peer.stableInfoAndSig.peerStableInfo self.containerMO.egoPeerStableInfoSig = peer.stableInfoAndSig.sig try self.onQueuePersist(changes: response.changes) os_log("JoinWithVoucher succeeded", log: tplogDebug) let keyHierarchyRecords = response.zoneKeyHierarchyRecords.compactMap { CKRecord($0) } reply(egoPeerID, keyHierarchyRecords, nil) } catch { os_log("JoinWithVoucher failed: %@", log: tplogDebug, String(describing: error)) reply(nil, [], error) } } } } } } } } func requestHealthCheck(requiresEscrowCheck: Bool, reply: @escaping (Bool, Bool, Bool, Error?) -> Void) { self.semaphore.wait() let reply: (Bool, Bool, Bool, Error?) -> Void = { os_log("health check complete: %@", log: tplogTrace, type: .info, traceError($3)) self.semaphore.signal() reply($0, $1, $2, $3) } os_log("requestHealthCheck requiring escrow check: %d", log: tplogDebug, type: .default, requiresEscrowCheck) self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID else { // No identity, nothing to do os_log("requestHealthCheck: No identity.", log: tplogDebug, type: .default) reply(false, false, false, ContainerError.noPreparedIdentity) return } let request = GetRepairActionRequest.with { $0.peerID = egoPeerID $0.requiresEscrowCheck = requiresEscrowCheck } self.cuttlefish.getRepairAction(request) { response, error in guard error == nil else { reply(false, false, false, error) return } guard let action = response?.repairAction else { os_log("repair response is empty, returning false: %@", log: tplogDebug, type: .default) reply(false, false, false, nil) return } var postRepairAccount: Bool = false var postRepairEscrow: Bool = false var resetOctagon: Bool = false switch action { case .noAction: break case .postRepairAccount: postRepairAccount = true break case .postRepairEscrow: postRepairEscrow = true break case .resetOctagon: resetOctagon = true break case .UNRECOGNIZED: break } reply(postRepairAccount, postRepairEscrow, resetOctagon, nil) } } } func preflightPreapprovedJoin(reply: @escaping (Bool, Error?) -> Void) { self.semaphore.wait() let reply: (Bool, Error?) -> Void = { os_log("preflightPreapprovedJoin complete: %@", log: tplogTrace, type: .info, traceError($1)) self.semaphore.signal() reply($0, $1) } self.fetchAndPersistChanges { error in guard error == nil else { os_log("preflightPreapprovedJoin unable to fetch changes: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "") reply(false, error) return } // We explicitly ignore the machine ID list here; we're only interested in the peer states: do they preapprove us? guard !self.model.allPeerIDs().isEmpty else { // If, after fetch and handle changes, there's no peers, then we can likely establish. reply(true, nil) return } guard let egoPeerID = self.containerMO.egoPeerID, let egoPermData = self.containerMO.egoPeerPermanentInfo, let egoPermSig = self.containerMO.egoPeerPermanentInfoSig else { os_log("preflightPreapprovedJoin: no prepared identity", log: tplogDebug, type: .debug) reply(false, ContainerError.noPreparedIdentity) return } let keyFactory = TPECPublicKeyFactory() guard let egoPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else { os_log("preflightPreapprovedJoin: invalid permanent info", log: tplogDebug, type: .debug) reply(false, ContainerError.invalidPermanentInfoOrSig) return } guard self.model.hasPotentiallyTrustedPeerPreapprovingKey(egoPermanentInfo.signingPubKey.spki()) else { os_log("preflightPreapprovedJoin: no peers preapprove our key", log: tplogDebug, type: .debug) reply(false, ContainerError.noPeersPreapprovePreparedIdentity) return } reply(true, nil) } } func preapprovedJoin(ckksKeys: [CKKSKeychainBackedKeySet], tlkShares: [CKKSTLKShare], preapprovedKeys: [Data]?, reply: @escaping (String?, [CKRecord], Error?) -> Void) { self.semaphore.wait() let reply: (String?, [CKRecord], Error?) -> Void = { os_log("preapprovedJoin complete: %@", log: tplogTrace, type: .info, traceError($2)) self.semaphore.signal() reply($0, $1, $2) } self.fetchAndPersistChangesIfNeeded { error in guard error == nil else { os_log("preapprovedJoin unable to fetch changes: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "") reply(nil, [], error) return } self.moc.performAndWait { // If, after fetch and handle changes, there's no peers, then fire off an establish // Note that if the establish fails, retrying this call might work. // That's up to the caller. if self.model.allPeerIDs().isEmpty { os_log("preapprovedJoin but no existing peers, attempting establish", log: tplogDebug, type: .debug) self.onqueueEstablish(ckksKeys: ckksKeys, tlkShares: tlkShares, preapprovedKeys: preapprovedKeys, reply: reply) return } // Fetch ego peer identity from local storage. guard let egoPeerID = self.containerMO.egoPeerID, let egoPermData = self.containerMO.egoPeerPermanentInfo, let egoPermSig = self.containerMO.egoPeerPermanentInfoSig, let egoStableData = self.containerMO.egoPeerStableInfo, let egoStableSig = self.containerMO.egoPeerStableInfoSig else { os_log("preapprovedJoin: no prepared identity", log: tplogDebug, type: .debug) reply(nil, [], ContainerError.noPreparedIdentity) return } let keyFactory = TPECPublicKeyFactory() guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: keyFactory) else { reply(nil, [], ContainerError.invalidPermanentInfoOrSig) return } guard let selfStableInfo = TPPeerStableInfo(data: egoStableData, sig: egoStableSig) else { reply(nil, [], ContainerError.invalidStableInfoOrSig) return } guard self.onqueueMachineIDAllowedByIDMS(machineID: selfPermanentInfo.machineID) else { os_log("preapprovedJoin: self machineID %@ (me) not on list", log: tplogDebug, type: .debug, selfPermanentInfo.machineID) self.onqueueTTRUntrusted() reply(nil, [], ContainerError.preparedIdentityNotOnAllowedList(selfPermanentInfo.machineID)) return } loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in guard let egoPeerKeys = egoPeerKeys else { os_log("preapprovedJoin: Don't have my own keys: can't join", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, [], error) return } guard self.model.hasPotentiallyTrustedPeerPreapprovingKey(egoPeerKeys.signingKey.publicKey().spki()) else { os_log("preapprovedJoin: no peers preapprove our key", log: tplogDebug, type: .debug) reply(nil, [], ContainerError.noPeersPreapprovePreparedIdentity) return } self.moc.performAndWait { let peer: Peer let newDynamicInfo: TPPeerDynamicInfo do { (peer, newDynamicInfo) = try self.onqueuePreparePeerForJoining(egoPeerID: egoPeerID, peerPermanentInfo: selfPermanentInfo, stableInfo: selfStableInfo, sponsorID: nil, preapprovedKeys: preapprovedKeys, vouchers: [], egoPeerKeys: egoPeerKeys) } catch { os_log("Unable to create peer for joining: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, [], error) return } let allTLKShares: [TLKShare] let viewKeys: [ViewKeys] do { (viewKeys, allTLKShares) = try self.makeSharesForNewKeySets(ckksKeys: ckksKeys, tlkShares: tlkShares, egoPeerKeys: egoPeerKeys, egoPeerDynamicInfo: newDynamicInfo, epoch: Int(selfPermanentInfo.epoch)) } catch { os_log("Unable to process keys before joining: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, [], error) return } var bottle: Bottle do { bottle = try self.assembleBottle(egoPeerID: egoPeerID) } catch { reply(nil, [], error) return } os_log("Beginning preapprovedJoin for peer %@", log: tplogDebug, type: .default, egoPeerID) os_log("preapprovedJoin permanentInfo: %@", log: tplogDebug, type: .debug, egoPermData.base64EncodedString()) os_log("preapprovedJoin permanentInfoSig: %@", log: tplogDebug, type: .debug, egoPermSig.base64EncodedString()) os_log("preapprovedJoin stableInfo: %@", log: tplogDebug, type: .debug, egoStableData.base64EncodedString()) os_log("preapprovedJoin stableInfoSig: %@", log: tplogDebug, type: .debug, egoStableSig.base64EncodedString()) os_log("preapprovedJoin dynamicInfo: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.peerDynamicInfo.base64EncodedString()) os_log("preapprovedJoin dynamicInfoSig: %@", log: tplogDebug, type: .debug, peer.dynamicInfoAndSig.sig.base64EncodedString()) os_log("preapprovedJoin vouchers: %@", log: tplogDebug, type: .debug, peer.vouchers.map { $0.voucher.base64EncodedString() }) os_log("preapprovedJoin voucher signatures: %@", log: tplogDebug, type: .debug, peer.vouchers.map { $0.sig.base64EncodedString() }) os_log("preapprovedJoin: uploading %d tlk shares", log: tplogDebug, type: .default, allTLKShares.count) do { os_log("preapprovedJoin peer: %@", log: tplogDebug, type: .debug, try peer.serializedData().base64EncodedString()) } catch { os_log("preapprovedJoin unable to encode peer: %@", log: tplogDebug, type: .debug, error as CVarArg) } let changeToken = self.containerMO.changeToken ?? "" let request = JoinWithVoucherRequest.with { $0.changeToken = changeToken $0.peer = peer $0.bottle = bottle $0.tlkShares = allTLKShares $0.viewKeys = viewKeys } self.cuttlefish.joinWithVoucher(request) { response, error in os_log("preapprovedJoin(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("preapprovedJoin failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, [], error ?? ContainerError.cloudkitResponseMissing) return } self.moc.performAndWait { do { self.containerMO.egoPeerStableInfo = peer.stableInfoAndSig.peerStableInfo self.containerMO.egoPeerStableInfoSig = peer.stableInfoAndSig.sig try self.onQueuePersist(changes: response.changes) os_log("preapprovedJoin succeeded", log: tplogDebug) let keyHierarchyRecords = response.zoneKeyHierarchyRecords.compactMap { CKRecord($0) } reply(egoPeerID, keyHierarchyRecords, nil) } catch { os_log("preapprovedJoin failed: %@", log: tplogDebug, String(describing: error)) reply(nil, [], error) } } } } } } } } func update(deviceName: String?, serialNumber: String?, osVersion: String?, policyVersion: UInt64?, policySecrets: [String: Data]?, reply: @escaping (TrustedPeersHelperPeerState?, Error?) -> Void) { self.semaphore.wait() let reply: (TrustedPeersHelperPeerState?, Error?) -> Void = { os_log("update complete: %@", log: tplogTrace, type: .info, traceError($1)) self.semaphore.signal() reply($0, $1) } // Get (and save) the latest from cuttlefish let stableChanges = StableChanges(deviceName: deviceName, serialNumber: serialNumber, osVersion: osVersion, policyVersion: policyVersion, policySecrets: policySecrets, recoverySigningPubKey: nil, recoveryEncryptionPubKey: nil) self.fetchChangesAndUpdateTrustIfNeeded(stableChanges: stableChanges, reply: reply) } func set(preapprovedKeys: [Data], reply: @escaping (Error?) -> Void) { self.semaphore.wait() let reply: (Error?) -> Void = { os_log("setPreapprovedKeys complete: %@", log: tplogTrace, type: .info, traceError($0)) self.semaphore.signal() reply($0) } self.moc.performAndWait { os_log("setPreapprovedKeys: %@", log: tplogDebug, type: .default, preapprovedKeys) guard let egoPeerID = self.containerMO.egoPeerID else { // No identity, nothing to do os_log("setPreapprovedKeys: No identity.", log: tplogDebug, type: .default) reply(ContainerError.noPreparedIdentity) return } loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: egoPeerID)) { signingKeyPair, error in guard let signingKeyPair = signingKeyPair else { os_log("setPreapprovedKeys: no signing key pair: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(error ?? ContainerError.unableToCreateKeyPair) return } self.moc.performAndWait { let dynamicInfo: TPPeerDynamicInfo do { dynamicInfo = try self.model.calculateDynamicInfoForPeer(withID: egoPeerID, addingPeerIDs: nil, removingPeerIDs: nil, preapprovedKeys: preapprovedKeys, signing: signingKeyPair, currentMachineIDs: self.onqueueCurrentMIDList()) } catch { os_log("setPreapprovedKeys: couldn't calculate dynamic info: %@", log: tplogDebug, type: .default, error as CVarArg) reply(error) return } os_log("setPreapprovedKeys: produced a dynamicInfo: %@", log: tplogDebug, type: .default, dynamicInfo) if dynamicInfo == self.model.peer(withID: egoPeerID)?.dynamicInfo { os_log("setPreapprovedKeys: no change; nothing to do.", log: tplogDebug, type: .default) reply(nil) return } os_log("setPreapprovedKeys: attempting updateTrust for %@ with: %@", log: tplogDebug, type: .default, egoPeerID, dynamicInfo) let request = UpdateTrustRequest.with { $0.changeToken = self.containerMO.changeToken ?? "" $0.peerID = egoPeerID $0.dynamicInfoAndSig = SignedPeerDynamicInfo(dynamicInfo) } self.cuttlefish.updateTrust(request) { response, error in os_log("setPreapprovedKeys(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("setPreapprovedKeys failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(error ?? ContainerError.cloudkitResponseMissing) return } os_log("setPreapprovedKeys: updateTrust suceeded", log: tplogDebug, type: .default) do { try self.persist(changes: response.changes) } catch { os_log("setPreapprovedKeys: could not persist changes: %@", log: tplogDebug, type: .default, error as CVarArg) reply(error) return } reply(nil) } } } } } func updateTLKs(ckksKeys: [CKKSKeychainBackedKeySet], tlkShares: [CKKSTLKShare], reply: @escaping ([CKRecord]?, Error?) -> Void) { self.semaphore.wait() let reply: ([CKRecord]?, Error?) -> Void = { os_log("updateTLKs complete: %@", log: tplogTrace, type: .info, traceError($1)) self.semaphore.signal() reply($0, $1) } os_log("Uploading some new TLKs: %@", log: tplogDebug, type: .default, ckksKeys) self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID, let egoPermData = self.containerMO.egoPeerPermanentInfo, let egoPermSig = self.containerMO.egoPeerPermanentInfoSig else { os_log("Have no self identity, can't make tlk shares", log: tplogDebug, type: .default) reply(nil, ContainerError.noPreparedIdentity) return } guard let selfPermanentInfo = TPPeerPermanentInfo(peerID: egoPeerID, data: egoPermData, sig: egoPermSig, keyFactory: TPECPublicKeyFactory()) else { os_log("Couldn't parse self identity", log: tplogDebug, type: .default, ckksKeys) reply(nil, ContainerError.invalidPermanentInfoOrSig) return } loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, error in guard let egoPeerKeys = egoPeerKeys else { os_log("Don't have my own peer keys; can't upload new TLKs: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "error missing") reply(nil, error) return } self.moc.performAndWait { guard let egoPeerDynamicInfo = self.model.getDynamicInfoForPeer(withID: egoPeerID) else { os_log("Unable to fetch dynamic info for self", log: tplogDebug, type: .default) reply(nil, ContainerError.missingDynamicInfo) return } let allTLKShares: [TLKShare] let viewKeys: [ViewKeys] do { (viewKeys, allTLKShares) = try self.makeSharesForNewKeySets(ckksKeys: ckksKeys, tlkShares: tlkShares, egoPeerKeys: egoPeerKeys, egoPeerDynamicInfo: egoPeerDynamicInfo, epoch: Int(selfPermanentInfo.epoch)) } catch { os_log("Unable to process keys before uploading: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, error) return } let request = UpdateTrustRequest.with { $0.changeToken = self.containerMO.changeToken ?? "" $0.peerID = egoPeerID $0.tlkShares = allTLKShares $0.viewKeys = viewKeys } self.cuttlefish.updateTrust(request) { response, error in os_log("UpdateTrust(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard error == nil else { reply(nil, error) return } let keyHierarchyRecords = response?.zoneKeyHierarchyRecords.compactMap { CKRecord($0) } ?? [] os_log("Recevied %d CKRecords back", log: tplogDebug, type: .default, keyHierarchyRecords.count) reply(keyHierarchyRecords, nil) } } } } } func getState(reply: @escaping (ContainerState) -> Void) { self.semaphore.wait() let reply: (ContainerState) -> Void = { os_log("getState complete: %@", log: tplogTrace, type: .info, $0.egoPeerID ?? "") self.semaphore.signal() reply($0) } self.moc.performAndWait { var state = ContainerState() state.egoPeerID = self.containerMO.egoPeerID if self.containerMO.bottles != nil { self.containerMO.bottles!.forEach { bottle in state.bottles.insert(bottle as! BottleMO) } } self.model.allPeers().forEach { peer in state.peers[peer.peerID] = peer } state.vouchers = Array(self.model.allVouchers()) reply(state) } } // This will only fetch changes if no changes have ever been fetched before private func fetchAndPersistChangesIfNeeded(reply: @escaping (Error?) -> Void) { self.moc.performAndWait { if self.containerMO.changeToken == nil { self.onqueueFetchAndPersistChanges(reply: reply) } else { reply(nil) } } } private func fetchAndPersistChanges(reply: @escaping (Error?) -> Void) { self.moc.performAndWait { self.onqueueFetchAndPersistChanges(reply: reply) } } private func onqueueFetchAndPersistChanges(reply: @escaping (Error?) -> Void) { let request = FetchChangesRequest.with { $0.changeToken = self.containerMO.changeToken ?? "" } os_log("Fetching with change token: %@", log: tplogDebug, type: .default, request.changeToken.count > 0 ? request.changeToken : "empty") self.cuttlefish.fetchChanges(request) { response, error in os_log("FetchChanges(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { switch error { case CuttlefishErrorMatcher(code: CuttlefishErrorCode.changeTokenExpired): os_log("change token is expired; resetting local CK storage", log: tplogDebug, type: .default) self.moc.performAndWait { do { try self.deleteLocalCloudKitData() } catch { os_log("Failed to reset local data: %@", log: tplogDebug, type: .default, error as CVarArg) reply(error) return } self.fetchAndPersistChanges(reply: reply) } return default: os_log("Fetch error is an unknown error: %@", log: tplogDebug, type: .default, String(describing: error)) } os_log("Could not fetch changes: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(error) return } do { try self.persist(changes: response.changes) } catch { os_log("Could not persist changes: %@", log: tplogDebug, type: .default, error as CVarArg) reply(error) return } if response.changes.more { os_log("persist: More changes indicated. Fetching...", log: tplogDebug, type: .default) self.fetchAndPersistChanges(reply: reply) return } else { os_log("persist: no more changes!", log: tplogDebug, type: .default) reply(nil) } } } // // Check for delta update in trust lists, that should lead to update of TLKShares // private func haveTrustMemberChanges(newDynamicInfo: TPPeerDynamicInfo, oldDynamicInfo: TPPeerDynamicInfo?) -> Bool { guard let oldDynamicInfo = oldDynamicInfo else { return true } if (newDynamicInfo.includedPeerIDs != oldDynamicInfo.includedPeerIDs) { return true } if (newDynamicInfo.excludedPeerIDs != oldDynamicInfo.excludedPeerIDs) { return true } if (newDynamicInfo.preapprovals != oldDynamicInfo.preapprovals) { return true } return false } // Fetch and persist changes from Cuttlefish. If this results // in us calculating a new dynamicInfo then give the new dynamicInfo // to Cuttlefish. If Cuttlefish returns more changes then persist // them locally, update dynamicInfo if needed, and keep doing that // until dynamicInfo is unchanged and there are no more changes to fetch. // // Must be holding the semaphore to call this, and it remains // the caller's responsibility to release it after it completes // (i.e. after reply is invoked). internal func fetchChangesAndUpdateTrustIfNeeded(stableChanges: StableChanges? = nil, changesPending: Bool = false, reply: @escaping (TrustedPeersHelperPeerState?, Error?) -> Void) { self.fetchAndPersistChanges { error in if let error = error { os_log("fetchChangesAndUpdateTrustIfNeeded: fetching failed: %@", log: tplogDebug, type: .default, error as CVarArg) reply(nil, error) return } self.updateTrustIfNeeded(stableChanges: stableChanges, changesPending: changesPending, reply: reply) } } // If this results in us calculating a new dynamicInfo then, // upload the new dynamicInfo // to Cuttlefish. If Cuttlefish returns more changes then persist // them locally, update dynamicInfo if needed, and keep doing that // until dynamicInfo is unchanged and there are no more changes to fetch. // // Must be holding the semaphore to call this, and it remains // the caller's responsibility to release it after it completes // (i.e. after reply is invoked). private func updateTrustIfNeeded(stableChanges: StableChanges? = nil, changesPending: Bool = false, reply: @escaping (TrustedPeersHelperPeerState?, Error?) -> Void) { self.moc.performAndWait { guard let egoPeerID = self.containerMO.egoPeerID else { // No identity, nothing to do os_log("updateTrustIfNeeded: No identity.", log: tplogDebug, type: .default) reply(TrustedPeersHelperPeerState(peerID: nil, isPreapproved: false, status: .unknown, memberChanges: changesPending, unknownMachineIDs: false, osVersion: nil), nil) return } loadEgoKeyPair(identifier: signingKeyIdentifier(peerID: egoPeerID)) { signingKeyPair, error in guard let signingKeyPair = signingKeyPair else { os_log("updateTrustIfNeeded: no signing key pair: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(TrustedPeersHelperPeerState(peerID: nil, isPreapproved: false, status: .unknown, memberChanges: changesPending, unknownMachineIDs: false, osVersion: nil), error) return } guard self.model.hasPeer(withID: egoPeerID) else { // Not in circle, nothing to do let isPreapproved = self.model.hasPotentiallyTrustedPeerPreapprovingKey(signingKeyPair.publicKey().spki()) os_log("updateTrustIfNeeded: ego peer is not in model, is %@", log: tplogDebug, type: .default, isPreapproved ? "preapproved" : "not yet preapproved") reply(TrustedPeersHelperPeerState(peerID: egoPeerID, isPreapproved: isPreapproved, status: .unknown, memberChanges: changesPending, unknownMachineIDs: false, osVersion: nil), nil) return } self.moc.performAndWait { let dynamicInfo: TPPeerDynamicInfo var stableInfo: TPPeerStableInfo? do { // FIXME We should be able to calculate the contents of dynamicInfo without the signingKeyPair, // and then only load the key if it has changed and we need to sign a new one. This would also // help make our detection of change immune from non-canonical serialization of dynamicInfo. dynamicInfo = try self.model.calculateDynamicInfoForPeer(withID: egoPeerID, addingPeerIDs: nil, removingPeerIDs: nil, preapprovedKeys: nil, signing: signingKeyPair, currentMachineIDs: self.onqueueCurrentMIDList()) stableInfo = try self.createNewStableInfoIfNeeded(stableChanges: stableChanges, egoPeerID: egoPeerID, dynamicInfo: dynamicInfo, signingKeyPair: signingKeyPair) } catch { os_log("updateTrustIfNeeded: couldn't calculate dynamic info: %@", log: tplogDebug, type: .default, error as CVarArg) reply(TrustedPeersHelperPeerState(peerID: egoPeerID, isPreapproved: false, status: self.model.statusOfPeer(withID: egoPeerID), memberChanges: changesPending, unknownMachineIDs: false, osVersion: nil), error) return } os_log("updateTrustIfNeeded: produced a stableInfo: %@", log: tplogDebug, type: .default, String(describing: stableInfo)) os_log("updateTrustIfNeeded: produced a dynamicInfo: %@", log: tplogDebug, type: .default, dynamicInfo) let peer = self.model.peer(withID: egoPeerID) if (stableInfo == nil || stableInfo == peer?.stableInfo) && dynamicInfo == peer?.dynamicInfo { os_log("updateTrustIfNeeded: complete.", log: tplogDebug, type: .default) // No change to the dynamicInfo: update the MID list now that we've reached a steady state do { self.onqueueUpdateMachineIDListFromModel(dynamicInfo: dynamicInfo) try self.moc.save() } catch { os_log("updateTrustIfNeeded: unable to remove untrusted MachineIDs: %@", log: tplogDebug, type: .default, error as CVarArg) } reply(TrustedPeersHelperPeerState(peerID: egoPeerID, isPreapproved: false, status: self.model.statusOfPeer(withID: egoPeerID), memberChanges: changesPending, unknownMachineIDs: self.onqueueFullIDMSListWouldBeHelpful(), osVersion: peer?.stableInfo?.osVersion), nil) return } // Check if we change that should trigger a notification that should trigger TLKShare updates let haveChanges = changesPending || self.haveTrustMemberChanges(newDynamicInfo: dynamicInfo, oldDynamicInfo: peer?.dynamicInfo) let signedDynamicInfo = SignedPeerDynamicInfo(dynamicInfo) os_log("updateTrustIfNeeded: attempting updateTrust for %@ with: %@", log: tplogDebug, type: .default, egoPeerID, dynamicInfo) var request = UpdateTrustRequest.with { $0.changeToken = self.containerMO.changeToken ?? "" $0.peerID = egoPeerID $0.dynamicInfoAndSig = signedDynamicInfo } if let stableInfo = stableInfo { request.stableInfoAndSig = SignedPeerStableInfo(stableInfo) } self.cuttlefish.updateTrust(request) { response, error in os_log("UpdateTrust(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard let response = response, error == nil else { os_log("UpdateTrust failed: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error") reply(nil, error ?? ContainerError.cloudkitResponseMissing) return } do { try self.persist(changes: response.changes) } catch { os_log("updateTrust failed: %@", log: tplogDebug, String(describing: error)) reply(nil, error) return } if response.changes.more { self.fetchChangesAndUpdateTrustIfNeeded(stableChanges: stableChanges, changesPending: haveChanges, reply: reply) } else { self.updateTrustIfNeeded(stableChanges: stableChanges, changesPending: haveChanges, reply: reply) } } } } } } private func persist(changes: Changes) throws { // This is some nonsense: I can't figure out how to tell swift to throw an exception across performAndWait. // So, do it ourself var outsideBlockError: Error? self.moc.performAndWait { os_log("persist: Received %d peer differences, more: %d", log: tplogDebug, type: .default, changes.differences.count, changes.more) os_log("persist: New change token: %@", log: tplogDebug, type: .default, changes.changeToken) do { try self.onQueuePersist(changes: changes) } catch { outsideBlockError = error } } if let outsideBlockError = outsideBlockError { throw outsideBlockError } } // Must be on moc queue to call this. // Changes are registered in the model and stored in moc. private func onQueuePersist(changes: Changes) throws { self.containerMO.changeToken = changes.changeToken self.containerMO.moreChanges = changes.more if changes.differences.count > 0 { self.model.clearViableBottles() } try changes.differences.forEach { peerDifference in if let operation = peerDifference.operation { switch operation { case .add(let peer): try self.addOrUpdate(peer: peer) case .update(let peer): try self.addOrUpdate(peer: peer) // Update containerMO ego data if it has changed. if peer.peerID == self.containerMO.egoPeerID { guard let stableInfoAndSig: TPPeerStableInfo = peer.stableInfoAndSig.toStableInfo() else { break } self.containerMO.egoPeerStableInfo = stableInfoAndSig.data self.containerMO.egoPeerStableInfoSig = stableInfoAndSig.sig } case .remove(let peer): self.model.deletePeer(withID: peer.peerID) if let peerMO = try self.fetchPeerMO(peerID: peer.peerID) { self.moc.delete(peerMO) } } } } let signingKey = changes.recoverySigningPubKey let encryptionKey = changes.recoveryEncryptionPubKey if signingKey.count > 0 && encryptionKey.count > 0 { self.addOrUpdate(signingKey: signingKey, encryptionKey: encryptionKey) } try self.moc.save() } // Must be on moc queue to call this // Deletes all things that should come back from CloudKit. Keeps the egoPeer data, though. private func deleteLocalCloudKitData() throws { os_log("Deleting all CloudKit data", log: tplogDebug, type: .default) do { let peerRequest = NSFetchRequest(entityName: "Peer") peerRequest.predicate = NSPredicate(format: "container == %@", self.containerMO) try self.moc.execute(NSBatchDeleteRequest(fetchRequest: peerRequest)) let bottleRequest = NSFetchRequest(entityName: "Bottle") bottleRequest.predicate = NSPredicate(format: "container == %@", self.containerMO) try self.moc.execute(NSBatchDeleteRequest(fetchRequest: bottleRequest)) self.containerMO.peers = nil self.containerMO.bottles = nil self.containerMO.changeToken = nil self.containerMO.moreChanges = false self.model = Container.loadModel(from: self.containerMO) try self.moc.save() } catch { os_log("Local delete failed: %@", log: tplogDebug, type: .default, error as CVarArg) throw error } os_log("Saved model with %d peers", log: tplogDebug, type: .default, self.model.allPeerIDs().count) } // Must be on moc queue to call this. private func addOrUpdate(signingKey: Data, encryptionKey: Data) { self.model.setRecoveryKeys( TPRecoveryKeyPair(signingSPKI: signingKey, encryptionSPKI: encryptionKey)) } // Must be on moc queue to call this. private func addOrUpdate(peer: Peer) throws { if !self.model.hasPeer(withID: peer.peerID) { // Add: guard let permanentInfo = peer.permanentInfoAndSig.toPermanentInfo(peerID: peer.peerID) else { // Ignoring bad peer return } let stableInfo = peer.stableInfoAndSig.toStableInfo() let dynamicInfo = peer.dynamicInfoAndSig.toDynamicInfo() let vouchers = peer.vouchers.compactMap { TPVoucher(infoWith: $0.voucher, sig: $0.sig) } let isEgoPeer = peer.peerID == self.containerMO.egoPeerID try self.registerPeerMO(permanentInfo: permanentInfo, stableInfo: stableInfo, dynamicInfo: dynamicInfo, vouchers: vouchers, isEgoPeer: isEgoPeer) } else { // Update: // The assertion here is that every peer registered in model is also present in containerMO let peerMO = try self.fetchPeerMO(peerID: peer.peerID)! if let stableInfo = peer.stableInfoAndSig.toStableInfo() { try self.model.update(stableInfo, forPeerWithID: peer.peerID) // Pull the stableInfo back out of the model, and persist that. // The model checks signatures and clocks to prevent replay attacks. let modelPeer = self.model.peer(withID: peer.peerID) peerMO.stableInfo = modelPeer?.stableInfo?.data peerMO.stableInfoSig = modelPeer?.stableInfo?.sig } if let dynamicInfo = peer.dynamicInfoAndSig.toDynamicInfo() { try self.model.update(dynamicInfo, forPeerWithID: peer.peerID) // Pull the dynamicInfo back out of the model, and persist that. // The model checks signatures and clocks to prevent replay attacks. let modelPeer = self.model.peer(withID: peer.peerID) peerMO.dynamicInfo = modelPeer?.dynamicInfo?.data peerMO.dynamicInfoSig = modelPeer?.dynamicInfo?.sig } peer.vouchers.forEach { if let voucher = TPVoucher(infoWith: $0.voucher, sig: $0.sig) { self.model.register(voucher) let voucherMO = VoucherMO(context: self.moc) voucherMO.voucherInfo = voucher.data voucherMO.voucherInfoSig = voucher.sig peerMO.addToVouchers(voucherMO) } } } } // Must be on moc queue to call this. private func fetchPeerMO(peerID: String) throws -> PeerMO? { let fetch = NSFetchRequest(entityName: "Peer") fetch.predicate = NSPredicate(format: "peerID == %@ && container == %@", peerID, self.containerMO) let peers = try self.moc.fetch(fetch) return peers.first as? PeerMO } // Must be on moc queue to call this. private func getPolicyDoc(_ policyVersion: UInt64) throws -> TPPolicyDocument { guard let policyDoc = self.model.policy(withVersion: policyVersion) else { throw ContainerError.unknownPolicyVersion(policyVersion) } assert(policyVersion == policyDoc.policyVersion) if policyVersion == prevailingPolicyVersion { assert(policyDoc.policyHash == prevailingPolicyHash) } return policyDoc } // Must be on moc queue to call this. private func createNewStableInfoIfNeeded(stableChanges: StableChanges?, egoPeerID: String, dynamicInfo: TPPeerDynamicInfo, signingKeyPair: _SFECKeyPair) throws -> TPPeerStableInfo? { func noChange(_ change: T?, _ existing: T?) -> Bool { return (nil == change) || change == existing } let existingStableInfo = self.model.peer(withID: egoPeerID)?.stableInfo if noChange(stableChanges?.deviceName, existingStableInfo?.deviceName) && noChange(stableChanges?.serialNumber, existingStableInfo?.serialNumber) && noChange(stableChanges?.osVersion, existingStableInfo?.osVersion) && noChange(stableChanges?.policyVersion, existingStableInfo?.policyVersion) && noChange(stableChanges?.policySecrets, existingStableInfo?.policySecrets) && noChange(stableChanges?.recoverySigningPubKey, existingStableInfo?.recoverySigningPublicKey) && noChange(stableChanges?.recoveryEncryptionPubKey, existingStableInfo?.recoveryEncryptionPublicKey) && self.model.doesPeerRecoveryKeyMatchPeers(egoPeerID) { return nil } let policyHash: String? if let policyVersion = stableChanges?.policyVersion { let policyDoc = try self.getPolicyDoc(policyVersion) policyHash = policyDoc.policyHash } else { policyHash = nil } // Determine which recovery key we'd like to be using, given our current idea of who to trust let newRecoveryKeys = self.model.bestRecoveryKey(with: dynamicInfo) return try self.model.createStableInfo(withPolicyVersion: stableChanges?.policyVersion ?? existingStableInfo?.policyVersion ?? prevailingPolicyVersion, policyHash: policyHash ?? existingStableInfo?.policyHash ?? prevailingPolicyHash, policySecrets: stableChanges?.policySecrets ?? existingStableInfo?.policySecrets, deviceName: stableChanges?.deviceName ?? existingStableInfo?.deviceName ?? "", serialNumber: stableChanges?.serialNumber ?? existingStableInfo?.serialNumber ?? "", osVersion: stableChanges?.osVersion ?? existingStableInfo?.osVersion ?? "", signing: signingKeyPair, recoverySigningPubKey: newRecoveryKeys?.signingSPKI ?? existingStableInfo?.recoverySigningPublicKey, recoveryEncryptionPubKey: newRecoveryKeys?.encryptionSPKI ?? existingStableInfo?.recoveryEncryptionPublicKey) } private func assembleBottle(egoPeerID: String) throws -> Bottle { let bottleMoSet = containerMO.bottles as? Set var bottleMOs = bottleMoSet?.filter { $0.peerID == egoPeerID } if let count = bottleMOs?.count { if count > 1 { throw ContainerError.tooManyBottlesForPeer } else if count == 0 { throw ContainerError.noBottleForPeer } } else { throw ContainerError.failedToAssembleBottle } let BM: BottleMO? = (bottleMOs?.removeFirst()) let bottle = try Bottle.with { $0.peerID = egoPeerID if let bottledPeerData = BM?.contents { $0.contents = bottledPeerData } else { throw ContainerError.failedToAssembleBottle } if let bottledPeerEscrowSigningSPKI = BM?.escrowedSigningSPKI { $0.escrowedSigningSpki = bottledPeerEscrowSigningSPKI } else { throw ContainerError.failedToAssembleBottle } if let bottledPeerSignatureUsingEscrowKey = BM?.signatureUsingEscrowKey { $0.signatureUsingEscrowKey = bottledPeerSignatureUsingEscrowKey } else { throw ContainerError.failedToAssembleBottle } if let bottledPeerSignatureUsingPeerKey = BM?.signatureUsingPeerKey { $0.signatureUsingPeerKey = bottledPeerSignatureUsingPeerKey } else { throw ContainerError.failedToAssembleBottle } if let bID = BM?.bottleID { $0.bottleID = bID } else { throw ContainerError.failedToAssembleBottle } } return bottle } func reportHealth(request: ReportHealthRequest, reply: @escaping (Error?) -> Void) { self.semaphore.wait() let reply: (Error?) -> Void = { os_log("reportHealth complete %@", log: tplogTrace, type: .info, traceError($0)) self.semaphore.signal() reply($0) } var updatedRequest = request if let egoPeerID = self.containerMO.egoPeerID { loadEgoKeys(peerID: egoPeerID) { egoPeerKeys, _ in updatedRequest.octagonEgoIdentity = (egoPeerKeys != nil) } } self.moc.performAndWait { self.cuttlefish.reportHealth(updatedRequest) { response, error in os_log("reportHealth(%@): %@, error: %@", log: tplogDebug, "\(String(describing: request))", "\(String(describing: response))", "\(String(describing: error))") guard error == nil else { reply(error) return } reply(nil) } } } func pushHealthInquiry(reply: @escaping (Error?) -> Void) { self.semaphore.wait() let reply: (Error?) -> Void = { os_log("reportHealth complete %@", log: tplogTrace, type: .info, traceError($0)) self.semaphore.signal() reply($0) } self.moc.performAndWait { self.cuttlefish.pushHealthInquiry(PushHealthInquiryRequest()) { response, error in os_log("pushHealthInquiry(): %@, error: %@", log: tplogDebug, "\(String(describing: response))", "\(String(describing: error))") guard error == nil else { reply(error) return } reply(nil) } } } }