]> git.saurik.com Git - apple/security.git/blob - keychain/TrustedPeersHelperUnitTests/FakeCuttlefish.swift
Security-59306.41.2.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 Foundation
10
11 enum FakeCuttlefishError: Error {
12 case notEmpty
13 case unknownChangeToken
14 case unknownPeerID
15 }
16
17 enum FakeCuttlefishOpinion {
18 case trusts
19 case trustsByPreapproval
20 case excludes
21 }
22
23 struct FakeCuttlefishAssertion: CustomStringConvertible {
24 let peer: String
25 let opinion: FakeCuttlefishOpinion
26 let target: String
27
28 func check(peer: Peer?, target: Peer?) -> Bool {
29 guard let peer = peer else {
30 return false
31 }
32
33 guard peer.hasDynamicInfoAndSig else {
34 // No opinions? You've failed this assertion.
35 return false
36 }
37
38 let dynamicInfo = TPPeerDynamicInfo(data: peer.dynamicInfoAndSig.peerDynamicInfo, sig: peer.dynamicInfoAndSig.sig)
39 guard let realDynamicInfo = dynamicInfo else {
40 return false
41 }
42
43 let targetPermanentInfo: TPPeerPermanentInfo? =
44 target != nil ? TPPeerPermanentInfo(peerID: self.target,
45 data: target!.permanentInfoAndSig.peerPermanentInfo,
46 sig: target!.permanentInfoAndSig.sig,
47 keyFactory: TPECPublicKeyFactory())
48 : nil
49
50 switch self.opinion {
51 case .trusts:
52 return realDynamicInfo.includedPeerIDs.contains(self.target)
53 case .trustsByPreapproval:
54 guard let pubSignSPKI = targetPermanentInfo?.signingPubKey.spki() else {
55 return false
56 }
57 let hash = TPHashBuilder.hash(with: .SHA256, of: pubSignSPKI)
58 return realDynamicInfo.preapprovals.contains(hash)
59 case .excludes:
60 return realDynamicInfo.excludedPeerIDs.contains(self.target)
61 }
62 }
63
64 var description: String {
65 return "DCA:(\(self.peer)\(self.opinion)\(self.target))"
66 }
67 }
68
69 @objc class FakeCuttlefishNotify: NSObject {
70 let pushes: (Data) -> Void
71 let containerName: String
72 @objc init(_ containerName: String, pushes: @escaping (Data) -> Void) {
73 self.containerName = containerName
74 self.pushes = pushes
75 }
76
77 @objc public func notify(_ function: String) throws {
78 let notification: [String: Dictionary<String, Any>] = [
79 "aps": ["content-available": 1],
80 "cf": [
81 "f": function,
82 "c": self.containerName,
83 ],
84 ]
85 let payload: Data
86 do {
87 payload = try JSONSerialization.data(withJSONObject: notification)
88 } catch {
89 throw error
90 }
91 self.pushes(payload)
92 }
93 }
94
95 extension ViewKey {
96 func fakeRecord(zoneID: CKRecordZone.ID) -> CKRecord {
97 let recordID = CKRecord.ID(__recordName: self.uuid, zoneID: zoneID)
98 let record = CKRecord(recordType: SecCKRecordIntermediateKeyType, recordID: recordID)
99
100 record[SecCKRecordWrappedKeyKey] = self.wrappedkeyBase64
101
102 switch(self.keyclass) {
103 case .tlk:
104 record[SecCKRecordKeyClassKey] = "tlk"
105 case .classA:
106 record[SecCKRecordKeyClassKey] = "classA"
107 case .classC:
108 record[SecCKRecordKeyClassKey] = "classC"
109 case .UNRECOGNIZED:
110 abort()
111 }
112
113 if self.parentkeyUuid.count > 0 {
114 // TODO: no idea how to tell it about the 'verify' action
115 record[SecCKRecordParentKeyRefKey] = CKRecord.Reference(recordID: CKRecord.ID(__recordName: self.parentkeyUuid, zoneID: zoneID), action: .none)
116 }
117
118 return record
119 }
120
121 func fakeKeyPointer(zoneID: CKRecordZone.ID) -> CKRecord {
122 let recordName: String
123 switch(self.keyclass) {
124 case .tlk:
125 recordName = "tlk"
126 case .classA:
127 recordName = "classA"
128 case .classC:
129 recordName = "classC"
130 case .UNRECOGNIZED:
131 abort()
132 }
133
134 let recordID = CKRecord.ID(__recordName: recordName, zoneID: zoneID)
135 let record = CKRecord(recordType: SecCKRecordCurrentKeyType, recordID: recordID)
136
137 // TODO: no idea how to tell it about the 'verify' action
138 record[SecCKRecordParentKeyRefKey] = CKRecord.Reference(recordID: CKRecord.ID(__recordName: self.uuid, zoneID: zoneID), action: .none)
139
140 return record
141 }
142 }
143
144 extension TLKShare {
145 func fakeRecord(zoneID: CKRecordZone.ID) -> CKRecord {
146 let recordID = CKRecord.ID(__recordName: "tlkshare-\(self.keyUuid)::\(self.receiver)::\(self.sender)", zoneID: zoneID)
147 let record = CKRecord(recordType: SecCKRecordTLKShareType, recordID: recordID)
148
149 record[SecCKRecordSenderPeerID] = self.sender
150 record[SecCKRecordReceiverPeerID] = self.receiver
151 record[SecCKRecordReceiverPublicEncryptionKey] = self.receiverPublicEncryptionKey
152 record[SecCKRecordCurve] = self.curve
153 record[SecCKRecordVersion] = self.version
154 record[SecCKRecordEpoch] = self.epoch
155 record[SecCKRecordPoisoned] = self.poisoned
156
157 // TODO: no idea how to tell it about the 'verify' action
158 record[SecCKRecordParentKeyRefKey] = CKRecord.Reference(recordID: CKRecord.ID(__recordName: self.keyUuid, zoneID: zoneID), action: .none)
159
160 record[SecCKRecordWrappedKeyKey] = self.wrappedkey
161 record[SecCKRecordSignature] = self.signature
162
163 return record
164 }
165 }
166
167 class FakeCuttlefishServer: CuttlefishAPIAsync {
168
169 struct State {
170 var peersByID: [String: Peer] = [:]
171 var recoverySigningPubKey: Data?
172 var recoveryEncryptionPubKey: Data?
173 var bottles: [Bottle] = []
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 nextFetchErrors: [Error] = []
195 var fetchViableBottlesError: [Error] = []
196 var nextJoinErrors: [Error] = []
197 var nextUpdateTrustErrors: [Error] = []
198 var returnNoActionResponse: Bool = false
199 var returnRepairAccountResponse: Bool = false
200 var returnRepairEscrowResponse: Bool = false
201 var returnResetOctagonResponse: Bool = false
202 var returnRepairErrorResponse: Error?
203 var fetchChangesCalledCount: Int = 0
204
205 var nextEstablishReturnsMoreChanges: Bool = false
206
207 var establishListener: ((EstablishRequest) -> NSError?)?
208 var updateListener: ((UpdateTrustRequest) -> NSError?)?
209 var fetchChangesListener: ((FetchChangesRequest) -> NSError?)?
210 var joinListener: ((JoinWithVoucherRequest) -> NSError?)?
211 var healthListener: ((GetRepairActionRequest) -> NSError?)?
212 var fetchViableBottlesListener: ((FetchViableBottlesRequest) -> NSError?)?
213 var resetListener: ((ResetRequest) -> NSError?)?
214
215 var fetchViableBottlesDontReturnBottleWithID: String?
216
217 init(_ notify: FakeCuttlefishNotify?, ckZones: NSMutableDictionary, ckksZoneKeys: NSMutableDictionary) {
218 self.notify = notify
219 self.fakeCKZones = ckZones
220 self.ckksZoneKeys = ckksZoneKeys
221 }
222
223 func deleteAllPeers() {
224 self.state.peersByID.removeAll()
225 self.makeSnapshot()
226 }
227
228 func pushNotify(_ function: String) {
229 if let notify = self.notify {
230 do {
231 try notify.notify(function)
232 } catch {
233 }
234 }
235 }
236
237 static func makeCloudKitCuttlefishError(code: CuttlefishErrorCode, retryAfter: TimeInterval = 5) -> NSError {
238 let cuttlefishError = CKPrettyError(domain: CuttlefishErrorDomain,
239 code: code.rawValue,
240 userInfo: [CuttlefishErrorRetryAfterKey: retryAfter])
241 let internalError = CKPrettyError(domain: CKInternalErrorDomain,
242 code: CKInternalErrorCode.errorInternalPluginError.rawValue,
243 userInfo: [NSUnderlyingErrorKey: cuttlefishError, ])
244 let ckError = CKPrettyError(domain: CKErrorDomain,
245 code: CKError.serverRejectedRequest.rawValue,
246 userInfo: [NSUnderlyingErrorKey: internalError,
247 CKErrorServerDescriptionKey: "Fake: FunctionError domain: CuttlefishError, code: \(code),\(code.rawValue)",
248 ])
249 return ckError
250 }
251
252 func makeSnapshot() {
253 self.currentChange += 1
254 self.currentChangeToken = "change\(self.currentChange)"
255 self.snapshotsByChangeToken[self.currentChangeToken] = self.state
256 }
257
258 func changesSince(snapshot: State) -> Changes {
259 return Changes.with { changes in
260 changes.changeToken = self.currentChangeToken
261
262 changes.differences = self.state.peersByID.compactMap({ (key: String, value: Peer) -> PeerDifference? in
263 let old = snapshot.peersByID[key]
264 if old == nil {
265 return PeerDifference.with {
266 $0.add = value
267 }
268 } else if old != value {
269 return PeerDifference.with {
270 $0.update = value
271 }
272 } else {
273 return nil
274 }
275 })
276 snapshot.peersByID.forEach { (key: String, _: Peer) in
277 if nil == self.state.peersByID[key] {
278 changes.differences.append(PeerDifference.with {
279 $0.remove = Peer.with {
280 $0.peerID = key
281 }
282 })
283 }
284 }
285
286 if self.state.recoverySigningPubKey != snapshot.recoverySigningPubKey {
287 changes.recoverySigningPubKey = self.state.recoverySigningPubKey ?? Data()
288 }
289 if self.state.recoveryEncryptionPubKey != snapshot.recoveryEncryptionPubKey {
290 changes.recoveryEncryptionPubKey = self.state.recoveryEncryptionPubKey ?? Data()
291 }
292
293 }
294 }
295
296 func reset(_ request: ResetRequest, completion: @escaping (ResetResponse?, Error?) -> Void) {
297 print("FakeCuttlefish: reset called")
298 if let resetListener = self.resetListener {
299 let possibleError = resetListener(request)
300 guard possibleError == nil else {
301 completion(nil, possibleError)
302 return
303 }
304 }
305 self.state = State()
306 self.makeSnapshot()
307 completion(ResetResponse.with {
308 $0.changes = self.changesSince(snapshot: State())
309 }, nil)
310 self.pushNotify("reset")
311 }
312
313 func newKeysConflict(viewKeys: [ViewKeys]) -> Bool {
314 #if OCTAGON_TEST_FILL_ZONEKEYS
315 for keys in viewKeys {
316 let rzid = CKRecordZone.ID(zoneName: keys.view)
317
318 if let currentViewKeys = self.ckksZoneKeys[rzid] as? CKKSCurrentKeySet {
319 // Uploading the current view keys is okay. Fail only if they don't match
320 if keys.newTlk.uuid != currentViewKeys.tlk!.uuid ||
321 keys.newClassA.uuid != currentViewKeys.classA!.uuid ||
322 keys.newClassC.uuid != currentViewKeys.classC!.uuid {
323 return true
324 }
325 }
326 }
327 #endif
328
329 return false
330 }
331
332 func store(viewKeys: [ViewKeys]) -> [CKRecord] {
333 var allRecords: [CKRecord] = []
334
335 viewKeys.forEach { viewKeys in
336 let rzid = CKRecordZone.ID(zoneName: viewKeys.view)
337 self.state.viewKeys[rzid] = viewKeys
338
339 // Real cuttlefish makes these zones for you
340 if self.fakeCKZones[rzid] == nil {
341 self.fakeCKZones[rzid] = FakeCKZone(zone: rzid)
342 }
343
344 if let fakeZone = self.fakeCKZones[rzid] as? FakeCKZone {
345 fakeZone.queue.sync {
346 let tlkRecord = viewKeys.newTlk.fakeRecord(zoneID: rzid)
347 let classARecord = viewKeys.newClassA.fakeRecord(zoneID: rzid)
348 let classCRecord = viewKeys.newClassC.fakeRecord(zoneID: rzid)
349
350 let tlkPointerRecord = viewKeys.newTlk.fakeKeyPointer(zoneID: rzid)
351 let classAPointerRecord = viewKeys.newClassA.fakeKeyPointer(zoneID: rzid)
352 let classCPointerRecord = viewKeys.newClassC.fakeKeyPointer(zoneID: rzid)
353
354 // Some tests don't link everything needed to make zonekeys
355 // Those tests don't get this nice behavior
356 #if OCTAGON_TEST_FILL_ZONEKEYS
357 let zoneKeys = self.ckksZoneKeys[rzid] as? ZoneKeys ?? ZoneKeys(forZoneName: rzid.zoneName)
358 self.ckksZoneKeys[rzid] = zoneKeys
359
360 zoneKeys.tlk = CKKSKey(ckRecord: tlkRecord)
361 zoneKeys.classA = CKKSKey(ckRecord: classARecord)
362 zoneKeys.classC = CKKSKey(ckRecord: classCRecord)
363
364 zoneKeys.currentTLKPointer = CKKSCurrentKeyPointer(ckRecord: tlkPointerRecord)
365 zoneKeys.currentClassAPointer = CKKSCurrentKeyPointer(ckRecord: classAPointerRecord)
366 zoneKeys.currentClassCPointer = CKKSCurrentKeyPointer(ckRecord: classCPointerRecord)
367 #endif
368
369 let zoneRecords = [tlkRecord,
370 classARecord,
371 classCRecord,
372 tlkPointerRecord,
373 classAPointerRecord,
374 classCPointerRecord, ]
375 // TODO a rolled tlk too
376
377 zoneRecords.forEach { record in
378 fakeZone._onqueueAdd(toZone: record)
379 }
380 allRecords.append(contentsOf: zoneRecords)
381 }
382 } else {
383 // we made the zone above, shoudn't ever get here
384 print("Received an unexpected zone id: \(rzid)")
385 abort()
386 }
387 }
388 return allRecords
389 }
390
391 func store(tlkShares: [TLKShare]) -> [CKRecord] {
392 var allRecords: [CKRecord] = []
393
394 tlkShares.forEach { share in
395 let rzid = CKRecordZone.ID(zoneName: share.view)
396
397 var c = self.state.tlkShares[rzid] ?? []
398 c.append(share)
399 self.state.tlkShares[rzid] = c
400
401 if let fakeZone = self.fakeCKZones[rzid] as? FakeCKZone {
402 let record = share.fakeRecord(zoneID: rzid)
403 fakeZone.add(toZone: record)
404 allRecords.append(record)
405
406 } else {
407 print("Received an unexpected zone id: \(rzid)")
408 }
409 }
410
411 return allRecords
412 }
413
414 func establish(_ request: EstablishRequest, completion: @escaping (EstablishResponse?, Error?) -> Void) {
415 print("FakeCuttlefish: establish called")
416 if !self.state.peersByID.isEmpty {
417 completion(nil, FakeCuttlefishError.notEmpty)
418 }
419
420 // Before performing write, check if we should error
421 if let establishListener = self.establishListener {
422 let possibleError = establishListener(request)
423 guard possibleError == nil else {
424 completion(nil, possibleError)
425 return
426 }
427 }
428
429 // Also check if we should bail due to conflicting viewKeys
430 if self.newKeysConflict(viewKeys: request.viewKeys) {
431 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
432 return
433 }
434
435 self.state.peersByID[request.peer.peerID] = request.peer
436 self.state.bottles.append(request.bottle)
437
438 var keyRecords: [CKRecord] = []
439 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
440 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
441
442 self.makeSnapshot()
443
444 let response = EstablishResponse.with {
445 if self.nextEstablishReturnsMoreChanges {
446 $0.changes = Changes.with {
447 $0.more = true
448 }
449 self.nextEstablishReturnsMoreChanges = false
450 } else {
451 $0.changes = self.changesSince(snapshot: State())
452 }
453 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
454 }
455
456 completion(response, nil)
457 self.pushNotify("establish")
458 }
459
460 func joinWithVoucher(_ request: JoinWithVoucherRequest, completion: @escaping (JoinWithVoucherResponse?, Error?) -> Void) {
461 print("FakeCuttlefish: joinWithVoucher called")
462
463 if let joinListener = self.joinListener {
464 let possibleError = joinListener(request)
465 guard possibleError == nil else {
466 completion(nil, possibleError)
467 return
468 }
469 }
470
471 if let injectedError = self.nextJoinErrors.first {
472 print("FakeCuttlefish: erroring with injected error: ", String(describing: injectedError))
473 self.nextJoinErrors.removeFirst()
474 completion(nil, injectedError)
475 return
476 }
477
478 // Also check if we should bail due to conflicting viewKeys
479 if self.newKeysConflict(viewKeys: request.viewKeys) {
480 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
481 return
482 }
483
484 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
485 completion(nil, FakeCuttlefishError.unknownChangeToken)
486 return
487 }
488 self.state.peersByID[request.peer.peerID] = request.peer
489 self.state.bottles.append(request.bottle)
490
491 var keyRecords: [CKRecord] = []
492 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
493 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
494
495 self.makeSnapshot()
496
497 completion(JoinWithVoucherResponse.with {
498 $0.changes = self.changesSince(snapshot: snapshot)
499 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
500 }, nil)
501 self.pushNotify("joinWithVoucher")
502 }
503
504 func updateTrust(_ request: UpdateTrustRequest, completion: @escaping (UpdateTrustResponse?, Error?) -> Void) {
505 print("FakeCuttlefish: updateTrust called: changeToken: ", request.changeToken, "peerID: ", request.peerID)
506
507 if let injectedError = self.nextUpdateTrustErrors.first {
508 print("FakeCuttlefish: updateTrust erroring with injected error: ", String(describing: injectedError))
509 self.nextUpdateTrustErrors.removeFirst()
510 completion(nil, injectedError)
511 return
512 }
513
514 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
515 completion(nil, FakeCuttlefishError.unknownChangeToken)
516 return
517 }
518 guard var peer = self.state.peersByID[request.peerID] else {
519 completion(nil, FakeCuttlefishError.unknownPeerID)
520 return
521 }
522 if request.hasStableInfoAndSig {
523 peer.stableInfoAndSig = request.stableInfoAndSig
524 }
525 if request.hasDynamicInfoAndSig {
526 peer.dynamicInfoAndSig = request.dynamicInfoAndSig
527 }
528 self.state.peersByID[request.peerID] = peer
529
530 // Before performing write, check if we should error
531 if let updateListener = self.updateListener {
532 let possibleError = updateListener(request)
533 guard possibleError == nil else {
534 completion(nil, possibleError)
535 return
536 }
537 }
538
539 // Also check if we should bail due to conflicting viewKeys
540 if self.newKeysConflict(viewKeys: request.viewKeys) {
541 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
542 return
543 }
544
545 var keyRecords: [CKRecord] = []
546 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
547 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
548
549 let newDynamicInfo = TPPeerDynamicInfo(data: peer.dynamicInfoAndSig.peerDynamicInfo,
550 sig: peer.dynamicInfoAndSig.sig)
551 print("FakeCuttlefish: new peer dynamicInfo: ", request.peerID, String(describing: newDynamicInfo?.dictionaryRepresentation()))
552
553 self.makeSnapshot()
554 let response = UpdateTrustResponse.with {
555 $0.changes = self.changesSince(snapshot: snapshot)
556 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
557 }
558
559 completion(response, nil)
560 self.pushNotify("updateTrust")
561 }
562
563 func setRecoveryKey(_ request: SetRecoveryKeyRequest, completion: @escaping (SetRecoveryKeyResponse?, Error?) -> Void) {
564 print("FakeCuttlefish: setRecoveryKey called")
565 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
566 completion(nil, FakeCuttlefishError.unknownChangeToken)
567 return
568 }
569 self.state.recoverySigningPubKey = request.recoverySigningPubKey
570 self.state.recoveryEncryptionPubKey = request.recoveryEncryptionPubKey
571 self.state.peersByID[request.peerID]?.stableInfoAndSig = request.stableInfoAndSig
572 self.makeSnapshot()
573 completion(SetRecoveryKeyResponse.with {
574 $0.changes = self.changesSince(snapshot: snapshot)
575 }, nil)
576 self.pushNotify("setRecoveryKey")
577 }
578
579 func fetchChanges(_ request: FetchChangesRequest, completion: @escaping (FetchChangesResponse?, Error?) -> Void) {
580 print("FakeCuttlefish: fetchChanges called: ", request.changeToken)
581
582 self.fetchChangesCalledCount += 1
583
584 if let fetchChangesListener = self.fetchChangesListener {
585 let possibleError = fetchChangesListener(request)
586 guard possibleError == nil else {
587 completion(nil, possibleError)
588 return
589 }
590 }
591
592 if let injectedError = self.nextFetchErrors.first {
593 print("FakeCuttlefish: fetchChanges erroring with injected error: ", String(describing: injectedError))
594 self.nextFetchErrors.removeFirst()
595 completion(nil, injectedError)
596 return
597 }
598
599 let snapshot: State
600 if request.changeToken == "" {
601 snapshot = State()
602 } else {
603 guard let s = self.snapshotsByChangeToken[request.changeToken] else {
604 completion(nil, FakeCuttlefishError.unknownChangeToken)
605 return
606 }
607 snapshot = s
608 }
609 let response = FetchChangesResponse.with {
610 $0.changes = self.changesSince(snapshot: snapshot)
611 }
612
613 completion(response, nil)
614 }
615
616 func fetchViableBottles(_ request: FetchViableBottlesRequest, completion: @escaping (FetchViableBottlesResponse?, Error?) -> Void) {
617 print("FakeCuttlefish: fetchViableBottles called")
618
619 if let fetchViableBottlesListener = self.fetchViableBottlesListener {
620 let possibleError = fetchViableBottlesListener(request)
621 guard possibleError == nil else {
622 completion(nil, possibleError)
623 return
624 }
625 }
626
627 if let injectedError = self.fetchViableBottlesError.first {
628 print("FakeCuttlefish: fetchViableBottles erroring with injected error: ", String(describing: injectedError))
629 self.fetchViableBottlesError.removeFirst()
630 completion(nil, injectedError)
631 return
632 }
633
634 let bottles = self.state.bottles.filter { $0.bottleID != fetchViableBottlesDontReturnBottleWithID }
635 completion(FetchViableBottlesResponse.with {
636 $0.viableBottles = bottles.compactMap { bottle in
637 EscrowPair.with {
638 $0.escrowRecordID = bottle.bottleID
639 $0.bottle = bottle
640 }
641 }
642 }, nil)
643 }
644
645 func fetchPolicyDocuments(_ request: FetchPolicyDocumentsRequest,
646 completion: @escaping (FetchPolicyDocumentsResponse?, Error?) -> Void) {
647 print("FakeCuttlefish: fetchPolicyDocuments called")
648 var response = FetchPolicyDocumentsResponse()
649
650 let policies = builtInPolicyDocuments()
651 let dummyPolicies = Dictionary(uniqueKeysWithValues: policies.map({ ($0.policyVersion, ($0.policyHash, $0.protobuf)) }))
652 for key in request.keys {
653 guard let (hash, data) = dummyPolicies[key.version] else {
654 continue
655 }
656 if hash == key.hash {
657 response.entries.append(PolicyDocumentMapEntry.with { $0.key = key; $0.value = data })
658 }
659 }
660 completion(response, nil)
661 }
662
663 func assertCuttlefishState(_ assertion: FakeCuttlefishAssertion) -> Bool {
664 return assertion.check(peer: self.state.peersByID[assertion.peer], target: self.state.peersByID[assertion.target])
665 }
666
667 func validatePeers(_: ValidatePeersRequest, completion: @escaping (ValidatePeersResponse?, Error?) -> Void) {
668 var response = ValidatePeersResponse()
669 response.validatorsHealth = 0.0
670 response.results = []
671 completion(response, nil)
672 }
673 func reportHealth(_: ReportHealthRequest, completion: @escaping (ReportHealthResponse?, Error?) -> Void) {
674 completion(ReportHealthResponse(), nil)
675 }
676 func pushHealthInquiry(_: PushHealthInquiryRequest, completion: @escaping (PushHealthInquiryResponse?, Error?) -> Void) {
677 completion(PushHealthInquiryResponse(), nil)
678 }
679
680 func getRepairAction(_ request: GetRepairActionRequest, completion: @escaping (GetRepairActionResponse?, Error?) -> Void) {
681 print("FakeCuttlefish: getRepairAction called")
682
683 if let healthListener = self.healthListener {
684 let possibleError = healthListener(request)
685 guard possibleError == nil else {
686 completion(nil, possibleError)
687 return
688 }
689 }
690
691 if self.returnRepairEscrowResponse {
692 let response = GetRepairActionResponse.with {
693 $0.repairAction = .postRepairEscrow
694 }
695 completion(response, nil)
696 } else if self.returnRepairAccountResponse {
697 let response = GetRepairActionResponse.with {
698 $0.repairAction = .postRepairAccount
699 }
700 completion(response, nil)
701 } else if self.returnResetOctagonResponse {
702 let response = GetRepairActionResponse.with {
703 $0.repairAction = .resetOctagon
704 }
705 completion(response, nil)
706 } else if self.returnNoActionResponse {
707 let response = GetRepairActionResponse.with {
708 $0.repairAction = .noAction
709 }
710 completion(response, nil)
711 } else if self.returnRepairErrorResponse != nil {
712 let response = GetRepairActionResponse.with {
713 $0.repairAction = .noAction
714 }
715 completion(response, self.returnRepairErrorResponse)
716 } else {
717 completion(GetRepairActionResponse(), nil)
718 }
719 }
720
721 func getSupportAppInfo(_: GetSupportAppInfoRequest, completion: @escaping (GetSupportAppInfoResponse?, Error?) -> Void) {
722 completion(GetSupportAppInfoResponse(), nil)
723 }
724 }
725
726 extension FakeCuttlefishServer: CloudKitCode.Invocable {
727 func invoke<RequestType, ResponseType>(function: String,
728 request: RequestType,
729 completion: @escaping (ResponseType?, Error?) -> Void) {
730 // Ideally we'd pattern match on both request and completion, but that crashes the swift compiler at this time (<rdar://problem/54412402>)
731 switch request {
732 case let request as ResetRequest:
733 self.reset(request, completion: completion as! (ResetResponse?, Error?) -> Void)
734 return
735 case let request as EstablishRequest:
736 self.establish(request, completion: completion as! (EstablishResponse?, Error?) -> Void)
737 return
738 case let request as JoinWithVoucherRequest:
739 self.joinWithVoucher(request, completion: completion as! (JoinWithVoucherResponse?, Error?) -> Void)
740 return
741 case let request as UpdateTrustRequest:
742 self.updateTrust(request, completion: completion as! (UpdateTrustResponse?, Error?) -> Void)
743 return
744 case let request as SetRecoveryKeyRequest:
745 self.setRecoveryKey(request, completion: completion as! (SetRecoveryKeyResponse?, Error?) -> Void)
746 return
747 case let request as FetchChangesRequest:
748 self.fetchChanges(request, completion: completion as! (FetchChangesResponse?, Error?) -> Void)
749 return
750 case let request as FetchViableBottlesRequest:
751 self.fetchViableBottles(request, completion: completion as! (FetchViableBottlesResponse?, Error?) -> Void)
752 return
753 case let request as FetchPolicyDocumentsRequest:
754 self.fetchPolicyDocuments(request, completion: completion as! (FetchPolicyDocumentsResponse?, Error?) -> Void)
755 return
756 case let request as ValidatePeersRequest:
757 self.validatePeers(request, completion: completion as! (ValidatePeersResponse?, Error?) -> Void)
758 return
759 case let request as ReportHealthRequest:
760 self.reportHealth(request, completion: completion as! (ReportHealthResponse?, Error?) -> Void)
761 return
762 case let request as PushHealthInquiryRequest:
763 self.pushHealthInquiry(request, completion: completion as! (PushHealthInquiryResponse?, Error?) -> Void)
764 return
765 case let request as GetRepairActionRequest:
766 self.getRepairAction(request, completion: completion as! (GetRepairActionResponse?, Error?) -> Void)
767 return
768 default:
769 abort()
770 }
771 }
772 }