]> git.saurik.com Git - apple/security.git/blame - keychain/TrustedPeersHelper/Container_MachineIDs.swift
Security-59754.80.3.tar.gz
[apple/security.git] / keychain / TrustedPeersHelper / Container_MachineIDs.swift
CommitLineData
b54c578e
A
1import CoreData
2import Foundation
3
4extension MachineMO {
5 func modifiedInPast(hours: Int) -> Bool {
6 guard let modifiedDate = self.modified else {
7 return false
8 }
9
7fb2cbd2 10 let dateLimit = Date(timeIntervalSinceNow: -60 * 60 * TimeInterval(hours))
b54c578e
A
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
31let cutoffHours = 48
32
33extension 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.
d64be36e 60 let runUpgrade = knownMachineMOs.contains { $0.allowed }
b54c578e
A
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
b3971512
A
73 func enforceIDMSListChanges(knownMachines: Set<MachineMO>) -> Bool {
74 if self.containerMO.honorIDMSListChanges == "YES"{
75 return true
76 } else if self.containerMO.honorIDMSListChanges == "NO" {
77 return false
78 } else if self.containerMO.honorIDMSListChanges == "UNKNOWN" && knownMachines.isEmpty {
79 return false
80 } else if self.containerMO.honorIDMSListChanges == "UNKNOWN" && !knownMachines.isEmpty {
81 return true
82 } else {
83 return true
84 }
85 }
86
87 func setAllowedMachineIDs(_ allowedMachineIDs: Set<String>, honorIDMSListChanges: Bool, reply: @escaping (Bool, Error?) -> Void) {
b54c578e
A
88 self.semaphore.wait()
89 let reply: (Bool, Error?) -> Void = {
b3971512 90 os_log("setAllowedMachineIDs complete: %{public}@", log: tplogTrace, type: .info, traceError($1))
b54c578e
A
91 self.semaphore.signal()
92 reply($0, $1)
93 }
94
b3971512 95 os_log("Setting allowed machine IDs: %{public}@", log: tplogDebug, type: .default, allowedMachineIDs)
b54c578e
A
96
97 // Note: we currently ignore any machineIDs that are set in the model, but never appeared on the
98 // Trusted Devices list. We should give them a grace period (1wk?) then kick them out.
99
100 self.moc.performAndWait {
101 do {
102 var differences = false
b3971512 103 self.containerMO.honorIDMSListChanges = honorIDMSListChanges ? "YES" : "NO"
b54c578e
A
104
105 var knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
7fb2cbd2 106 let knownMachineIDs = Set(knownMachines.compactMap { $0.machineID })
b54c578e
A
107
108 knownMachines.forEach { machine in
109 guard let mid = machine.machineID else {
b3971512 110 os_log("Machine has no ID: %{public}@", log: tplogDebug, type: .default, machine)
b54c578e
A
111 return
112 }
113 if allowedMachineIDs.contains(mid) {
114 if machine.status == TPMachineIDStatus.allowed.rawValue {
b3971512 115 os_log("Machine ID still trusted: %{public}@", log: tplogDebug, type: .default, String(describing: machine.machineID))
b54c578e 116 } else {
b3971512 117 os_log("Machine ID newly retrusted: %{public}@", log: tplogDebug, type: .default, String(describing: machine.machineID))
b54c578e
A
118 differences = true
119 }
120 machine.status = Int64(TPMachineIDStatus.allowed.rawValue)
121 machine.seenOnFullList = true
122 machine.modified = Date()
123 } else {
124 // This machine ID is not on the list. What, if anything, should be done?
125 if machine.status == TPMachineIDStatus.allowed.rawValue {
126 // IDMS sometimes has list consistency issues. So, if we see a device 'disappear' from the list, it may or may not
127 // actually have disappered: we may have received an 'add' push and then fetched the list too quickly.
128 // To hack around this, we track whether we've seen the machine on the full list yet. If we haven't, this was likely
129 // the result of an 'add' push, and will be given 48 hours of grace before being removed.
130 if machine.seenOnFullList {
131 machine.status = Int64(TPMachineIDStatus.disallowed.rawValue)
132 machine.modified = Date()
b3971512 133 os_log("Newly distrusted machine ID: %{public}@", log: tplogDebug, type: .default, String(describing: machine.machineID))
b54c578e 134 differences = true
b54c578e
A
135 } else {
136 if machine.modifiedInPast(hours: cutoffHours) {
b3971512 137 os_log("Allowed-but-unseen machine ID isn't on full list, last modified %{public}@, ignoring: %{public}@", log: tplogDebug, type: .default, machine.modifiedDate(), String(describing: machine.machineID))
b54c578e 138 } else {
b3971512 139 os_log("Allowed-but-unseen machine ID isn't on full list, last modified %{public}@, distrusting: %{public}@", log: tplogDebug, type: .default, machine.modifiedDate(), String(describing: machine.machineID))
b54c578e
A
140 machine.status = Int64(TPMachineIDStatus.disallowed.rawValue)
141 machine.modified = Date()
142 differences = true
143 }
144 }
b54c578e
A
145 } else if machine.status == TPMachineIDStatus.unknown.rawValue {
146 if machine.modifiedInPast(hours: cutoffHours) {
b3971512 147 os_log("Unknown machine ID last modified %{public}@; leaving unknown: %{public}@", log: tplogDebug, type: .default, machine.modifiedDate(), String(describing: machine.machineID))
b54c578e 148 } else {
b3971512 149 os_log("Unknown machine ID last modified %{public}@; distrusting: %{public}@", log: tplogDebug, type: .default, machine.modifiedDate(), String(describing: machine.machineID))
b54c578e
A
150 machine.status = Int64(TPMachineIDStatus.disallowed.rawValue)
151 machine.modified = Date()
152 differences = true
153 }
154 }
155 }
156 }
157
158 // Do we need to create any further objects?
159 allowedMachineIDs.forEach { machineID in
b3971512 160 if !knownMachineIDs.contains(machineID) {
b54c578e
A
161 // We didn't know about this machine before; it's newly trusted!
162 let machine = MachineMO(context: self.moc)
163 machine.machineID = machineID
164 machine.container = containerMO
165 machine.seenOnFullList = true
166 machine.modified = Date()
167 machine.status = Int64(TPMachineIDStatus.allowed.rawValue)
b3971512 168 os_log("Newly trusted machine ID: %{public}@", log: tplogDebug, type: .default, String(describing: machine.machineID))
b54c578e
A
169 differences = true
170
171 self.containerMO.addToMachines(machine)
172 knownMachines.insert(machine)
173 }
174 }
175
b3971512 176 if self.enforceIDMSListChanges(knownMachines: knownMachines) {
7fb2cbd2
A
177 // Are there any machine IDs in the model that aren't in the list? If so, add them as "unknown"
178 let modelMachineIDs = self.model.allMachineIDs()
179 modelMachineIDs.forEach { peerMachineID in
180 if !knownMachineIDs.contains(peerMachineID) && !allowedMachineIDs.contains(peerMachineID) {
b3971512 181 os_log("Peer machineID is unknown, beginning grace period: %{public}@", log: tplogDebug, type: .default, peerMachineID)
7fb2cbd2
A
182 let machine = MachineMO(context: self.moc)
183 machine.machineID = peerMachineID
184 machine.container = containerMO
185 machine.seenOnFullList = false
186 machine.modified = Date()
187 machine.status = Int64(TPMachineIDStatus.unknown.rawValue)
188 differences = true
b54c578e 189
7fb2cbd2
A
190 self.containerMO.addToMachines(machine)
191 }
b54c578e 192 }
7fb2cbd2 193 } else {
b3971512 194 os_log("Believe we're in a demo account, not enforcing IDMS list", log: tplogDebug, type: .default)
b54c578e
A
195 }
196
197 // We no longer use allowed machine IDs.
198 self.containerMO.allowedMachineIDs = NSSet()
199
200 try self.moc.save()
201
202 reply(differences, nil)
203 } catch {
b3971512 204 os_log("Error setting machine ID list: %{public}@", log: tplogDebug, type: .default, (error as CVarArg?) ?? "no error")
b54c578e
A
205 reply(false, error)
206 }
207 }
208 }
209
210 func addAllow(_ machineIDs: [String], reply: @escaping (Error?) -> Void) {
211 self.semaphore.wait()
212 let reply: (Error?) -> Void = {
b3971512 213 os_log("addAllow complete: %{public}@", log: tplogTrace, type: .info, traceError($0))
b54c578e
A
214 self.semaphore.signal()
215 reply($0)
216 }
217
b3971512 218 os_log("Adding allowed machine IDs: %{public}@", log: tplogDebug, type: .default, machineIDs)
b54c578e
A
219
220 self.moc.performAndWait {
221 do {
222 var knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
7fb2cbd2 223 let knownMachineIDs = Set(knownMachines.compactMap { $0.machineID })
b54c578e
A
224
225 // We treat an add push as authoritative (even though we should really confirm it with a full list fetch).
226 // 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.
227 machineIDs.forEach { machineID in
228 if knownMachineIDs.contains(machineID) {
229 knownMachines.forEach { machine in
230 if machine.machineID == machineID {
231 machine.status = Int64(TPMachineIDStatus.allowed.rawValue)
232 machine.modified = Date()
b3971512 233 os_log("Continue to trust machine ID: %{public}@", log: tplogDebug, type: .default, String(describing: machine.machineID))
b54c578e
A
234 }
235 }
b54c578e
A
236 } else {
237 let machine = MachineMO(context: self.moc)
238 machine.machineID = machineID
239 machine.container = containerMO
240 machine.seenOnFullList = false
241 machine.modified = Date()
242 machine.status = Int64(TPMachineIDStatus.allowed.rawValue)
b3971512 243 os_log("Newly trusted machine ID: %{public}@", log: tplogDebug, type: .default, String(describing: machine.machineID))
b54c578e
A
244 self.containerMO.addToMachines(machine)
245
246 knownMachines.insert(machine)
247 }
248 }
249
250 try self.moc.save()
251 reply(nil)
252 } catch {
253 reply(error)
254 }
255 }
256 }
257
258 func removeAllow(_ machineIDs: [String], reply: @escaping (Error?) -> Void) {
259 self.semaphore.wait()
260 let reply: (Error?) -> Void = {
b3971512 261 os_log("removeAllow complete: %{public}@", log: tplogTrace, type: .info, traceError($0))
b54c578e
A
262 self.semaphore.signal()
263 reply($0)
264 }
265
b3971512 266 os_log("Removing allowed machine IDs: %{public}@", log: tplogDebug, type: .default, machineIDs)
b54c578e
A
267
268 self.moc.performAndWait {
269 do {
270 var knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
7fb2cbd2 271 let knownMachineIDs = Set(knownMachines.compactMap { $0.machineID })
b54c578e
A
272
273 // This is an odd approach: we'd like to confirm that this MID was actually removed (and not just a delayed push).
274 // So, let's set the status to "unknown", and its modification date to the distant past.
275 // 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).
276 machineIDs.forEach { machineID in
277 if knownMachineIDs.contains(machineID) {
278 knownMachines.forEach { machine in
279 if machine.machineID == machineID {
280 machine.status = Int64(TPMachineIDStatus.unknown.rawValue)
281 machine.modified = Date.distantPast
b3971512 282 os_log("Now suspicious of machine ID: %{public}@", log: tplogDebug, type: .default, String(describing: machine.machineID))
b54c578e
A
283 }
284 }
b54c578e
A
285 } else {
286 let machine = MachineMO(context: self.moc)
287 machine.machineID = machineID
288 machine.container = containerMO
289 machine.status = Int64(TPMachineIDStatus.unknown.rawValue)
290 machine.modified = Date.distantPast
b3971512 291 os_log("Suspicious of new machine ID: %{public}@", log: tplogDebug, type: .default, String(describing: machine.machineID))
b54c578e
A
292 self.containerMO.addToMachines(machine)
293
294 knownMachines.insert(machine)
295 }
296 }
297
298 try self.moc.save()
299 reply(nil)
300 } catch {
301 reply(error)
302 }
303 }
304 }
305
805875f8
A
306 func fetchAllowedMachineIDs(reply: @escaping (Set<String>?, Error?) -> Void) {
307 self.semaphore.wait()
308 let reply: (Set<String>?, Error?) -> Void = {
b3971512 309 os_log("fetchAllowedMachineIDs complete: %{public}@", log: tplogTrace, type: .info, traceError($1))
805875f8
A
310 self.semaphore.signal()
311 reply($0, $1)
312 }
313
314 os_log("Fetching allowed machine IDs", log: tplogDebug, type: .default)
315
316 self.moc.performAndWait {
317 let knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
b3971512 318 let allowedMachineIDs = knownMachines.filter { $0.status == Int64(TPMachineIDStatus.allowed.rawValue) }.compactMap { $0.machineID }
805875f8
A
319
320 reply(Set(allowedMachineIDs), nil)
321 }
322 }
323
b54c578e 324 func onqueueMachineIDAllowedByIDMS(machineID: String) -> Bool {
b3971512 325
b54c578e 326 // For Demo accounts, if the list is entirely empty, then everything is allowed
b3971512
A
327 let knownMachines = containerMO.machines as? Set<MachineMO> ?? Set()
328
329 if !self.enforceIDMSListChanges(knownMachines: knownMachines) {
330 os_log("not enforcing idms list changes; allowing %{public}@", log: tplogDebug, type: .debug, machineID)
b54c578e
A
331 return true
332 }
333
334 // Note: this function rejects grey devices: machineIDs that are neither allowed nor disallowed
b3971512
A
335 for mo in knownMachines where mo.machineID == machineID {
336 if mo.status == TPMachineIDStatus.allowed.rawValue {
337 return true
338 } else {
339 os_log("machineID %{public}@ not explicitly allowed: %{public}@", log: tplogDebug, type: .debug, machineID, mo)
340 return false
b54c578e
A
341 }
342 }
343
344 // Didn't find it? reject.
b3971512 345 os_log("machineID %{public}@ not found on list", log: tplogDebug, type: .debug, machineID)
b54c578e
A
346 return false
347 }
348
349 func onqueueCurrentMIDList() -> TPMachineIDList {
350 let machines = containerMO.machines as? Set<MachineMO> ?? Set()
351 return TPMachineIDList(entries: machines.map { $0.asTPMachineID() })
352 }
353
354 func onqueueUpdateMachineIDListFromModel(dynamicInfo: TPPeerDynamicInfo) {
355 // This function is intended to be called once the model is in a steady state of adds and deletes.
356 //
357 // First, we should ensure that we've written down the MIDs of all trusted peers. That way, if they
358 // aren't on the MID list now, we'll start the timer for them to be removed if they never make it.
359 // (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.)
360
361 // Second, we should remove all disallowed MIDs, as those values have been used.
362 // We don't want to automatically kick out new peers if they rejoin with the same MID.
363
364 let machines = containerMO.machines as? Set<MachineMO> ?? Set()
7fb2cbd2 365 let knownMachineIDs = Set(machines.compactMap { $0.machineID })
b54c578e
A
366
367 // Peers trust themselves. So if the ego peer is in Octagon, its machineID will be in this set
368 let trustedMachineIDs = Set(dynamicInfo.includedPeerIDs.compactMap { self.model.peer(withID: $0)?.permanentInfo.machineID })
369
370 // if this account is not a demo account...
b3971512 371 if self.enforceIDMSListChanges(knownMachines: machines) {
b54c578e 372 for peerMachineID in trustedMachineIDs.subtracting(knownMachineIDs) {
b3971512 373 os_log("Peer machineID is unknown, beginning grace period: %{public}@", log: tplogDebug, type: .default, peerMachineID)
b54c578e
A
374 let machine = MachineMO(context: self.moc)
375 machine.machineID = peerMachineID
376 machine.container = self.containerMO
377 machine.seenOnFullList = false
378 machine.modified = Date()
379 machine.status = Int64(TPMachineIDStatus.unknown.rawValue)
380
381 self.containerMO.addToMachines(machine)
382 }
383 } else {
b3971512 384 os_log("Not enforcing IDMS list changes", log: tplogDebug, type: .default)
b54c578e
A
385 }
386
b3971512
A
387 for mo in (machines) where mo.status == TPMachineIDStatus.disallowed.rawValue {
388 os_log("Dropping knowledge of machineID %{public}@", log: tplogDebug, type: .debug, String(describing: mo.machineID))
389 self.containerMO.removeFromMachines(mo)
b54c578e
A
390 }
391 }
392
393 // Computes if a full list fetch would be 'useful'
394 // Useful means that there's an unknown MID whose modification date is before the cutoff
395 // A full list fetch would either confirm it as 'untrusted' or make it trusted again
396 func onqueueFullIDMSListWouldBeHelpful() -> Bool {
b3971512
A
397
398 if self.containerMO.honorIDMSListChanges == "UNKNOWN" {
399 return true
400 }
401
b54c578e
A
402 let unknownMOs = (containerMO.machines as? Set<MachineMO> ?? Set()).filter { $0.status == TPMachineIDStatus.unknown.rawValue }
403 let outdatedMOs = unknownMOs.filter { !$0.modifiedInPast(hours: cutoffHours) }
404
b3971512 405 return !outdatedMOs.isEmpty
b54c578e
A
406 }
407}