FakeCuttlefish.swift
1 // 2 // FakeCuttlefish.swift 3 // Security 4 // 5 // Created by Ben Williamson on 5/23/18. 6 // 7 8 import CloudKitCode 9 import CloudKitCodeProtobuf 10 import Foundation 11 12 enum FakeCuttlefishOpinion { 13 case trusts 14 case trustsByPreapproval 15 case excludes 16 } 17 18 struct FakeCuttlefishAssertion: CustomStringConvertible { 19 let peer: String 20 let opinion: FakeCuttlefishOpinion 21 let target: String 22 23 func check(peer: Peer?, target: Peer?) -> Bool { 24 guard let peer = peer else { 25 return false 26 } 27 28 guard peer.hasDynamicInfoAndSig else { 29 // No opinions? You've failed this assertion. 30 return false 31 } 32 33 let dynamicInfo = TPPeerDynamicInfo(data: peer.dynamicInfoAndSig.peerDynamicInfo, sig: peer.dynamicInfoAndSig.sig) 34 guard let realDynamicInfo = dynamicInfo else { 35 return false 36 } 37 38 let targetPermanentInfo: TPPeerPermanentInfo? = 39 target != nil ? TPPeerPermanentInfo(peerID: self.target, 40 data: target!.permanentInfoAndSig.peerPermanentInfo, 41 sig: target!.permanentInfoAndSig.sig, 42 keyFactory: TPECPublicKeyFactory()) 43 : nil 44 45 switch self.opinion { 46 case .trusts: 47 return realDynamicInfo.includedPeerIDs.contains(self.target) 48 case .trustsByPreapproval: 49 guard let pubSignSPKI = targetPermanentInfo?.signingPubKey.spki() else { 50 return false 51 } 52 let hash = TPHashBuilder.hash(with: .SHA256, of: pubSignSPKI) 53 return realDynamicInfo.preapprovals.contains(hash) 54 case .excludes: 55 return realDynamicInfo.excludedPeerIDs.contains(self.target) 56 } 57 } 58 59 var description: String { 60 return "DCA:(\(self.peer)\(self.opinion)\(self.target))" 61 } 62 } 63 64 @objc 65 class FakeCuttlefishNotify: NSObject { 66 let pushes: (Data) -> Void 67 let containerName: String 68 69 @objc 70 init(_ containerName: String, pushes: @escaping (Data) -> Void) { 71 self.containerName = containerName 72 self.pushes = pushes 73 } 74 75 @objc 76 func notify(_ function: String) throws { 77 let notification: [String: [String: Any]] = [ 78 "aps": ["content-available": 1], 79 "cf": [ 80 "f": function, 81 "c": self.containerName, 82 ], 83 ] 84 let payload: Data 85 do { 86 payload = try JSONSerialization.data(withJSONObject: notification) 87 } catch { 88 throw error 89 } 90 self.pushes(payload) 91 } 92 } 93 94 extension ViewKey { 95 func fakeRecord(zoneID: CKRecordZone.ID) -> CKRecord { 96 let recordID = CKRecord.ID(__recordName: self.uuid, zoneID: zoneID) 97 let record = CKRecord(recordType: SecCKRecordIntermediateKeyType, recordID: recordID) 98 99 record[SecCKRecordWrappedKeyKey] = self.wrappedkeyBase64 100 101 switch self.keyclass { 102 case .tlk: 103 record[SecCKRecordKeyClassKey] = "tlk" 104 case .classA: 105 record[SecCKRecordKeyClassKey] = "classA" 106 case .classC: 107 record[SecCKRecordKeyClassKey] = "classC" 108 case .UNRECOGNIZED: 109 abort() 110 } 111 112 if !self.parentkeyUuid.isEmpty { 113 // TODO: no idea how to tell it about the 'verify' action 114 record[SecCKRecordParentKeyRefKey] = CKRecord.Reference(recordID: CKRecord.ID(__recordName: self.parentkeyUuid, zoneID: zoneID), action: .none) 115 } 116 117 return record 118 } 119 120 func fakeKeyPointer(zoneID: CKRecordZone.ID) -> CKRecord { 121 let recordName: String 122 switch self.keyclass { 123 case .tlk: 124 recordName = "tlk" 125 case .classA: 126 recordName = "classA" 127 case .classC: 128 recordName = "classC" 129 case .UNRECOGNIZED: 130 abort() 131 } 132 133 let recordID = CKRecord.ID(__recordName: recordName, zoneID: zoneID) 134 let record = CKRecord(recordType: SecCKRecordCurrentKeyType, recordID: recordID) 135 136 // TODO: no idea how to tell it about the 'verify' action 137 record[SecCKRecordParentKeyRefKey] = CKRecord.Reference(recordID: CKRecord.ID(__recordName: self.uuid, zoneID: zoneID), action: .none) 138 139 return record 140 } 141 } 142 143 extension TLKShare { 144 func fakeRecord(zoneID: CKRecordZone.ID) -> CKRecord { 145 let recordID = CKRecord.ID(__recordName: "tlkshare-\(self.keyUuid)::\(self.receiver)::\(self.sender)", zoneID: zoneID) 146 let record = CKRecord(recordType: SecCKRecordTLKShareType, recordID: recordID) 147 148 record[SecCKRecordSenderPeerID] = self.sender 149 record[SecCKRecordReceiverPeerID] = self.receiver 150 record[SecCKRecordReceiverPublicEncryptionKey] = self.receiverPublicEncryptionKey 151 record[SecCKRecordCurve] = self.curve 152 record[SecCKRecordVersion] = self.version 153 record[SecCKRecordEpoch] = self.epoch 154 record[SecCKRecordPoisoned] = self.poisoned 155 156 // TODO: no idea how to tell it about the 'verify' action 157 record[SecCKRecordParentKeyRefKey] = CKRecord.Reference(recordID: CKRecord.ID(__recordName: self.keyUuid, zoneID: zoneID), action: .none) 158 159 record[SecCKRecordWrappedKeyKey] = self.wrappedkey 160 record[SecCKRecordSignature] = self.signature 161 162 return record 163 } 164 } 165 166 class FakeCuttlefishServer: CuttlefishAPIAsync { 167 168 struct State { 169 var peersByID: [String: Peer] = [:] 170 var recoverySigningPubKey: Data? 171 var recoveryEncryptionPubKey: Data? 172 var bottles: [Bottle] = [] 173 var escrowRecords: [EscrowInformation] = [] 174 175 var viewKeys: [CKRecordZone.ID: ViewKeys] = [:] 176 var tlkShares: [CKRecordZone.ID: [TLKShare]] = [:] 177 178 init() { 179 } 180 181 func model(updating newPeer: Peer?) throws -> TPModel { 182 let model = TPModel(decrypter: Decrypter()) 183 184 for existingPeer in self.peersByID.values { 185 if let pi = existingPeer.permanentInfoAndSig.toPermanentInfo(peerID: existingPeer.peerID), 186 let si = existingPeer.stableInfoAndSig.toStableInfo(), 187 let di = existingPeer.dynamicInfoAndSig.toDynamicInfo() { 188 model.registerPeer(with: pi) 189 try model.update(si, forPeerWithID: existingPeer.peerID) 190 try model.update(di, forPeerWithID: existingPeer.peerID) 191 } 192 } 193 194 if let peerUpdate = newPeer { 195 // This peer needs to have been in the model before; on pain of throwing here 196 if let si = peerUpdate.stableInfoAndSig.toStableInfo(), 197 let di = peerUpdate.dynamicInfoAndSig.toDynamicInfo() { 198 try model.update(si, forPeerWithID: peerUpdate.peerID) 199 try model.update(di, forPeerWithID: peerUpdate.peerID) 200 } 201 } 202 203 return model 204 } 205 } 206 207 var state = State() 208 var snapshotsByChangeToken: [String: State] = [:] 209 var currentChange: Int = 0 210 var currentChangeToken: String = "" 211 let notify: FakeCuttlefishNotify? 212 213 //var fakeCKZones: [CKRecordZone.ID: FakeCKZone] 214 var fakeCKZones: NSMutableDictionary 215 216 // @property (nullable) NSMutableDictionary<CKRecordZoneID*, ZoneKeys*>* keys; 217 var ckksZoneKeys: NSMutableDictionary 218 219 var injectLegacyEscrowRecords: Bool = false 220 var includeEscrowRecords: Bool = true 221 222 var nextFetchErrors: [Error] = [] 223 var fetchViableBottlesError: [Error] = [] 224 var nextJoinErrors: [Error] = [] 225 var nextUpdateTrustErrors: [Error] = [] 226 var returnNoActionResponse: Bool = false 227 var returnRepairAccountResponse: Bool = false 228 var returnRepairEscrowResponse: Bool = false 229 var returnResetOctagonResponse: Bool = false 230 var returnLeaveTrustResponse: Bool = false 231 var returnRepairErrorResponse: Error? 232 var fetchChangesCalledCount: Int = 0 233 var fetchChangesReturnEmptyResponse: Bool = false 234 235 var fetchViableBottlesEscrowRecordCacheTimeout: TimeInterval = 2.0 236 237 var nextEstablishReturnsMoreChanges: Bool = false 238 239 var establishListener: ((EstablishRequest) -> NSError?)? 240 var updateListener: ((UpdateTrustRequest) -> NSError?)? 241 var fetchChangesListener: ((FetchChangesRequest) -> NSError?)? 242 var joinListener: ((JoinWithVoucherRequest) -> NSError?)? 243 var healthListener: ((GetRepairActionRequest) -> NSError?)? 244 var fetchViableBottlesListener: ((FetchViableBottlesRequest) -> NSError?)? 245 var resetListener: ((ResetRequest) -> NSError?)? 246 var setRecoveryKeyListener: ((SetRecoveryKeyRequest) -> NSError?)? 247 248 // Any policies in here will be returned by FetchPolicy before any inbuilt policies 249 var policyOverlay: [TPPolicyDocument] = [] 250 251 var fetchViableBottlesDontReturnBottleWithID: String? 252 253 init(_ notify: FakeCuttlefishNotify?, ckZones: NSMutableDictionary, ckksZoneKeys: NSMutableDictionary) { 254 self.notify = notify 255 self.fakeCKZones = ckZones 256 self.ckksZoneKeys = ckksZoneKeys 257 } 258 259 func deleteAllPeers() { 260 self.state.peersByID.removeAll() 261 self.makeSnapshot() 262 } 263 264 func pushNotify(_ function: String) { 265 if let notify = self.notify { 266 do { 267 try notify.notify(function) 268 } catch { 269 } 270 } 271 } 272 273 static func makeCloudKitCuttlefishError(code: CuttlefishErrorCode, retryAfter: TimeInterval = 5) -> NSError { 274 let cuttlefishError = CKPrettyError(domain: CuttlefishErrorDomain, 275 code: code.rawValue, 276 userInfo: [CuttlefishErrorRetryAfterKey: retryAfter]) 277 let internalError = CKPrettyError(domain: CKInternalErrorDomain, 278 code: CKInternalErrorCode.errorInternalPluginError.rawValue, 279 userInfo: [NSUnderlyingErrorKey: cuttlefishError, ]) 280 let ckError = CKPrettyError(domain: CKErrorDomain, 281 code: CKError.serverRejectedRequest.rawValue, 282 userInfo: [NSUnderlyingErrorKey: internalError, 283 CKErrorServerDescriptionKey: "Fake: FunctionError domain: CuttlefishError, code: \(code),\(code.rawValue)", 284 ]) 285 return ckError 286 } 287 288 func makeSnapshot() { 289 self.currentChange += 1 290 self.currentChangeToken = "change\(self.currentChange)" 291 self.snapshotsByChangeToken[self.currentChangeToken] = self.state 292 } 293 294 func changesSince(snapshot: State) -> Changes { 295 return Changes.with { changes in 296 changes.changeToken = self.currentChangeToken 297 298 changes.differences = self.state.peersByID.compactMap { (key: String, value: Peer) -> PeerDifference? in 299 let old = snapshot.peersByID[key] 300 if old == nil { 301 return PeerDifference.with { 302 $0.add = value 303 } 304 } else if old != value { 305 return PeerDifference.with { 306 $0.update = value 307 } 308 } else { 309 return nil 310 } 311 } 312 snapshot.peersByID.forEach { (key: String, _: Peer) in 313 if self.state.peersByID[key] == nil { 314 changes.differences.append(PeerDifference.with { 315 $0.remove = Peer.with { 316 $0.peerID = key 317 } 318 }) 319 } 320 } 321 322 if self.state.recoverySigningPubKey != snapshot.recoverySigningPubKey { 323 changes.recoverySigningPubKey = self.state.recoverySigningPubKey ?? Data() 324 } 325 if self.state.recoveryEncryptionPubKey != snapshot.recoveryEncryptionPubKey { 326 changes.recoveryEncryptionPubKey = self.state.recoveryEncryptionPubKey ?? Data() 327 } 328 } 329 } 330 331 func reset(_ request: ResetRequest, completion: @escaping (ResetResponse?, Error?) -> Void) { 332 print("FakeCuttlefish: reset called") 333 if let resetListener = self.resetListener { 334 let possibleError = resetListener(request) 335 guard possibleError == nil else { 336 completion(nil, possibleError) 337 return 338 } 339 } 340 self.state = State() 341 self.makeSnapshot() 342 completion(ResetResponse.with { 343 $0.changes = self.changesSince(snapshot: State()) 344 }, nil) 345 self.pushNotify("reset") 346 } 347 348 func newKeysConflict(viewKeys: [ViewKeys]) -> Bool { 349 #if OCTAGON_TEST_FILL_ZONEKEYS 350 for keys in viewKeys { 351 let rzid = CKRecordZone.ID(zoneName: keys.view) 352 353 if let currentViewKeys = self.ckksZoneKeys[rzid] as? CKKSCurrentKeySet { 354 // Uploading the current view keys is okay. Fail only if they don't match 355 if keys.newTlk.uuid != currentViewKeys.tlk!.uuid || 356 keys.newClassA.uuid != currentViewKeys.classA!.uuid || 357 keys.newClassC.uuid != currentViewKeys.classC!.uuid { 358 return true 359 } 360 } 361 } 362 #endif 363 364 return false 365 } 366 367 func store(viewKeys: [ViewKeys]) -> [CKRecord] { 368 var allRecords: [CKRecord] = [] 369 370 viewKeys.forEach { viewKeys in 371 let rzid = CKRecordZone.ID(zoneName: viewKeys.view) 372 self.state.viewKeys[rzid] = viewKeys 373 374 // Real cuttlefish makes these zones for you 375 if self.fakeCKZones[rzid] == nil { 376 self.fakeCKZones[rzid] = FakeCKZone(zone: rzid) 377 } 378 379 if let fakeZone = self.fakeCKZones[rzid] as? FakeCKZone { 380 fakeZone.queue.sync { 381 let tlkRecord = viewKeys.newTlk.fakeRecord(zoneID: rzid) 382 let classARecord = viewKeys.newClassA.fakeRecord(zoneID: rzid) 383 let classCRecord = viewKeys.newClassC.fakeRecord(zoneID: rzid) 384 385 let tlkPointerRecord = viewKeys.newTlk.fakeKeyPointer(zoneID: rzid) 386 let classAPointerRecord = viewKeys.newClassA.fakeKeyPointer(zoneID: rzid) 387 let classCPointerRecord = viewKeys.newClassC.fakeKeyPointer(zoneID: rzid) 388 389 // Some tests don't link everything needed to make zonekeys 390 // Those tests don't get this nice behavior 391 #if OCTAGON_TEST_FILL_ZONEKEYS 392 let zoneKeys = self.ckksZoneKeys[rzid] as? ZoneKeys ?? ZoneKeys(forZoneName: rzid.zoneName) 393 self.ckksZoneKeys[rzid] = zoneKeys 394 395 zoneKeys.tlk = CKKSKey(ckRecord: tlkRecord) 396 zoneKeys.classA = CKKSKey(ckRecord: classARecord) 397 zoneKeys.classC = CKKSKey(ckRecord: classCRecord) 398 399 zoneKeys.currentTLKPointer = CKKSCurrentKeyPointer(ckRecord: tlkPointerRecord) 400 zoneKeys.currentClassAPointer = CKKSCurrentKeyPointer(ckRecord: classAPointerRecord) 401 zoneKeys.currentClassCPointer = CKKSCurrentKeyPointer(ckRecord: classCPointerRecord) 402 #endif 403 404 let zoneRecords = [tlkRecord, 405 classARecord, 406 classCRecord, 407 tlkPointerRecord, 408 classAPointerRecord, 409 classCPointerRecord, ] 410 // TODO a rolled tlk too 411 412 zoneRecords.forEach { record in 413 fakeZone._onqueueAdd(toZone: record) 414 } 415 allRecords.append(contentsOf: zoneRecords) 416 } 417 } else { 418 // we made the zone above, shoudn't ever get here 419 print("Received an unexpected zone id: \(rzid)") 420 abort() 421 } 422 } 423 return allRecords 424 } 425 426 func store(tlkShares: [TLKShare]) -> [CKRecord] { 427 var allRecords: [CKRecord] = [] 428 429 tlkShares.forEach { share in 430 let rzid = CKRecordZone.ID(zoneName: share.view) 431 432 var c = self.state.tlkShares[rzid] ?? [] 433 c.append(share) 434 self.state.tlkShares[rzid] = c 435 436 if let fakeZone = self.fakeCKZones[rzid] as? FakeCKZone { 437 let record = share.fakeRecord(zoneID: rzid) 438 fakeZone.add(toZone: record) 439 allRecords.append(record) 440 } else { 441 print("Received an unexpected zone id: \(rzid)") 442 } 443 } 444 445 return allRecords 446 } 447 448 func establish(_ request: EstablishRequest, completion: @escaping (EstablishResponse?, Error?) -> Void) { 449 print("FakeCuttlefish: establish called") 450 if !self.state.peersByID.isEmpty { 451 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .establishFailed)) 452 } 453 454 // Before performing write, check if we should error 455 if let establishListener = self.establishListener { 456 let possibleError = establishListener(request) 457 guard possibleError == nil else { 458 completion(nil, possibleError) 459 return 460 } 461 } 462 463 // Also check if we should bail due to conflicting viewKeys 464 if self.newKeysConflict(viewKeys: request.viewKeys) { 465 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists)) 466 return 467 } 468 469 self.state.peersByID[request.peer.peerID] = request.peer 470 self.state.bottles.append(request.bottle) 471 let escrowInformation = EscrowInformation.with { 472 $0.label = "com.apple.icdp.record." + request.bottle.bottleID 473 $0.creationDate = Google_Protobuf_Timestamp(date: Date()) 474 $0.remainingAttempts = 10 475 $0.silentAttemptAllowed = 1 476 $0.recordStatus = .valid 477 let e = EscrowInformation.Metadata.with { 478 $0.backupKeybagDigest = Data() 479 $0.secureBackupUsesMultipleIcscs = 1 480 $0.secureBackupTimestamp = Google_Protobuf_Timestamp(date: Date()) 481 $0.peerInfo = Data() 482 $0.bottleID = request.bottle.bottleID 483 $0.escrowedSpki = request.bottle.escrowedSigningSpki 484 let cm = EscrowInformation.Metadata.ClientMetadata.with { 485 $0.deviceColor = "#202020" 486 $0.deviceEnclosureColor = "#020202" 487 $0.deviceModel = "model" 488 $0.deviceModelClass = "modelClass" 489 $0.deviceModelVersion = "modelVersion" 490 $0.deviceMid = "mid" 491 $0.deviceName = "my device" 492 $0.devicePlatform = 1 493 $0.secureBackupNumericPassphraseLength = 6 494 $0.secureBackupMetadataTimestamp = Google_Protobuf_Timestamp(date: Date()) 495 $0.secureBackupUsesNumericPassphrase = 1 496 $0.secureBackupUsesComplexPassphrase = 1 497 } 498 $0.clientMetadata = cm 499 } 500 $0.escrowInformationMetadata = e 501 } 502 self.state.escrowRecords.append(escrowInformation) 503 504 var keyRecords: [CKRecord] = [] 505 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys)) 506 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares)) 507 508 self.makeSnapshot() 509 510 let response = EstablishResponse.with { 511 if self.nextEstablishReturnsMoreChanges { 512 $0.changes = Changes.with { 513 $0.more = true 514 } 515 self.nextEstablishReturnsMoreChanges = false 516 } else { 517 $0.changes = self.changesSince(snapshot: State()) 518 } 519 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) } 520 } 521 522 completion(response, nil) 523 self.pushNotify("establish") 524 } 525 526 func joinWithVoucher(_ request: JoinWithVoucherRequest, completion: @escaping (JoinWithVoucherResponse?, Error?) -> Void) { 527 print("FakeCuttlefish: joinWithVoucher called") 528 529 if let joinListener = self.joinListener { 530 let possibleError = joinListener(request) 531 guard possibleError == nil else { 532 completion(nil, possibleError) 533 return 534 } 535 } 536 537 if let injectedError = self.nextJoinErrors.first { 538 print("FakeCuttlefish: erroring with injected error: ", String(describing: injectedError)) 539 self.nextJoinErrors.removeFirst() 540 completion(nil, injectedError) 541 return 542 } 543 544 // Also check if we should bail due to conflicting viewKeys 545 if self.newKeysConflict(viewKeys: request.viewKeys) { 546 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists)) 547 return 548 } 549 550 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else { 551 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .changeTokenExpired)) 552 return 553 } 554 self.state.peersByID[request.peer.peerID] = request.peer 555 self.state.bottles.append(request.bottle) 556 let escrowInformation = EscrowInformation.with { 557 $0.label = "com.apple.icdp.record." + request.bottle.bottleID 558 $0.creationDate = Google_Protobuf_Timestamp(date: Date()) 559 $0.remainingAttempts = 10 560 $0.silentAttemptAllowed = 1 561 $0.recordStatus = .valid 562 let e = EscrowInformation.Metadata.with { 563 $0.backupKeybagDigest = Data() 564 $0.secureBackupUsesMultipleIcscs = 1 565 $0.secureBackupTimestamp = Google_Protobuf_Timestamp(date: Date()) 566 $0.peerInfo = Data() 567 $0.bottleID = request.bottle.bottleID 568 $0.escrowedSpki = request.bottle.escrowedSigningSpki 569 let cm = EscrowInformation.Metadata.ClientMetadata.with { 570 $0.deviceColor = "#202020" 571 $0.deviceEnclosureColor = "#020202" 572 $0.deviceModel = "model" 573 $0.deviceModelClass = "modelClass" 574 $0.deviceModelVersion = "modelVersion" 575 $0.deviceMid = "mid" 576 $0.deviceName = "my device" 577 $0.devicePlatform = 1 578 $0.secureBackupNumericPassphraseLength = 6 579 $0.secureBackupMetadataTimestamp = Google_Protobuf_Timestamp(date: Date()) 580 $0.secureBackupUsesNumericPassphrase = 1 581 $0.secureBackupUsesComplexPassphrase = 1 582 } 583 $0.clientMetadata = cm 584 } 585 $0.escrowInformationMetadata = e 586 } 587 self.state.escrowRecords.append(escrowInformation) 588 var keyRecords: [CKRecord] = [] 589 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys)) 590 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares)) 591 592 self.makeSnapshot() 593 594 completion(JoinWithVoucherResponse.with { 595 $0.changes = self.changesSince(snapshot: snapshot) 596 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) } 597 }, nil) 598 self.pushNotify("joinWithVoucher") 599 } 600 601 func updateTrust(_ request: UpdateTrustRequest, completion: @escaping (UpdateTrustResponse?, Error?) -> Void) { 602 print("FakeCuttlefish: updateTrust called: changeToken: ", request.changeToken, "peerID: ", request.peerID) 603 604 if let injectedError = self.nextUpdateTrustErrors.first { 605 print("FakeCuttlefish: updateTrust erroring with injected error: ", String(describing: injectedError)) 606 self.nextUpdateTrustErrors.removeFirst() 607 completion(nil, injectedError) 608 return 609 } 610 611 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else { 612 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .changeTokenExpired)) 613 return 614 } 615 guard let existingPeer = self.state.peersByID[request.peerID] else { 616 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .updateTrustPeerNotFound)) 617 return 618 } 619 620 // copy Peer so that we can update it safely 621 var peer = try! Peer(serializedData: try! existingPeer.serializedData()) 622 623 // Before performing write, check if we should error 624 if let updateListener = self.updateListener { 625 let possibleError = updateListener(request) 626 guard possibleError == nil else { 627 completion(nil, possibleError) 628 return 629 } 630 } 631 632 if request.hasStableInfoAndSig { 633 peer.stableInfoAndSig = request.stableInfoAndSig 634 } 635 if request.hasDynamicInfoAndSig { 636 peer.dynamicInfoAndSig = request.dynamicInfoAndSig 637 } 638 639 // Will Cuttlefish reject this due to peer graph issues? 640 do { 641 let model = try self.state.model(updating: peer) 642 643 // Is there any non-excluded peer that trusts itself? 644 let nonExcludedPeerIDs = model.allPeerIDs().filter { !model.statusOfPeer(withID: $0).contains(.excluded) } 645 let selfTrustedPeerIDs = nonExcludedPeerIDs.filter { model.statusOfPeer(withID: $0).contains(.selfTrust) } 646 647 guard selfTrustedPeerIDs.count > 0 else { 648 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .resultGraphHasNoPotentiallyTrustedPeers)) 649 return 650 } 651 } catch { 652 print("FakeCuttlefish: updateTrust failed to make model: ", String(describing: error)) 653 } 654 655 // Cuttlefish has accepted the write. 656 self.state.peersByID[request.peerID] = peer 657 658 // Also check if we should bail due to conflicting viewKeys 659 if self.newKeysConflict(viewKeys: request.viewKeys) { 660 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists)) 661 return 662 } 663 664 var keyRecords: [CKRecord] = [] 665 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys)) 666 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares)) 667 668 let newDynamicInfo = TPPeerDynamicInfo(data: peer.dynamicInfoAndSig.peerDynamicInfo, 669 sig: peer.dynamicInfoAndSig.sig) 670 print("FakeCuttlefish: new peer dynamicInfo: ", request.peerID, String(describing: newDynamicInfo?.dictionaryRepresentation())) 671 672 self.makeSnapshot() 673 let response = UpdateTrustResponse.with { 674 $0.changes = self.changesSince(snapshot: snapshot) 675 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) } 676 } 677 678 completion(response, nil) 679 self.pushNotify("updateTrust") 680 } 681 682 func setRecoveryKey(_ request: SetRecoveryKeyRequest, completion: @escaping (SetRecoveryKeyResponse?, Error?) -> Void) { 683 print("FakeCuttlefish: setRecoveryKey called") 684 685 if let listener = self.setRecoveryKeyListener { 686 let operationError = listener(request) 687 guard operationError == nil else { 688 completion(nil, operationError) 689 return 690 } 691 } 692 693 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else { 694 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .changeTokenExpired)) 695 return 696 } 697 self.state.recoverySigningPubKey = request.recoverySigningPubKey 698 self.state.recoveryEncryptionPubKey = request.recoveryEncryptionPubKey 699 self.state.peersByID[request.peerID]?.stableInfoAndSig = request.stableInfoAndSig 700 701 var keyRecords: [CKRecord] = [] 702 //keyRecords.append(contentsOf: store(viewKeys: request.viewKeys)) 703 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares)) 704 705 self.makeSnapshot() 706 completion(SetRecoveryKeyResponse.with { 707 $0.changes = self.changesSince(snapshot: snapshot) 708 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) } 709 }, nil) 710 self.pushNotify("setRecoveryKey") 711 } 712 713 func fetchChanges(_ request: FetchChangesRequest, completion: @escaping (FetchChangesResponse?, Error?) -> Void) { 714 print("FakeCuttlefish: fetchChanges called: ", request.changeToken) 715 716 self.fetchChangesCalledCount += 1 717 718 if let fetchChangesListener = self.fetchChangesListener { 719 let possibleError = fetchChangesListener(request) 720 guard possibleError == nil else { 721 completion(nil, possibleError) 722 return 723 } 724 if fetchChangesReturnEmptyResponse == true { 725 completion(FetchChangesResponse(), nil) 726 return 727 } 728 } 729 730 if let injectedError = self.nextFetchErrors.first { 731 print("FakeCuttlefish: fetchChanges erroring with injected error: ", String(describing: injectedError)) 732 self.nextFetchErrors.removeFirst() 733 completion(nil, injectedError) 734 return 735 } 736 737 let snapshot: State 738 if request.changeToken.isEmpty { 739 snapshot = State() 740 } else { 741 guard let s = self.snapshotsByChangeToken[request.changeToken] else { 742 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .changeTokenExpired)) 743 return 744 } 745 snapshot = s 746 } 747 let response = FetchChangesResponse.with { 748 $0.changes = self.changesSince(snapshot: snapshot) 749 } 750 751 completion(response, nil) 752 } 753 754 func fetchViableBottles(_ request: FetchViableBottlesRequest, completion: @escaping (FetchViableBottlesResponse?, Error?) -> Void) { 755 print("FakeCuttlefish: fetchViableBottles called") 756 757 if let fetchViableBottlesListener = self.fetchViableBottlesListener { 758 let possibleError = fetchViableBottlesListener(request) 759 guard possibleError == nil else { 760 completion(nil, possibleError) 761 return 762 } 763 } 764 765 if let injectedError = self.fetchViableBottlesError.first { 766 print("FakeCuttlefish: fetchViableBottles erroring with injected error: ", String(describing: injectedError)) 767 self.fetchViableBottlesError.removeFirst() 768 completion(nil, injectedError) 769 return 770 } 771 772 var legacy: [EscrowInformation] = [] 773 if self.injectLegacyEscrowRecords { 774 print("FakeCuttlefish: fetchViableBottles injecting legacy records") 775 let record = EscrowInformation.with { 776 $0.label = "fake-label" 777 } 778 legacy.append(record) 779 } 780 let bottles = self.state.bottles.filter { $0.bottleID != fetchViableBottlesDontReturnBottleWithID } 781 782 completion(FetchViableBottlesResponse.with { 783 $0.viableBottles = bottles.compactMap { bottle in 784 EscrowPair.with { 785 $0.escrowRecordID = bottle.bottleID 786 $0.bottle = bottle 787 if self.includeEscrowRecords { 788 $0.record = self.state.escrowRecords.first { $0.escrowInformationMetadata.bottleID == bottle.bottleID } ?? EscrowInformation() 789 } 790 } 791 } 792 if self.injectLegacyEscrowRecords { 793 $0.legacyRecords = legacy 794 } 795 }, nil) 796 } 797 798 func fetchPolicyDocuments(_ request: FetchPolicyDocumentsRequest, 799 completion: @escaping (FetchPolicyDocumentsResponse?, Error?) -> Void) { 800 print("FakeCuttlefish: fetchPolicyDocuments called") 801 var response = FetchPolicyDocumentsResponse() 802 803 let policies = builtInPolicyDocuments() 804 let dummyPolicies = Dictionary(uniqueKeysWithValues: policies.map { ($0.version.versionNumber, ($0.version.policyHash, $0.protobuf)) }) 805 let overlayPolicies = Dictionary(uniqueKeysWithValues: self.policyOverlay.map { ($0.version.versionNumber, ($0.version.policyHash, $0.protobuf)) }) 806 807 for key in request.keys { 808 if let (hash, data) = overlayPolicies[key.version], hash == key.hash { 809 response.entries.append(PolicyDocumentMapEntry.with { $0.key = key; $0.value = data }) 810 continue 811 } 812 813 guard let (hash, data) = dummyPolicies[key.version] else { 814 continue 815 } 816 if hash == key.hash { 817 response.entries.append(PolicyDocumentMapEntry.with { $0.key = key; $0.value = data }) 818 } 819 } 820 completion(response, nil) 821 } 822 823 func assertCuttlefishState(_ assertion: FakeCuttlefishAssertion) -> Bool { 824 return assertion.check(peer: self.state.peersByID[assertion.peer], target: self.state.peersByID[assertion.target]) 825 } 826 827 func validatePeers(_: ValidatePeersRequest, completion: @escaping (ValidatePeersResponse?, Error?) -> Void) { 828 var response = ValidatePeersResponse() 829 response.validatorsHealth = 0.0 830 response.results = [] 831 completion(response, nil) 832 } 833 func reportHealth(_: ReportHealthRequest, completion: @escaping (ReportHealthResponse?, Error?) -> Void) { 834 completion(ReportHealthResponse(), nil) 835 } 836 func pushHealthInquiry(_: PushHealthInquiryRequest, completion: @escaping (PushHealthInquiryResponse?, Error?) -> Void) { 837 completion(PushHealthInquiryResponse(), nil) 838 } 839 840 func getRepairAction(_ request: GetRepairActionRequest, completion: @escaping (GetRepairActionResponse?, Error?) -> Void) { 841 print("FakeCuttlefish: getRepairAction called") 842 843 if let healthListener = self.healthListener { 844 let possibleError = healthListener(request) 845 guard possibleError == nil else { 846 completion(nil, possibleError) 847 return 848 } 849 } 850 851 if self.returnRepairEscrowResponse { 852 let response = GetRepairActionResponse.with { 853 $0.repairAction = .postRepairEscrow 854 } 855 completion(response, nil) 856 } else if self.returnRepairAccountResponse { 857 let response = GetRepairActionResponse.with { 858 $0.repairAction = .postRepairAccount 859 } 860 completion(response, nil) 861 } else if self.returnResetOctagonResponse { 862 let response = GetRepairActionResponse.with { 863 $0.repairAction = .resetOctagon 864 } 865 completion(response, nil) 866 } else if returnLeaveTrustResponse { 867 let response = GetRepairActionResponse.with { 868 $0.repairAction = .leaveTrust 869 } 870 completion(response, nil) 871 } else if self.returnNoActionResponse { 872 let response = GetRepairActionResponse.with { 873 $0.repairAction = .noAction 874 } 875 completion(response, nil) 876 } else if self.returnRepairErrorResponse != nil { 877 let response = GetRepairActionResponse.with { 878 $0.repairAction = .noAction 879 } 880 completion(response, self.returnRepairErrorResponse) 881 } else { 882 completion(GetRepairActionResponse(), nil) 883 } 884 } 885 886 func getClubCertificates(_: GetClubCertificatesRequest, completion: @escaping (GetClubCertificatesResponse?, Error?) -> Void) { 887 completion(GetClubCertificatesResponse(), nil) 888 } 889 890 func getSupportAppInfo(_: GetSupportAppInfoRequest, completion: @escaping (GetSupportAppInfoResponse?, Error?) -> Void) { 891 completion(GetSupportAppInfoResponse(), nil) 892 } 893 894 func fetchSosiCloudIdentity(_: FetchSOSiCloudIdentityRequest, completion: @escaping (FetchSOSiCloudIdentityResponse?, Error?) -> Void) { 895 completion(FetchSOSiCloudIdentityResponse(), nil) 896 } 897 func addCustodianRecoveryKey(_: AddCustodianRecoveryKeyRequest, completion: @escaping (AddCustodianRecoveryKeyResponse?, Error?) -> Void) { 898 completion(AddCustodianRecoveryKeyResponse(), nil) 899 } 900 func resetAccountCdpcontents(_: ResetAccountCDPContentsRequest, completion: @escaping (ResetAccountCDPContentsResponse?, Error?) -> Void) { 901 completion(ResetAccountCDPContentsResponse(), nil) 902 } 903 } 904 905 extension FakeCuttlefishServer: CloudKitCode.Invocable { 906 func invoke<RequestType, ResponseType>(function: String, 907 request: RequestType, 908 completion: @escaping (ResponseType?, Error?) -> Void) { 909 // Ideally we'd pattern match on both request and completion, but that crashes the swift compiler at this time (<rdar://problem/54412402>) 910 switch request { 911 case let request as ResetRequest: 912 self.reset(request, completion: completion as! (ResetResponse?, Error?) -> Void) 913 return 914 case let request as EstablishRequest: 915 self.establish(request, completion: completion as! (EstablishResponse?, Error?) -> Void) 916 return 917 case let request as JoinWithVoucherRequest: 918 self.joinWithVoucher(request, completion: completion as! (JoinWithVoucherResponse?, Error?) -> Void) 919 return 920 case let request as UpdateTrustRequest: 921 self.updateTrust(request, completion: completion as! (UpdateTrustResponse?, Error?) -> Void) 922 return 923 case let request as SetRecoveryKeyRequest: 924 self.setRecoveryKey(request, completion: completion as! (SetRecoveryKeyResponse?, Error?) -> Void) 925 return 926 case let request as FetchChangesRequest: 927 self.fetchChanges(request, completion: completion as! (FetchChangesResponse?, Error?) -> Void) 928 return 929 case let request as FetchViableBottlesRequest: 930 self.fetchViableBottles(request, completion: completion as! (FetchViableBottlesResponse?, Error?) -> Void) 931 return 932 case let request as FetchPolicyDocumentsRequest: 933 self.fetchPolicyDocuments(request, completion: completion as! (FetchPolicyDocumentsResponse?, Error?) -> Void) 934 return 935 case let request as ValidatePeersRequest: 936 self.validatePeers(request, completion: completion as! (ValidatePeersResponse?, Error?) -> Void) 937 return 938 case let request as ReportHealthRequest: 939 self.reportHealth(request, completion: completion as! (ReportHealthResponse?, Error?) -> Void) 940 return 941 case let request as PushHealthInquiryRequest: 942 self.pushHealthInquiry(request, completion: completion as! (PushHealthInquiryResponse?, Error?) -> Void) 943 return 944 case let request as GetRepairActionRequest: 945 self.getRepairAction(request, completion: completion as! (GetRepairActionResponse?, Error?) -> Void) 946 return 947 default: 948 abort() 949 } 950 } 951 }