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