]> git.saurik.com Git - apple/security.git/blob - keychain/TrustedPeersHelperUnitTests/FakeCuttlefish.swift
Security-59306.11.20.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
214 var fetchViableBottlesDontReturnBottleWithID: String?
215
216 init(_ notify: FakeCuttlefishNotify?, ckZones: NSMutableDictionary, ckksZoneKeys: NSMutableDictionary) {
217 self.notify = notify
218 self.fakeCKZones = ckZones
219 self.ckksZoneKeys = ckksZoneKeys
220 }
221
222 func deleteAllPeers() {
223 self.state.peersByID.removeAll()
224 self.makeSnapshot()
225 }
226
227 func pushNotify(_ function: String) {
228 if let notify = self.notify {
229 do {
230 try notify.notify(function)
231 } catch {
232 }
233 }
234 }
235
236 static func makeCloudKitCuttlefishError(code: CuttlefishErrorCode) -> NSError {
237 return CKPrettyError(domain: CKInternalErrorDomain,
238 code: CKInternalErrorCode.errorInternalPluginError.rawValue,
239 userInfo: [NSUnderlyingErrorKey: NSError(domain: CuttlefishErrorDomain,
240 code: code.rawValue,
241 userInfo: nil)])
242 }
243
244 func makeSnapshot() {
245 self.currentChange += 1
246 self.currentChangeToken = "change\(self.currentChange)"
247 self.snapshotsByChangeToken[self.currentChangeToken] = self.state
248 }
249
250 func changesSince(snapshot: State) -> Changes {
251 return Changes.with { changes in
252 changes.changeToken = self.currentChangeToken
253
254 changes.differences = self.state.peersByID.compactMap({ (key: String, value: Peer) -> PeerDifference? in
255 let old = snapshot.peersByID[key]
256 if old == nil {
257 return PeerDifference.with {
258 $0.add = value
259 }
260 } else if old != value {
261 return PeerDifference.with {
262 $0.update = value
263 }
264 } else {
265 return nil
266 }
267 })
268 snapshot.peersByID.forEach { (key: String, _: Peer) in
269 if nil == self.state.peersByID[key] {
270 changes.differences.append(PeerDifference.with {
271 $0.remove = Peer.with {
272 $0.peerID = key
273 }
274 })
275 }
276 }
277
278 if self.state.recoverySigningPubKey != snapshot.recoverySigningPubKey {
279 changes.recoverySigningPubKey = self.state.recoverySigningPubKey ?? Data()
280 }
281 if self.state.recoveryEncryptionPubKey != snapshot.recoveryEncryptionPubKey {
282 changes.recoveryEncryptionPubKey = self.state.recoveryEncryptionPubKey ?? Data()
283 }
284
285 }
286 }
287
288 func reset(_ request: ResetRequest, completion: @escaping (ResetResponse?, Error?) -> Void) {
289 print("FakeCuttlefish: reset called")
290 self.state = State()
291 self.makeSnapshot()
292 completion(ResetResponse.with {
293 $0.changes = self.changesSince(snapshot: State())
294 }, nil)
295 self.pushNotify("reset")
296 }
297
298 func newKeysConflict(viewKeys: [ViewKeys]) -> Bool {
299 #if OCTAGON_TEST_FILL_ZONEKEYS
300 for keys in viewKeys {
301 let rzid = CKRecordZone.ID(zoneName: keys.view)
302
303 if let currentViewKeys = self.ckksZoneKeys[rzid] as? CKKSCurrentKeySet {
304 // Uploading the current view keys is okay. Fail only if they don't match
305 if keys.newTlk.uuid != currentViewKeys.tlk!.uuid ||
306 keys.newClassA.uuid != currentViewKeys.classA!.uuid ||
307 keys.newClassC.uuid != currentViewKeys.classC!.uuid {
308 return true
309 }
310 }
311 }
312 #endif
313
314 return false
315 }
316
317 func store(viewKeys: [ViewKeys]) -> [CKRecord] {
318 var allRecords: [CKRecord] = []
319
320 viewKeys.forEach { viewKeys in
321 let rzid = CKRecordZone.ID(zoneName: viewKeys.view)
322 self.state.viewKeys[rzid] = viewKeys
323
324 // Real cuttlefish makes these zones for you
325 if self.fakeCKZones[rzid] == nil {
326 self.fakeCKZones[rzid] = FakeCKZone(zone: rzid)
327 }
328
329 if let fakeZone = self.fakeCKZones[rzid] as? FakeCKZone {
330 fakeZone.queue.sync {
331 let tlkRecord = viewKeys.newTlk.fakeRecord(zoneID: rzid)
332 let classARecord = viewKeys.newClassA.fakeRecord(zoneID: rzid)
333 let classCRecord = viewKeys.newClassC.fakeRecord(zoneID: rzid)
334
335 let tlkPointerRecord = viewKeys.newTlk.fakeKeyPointer(zoneID: rzid)
336 let classAPointerRecord = viewKeys.newClassA.fakeKeyPointer(zoneID: rzid)
337 let classCPointerRecord = viewKeys.newClassC.fakeKeyPointer(zoneID: rzid)
338
339 // Some tests don't link everything needed to make zonekeys
340 // Those tests don't get this nice behavior
341 #if OCTAGON_TEST_FILL_ZONEKEYS
342 let zoneKeys = self.ckksZoneKeys[rzid] as? ZoneKeys ?? ZoneKeys(forZoneName: rzid.zoneName)
343 self.ckksZoneKeys[rzid] = zoneKeys
344
345 zoneKeys.tlk = CKKSKey(ckRecord: tlkRecord)
346 zoneKeys.classA = CKKSKey(ckRecord: classARecord)
347 zoneKeys.classC = CKKSKey(ckRecord: classCRecord)
348
349 zoneKeys.currentTLKPointer = CKKSCurrentKeyPointer(ckRecord: tlkPointerRecord)
350 zoneKeys.currentClassAPointer = CKKSCurrentKeyPointer(ckRecord: classAPointerRecord)
351 zoneKeys.currentClassCPointer = CKKSCurrentKeyPointer(ckRecord: classCPointerRecord)
352 #endif
353
354 let zoneRecords = [tlkRecord,
355 classARecord,
356 classCRecord,
357 tlkPointerRecord,
358 classAPointerRecord,
359 classCPointerRecord, ]
360 // TODO a rolled tlk too
361
362 zoneRecords.forEach { record in
363 fakeZone._onqueueAdd(toZone: record)
364 }
365 allRecords.append(contentsOf: zoneRecords)
366 }
367 } else {
368 // we made the zone above, shoudn't ever get here
369 print("Received an unexpected zone id: \(rzid)")
370 abort()
371 }
372 }
373 return allRecords
374 }
375
376 func store(tlkShares: [TLKShare]) -> [CKRecord] {
377 var allRecords: [CKRecord] = []
378
379 tlkShares.forEach { share in
380 let rzid = CKRecordZone.ID(zoneName: share.view)
381
382 var c = self.state.tlkShares[rzid] ?? []
383 c.append(share)
384 self.state.tlkShares[rzid] = c
385
386 if let fakeZone = self.fakeCKZones[rzid] as? FakeCKZone {
387 let record = share.fakeRecord(zoneID: rzid)
388 fakeZone.add(toZone: record)
389 allRecords.append(record)
390
391 } else {
392 print("Received an unexpected zone id: \(rzid)")
393 }
394 }
395
396 return allRecords
397 }
398
399 func establish(_ request: EstablishRequest, completion: @escaping (EstablishResponse?, Error?) -> Void) {
400 print("FakeCuttlefish: establish called")
401 if !self.state.peersByID.isEmpty {
402 completion(nil, FakeCuttlefishError.notEmpty)
403 }
404
405 // Before performing write, check if we should error
406 if let establishListener = self.establishListener {
407 let possibleError = establishListener(request)
408 guard possibleError == nil else {
409 completion(nil, possibleError)
410 return;
411 }
412 }
413
414 // Also check if we should bail due to conflicting viewKeys
415 if self.newKeysConflict(viewKeys: request.viewKeys) {
416 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
417 return
418 }
419
420 self.state.peersByID[request.peer.peerID] = request.peer
421 self.state.bottles.append(request.bottle)
422
423 var keyRecords: [CKRecord] = []
424 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
425 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
426
427 self.makeSnapshot()
428
429
430 let response = EstablishResponse.with {
431 if self.nextEstablishReturnsMoreChanges {
432 $0.changes = Changes.with {
433 $0.more = true
434 }
435 self.nextEstablishReturnsMoreChanges = false
436 } else {
437 $0.changes = self.changesSince(snapshot: State())
438 }
439 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
440 }
441
442 completion(response, nil)
443 self.pushNotify("establish")
444 }
445
446 func joinWithVoucher(_ request: JoinWithVoucherRequest, completion: @escaping (JoinWithVoucherResponse?, Error?) -> Void) {
447 print("FakeCuttlefish: joinWithVoucher called")
448
449 if let joinListener = self.joinListener {
450 let possibleError = joinListener(request)
451 guard possibleError == nil else {
452 completion(nil, possibleError)
453 return;
454 }
455 }
456
457 if let injectedError = self.nextJoinErrors.first {
458 print("FakeCuttlefish: erroring with injected error: ", String(describing: injectedError))
459 self.nextJoinErrors.removeFirst()
460 completion(nil, injectedError)
461 return
462 }
463
464 // Also check if we should bail due to conflicting viewKeys
465 if self.newKeysConflict(viewKeys: request.viewKeys) {
466 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
467 return
468 }
469
470 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
471 completion(nil, FakeCuttlefishError.unknownChangeToken)
472 return
473 }
474 self.state.peersByID[request.peer.peerID] = request.peer
475 self.state.bottles.append(request.bottle)
476
477 var keyRecords: [CKRecord] = []
478 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
479 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
480
481 self.makeSnapshot()
482
483 completion(JoinWithVoucherResponse.with {
484 $0.changes = self.changesSince(snapshot: snapshot)
485 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
486 }, nil)
487 self.pushNotify("joinWithVoucher")
488 }
489
490 func updateTrust(_ request: UpdateTrustRequest, completion: @escaping (UpdateTrustResponse?, Error?) -> Void) {
491 print("FakeCuttlefish: updateTrust called: changeToken: ", request.changeToken, "peerID: ", request.peerID)
492
493 if let injectedError = self.nextUpdateTrustErrors.first {
494 print("FakeCuttlefish: updateTrust erroring with injected error: ", String(describing: injectedError))
495 self.nextUpdateTrustErrors.removeFirst()
496 completion(nil, injectedError)
497 return
498 }
499
500 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
501 completion(nil, FakeCuttlefishError.unknownChangeToken)
502 return
503 }
504 guard var peer = self.state.peersByID[request.peerID] else {
505 completion(nil, FakeCuttlefishError.unknownPeerID)
506 return
507 }
508 if request.hasStableInfoAndSig {
509 peer.stableInfoAndSig = request.stableInfoAndSig
510 }
511 if request.hasDynamicInfoAndSig {
512 peer.dynamicInfoAndSig = request.dynamicInfoAndSig
513 }
514 self.state.peersByID[request.peerID] = peer
515
516 // Before performing write, check if we should error
517 if let updateListener = self.updateListener {
518 let possibleError = updateListener(request)
519 guard possibleError == nil else {
520 completion(nil, possibleError)
521 return;
522 }
523 }
524
525 // Also check if we should bail due to conflicting viewKeys
526 if self.newKeysConflict(viewKeys: request.viewKeys) {
527 completion(nil, FakeCuttlefishServer.makeCloudKitCuttlefishError(code: .keyHierarchyAlreadyExists))
528 return
529 }
530
531 var keyRecords: [CKRecord] = []
532 keyRecords.append(contentsOf: store(viewKeys: request.viewKeys))
533 keyRecords.append(contentsOf: store(tlkShares: request.tlkShares))
534
535 let newDynamicInfo = TPPeerDynamicInfo(data: peer.dynamicInfoAndSig.peerDynamicInfo,
536 sig: peer.dynamicInfoAndSig.sig)
537 print("FakeCuttlefish: new peer dynamicInfo: ", request.peerID, String(describing: newDynamicInfo?.dictionaryRepresentation()))
538
539 self.makeSnapshot()
540 let response = UpdateTrustResponse.with {
541 $0.changes = self.changesSince(snapshot: snapshot)
542 $0.zoneKeyHierarchyRecords = keyRecords.map { try! CloudKitCode.Ckcode_RecordTransport($0) }
543 }
544
545 completion(response, nil)
546 self.pushNotify("updateTrust")
547 }
548
549 func setRecoveryKey(_ request: SetRecoveryKeyRequest, completion: @escaping (SetRecoveryKeyResponse?, Error?) -> Void) {
550 print("FakeCuttlefish: setRecoveryKey called")
551 guard let snapshot = self.snapshotsByChangeToken[request.changeToken] else {
552 completion(nil, FakeCuttlefishError.unknownChangeToken)
553 return
554 }
555 self.state.recoverySigningPubKey = request.recoverySigningPubKey
556 self.state.recoveryEncryptionPubKey = request.recoveryEncryptionPubKey
557 self.state.peersByID[request.peerID]?.stableInfoAndSig = request.stableInfoAndSig
558 self.makeSnapshot()
559 completion(SetRecoveryKeyResponse.with {
560 $0.changes = self.changesSince(snapshot: snapshot)
561 }, nil)
562 self.pushNotify("setRecoveryKey")
563 }
564
565 func fetchChanges(_ request: FetchChangesRequest, completion: @escaping (FetchChangesResponse?, Error?) -> Void) {
566 print("FakeCuttlefish: fetchChanges called: ", request.changeToken)
567
568 self.fetchChangesCalledCount += 1
569
570 if let fetchChangesListener = self.fetchChangesListener {
571 let possibleError = fetchChangesListener(request)
572 guard possibleError == nil else {
573 completion(nil, possibleError)
574 return;
575 }
576 }
577
578 if let injectedError = self.nextFetchErrors.first {
579 print("FakeCuttlefish: fetchChanges erroring with injected error: ", String(describing: injectedError))
580 self.nextFetchErrors.removeFirst()
581 completion(nil, injectedError)
582 return
583 }
584
585 let snapshot: State
586 if request.changeToken == "" {
587 snapshot = State()
588 } else {
589 guard let s = self.snapshotsByChangeToken[request.changeToken] else {
590 completion(nil, FakeCuttlefishError.unknownChangeToken)
591 return
592 }
593 snapshot = s
594 }
595 let response = FetchChangesResponse.with {
596 $0.changes = self.changesSince(snapshot: snapshot)
597 }
598
599 completion(response, nil)
600 }
601
602 func fetchViableBottles(_ request: FetchViableBottlesRequest, completion: @escaping (FetchViableBottlesResponse?, Error?) -> Void) {
603 print("FakeCuttlefish: fetchViableBottles called")
604
605 if let fetchViableBottlesListener = self.fetchViableBottlesListener {
606 let possibleError = fetchViableBottlesListener(request)
607 guard possibleError == nil else {
608 completion(nil, possibleError)
609 return;
610 }
611 }
612
613 if let injectedError = self.fetchViableBottlesError.first {
614 print("FakeCuttlefish: fetchViableBottles erroring with injected error: ", String(describing: injectedError))
615 self.fetchViableBottlesError.removeFirst()
616 completion(nil, injectedError)
617 return
618 }
619
620 let bottles = self.state.bottles.filter { $0.bottleID != fetchViableBottlesDontReturnBottleWithID }
621 completion(FetchViableBottlesResponse.with {
622 $0.viableBottles = bottles.compactMap { bottle in
623 EscrowPair.with {
624 $0.escrowRecordID = bottle.bottleID
625 $0.bottle = bottle
626 }
627 }
628 }, nil)
629 }
630
631 func fetchPolicyDocuments(_ request: FetchPolicyDocumentsRequest,
632 completion: @escaping (FetchPolicyDocumentsResponse?, Error?) -> Void) {
633 print("FakeCuttlefish: fetchPolicyDocuments called")
634 var response = FetchPolicyDocumentsResponse()
635
636 let policies = builtInPolicyDocuments()
637 let dummyPolicies = Dictionary(uniqueKeysWithValues: policies.map({ ($0.policyVersion, ($0.policyHash, $0.protobuf)) }))
638 for key in request.keys {
639 guard let (hash, data) = dummyPolicies[key.version] else {
640 continue
641 }
642 if hash == key.hash {
643 response.entries.append(PolicyDocumentMapEntry.with { $0.key = key; $0.value = data })
644 }
645 }
646 completion(response, nil)
647 }
648
649 func assertCuttlefishState(_ assertion: FakeCuttlefishAssertion) -> Bool {
650 return assertion.check(peer: self.state.peersByID[assertion.peer], target: self.state.peersByID[assertion.target])
651 }
652
653 func validatePeers(_: ValidatePeersRequest, completion: @escaping (ValidatePeersResponse?, Error?) -> Void) {
654 var response = ValidatePeersResponse()
655 response.validatorsHealth = 0.0
656 response.results = []
657 completion(response, nil)
658 }
659 func reportHealth(_: ReportHealthRequest, completion: @escaping (ReportHealthResponse?, Error?) -> Void) {
660 completion(ReportHealthResponse(), nil)
661 }
662 func pushHealthInquiry(_: HealthInquiryRequest, completion: @escaping (HealthInquiryResponse?, Error?) -> Void) {
663 completion(HealthInquiryResponse(), nil)
664 }
665
666 func getRepairAction(_ request: GetRepairActionRequest, completion: @escaping (GetRepairActionResponse?, Error?) -> Void) {
667 print("FakeCuttlefish: getRepairAction called")
668
669 if let healthListener = self.healthListener {
670 let possibleError = healthListener(request)
671 guard possibleError == nil else {
672 completion(nil, possibleError)
673 return;
674 }
675 }
676
677 if self.returnRepairEscrowResponse {
678 let response = GetRepairActionResponse.with {
679 $0.repairAction = .postRepairEscrow
680 }
681 completion(response, nil)
682 }
683 else if self.returnRepairAccountResponse {
684 let response = GetRepairActionResponse.with {
685 $0.repairAction = .postRepairAccount
686 }
687 completion(response, nil)
688 }
689 else if self.returnResetOctagonResponse {
690 let response = GetRepairActionResponse.with {
691 $0.repairAction = .resetOctagon
692 }
693 completion(response, nil)
694 }
695 else if self.returnNoActionResponse {
696 let response = GetRepairActionResponse.with {
697 $0.repairAction = .noAction
698 }
699 completion(response, nil)
700 } else if self.returnRepairErrorResponse != nil {
701 let response = GetRepairActionResponse.with {
702 $0.repairAction = .noAction
703 }
704 completion(response, self.returnRepairErrorResponse)
705 }
706 else {
707 completion(GetRepairActionResponse(), nil)
708 }
709 }
710 }
711
712 extension FakeCuttlefishServer : CloudKitCode.Invocable {
713 func invoke<RequestType, ResponseType>(function: String,
714 request: RequestType,
715 completion: @escaping (ResponseType?, Error?) -> Void) {
716 // Ideally we'd pattern match on both request and completion, but that crashes the swift compiler at this time (<rdar://problem/54412402>)
717 switch request {
718 case let request as ResetRequest:
719 self.reset(request, completion: completion as! (ResetResponse?, Error?) -> Void)
720 return
721 case let request as EstablishRequest:
722 self.establish(request, completion: completion as! (EstablishResponse?, Error?) -> Void)
723 return
724 case let request as JoinWithVoucherRequest:
725 self.joinWithVoucher(request, completion: completion as! (JoinWithVoucherResponse?, Error?) -> Void)
726 return
727 case let request as UpdateTrustRequest:
728 self.updateTrust(request, completion: completion as! (UpdateTrustResponse?, Error?) -> Void)
729 return
730 case let request as SetRecoveryKeyRequest:
731 self.setRecoveryKey(request, completion: completion as! (SetRecoveryKeyResponse?, Error?) -> Void)
732 return
733 case let request as FetchChangesRequest:
734 self.fetchChanges(request, completion: completion as! (FetchChangesResponse?, Error?) -> Void)
735 return
736 case let request as FetchViableBottlesRequest:
737 self.fetchViableBottles(request, completion: completion as! (FetchViableBottlesResponse?, Error?) -> Void)
738 return
739 case let request as FetchPolicyDocumentsRequest:
740 self.fetchPolicyDocuments(request, completion: completion as! (FetchPolicyDocumentsResponse?, Error?) -> Void)
741 return
742 case let request as ValidatePeersRequest:
743 self.validatePeers(request, completion: completion as! (ValidatePeersResponse?, Error?) -> Void)
744 return
745 case let request as ReportHealthRequest:
746 self.reportHealth(request, completion: completion as! (ReportHealthResponse?, Error?) -> Void)
747 return
748 case let request as HealthInquiryRequest:
749 self.pushHealthInquiry(request, completion: completion as! (HealthInquiryResponse?, Error?) -> Void)
750 return
751 case let request as GetRepairActionRequest:
752 self.getRepairAction(request, completion: completion as! (GetRepairActionResponse?, Error?) -> Void)
753 return
754 default:
755 abort()
756 }
757 }
758 }