/ keychain / TrustedPeersHelperUnitTests / FakeCuttlefish.swift
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  }