--- /dev/null
+/*
+ * 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"
+
+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)
+}
+
+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)"
+ }
+ }
+}
+
+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
+ }
+ }
+
+ 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: kCFBooleanFalse,
+ 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: kCFBooleanFalse,
+ 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, <rdar://problem/39597940>
+ 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)
+ }
+ }
+}
+
+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? = nil
+
+ //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? = nil
+
+ 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<BottleMO>()
+ 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<NSFetchRequestResult>(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<PeerMO>
+ 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<PolicyMO>
+ 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<MachineMO> ?? 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 peerCountsByModelID = self.model.peerCountsByModelID()
+
+ 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,
+ peerCountsByModelID: peerCountsByModelID,
+ 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,
+ peerCountsByModelID: peerCountsByModelID,
+ isExcluded: true,
+ isLocked: false)
+
+ reply(egoStatus, loadError)
+ return
+ }
+
+ let egoStatus = TrustedPeersHelperEgoPeerStatus(egoPeerID: egoPeerID,
+ status: status,
+ peerCountsByModelID: peerCountsByModelID,
+ 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,
+ peerCountsByModelID: peerCountsByModelID,
+ 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,
+ peerCountsByModelID: peerCountsByModelID,
+ 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,
+ peerCountsByModelID: [:],
+ 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.hasPeerPreapprovingKey(permanentInfo.signingPubKey.spki())
+ os_log("fetchTrustState: ego peer is %@", log: tplogDebug, type: .default, isPreapproved ? "preapproved" : "not yet preapproved")
+
+ let egoPeerStatus = TrustedPeersHelperPeerState(peerID: egoPeerID,
+ isPreapproved: isPreapproved,
+ status: self.model.statusOfPeer(withID: egoPeerID),
+ memberChanges: false,
+ unknownMachineIDs: self.onqueueFullIDMSListWouldBeHelpful())
+
+ 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), [], 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<BottleMO> {
+ 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()
+
+ 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(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 request = ResetRequest()
+ 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() -> Void {
+ 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<BottleMO>, bottleID: String) -> (Bool) {
+ let bmos = bottleMOs.filter {
+ $0.bottleID == bottleID
+ }
+ return !bmos.isEmpty
+ }
+
+ func findBottleForEscrowRecordID(bottleID: String, reply: @escaping (BottleMO?, Error?) -> Void) {
+
+ var bmo: BottleMO?
+ var bottles: Set<BottleMO> = []
+ var shouldPerformFetch = false
+
+ if let containerBottles = self.containerMO.bottles as? Set<BottleMO> {
+ 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<BottleMO> 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("findBottleForEscrowRecordID 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 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.findBottleForEscrowRecordID(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: <rdar://problem/47899980> 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<String>,
+ 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<String>,
+ 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<BottleMO> 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<BottleMO> = self.containerMO.bottles as? Set<BottleMO> 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<BottleMO> {
+ let matchingBottles: Set<BottleMO> = 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<BottleMO> {
+ let matchingBottles: Set<BottleMO> = 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.init(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)] ?? "<nil>", 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.hasPeerPreapprovingKey(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.hasPeerPreapprovingKey(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 ?? "<NULL>")
+ 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)
+ }
+
+ 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), 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), error)
+ return
+ }
+ guard self.model.hasPeer(withID: egoPeerID) else {
+ // Not in circle, nothing to do
+ let isPreapproved = self.model.hasPeerPreapprovingKey(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),
+ 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),
+ 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()),
+ 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<NSFetchRequestResult>(entityName: "Peer")
+ peerRequest.predicate = NSPredicate(format: "container == %@", self.containerMO)
+ try self.moc.execute(NSBatchDeleteRequest(fetchRequest: peerRequest))
+
+ let bottleRequest = NSFetchRequest<NSFetchRequestResult>(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<NSFetchRequestResult>(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<T: Equatable>(_ 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<BottleMO>
+
+ 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(HealthInquiryRequest()) { response, error in
+ os_log("pushHealthInquiry(): %@, error: %@", log: tplogDebug, "\(String(describing: response))", "\(String(describing: error))")
+ guard error == nil else {
+ reply(error)
+ return
+ }
+ reply(nil)
+ }
+ }
+ }
+}