]>
Commit | Line | Data |
---|---|---|
b54c578e A |
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 | ||
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 | |
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. | |
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 | } |