Compare commits

..

3 Commits

Author SHA1 Message Date
1472624b1d fix: Use stanza receival timestamps to guard against stale kex messages
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-10-02 19:23:58 +02:00
0826d043d5 feat: Attempt to detect already decrypted messages 2022-10-02 17:03:39 +02:00
2aa3674c4b fix: Fix receiving an old key exchange breaking decryption
This was mostly caused by Dart not copying values but referencing
them. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.

We know make some assumptions about received key exchanges, so this
needs some field testing.
2022-10-02 14:56:20 +02:00
8 changed files with 180 additions and 108 deletions

View File

@ -119,6 +119,8 @@ void main() async {
aliceDevice.id, aliceDevice.id,
// The deserialised keys // The deserialised keys
keys, keys,
// Since the message was not delayed, we use the current time
DateTime.now().millisecondsSinceEpoch,
); );
// All Bob has to do now is replace the OMEMO wrapper element // All Bob has to do now is replace the OMEMO wrapper element

View File

@ -68,6 +68,7 @@ class OmemoDoubleRatchet {
this.sessionAd, this.sessionAd,
this.mkSkipped, // MKSKIPPED this.mkSkipped, // MKSKIPPED
this.acknowledged, this.acknowledged,
this.kexTimestamp,
); );
factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) { factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) {
@ -85,6 +86,7 @@ class OmemoDoubleRatchet {
'ik_pub': 'base/64/encoded', 'ik_pub': 'base/64/encoded',
'session_ad': 'base/64/encoded', 'session_ad': 'base/64/encoded',
'acknowledged': true | false, 'acknowledged': true | false,
'kex_timestamp': int,
'mkskipped': [ 'mkskipped': [
{ {
'key': 'base/64/encoded', 'key': 'base/64/encoded',
@ -129,6 +131,7 @@ class OmemoDoubleRatchet {
base64.decode(data['session_ad']! as String), base64.decode(data['session_ad']! as String),
mkSkipped, mkSkipped,
data['acknowledged']! as bool, data['acknowledged']! as bool,
data['kex_timestamp']! as int,
); );
} }
@ -160,6 +163,10 @@ class OmemoDoubleRatchet {
final Map<SkippedKey, List<int>> mkSkipped; final Map<SkippedKey, List<int>> mkSkipped;
/// The point in time at which we performed the kex exchange to create this ratchet.
/// Precision is milliseconds since epoch.
int kexTimestamp;
/// Indicates whether we received an empty OMEMO message after building a session with /// Indicates whether we received an empty OMEMO message after building a session with
/// the device. /// the device.
bool acknowledged; bool acknowledged;
@ -167,7 +174,7 @@ class OmemoDoubleRatchet {
/// Create an OMEMO session using the Signed Pre Key [spk], the shared secret [sk] that /// Create an OMEMO session using the Signed Pre Key [spk], the shared secret [sk] that
/// was obtained using a X3DH and the associated data [ad] that was also obtained through /// was obtained using a X3DH and the associated data [ad] that was also obtained through
/// a X3DH. [ik] refers to Bob's (the receiver's) IK public key. /// a X3DH. [ik] refers to Bob's (the receiver's) IK public key.
static Future<OmemoDoubleRatchet> initiateNewSession(OmemoPublicKey spk, OmemoPublicKey ik, List<int> sk, List<int> ad) async { static Future<OmemoDoubleRatchet> initiateNewSession(OmemoPublicKey spk, OmemoPublicKey ik, List<int> sk, List<int> ad, int timestamp) async {
final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final dhr = spk; final dhr = spk;
final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0)); final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0));
@ -186,6 +193,7 @@ class OmemoDoubleRatchet {
ad, ad,
{}, {},
false, false,
timestamp,
); );
} }
@ -193,7 +201,7 @@ class OmemoDoubleRatchet {
/// Pre Key keypair [spk], the shared secret [sk] that was obtained through a X3DH and /// Pre Key keypair [spk], the shared secret [sk] that was obtained through a X3DH and
/// the associated data [ad] that was also obtained through a X3DH. [ik] refers to /// the associated data [ad] that was also obtained through a X3DH. [ik] refers to
/// Alice's (the initiator's) IK public key. /// Alice's (the initiator's) IK public key.
static Future<OmemoDoubleRatchet> acceptNewSession(OmemoKeyPair spk, OmemoPublicKey ik, List<int> sk, List<int> ad) async { static Future<OmemoDoubleRatchet> acceptNewSession(OmemoKeyPair spk, OmemoPublicKey ik, List<int> sk, List<int> ad, int kexTimestamp) async {
return OmemoDoubleRatchet( return OmemoDoubleRatchet(
spk, spk,
null, null,
@ -207,6 +215,7 @@ class OmemoDoubleRatchet {
ad, ad,
{}, {},
false, false,
kexTimestamp,
); );
} }
@ -233,6 +242,7 @@ class OmemoDoubleRatchet {
'session_ad': base64.encode(sessionAd), 'session_ad': base64.encode(sessionAd),
'mkskipped': mkSkippedSerialised, 'mkskipped': mkSkippedSerialised,
'acknowledged': acknowledged, 'acknowledged': acknowledged,
'kex_timestamp': kexTimestamp,
}; };
} }
@ -268,18 +278,18 @@ class OmemoDoubleRatchet {
} }
Future<void> _dhRatchet(OmemoMessage header) async { Future<void> _dhRatchet(OmemoMessage header) async {
pn = header.n!; pn = ns;
ns = 0; ns = 0;
nr = 0; nr = 0;
dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519); dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519);
final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0)); final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
rk = newRk; rk = List.from(newRk);
ckr = newRk; ckr = List.from(newRk);
dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0)); final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
rk = newNewRk; rk = List.from(newNewRk);
cks = newNewRk; cks = List.from(newNewRk);
} }
/// Encrypt [plaintext] using the Double Ratchet. /// Encrypt [plaintext] using the Double Ratchet.
@ -313,8 +323,8 @@ class OmemoDoubleRatchet {
} }
final dhPubMatches = listsEqual( final dhPubMatches = listsEqual(
header.dhPub ?? <int>[], header.dhPub!,
await dhr?.getBytes() ?? <int>[], (await dhr?.getBytes()) ?? <int>[],
); );
if (!dhPubMatches) { if (!dhPubMatches) {
await _skipMessageKeys(header.pn!); await _skipMessageKeys(header.pn!);
@ -330,12 +340,40 @@ class OmemoDoubleRatchet {
return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd); return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
} }
OmemoDoubleRatchet clone() {
return OmemoDoubleRatchet(
dhs,
dhr,
rk,
cks != null ?
List<int>.from(cks!) :
null,
ckr != null ?
List<int>.from(ckr!) :
null,
ns,
nr,
pn,
ik,
sessionAd,
Map<SkippedKey, List<int>>.from(mkSkipped),
acknowledged,
kexTimestamp,
);
}
@visibleForTesting @visibleForTesting
Future<bool> equals(OmemoDoubleRatchet other) async { Future<bool> equals(OmemoDoubleRatchet other) async {
// ignore: invalid_use_of_visible_for_testing_member final dhrMatch = dhr == null ?
final dhrMatch = dhr == null ? other.dhr == null : await dhr!.equals(other.dhr!); other.dhr == null :
final ckrMatch = ckr == null ? other.ckr == null : listsEqual(ckr!, other.ckr!); // ignore: invalid_use_of_visible_for_testing_member
final cksMatch = cks == null ? other.cks == null : listsEqual(cks!, other.cks!); other.dhr != null && await dhr!.equals(other.dhr!);
final ckrMatch = ckr == null ?
other.ckr == null :
other.ckr != null && listsEqual(ckr!, other.ckr!);
final cksMatch = cks == null ?
other.cks == null :
other.cks != null && listsEqual(cks!, other.cks!);
// ignore: invalid_use_of_visible_for_testing_member // ignore: invalid_use_of_visible_for_testing_member
final dhsMatch = await dhs.equals(other.dhs); final dhsMatch = await dhs.equals(other.dhs);
@ -351,6 +389,7 @@ class OmemoDoubleRatchet {
ns == other.ns && ns == other.ns &&
nr == other.nr && nr == other.nr &&
pn == other.pn && pn == other.pn &&
listsEqual(sessionAd, other.sessionAd); listsEqual(sessionAd, other.sessionAd) &&
kexTimestamp == other.kexTimestamp;
} }
} }

View File

@ -30,3 +30,16 @@ class NoDecryptionKeyException implements Exception {
class UnknownSignedPrekeyException implements Exception { class UnknownSignedPrekeyException implements Exception {
String errMsg() => 'Unknown Signed Prekey used.'; String errMsg() => 'Unknown Signed Prekey used.';
} }
/// Triggered by the Session Manager when the received Key Exchange message does not meet
/// the requirement that a key exchange, given that the ratchet already exists, must be
/// sent after its creation.
class InvalidKeyExchangeException implements Exception {
String errMsg() => 'The key exchange was sent before the last kex finished';
}
/// Triggered by the Session Manager when a message's sequence number is smaller than we
/// expect it to be.
class MessageAlreadyDecryptedException implements Exception {
String errMsg() => 'The message has already been decrypted';
}

View File

@ -73,3 +73,7 @@ OmemoKeyPair? decodeKeyPairIfNotNull(String? pk, String? sk, KeyPairType type) {
type, type,
); );
} }
int getTimestamp() {
return DateTime.now().millisecondsSinceEpoch;
}

View File

@ -154,6 +154,7 @@ class OmemoSessionManager {
bundle.ik, bundle.ik,
kexResult.sk, kexResult.sk,
kexResult.ad, kexResult.ad,
getTimestamp(),
); );
await _trustManager.onNewSession(jid, deviceId); await _trustManager.onNewSession(jid, deviceId);
@ -200,6 +201,7 @@ class OmemoSessionManager {
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519), OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
kexResult.sk, kexResult.sk,
kexResult.ad, kexResult.ad,
getTimestamp(),
); );
await _trustManager.onNewSession(jid, deviceId); await _trustManager.onNewSession(jid, deviceId);
@ -240,7 +242,11 @@ class OmemoSessionManager {
final kex = <int, OmemoKeyExchange>{}; final kex = <int, OmemoKeyExchange>{};
if (newSessions != null) { if (newSessions != null) {
for (final newSession in newSessions) { for (final newSession in newSessions) {
kex[newSession.id] = await addSessionFromBundle(newSession.jid, newSession.id, newSession); kex[newSession.id] = await addSessionFromBundle(
newSession.jid,
newSession.id,
newSession,
);
} }
} }
@ -300,7 +306,7 @@ class OmemoSessionManager {
/// [mapKey] with [oldRatchet]. /// [mapKey] with [oldRatchet].
Future<void> _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) async { Future<void> _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) async {
await _lock.synchronized(() { await _lock.synchronized(() {
_log.finest('Restoring ratchet ${mapKey.jid}:${mapKey.deviceId}'); _log.finest('Restoring ratchet ${mapKey.jid}:${mapKey.deviceId} to ${oldRatchet.nr}');
_ratchetMap[mapKey] = oldRatchet; _ratchetMap[mapKey] = oldRatchet;
// Commit the ratchet // Commit the ratchet
@ -318,12 +324,15 @@ class OmemoSessionManager {
/// <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
/// <encrypted /> element. /// <encrypted /> element.
/// [timestamp] refers to the time the message was sent. This might be either what the
/// server tells you via "XEP-0203: Delayed Delivery" or the point in time at which
/// you received the stanza, if no Delayed Delivery element was found.
/// ///
/// If the received message is an empty OMEMO message, i.e. there is no <payload /> /// If the received message is an empty OMEMO message, i.e. there is no <payload />
/// element, then [ciphertext] must be set to null. In this case, this function /// element, then [ciphertext] must be set to null. In this case, this function
/// will return null as there is no message to be decrypted. This, however, is used /// will return null as there is no message to be decrypted. This, however, is used
/// to set up sessions or advance the ratchets. /// to set up sessions or advance the ratchets.
Future<String?> decryptMessage(List<int>? ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys) async { Future<String?> decryptMessage(List<int>? ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys, int timestamp) async {
// Try to find a session we can decrypt with. // Try to find a session we can decrypt with.
var device = await getDevice(); var device = await getDevice();
final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id); final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id);
@ -335,22 +344,32 @@ class OmemoSessionManager {
final decodedRawKey = base64.decode(rawKey.value); final decodedRawKey = base64.decode(rawKey.value);
OmemoAuthenticatedMessage authMessage; OmemoAuthenticatedMessage authMessage;
OmemoDoubleRatchet? oldRatchet; OmemoDoubleRatchet? oldRatchet;
OmemoMessage? message;
if (rawKey.kex) { if (rawKey.kex) {
// If the ratchet already existed, we store it. If it didn't, oldRatchet will stay // If the ratchet already existed, we store it. If it didn't, oldRatchet will stay
// null. // null.
oldRatchet = await _getRatchet(ratchetKey); final oldRatchet = (await _getRatchet(ratchetKey))?.clone();
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
authMessage = kex.message!;
message = OmemoMessage.fromBuffer(authMessage.message!);
// Guard against old key exchanges
if (oldRatchet != null) {
_log.finest('KEX for existent ratchet. ${oldRatchet.pn}');
if (oldRatchet.kexTimestamp > timestamp) {
throw InvalidKeyExchangeException();
}
}
// TODO(PapaTutuWawa): Only do this when we should // TODO(PapaTutuWawa): Only do this when we should
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
await _addSessionFromKeyExchange( await _addSessionFromKeyExchange(
senderJid, senderJid,
senderDeviceId, senderDeviceId,
kex, kex,
); );
authMessage = kex.message!;
// Replace the OPK // Replace the OPK
// TODO(PapaTutuWawa): Replace the OPK when we know that the KEX worked
await _deviceLock.synchronized(() async { await _deviceLock.synchronized(() async {
device = await device.replaceOnetimePrekey(kex.pkId!); device = await device.replaceOnetimePrekey(kex.pkId!);
@ -359,6 +378,7 @@ class OmemoSessionManager {
}); });
} else { } else {
authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey); authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey);
message = OmemoMessage.fromBuffer(authMessage.message!);
} }
final devices = _deviceMap[senderJid]; final devices = _deviceMap[senderJid];
@ -369,11 +389,10 @@ class OmemoSessionManager {
throw NoDecryptionKeyException(); throw NoDecryptionKeyException();
} }
final message = OmemoMessage.fromBuffer(authMessage.message!);
List<int>? keyAndHmac; List<int>? keyAndHmac;
// We can guarantee that the ratchet exists at this point in time // We can guarantee that the ratchet exists at this point in time
final ratchet = (await _getRatchet(ratchetKey))!; final ratchet = (await _getRatchet(ratchetKey))!;
oldRatchet ??= ratchet ; oldRatchet ??= ratchet.clone();
try { try {
if (rawKey.kex) { if (rawKey.kex) {

View File

@ -84,12 +84,14 @@ void main() {
ikBob.pk, ikBob.pk,
resultAlice.sk, resultAlice.sk,
resultAlice.ad, resultAlice.ad,
0,
); );
final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession( final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession(
spkBob, spkBob,
ikAlice.pk, ikAlice.pk,
resultBob.sk, resultBob.sk,
resultBob.ad, resultBob.ad,
0,
); );
expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd); expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd);

View File

@ -90,6 +90,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
// The ratchet should be modified two times: Once for when the ratchet is created and // The ratchet should be modified two times: Once for when the ratchet is created and
@ -121,6 +122,7 @@ void main() {
bobJid, bobJid,
await bobSession.getDeviceId(), await bobSession.getDeviceId(),
bobResponseMessage.encryptedKeys, bobResponseMessage.encryptedKeys,
0,
); );
expect(bobResponseText, aliceReceivedMessage); expect(bobResponseText, aliceReceivedMessage);
}); });
@ -170,6 +172,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
@ -189,6 +192,7 @@ void main() {
bobJid, bobJid,
await bobSession.getDeviceId(), await bobSession.getDeviceId(),
bobResponseMessage.encryptedKeys, bobResponseMessage.encryptedKeys,
0,
); );
expect(bobResponseText, aliceReceivedMessage); expect(bobResponseText, aliceReceivedMessage);
@ -245,6 +249,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession1.getDeviceId(), await aliceSession1.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
@ -254,6 +259,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession1.getDeviceId(), await aliceSession1.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, aliceMessage2); expect(messagePlaintext, aliceMessage2);
}); });
@ -294,6 +300,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(bobMessage, null); expect(bobMessage, null);
@ -371,6 +378,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
}); });
@ -437,6 +445,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
for (var i = 0; i < 100; i++) { for (var i = 0; i < 100; i++) {
@ -456,6 +465,7 @@ void main() {
bobJid, bobJid,
await bobSession.getDeviceId(), await bobSession.getDeviceId(),
bobResponseMessage.encryptedKeys, bobResponseMessage.encryptedKeys,
0,
); );
expect(messageText, aliceReceivedMessage); expect(messageText, aliceReceivedMessage);
} }
@ -610,6 +620,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
msg1.encryptedKeys, msg1.encryptedKeys,
0,
); );
final aliceRatchet1 = aliceSession.getRatchet( final aliceRatchet1 = aliceSession.getRatchet(
bobJid, bobJid,
@ -634,6 +645,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
msg2.encryptedKeys, msg2.encryptedKeys,
getTimestamp(),
); );
final aliceRatchet2 = aliceSession.getRatchet( final aliceRatchet2 = aliceSession.getRatchet(
bobJid, bobJid,
@ -653,7 +665,7 @@ void main() {
expect(await bobRatchet1.equals(bobRatchet2), false); expect(await bobRatchet1.equals(bobRatchet2), false);
}); });
test('Test receiving an old message that contains a KEX', () async { test('Test receiving old messages including a KEX', () async {
const aliceJid = 'alice@server.example'; const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example'; const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions // Alice and Bob generate their sessions
@ -668,6 +680,9 @@ void main() {
opkAmount: 2, opkAmount: 2,
); );
final bobsReceivedMessages = List<EncryptionResult>.empty(growable: true);
final bobsReceivedMessagesTimestamps = List<int>.empty(growable: true);
// Alice sends Bob a message // Alice sends Bob a message
final msg1 = await aliceSession.encryptToJid( final msg1 = await aliceSession.encryptToJid(
bobJid, bobJid,
@ -676,12 +691,16 @@ void main() {
await bobSession.getDeviceBundle(), await bobSession.getDeviceBundle(),
], ],
); );
bobsReceivedMessages.add(msg1);
final t1 = getTimestamp();
bobsReceivedMessagesTimestamps.add(t1);
await bobSession.decryptMessage( await bobSession.decryptMessage(
msg1.ciphertext, msg1.ciphertext,
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
msg1.encryptedKeys, msg1.encryptedKeys,
t1,
); );
// Bob responds // Bob responds
@ -689,97 +708,69 @@ void main() {
aliceJid, aliceJid,
'Hello!', 'Hello!',
); );
await aliceSession.decryptMessage( await aliceSession.decryptMessage(
msg2.ciphertext, msg2.ciphertext,
bobJid, bobJid,
await bobSession.getDeviceId(), await bobSession.getDeviceId(),
msg2.encryptedKeys, msg2.encryptedKeys,
getTimestamp(),
); );
// Due to some issue with the transport protocol, the first message Bob received is // Send some messages between the two
// received again for (var i = 0; i < 100; i++) {
try { final msg = await aliceSession.encryptToJid(
await bobSession.decryptMessage( bobJid,
msg1.ciphertext, 'Hello $i',
);
bobsReceivedMessages.add(msg);
final t = getTimestamp();
bobsReceivedMessagesTimestamps.add(t);
final result = await bobSession.decryptMessage(
msg.ciphertext,
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
msg1.encryptedKeys, msg.encryptedKeys,
t,
); );
expect(true, false);
} on InvalidMessageHMACException { expect(result, 'Hello $i');
// 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
} }
// Due to some issue with the transport protocol, the messages to Bob are received
// again.
final ratchetPreError = bobSession
.getRatchet(aliceJid, await aliceSession.getDeviceId())
.clone();
var invalidKex = 0;
var errorCounter = 0;
for (var i = 0; i < bobsReceivedMessages.length; i++) {
final msg = bobsReceivedMessages[i];
try {
await bobSession.decryptMessage(
msg.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg.encryptedKeys,
bobsReceivedMessagesTimestamps[i],
);
expect(true, false);
} on InvalidMessageHMACException catch (_) {
errorCounter++;
} on InvalidKeyExchangeException catch (_) {
invalidKex++;
}
}
final ratchetPostError = bobSession
.getRatchet(aliceJid, await aliceSession.getDeviceId())
.clone();
// The 100 messages including the initial KEX message
expect(invalidKex, 1);
expect(errorCounter, 100);
expect(await ratchetPreError.equals(ratchetPostError), true);
final msg3 = await aliceSession.encryptToJid( final msg3 = await aliceSession.encryptToJid(
bobJid, bobJid,
'Are you okay?', 'Are you okay?',
@ -789,6 +780,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
msg3.encryptedKeys, msg3.encryptedKeys,
104,
); );
expect(result, 'Are you okay?'); expect(result, 'Are you okay?');

View File

@ -62,6 +62,7 @@ void main() {
aliceJid, aliceJid,
await aliceSession.getDeviceId(), await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
getTimestamp(),
); );
final aliceOld = aliceSession.getRatchet(bobJid, await bobSession.getDeviceId()); final aliceOld = aliceSession.getRatchet(bobJid, await bobSession.getDeviceId());
final aliceSerialised = jsonify(await aliceOld.toJson()); final aliceSerialised = jsonify(await aliceOld.toJson());