diff --git a/analysis_options.yaml b/analysis_options.yaml index 5ded12b..beb23d6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,3 +10,7 @@ linter: analyzer: exclude: - "lib/protobuf/*.dart" + # TODO: Remove once OmemoSessionManager is gone + - "test/omemo_test.dart" + - "example/omemo_dart_example.dart" + - "test/serialisation_test.dart" diff --git a/lib/src/omemo/omemomanager.dart b/lib/src/omemo/omemomanager.dart index 75d06cc..5743abe 100644 --- a/lib/src/omemo/omemomanager.dart +++ b/lib/src/omemo/omemomanager.dart @@ -41,13 +41,15 @@ class OmemoManager { /// Functions for connecting with the OMEMO library - /// Send an empty OMEMO:2 message using the encrypted payload [result] to [recipientJid]. + /// Send an empty OMEMO:2 message using the encrypted payload @result to + /// @recipientJid. final Future Function(EncryptionResult result, String recipientJid) sendEmptyOmemoMessage; - /// Fetch the list of device ids associated with [jid]. - final Future> Function(String jid) fetchDeviceList; + /// Fetch the list of device ids associated with @jid. If the device list cannot be + /// fetched, return null. + final Future?> Function(String jid) fetchDeviceList; - /// Fetch the device bundle for the device with id [id] of [jid]. If it cannot be fetched, return null. + /// Fetch the device bundle for the device with id @id of jid. If it cannot be fetched, return null. final Future Function(String jid, int id) fetchDeviceBundle; /// Map bare JID to its known devices @@ -316,10 +318,7 @@ class OmemoManager { } final devices = _deviceList[senderJid]; - if (devices == null) { - throw NoDecryptionKeyException(); - } - if (!devices.contains(senderDeviceId)) { + if (devices?.contains(senderDeviceId) != true) { throw NoDecryptionKeyException(); } @@ -367,6 +366,8 @@ class OmemoManager { if (!_deviceListRequested.containsKey(jid) || !_deviceList.containsKey(jid)) { // We don't have an up-to-date version of the device list final newDeviceList = await fetchDeviceList(jid); + if (newDeviceList == null) return []; + _deviceList[jid] = newDeviceList; bundlesToFetch = newDeviceList .where((id) { @@ -383,6 +384,7 @@ class OmemoManager { .toList(); } + _log.finest('Fetching bundles $bundlesToFetch for $jid'); final newBundles = List.empty(growable: true); for (final id in bundlesToFetch) { final bundle = await fetchDeviceBundle(jid, id); diff --git a/lib/src/omemo/sessionmanager.dart b/lib/src/omemo/sessionmanager.dart index 33be86e..eb8409d 100644 --- a/lib/src/omemo/sessionmanager.dart +++ b/lib/src/omemo/sessionmanager.dart @@ -27,6 +27,7 @@ import 'package:synchronized/synchronized.dart'; @Deprecated('Use OmemoManager instead') class OmemoSessionManager { + @Deprecated('Use OmemoManager instead') OmemoSessionManager(this._device, this._deviceMap, this._ratchetMap, this._trustManager) : _lock = Lock(), _deviceLock = Lock(), @@ -35,6 +36,7 @@ class OmemoSessionManager { /// Deserialise the OmemoSessionManager from JSON data [data] that does not contain /// the ratchet sessions. + @Deprecated('Use OmemoManager instead') factory OmemoSessionManager.fromJsonWithoutSessions( Map data, Map ratchetMap, diff --git a/test/omemomanager_test.dart b/test/omemomanager_test.dart index b6c5c08..a0b89b8 100644 --- a/test/omemomanager_test.dart +++ b/test/omemomanager_test.dart @@ -243,4 +243,335 @@ void main() { 42, ); }); + + test('Test receiving a message encrypted for another device', () async { + const aliceJid = 'alice@server1'; + const bobJid = 'bob@server2'; + var oldDevice = true; + + final aliceDevice = await Device.generateNewDevice(aliceJid, opkAmount: 1); + final bobOldDevice = await Device.generateNewDevice(bobJid, opkAmount: 1); + final bobCurrentDevice = await Device.generateNewDevice(bobJid, opkAmount: 1); + + final aliceManager = omemo.OmemoManager( + aliceDevice, + AlwaysTrustingTrustManager(), + (result, recipientJid) async {}, + (jid) async { + expect(jid, bobJid); + + return oldDevice ? + [ bobOldDevice.id ] : + [ bobCurrentDevice.id ]; + }, + (jid, id) async { + expect(jid, bobJid); + return oldDevice ? + bobOldDevice.toBundle() : + bobCurrentDevice.toBundle(); + }, + ); + final bobManager = omemo.OmemoManager( + bobCurrentDevice, + AlwaysTrustingTrustManager(), + (result, recipientJid) async {}, + (jid) async => [], + (jid, id) async => null, + ); + + // Alice encrypts a message to Bob + final aliceResult1 = await aliceManager.onOutgoingStanza( + const OmemoOutgoingStanza( + [bobJid], + 'Hello Bob', + ), + ); + + // Bob's current device receives it + final bobResult1 = await bobManager.onIncomingStanza( + OmemoIncomingStanza( + aliceJid, + aliceDevice.id, + DateTime.now().millisecondsSinceEpoch, + aliceResult1!.encryptedKeys, + base64.encode(aliceResult1.ciphertext!), + ), + ); + + expect(bobResult1.payload, null); + expect(bobResult1.error is NotEncryptedForDeviceException, true); + + // Now Alice's client loses and regains the connection + aliceManager.onNewConnection(); + oldDevice = false; + + // And Alice sends a new message + final aliceResult2 = await aliceManager.onOutgoingStanza( + const OmemoOutgoingStanza( + [bobJid], + 'Hello Bob x2', + ), + ); + final bobResult2 = await bobManager.onIncomingStanza( + OmemoIncomingStanza( + aliceJid, + aliceDevice.id, + DateTime.now().millisecondsSinceEpoch, + aliceResult2!.encryptedKeys, + base64.encode(aliceResult2.ciphertext!), + ), + ); + + expect(aliceResult2.encryptedKeys.length, 1); + expect(bobResult2.payload, 'Hello Bob x2'); + }); + + test('Test receiving a response from a new device', () async { + const aliceJid = 'alice@server1'; + const bobJid = 'bob@server2'; + var bothDevices = false; + + final aliceDevice = await Device.generateNewDevice(aliceJid, opkAmount: 1); + final bobDevice1 = await Device.generateNewDevice(bobJid, opkAmount: 1); + final bobDevice2 = await Device.generateNewDevice(bobJid, opkAmount: 1); + + final aliceManager = omemo.OmemoManager( + aliceDevice, + AlwaysTrustingTrustManager(), + (result, recipientJid) async {}, + (jid) async { + expect(jid, bobJid); + + return [ + bobDevice1.id, + + if (bothDevices) + bobDevice2.id, + ]; + }, + (jid, id) async { + expect(jid, bobJid); + + if (bothDevices) { + if (id == bobDevice1.id) { + return bobDevice1.toBundle(); + } else if (id == bobDevice2.id) { + return bobDevice2.toBundle(); + } + } else { + if (id == bobDevice1.id) return bobDevice1.toBundle(); + } + + return null; + }, + ); + final bobManager1 = omemo.OmemoManager( + bobDevice1, + AlwaysTrustingTrustManager(), + (result, recipientJid) async {}, + (jid) async => [], + (jid, id) async => null, + ); + final bobManager2 = omemo.OmemoManager( + bobDevice2, + AlwaysTrustingTrustManager(), + (result, recipientJid) async {}, + (jid) async { + expect(jid, aliceJid); + return [aliceDevice.id]; + }, + (jid, id) async { + expect(jid, aliceJid); + return aliceDevice.toBundle(); + }, + ); + + // Alice sends a message to Bob + final aliceResult1 = await aliceManager.onOutgoingStanza( + const OmemoOutgoingStanza( + [bobJid], + 'Hello Bob!', + ), + ); + + // Bob decrypts it + final bobResult1 = await bobManager1.onIncomingStanza( + OmemoIncomingStanza( + aliceJid, + aliceDevice.id, + DateTime.now().millisecondsSinceEpoch, + aliceResult1!.encryptedKeys, + base64.encode(aliceResult1.ciphertext!), + ), + ); + + expect(aliceResult1.encryptedKeys.length, 1); + expect(bobResult1.payload, 'Hello Bob!'); + + // Now Bob encrypts from his new device + bothDevices = true; + final bobResult2 = await bobManager2.onOutgoingStanza( + const OmemoOutgoingStanza( + [aliceJid], + 'Hello from my new device', + ), + ); + + // And Alice decrypts it + final aliceResult2 = await aliceManager.onIncomingStanza( + OmemoIncomingStanza( + bobJid, + bobDevice2.id, + DateTime.now().millisecondsSinceEpoch, + bobResult2!.encryptedKeys, + base64.encode(bobResult2.ciphertext!), + ), + ); + + expect(aliceResult2.payload, 'Hello from my new device'); + }); + + test('Test receiving a device list update', () async { + const aliceJid = 'alice@server1'; + const bobJid = 'bob@server2'; + var bothDevices = false; + + final aliceDevice = await Device.generateNewDevice(aliceJid, opkAmount: 1); + final bobDevice1 = await Device.generateNewDevice(bobJid, opkAmount: 1); + final bobDevice2 = await Device.generateNewDevice(bobJid, opkAmount: 1); + + final aliceManager = omemo.OmemoManager( + aliceDevice, + AlwaysTrustingTrustManager(), + (result, recipientJid) async {}, + (jid) async { + expect(jid, bobJid); + + return [ + bobDevice1.id, + + if (bothDevices) + bobDevice2.id, + ]; + }, + (jid, id) async { + expect(jid, bobJid); + + if (bothDevices) { + if (id == bobDevice1.id) { + return bobDevice1.toBundle(); + } else if (id == bobDevice2.id) { + return bobDevice2.toBundle(); + } + } else { + if (id == bobDevice1.id) return bobDevice1.toBundle(); + } + + return null; + }, + ); + final bobManager1 = omemo.OmemoManager( + bobDevice1, + AlwaysTrustingTrustManager(), + (result, recipientJid) async {}, + (jid) async => null, + (jid, id) async => null, + ); + final bobManager2 = omemo.OmemoManager( + bobDevice2, + AlwaysTrustingTrustManager(), + (result, recipientJid) async {}, + (jid) async => null, + (jid, id) async => null, + ); + + // Alice sends a message to Bob + final aliceResult1 = await aliceManager.onOutgoingStanza( + const OmemoOutgoingStanza( + [bobJid], + 'Hello Bob!', + ), + ); + + // Bob decrypts it + final bobResult1 = await bobManager1.onIncomingStanza( + OmemoIncomingStanza( + aliceJid, + aliceDevice.id, + DateTime.now().millisecondsSinceEpoch, + aliceResult1!.encryptedKeys, + base64.encode(aliceResult1.ciphertext!), + ), + ); + + expect(aliceResult1.encryptedKeys.length, 1); + expect(bobResult1.payload, 'Hello Bob!'); + + // Bob acks the ratchet session + await aliceManager.ratchetAcknowledged(bobJid, bobDevice1.id); + + // Bob now publishes a new device + bothDevices = true; + aliceManager.onDeviceListUpdate( + bobJid, + [ + bobDevice1.id, + bobDevice2.id, + ], + ); + + // Now Alice encrypts another message + final aliceResult2 = await aliceManager.onOutgoingStanza( + const OmemoOutgoingStanza( + [bobJid], + 'Hello Bob! x2', + ), + ); + + expect(aliceResult2!.encryptedKeys.length, 2); + + // And Bob decrypts it + final bobResult21 = await bobManager1.onIncomingStanza( + OmemoIncomingStanza( + aliceJid, + aliceDevice.id, + DateTime.now().millisecondsSinceEpoch, + aliceResult2.encryptedKeys, + base64.encode(aliceResult2.ciphertext!), + ), + ); + final bobResult22 = await bobManager2.onIncomingStanza( + OmemoIncomingStanza( + aliceJid, + aliceDevice.id, + DateTime.now().millisecondsSinceEpoch, + aliceResult2.encryptedKeys, + base64.encode(aliceResult2.ciphertext!), + ), + ); + + expect(bobResult21.payload, 'Hello Bob! x2'); + expect(bobResult22.payload, 'Hello Bob! x2'); + + // Bob2 now responds + final bobResult32 = await bobManager2.onOutgoingStanza( + const OmemoOutgoingStanza( + [aliceJid], + 'Hello Alice!', + ), + ); + + // And Alice responds + final aliceResult3 = await aliceManager.onIncomingStanza( + OmemoIncomingStanza( + bobJid, + bobDevice2.id, + DateTime.now().millisecondsSinceEpoch, + bobResult32!.encryptedKeys, + base64.encode(bobResult32.ciphertext!), + ), + ); + + expect(aliceResult3.payload, 'Hello Alice!'); + }); }