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