fix: Fix ratchets going out of sync

This commit is contained in:
PapaTutuWawa 2023-06-15 01:26:49 +02:00
parent c483585d0b
commit 87a985fee0
4 changed files with 154 additions and 22 deletions

View File

@ -9,8 +9,6 @@ linter:
analyzer: analyzer:
exclude: exclude:
- "lib/protobuf/*.dart" - "lib/src/protobuf/*.dart"
# TODO: Remove once OmemoSessionManager is gone
- "test/omemo_test.dart"
- "example/omemo_dart_example.dart" - "example/omemo_dart_example.dart"
- "test/serialisation_test.dart" - "test/serialisation_test.dart"

View File

@ -200,6 +200,7 @@ class OmemoDoubleRatchet {
); );
rk = List.from(newRk1); rk = List.from(newRk1);
ckr = List.from(newRk1); ckr = List.from(newRk1);
dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final newRk2 = await kdfRk( final newRk2 = await kdfRk(
rk, rk,
@ -226,6 +227,7 @@ class OmemoDoubleRatchet {
final newCkr = await kdfCk(ckr!, kdfCkNextChainKey); final newCkr = await kdfCk(ckr!, kdfCkNextChainKey);
final mk = await kdfCk(ckr!, kdfCkNextMessageKey); final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
ckr = newCkr; ckr = newCkr;
mkSkipped[SkippedKey(dhr!, nr)] = mk; mkSkipped[SkippedKey(dhr!, nr)] = mk;
nr++; nr++;
} }
@ -309,6 +311,7 @@ class OmemoDoubleRatchet {
final ck = await kdfCk(ckr!, kdfCkNextChainKey); final ck = await kdfCk(ckr!, kdfCkNextChainKey);
final mk = await kdfCk(ckr!, kdfCkNextMessageKey); final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
ckr = ck; ckr = ck;
nr++;
return _decrypt(message, header.ciphertext, mk); return _decrypt(message, header.ciphertext, mk);
} }

View File

@ -118,6 +118,7 @@ class OmemoManager {
/// Enter the critical section for performing cryptographic operations on the ratchets /// Enter the critical section for performing cryptographic operations on the ratchets
Future<void> _enterRatchetCriticalSection(String jid) async { Future<void> _enterRatchetCriticalSection(String jid) async {
return;
final completer = await _ratchetCriticalSectionLock.synchronized(() { final completer = await _ratchetCriticalSectionLock.synchronized(() {
if (_ratchetCriticalSectionQueue.containsKey(jid)) { if (_ratchetCriticalSectionQueue.containsKey(jid)) {
final c = Completer<void>(); final c = Completer<void>();
@ -136,6 +137,7 @@ class OmemoManager {
/// Leave the critical section for the ratchets. /// Leave the critical section for the ratchets.
Future<void> _leaveRatchetCriticalSection(String jid) async { Future<void> _leaveRatchetCriticalSection(String jid) async {
return;
await _ratchetCriticalSectionLock.synchronized(() { await _ratchetCriticalSectionLock.synchronized(() {
if (_ratchetCriticalSectionQueue.containsKey(jid)) { if (_ratchetCriticalSectionQueue.containsKey(jid)) {
if (_ratchetCriticalSectionQueue[jid]!.isEmpty) { if (_ratchetCriticalSectionQueue[jid]!.isEmpty) {
@ -229,6 +231,38 @@ class OmemoManager {
return bundles; return bundles;
} }
Future<void> _maybeSendEmptyMessage(RatchetMapKey key, bool created, bool replaced) async {
final ratchet = _ratchetMap[key]!;
if (ratchet.acknowledged) {
// The ratchet is acknowledged
_log.finest('Checking whether to heartbeat to ${key.jid}, ratchet.nr (${ratchet.nr}) >= 53: ${ratchet.nr >= 53}, created: $created, replaced: $replaced');
if (ratchet.nr >= 53 || created || replaced) {
await sendEmptyOmemoMessageImpl(
await _onOutgoingStanzaImpl(
OmemoOutgoingStanza(
[key.jid],
null,
),
),
key.jid,
);
}
} else {
// Ratchet is not acknowledged
_log.finest('Sending acknowledgement heartbeat to ${key.jid}');
await ratchetAcknowledged(key.jid, key.deviceId);
await sendEmptyOmemoMessageImpl(
await _onOutgoingStanzaImpl(
OmemoOutgoingStanza(
[key.jid],
null,
),
),
key.jid,
);
}
}
/// ///
Future<DecryptionResult> onIncomingStanza(OmemoIncomingStanza stanza) async { Future<DecryptionResult> onIncomingStanza(OmemoIncomingStanza stanza) async {
// NOTE: We do this so that we cannot forget to acquire and free the critical // NOTE: We do this so that we cannot forget to acquire and free the critical
@ -253,6 +287,7 @@ class OmemoManager {
final ratchetKey = RatchetMapKey(stanza.bareSenderJid, stanza.senderDeviceId); final ratchetKey = RatchetMapKey(stanza.bareSenderJid, stanza.senderDeviceId);
if (key.kex) { if (key.kex) {
_log.finest('Decoding message as OMEMOKeyExchange');
final kexMessage = OMEMOKeyExchange.fromBuffer(base64Decode(key.value)); final kexMessage = OMEMOKeyExchange.fromBuffer(base64Decode(key.value));
// TODO: Check if we already have such a session and if we can build it // TODO: Check if we already have such a session and if we can build it
@ -328,6 +363,13 @@ class OmemoManager {
stanza.senderDeviceId, stanza.senderDeviceId,
); );
// If we received an empty OMEMO message, mark the ratchet as acknowledged
if (result.get<String?>() == null) {
if (!ratchet.acknowledged) {
ratchet.acknowledged = true;
}
}
// Commit the ratchet // Commit the ratchet
_ratchetMap[ratchetKey] = ratchet; _ratchetMap[ratchetKey] = ratchet;
_deviceList.appendOrCreate(stanza.bareSenderJid, stanza.senderDeviceId); _deviceList.appendOrCreate(stanza.bareSenderJid, stanza.senderDeviceId);
@ -352,6 +394,10 @@ class OmemoManager {
}); });
} }
// Send the hearbeat, if we have to
// TODO: Handle replace
await _maybeSendEmptyMessage(ratchetKey, true, false);
return DecryptionResult( return DecryptionResult(
result.get<String?>(), result.get<String?>(),
null, null,
@ -367,7 +413,8 @@ class OmemoManager {
); );
} }
final ratchet = _ratchetMap[key]!.clone(); _log.finest('Decoding message as OMEMOAuthenticatedMessage');
final ratchet = _ratchetMap[ratchetKey]!.clone();
final authMessage = OMEMOAuthenticatedMessage.fromBuffer(base64Decode(key.value)); final authMessage = OMEMOAuthenticatedMessage.fromBuffer(base64Decode(key.value));
final keyAndHmac = await ratchet.ratchetDecrypt(authMessage); final keyAndHmac = await ratchet.ratchetDecrypt(authMessage);
if (keyAndHmac.isType<OmemoError>()) { if (keyAndHmac.isType<OmemoError>()) {
@ -389,7 +436,15 @@ class OmemoManager {
); );
} }
// If we received an empty OMEMO message, mark the ratchet as acknowledged
if (result.get<String?>() == null) {
if (!ratchet.acknowledged) {
ratchet.acknowledged = true;
}
}
// Message was successfully decrypted, so commit the ratchet // Message was successfully decrypted, so commit the ratchet
_ratchetMap[ratchetKey] = ratchet;
_eventStreamController.add( _eventStreamController.add(
RatchetModifiedEvent( RatchetModifiedEvent(
stanza.bareSenderJid, stanza.bareSenderJid,
@ -400,6 +455,9 @@ class OmemoManager {
), ),
); );
// Send a heartbeat, if required.
await _maybeSendEmptyMessage(ratchetKey, false, false);
return DecryptionResult( return DecryptionResult(
result.get<String?>(), result.get<String?>(),
null, null,
@ -541,21 +599,21 @@ class OmemoManager {
} }
// Encrypt // Encrypt
final ratchet = _ratchetMap[ratchetKey]!.clone(); final ratchet = _ratchetMap[ratchetKey]!;
final authMessage = await ratchet.ratchetEncrypt(payloadKey); final authMessage = await ratchet.ratchetEncrypt(payloadKey);
// Package // Package
if (kex.containsKey(ratchetKey)) { if (kex.containsKey(ratchetKey)) {
final kexMessage = kex[ratchetKey]!..message = authMessage; final kexMessage = kex[ratchetKey]!..message = authMessage;
encryptedKeys.appendOrCreate( encryptedKeys.appendOrCreate(
jid,
EncryptedKey(
jid, jid,
device, EncryptedKey(
base64Encode(kexMessage.writeToBuffer()), jid,
true, device,
), base64Encode(kexMessage.writeToBuffer()),
); true,
),
);
} else if (!ratchet.acknowledged) { } else if (!ratchet.acknowledged) {
// The ratchet as not yet been acked // The ratchet as not yet been acked
if (ratchet.kex == null) { if (ratchet.kex == null) {
@ -612,8 +670,16 @@ class OmemoManager {
); );
} }
// TODO // Sends an empty OMEMO message (heartbeat) to [jid].
Future<void> sendOmemoHeartbeat(String jid) async {} Future<void> sendOmemoHeartbeat(String jid) async {
final result = await onOutgoingStanza(
OmemoOutgoingStanza(
[jid],
null,
),
);
await sendEmptyOmemoMessageImpl(result, jid);
}
// TODO // TODO
Future<void> removeAllRatchets(String jid) async {} Future<void> removeAllRatchets(String jid) async {}
@ -624,8 +690,23 @@ class OmemoManager {
// TODO // TODO
Future<void> onNewConnection() async {} Future<void> onNewConnection() async {}
// TODO // Mark the ratchet [jid]:[device] as acknowledged.
Future<void> ratchetAcknowledged(String jid, int device) async {} Future<void> ratchetAcknowledged(String jid, int device) async {
await _enterRatchetCriticalSection(jid);
final ratchetKey = RatchetMapKey(jid, device);
if (!_ratchetMap.containsKey(ratchetKey)) {
_log.warning('Cannot mark $jid:$device as acknowledged as the ratchet does not exist');
} else {
// Commit
final ratchet = _ratchetMap[ratchetKey]!..acknowledged = true;
_eventStreamController.add(
RatchetModifiedEvent(jid, device, ratchet, false, false),
);
}
await _leaveRatchetCriticalSection(jid);
}
// TODO // TODO
Future<List<DeviceFingerprint>> getFingerprintsForJid(String jid) async => []; Future<List<DeviceFingerprint>> getFingerprintsForJid(String jid) async => [];

View File

@ -1,7 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart';
import 'package:omemo_dart/src/protobuf/schema.pb.dart';
import 'package:omemo_dart/src/trust/always.dart'; import 'package:omemo_dart/src/trust/always.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -108,7 +107,7 @@ void main() {
bobJid, bobJid,
bobDevice.id, bobDevice.id,
DateTime.now().millisecondsSinceEpoch, DateTime.now().millisecondsSinceEpoch,
bobResult2.encryptedKeys[bobJid]!, bobResult2.encryptedKeys[aliceJid]!,
base64.encode(bobResult2.ciphertext!), base64.encode(bobResult2.ciphertext!),
false, false,
), ),
@ -130,6 +129,7 @@ void main() {
await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1); await OmemoDevice.generateNewDevice(aliceJid, opkAmount: 1);
final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1); final bobDevice = await OmemoDevice.generateNewDevice(bobJid, opkAmount: 1);
EncryptionResult? bobEmptyMessage;
final aliceManager = OmemoManager( final aliceManager = OmemoManager(
aliceDevice, aliceDevice,
AlwaysTrustingTrustManager(), AlwaysTrustingTrustManager(),
@ -151,6 +151,7 @@ void main() {
AlwaysTrustingTrustManager(), AlwaysTrustingTrustManager(),
(result, recipientJid) async { (result, recipientJid) async {
bobEmptyMessageSent++; bobEmptyMessageSent++;
bobEmptyMessage = result;
}, },
(jid) async { (jid) async {
expect(jid, aliceJid); expect(jid, aliceJid);
@ -188,10 +189,20 @@ void main() {
expect(bobResult.payload, 'Hello world'); expect(bobResult.payload, 'Hello world');
// Bob acknowledges the message // Bob acknowledges the message
await aliceManager.ratchetAcknowledged(bobJid, bobDevice.id); await aliceManager.onIncomingStanza(
OmemoIncomingStanza(
bobJid,
bobDevice.id,
getTimestamp(),
bobEmptyMessage!.encryptedKeys[aliceJid]!,
null,
false,
),
);
// Alice now sends 52 messages that Bob decrypts // Alice now sends 52 messages that Bob decrypts
for (var i = 0; i <= 51; i++) { for (var i = 0; i < 52; i++) {
Logger.root.finest('${i+1}/52');
final aliceResultLoop = await aliceManager.onOutgoingStanza( final aliceResultLoop = await aliceManager.onOutgoingStanza(
OmemoOutgoingStanza( OmemoOutgoingStanza(
[bobJid], [bobJid],
@ -199,6 +210,8 @@ void main() {
), ),
); );
expect(aliceResultLoop.encryptedKeys[bobJid]!.first.kex, false);
final bobResultLoop = await bobManager.onIncomingStanza( final bobResultLoop = await bobManager.onIncomingStanza(
OmemoIncomingStanza( OmemoIncomingStanza(
aliceJid, aliceJid,
@ -210,6 +223,7 @@ void main() {
), ),
); );
expect(bobResultLoop.error, null);
expect(aliceEmptyMessageSent, 0); expect(aliceEmptyMessageSent, 0);
expect(bobEmptyMessageSent, 1); expect(bobEmptyMessageSent, 1);
expect(bobResultLoop.payload, 'Test message $i'); expect(bobResultLoop.payload, 'Test message $i');
@ -237,6 +251,42 @@ void main() {
expect(aliceEmptyMessageSent, 0); expect(aliceEmptyMessageSent, 0);
expect(bobEmptyMessageSent, 2); expect(bobEmptyMessageSent, 2);
expect(bobResultFinal.payload, 'Test message last'); expect(bobResultFinal.payload, 'Test message last');
// Alice receives it and sends another message
final aliceResultPostFinal = await aliceManager.onIncomingStanza(
OmemoIncomingStanza(
bobJid,
bobDevice.id,
getTimestamp(),
bobEmptyMessage!.encryptedKeys[aliceJid]!,
null,
false,
),
);
expect(aliceResultPostFinal.error, null);
final aliceMessagePostFinal = await aliceManager.onOutgoingStanza(
const OmemoOutgoingStanza(
[bobJid],
"I'm not done yet!",
),
);
// And Bob decrypts it
final bobResultPostFinal = await bobManager.onIncomingStanza(
OmemoIncomingStanza(
aliceJid,
aliceDevice.id,
getTimestamp(),
aliceMessagePostFinal.encryptedKeys[bobJid]!,
base64Encode(aliceMessagePostFinal.ciphertext!),
false,
),
);
expect(bobResultPostFinal.error, null);
expect(bobResultPostFinal.payload, "I'm not done yet!");
expect(aliceEmptyMessageSent, 0);
expect(bobEmptyMessageSent, 2);
}); });
test('Test accessing data without it existing', () async { test('Test accessing data without it existing', () async {
@ -770,7 +820,7 @@ void main() {
// Alice has to reconnect but has no connection yet // Alice has to reconnect but has no connection yet
failure = true; failure = true;
aliceManager.onNewConnection(); await aliceManager.onNewConnection();
// Alice sends another message to Bob // Alice sends another message to Bob
final aliceResult2 = await aliceManager.onOutgoingStanza( final aliceResult2 = await aliceManager.onOutgoingStanza(