]> git.saurik.com Git - apple/security.git/blob - keychain/TrustedPeersHelperUnitTests/FakeCuttlefish.swift
Security-59754.60.13.tar.gz
[apple/security.git] / keychain / TrustedPeersHelperUnitTests / 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
182 var state = State()
183 var snapshotsByChangeToken: [String: State] = [:]
184 var currentChange: Int = 0
185 var currentChangeToken: String = ""
186 let notify: FakeCuttlefishNotify?
187
188 //var fakeCKZones: [CKRecordZone.ID: FakeCKZone]
189 var fakeCKZones: NSMutableDictionary
190
191 // @property (nullable) NSMutableDictionary<CKRecordZoneID*, ZoneKeys*>* keys;
192 var ckksZoneKeys: NSMutableDictionary
193
194 var injectLegacyEscrowRecords: Bool = false
195 var includeEscrowRecords: Bool = true
196
197 var nextFetchErrors: [Error] = []
198 var fetchViableBottlesError: [Error] = []
199 var nextJoinErrors: [Error] = []
200 var nextUpdateTrustErrors: [Error] = []
201 var returnNoActionResponse: Bool = false
202 var returnRepairAccountResponse: Bool = false
203 var returnRepairEscrowResponse: Bool = false
204 var returnResetOctagonResponse: Bool = false
205 var returnLeaveTrustResponse: Bool = false
206 var returnRepairErrorResponse: Error?
207 var fetchChangesCalledCount: Int = 0
208 var fetchChangesReturnEmptyResponse: Bool = false
209
210 var fetchViableBottlesEscrowRecordCacheTimeout: TimeInterval = 2.0
211
212 var nextEstablishReturnsMoreChanges: Bool = false
213
214 var establishListener: ((EstablishRequest) -> NSError?)?
215 var updateListener: ((UpdateTrustRequest) -> NSError?)?
216 var fetchChangesListener: ((FetchChangesRequest) -> NSError?)?
217 var joinListener: ((JoinWithVoucherRequest) -> NSError?)?
218 var healthListener: ((GetRepairActionRequest) -> NSError?)?
219 var fetchViableBottlesListener: ((FetchViableBottlesRequest) -> NSError?)?
220 var resetListener: ((ResetRequest) -> NSError?)?
221 var setRecoveryKeyListener: ((SetRecoveryKeyRequest) -> NSError?)?
222
223 // Any policies in here will be returned by FetchPolicy before any inbuilt policies
224 var policyOverlay: [TPPolicyDocument] = []
225
226 var fetchViableBottlesDontReturnBottleWithID: String?
227
228 init(_ notify: FakeCuttlefishNotify?, ckZones: NSMutableDictionary, ckksZoneKeys: NSMutableDictionary) {
229 self.notify = notify
230 self.fakeCKZones = ckZones
231 self.ckksZoneKeys = ckksZoneKeys
232 }
233
234 func deleteAllPeers() {
235 self.state.peersByID.removeAll()
236 self.makeSnapshot()
237 }
238
239 func pushNotify(_ function: String) {
240 if let notify = self.notify {
241 do {
242 try notify.notify(function)
243 } catch {
244 }
245 }
246 }
247
248 static func makeCloudKitCuttlefishError(code: CuttlefishErrorCode, retryAfter: TimeInterval = 5) -> NSError {
249 let cuttlefishError = CKPrettyError(domain: CuttlefishErrorDomain,
250 code: code.rawValue,
251 userInfo: [CuttlefishErrorRetryAfterKey: retryAfter])
252 let internalError = CKPrettyError(domain: CKInternalErrorDomain,
253 code: CKInternalErrorCode.errorInternalPluginError.rawValue,
254 userInfo: [NSUnderlyingErrorKey: cuttlefishError, ])
255 let ckError = CKPrettyError(domain: CKErrorDomain,
256 code: CKError.serverRejectedRequest.rawValue,
257 userInfo: [NSUnderlyingErrorKey: internalError,
258 CKErrorServerDescriptionKey: "Fake: FunctionError domain: CuttlefishError, code: \(code),\(code.rawValue)",
259 ])
260 return ckError
261 }
262
263 func makeSnapshot() {
264 self.currentChange += 1
265 self.currentChangeToken = "change\(self.currentChange)"
266 self.snapshotsByChangeToken[self.currentChangeToken] = self.state
267 }
268
269 func changesSince(snapshot: State) -> Changes {
270 return Changes.with { changes in
271 changes.changeToken = self.currentChangeToken
272
273 changes.differences = self.state.peersByID.compactMap { (key: String, value: Peer) -> PeerDifference? in
274 let old = snapshot.peersByID[key]
275 if old == nil {
276 return PeerDifference.with {
277 $0.add = value
278 }
279 } else if old != value {
280 return PeerDifference.with {
281 $0.update = value
282 }
283 } else {
284 return nil
285 }
286 }
287 snapshot.peersByID.forEach { (key: String, _: Peer) in
288 if self.state.peersByID[key] == nil {
289 changes.differences.append(PeerDifference.with {
290 $0.remove = Peer.with {
291 $0.peerID = key
292 }
293 })
294 }
295 }
296
297 if self.state.recoverySigningPubKey != snapshot.recoverySigningPubKey {
298 changes.recoverySigningPubKey = self.state.recoverySigningPubKey ?? Data()
299 }
300 if self.state.recoveryEncryptionPubKey != snapshot.recoveryEncryptionPubKey {
301 changes.recoveryEncryptionPubKey = self.state.recoveryEncryptionPubKey ?? Data()
302 }
303 }
304 }
305
306 func reset(_ request: ResetRequest, completion: @escaping (ResetResponse?, Error?) -> Void) {
307 print("FakeCuttlefish: reset called")
308 if let resetListener = self.resetListener {
309 let possibleError = resetListener(request)
310 guard possibleError == nil else {
311 completion(nil, possibleError)
312 return
313 }
314 }
315 self.state = State()
316 self.makeSnapshot()
317 completion(ResetResponse.with {
318 $0.changes = self.changesSince(snapshot: State())
319 }, nil)
320 self.pushNotify("reset")
321 }
322
323 func newKeysConflict(viewKeys: [ViewKeys]) -> Bool {
324 #if OCTAGON_TEST_FILL_ZONEKEYS
325 for keys in viewKeys {
326 let rzid = CKRecordZone.ID(zoneName: keys.view)
327
328 if let currentViewKeys = self.ckksZoneKeys[rzid] as? CKKSCurrentKeySet {
329 // Uploading the current view keys is okay. Fail only if they don't match
330 if keys.newTlk.uuid != currentViewKeys.tlk!.uuid ||
331 keys.newClassA.uuid != currentViewKeys.classA!.uuid ||
332 keys.newClassC.uuid != currentViewKeys.classC!.uuid {
333 return true
334 }
335 }
336 }
337 #endif
338
339 return false
340 }
341
342 func store(viewKeys: [ViewKeys]) -> [CKRecord] {
343 var allRecords: [CKRecord] = []
344
345 viewKeys.forEach { viewKeys in
346 let rzid = CKRecordZone.ID(zoneName: viewKeys.view)
347 self.state.viewKeys[rzid] = viewKeys
348
349 // Real cuttlefish makes these zones for you
350 if self.fakeCKZones[rzid] == nil {
351 self.fakeCKZones[rzid] = FakeCKZone(zone: rzid)
352 }
353
354 if let fakeZone = self.fakeCKZones[rzid] as? FakeCKZone {
355 fakeZone.queue.sync {
356 let tlkRecord = viewKeys.newTlk.fakeRecord(zoneID: rzid)
357 let classARecord = viewKeys.newClassA.fakeRecord(zoneID: rzid)
358 let classCRecord = viewKeys.newClassC.fakeRecord(zoneID: rzid)
359
360 let tlkPointerRecord = viewKeys.newTlk.fakeKeyPointer(zoneID: rzid)
361 let classAPointerRecord = viewKeys.newClassA.fakeKeyPointer(zoneID: rzid)
362 let classCPointerRecord = viewKeys.newClassC.fakeKeyPointer(zoneID: rzid)
363
364 // Some tests don't link everything needed to make zonekeys
365 // Those tests don't get this nice behavior
366 #if OCTAGON_TEST_FILL_ZONEKEYS
367 let zoneKeys = self.ckksZoneKeys[rzid] as? ZoneKeys ?? ZoneKeys(forZoneName: rzid.zoneName)
368 self.ckksZoneKeys[rzid] = zoneKeys
369
370 zoneKeys.tlk = CKKSKey(ckRecord: tlkRecord)
371 zoneKeys.classA = CKKSKey(ckRecord: classARecord)
372 zoneKeys.classC = CKKSKey(ckRecord: classCRecord)
373
374 zoneKeys.currentTLKPointer = CKKSCurrentKeyPointer(ckRecord: tlkPointerRecord)
375 zoneKeys.currentClassAPointer = CKKSCurrentKeyPointer(ckRecord: classAPointerRecord)
376 zoneKeys.currentClassCPointer = CKKSCurrentKeyPointer(ckRecord: classCPointerRecord)
377 #endif
378
379 let zoneRecords = [tlkRecord,
380 classARecord,
381 classCRecord,
382 tlkPointerRecord,
383 classAPointerRecord,
384 classCPointerRecord, ]
385 // TODO a rolled tlk too
386
387 zoneRecords.forEach { record in
388 fakeZone._onqueueAdd(toZone: record)
389 }
390 allRecords.append(contentsOf: zoneRecords)
391 }
392 } else {
393 // we made the zone above, shoudn't ever get here
394 print("Received an unexpected zone id: \(rzid)")
395 abort()
396 }
397 }
398 return allRecords
399 }
400
401 func store(tlkShares: [TLKShare]) -> [CKRecord] {
402 var allRecords: [CKRecord] = []
403
404 tlkShares.forEach { share in
405 let rzid = CKRecordZone.ID(zoneName: share.view)
406
407 var c = self.state.tlkShares[rzid] ?? []
408 c.append(share)
409 self.state.tlkShares[rzid] = c
410
411 if let fakeZone = self.fakeCKZones[rzid] as? FakeCKZone {
412 let record = share.fakeRecord(zoneID: rzid)
413 fakeZone.add(toZone: record)
414 allRecords.append(record)
415 } else {
416 print("Received an unexpected zone id: \(rzid)")
417 }
418 }
419
420 return allRecords
421 }
422
423 func establish(_ request: EstablishRequest, completion: @escaping (EstablishResponse?, Error?) -> Void) {
424 print("FakeCuttlefish: establish called")
425 if !self.state.peersByID.isEmpty {
426 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .establishFailed))
427 }
428
429 // Before performing write, check if we should error
430 if let establishListener = self.establishListener {
431 let possibleError = establishListener(request)
432 guard possibleError == nil else {
433 completion(nil, possibleError)
434 return
435 }
436 }
437
438 // Also check if we should bail due to conflicting viewKeys
439 if self.newKeysConflict(viewKeys: request.viewKeys) {
440 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
441 return
442 }
443
444 self.state.peersByID[request.peer.peerID] = request.peer
445 self.state.bottles.append(request.bottle)
446 let escrowInformation = EscrowInformation.with {
447 $0.label = "com.apple.icdp.record." + request.bottle.bottleID
448 $0.creationDate = Google_Protobuf_Timestamp(date: Date())
449 $0.remainingAttempts = 10
450 $0.silentAttemptAllowed = 1
451 $0.recordStatus = .valid
452 let e = EscrowInformation.Metadata.with {
453 $0.backupKeybagDigest = Data()
454 $0.secureBackupUsesMultipleIcscs = 1
455 $0.secureBackupTimestamp = Google_Protobuf_Timestamp(date: Date())
456 $0.peerInfo = Data()
457 $0.bottleID = request.bottle.bottleID
458 $0.escrowedSpki = request.bottle.escrowedSigningSpki
459 let cm = EscrowInformation.Metadata.ClientMetadata.with {
460 $0.deviceColor = "#202020"
461 $0.deviceEnclosureColor = "#020202"
462 $0.deviceModel = "model"
463 $0.deviceModelClass = "modelClass"
464 $0.deviceModelVersion = "modelVersion"
465 $0.deviceMid = "mid"
466 $0.deviceName = "my device"
467 $0.devicePlatform = 1
468 $0.secureBackupNumericPassphraseLength = 6
469 $0.secureBackupMetadataTimestamp = Google_Protobuf_Timestamp(date: Date())
470 $0.secureBackupUsesNumericPassphrase = 1
471 $0.secureBackupUsesComplexPassphrase = 1
472 }
473 $0.clientMetadata = cm
474 }
475 $0.escrowInformationMetadata = e
476 }
477 self.state.escrowRecords.append(escrowInformation)
478
479 var keyRecords: [CKRecord] = []
480 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
481 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
482
483 self.makeSnapshot()
484
485 let response = EstablishResponse.with {
486 if self.nextEstablishReturnsMoreChanges {
487 $0.changes = Changes.with {
488 $0.more = true
489 }
490 self.nextEstablishReturnsMoreChanges = false
491 } else {
492 $0.changes = self.changesSince(snapshot: State())
493 }
494 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
495 }
496
497 completion(response, nil)
498 self.pushNotify("establish")
499 }
500
501 func joinWithVoucher(_ request: JoinWithVoucherRequest, completion: @escaping (JoinWithVoucherResponse?, Error?) -> Void) {
502 print("FakeCuttlefish: joinWithVoucher called")
503
504 if let joinListener = self.joinListener {
505 let possibleError = joinListener(request)
506 guard possibleError == nil else {
507 completion(nil, possibleError)
508 return
509 }
510 }
511
512 if let injectedError = self.nextJoinErrors.first {
513 print("FakeCuttlefish: erroring with injected error: ", String(describing: injectedError))
514 self.nextJoinErrors.removeFirst()
515 completion(nil, injectedError)
516 return
517 }
518
519 // Also check if we should bail due to conflicting viewKeys
520 if self.newKeysConflict(viewKeys: request.viewKeys) {
521 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
522 return
523 }
524
525 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
526 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .changeTokenExpired))
527 return
528 }
529 self.state.peersByID[request.peer.peerID] = request.peer
530 self.state.bottles.append(request.bottle)
531 let escrowInformation = EscrowInformation.with {
532 $0.label = "com.apple.icdp.record." + request.bottle.bottleID
533 $0.creationDate = Google_Protobuf_Timestamp(date: Date())
534 $0.remainingAttempts = 10
535 $0.silentAttemptAllowed = 1
536 $0.recordStatus = .valid
537 let e = EscrowInformation.Metadata.with {
538 $0.backupKeybagDigest = Data()
539 $0.secureBackupUsesMultipleIcscs = 1
540 $0.secureBackupTimestamp = Google_Protobuf_Timestamp(date: Date())
541 $0.peerInfo = Data()
542 $0.bottleID = request.bottle.bottleID
543 $0.escrowedSpki = request.bottle.escrowedSigningSpki
544 let cm = EscrowInformation.Metadata.ClientMetadata.with {
545 $0.deviceColor = "#202020"
546 $0.deviceEnclosureColor = "#020202"
547 $0.deviceModel = "model"
548 $0.deviceModelClass = "modelClass"
549 $0.deviceModelVersion = "modelVersion"
550 $0.deviceMid = "mid"
551 $0.deviceName = "my device"
552 $0.devicePlatform = 1
553 $0.secureBackupNumericPassphraseLength = 6
554 $0.secureBackupMetadataTimestamp = Google_Protobuf_Timestamp(date: Date())
555 $0.secureBackupUsesNumericPassphrase = 1
556 $0.secureBackupUsesComplexPassphrase = 1
557 }
558 $0.clientMetadata = cm
559 }
560 $0.escrowInformationMetadata = e
561 }
562 self.state.escrowRecords.append(escrowInformation)
563 var keyRecords: [CKRecord] = []
564 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
565 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
566
567 self.makeSnapshot()
568
569 completion(JoinWithVoucherResponse.with {
570 $0.changes = self.changesSince(snapshot: snapshot)
571 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
572 }, nil)
573 self.pushNotify("joinWithVoucher")
574 }
575
576 func updateTrust(_ request: UpdateTrustRequest, completion: @escaping (UpdateTrustResponse?, Error?) -> Void) {
577 print("FakeCuttlefish: updateTrust called: changeToken: ", request.changeToken, "peerID: ", request.peerID)
578
579 if let injectedError = self.nextUpdateTrustErrors.first {
580 print("FakeCuttlefish: updateTrust erroring with injected error: ", String(describing: injectedError))
581 self.nextUpdateTrustErrors.removeFirst()
582 completion(nil, injectedError)
583 return
584 }
585
586 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
587 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .changeTokenExpired))
588 return
589 }
590 guard var peer = self.state.peersByID[request.peerID] else {
591 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .updateTrustPeerNotFound))
592 return
593 }
594 if request.hasStableInfoAndSig {
595 peer.stableInfoAndSig = request.stableInfoAndSig
596 }
597 if request.hasDynamicInfoAndSig {
598 peer.dynamicInfoAndSig = request.dynamicInfoAndSig
599 }
600 self.state.peersByID[request.peerID] = peer
601
602 // Before performing write, check if we should error
603 if let updateListener = self.updateListener {
604 let possibleError = updateListener(request)
605 guard possibleError == nil else {
606 completion(nil, possibleError)
607 return
608 }
609 }
610
611 // Also check if we should bail due to conflicting viewKeys
612 if self.newKeysConflict(viewKeys: request.viewKeys) {
613 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
614 return
615 }
616
617 var keyRecords: [CKRecord] = []
618 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
619 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
620
621 let newDynamicInfo = TPPeerDynamicInfo(data: peer.dynamicInfoAndSig.peerDynamicInfo,
622 sig: peer.dynamicInfoAndSig.sig)
623 print("FakeCuttlefish: new peer dynamicInfo: ", request.peerID, String(describing: newDynamicInfo?.dictionaryRepresentation()))
624
625 self.makeSnapshot()
626 let response = UpdateTrustResponse.with {
627 $0.changes = self.changesSince(snapshot: snapshot)
628 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
629 }
630
631 completion(response, nil)
632 self.pushNotify("updateTrust")
633 }
634
635 func setRecoveryKey(_ request: SetRecoveryKeyRequest, completion: @escaping (SetRecoveryKeyResponse?, Error?) -> Void) {
636 print("FakeCuttlefish: setRecoveryKey called")
637
638 if let listener = self.setRecoveryKeyListener {
639 let operationError = listener(request)
640 guard operationError == nil else {
641 completion(nil, operationError)
642 return
643 }
644 }
645
646 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
647 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .changeTokenExpired))
648 return
649 }
650 self.state.recoverySigningPubKey = request.recoverySigningPubKey
651 self.state.recoveryEncryptionPubKey = request.recoveryEncryptionPubKey
652 self.state.peersByID[request.peerID]?.stableInfoAndSig = request.stableInfoAndSig
653
654 var keyRecords: [CKRecord] = []
655 //keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
656 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
657
658 self.makeSnapshot()
659 completion(SetRecoveryKeyResponse.with {
660 $0.changes = self.changesSince(snapshot: snapshot)
661 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
662 }, nil)
663 self.pushNotify("setRecoveryKey")
664 }
665
666 func fetchChanges(_ request: FetchChangesRequest, completion: @escaping (FetchChangesResponse?, Error?) -> Void) {
667 print("FakeCuttlefish: fetchChanges called: ", request.changeToken)
668
669 self.fetchChangesCalledCount += 1
670
671 if let fetchChangesListener = self.fetchChangesListener {
672 let possibleError = fetchChangesListener(request)
673 guard possibleError == nil else {
674 completion(nil, possibleError)
675 return
676 }
677 if fetchChangesReturnEmptyResponse == true {
678 completion(FetchChangesResponse(), nil)
679 return
680 }
681 }
682
683 if let injectedError = self.nextFetchErrors.first {
684 print("FakeCuttlefish: fetchChanges erroring with injected error: ", String(describing: injectedError))
685 self.nextFetchErrors.removeFirst()
686 completion(nil, injectedError)
687 return
688 }
689
690 let snapshot: State
691 if request.changeToken.isEmpty {
692 snapshot = State()
693 } else {
694 guard let s = self.snapshotsByChangeToken[request.changeToken] else {
695 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .changeTokenExpired))
696 return
697 }
698 snapshot = s
699 }
700 let response = FetchChangesResponse.with {
701 $0.changes = self.changesSince(snapshot: snapshot)
702 }
703
704 completion(response, nil)
705 }
706
707 func fetchViableBottles(_ request: FetchViableBottlesRequest, completion: @escaping (FetchViableBottlesResponse?, Error?) -> Void) {
708 print("FakeCuttlefish: fetchViableBottles called")
709
710 if let fetchViableBottlesListener = self.fetchViableBottlesListener {
711 let possibleError = fetchViableBottlesListener(request)
712 guard possibleError == nil else {
713 completion(nil, possibleError)
714 return
715 }
716 }
717
718 if let injectedError = self.fetchViableBottlesError.first {
719 print("FakeCuttlefish: fetchViableBottles erroring with injected error: ", String(describing: injectedError))
720 self.fetchViableBottlesError.removeFirst()
721 completion(nil, injectedError)
722 return
723 }
724
725 var legacy: [EscrowInformation] = []
726 if self.injectLegacyEscrowRecords {
727 print("FakeCuttlefish: fetchViableBottles injecting legacy records")
728 let record = EscrowInformation.with {
729 $0.label = "fake-label"
730 }
731 legacy.append(record)
732 }
733 let bottles = self.state.bottles.filter { $0.bottleID != fetchViableBottlesDontReturnBottleWithID }
734
735 completion(FetchViableBottlesResponse.with {
736 $0.viableBottles = bottles.compactMap { bottle in
737 EscrowPair.with {
738 $0.escrowRecordID = bottle.bottleID
739 $0.bottle = bottle
740 if self.includeEscrowRecords {
741 $0.record = self.state.escrowRecords.first { $0.escrowInformationMetadata.bottleID == bottle.bottleID } ?? EscrowInformation()
742 }
743 }
744 }
745 if self.injectLegacyEscrowRecords {
746 $0.legacyRecords = legacy
747 }
748 }, nil)
749 }
750
751 func fetchPolicyDocuments(_ request: FetchPolicyDocumentsRequest,
752 completion: @escaping (FetchPolicyDocumentsResponse?, Error?) -> Void) {
753 print("FakeCuttlefish: fetchPolicyDocuments called")
754 var response = FetchPolicyDocumentsResponse()
755
756 let policies = builtInPolicyDocuments()
757 let dummyPolicies = Dictionary(uniqueKeysWithValues: policies.map { ($0.version.versionNumber, ($0.version.policyHash, $0.protobuf)) })
758 let overlayPolicies = Dictionary(uniqueKeysWithValues: self.policyOverlay.map { ($0.version.versionNumber, ($0.version.policyHash, $0.protobuf)) })
759
760 for key in request.keys {
761 if let (hash, data) = overlayPolicies[key.version], hash == key.hash {
762 response.entries.append(PolicyDocumentMapEntry.with { $0.key = key; $0.value = data })
763 continue
764 }
765
766 guard let (hash, data) = dummyPolicies[key.version] else {
767 continue
768 }
769 if hash == key.hash {
770 response.entries.append(PolicyDocumentMapEntry.with { $0.key = key; $0.value = data })
771 }
772 }
773 completion(response, nil)
774 }
775
776 func assertCuttlefishState(_ assertion: FakeCuttlefishAssertion) -> Bool {
777 return assertion.check(peer: self.state.peersByID[assertion.peer], target: self.state.peersByID[assertion.target])
778 }
779
780 func validatePeers(_: ValidatePeersRequest, completion: @escaping (ValidatePeersResponse?, Error?) -> Void) {
781 var response = ValidatePeersResponse()
782 response.validatorsHealth = 0.0
783 response.results = []
784 completion(response, nil)
785 }
786 func reportHealth(_: ReportHealthRequest, completion: @escaping (ReportHealthResponse?, Error?) -> Void) {
787 completion(ReportHealthResponse(), nil)
788 }
789 func pushHealthInquiry(_: PushHealthInquiryRequest, completion: @escaping (PushHealthInquiryResponse?, Error?) -> Void) {
790 completion(PushHealthInquiryResponse(), nil)
791 }
792
793 func getRepairAction(_ request: GetRepairActionRequest, completion: @escaping (GetRepairActionResponse?, Error?) -> Void) {
794 print("FakeCuttlefish: getRepairAction called")
795
796 if let healthListener = self.healthListener {
797 let possibleError = healthListener(request)
798 guard possibleError == nil else {
799 completion(nil, possibleError)
800 return
801 }
802 }
803
804 if self.returnRepairEscrowResponse {
805 let response = GetRepairActionResponse.with {
806 $0.repairAction = .postRepairEscrow
807 }
808 completion(response, nil)
809 } else if self.returnRepairAccountResponse {
810 let response = GetRepairActionResponse.with {
811 $0.repairAction = .postRepairAccount
812 }
813 completion(response, nil)
814 } else if self.returnResetOctagonResponse {
815 let response = GetRepairActionResponse.with {
816 $0.repairAction = .resetOctagon
817 }
818 completion(response, nil)
819 } else if returnLeaveTrustResponse {
820 let response = GetRepairActionResponse.with {
821 $0.repairAction = .leaveTrust
822 }
823 completion(response, nil)
824 } else if self.returnNoActionResponse {
825 let response = GetRepairActionResponse.with {
826 $0.repairAction = .noAction
827 }
828 completion(response, nil)
829 } else if self.returnRepairErrorResponse != nil {
830 let response = GetRepairActionResponse.with {
831 $0.repairAction = .noAction
832 }
833 completion(response, self.returnRepairErrorResponse)
834 } else {
835 completion(GetRepairActionResponse(), nil)
836 }
837 }
838
839 func getClubCertificates(_: GetClubCertificatesRequest, completion: @escaping (GetClubCertificatesResponse?, Error?) -> Void) {
840 completion(GetClubCertificatesResponse(), nil)
841 }
842
843 func getSupportAppInfo(_: GetSupportAppInfoRequest, completion: @escaping (GetSupportAppInfoResponse?, Error?) -> Void) {
844 completion(GetSupportAppInfoResponse(), nil)
845 }
846
847 func fetchSosiCloudIdentity(_: FetchSOSiCloudIdentityRequest, completion: @escaping (FetchSOSiCloudIdentityResponse?, Error?) -> Void) {
848 completion(FetchSOSiCloudIdentityResponse(), nil)
849 }
850
851 func addCustodianRecoveryKey(_: AddCustodianRecoveryKeyRequest, completion: @escaping (AddCustodianRecoveryKeyResponse?, Error?) -> Void) {
852 completion(AddCustodianRecoveryKeyResponse(), nil)
853 }
854 func resetAccountCdpcontents(_: ResetAccountCDPContentsRequest, completion: @escaping (ResetAccountCDPContentsResponse?, Error?) -> Void) {
855 completion(ResetAccountCDPContentsResponse(), nil)
856 }
857 }
858
859 extension FakeCuttlefishServer: CloudKitCode.Invocable {
860 func invoke<RequestType, ResponseType>(function: String,
861 request: RequestType,
862 completion: @escaping (ResponseType?, Error?) -> Void) {
863 // Ideally we'd pattern match on both request and completion, but that crashes the swift compiler at this time (<rdar://problem/54412402>)
864 switch request {
865 case let request as ResetRequest:
866 self.reset(request, completion: completion as! (ResetResponse?, Error?) -> Void)
867 return
868 case let request as EstablishRequest:
869 self.establish(request, completion: completion as! (EstablishResponse?, Error?) -> Void)
870 return
871 case let request as JoinWithVoucherRequest:
872 self.joinWithVoucher(request, completion: completion as! (JoinWithVoucherResponse?, Error?) -> Void)
873 return
874 case let request as UpdateTrustRequest:
875 self.updateTrust(request, completion: completion as! (UpdateTrustResponse?, Error?) -> Void)
876 return
877 case let request as SetRecoveryKeyRequest:
878 self.setRecoveryKey(request, completion: completion as! (SetRecoveryKeyResponse?, Error?) -> Void)
879 return
880 case let request as FetchChangesRequest:
881 self.fetchChanges(request, completion: completion as! (FetchChangesResponse?, Error?) -> Void)
882 return
883 case let request as FetchViableBottlesRequest:
884 self.fetchViableBottles(request, completion: completion as! (FetchViableBottlesResponse?, Error?) -> Void)
885 return
886 case let request as FetchPolicyDocumentsRequest:
887 self.fetchPolicyDocuments(request, completion: completion as! (FetchPolicyDocumentsResponse?, Error?) -> Void)
888 return
889 case let request as ValidatePeersRequest:
890 self.validatePeers(request, completion: completion as! (ValidatePeersResponse?, Error?) -> Void)
891 return
892 case let request as ReportHealthRequest:
893 self.reportHealth(request, completion: completion as! (ReportHealthResponse?, Error?) -> Void)
894 return
895 case let request as PushHealthInquiryRequest:
896 self.pushHealthInquiry(request, completion: completion as! (PushHealthInquiryResponse?, Error?) -> Void)
897 return
898 case let request as GetRepairActionRequest:
899 self.getRepairAction(request, completion: completion as! (GetRepairActionResponse?, Error?) -> Void)
900 return
901 default:
902 abort()
903 }
904 }
905 }