fix: Restore the ratchets in case of an error

This means that if the ratchet fails to decrypt a message, from the
outside it will be as if that one message had never been received.
Thus, the ratchet can be used normally. This is to guard against
messages that are received again.
This commit is contained in:
PapaTutuWawa 2022-09-14 21:58:41 +02:00
parent 8991599a0b
commit c5c579810e
2 changed files with 177 additions and 8 deletions

View File

@ -297,6 +297,16 @@ class OmemoSessionManager {
); );
} }
/// In case a decryption error occurs, the Double Ratchet spec says to just restore
/// the ratchet to its old state. As such, this function restores the ratchet at
/// [mapKey] with [oldRatchet].
Future<void> _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) async {
await _lock.synchronized(() {
print('RESTORING RATCHETS');
_ratchetMap[mapKey] = oldRatchet;
});
}
/// Attempt to decrypt [ciphertext]. [keys] refers to the <key /> elements inside the /// Attempt to decrypt [ciphertext]. [keys] refers to the <key /> elements inside the
/// <keys /> element with a "jid" attribute matching our own. [senderJid] refers to the /// <keys /> element with a "jid" attribute matching our own. [senderJid] refers to the
/// bare Jid of the sender. [senderDeviceId] refers to the "sid" attribute of the /// bare Jid of the sender. [senderDeviceId] refers to the "sid" attribute of the
@ -314,9 +324,15 @@ class OmemoSessionManager {
throw NotEncryptedForDeviceException(); throw NotEncryptedForDeviceException();
} }
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
final decodedRawKey = base64.decode(rawKey.value); final decodedRawKey = base64.decode(rawKey.value);
OmemoAuthenticatedMessage authMessage; OmemoAuthenticatedMessage authMessage;
OmemoDoubleRatchet? oldRatchet;
if (rawKey.kex) { if (rawKey.kex) {
// If the ratchet already existed, we store it. If it didn't, oldRatchet will stay
// null.
oldRatchet = await _getRatchet(ratchetKey);
// TODO(PapaTutuWawa): Only do this when we should // TODO(PapaTutuWawa): Only do this when we should
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey); final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
await _addSessionFromKeyExchange( await _addSessionFromKeyExchange(
@ -347,31 +363,38 @@ class OmemoSessionManager {
} }
final message = OmemoMessage.fromBuffer(authMessage.message!); final message = OmemoMessage.fromBuffer(authMessage.message!);
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
List<int>? keyAndHmac; List<int>? keyAndHmac;
await _lock.synchronized(() async { // We can guarantee that the ratchet exists at this point in time
final ratchet = _ratchetMap[ratchetKey]!; final ratchet = (await _getRatchet(ratchetKey))!;
oldRatchet ??= ratchet ;
try {
if (rawKey.kex) { if (rawKey.kex) {
keyAndHmac = await ratchet.ratchetDecrypt(message, authMessage.writeToBuffer()); keyAndHmac = await ratchet.ratchetDecrypt(message, authMessage.writeToBuffer());
} else { } else {
keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey); keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey);
} }
} on InvalidMessageHMACException {
await _restoreRatchet(ratchetKey, oldRatchet);
rethrow;
}
// Commit the ratchet // Commit the ratchet
_eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet)); _eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet));
});
// Empty OMEMO messages should just have the key decrypted and/or session set up. // Empty OMEMO messages should just have the key decrypted and/or session set up.
if (ciphertext == null) { if (ciphertext == null) {
return null; return null;
} }
final key = keyAndHmac!.sublist(0, 32); final key = keyAndHmac.sublist(0, 32);
final hmac = keyAndHmac!.sublist(32, 48); final hmac = keyAndHmac.sublist(32, 48);
final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString); final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
final computedHmac = await truncatedHmac(ciphertext, derivedKeys.authenticationKey); final computedHmac = await truncatedHmac(ciphertext, derivedKeys.authenticationKey);
if (!listsEqual(hmac, computedHmac)) { if (!listsEqual(hmac, computedHmac)) {
// TODO(PapaTutuWawa): I am unsure if we should restore the ratchet here
await _restoreRatchet(ratchetKey, oldRatchet);
throw InvalidMessageHMACException(); throw InvalidMessageHMACException();
} }
@ -515,6 +538,12 @@ class OmemoSessionManager {
}); });
} }
Future<OmemoDoubleRatchet?> _getRatchet(RatchetMapKey key) async {
return _lock.synchronized(() async {
return _ratchetMap[key];
});
}
@visibleForTesting @visibleForTesting
OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!; OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!;

View File

@ -614,4 +614,144 @@ void main() {
expect(await aliceRatchet1.equals(aliceRatchet2), false); expect(await aliceRatchet1.equals(aliceRatchet2), false);
expect(await bobRatchet1.equals(bobRatchet2), false); expect(await bobRatchet1.equals(bobRatchet2), false);
}); });
test('Test receiving an old message that contains a KEX', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 2,
);
// Alice sends Bob a message
final msg1 = await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
// Bob responds
final msg2 = await bobSession.encryptToJid(
aliceJid,
'Hello!',
);
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
// Due to some issue with the transport protocol, the first message Bob received is
// received again
try {
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
expect(true, false);
} on InvalidMessageHMACException {
// NOOP
}
final msg3 = await aliceSession.encryptToJid(
bobJid,
'Are you okay?',
);
final result = await bobSession.decryptMessage(
msg3.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg3.encryptedKeys,
);
expect(result, 'Are you okay?');
});
test('Test receiving an old message that does not contain a KEX', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 2,
);
// Alice sends Bob a message
final msg1 = await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
// Bob responds
final msg2 = await bobSession.encryptToJid(
aliceJid,
'Hello!',
);
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
// Due to some issue with the transport protocol, the first message Alice received is
// received again.
try {
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
expect(true, false);
} catch (_) {
// NOOP
}
final msg3 = await aliceSession.encryptToJid(
bobJid,
'Are you okay?',
);
final result = await bobSession.decryptMessage(
msg3.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg3.encryptedKeys,
);
expect(result, 'Are you okay?');
});
} }