chatsecureios / ChatSecure / Classes / Controllers / OTROMEMOSignalCoordinator.swift @ 8d76e2e3
History | View | Annotate | Download (30.7 KB)
1 |
// |
---|---|
2 |
// OTROMEMOSignalCoordinator.swift |
3 |
// ChatSecure |
4 |
// |
5 |
// Created by David Chiles on 8/4/16. |
6 |
// Copyright © 2016 Chris Ballinger. All rights reserved. |
7 |
// |
8 |
|
9 |
import UIKit |
10 |
import XMPPFramework |
11 |
import YapDatabase |
12 |
import CocoaLumberjack |
13 |
import SignalProtocolObjC |
14 |
|
15 |
/** |
16 |
* This is the glue between XMPP/OMEMO and Signal |
17 |
*/ |
18 |
@objc open class OTROMEMOSignalCoordinator: NSObject { |
19 |
|
20 |
@objc public static let DeviceListUpdateNotificationName = Notification.Name(rawValue: "DeviceListUpdateNotification") |
21 |
|
22 |
open let signalEncryptionManager:OTRAccountSignalEncryptionManager |
23 |
open let omemoStorageManager:OTROMEMOStorageManager |
24 |
@objc open let accountYapKey:String |
25 |
@objc open let databaseConnection:YapDatabaseConnection |
26 |
@objc open weak var omemoModule:OMEMOModule? |
27 |
@objc open weak var omemoModuleQueue:DispatchQueue? |
28 |
@objc open var callbackQueue:DispatchQueue |
29 |
@objc open let workQueue:DispatchQueue |
30 |
@objc open let messageStorage: MessageStorage |
31 |
|
32 |
fileprivate var myJID:XMPPJID? { |
33 |
get { |
34 |
return omemoModule?.xmppStream?.myJID |
35 |
} |
36 |
} |
37 |
let preKeyCount:UInt = 100 |
38 |
fileprivate var outstandingXMPPStanzaResponseBlocks:[String: (Bool) -> Void] |
39 |
/** |
40 |
Create a OTROMEMOSignalCoordinator for an account. |
41 |
|
42 |
- parameter accountYapKey: The accounts unique yap key |
43 |
- parameter databaseConnection: A yap database connection on which all operations will be completed on |
44 |
` */ |
45 |
@objc public required init(accountYapKey:String, databaseConnection:YapDatabaseConnection, |
46 |
messageStorage: MessageStorage) throws { |
47 |
try self.signalEncryptionManager = OTRAccountSignalEncryptionManager(accountKey: accountYapKey,databaseConnection: databaseConnection) |
48 |
self.omemoStorageManager = OTROMEMOStorageManager(accountKey: accountYapKey, accountCollection: OTRAccount.collection, databaseConnection: databaseConnection) |
49 |
self.accountYapKey = accountYapKey |
50 |
self.databaseConnection = databaseConnection |
51 |
self.outstandingXMPPStanzaResponseBlocks = [:] |
52 |
self.callbackQueue = DispatchQueue(label: "OTROMEMOSignalCoordinator-callback", attributes: []) |
53 |
self.workQueue = DispatchQueue(label: "OTROMEMOSignalCoordinator-work", attributes: []) |
54 |
self.messageStorage = messageStorage |
55 |
} |
56 |
|
57 |
/** |
58 |
Checks that a jid matches our own JID using XMPPJIDCompareBare |
59 |
*/ |
60 |
fileprivate func isOurJID(_ jid:XMPPJID) -> Bool { |
61 |
guard let ourJID = self.myJID else { |
62 |
return false; |
63 |
} |
64 |
|
65 |
return jid.isEqual(to: ourJID, options: .bare) |
66 |
} |
67 |
|
68 |
/** Always call on internal work queue */ |
69 |
fileprivate func callAndRemoveOutstandingBundleBlock(_ elementId:String,success:Bool) { |
70 |
|
71 |
guard let outstandingBlock = self.outstandingXMPPStanzaResponseBlocks[elementId] else { |
72 |
return |
73 |
} |
74 |
outstandingBlock(success) |
75 |
self.outstandingXMPPStanzaResponseBlocks.removeValue(forKey: elementId) |
76 |
} |
77 |
|
78 |
/** |
79 |
This must be called before sending every message. It ensures that for every device there is a session and if not the bundles are fetched. |
80 |
|
81 |
- parameter buddyYapKey: The yap key for the buddy to check |
82 |
- parameter completion: The completion closure called on callbackQueue. If it successfully was able to fetch all the bundles or if it wasn't necessary. If there were no devices for this buddy it will also return flase |
83 |
*/ |
84 |
open func prepareSessionForBuddy(_ buddyYapKey:String, completion:@escaping (Bool) -> Void) { |
85 |
self.prepareSession(buddyYapKey, yapCollection: OTRBuddy.collection, completion: completion) |
86 |
} |
87 |
|
88 |
/** |
89 |
This must be called before sending every message. It ensures that for every device there is a session and if not the bundles are fetched. |
90 |
|
91 |
- parameter yapKey: The yap key for the buddy or account to check |
92 |
- parameter yapCollection: The yap key for the buddy or account to check |
93 |
- parameter completion: The completion closure called on callbackQueue. If it successfully was able to fetch all the bundles or if it wasn't necessary. If there were no devices for this buddy it will also return flase |
94 |
*/ |
95 |
open func prepareSession(_ yapKey:String, yapCollection:String, completion:@escaping (Bool) -> Void) { |
96 |
var devices:[OTROMEMODevice]? = nil |
97 |
var user:String? = nil |
98 |
|
99 |
//Get all the devices ID's for this buddy as well as their username for use with signal and XMPPFramework. |
100 |
self.databaseConnection.read { (transaction) in |
101 |
devices = OTROMEMODevice.allDevices(forParentKey: yapKey, collection: yapCollection, transaction: transaction) |
102 |
user = self.fetchUsername(yapKey, yapCollection: yapCollection, transaction: transaction) |
103 |
} |
104 |
|
105 |
guard let devs = devices, let username = user, let jid = XMPPJID(string:username) else { |
106 |
self.callbackQueue.async(execute: { |
107 |
completion(false) |
108 |
}) |
109 |
return |
110 |
} |
111 |
|
112 |
var finalSuccess = true |
113 |
self.workQueue.async { [weak self] in |
114 |
guard let strongself = self else { |
115 |
return |
116 |
} |
117 |
|
118 |
let group = DispatchGroup() |
119 |
//For each device Check if we have a session. If not then we need to fetch it from their XMPP server. |
120 |
for device in devs where device.deviceId.uint32Value != self?.signalEncryptionManager.registrationId { |
121 |
if !strongself.signalEncryptionManager.sessionRecordExistsForUsername(username, deviceId: device.deviceId.int32Value) || device.publicIdentityKeyData == nil { |
122 |
//No session for this buddy and device combo. We need to fetch the bundle. |
123 |
//No public idenitty key data. We don't have enough information (for the user and UI) to encrypt this message. |
124 |
|
125 |
let elementId = UUID().uuidString |
126 |
|
127 |
group.enter() |
128 |
// Hold on to a closure so that when we get the call back from OMEMOModule we can call this closure. |
129 |
strongself.outstandingXMPPStanzaResponseBlocks[elementId] = { success in |
130 |
if (!success) { |
131 |
finalSuccess = false |
132 |
} |
133 |
|
134 |
group.leave() |
135 |
} |
136 |
//Fetch the bundle |
137 |
strongself.omemoModule?.fetchBundle(forDeviceId: device.deviceId.uint32Value, jid: jid, elementId: elementId) |
138 |
} |
139 |
} |
140 |
|
141 |
if let cQueue = self?.callbackQueue { |
142 |
group.notify(queue: cQueue) { |
143 |
completion(finalSuccess) |
144 |
} |
145 |
|
146 |
} |
147 |
} |
148 |
} |
149 |
|
150 |
fileprivate func fetchUsername(_ yapKey:String, yapCollection:String, transaction:YapDatabaseReadTransaction) -> String? { |
151 |
if let object = transaction.object(forKey: yapKey, inCollection: yapCollection) { |
152 |
if let account = object as? OTRAccount { |
153 |
return account.username |
154 |
} else if let buddy = object as? OTRBuddy { |
155 |
return buddy.username |
156 |
} |
157 |
} |
158 |
return nil |
159 |
} |
160 |
|
161 |
/** |
162 |
Check if we have valid sessions with our other devices and if not fetch their bundles and start sessions. |
163 |
*/ |
164 |
func prepareSessionWithOurDevices(_ completion:@escaping (_ success:Bool) -> Void) { |
165 |
self.prepareSession(self.accountYapKey, yapCollection: OTRAccount.collection, completion: completion) |
166 |
} |
167 |
|
168 |
fileprivate func encryptPayloadWithSignalForDevice(_ device:OTROMEMODevice, payload:Data) throws -> OMEMOKeyData? { |
169 |
var user:String? = nil |
170 |
self.databaseConnection.read({ (transaction) in |
171 |
user = self.fetchUsername(device.parentKey, yapCollection: device.parentCollection, transaction: transaction) |
172 |
}) |
173 |
if let username = user { |
174 |
let encryptedKeyData = try self.signalEncryptionManager.encryptToAddress(payload, name: username, deviceId: device.deviceId.uint32Value) |
175 |
var isPreKey = false |
176 |
if (encryptedKeyData.type == .preKeyMessage) { |
177 |
isPreKey = true |
178 |
} |
179 |
return OMEMOKeyData(deviceId: device.deviceId.uint32Value, data: encryptedKeyData.data, isPreKey: isPreKey) |
180 |
} |
181 |
return nil |
182 |
} |
183 |
|
184 |
/** |
185 |
Gathers necessary information to encrypt a message to the buddy and all this accounts devices that are trusted. Then sends the payload via OMEMOModule |
186 |
|
187 |
- parameter messageBody: The boddy of the message |
188 |
- parameter buddyYapKey: The unique buddy yap key. Used for looking up the username and devices |
189 |
- parameter messageId: The preffered XMPP element Id to be used. |
190 |
- parameter completion: The completion block is called after all the necessary omemo preperation has completed and sendKeyData:iv:toJID:payload:elementId: is invoked |
191 |
*/ |
192 |
open func encryptAndSendMessage(_ message: OTROutgoingMessage, buddyYapKey:String, messageId:String?, completion:@escaping (Bool,NSError?) -> Void) { |
193 |
// Gather bundles for buddy and account here |
194 |
let group = DispatchGroup() |
195 |
|
196 |
let prepareCompletion = { (success:Bool) in |
197 |
group.leave() |
198 |
} |
199 |
|
200 |
group.enter() |
201 |
group.enter() |
202 |
self.prepareSessionForBuddy(buddyYapKey, completion: prepareCompletion) |
203 |
self.prepareSessionWithOurDevices(prepareCompletion) |
204 |
//Even if something went wrong fetching bundles we should push ahead. We may have sessions that can be used. |
205 |
|
206 |
group.notify(queue: self.workQueue) { [weak self] in |
207 |
guard let strongSelf = self else { |
208 |
return |
209 |
} |
210 |
//Strong self work here |
211 |
var bud:OTRBuddy? = nil |
212 |
strongSelf.databaseConnection.read { (transaction) in |
213 |
bud = OTRBuddy.fetchObject(withUniqueID: buddyYapKey, transaction: transaction) |
214 |
} |
215 |
|
216 |
guard let messageBody = message.text, |
217 |
let ivData = OTRSignalEncryptionHelper.generateIV(), let keyData = OTRSignalEncryptionHelper.generateSymmetricKey(), let messageBodyData = messageBody.data(using: String.Encoding.utf8) , let buddy = bud, let buddyJid = XMPPJID(string: buddy.username) else { |
218 |
return |
219 |
} |
220 |
do { |
221 |
//Create the encrypted payload |
222 |
let payload = try OTRSignalEncryptionHelper.encryptData(messageBodyData, key: keyData, iv: ivData) |
223 |
|
224 |
|
225 |
// this does the signal encryption. If we fail it doesn't matter here. We end up trying the next device and fail later if no devices worked. |
226 |
let encryptClosure:(OTROMEMODevice) -> (OMEMOKeyData?) = { device in |
227 |
do { |
228 |
return try strongSelf.encryptPayloadWithSignalForDevice(device, payload: keyData as Data) |
229 |
} catch { |
230 |
return nil |
231 |
} |
232 |
} |
233 |
|
234 |
/** |
235 |
1. Get all devices for this buddy. |
236 |
2. Filter only devices that are trusted. |
237 |
3. encrypt to those devices. |
238 |
4. Remove optional values |
239 |
*/ |
240 |
let buddyKeyDataArray = strongSelf.omemoStorageManager.getDevicesForParentYapKey(buddy.uniqueId, yapCollection: type(of: buddy).collection, trusted: true).map(encryptClosure).flatMap{ $0 } |
241 |
|
242 |
// Stop here if we were not able to encrypt to any of the buddies |
243 |
if (buddyKeyDataArray.count == 0) { |
244 |
strongSelf.callbackQueue.async(execute: { |
245 |
let error = NSError.chatSecureError(OTROMEMOError.noDevicesForBuddy, userInfo: nil) |
246 |
completion(false,error) |
247 |
}) |
248 |
return |
249 |
} |
250 |
|
251 |
/** |
252 |
1. Get all devices for this this account. |
253 |
2. Filter only devices that are trusted and not ourselves. |
254 |
3. encrypt to those devices. |
255 |
4. Remove optional values |
256 |
*/ |
257 |
let ourDevicesKeyData = strongSelf.omemoStorageManager.getDevicesForOurAccount(true).filter({ (device) -> Bool in |
258 |
return device.deviceId.uint32Value != strongSelf.signalEncryptionManager.registrationId |
259 |
}).map(encryptClosure).flatMap{ $0 } |
260 |
|
261 |
// Combine teh two arrays for all key data |
262 |
let keyDataArray = ourDevicesKeyData + buddyKeyDataArray |
263 |
|
264 |
//Make sure we have encrypted the symetric key to someone |
265 |
if (keyDataArray.count > 0) { |
266 |
guard let payloadData = payload?.data, let authTag = payload?.authTag else { |
267 |
return |
268 |
} |
269 |
let finalPayload = NSMutableData() |
270 |
finalPayload.append(payloadData) |
271 |
finalPayload.append(authTag) |
272 |
strongSelf.omemoModule?.sendKeyData(keyDataArray, iv: ivData, to: buddyJid, payload: finalPayload as Data, elementId: messageId) |
273 |
strongSelf.callbackQueue.async(execute: { |
274 |
completion(true,nil) |
275 |
}) |
276 |
return |
277 |
} else { |
278 |
strongSelf.callbackQueue.async(execute: { |
279 |
let error = NSError.chatSecureError(OTROMEMOError.noDevices, userInfo: nil) |
280 |
completion(false,error) |
281 |
}) |
282 |
return |
283 |
} |
284 |
} catch let err as NSError { |
285 |
//This should only happen if we had an error encrypting the payload |
286 |
strongSelf.callbackQueue.async(execute: { |
287 |
completion(false,err) |
288 |
}) |
289 |
return |
290 |
} |
291 |
|
292 |
} |
293 |
} |
294 |
|
295 |
/** |
296 |
Remove a device from the yap store and from the XMPP server. |
297 |
|
298 |
- parameter deviceId: The OMEMO device id |
299 |
*/ |
300 |
open func removeDevice(_ devices:[OTROMEMODevice], completion:@escaping ((Bool) -> Void)) { |
301 |
|
302 |
self.workQueue.async { [weak self] in |
303 |
|
304 |
guard let accountKey = self?.accountYapKey else { |
305 |
completion(false) |
306 |
return |
307 |
} |
308 |
//Array with tuple of username and the device |
309 |
//Needed to avoid nesting yap transactions |
310 |
var usernameDeviceArray = [(String,OTROMEMODevice)]() |
311 |
self?.databaseConnection.readWrite({ (transaction) in |
312 |
devices.forEach({ (device) in |
313 |
|
314 |
// Get the username if buddy or account. Could possibly be extracted into a extension or protocol |
315 |
let extractUsername:(AnyObject?) -> String? = { object in |
316 |
switch object { |
317 |
case let buddy as OTRBuddy: |
318 |
return buddy.username |
319 |
case let account as OTRAccount: |
320 |
return account.username |
321 |
default: return nil |
322 |
} |
323 |
} |
324 |
|
325 |
//Need the parent object to get the username |
326 |
let buddyOrAccount = transaction.object(forKey: device.parentKey, inCollection: device.parentCollection) |
327 |
|
328 |
|
329 |
if let username = extractUsername(buddyOrAccount as AnyObject?) { |
330 |
usernameDeviceArray.append((username,device)) |
331 |
} |
332 |
device.remove(with: transaction) |
333 |
}) |
334 |
}) |
335 |
|
336 |
//For each username device pair remove the underlying signal session |
337 |
usernameDeviceArray.forEach({ (username,device) in |
338 |
_ = self?.signalEncryptionManager.removeSessionRecordForUsername(username, deviceId: device.deviceId.int32Value) |
339 |
}) |
340 |
|
341 |
|
342 |
let remoteDevicesToRemove = devices.filter({ (device) -> Bool in |
343 |
// Can only remove devices that belong to this account from the remote server. |
344 |
return device.parentKey == accountKey && device.parentCollection == OTRAccount.collection |
345 |
}) |
346 |
|
347 |
if( remoteDevicesToRemove.count > 0 ) { |
348 |
let elementId = UUID().uuidString |
349 |
let deviceIds = remoteDevicesToRemove.map({ (device) -> NSNumber in |
350 |
return device.deviceId |
351 |
}) |
352 |
self?.outstandingXMPPStanzaResponseBlocks[elementId] = { success in |
353 |
completion(success) |
354 |
} |
355 |
self?.omemoModule?.removeDeviceIds(deviceIds, elementId: elementId) |
356 |
} else { |
357 |
completion(true) |
358 |
} |
359 |
} |
360 |
} |
361 |
|
362 |
open func processKeyData(_ keyData: [OMEMOKeyData], iv: Data, senderDeviceId: UInt32, forJID: XMPPJID, payload: Data?, delayed: Date?, forwarded: Bool, isIncoming: Bool, message: XMPPMessage) { |
363 |
let aesGcmBlockLength = 16 |
364 |
guard let encryptedPayload = payload, encryptedPayload.count > 0, let myJID = self.myJID else { |
365 |
return |
366 |
} |
367 |
var addressJID = forJID.bareJID |
368 |
if !isIncoming { |
369 |
addressJID = myJID.bareJID |
370 |
} |
371 |
let rid = self.signalEncryptionManager.registrationId |
372 |
|
373 |
//Could have multiple matching device id. This is extremely rare but possible that the sender has another device that collides with our device id. |
374 |
var unencryptedKeyData: Data? |
375 |
for key in keyData where key.deviceId == rid { |
376 |
let keyData = key.data |
377 |
do { |
378 |
unencryptedKeyData = try self.signalEncryptionManager.decryptFromAddress(keyData, name: addressJID.bare, deviceId: senderDeviceId) |
379 |
// have successfully decripted the AES key. We should break and use it to decrypt the payload |
380 |
break |
381 |
} catch let error { |
382 |
DDLogError("Error decrypting OMEMO message for \(addressJID): \(error) \(message)") |
383 |
let nsError = error as NSError |
384 |
if nsError.domain == SignalErrorDomain, nsError.code == SignalError.duplicateMessage.rawValue { |
385 |
// duplicate messages are benign and can be ignored |
386 |
DDLogInfo("Ignoring duplicate OMEMO message: \(message)") |
387 |
return |
388 |
} |
389 |
let buddyAddress = SignalAddress(name: addressJID.bare, deviceId: Int32(senderDeviceId)) |
390 |
if self.signalEncryptionManager.storage.sessionRecordExists(for: buddyAddress) { |
391 |
// Session is corrupted |
392 |
let _ = self.signalEncryptionManager.storage.deleteSessionRecord(for: buddyAddress) |
393 |
DDLogError("Session exists and is possibly corrupted. Deleting...") |
394 |
} |
395 |
return |
396 |
} |
397 |
} |
398 |
|
399 |
guard var aesKey = unencryptedKeyData else { |
400 |
return |
401 |
} |
402 |
var authTag: Data? |
403 |
|
404 |
// Treat >=32 bytes OMEMO 'keys' as containing the auth tag. |
405 |
// https://github.com/ChatSecure/ChatSecure-iOS/issues/647 |
406 |
if (aesKey.count >= aesGcmBlockLength * 2) { |
407 |
|
408 |
authTag = aesKey.subdata(in: aesGcmBlockLength..<aesKey.count) |
409 |
aesKey = aesKey.subdata(in: 0..<aesGcmBlockLength) |
410 |
} |
411 |
|
412 |
var tmpBody: Data? |
413 |
// If there's already an auth tag, that means the payload |
414 |
// doesn't contain the auth tag. |
415 |
if authTag != nil { // omemo namespace |
416 |
tmpBody = encryptedPayload |
417 |
} else { // 'siacs' namespace fallback |
418 |
|
419 |
tmpBody = encryptedPayload.subdata(in: 0..<encryptedPayload.count - aesGcmBlockLength) |
420 |
authTag = encryptedPayload.subdata(in: encryptedPayload.count - aesGcmBlockLength..<encryptedPayload.count) |
421 |
} |
422 |
guard let tag = authTag, let encryptedBody = tmpBody else { |
423 |
return |
424 |
} |
425 |
|
426 |
do { |
427 |
guard let messageBody = try OTRSignalEncryptionHelper.decryptData(encryptedBody, key: aesKey, iv: iv, authTag: tag), |
428 |
let messageString = String(data: messageBody, encoding: String.Encoding.utf8), |
429 |
messageString.count > 0 else { |
430 |
return |
431 |
} |
432 |
|
433 |
let preSave: MessageStorage.PreSave = { message, transaction in |
434 |
guard let buddy = OTRXMPPBuddy.fetchBuddy(jid: forJID, accountUniqueId: self.accountYapKey, transaction: transaction) else { |
435 |
return |
436 |
} |
437 |
|
438 |
let deviceNumber = NSNumber(value: senderDeviceId as UInt32) |
439 |
let deviceYapKey = OTROMEMODevice.yapKey(withDeviceId: deviceNumber, parentKey: buddy.uniqueId, parentCollection: OTRBuddy.collection) |
440 |
message.messageSecurityInfo = OTRMessageEncryptionInfo.init(omemoDevice: deviceYapKey, collection: OTROMEMODevice.collection) |
441 |
|
442 |
message.save(with: transaction) |
443 |
|
444 |
// Should we be using the date of the xmpp message? |
445 |
buddy.lastMessageId = message.uniqueId |
446 |
buddy.save(with: transaction) |
447 |
|
448 |
//Update device last received message |
449 |
guard let device = OTROMEMODevice.fetchObject(withUniqueID: deviceYapKey, transaction: transaction) else { |
450 |
return |
451 |
} |
452 |
let newDevice = OTROMEMODevice(deviceId: device.deviceId, trustLevel: device.trustLevel, parentKey: device.parentKey, parentCollection: device.parentCollection, publicIdentityKeyData: device.publicIdentityKeyData, lastSeenDate: Date()) |
453 |
newDevice.save(with: transaction) |
454 |
} |
455 |
|
456 |
if forwarded { |
457 |
self.messageStorage.handleForwardedMessage(message, forJID: forJID, body: messageString, accountId: self.accountYapKey, delayed: delayed, isIncoming: isIncoming, preSave: preSave) |
458 |
} else { |
459 |
self.messageStorage.handleDirectMessage(message, body: messageString, accountId: self.accountYapKey, preSave: preSave) |
460 |
} |
461 |
} catch let error { |
462 |
DDLogError("Message decryption error: \(error)") |
463 |
return |
464 |
} |
465 |
} |
466 |
} |
467 |
|
468 |
extension OTROMEMOSignalCoordinator: OMEMOModuleDelegate { |
469 |
|
470 |
public func omemo(_ omemo: OMEMOModule, publishedDeviceIds deviceIds: [NSNumber], responseIq: XMPPIQ, outgoingIq: XMPPIQ) { |
471 |
DDLogVerbose("publishedDeviceIds: \(responseIq)") |
472 |
|
473 |
} |
474 |
|
475 |
public func omemo(_ omemo: OMEMOModule, failedToPublishDeviceIds deviceIds: [NSNumber], errorIq: XMPPIQ?, outgoingIq: XMPPIQ) { |
476 |
DDLogWarn("failedToPublishDeviceIds: \(String(describing: errorIq))") |
477 |
} |
478 |
|
479 |
public func omemo(_ omemo: OMEMOModule, deviceListUpdate deviceIds: [NSNumber], from fromJID: XMPPJID, incomingElement: XMPPElement) { |
480 |
DDLogVerbose("deviceListUpdate: \(fromJID) \(deviceIds)") |
481 |
self.workQueue.async { [weak self] in |
482 |
if let eid = incomingElement.elementID { |
483 |
self?.callAndRemoveOutstandingBundleBlock(eid, success: true) |
484 |
} |
485 |
} |
486 |
} |
487 |
|
488 |
public func omemo(_ omemo: OMEMOModule, failedToFetchDeviceIdsFor fromJID: XMPPJID, errorIq: XMPPIQ?, outgoingIq: XMPPIQ) { |
489 |
DDLogWarn("failedToFetchDeviceIdsFor \(fromJID)") |
490 |
} |
491 |
|
492 |
public func omemo(_ omemo: OMEMOModule, publishedBundle bundle: OMEMOBundle, responseIq: XMPPIQ, outgoingIq: XMPPIQ) { |
493 |
DDLogVerbose("publishedBundle: \(responseIq) \(outgoingIq)") |
494 |
} |
495 |
|
496 |
public func omemo(_ omemo: OMEMOModule, failedToPublishBundle bundle: OMEMOBundle, errorIq: XMPPIQ?, outgoingIq: XMPPIQ) { |
497 |
DDLogWarn("failedToPublishBundle: \(String(describing: errorIq)) \(outgoingIq)") |
498 |
} |
499 |
|
500 |
public func omemo(_ omemo: OMEMOModule, fetchedBundle bundle: OMEMOBundle, from fromJID: XMPPJID, responseIq: XMPPIQ, outgoingIq: XMPPIQ) { |
501 |
DDLogVerbose("fetchedBundle: \(responseIq) \(outgoingIq)") |
502 |
|
503 |
if (self.isOurJID(fromJID) && bundle.deviceId == self.signalEncryptionManager.registrationId) { |
504 |
//DDLogVerbose("fetchedOurOwnBundle: \(responseIq) \(outgoingIq)") |
505 |
|
506 |
//We fetched our own bundle |
507 |
if let ourDatabaseBundle = self.fetchMyBundle() { |
508 |
//This bundle doesn't have the correct identity key. Something has gone wrong and we should republish |
509 |
if ourDatabaseBundle.identityKey != bundle.identityKey { |
510 |
//DDLogError("Bundle identityKeys do not match! \(ourDatabaseBundle.identityKey) vs \(bundle.identityKey)") |
511 |
omemo.publishBundle(ourDatabaseBundle, elementId: nil) |
512 |
} |
513 |
} |
514 |
return; |
515 |
} |
516 |
|
517 |
self.workQueue.async { [weak self] in |
518 |
let elementId = outgoingIq.elementID |
519 |
if (bundle.preKeys.count == 0) { |
520 |
self?.callAndRemoveOutstandingBundleBlock(elementId!, success: false) |
521 |
return |
522 |
} |
523 |
var result = false |
524 |
//Consume the incoming bundle. This goes through signal and should hit the storage delegate. So we don't need to store ourselves here. |
525 |
do { |
526 |
try self?.signalEncryptionManager.consumeIncomingBundle(fromJID.bare, bundle: bundle) |
527 |
result = true |
528 |
} catch let err { |
529 |
DDLogWarn("Error consuming incoming bundle: \(err) \(responseIq.prettyXMLString())") |
530 |
} |
531 |
self?.callAndRemoveOutstandingBundleBlock(elementId!, success: result) |
532 |
} |
533 |
|
534 |
} |
535 |
public func omemo(_ omemo: OMEMOModule, failedToFetchBundleForDeviceId deviceId: UInt32, from fromJID: XMPPJID, errorIq: XMPPIQ?, outgoingIq: XMPPIQ) { |
536 |
|
537 |
self.workQueue.async { [weak self] in |
538 |
let elementId = outgoingIq.elementID |
539 |
self?.callAndRemoveOutstandingBundleBlock(elementId!, success: false) |
540 |
} |
541 |
} |
542 |
|
543 |
public func omemo(_ omemo: OMEMOModule, removedBundleId bundleId: UInt32, responseIq: XMPPIQ, outgoingIq: XMPPIQ) { |
544 |
|
545 |
} |
546 |
|
547 |
public func omemo(_ omemo: OMEMOModule, failedToRemoveBundleId bundleId: UInt32, errorIq: XMPPIQ?, outgoingIq: XMPPIQ) { |
548 |
|
549 |
} |
550 |
|
551 |
public func omemo(_ omemo: OMEMOModule, failedToRemoveDeviceIds deviceIds: [NSNumber], errorIq: XMPPIQ?, elementId: String?) { |
552 |
self.workQueue.async { [weak self] in |
553 |
if let eid = elementId { |
554 |
self?.callAndRemoveOutstandingBundleBlock(eid, success: false) |
555 |
} |
556 |
} |
557 |
} |
558 |
|
559 |
public func omemo(_ omemo: OMEMOModule, receivedKeyData keyData: [OMEMOKeyData], iv: Data, senderDeviceId: UInt32, from fromJID: XMPPJID, payload: Data?, message: XMPPMessage) { |
560 |
self.processKeyData(keyData, iv: iv, senderDeviceId: senderDeviceId, forJID: fromJID, payload: payload, delayed: nil, forwarded: false, isIncoming: true, message: message) |
561 |
} |
562 |
|
563 |
public func omemo(_ omemo: OMEMOModule, receivedForwardedKeyData keyData: [OMEMOKeyData], iv: Data, senderDeviceId: UInt32, for forJID: XMPPJID, payload: Data?, isIncoming: Bool, delayed: Date?, forwardedMessage: XMPPMessage, originalMessage: XMPPMessage) { |
564 |
self.processKeyData(keyData, iv: iv, senderDeviceId: senderDeviceId, forJID: forJID, payload: payload, delayed: delayed, forwarded: true, isIncoming: isIncoming, message: forwardedMessage) |
565 |
} |
566 |
} |
567 |
|
568 |
extension OTROMEMOSignalCoordinator:OMEMOStorageDelegate { |
569 |
|
570 |
public func configure(withParent aParent: OMEMOModule, queue: DispatchQueue) -> Bool { |
571 |
self.omemoModule = aParent |
572 |
self.omemoModuleQueue = queue |
573 |
return true |
574 |
} |
575 |
|
576 |
public func storeDeviceIds(_ deviceIds: [NSNumber], for jid: XMPPJID) { |
577 |
|
578 |
let isOurDeviceList = self.isOurJID(jid) |
579 |
|
580 |
if (isOurDeviceList) { |
581 |
self.omemoStorageManager.storeOurDevices(deviceIds) |
582 |
} else { |
583 |
self.omemoStorageManager.storeBuddyDevices(deviceIds, buddyUsername: jid.bare, completion: {() -> Void in |
584 |
|
585 |
//Devices updated for buddy |
586 |
DispatchQueue.main.async { |
587 |
NotificationCenter.default.post(name: OTROMEMOSignalCoordinator.DeviceListUpdateNotificationName, object: self, userInfo: ["jid":jid]) |
588 |
} |
589 |
}) |
590 |
} |
591 |
} |
592 |
|
593 |
public func fetchDeviceIds(for jid: XMPPJID) -> [NSNumber] { |
594 |
var devices:[OTROMEMODevice]? |
595 |
if self.isOurJID(jid) { |
596 |
devices = self.omemoStorageManager.getDevicesForOurAccount(nil) |
597 |
|
598 |
} else { |
599 |
devices = self.omemoStorageManager.getDevicesForBuddy(jid.bare, trusted:nil) |
600 |
} |
601 |
//Convert from devices array to NSNumber array. |
602 |
return (devices?.map({ (device) -> NSNumber in |
603 |
return device.deviceId |
604 |
})) ?? [NSNumber]() |
605 |
|
606 |
} |
607 |
|
608 |
//Always returns most complete bundle with correct count of prekeys |
609 |
public func fetchMyBundle() -> OMEMOBundle? { |
610 |
var _bundle: OMEMOBundle? = nil |
611 |
|
612 |
do { |
613 |
_bundle = try signalEncryptionManager.storage.fetchOurExistingBundle() |
614 |
|
615 |
} catch let omemoError as OMEMOBundleError { |
616 |
switch omemoError { |
617 |
case .invalid: |
618 |
DDLogError("Found invalid stored bundle!") |
619 |
// delete??? |
620 |
break |
621 |
default: |
622 |
break |
623 |
} |
624 |
} catch let error { |
625 |
DDLogError("Other error fetching bundle! \(error)") |
626 |
} |
627 |
let maxTries = 50 |
628 |
var tries = 0 |
629 |
while _bundle == nil && tries < maxTries { |
630 |
tries = tries + 1 |
631 |
do { |
632 |
_bundle = try self.signalEncryptionManager.generateOutgoingBundle(self.preKeyCount) |
633 |
} catch let error { |
634 |
DDLogError("Error generating bundle! Try #\(tries)/\(maxTries) \(error)") |
635 |
} |
636 |
} |
637 |
guard let bundle = _bundle else { |
638 |
DDLogError("Could not fetch or generate valid bundle!") |
639 |
return nil |
640 |
} |
641 |
|
642 |
var preKeys = bundle.preKeys |
643 |
|
644 |
let keysToGenerate = Int(self.preKeyCount) - preKeys.count |
645 |
|
646 |
//Check if we don't have all the prekeys we need |
647 |
if (keysToGenerate > 0) { |
648 |
var start:UInt = 0 |
649 |
if let maxId = self.signalEncryptionManager.storage.currentMaxPreKeyId() { |
650 |
start = UInt(maxId) + 1 |
651 |
} |
652 |
|
653 |
if let newPreKeys = self.signalEncryptionManager.generatePreKeys(start, count: UInt(keysToGenerate)) { |
654 |
let omemoKeys = OMEMOPreKey.preKeysFromSignal(newPreKeys) |
655 |
preKeys.append(contentsOf: omemoKeys) |
656 |
} |
657 |
} |
658 |
|
659 |
let newBundle = bundle.copyBundle(newPreKeys: preKeys) |
660 |
return newBundle |
661 |
} |
662 |
|
663 |
public func isSessionValid(_ jid: XMPPJID, deviceId: UInt32) -> Bool { |
664 |
return self.signalEncryptionManager.sessionRecordExistsForUsername(jid.bare, deviceId: Int32(deviceId)) |
665 |
} |
666 |
} |