import 'dart:convert'; import 'package:logging/logging.dart'; import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/src/protobuf/omemo_key_exchange.dart'; import 'package:omemo_dart/src/trust/always.dart'; import 'package:test/test.dart'; class TestingTrustManager extends AlwaysTrustingTrustManager { final Map devices = {}; @override Future onNewSession(String jid, int deviceId) async { devices[jid] = deviceId; } } void main() { Logger.root ..level = Level.ALL ..onRecord.listen((record) { // ignore: avoid_print print('${record.level.name}: ${record.message}'); }); test('Test sending a message without the device list cache', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; var aliceEmptyMessageSent = 0; var bobEmptyMessageSent = 0; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async { aliceEmptyMessageSent++; }, (jid) async { expect(jid, bobJid); return [ bobDevice.id ]; }, (jid, id) async { expect(jid, bobJid); return bobDevice.toBundle(); }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async { bobEmptyMessageSent++; }, (jid) async { expect(jid, aliceJid); return [aliceDevice.id]; }, (jid, id) async { expect(jid, aliceJid); return aliceDevice.toBundle(); }, (jid) async {}, ); // Alice sends a message final aliceResult = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello world', ), ); // Bob must be able to decrypt the message final bobResult = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult.encryptedKeys, base64.encode(aliceResult.ciphertext!), ), ); expect(bobResult.payload, 'Hello world'); expect(bobResult.error, null); expect(aliceEmptyMessageSent, 0); expect(bobEmptyMessageSent, 1); // Alice receives the ack message await aliceManager.ratchetAcknowledged( bobJid, bobDevice.id, ); // Bob now responds final bobResult2 = await bobManager.onOutgoingStanza( const OmemoOutgoingStanza( [aliceJid], 'Hello world, Alice', ), ); final aliceResult2 = await aliceManager.onIncomingStanza( OmemoIncomingStanza( bobJid, bobDevice.id, DateTime.now().millisecondsSinceEpoch, bobResult2.encryptedKeys, base64.encode(bobResult2.ciphertext!), ), ); expect(aliceResult2.error, null); expect(aliceEmptyMessageSent, 0); expect(bobEmptyMessageSent, 1); expect(aliceResult2.payload, 'Hello world, Alice'); }); test('Test triggering the heartbeat', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; var aliceEmptyMessageSent = 0; var bobEmptyMessageSent = 0; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async { aliceEmptyMessageSent++; }, (jid) async { expect(jid, bobJid); return [ bobDevice.id ]; }, (jid, id) async { expect(jid, bobJid); return bobDevice.toBundle(); }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async { bobEmptyMessageSent++; }, (jid) async { expect(jid, aliceJid); return [aliceDevice.id]; }, (jid, id) async { expect(jid, aliceJid); return aliceDevice.toBundle(); }, (jid) async {}, ); // Alice sends a message final aliceResult = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello world', ), ); // Bob must be able to decrypt the message final bobResult = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult.encryptedKeys, base64.encode(aliceResult.ciphertext!), ), ); expect(aliceEmptyMessageSent, 0); expect(bobEmptyMessageSent, 1); expect(bobResult.payload, 'Hello world'); // Bob acknowledges the message await aliceManager.ratchetAcknowledged(bobJid, bobDevice.id); // Alice now sends 52 messages that Bob decrypts for (var i = 0; i <= 51; i++) { final aliceResultLoop = await aliceManager.onOutgoingStanza( OmemoOutgoingStanza( [bobJid], 'Test message $i', ), ); final bobResultLoop = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResultLoop.encryptedKeys, base64.encode(aliceResultLoop.ciphertext!), ), ); expect(aliceEmptyMessageSent, 0); expect(bobEmptyMessageSent, 1); expect(bobResultLoop.payload, 'Test message $i'); } // Alice sends a final message that triggers a heartbeat final aliceResultFinal = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Test message last', ), ); final bobResultFinal = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResultFinal.encryptedKeys, base64.encode(aliceResultFinal.ciphertext!), ), ); expect(aliceEmptyMessageSent, 0); expect(bobEmptyMessageSent, 2); expect(bobResultFinal.payload, 'Test message last'); }); test('Test accessing data without it existing', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => [], (jid, id) async => null, (jid) async {}, ); // Get non-existant fingerprints expect( await aliceManager.getFingerprintsForJid(bobJid), null, ); // Ack a non-existant ratchet await aliceManager.ratchetAcknowledged( bobJid, 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 OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobOldDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final bobCurrentDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = 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(); }, (jid) async {}, ); final bobManager = OmemoManager( bobCurrentDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => [], (jid, id) async => null, (jid) async {}, ); // 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 OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice1 = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final bobDevice2 = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = 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; }, (jid) async {}, ); final bobManager1 = OmemoManager( bobDevice1, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => [], (jid, id) async => null, (jid) async {}, ); final bobManager2 = OmemoManager( bobDevice2, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { expect(jid, aliceJid); return [aliceDevice.id]; }, (jid, id) async { expect(jid, aliceJid); return aliceDevice.toBundle(); }, (jid) async {}, ); // 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 OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice1 = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final bobDevice2 = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = 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; }, (jid) async {}, ); final bobManager1 = OmemoManager( bobDevice1, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); final bobManager2 = OmemoManager( bobDevice2, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); // 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!'); }); test('Test sending a message to two different JIDs', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; const cocoJid = 'coco@server3'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final cocoDevice = await OmemoDevice.generateNewDevice(cocoJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { if (jid == bobJid) { return [bobDevice.id]; } else if (jid == cocoJid) { return [cocoDevice.id]; } return null; }, (jid, id) async { if (jid == bobJid) { return bobDevice.toBundle(); } else if (jid == cocoJid) { return cocoDevice.toBundle(); } return null; }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); final cocoManager = OmemoManager( cocoDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); // Alice sends a message to Bob and Coco final aliceResult = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid, cocoJid], 'Hello Bob and Coco!', ), ); // Bob and Coco decrypt them final bobResult = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult.encryptedKeys, base64.encode(aliceResult.ciphertext!), ), ); final cocoResult = await cocoManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult.encryptedKeys, base64.encode(aliceResult.ciphertext!), ), ); expect(bobResult.error, null); expect(cocoResult.error, null); expect(bobResult.payload, 'Hello Bob and Coco!'); expect(cocoResult.payload, 'Hello Bob and Coco!'); }); test('Test a fetch failure', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; var failure = false; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { expect(jid, bobJid); return failure ? null : [bobDevice.id]; }, (jid, id) async { expect(jid, bobJid); return failure ? null : bobDevice.toBundle(); }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); // Alice sends a message to Bob and Coco final aliceResult1 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello Bob!', ), ); // Bob decrypts it final bobResult1 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult1.encryptedKeys, base64.encode(aliceResult1.ciphertext!), ), ); expect(bobResult1.error, null); expect(bobResult1.payload, 'Hello Bob!'); // Bob acks the message await aliceManager.ratchetAcknowledged( bobJid, bobDevice.id, ); // Alice has to reconnect but has no connection yet failure = true; aliceManager.onNewConnection(); // Alice sends another message to Bob final aliceResult2 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello Bob! x2', ), ); // And Bob decrypts it final bobResult2 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult2.encryptedKeys, base64.encode(aliceResult2.ciphertext!), ), ); expect(bobResult2.error, null); expect(bobResult2.payload, 'Hello Bob! x2'); }); test('Test sending a message with failed lookups', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { expect(jid, bobJid); return null; }, (jid, id) async { expect(jid, bobJid); return null; }, (jid) async {}, ); // Alice sends a message to Bob final aliceResult = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello Bob!', ), ); expect(aliceResult.isSuccess(1), false); expect(aliceResult.jidEncryptionErrors[bobJid] is NoKeyMaterialAvailableException, true); }); test('Test sending a message two two JIDs with failed lookups', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; const cocoJid = 'coco@server3'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { if (jid == bobJid) { return [bobDevice.id]; } return null; }, (jid, id) async { if (jid == bobJid) { return bobDevice.toBundle(); } return null; }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); // Alice sends a message to Bob and Coco final aliceResult = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid, cocoJid], 'Hello Bob and Coco!', ), ); expect(aliceResult.isSuccess(2), true); expect(aliceResult.jidEncryptionErrors[cocoJid] is NoKeyMaterialAvailableException, true); // Bob decrypts it final bobResult = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult.encryptedKeys, base64.encode(aliceResult.ciphertext!), ), ); expect(bobResult.payload, 'Hello Bob and Coco!'); }); test('Test sending multiple messages back and forth', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { expect(jid, bobJid); return [bobDevice.id]; }, (jid, id) async { expect(jid, bobJid); return bobDevice.toBundle(); }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); // Alice encrypts a message for Bob final aliceMessage = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello Bob!', ), ); // And Bob decrypts it await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceMessage.encryptedKeys, base64.encode(aliceMessage.ciphertext!), ), ); // Ratchets are acked await aliceManager.ratchetAcknowledged( bobJid, bobDevice.id, ); for (var i = 0; i < 100; i++) { final messageText = 'Test Message #$i'; // Bob responds to Alice final bobResponseMessage = await bobManager.onOutgoingStanza( OmemoOutgoingStanza( [aliceJid], messageText, ), ); expect(bobResponseMessage.isSuccess(1), true); final aliceReceivedMessage = await aliceManager.onIncomingStanza( OmemoIncomingStanza( bobJid, bobDevice.id, DateTime.now().millisecondsSinceEpoch, bobResponseMessage.encryptedKeys, base64.encode(bobResponseMessage.ciphertext!), ), ); expect(aliceReceivedMessage.payload, messageText); } }); test('Test removing all ratchets and sending a message', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); EncryptionResult? aliceEmptyMessage; final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async { aliceEmptyMessage = result; }, (jid) async { expect(jid, bobJid); return [bobDevice.id]; }, (jid, id) async { expect(jid, bobJid); return bobDevice.toBundle(); }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); // Alice encrypts a message for Bob final aliceResult1 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello Bob!', ), ); // And Bob decrypts it await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult1.encryptedKeys, base64.encode(aliceResult1.ciphertext!), ), ); // Ratchets are acked await aliceManager.ratchetAcknowledged( bobJid, bobDevice.id, ); // Alice now removes all ratchets for Bob and sends another new message await aliceManager.removeAllRatchets(bobJid); expect(aliceManager.getRatchet(RatchetMapKey(bobJid, bobDevice.id)), null); // Alice prepares an empty OMEMO message await aliceManager.sendOmemoHeartbeat(bobJid); // And Bob receives it final bobResult2 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, await aliceManager.getDeviceId(), DateTime.now().millisecondsSinceEpoch, aliceEmptyMessage!.encryptedKeys, null, ), ); expect(bobResult2.error, null); // Bob acks the new ratchet await aliceManager.ratchetAcknowledged( bobJid, await bobManager.getDeviceId(), ); // Alice sends another message final aliceResult3 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'I did not trust your last device, Bob!', ), ); // Bob decrypts it final bobResult3 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult3.encryptedKeys, base64.encode(aliceResult3.ciphertext!), ), ); expect(bobResult3.error, null); expect(bobResult3.payload, 'I did not trust your last device, Bob!'); // Bob responds final bobResult4 = await bobManager.onOutgoingStanza( const OmemoOutgoingStanza( [aliceJid], "That's okay.", ), ); // Alice decrypts final aliceResult4 = await aliceManager.onIncomingStanza( OmemoIncomingStanza( bobJid, bobDevice.id, DateTime.now().millisecondsSinceEpoch, bobResult4.encryptedKeys, base64.encode(bobResult4.ciphertext!), ), ); expect(aliceResult4.error, null); expect(aliceResult4.payload, "That's okay."); }); test('Test removing all ratchets and sending a message without post-heartbeat ack', () async { // This test is the same as "Test removing all ratchets and sending a message" except // that bob does not ack the ratchet after Alice's heartbeat after she recreated // all ratchets. const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); EncryptionResult? aliceEmptyMessage; final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async { aliceEmptyMessage = result; }, (jid) async { expect(jid, bobJid); return [bobDevice.id]; }, (jid, id) async { expect(jid, bobJid); return bobDevice.toBundle(); }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async => null, (jid, id) async => null, (jid) async {}, ); // Alice encrypts a message for Bob final aliceResult1 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello Bob!', ), ); // And Bob decrypts it await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult1.encryptedKeys, base64.encode(aliceResult1.ciphertext!), ), ); // Ratchets are acked await aliceManager.ratchetAcknowledged( bobJid, bobDevice.id, ); // Alice now removes all ratchets for Bob and sends another new message await aliceManager.removeAllRatchets(bobJid); expect(aliceManager.getRatchet(RatchetMapKey(bobJid, bobDevice.id)), null); // Alice prepares an empty OMEMO message await aliceManager.sendOmemoHeartbeat(bobJid); // And Bob receives it final bobResult2 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, await aliceManager.getDeviceId(), DateTime.now().millisecondsSinceEpoch, aliceEmptyMessage!.encryptedKeys, null, ), ); expect(bobResult2.error, null); // Alice sends another message final aliceResult3 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'I did not trust your last device, Bob!', ), ); // Bob decrypts it final bobResult3 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult3.encryptedKeys, base64.encode(aliceResult3.ciphertext!), ), ); expect(bobResult3.error, null); expect(bobResult3.payload, 'I did not trust your last device, Bob!'); // Bob responds final bobResult4 = await bobManager.onOutgoingStanza( const OmemoOutgoingStanza( [aliceJid], "That's okay.", ), ); // Alice decrypts final aliceResult4 = await aliceManager.onIncomingStanza( OmemoIncomingStanza( bobJid, bobDevice.id, DateTime.now().millisecondsSinceEpoch, bobResult4.encryptedKeys, base64.encode(bobResult4.ciphertext!), ), ); expect(aliceResult4.error, null); expect(aliceResult4.payload, "That's okay."); }); test('Test resending key exchanges', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { expect(jid, bobJid); return [ bobDevice.id ]; }, (jid, id) async { expect(jid, bobJid); return bobDevice.toBundle(); }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { expect(jid, aliceJid); return [aliceDevice.id]; }, (jid, id) async { expect(jid, aliceJid); return aliceDevice.toBundle(); }, (jid) async {}, ); // Alice sends Bob a message final aliceResult1 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello World!', ), ); // The first message must be a KEX message expect(aliceResult1.encryptedKeys.first.kex, true); // Bob decrypts Alice's message final bobResult1 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult1.encryptedKeys, base64.encode(aliceResult1.ciphertext!), ), ); expect(bobResult1.error, null); expect(bobResult1.payload, 'Hello World!'); // Alice immediately sends another message final aliceResult2 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello Bob', ), ); // The response should contain a KEX expect(aliceResult2.encryptedKeys.first.kex, true); // The basic data should be the same final parsedFirstKex = OmemoKeyExchange.fromBuffer( base64.decode(aliceResult1.encryptedKeys.first.value), ); final parsedSecondKex = OmemoKeyExchange.fromBuffer( base64.decode(aliceResult2.encryptedKeys.first.value), ); expect(parsedSecondKex.pkId, parsedFirstKex.pkId); expect(parsedSecondKex.spkId, parsedFirstKex.spkId); expect(parsedSecondKex.ik, parsedFirstKex.ik); expect(parsedSecondKex.ek, parsedFirstKex.ek); // Alice decrypts it final bobResult2 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult2.encryptedKeys, base64.encode(aliceResult2.ciphertext!), ), ); expect(bobResult2.error, null); expect(bobResult2.payload, 'Hello Bob'); // Bob also sends a message final bobResult3 = await bobManager.onOutgoingStanza( const OmemoOutgoingStanza( [aliceJid], 'Hello Alice!', ), ); // Alice decrypts it final aliceResult3 = await aliceManager.onIncomingStanza( OmemoIncomingStanza( bobJid, bobDevice.id, DateTime.now().millisecondsSinceEpoch, bobResult3.encryptedKeys, base64.encode(bobResult3.ciphertext!), ), ); expect(aliceResult3.error, null); expect(aliceResult3.payload, 'Hello Alice!'); // Bob now acks the ratchet await aliceManager.ratchetAcknowledged( bobJid, bobDevice.id, ); // Alice replies final aliceResult4 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hi Bob', ), ); // The response should contain no KEX expect(aliceResult4.encryptedKeys.first.kex, false); // Bob decrypts it final bobResult4 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult4.encryptedKeys, base64.encode(aliceResult4.ciphertext!), ), ); expect(bobResult4.error, null); expect(bobResult4.payload, 'Hi Bob'); }); test('Test correct trust behaviour on receiving', () async { const aliceJid = 'alice@server1'; const bobJid = 'bob@server2'; final aliceDevice = await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final aliceManager = OmemoManager( aliceDevice, AlwaysTrustingTrustManager(), (result, recipientJid) async {}, (jid) async { expect(jid, bobJid); return [bobDevice.id]; }, (jid, id) async { expect(jid, bobJid); return bobDevice.toBundle(); }, (jid) async {}, ); final bobManager = OmemoManager( bobDevice, TestingTrustManager(), (result, recipientJid) async {}, (jid) async { expect(jid, aliceJid); return [aliceDevice.id]; }, (jid, id) async { expect(jid, aliceJid); return aliceDevice.toBundle(); }, (jid) async {}, ); // Alice sends Bob a message final aliceResult1 = await aliceManager.onOutgoingStanza( const OmemoOutgoingStanza( [bobJid], 'Hello World!', ), ); // Bob decrypts Alice's message final bobResult1 = await bobManager.onIncomingStanza( OmemoIncomingStanza( aliceJid, aliceDevice.id, DateTime.now().millisecondsSinceEpoch, aliceResult1.encryptedKeys, base64.encode(aliceResult1.ciphertext!), ), ); expect(bobResult1.error, null); // Bob should have some trust state expect( (bobManager.trustManager as TestingTrustManager).devices[aliceJid], await aliceManager.getDeviceId(), ); }); }