diff --git a/lib/src/omemo/bundle.dart b/lib/src/omemo/bundle.dart index b518bd1..7896a7b 100644 --- a/lib/src/omemo/bundle.dart +++ b/lib/src/omemo/bundle.dart @@ -5,6 +5,7 @@ import 'package:omemo_dart/src/keys.dart'; class OmemoBundle { const OmemoBundle( + this.jid, this.id, this.spkEncoded, this.spkId, @@ -12,6 +13,9 @@ class OmemoBundle { this.ikEncoded, this.opksEncoded, ); + /// The bare Jid the Bundle belongs to + final String jid; + /// The device Id final int id; /// The SPK but base64 encoded final String spkEncoded; diff --git a/lib/src/omemo/device.dart b/lib/src/omemo/device.dart index e9df85d..5e3f41e 100644 --- a/lib/src/omemo/device.dart +++ b/lib/src/omemo/device.dart @@ -10,7 +10,7 @@ import 'package:omemo_dart/src/x3dh/x3dh.dart'; @immutable class Device { - const Device(this.id, this.ik, this.spk, this.spkId, this.spkSignature, this.opks); + const Device(this.jid, this.id, this.ik, this.spk, this.spkId, this.spkSignature, this.opks); /// Deserialize the Device factory Device.fromJson(Map data) { @@ -19,6 +19,7 @@ class Device { // key. /* { + 'jid': 'alice@...', 'id': 123, 'ik': 'base/64/encoded', 'ik_pub': 'base/64/encoded', @@ -45,6 +46,7 @@ class Device { } return Device( + data['jid']! as String, data['id']! as int, OmemoKeyPair.fromBytes( base64.decode(data['ik_pub']! as String), @@ -63,7 +65,7 @@ class Device { } /// Generate a completely new device, i.e. cryptographic identity. - static Future generateNewDevice({ int opkAmount = 100 }) async { + static Future generateNewDevice(String jid, { int opkAmount = 100 }) async { final id = generateRandom32BitNumber(); final ik = await OmemoKeyPair.generateNewPair(KeyPairType.ed25519); final spk = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); @@ -75,8 +77,11 @@ class Device { opks[i] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); } - return Device(id, ik, spk, spkId, signature, opks); + return Device(jid, id, ik, spk, spkId, signature, opks); } + + /// Our bare Jid + final String jid; /// The device Id final int id; @@ -100,6 +105,7 @@ class Device { opks[id] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); return Device( + jid, id, ik, spk, @@ -117,6 +123,7 @@ class Device { final newSignature = await sig(ik, await newSpk.pk.getBytes()); return Device( + jid, id, ik, newSpk, @@ -134,6 +141,7 @@ class Device { } return OmemoBundle( + jid, id, base64.encode(await spk.pk.getBytes()), spkId, @@ -156,6 +164,7 @@ class Device { } return { + 'jid': jid, 'id': id, 'ik': base64.encode(await ik.sk.getBytes()), 'ik_pub': base64.encode(await ik.pk.getBytes()), @@ -189,6 +198,7 @@ class Device { return id == other.id && ikMatch && spkMatch && + jid == other.jid && listsEqual(spkSignature, other.spkSignature) && spkId == other.spkId && opksMatch; diff --git a/lib/src/omemo/sessionmanager.dart b/lib/src/omemo/sessionmanager.dart index 6035a7c..76f3bc0 100644 --- a/lib/src/omemo/sessionmanager.dart +++ b/lib/src/omemo/sessionmanager.dart @@ -40,7 +40,8 @@ class EncryptionResult { @immutable class EncryptedKey { - const EncryptedKey(this.rid, this.value, this.kex); + const EncryptedKey(this.jid, this.rid, this.value, this.kex); + final String jid; final int rid; final String value; final bool kex; @@ -71,9 +72,9 @@ class OmemoSessionManager { } /// Generate a new cryptographic identity. - static Future generateNewIdentity({ int opkAmount = 100 }) async { + static Future generateNewIdentity(String jid, { int opkAmount = 100 }) async { assert(opkAmount > 0, 'opkAmount must be bigger than 0.'); - final device = await Device.generateNewDevice(opkAmount: opkAmount); + final device = await Device.generateNewDevice(jid, opkAmount: opkAmount); return OmemoSessionManager(device, {}, {}); } @@ -183,10 +184,15 @@ class OmemoSessionManager { await _addSession(jid, deviceId, ratchet); } + + /// Like [encryptToJids] but only for one Jid [jid]. + Future encryptToJid(String jid, String plaintext, { List? newSessions }) { + return encryptToJids([jid], plaintext, newSessions: newSessions); + } - /// Encrypt the key [plaintext] for all known bundles of [jid]. Returns a map that - /// maps the Bundle Id to the ciphertext of [plaintext]. - Future encryptToJid(String jid, String plaintext, { List? newSessions }) async { + /// Encrypt the key [plaintext] for all known bundles of the Jids in [jids]. Returns a + /// map that maps the device Id to the ciphertext of [plaintext]. + Future encryptToJids(List jids, String plaintext, { List? newSessions }) async { final encryptedKeys = List.empty(growable: true); // Generate the key and encrypt the plaintext @@ -203,38 +209,42 @@ class OmemoSessionManager { final kex = {}; if (newSessions != null) { for (final newSession in newSessions) { - kex[newSession.id] = await addSessionFromBundle(jid, newSession.id, newSession); + kex[newSession.id] = await addSessionFromBundle(newSession.jid, newSession.id, newSession); } } await _lock.synchronized(() async { // We assume that the user already checked if the session exists - for (final deviceId in _deviceMap[jid]!) { - final ratchetKey = RatchetMapKey(jid, deviceId); - final ratchet = _ratchetMap[ratchetKey]!; - final ciphertext = (await ratchet.ratchetEncrypt(concatKey)).ciphertext; + for (final jid in jids) { + for (final deviceId in _deviceMap[jid]!) { + final ratchetKey = RatchetMapKey(jid, deviceId); + final ratchet = _ratchetMap[ratchetKey]!; + final ciphertext = (await ratchet.ratchetEncrypt(concatKey)).ciphertext; - // Commit the ratchet - _eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet)); - - if (kex.isNotEmpty && kex.containsKey(deviceId)) { - final k = kex[deviceId]! + // Commit the ratchet + _eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet)); + + if (kex.isNotEmpty && kex.containsKey(deviceId)) { + final k = kex[deviceId]! ..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext); - encryptedKeys.add( - EncryptedKey( - deviceId, - base64.encode(k.writeToBuffer()), - true, - ), - ); - } else { - encryptedKeys.add( - EncryptedKey( - deviceId, - base64.encode(ciphertext), - false, - ), - ); + encryptedKeys.add( + EncryptedKey( + jid, + deviceId, + base64.encode(k.writeToBuffer()), + true, + ), + ); + } else { + encryptedKeys.add( + EncryptedKey( + jid, + deviceId, + base64.encode(ciphertext), + false, + ), + ); + } } } }); diff --git a/test/double_ratchet_test.dart b/test/double_ratchet_test.dart index 4dcbeaa..58f6131 100644 --- a/test/double_ratchet_test.dart +++ b/test/double_ratchet_test.dart @@ -36,11 +36,13 @@ void main() { test('Test the Double Ratchet', () async { // Generate keys + const bobJid = 'bob@other.example.server'; final ikAlice = await OmemoKeyPair.generateNewPair(KeyPairType.ed25519); final ikBob = await OmemoKeyPair.generateNewPair(KeyPairType.ed25519); final spkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final opkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final bundleBob = OmemoBundle( + bobJid, 1, await spkBob.pk.asBase64(), 3, diff --git a/test/omemo_test.dart b/test/omemo_test.dart index fcf8bd7..cbcc687 100644 --- a/test/omemo_test.dart +++ b/test/omemo_test.dart @@ -10,8 +10,8 @@ void main() { var deviceModified = false; var ratchetModified = 0; var deviceMapModified = 0; - final aliceSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); - final bobSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); + final aliceSession = await OmemoSessionManager.generateNewIdentity(aliceJid, opkAmount: 1); + final bobSession = await OmemoSessionManager.generateNewIdentity(bobJid, opkAmount: 1); final bobOpks = (await bobSession.getDevice()).opks.values.toList(); bobSession.eventStream.listen((event) { if (event is DeviceModifiedEvent) { @@ -83,10 +83,10 @@ void main() { const bobJid = 'bob@other.server.example'; // Alice and Bob generate their sessions - final aliceSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); - final bobSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); + final aliceSession = await OmemoSessionManager.generateNewIdentity(aliceJid, opkAmount: 1); + final bobSession = await OmemoSessionManager.generateNewIdentity(bobJid, opkAmount: 1); // Bob's other device - final bobSession2 = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); + final bobSession2 = await OmemoSessionManager.generateNewIdentity(bobJid, opkAmount: 1); // Alice encrypts a message for Bob const messagePlaintext = 'Hello Bob!'; @@ -143,4 +143,47 @@ void main() { ..getRatchet(bobJid, fingerprints[0].deviceId) ..getRatchet(bobJid, fingerprints[1].deviceId); }); + + test('Test using OMEMO sessions with encrypt to self', () async { + const aliceJid = 'alice@server.example'; + const bobJid = 'bob@other.server.example'; + + // Alice and Bob generate their sessions + final aliceSession1 = await OmemoSessionManager.generateNewIdentity(aliceJid, opkAmount: 1); + final aliceSession2 = await OmemoSessionManager.generateNewIdentity(aliceJid, opkAmount: 1); + final bobSession = await OmemoSessionManager.generateNewIdentity(bobJid, opkAmount: 1); + + // Alice encrypts a message for Bob + const messagePlaintext = 'Hello Bob!'; + final aliceMessage = await aliceSession1.encryptToJids( + [bobJid, aliceJid], + messagePlaintext, + newSessions: [ + await (await bobSession.getDevice()).toBundle(), + await (await aliceSession2.getDevice()).toBundle(), + ], + ); + expect(aliceMessage.encryptedKeys.length, 2); + + // Alice sends the message to Bob + // ... + + // Bob decrypts it + final bobMessage = await bobSession.decryptMessage( + aliceMessage.ciphertext, + aliceJid, + (await aliceSession1.getDevice()).id, + aliceMessage.encryptedKeys, + ); + expect(messagePlaintext, bobMessage); + + // Alice's other device decrypts it + final aliceMessage2 = await aliceSession2.decryptMessage( + aliceMessage.ciphertext, + aliceJid, + (await aliceSession1.getDevice()).id, + aliceMessage.encryptedKeys, + ); + expect(messagePlaintext, aliceMessage2); + }); } diff --git a/test/serialisation_test.dart b/test/serialisation_test.dart index 16b9af1..dab1ef0 100644 --- a/test/serialisation_test.dart +++ b/test/serialisation_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { test('Test serialising and deserialising the Device', () async { // Generate a random session - final oldSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); + final oldSession = await OmemoSessionManager.generateNewIdentity('user@test.server', opkAmount: 1); final oldDevice = await oldSession.getDevice(); final serialised = await oldDevice.toJson(); @@ -16,8 +16,8 @@ void main() { // Generate a random ratchet const aliceJid = 'alice@server.example'; const bobJid = 'bob@other.server.example'; - final aliceSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); - final bobSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); + final aliceSession = await OmemoSessionManager.generateNewIdentity(aliceJid, opkAmount: 1); + final bobSession = await OmemoSessionManager.generateNewIdentity(bobJid, opkAmount: 1); final aliceMessage = await aliceSession.encryptToJid( bobJid, 'Hello Bob!', @@ -40,8 +40,8 @@ void main() { test('Test serialising and deserialising the OmemoSessionManager', () async { // Generate a random session - final oldSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 4); - final bobSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 4); + final oldSession = await OmemoSessionManager.generateNewIdentity('a@server', opkAmount: 4); + final bobSession = await OmemoSessionManager.generateNewIdentity('b@other.server', opkAmount: 4); await oldSession.addSessionFromBundle( 'bob@localhost', (await bobSession.getDevice()).id, diff --git a/test/x3dh_test.dart b/test/x3dh_test.dart index dd0c092..7b41cd6 100644 --- a/test/x3dh_test.dart +++ b/test/x3dh_test.dart @@ -11,6 +11,7 @@ void main() { final spkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final opkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final bundleBob = OmemoBundle( + 'alice@some.server', 1, await spkBob.pk.asBase64(), 3, @@ -53,6 +54,7 @@ void main() { final spkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final opkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final bundleBob = OmemoBundle( + 'bob@some.server', 1, await spkBob.pk.asBase64(), 3,