]> git.saurik.com Git - apple/security.git/blob - keychain/TrustedPeersHelper/Container_MachineIDs.swift
Security-59306.11.20.tar.gz
[apple/security.git] / keychain / TrustedPeersHelper / Container_MachineIDs.swift
1
2 import CoreData
3 import Foundation
4
5 extension MachineMO {
6 func modifiedInPast(hours: Int) -> Bool {
7 guard let modifiedDate = self.modified else {
8 return false
9 }
10
11 let dateLimit = Date(timeIntervalSinceNow: -60*60*TimeInterval(hours))
12 return modifiedDate.compare(dateLimit) == ComparisonResult.orderedDescending
13 }
14
15 func modifiedDate() -> String {
16 guard let modifiedDate = self.modified else {
17 return "unknown"
18 }
19
20 let dateFormatter = ISO8601DateFormatter()
21 return dateFormatter.string(from: modifiedDate)
22 }
23
24 func asTPMachineID() -> TPMachineID {
25 return TPMachineID(machineID: self.machineID ?? "unknown",
26 status: TPMachineIDStatus(rawValue: UInt(self.status)) ?? .unknown,
27 modified: self.modified ?? Date())
28 }
29 }
30
31 // You get two days of grace before you're removed
32 let cutoffHours = 48
33
34 extension Container {
35 // CoreData suggests not using heavyweight migrations, so we have two locations to store the machine ID list.
36 // Perform our own migration from the no-longer-used field.
37 internal static func onqueueUpgradeMachineIDSetToModel(container: ContainerMO, moc: NSManagedObjectContext) {
38 let knownMachineMOs = container.machines as? Set<MachineMO> ?? Set()
39 let knownMachineIDs = Set(knownMachineMOs.compactMap { $0.machineID })
40
41 let allowedMachineIDs = container.allowedMachineIDs as? Set<String> ?? Set()
42 let missingIDs = allowedMachineIDs.filter { !knownMachineIDs.contains($0) }
43
44 missingIDs.forEach { id in
45 let mid = MachineMO(context: moc)
46 mid.machineID = id
47 mid.seenOnFullList = true
48 mid.status = Int64(TPMachineIDStatus.allowed.rawValue)
49 mid.modified = Date()
50 container.addToMachines(mid)
51 }
52
53 container.allowedMachineIDs = Set<String>() as NSSet
54 }
55
56 internal static func onqueueUpgradeMachineIDSetToUseStatus(container: ContainerMO, moc: NSManagedObjectContext) {
57 let knownMachineMOs = container.machines as? Set<MachineMO> ?? Set()
58
59 // Once we run this upgrade, we will set the allowed bool to false, since it's unused.
60 // Therefore, if we have a single record with "allowed" set, we haven't run the upgrade.
61 let runUpgrade = knownMachineMOs.filter { $0.allowed }.count > 0
62 if runUpgrade {
63 knownMachineMOs.forEach { mo in
64 if mo.allowed {
65 mo.status = Int64(TPMachineIDStatus.allowed.rawValue)
66 } else {
67 mo.status = Int64(TPMachineIDStatus.disallowed.rawValue)
68 }
69 mo.allowed = false
70 }
71 }
72 }
73
74 func setAllowedMachineIDs(_ allowedMachineIDs: Set<String>, reply: @escaping (Bool, Error?) -> Void) {
75 self.semaphore.wait()
76 let reply: (Bool, Error?) -> Void = {
77 os_log("setAllowedMachineIDs complete: %@", log: tplogTrace, type: .info, traceError($1))
78 self.semaphore.signal()
79 reply($0, $1)
80 }
81
82 os_log("Setting allowed machine IDs: %@", log: tplogDebug, type: .default, allowedMachineIDs)
83
84 // Note: we currently ignore any machineIDs that are set in the model, but never appeared on the
85 // Trusted Devices list. We should give them a grace period (1wk?) then kick them out.
86
87 self.moc.performAndWait {
88 do {
89 var differences = false
90
91 var knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
92 let knownMachineIDs = Set(knownMachines.compactMap { $0.machineID } )
93
94
95 knownMachines.forEach { machine in
96 guard let mid = machine.machineID else {
97 os_log("Machine has no ID: %@", log: tplogDebug, type: .default, machine)
98 return
99 }
100 if allowedMachineIDs.contains(mid) {
101 if machine.status == TPMachineIDStatus.allowed.rawValue {
102 os_log("Machine ID still trusted: %@", log: tplogDebug, type: .default, String(describing: machine.machineID))
103 } else {
104 os_log("Machine ID newly retrusted: %@", log: tplogDebug, type: .default, String(describing: machine.machineID))
105 differences = true
106 }
107 machine.status = Int64(TPMachineIDStatus.allowed.rawValue)
108 machine.seenOnFullList = true
109 machine.modified = Date()
110 } else {
111 // This machine ID is not on the list. What, if anything, should be done?
112 if machine.status == TPMachineIDStatus.allowed.rawValue {
113 // IDMS sometimes has list consistency issues. So, if we see a device 'disappear' from the list, it may or may not
114 // actually have disappered: we may have received an 'add' push and then fetched the list too quickly.
115 // To hack around this, we track whether we've seen the machine on the full list yet. If we haven't, this was likely
116 // the result of an 'add' push, and will be given 48 hours of grace before being removed.
117 if machine.seenOnFullList {
118 machine.status = Int64(TPMachineIDStatus.disallowed.rawValue)
119 machine.modified = Date()
120 os_log("Newly distrusted machine ID: %@", log: tplogDebug, type: .default, String(describing: machine.machineID))
121 differences = true
122
123 } else {
124 if machine.modifiedInPast(hours: cutoffHours) {
125 os_log("Allowed-but-unseen machine ID isn't on full list, last modified %@, ignoring: %@", log: tplogDebug, type: .default, machine.modifiedDate(), String(describing: machine.machineID))
126 } else {
127 os_log("Allowed-but-unseen machine ID isn't on full list, last modified %@, distrusting: %@", log: tplogDebug, type: .default, machine.modifiedDate(), String(describing: machine.machineID))
128 machine.status = Int64(TPMachineIDStatus.disallowed.rawValue)
129 machine.modified = Date()
130 differences = true
131 }
132 }
133
134 } else if machine.status == TPMachineIDStatus.unknown.rawValue {
135 if machine.modifiedInPast(hours: cutoffHours) {
136 os_log("Unknown machine ID last modified %@; leaving unknown: %@", log: tplogDebug, type: .default, machine.modifiedDate(), String(describing: machine.machineID))
137 } else {
138 os_log("Unknown machine ID last modified %@; distrusting: %@", log: tplogDebug, type: .default, machine.modifiedDate(), String(describing: machine.machineID))
139 machine.status = Int64(TPMachineIDStatus.disallowed.rawValue)
140 machine.modified = Date()
141 differences = true
142 }
143 }
144 }
145 }
146
147 // Do we need to create any further objects?
148 allowedMachineIDs.forEach { machineID in
149 if(!knownMachineIDs.contains(machineID)) {
150 // We didn't know about this machine before; it's newly trusted!
151 let machine = MachineMO(context: self.moc)
152 machine.machineID = machineID
153 machine.container = containerMO
154 machine.seenOnFullList = true
155 machine.modified = Date()
156 machine.status = Int64(TPMachineIDStatus.allowed.rawValue)
157 os_log("Newly trusted machine ID: %@", log: tplogDebug, type: .default, String(describing: machine.machineID))
158 differences = true
159
160 self.containerMO.addToMachines(machine)
161 knownMachines.insert(machine)
162 }
163 }
164
165 // Now, are there any machine IDs in the model that aren't in the list? If so, add them as "unknown"
166 let modelMachineIDs = self.model.allMachineIDs()
167 modelMachineIDs.forEach { peerMachineID in
168 if !knownMachineIDs.contains(peerMachineID) && !allowedMachineIDs.contains(peerMachineID) {
169 os_log("Peer machineID is unknown, beginning grace period: %@", log: tplogDebug, type: .default, peerMachineID)
170 let machine = MachineMO(context: self.moc)
171 machine.machineID = peerMachineID
172 machine.container = containerMO
173 machine.seenOnFullList = false
174 machine.modified = Date()
175 machine.status = Int64(TPMachineIDStatus.unknown.rawValue)
176 differences = true
177
178 self.containerMO.addToMachines(machine)
179 }
180 }
181
182 // We no longer use allowed machine IDs.
183 self.containerMO.allowedMachineIDs = NSSet()
184
185 try self.moc.save()
186
187 reply(differences, nil)
188 } catch {
189 os_log("Error setting machine ID list: %@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
190 reply(false, error)
191 }
192 }
193 }
194
195 func addAllow(_ machineIDs: [String], reply: @escaping (Error?) -> Void) {
196 self.semaphore.wait()
197 let reply: (Error?) -> Void = {
198 os_log("addAllow complete: %@", log: tplogTrace, type: .info, traceError($0))
199 self.semaphore.signal()
200 reply($0)
201 }
202
203 os_log("Adding allowed machine IDs: %@", log: tplogDebug, type: .default, machineIDs)
204
205 self.moc.performAndWait {
206 do {
207 var knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
208 let knownMachineIDs = Set(knownMachines.compactMap { $0.machineID } )
209
210 // We treat an add push as authoritative (even though we should really confirm it with a full list fetch).
211 // We can get away with this as we're using this list as a deny-list, and if we accidentally don't deny someone fast enough, that's okay.
212 machineIDs.forEach { machineID in
213 if knownMachineIDs.contains(machineID) {
214 knownMachines.forEach { machine in
215 if machine.machineID == machineID {
216 machine.status = Int64(TPMachineIDStatus.allowed.rawValue)
217 machine.modified = Date()
218 os_log("Continue to trust machine ID: %@", log: tplogDebug, type: .default, String(describing: machine.machineID))
219 }
220 }
221
222 } else {
223 let machine = MachineMO(context: self.moc)
224 machine.machineID = machineID
225 machine.container = containerMO
226 machine.seenOnFullList = false
227 machine.modified = Date()
228 machine.status = Int64(TPMachineIDStatus.allowed.rawValue)
229 os_log("Newly trusted machine ID: %@", log: tplogDebug, type: .default, String(describing: machine.machineID))
230 self.containerMO.addToMachines(machine)
231
232 knownMachines.insert(machine)
233 }
234 }
235
236 try self.moc.save()
237 reply(nil)
238 } catch {
239 reply(error)
240 }
241 }
242 }
243
244 func removeAllow(_ machineIDs: [String], reply: @escaping (Error?) -> Void) {
245 self.semaphore.wait()
246 let reply: (Error?) -> Void = {
247 os_log("removeAllow complete: %@", log: tplogTrace, type: .info, traceError($0))
248 self.semaphore.signal()
249 reply($0)
250 }
251
252 os_log("Removing allowed machine IDs: %@", log: tplogDebug, type: .default, machineIDs)
253
254 self.moc.performAndWait {
255 do {
256 var knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
257 let knownMachineIDs = Set(knownMachines.compactMap { $0.machineID } )
258
259 // This is an odd approach: we'd like to confirm that this MID was actually removed (and not just a delayed push).
260 // So, let's set the status to "unknown", and its modification date to the distant past.
261 // The next time we fetch the full list, we'll confirm the removal (or, if the removal push was spurious, re-add the MID as trusted).
262 machineIDs.forEach { machineID in
263 if knownMachineIDs.contains(machineID) {
264 knownMachines.forEach { machine in
265 if machine.machineID == machineID {
266 machine.status = Int64(TPMachineIDStatus.unknown.rawValue)
267 machine.modified = Date.distantPast
268 os_log("Now suspicious of machine ID: %@", log: tplogDebug, type: .default, String(describing: machine.machineID))
269 }
270 }
271
272 } else {
273 let machine = MachineMO(context: self.moc)
274 machine.machineID = machineID
275 machine.container = containerMO
276 machine.status = Int64(TPMachineIDStatus.unknown.rawValue)
277 machine.modified = Date.distantPast
278 os_log("Suspicious of new machine ID: %@", log: tplogDebug, type: .default, String(describing: machine.machineID))
279 self.containerMO.addToMachines(machine)
280
281 knownMachines.insert(machine)
282 }
283 }
284
285 try self.moc.save()
286 reply(nil)
287 } catch {
288 reply(error)
289 }
290 }
291 }
292
293 func onqueueMachineIDAllowedByIDMS(machineID: String) -> Bool {
294 // For Demo accounts, if the list is entirely empty, then everything is allowed
295 let machines = containerMO.machines as? Set<MachineMO> ?? Set()
296 if machines.count == 0 {
297 os_log("machineID list is empty; allowing %@", log: tplogDebug, type: .debug, machineID)
298 return true
299 }
300
301 // Note: this function rejects grey devices: machineIDs that are neither allowed nor disallowed
302 for mo in machines {
303 if mo.machineID == machineID {
304 if mo.status == TPMachineIDStatus.allowed.rawValue {
305 return true
306 } else {
307 os_log("machineID %@ not explicitly allowed: %@", log: tplogDebug, type: .debug, machineID, mo)
308 return false
309 }
310 }
311 }
312
313 // Didn't find it? reject.
314 os_log("machineID %@ not found on list", log: tplogDebug, type: .debug, machineID)
315 return false
316 }
317
318 func onqueueCurrentMIDList() -> TPMachineIDList {
319 let machines = containerMO.machines as? Set<MachineMO> ?? Set()
320 return TPMachineIDList(entries: machines.map { $0.asTPMachineID() })
321 }
322
323 func onqueueUpdateMachineIDListFromModel(dynamicInfo: TPPeerDynamicInfo) {
324 // This function is intended to be called once the model is in a steady state of adds and deletes.
325 //
326 // First, we should ensure that we've written down the MIDs of all trusted peers. That way, if they
327 // aren't on the MID list now, we'll start the timer for them to be removed if they never make it.
328 // (But! don't do this if we think this is a Demo account. Those don't have a list, and we shouldn't make one.)
329
330 // Second, we should remove all disallowed MIDs, as those values have been used.
331 // We don't want to automatically kick out new peers if they rejoin with the same MID.
332
333 let machines = containerMO.machines as? Set<MachineMO> ?? Set()
334 let knownMachineIDs = Set(machines.compactMap { $0.machineID } )
335
336 // Peers trust themselves. So if the ego peer is in Octagon, its machineID will be in this set
337 let trustedMachineIDs = Set(dynamicInfo.includedPeerIDs.compactMap { self.model.peer(withID: $0)?.permanentInfo.machineID })
338
339 // if this account is not a demo account...
340 if machines.count > 0 {
341 for peerMachineID in trustedMachineIDs.subtracting(knownMachineIDs) {
342 os_log("Peer machineID is unknown, beginning grace period: %@", log: tplogDebug, type: .default, peerMachineID)
343 let machine = MachineMO(context: self.moc)
344 machine.machineID = peerMachineID
345 machine.container = self.containerMO
346 machine.seenOnFullList = false
347 machine.modified = Date()
348 machine.status = Int64(TPMachineIDStatus.unknown.rawValue)
349
350 self.containerMO.addToMachines(machine)
351 }
352 } else {
353 os_log("Believe we're in a demo account; not starting an unknown machine ID timer", log: tplogDebug, type: .default)
354 }
355
356 for mo in (machines) {
357 if mo.status == TPMachineIDStatus.disallowed.rawValue {
358 os_log("Dropping knowledge of machineID %@", log: tplogDebug, type: .debug, String(describing: mo.machineID))
359 self.containerMO.removeFromMachines(mo)
360 }
361 }
362 }
363
364 // Computes if a full list fetch would be 'useful'
365 // Useful means that there's an unknown MID whose modification date is before the cutoff
366 // A full list fetch would either confirm it as 'untrusted' or make it trusted again
367 func onqueueFullIDMSListWouldBeHelpful() -> Bool {
368 let unknownMOs = (containerMO.machines as? Set<MachineMO> ?? Set()).filter { $0.status == TPMachineIDStatus.unknown.rawValue }
369 let outdatedMOs = unknownMOs.filter { !$0.modifiedInPast(hours: cutoffHours) }
370
371 return outdatedMOs.count > 0
372 }
373 }