4 let logger = Logger(subsystem: "com.apple.security.trustedpeers", category: "tpctl")
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 policySecrets == nil {
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
274 guard let voucherBase64 = argIterator.next() else {
275 print("Error: join needs a voucher")
280 guard let voucherData = Data(base64Encoded: voucherBase64) else {
281 print("Error: voucher must be base-64 data")
286 guard let voucherSigBase64 = argIterator.next() else {
287 print("Error: join needs a voucherSig")
292 guard let voucherSigData = Data(base64Encoded: voucherSigBase64) else {
293 print("Error: voucherSig must be base-64 data")
298 voucher = voucherData
299 voucherSig = voucherSigData
301 commands.append(.join(voucher, voucherSig))
304 commands.append(.localReset)
307 commands.append(.prepare)
309 case "healthInquiry":
310 commands.append(.healthInquiry)
313 commands.append(.reset)
316 commands.append(.update)
319 commands.append(.supportApp)
322 commands.append(.validate)
324 case "viable-bottles":
325 commands.append(.viableBottles)
329 let permanentInfo: Data
330 let permanentInfoSig: Data
332 let stableInfoSig: Data
334 if let configuration = configurationData {
335 guard let peerIDString = configuration["peerID"] as? String else {
336 print("Error: vouch needs a peerID")
337 exitUsage(EXIT_FAILURE)
340 guard let permanentInfoData = extractJSONData(dictionary: configuration, key: "permanentInfo") else {
341 print("Error: vouch needs a permanentInfo")
342 exitUsage(EXIT_FAILURE)
344 guard let permanentInfoSigData = extractJSONData(dictionary: configuration, key: "permanentInfoSig") else {
345 print("Error: vouch needs a permanentInfoSig")
346 exitUsage(EXIT_FAILURE)
348 guard let stableInfoData = extractJSONData(dictionary: configuration, key: "stableInfo") else {
349 print("Error: vouch needs a stableInfo")
350 exitUsage(EXIT_FAILURE)
352 guard let stableInfoSigData = extractJSONData(dictionary: configuration, key: "stableInfoSig") else {
353 print("Error: vouch needs a stableInfoSig")
354 exitUsage(EXIT_FAILURE)
357 peerID = peerIDString
358 permanentInfo = permanentInfoData
359 permanentInfoSig = permanentInfoSigData
360 stableInfo = stableInfoData
361 stableInfoSig = stableInfoSigData
363 guard let peerIDString = argIterator.next() else {
364 print("Error: vouch needs a peerID")
368 guard let permanentInfoBase64 = argIterator.next() else {
369 print("Error: vouch needs a permanentInfo")
373 guard let permanentInfoSigBase64 = argIterator.next() else {
374 print("Error: vouch needs a permanentInfoSig")
378 guard let stableInfoBase64 = argIterator.next() else {
379 print("Error: vouch needs a stableInfo")
383 guard let stableInfoSigBase64 = argIterator.next() else {
384 print("Error: vouch needs a stableInfoSig")
389 guard let permanentInfoData = Data(base64Encoded: permanentInfoBase64) else {
390 print("Error: permanentInfo must be base-64 data")
395 guard let permanentInfoSigData = Data(base64Encoded: permanentInfoSigBase64) else {
396 print("Error: permanentInfoSig must be base-64 data")
400 guard let stableInfoData = Data(base64Encoded: stableInfoBase64) else {
401 print("Error: stableInfo must be base-64 data")
406 guard let stableInfoSigData = Data(base64Encoded: stableInfoSigBase64) else {
407 print("Error: stableInfoSig must be base-64 data")
412 peerID = peerIDString
413 permanentInfo = permanentInfoData
414 permanentInfoSig = permanentInfoSigData
415 stableInfo = stableInfoData
416 stableInfoSig = stableInfoSigData
419 commands.append(.vouch(peerID, permanentInfo, permanentInfoSig, stableInfo, stableInfoSig))
421 case "vouchWithBottle":
422 guard let bottleID = argIterator.next() else {
423 print("Error: vouchWithBottle needs a bottleID")
427 guard let entropyBase64 = argIterator.next() else {
428 print("Error: vouchWithBottle needs entropy")
432 guard let salt = argIterator.next() else {
433 print("Error: vouchWithBottle needs a salt")
438 guard let entropy = Data(base64Encoded: entropyBase64) else {
439 print("Error: entropy must be base-64 data")
444 commands.append(.vouchWithBottle(bottleID, entropy, salt))
447 var machineIDs = Set<String>()
448 var performIDMS = false
449 while let arg = argIterator.next() {
453 machineIDs.insert(arg)
456 commands.append(.allow(machineIDs, performIDMS))
459 print("Unknown argument:", arg)
464 if commands.isEmpty {
468 // JSONSerialization has no idea how to handle NSData. Help it out.
469 func cleanDictionaryForJSON(_ d: [AnyHashable: Any]) -> [AnyHashable: Any] {
470 func cleanValue(_ value: Any) -> Any {
472 case let subDict as [AnyHashable: Any]:
473 return cleanDictionaryForJSON(subDict)
474 case let subArray as [Any]:
475 return subArray.map(cleanValue)
476 case let data as Data:
477 return data.base64EncodedString()
483 return d.mapValues(cleanValue)
486 // Bring up a connection to TrustedPeersHelper
487 let connection = NSXPCConnection(serviceName: "com.apple.TrustedPeersHelper")
489 connection.remoteObjectInterface = TrustedPeersHelperSetupProtocol(NSXPCInterface(with: TrustedPeersHelperProtocol.self))
492 let tpHelper = connection.synchronousRemoteObjectProxyWithErrorHandler { error in print("Unable to connect to TPHelper:", error) } as! TrustedPeersHelperProtocol
494 for command in commands {
497 logger.log("dumping (\(container), \(context))")
498 tpHelper.dump(withContainer: container, context: context) { reply, error in
499 guard error == nil else {
500 print("Error dumping:", error!)
504 if let reply = reply {
506 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(reply)))
508 print("Error encoding JSON: \(error)")
511 print("Error: no results, but no error either?")
516 logger.log("departing (\(container), \(context))")
517 tpHelper.departByDistrustingSelf(withContainer: container, context: context) { error in
518 guard error == nil else {
519 print("Error departing:", error!)
523 print("Depart successful")
526 case .distrust(let peerIDs):
527 logger.log("distrusting \(peerIDs.description) for (\(container), \(context))")
528 tpHelper.distrustPeerIDs(withContainer: container, context: context, peerIDs: peerIDs) { error in
529 guard error == nil else {
530 print("Error distrusting:", error!)
533 print("Distrust successful")
536 case let .join(voucher, voucherSig):
537 logger.log("joining (\(container), \(context))")
538 tpHelper.join(withContainer: container,
540 voucherData: voucher,
541 voucherSig: voucherSig,
544 preapprovedKeys: preapprovedKeys ?? []) { peerID, _, _, error in
545 guard error == nil else {
546 print("Error joining:", error!)
549 print("Join successful. PeerID:", peerID!)
553 logger.log("establishing (\(container), \(context))")
554 tpHelper.establish(withContainer: container,
558 preapprovedKeys: preapprovedKeys ?? []) { peerID, _, _, error in
559 guard error == nil else {
560 print("Error establishing:", error!)
563 print("Establish successful. Peer ID:", peerID!)
567 logger.log("healthInquiry (\(container), \(context))")
568 tpHelper.pushHealthInquiry(withContainer: container, context: context) { error in
569 guard error == nil else {
570 print("Error healthInquiry: \(String(describing: error))")
573 print("healthInquiry successful")
577 logger.log("local-reset (\(container), \(context))")
578 tpHelper.localReset(withContainer: container, context: context) { error in
579 guard error == nil else {
580 print("Error resetting:", error!)
584 logger.log("local-reset (\(container), \(context)): successful")
585 print("Local reset successful")
589 logger.log("supportApp (\(container), \(context))")
591 tpHelper.getSupportAppInfo(withContainer: container, context: context) { data, error in
592 guard error == nil else {
593 print("Error getting supportApp:", error!)
599 let string = try GetSupportAppInfoResponse(serializedData: data).jsonString()
602 print("Error decoding protobuf: \(error)")
605 print("Error: no results, but no error either?")
610 logger.log("preparing (\(container), \(context))")
612 if machineID == nil {
613 let anisetteController = AKAnisetteProvisioningController()
615 let anisetteData = try anisetteController.anisetteData()
616 machineID = anisetteData.machineID
617 guard machineID != nil else {
618 print("failed to get machineid from anisette data")
623 let deviceInfo = OTDeviceInformationActualAdapter()
625 serialNumber = serialNumber ?? deviceInfo.serialNumber()
626 guard let serialNumber = serialNumber else {
627 print("failed to get serial number")
631 tpHelper.prepare(withContainer: container,
634 machineID: machineID!,
635 bottleSalt: bottleSalt,
636 bottleID: UUID().uuidString,
637 modelID: modelID ?? deviceInfo.modelID(),
638 deviceName: deviceName ?? deviceInfo.deviceName(),
639 serialNumber: serialNumber,
640 osVersion: osVersion ?? deviceInfo.osVersion(),
642 policySecrets: policySecrets,
643 syncUserControllableViews: .UNKNOWN,
644 signingPrivKeyPersistentRef: nil,
645 encPrivKeyPersistentRef: nil) { peerID, permanentInfo, permanentInfoSig, stableInfo, stableInfoSig, syncingPolicy, error in
646 guard error == nil else {
647 print("Error preparing:", error!)
653 "permanentInfo": permanentInfo!.base64EncodedString(),
654 "permanentInfoSig": permanentInfoSig!.base64EncodedString(),
655 "stableInfo": stableInfo!.base64EncodedString(),
656 "stableInfoSig": stableInfoSig!.base64EncodedString(),
657 "machineID": machineID!,
658 "views": Array(syncingPolicy?.viewList ?? Set()),
661 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(result)))
663 print("Error encoding JSON: \(error)")
668 logger.log("updating (\(container), \(context))")
669 tpHelper.update(withContainer: container,
671 deviceName: deviceName,
672 serialNumber: serialNumber,
673 osVersion: osVersion,
675 policySecrets: policySecrets,
676 syncUserControllableViews: nil) { _, _, error in
677 guard error == nil else {
678 print("Error updating:", error!)
682 print("Update complete")
686 logger.log("resetting (\(container), \(context))")
687 tpHelper.reset(withContainer: container, context: context, resetReason: .userInitiatedReset) { error in
688 guard error == nil else {
689 print("Error during reset:", error!)
693 print("Reset complete")
697 logger.log("validate (\(container), \(context))")
698 tpHelper.validatePeers(withContainer: container, context: context) { reply, error in
699 guard error == nil else {
700 print("Error validating:", error!)
704 if let reply = reply {
706 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(reply)))
708 print("Error encoding JSON: \(error)")
711 print("Error: no results, but no error either?")
716 logger.log("viableBottles (\(container), \(context))")
717 tpHelper.fetchViableBottles(withContainer: container, context: context) { sortedBottleIDs, partialBottleIDs, error in
718 guard error == nil else {
719 print("Error fetching viable bottles:", error!)
722 var result: [String: [String]] = [:]
723 result["sortedBottleIDs"] = sortedBottleIDs
724 result["partialBottleIDs"] = partialBottleIDs
726 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(result)))
728 print("Error encoding JSON: \(error)")
732 case let .vouch(peerID, permanentInfo, permanentInfoSig, stableInfo, stableInfoSig):
733 logger.log("vouching (\(container), \(context))")
734 tpHelper.vouch(withContainer: container,
737 permanentInfo: permanentInfo,
738 permanentInfoSig: permanentInfoSig,
739 stableInfo: stableInfo,
740 stableInfoSig: stableInfoSig,
742 ) { voucher, voucherSig, error in
743 guard error == nil else {
744 print("Error during vouch:", error!)
749 let result = ["voucher": voucher!.base64EncodedString(), "voucherSig": voucherSig!.base64EncodedString()]
750 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(result)))
752 print("Error during processing vouch results: \(error)")
756 case let .vouchWithBottle(bottleID, entropy, salt):
757 logger.log("vouching with bottle (\(container), \(context))")
758 tpHelper.vouchWithBottle(withContainer: container,
763 tlkShares: []) { voucher, voucherSig, _, _, error in
764 guard error == nil else {
765 print("Error during vouchWithBottle", error!)
769 let result = ["voucher": voucher!.base64EncodedString(), "voucherSig": voucherSig!.base64EncodedString()]
770 print(try TPCTLObjectiveC.jsonSerialize(cleanDictionaryForJSON(result)))
772 print("Error during processing vouch results: \(error)")
776 case let .allow(machineIDs, performIDMS):
777 logger.log("allow-listing (\(container), \(context))")
779 var idmsDeviceIDs: Set<String> = Set()
780 var accountIsDemo: Bool = false
783 let store = ACAccountStore()
784 guard let account = store.aa_primaryAppleAccount() else {
785 print("Unable to fetch primary Apple account!")
789 let requestArguments = AKDeviceListRequestContext()
790 requestArguments.altDSID = account.aa_altDSID
791 requestArguments.services = [AKServiceNameiCloud]
793 let akManager = AKAccountManager.sharedInstance
794 let authKitAccount = akManager.authKitAccount(withAltDSID: account.aa_altDSID)
795 if let account = authKitAccount {
796 accountIsDemo = akManager.demoAccount(for: account)
799 guard let controller = AKAppleIDAuthenticationController() else {
800 print("Unable to create AKAppleIDAuthenticationController!")
803 let semaphore = DispatchSemaphore(value: 0)
805 controller.fetchDeviceList(with: requestArguments) { deviceList, error in
806 guard error == nil else {
807 print("Unable to fetch IDMS device list: \(error!)")
810 guard let deviceList = deviceList else {
811 print("IDMS returned empty device list")
815 idmsDeviceIDs = Set(deviceList.map { $0.machineId })
820 let allMachineIDs = machineIDs.union(idmsDeviceIDs)
821 print("Setting allowed machineIDs to \(allMachineIDs)")
822 tpHelper.setAllowedMachineIDsWithContainer(container, context: context, allowedMachineIDs: allMachineIDs, honorIDMSListChanges: accountIsDemo) { listChanged, error in
823 guard error == nil else {
824 print("Error during allow:", error!)
828 print("Allow complete, differences: \(listChanged)")