Statistics
| Branch: | Tag: | Revision:

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
}