4 let tplogDebug = OSLog(subsystem: "com.apple.security.trustedpeers", category: "debug")
6 // This should definitely use the ArgumentParser library from the Utility package.
7 // However, right now that's not accessible from this code due to build system issues.
10 let programName = CommandLine.arguments[0]
11 var args: ArraySlice<String> = CommandLine.arguments[1...]
13 var context: String = OTDefaultContext
14 var container: String = "com.apple.security.keychain"
16 // Only used for prepare, but in the absence of a real command line library this is the easiest way to get them
18 var machineID: String?
20 var bottleSalt: String = ""
22 // Only used for join, establish and update
23 var preapprovedKeys: [Data]?
24 var deviceName: String?
25 var serialNumber: String?
26 var osVersion: String?
27 var policySecrets: [String: Data]?
32 case distrust(Set<String>)
42 case vouch(String, Data, Data, Data, Data)
43 case vouchWithBottle(String, Data, String)
44 case allow(Set<String>, Bool)
49 print("Usage:", (CommandLine.arguments[0] as NSString).lastPathComponent,
50 "[--container CONTAINER] [--context CONTEXT] COMMAND")
53 print(" allow [--idms] MACHINEID ...")
54 print(" Set the (space-separated) list of machine IDs allowed in the account. If --idms is provided, append the IDMS trusted device list")
55 print(" dump Print the state of the world as this peer knows it")
56 print(" depart Attempt to depart the account and mark yourself as untrusted")
57 print(" distrust PEERID ... Distrust one or more peers by peer ID")
58 print(" establish Calls Cuttlefish Establish, creating a new account-wide trust arena with a single peer (previously generated with prepare")
59 print(" healthInquiry Request peers to check in with reportHealth")
60 print(" join VOUCHER VOUCHERSIG Join a circle using this (base64) voucher and voucherSig")
61 print(" local-reset Resets the local cuttlefish database, and ignores all previous information. Does not change anything off-device")
62 print(" prepare [--modelid MODELID] [--machineid MACHINEID] [--epoch EPOCH] [--bottlesalt BOTTLESALT]")
63 print(" Creates a new identity and returns its attributes. If not provided, modelid and machineid will be given some defaults (ignoring the local device)")
64 print(" supportApp Get SupportApp information from Cuttlefish")
65 print(" update Fetch new information from Cuttlefish, and perform any actions this node deems necessary")
66 print(" validate Vvalidate SOS and Octagon data structures from server side")
67 print(" viable-bottles Show bottles in preference order of server")
68 print(" vouch PEERID PERMANENTINFO PERMANENTINFOSIG STABLEINFO STABLEINFOSIG")
69 print(" Create a voucher for a new peer. permanentInfo, permanentInfoSig, stableInfo, stableInfoSig should be base64 data")
70 print(" vouchWithBottle BOTTLEID ENTROPY SALT")
71 print(" Create a voucher for the ego peer using the given bottle. entropy should be base64 data.")
72 print(" reset Resets Cuttlefish for this account")
74 print("Options applying to `join', `establish' and `update'")
75 print(" --preapprove KEY... Sets the (space-separated base64) list of public keys that are preapproved.")
76 print(" --device-name NAME Sets the device name string.")
77 print(" --os-version VERSION Sets the OS version string.")
78 print(" --policy-version VERSION Sets the policy version.")
79 print(" --policy-secret NAME DATA Adds a name-value pair to policy secrets. DATA must be base-64.")
80 print("Options applying to `vouch' and `join'")
81 print(" --config FILE Configuration file with json data.")
85 func exitUsage(_ code: Int32) -> Never {
90 func extractJSONData(dictionary: [String: Any], key: String) -> Data? {
91 guard let b64string = dictionary[key] as? String else {
94 guard let data = Data(base64Encoded: b64string) else {
100 func jsonFromFile(filename: String) -> [String: Any] {
102 let json: [String: Any]?
104 data = try Data(contentsOf: URL(fileURLWithPath: filename), options: .mappedIfSafe)
105 json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
107 print("Error: failed to parse json file \(filename): \(error)")
110 guard let dictionary = json else {
111 print("Error: failed to get dictionary in file \(filename)")
117 var commands: [Command] = []
118 var argIterator = args.makeIterator()
119 var configurationData: [String: Any]?
121 while let arg = argIterator.next() {
124 let newContainer = argIterator.next()
125 guard newContainer != nil else {
126 print("Error: --container takes a value")
130 container = newContainer!
133 let newContext = argIterator.next()
134 guard newContext != nil else {
135 print("Error: --context takes a value")
139 context = newContext!
142 let newModelID = argIterator.next()
143 guard newModelID != nil else {
144 print("Error: --modelid takes a value")
148 modelID = newModelID!
151 let newMachineID = argIterator.next()
152 guard newMachineID != nil else {
153 print("Error: --machineid takes a value")
157 machineID = newMachineID!
160 let newEpoch = argIterator.next()
161 guard newEpoch != nil else {
162 print("Error: --epoch takes a value")
166 epoch = UInt64(newEpoch!)!
169 let salt = argIterator.next()
170 guard salt != nil else {
171 print("Error: --bottlesalt takes a value")
178 var newPreapprovedKeys: [Data] = []
179 while let arg = argIterator.next() {
180 let data = Data(base64Encoded: arg)
181 guard let key = data else {
182 print("Error: preapproved keys must be base-64 data")
185 newPreapprovedKeys.append(key)
187 preapprovedKeys = newPreapprovedKeys
189 case "--device-name":
190 guard let newDeviceName = argIterator.next() else {
191 print("Error: --device-name takes a string argument")
194 deviceName = newDeviceName
196 case "--serial-number":
197 guard let newSerialNumber = argIterator.next() else {
198 print("Error: --serial-number takes a string argument")
201 serialNumber = newSerialNumber
204 guard let newOsVersion = argIterator.next() else {
205 print("Error: --os-version takes a string argument")
208 osVersion = newOsVersion
210 case "--policy-version":
211 guard let _ = UInt64(argIterator.next() ?? "") else {
212 print("Error: --policy-version takes an integer argument")
215 // Option ignored for now
217 case "--policy-secret":
218 guard let name = argIterator.next(), let dataBase64 = argIterator.next() else {
219 print("Error: --policy-secret takes a name and data")
222 guard let data = Data(base64Encoded: dataBase64) else {
223 print("Error: --policy-secret data must be base-64")
226 if nil == policySecrets {
229 policySecrets![name] = data
232 guard let filename = argIterator.next() else {
233 print("Error: --config file argument missing")
237 configurationData = jsonFromFile(filename: filename)
243 commands.append(.dump)
246 commands.append(.depart)
249 var peerIDs = Set<String>()
250 while let arg = argIterator.next() {
253 commands.append(.distrust(peerIDs))
256 commands.append(.establish)
262 if let configuration = configurationData {
263 guard let voucherData = extractJSONData(dictionary: configuration, key: "voucher") else {
264 print("Error: join needs a voucher")
265 exitUsage(EXIT_FAILURE)
267 guard let voucherSigData = extractJSONData(dictionary: configuration, key: "voucherSig") else {
268 print("Error: join needs a voucherSig")
269 exitUsage(EXIT_FAILURE)
271 voucher = voucherData
272 voucherSig = voucherSigData
275 guard let voucherBase64 = argIterator.next() else {
276 print("Error: join needs a voucher")
281 guard let voucherData = Data(base64Encoded: voucherBase64) else {
282 print("Error: voucher must be base-64 data")
287 guard let voucherSigBase64 = argIterator.next() else {
288 print("Error: join needs a voucherSig")
293 guard let voucherSigData = Data(base64Encoded: voucherSigBase64) else {
294 print("Error: voucherSig must be base-64 data")
299 voucher = voucherData
300 voucherSig = voucherSigData
302 commands.append(.join(voucher, voucherSig))
305 commands.append(.localReset)
308 commands.append(.prepare)
310 case "healthInquiry":
311 commands.append(.healthInquiry)
314 commands.append(.reset)
317 commands.append(.update)
320 commands.append(.supportApp)
323 commands.append(.validate)
325 case "viable-bottles":
326 commands.append(.viableBottles)
330 let permanentInfo: Data
331 let permanentInfoSig: Data
333 let stableInfoSig: Data
335 if let configuration = configurationData {
336 guard let peerIDString = configuration["peerID"] as? String else {
337 print("Error: vouch needs a peerID")
338 exitUsage(EXIT_FAILURE)
341 guard let permanentInfoData = extractJSONData(dictionary: configuration, key: "permanentInfo") else {
342 print("Error: vouch needs a permanentInfo")
343 exitUsage(EXIT_FAILURE)
345 guard let permanentInfoSigData = extractJSONData(dictionary: configuration, key: "permanentInfoSig") else {
346 print("Error: vouch needs a permanentInfoSig")
347 exitUsage(EXIT_FAILURE)
349 guard let stableInfoData = extractJSONData(dictionary: configuration, key: "stableInfo") else {
350 print("Error: vouch needs a stableInfo")
351 exitUsage(EXIT_FAILURE)
353 guard let stableInfoSigData = extractJSONData(dictionary: configuration, key: "stableInfoSig") else {
354 print("Error: vouch needs a stableInfoSig")
355 exitUsage(EXIT_FAILURE)
358 peerID = peerIDString
359 permanentInfo = permanentInfoData
360 permanentInfoSig = permanentInfoSigData
361 stableInfo = stableInfoData
362 stableInfoSig = stableInfoSigData
366 guard let peerIDString = argIterator.next() else {
367 print("Error: vouch needs a peerID")
371 guard let permanentInfoBase64 = argIterator.next() else {
372 print("Error: vouch needs a permanentInfo")
376 guard let permanentInfoSigBase64 = argIterator.next() else {
377 print("Error: vouch needs a permanentInfoSig")
381 guard let stableInfoBase64 = argIterator.next() else {
382 print("Error: vouch needs a stableInfo")
386 guard let stableInfoSigBase64 = argIterator.next() else {
387 print("Error: vouch needs a stableInfoSig")
392 guard let permanentInfoData = Data(base64Encoded: permanentInfoBase64) else {
393 print("Error: permanentInfo must be base-64 data")
398 guard let permanentInfoSigData = Data(base64Encoded: permanentInfoSigBase64) else {
399 print("Error: permanentInfoSig must be base-64 data")
403 guard let stableInfoData = Data(base64Encoded: stableInfoBase64) else {
404 print("Error: stableInfo must be base-64 data")
409 guard let stableInfoSigData = Data(base64Encoded: stableInfoSigBase64) else {
410 print("Error: stableInfoSig must be base-64 data")
415 peerID = peerIDString
416 permanentInfo = permanentInfoData
417 permanentInfoSig = permanentInfoSigData
418 stableInfo = stableInfoData
419 stableInfoSig = stableInfoSigData
422 commands.append(.vouch(peerID, permanentInfo, permanentInfoSig, stableInfo, stableInfoSig))
424 case "vouchWithBottle":
425 guard let bottleID = argIterator.next() else {
426 print("Error: vouchWithBottle needs a bottleID")
430 guard let entropyBase64 = argIterator.next() else {
431 print("Error: vouchWithBottle needs entropy")
435 guard let salt = argIterator.next() else {
436 print("Error: vouchWithBottle needs a salt")
441 guard let entropy = Data(base64Encoded: entropyBase64) else {
442 print("Error: entropy must be base-64 data")
447 commands.append(.vouchWithBottle(bottleID, entropy, salt))
450 var machineIDs = Set<String>()
451 var performIDMS = false
452 while let arg = argIterator.next() {
456 machineIDs.insert(arg)
459 commands.append(.allow(machineIDs, performIDMS))
462 print("Unknown argument:", arg)
467 if commands.isEmpty {
471 // JSONSerialization has no idea how to handle NSData. Help it out.
472 func cleanDictionaryForJSON(_ d: [AnyHashable: Any]) -> [AnyHashable: Any] {
473 func cleanValue(_ value: Any) -> Any {
475 case let subDict as [AnyHashable: Any]:
476 return cleanDictionaryForJSON(subDict)
477 case let subArray as [Any]:
478 return subArray.map(cleanValue)
479 case let data as Data:
480 return data.base64EncodedString()
486 return d.mapValues(cleanValue)
489 // Bring up a connection to TrustedPeersHelper
490 let connection = NSXPCConnection(serviceName: "com.apple.TrustedPeersHelper")
491 connection.remoteObjectInterface = TrustedPeersHelperSetupProtocol(NSXPCInterface(with: TrustedPeersHelperProtocol.self))
493 let tpHelper = connection.synchronousRemoteObjectProxyWithErrorHandler { error in print("Unable to connect to TPHelper:", error) } as! TrustedPeersHelperProtocol
495 for command in commands {
498 os_log("dumping (%@, %@)", log: tplogDebug, type: .default, container, context)
499 tpHelper.dump(withContainer: container, context: context) { reply, error in
500 guard error == nil else {
501 print("Error dumping:", error!)
505 if let reply = reply {
507 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(reply)))
509 print("Error encoding JSON: \(error)")
512 print("Error: no results, but no error either?")
517 os_log("departing (%@, %@)", log: tplogDebug, type: .default, container, context)
518 tpHelper.departByDistrustingSelf(withContainer: container, context: context) { error in
519 guard error == nil else {
520 print("Error departing:", error!)
524 print("Depart successful")
527 case .distrust(let peerIDs):
528 os_log("distrusting %@ for (%@, %@)", log: tplogDebug, type: .default, peerIDs, container, context)
529 tpHelper.distrustPeerIDs(withContainer: container, context: context, peerIDs: peerIDs) { error in
530 guard error == nil else {
531 print("Error distrusting:", error!)
534 print("Distrust successful")
537 case .join(let voucher, let voucherSig):
538 os_log("joining (%@, %@)", log: tplogDebug, type: .default, container, context)
539 tpHelper.join(withContainer: container,
541 voucherData: voucher,
542 voucherSig: voucherSig,
545 preapprovedKeys: preapprovedKeys ?? []) { peerID, _, _, _, error in
546 guard error == nil else {
547 print("Error joining:", error!)
550 print("Join successful. PeerID:", peerID!)
554 os_log("establishing (%@, %@)", log: tplogDebug, type: .default, container, context)
555 tpHelper.establish(withContainer: container,
559 preapprovedKeys: preapprovedKeys ?? []) { peerID, _, error in
560 guard error == nil else {
561 print("Error establishing:", error!)
564 print("Establish successful. Peer ID:", peerID!)
568 os_log("healthInquiry (%@, %@)", log: tplogDebug, type: .default, container, context)
569 tpHelper.pushHealthInquiry(withContainer: container, context: context) { error in
570 guard error == nil else {
571 print("Error healthInquiry: \(String(describing: error))")
574 print("healthInquiry successful")
578 os_log("local-reset (%@, %@)", log: tplogDebug, type: .default, container, context)
579 tpHelper.localReset(withContainer: container, context: context) { error in
580 guard error == nil else {
581 print("Error resetting:", error!)
585 os_log("local-reset (%@, %@): successful", log: tplogDebug, type: .default, container, context)
586 print("Local reset successful")
590 os_log("supportApp (%@, %@)", log: tplogDebug, type: .default, container, context)
592 tpHelper.getSupportAppInfo(withContainer: container, context: context) { data, error in
593 guard error == nil else {
594 print("Error getting supportApp:", error!)
600 let string = try GetSupportAppInfoResponse(serializedData: data).jsonString()
603 print("Error decoding protobuf: \(error)")
606 print("Error: no results, but no error either?")
611 os_log("preparing (%@, %@)", log: tplogDebug, type: .default, container, context)
613 if machineID == nil {
614 let anisetteController = AKAnisetteProvisioningController()
616 let anisetteData = try anisetteController.anisetteData()
617 machineID = anisetteData.machineID
618 guard machineID != nil else {
619 print("failed to get machineid from anisette data")
624 let deviceInfo = OTDeviceInformationActualAdapter()
626 tpHelper.prepare(withContainer: container,
629 machineID: machineID!,
630 bottleSalt: bottleSalt,
631 bottleID: UUID().uuidString,
632 modelID: modelID ?? deviceInfo.modelID(),
633 deviceName: deviceName ?? deviceInfo.deviceName(),
634 serialNumber: serialNumber ?? deviceInfo.serialNumber(),
635 osVersion: osVersion ?? deviceInfo.osVersion(),
637 policySecrets: policySecrets,
638 signingPrivKeyPersistentRef: nil,
639 encPrivKeyPersistentRef: nil) { peerID, permanentInfo, permanentInfoSig, stableInfo, stableInfoSig, views, _, error in
640 guard error == nil else {
641 print("Error preparing:", error!)
647 "permanentInfo": permanentInfo!.base64EncodedString(),
648 "permanentInfoSig": permanentInfoSig!.base64EncodedString(),
649 "stableInfo": stableInfo!.base64EncodedString(),
650 "stableInfoSig": stableInfoSig!.base64EncodedString(),
651 "machineID": machineID!,
652 "views": Array(views ?? Set()),
655 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(result)))
657 print("Error encoding JSON: \(error)")
662 os_log("updating (%@, %@)", log: tplogDebug, type: .default, container, context)
663 tpHelper.update(withContainer: container,
665 deviceName: deviceName,
666 serialNumber: serialNumber,
667 osVersion: osVersion,
669 policySecrets: policySecrets) { _, error in
670 guard error == nil else {
671 print("Error updating:", error!)
675 print("Update complete")
679 os_log("resetting (%@, %@)", log: tplogDebug, type: .default, container, context)
680 tpHelper.reset(withContainer: container, context: context, resetReason: .userInitiatedReset) { error in
681 guard error == nil else {
682 print("Error during reset:", error!)
686 print("Reset complete")
690 os_log("validate (%@, %@)", log: tplogDebug, type: .default, container, context)
691 tpHelper.validatePeers(withContainer: container, context: context) { reply, error in
692 guard error == nil else {
693 print("Error validating:", error!)
697 if let reply = reply {
699 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(reply)))
701 print("Error encoding JSON: \(error)")
704 print("Error: no results, but no error either?")
709 os_log("viableBottles (%@, %@)", log: tplogDebug, type: .default, container, context)
710 tpHelper.fetchViableBottles(withContainer: container, context: context) { sortedBottleIDs, partialBottleIDs, error in
711 guard error == nil else {
712 print("Error fetching viable bottles:", error!)
715 var result: [String: [String]] = [:]
716 result["sortedBottleIDs"] = sortedBottleIDs
717 result["partialBottleIDs"] = partialBottleIDs
719 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(result)))
721 print("Error encoding JSON: \(error)")
725 case .vouch(let peerID, let permanentInfo, let permanentInfoSig, let stableInfo, let stableInfoSig):
726 os_log("vouching (%@, %@)", log: tplogDebug, type: .default, container, context)
727 tpHelper.vouch(withContainer: container,
730 permanentInfo: permanentInfo,
731 permanentInfoSig: permanentInfoSig,
732 stableInfo: stableInfo,
733 stableInfoSig: stableInfoSig,
735 ) { voucher, voucherSig, error in
736 guard error == nil else {
737 print("Error during vouch:", error!)
742 let result = ["voucher": voucher!.base64EncodedString(), "voucherSig": voucherSig!.base64EncodedString()]
743 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(result)))
745 print("Error during processing vouch results: \(error)")
749 case .vouchWithBottle(let bottleID, let entropy, let salt):
750 os_log("vouching with bottle (%@, %@)", log: tplogDebug, type: .default, container, context)
751 tpHelper.vouchWithBottle(withContainer: container,
756 tlkShares: []) { voucher, voucherSig, _, _, error in
757 guard error == nil else {
758 print("Error during vouchWithBottle", error!)
762 let result = ["voucher": voucher!.base64EncodedString(), "voucherSig": voucherSig!.base64EncodedString()]
763 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(result)))
765 print("Error during processing vouch results: \(error)")
769 case .allow(let machineIDs, let performIDMS):
770 os_log("allow-listing (%@, %@)", log: tplogDebug, type: .default, container, context)
772 var idmsDeviceIDs: Set<String> = Set()
773 var accountIsDemo: Bool = false
776 let store = ACAccountStore()
777 guard let account = store.aa_primaryAppleAccount() else {
778 print("Unable to fetch primary Apple account!")
782 let requestArguments = AKDeviceListRequestContext()
783 requestArguments.altDSID = account.aa_altDSID
784 requestArguments.services = [AKServiceNameiCloud]
786 let akManager = AKAccountManager.sharedInstance
787 let authKitAccount = akManager.authKitAccount(withAltDSID: account.aa_altDSID)
788 if let account = authKitAccount {
789 accountIsDemo = akManager.demoAccount(for: account)
792 guard let controller = AKAppleIDAuthenticationController() else {
793 print("Unable to create AKAppleIDAuthenticationController!")
796 let semaphore = DispatchSemaphore(value: 0)
798 controller.fetchDeviceList(with: requestArguments) { deviceList, error in
799 guard error == nil else {
800 print("Unable to fetch IDMS device list: \(error!)")
803 guard let deviceList = deviceList else {
804 print("IDMS returned empty device list")
808 idmsDeviceIDs = Set(deviceList.map { $0.machineId })
813 let allMachineIDs = machineIDs.union(idmsDeviceIDs)
814 print("Setting allowed machineIDs to \(allMachineIDs)")
815 tpHelper.setAllowedMachineIDsWithContainer(container, context: context, allowedMachineIDs: allMachineIDs, honorIDMSListChanges: accountIsDemo) { listChanged, error in
816 guard error == nil else {
817 print("Error during allow:", error!)
821 print("Allow complete, differences: \(listChanged)")