fix: Use stanza receival timestamps to guard against stale kex messages
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
0826d043d5
commit
1472624b1d
@ -119,6 +119,8 @@ void main() async {
|
||||
aliceDevice.id,
|
||||
// The deserialised 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
|
||||
|
@ -68,6 +68,7 @@ class OmemoDoubleRatchet {
|
||||
this.sessionAd,
|
||||
this.mkSkipped, // MKSKIPPED
|
||||
this.acknowledged,
|
||||
this.kexTimestamp,
|
||||
);
|
||||
|
||||
factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) {
|
||||
@ -85,6 +86,7 @@ class OmemoDoubleRatchet {
|
||||
'ik_pub': 'base/64/encoded',
|
||||
'session_ad': 'base/64/encoded',
|
||||
'acknowledged': true | false,
|
||||
'kex_timestamp': int,
|
||||
'mkskipped': [
|
||||
{
|
||||
'key': 'base/64/encoded',
|
||||
@ -129,6 +131,7 @@ class OmemoDoubleRatchet {
|
||||
base64.decode(data['session_ad']! as String),
|
||||
mkSkipped,
|
||||
data['acknowledged']! as bool,
|
||||
data['kex_timestamp']! as int,
|
||||
);
|
||||
}
|
||||
|
||||
@ -160,14 +163,18 @@ class OmemoDoubleRatchet {
|
||||
|
||||
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
|
||||
/// the device.
|
||||
/// the device.
|
||||
bool acknowledged;
|
||||
|
||||
/// 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
|
||||
/// 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, int pn) 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 dhr = spk;
|
||||
final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0));
|
||||
@ -181,11 +188,12 @@ class OmemoDoubleRatchet {
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
pn,
|
||||
0,
|
||||
ik,
|
||||
ad,
|
||||
{},
|
||||
false,
|
||||
timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
@ -193,7 +201,7 @@ class OmemoDoubleRatchet {
|
||||
/// 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
|
||||
/// 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(
|
||||
spk,
|
||||
null,
|
||||
@ -207,6 +215,7 @@ class OmemoDoubleRatchet {
|
||||
ad,
|
||||
{},
|
||||
false,
|
||||
kexTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
@ -233,6 +242,7 @@ class OmemoDoubleRatchet {
|
||||
'session_ad': base64.encode(sessionAd),
|
||||
'mkskipped': mkSkippedSerialised,
|
||||
'acknowledged': acknowledged,
|
||||
'kex_timestamp': kexTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
@ -268,18 +278,18 @@ class OmemoDoubleRatchet {
|
||||
}
|
||||
|
||||
Future<void> _dhRatchet(OmemoMessage header) async {
|
||||
pn = header.n!;
|
||||
pn = ns;
|
||||
ns = 0;
|
||||
nr = 0;
|
||||
dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519);
|
||||
|
||||
final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
|
||||
rk = newRk;
|
||||
ckr = newRk;
|
||||
rk = List.from(newRk);
|
||||
ckr = List.from(newRk);
|
||||
dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
|
||||
final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
|
||||
rk = newNewRk;
|
||||
cks = newNewRk;
|
||||
rk = List.from(newNewRk);
|
||||
cks = List.from(newNewRk);
|
||||
}
|
||||
|
||||
/// Encrypt [plaintext] using the Double Ratchet.
|
||||
@ -313,8 +323,8 @@ class OmemoDoubleRatchet {
|
||||
}
|
||||
|
||||
final dhPubMatches = listsEqual(
|
||||
header.dhPub ?? <int>[],
|
||||
await dhr?.getBytes() ?? <int>[],
|
||||
header.dhPub!,
|
||||
(await dhr?.getBytes()) ?? <int>[],
|
||||
);
|
||||
if (!dhPubMatches) {
|
||||
await _skipMessageKeys(header.pn!);
|
||||
@ -348,6 +358,7 @@ class OmemoDoubleRatchet {
|
||||
sessionAd,
|
||||
Map<SkippedKey, List<int>>.from(mkSkipped),
|
||||
acknowledged,
|
||||
kexTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
@ -378,6 +389,7 @@ class OmemoDoubleRatchet {
|
||||
ns == other.ns &&
|
||||
nr == other.nr &&
|
||||
pn == other.pn &&
|
||||
listsEqual(sessionAd, other.sessionAd);
|
||||
listsEqual(sessionAd, other.sessionAd) &&
|
||||
kexTimestamp == other.kexTimestamp;
|
||||
}
|
||||
}
|
||||
|
@ -31,14 +31,11 @@ class UnknownSignedPrekeyException implements Exception {
|
||||
String errMsg() => 'Unknown Signed Prekey used.';
|
||||
}
|
||||
|
||||
/// Triggered by the Session Manager when the received Key Exchange message does not
|
||||
/// meet our expectations. This happens when the PN attribute of the message is not equal
|
||||
/// to our receive number.
|
||||
/// 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 {
|
||||
const InvalidKeyExchangeException(this.expectedPn, this.actualPn);
|
||||
final int expectedPn;
|
||||
final int actualPn;
|
||||
String errMsg() => 'The pn attribute of the key exchange is invalid. Expected $expectedPn, got $actualPn';
|
||||
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
|
||||
|
@ -73,3 +73,7 @@ OmemoKeyPair? decodeKeyPairIfNotNull(String? pk, String? sk, KeyPairType type) {
|
||||
type,
|
||||
);
|
||||
}
|
||||
|
||||
int getTimestamp() {
|
||||
return DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ class OmemoSessionManager {
|
||||
/// Create a ratchet session initiated by Alice to the user with Jid [jid] and the device
|
||||
/// [deviceId] from the bundle [bundle].
|
||||
@visibleForTesting
|
||||
Future<OmemoKeyExchange> addSessionFromBundle(String jid, int deviceId, OmemoBundle bundle, int pn) async {
|
||||
Future<OmemoKeyExchange> addSessionFromBundle(String jid, int deviceId, OmemoBundle bundle) async {
|
||||
final device = await getDevice();
|
||||
final kexResult = await x3dhFromBundle(
|
||||
bundle,
|
||||
@ -154,7 +154,7 @@ class OmemoSessionManager {
|
||||
bundle.ik,
|
||||
kexResult.sk,
|
||||
kexResult.ad,
|
||||
pn,
|
||||
getTimestamp(),
|
||||
);
|
||||
|
||||
await _trustManager.onNewSession(jid, deviceId);
|
||||
@ -201,6 +201,7 @@ class OmemoSessionManager {
|
||||
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
|
||||
kexResult.sk,
|
||||
kexResult.ad,
|
||||
getTimestamp(),
|
||||
);
|
||||
|
||||
await _trustManager.onNewSession(jid, deviceId);
|
||||
@ -241,21 +242,10 @@ class OmemoSessionManager {
|
||||
final kex = <int, OmemoKeyExchange>{};
|
||||
if (newSessions != null) {
|
||||
for (final newSession in newSessions) {
|
||||
final session = await _getRatchet(
|
||||
RatchetMapKey(
|
||||
newSession.jid,
|
||||
newSession.id,
|
||||
),
|
||||
);
|
||||
|
||||
final pn = session != null ?
|
||||
session.ns :
|
||||
0;
|
||||
kex[newSession.id] = await addSessionFromBundle(
|
||||
newSession.jid,
|
||||
newSession.id,
|
||||
newSession,
|
||||
pn,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -334,12 +324,15 @@ class OmemoSessionManager {
|
||||
/// <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
|
||||
/// <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 />
|
||||
/// 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
|
||||
/// 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.
|
||||
var device = await getDevice();
|
||||
final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id);
|
||||
@ -363,8 +356,8 @@ class OmemoSessionManager {
|
||||
// Guard against old key exchanges
|
||||
if (oldRatchet != null) {
|
||||
_log.finest('KEX for existent ratchet. ${oldRatchet.pn}');
|
||||
if (message.pn != oldRatchet.nr) {
|
||||
throw InvalidKeyExchangeException(oldRatchet.nr, message.pn!);
|
||||
if (oldRatchet.kexTimestamp > timestamp) {
|
||||
throw InvalidKeyExchangeException();
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,12 +394,6 @@ class OmemoSessionManager {
|
||||
final ratchet = (await _getRatchet(ratchetKey))!;
|
||||
oldRatchet ??= ratchet.clone();
|
||||
|
||||
if (!rawKey.kex) {
|
||||
if (message.n! < ratchet.nr - 1) {
|
||||
throw MessageAlreadyDecryptedException();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (rawKey.kex) {
|
||||
keyAndHmac = await ratchet.ratchetDecrypt(message, authMessage.writeToBuffer());
|
||||
|
@ -91,6 +91,7 @@ void main() {
|
||||
ikAlice.pk,
|
||||
resultBob.sk,
|
||||
resultBob.ad,
|
||||
0,
|
||||
);
|
||||
|
||||
expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd);
|
||||
|
@ -90,6 +90,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
aliceMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(messagePlaintext, bobMessage);
|
||||
// The ratchet should be modified two times: Once for when the ratchet is created and
|
||||
@ -121,6 +122,7 @@ void main() {
|
||||
bobJid,
|
||||
await bobSession.getDeviceId(),
|
||||
bobResponseMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(bobResponseText, aliceReceivedMessage);
|
||||
});
|
||||
@ -170,6 +172,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
aliceMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(messagePlaintext, bobMessage);
|
||||
|
||||
@ -189,6 +192,7 @@ void main() {
|
||||
bobJid,
|
||||
await bobSession.getDeviceId(),
|
||||
bobResponseMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(bobResponseText, aliceReceivedMessage);
|
||||
|
||||
@ -245,6 +249,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession1.getDeviceId(),
|
||||
aliceMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(messagePlaintext, bobMessage);
|
||||
|
||||
@ -254,6 +259,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession1.getDeviceId(),
|
||||
aliceMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(messagePlaintext, aliceMessage2);
|
||||
});
|
||||
@ -294,6 +300,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
aliceMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(bobMessage, null);
|
||||
|
||||
@ -371,6 +378,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
aliceMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(messagePlaintext, bobMessage);
|
||||
});
|
||||
@ -437,6 +445,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
aliceMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
|
||||
for (var i = 0; i < 100; i++) {
|
||||
@ -456,6 +465,7 @@ void main() {
|
||||
bobJid,
|
||||
await bobSession.getDeviceId(),
|
||||
bobResponseMessage.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
expect(messageText, aliceReceivedMessage);
|
||||
}
|
||||
@ -610,6 +620,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
msg1.encryptedKeys,
|
||||
0,
|
||||
);
|
||||
final aliceRatchet1 = aliceSession.getRatchet(
|
||||
bobJid,
|
||||
@ -634,6 +645,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
msg2.encryptedKeys,
|
||||
getTimestamp(),
|
||||
);
|
||||
final aliceRatchet2 = aliceSession.getRatchet(
|
||||
bobJid,
|
||||
@ -669,6 +681,7 @@ void main() {
|
||||
);
|
||||
|
||||
final bobsReceivedMessages = List<EncryptionResult>.empty(growable: true);
|
||||
final bobsReceivedMessagesTimestamps = List<int>.empty(growable: true);
|
||||
|
||||
// Alice sends Bob a message
|
||||
final msg1 = await aliceSession.encryptToJid(
|
||||
@ -679,11 +692,15 @@ void main() {
|
||||
],
|
||||
);
|
||||
bobsReceivedMessages.add(msg1);
|
||||
final t1 = getTimestamp();
|
||||
bobsReceivedMessagesTimestamps.add(t1);
|
||||
|
||||
await bobSession.decryptMessage(
|
||||
msg1.ciphertext,
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
msg1.encryptedKeys,
|
||||
t1,
|
||||
);
|
||||
|
||||
// Bob responds
|
||||
@ -691,13 +708,15 @@ void main() {
|
||||
aliceJid,
|
||||
'Hello!',
|
||||
);
|
||||
|
||||
await aliceSession.decryptMessage(
|
||||
msg2.ciphertext,
|
||||
bobJid,
|
||||
await bobSession.getDeviceId(),
|
||||
msg2.encryptedKeys,
|
||||
getTimestamp(),
|
||||
);
|
||||
|
||||
|
||||
// Send some messages between the two
|
||||
for (var i = 0; i < 100; i++) {
|
||||
final msg = await aliceSession.encryptToJid(
|
||||
@ -705,11 +724,14 @@ void main() {
|
||||
'Hello $i',
|
||||
);
|
||||
bobsReceivedMessages.add(msg);
|
||||
final t = getTimestamp();
|
||||
bobsReceivedMessagesTimestamps.add(t);
|
||||
final result = await bobSession.decryptMessage(
|
||||
msg.ciphertext,
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
msg.encryptedKeys,
|
||||
t,
|
||||
);
|
||||
|
||||
expect(result, 'Hello $i');
|
||||
@ -720,20 +742,23 @@ void main() {
|
||||
final ratchetPreError = bobSession
|
||||
.getRatchet(aliceJid, await aliceSession.getDeviceId())
|
||||
.clone();
|
||||
var invalidKex = 0;
|
||||
var errorCounter = 0;
|
||||
for (final msg in bobsReceivedMessages) {
|
||||
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 MessageAlreadyDecryptedException catch (_) {
|
||||
} on InvalidMessageHMACException catch (_) {
|
||||
errorCounter++;
|
||||
} on InvalidKeyExchangeException catch (_) {
|
||||
errorCounter++;
|
||||
invalidKex++;
|
||||
}
|
||||
}
|
||||
final ratchetPostError = bobSession
|
||||
@ -741,7 +766,8 @@ void main() {
|
||||
.clone();
|
||||
|
||||
// The 100 messages including the initial KEX message
|
||||
expect(errorCounter, 101);
|
||||
expect(invalidKex, 1);
|
||||
expect(errorCounter, 100);
|
||||
expect(await ratchetPreError.equals(ratchetPostError), true);
|
||||
|
||||
|
||||
@ -754,6 +780,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
msg3.encryptedKeys,
|
||||
104,
|
||||
);
|
||||
|
||||
expect(result, 'Are you okay?');
|
||||
|
@ -62,6 +62,7 @@ void main() {
|
||||
aliceJid,
|
||||
await aliceSession.getDeviceId(),
|
||||
aliceMessage.encryptedKeys,
|
||||
getTimestamp(),
|
||||
);
|
||||
final aliceOld = aliceSession.getRatchet(bobJid, await bobSession.getDeviceId());
|
||||
final aliceSerialised = jsonify(await aliceOld.toJson());
|
||||
@ -86,7 +87,6 @@ void main() {
|
||||
'bob@localhost',
|
||||
await bobSession.getDeviceId(),
|
||||
await bobSession.getDeviceBundle(),
|
||||
0,
|
||||
);
|
||||
|
||||
// Serialise and deserialise
|
||||
|
Loading…
Reference in New Issue
Block a user