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