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:
parent
8991599a0b
commit
c5c579810e
@ -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)]!;
|
||||||
|
|
||||||
|
@ -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?');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user