feat: Handle empty OMEMO messages
This commit is contained in:
parent
8c1a78e360
commit
9f986c3369
@ -28,7 +28,7 @@ class EncryptionResult {
|
|||||||
const EncryptionResult(this.ciphertext, this.encryptedKeys);
|
const EncryptionResult(this.ciphertext, this.encryptedKeys);
|
||||||
|
|
||||||
/// The actual message that was encrypted
|
/// The actual message that was encrypted
|
||||||
final List<int> ciphertext;
|
final List<int>? ciphertext;
|
||||||
|
|
||||||
/// Mapping of the device Id to the key for decrypting ciphertext, encrypted
|
/// Mapping of the device Id to the key for decrypting ciphertext, encrypted
|
||||||
/// for the ratchet with said device Id
|
/// for the ratchet with said device Id
|
||||||
@ -186,25 +186,35 @@ class OmemoSessionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Like [encryptToJids] but only for one Jid [jid].
|
/// Like [encryptToJids] but only for one Jid [jid].
|
||||||
Future<EncryptionResult> encryptToJid(String jid, String plaintext, { List<OmemoBundle>? newSessions }) {
|
Future<EncryptionResult> encryptToJid(String jid, String? plaintext, { List<OmemoBundle>? newSessions }) {
|
||||||
return encryptToJids([jid], plaintext, newSessions: newSessions);
|
return encryptToJids([jid], plaintext, newSessions: newSessions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt the key [plaintext] for all known bundles of the Jids in [jids]. Returns a
|
/// Encrypt the key [plaintext] for all known bundles of the Jids in [jids]. Returns a
|
||||||
/// map that maps the device Id to the ciphertext of [plaintext].
|
/// map that maps the device Id to the ciphertext of [plaintext].
|
||||||
Future<EncryptionResult> encryptToJids(List<String> jids, String plaintext, { List<OmemoBundle>? newSessions }) async {
|
///
|
||||||
|
/// If [plaintext] is null, then the result will be an empty OMEMO message, i.e. one that
|
||||||
|
/// does not contain a <payload /> element. This means that the ciphertext attribute of
|
||||||
|
/// the result will be null as well.
|
||||||
|
Future<EncryptionResult> encryptToJids(List<String> jids, String? plaintext, { List<OmemoBundle>? newSessions }) async {
|
||||||
final encryptedKeys = List<EncryptedKey>.empty(growable: true);
|
final encryptedKeys = List<EncryptedKey>.empty(growable: true);
|
||||||
|
|
||||||
// Generate the key and encrypt the plaintext
|
var ciphertext = const <int>[];
|
||||||
final key = generateRandomBytes(32);
|
var keyPayload = const <int>[];
|
||||||
final keys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
|
if (plaintext != null) {
|
||||||
final ciphertext = await aes256CbcEncrypt(
|
// Generate the key and encrypt the plaintext
|
||||||
utf8.encode(plaintext),
|
final key = generateRandomBytes(32);
|
||||||
keys.encryptionKey,
|
final keys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
|
||||||
keys.iv,
|
ciphertext = await aes256CbcEncrypt(
|
||||||
);
|
utf8.encode(plaintext),
|
||||||
final hmac = await truncatedHmac(ciphertext, keys.authenticationKey);
|
keys.encryptionKey,
|
||||||
final concatKey = concat([key, hmac]);
|
keys.iv,
|
||||||
|
);
|
||||||
|
final hmac = await truncatedHmac(ciphertext, keys.authenticationKey);
|
||||||
|
keyPayload = concat([key, hmac]);
|
||||||
|
} else {
|
||||||
|
keyPayload = List<int>.filled(32, 0x0);
|
||||||
|
}
|
||||||
|
|
||||||
final kex = <int, OmemoKeyExchange>{};
|
final kex = <int, OmemoKeyExchange>{};
|
||||||
if (newSessions != null) {
|
if (newSessions != null) {
|
||||||
@ -219,14 +229,14 @@ class OmemoSessionManager {
|
|||||||
for (final deviceId in _deviceMap[jid]!) {
|
for (final deviceId in _deviceMap[jid]!) {
|
||||||
final ratchetKey = RatchetMapKey(jid, deviceId);
|
final ratchetKey = RatchetMapKey(jid, deviceId);
|
||||||
final ratchet = _ratchetMap[ratchetKey]!;
|
final ratchet = _ratchetMap[ratchetKey]!;
|
||||||
final ciphertext = (await ratchet.ratchetEncrypt(concatKey)).ciphertext;
|
final ciphertext = (await ratchet.ratchetEncrypt(keyPayload)).ciphertext;
|
||||||
|
|
||||||
// Commit the ratchet
|
// Commit the ratchet
|
||||||
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
|
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
|
||||||
|
|
||||||
if (kex.isNotEmpty && kex.containsKey(deviceId)) {
|
if (kex.isNotEmpty && kex.containsKey(deviceId)) {
|
||||||
final k = kex[deviceId]!
|
final k = kex[deviceId]!
|
||||||
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
|
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
|
||||||
encryptedKeys.add(
|
encryptedKeys.add(
|
||||||
EncryptedKey(
|
EncryptedKey(
|
||||||
jid,
|
jid,
|
||||||
@ -250,7 +260,7 @@ class OmemoSessionManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return EncryptionResult(
|
return EncryptionResult(
|
||||||
ciphertext,
|
plaintext != null ? ciphertext : null,
|
||||||
encryptedKeys,
|
encryptedKeys,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -259,7 +269,12 @@ 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.
|
||||||
Future<String> decryptMessage(List<int> ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys) async {
|
///
|
||||||
|
/// 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 {
|
||||||
// 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);
|
||||||
@ -314,6 +329,11 @@ class OmemoSessionManager {
|
|||||||
_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.
|
||||||
|
if (ciphertext == 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);
|
||||||
@ -322,7 +342,7 @@ class OmemoSessionManager {
|
|||||||
if (!listsEqual(hmac, computedHmac)) {
|
if (!listsEqual(hmac, computedHmac)) {
|
||||||
throw InvalidMessageHMACException();
|
throw InvalidMessageHMACException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv);
|
final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv);
|
||||||
return utf8.decode(plaintext);
|
return utf8.decode(plaintext);
|
||||||
}
|
}
|
||||||
|
@ -186,4 +186,39 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(messagePlaintext, aliceMessage2);
|
expect(messagePlaintext, aliceMessage2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Test using sending empty OMEMO messages', () async {
|
||||||
|
const aliceJid = 'alice@server.example';
|
||||||
|
const bobJid = 'bob@other.server.example';
|
||||||
|
|
||||||
|
// Alice and Bob generate their sessions
|
||||||
|
final aliceSession = await OmemoSessionManager.generateNewIdentity(aliceJid, opkAmount: 1);
|
||||||
|
final bobSession = await OmemoSessionManager.generateNewIdentity(bobJid, opkAmount: 1);
|
||||||
|
|
||||||
|
// Alice encrypts a message for Bob
|
||||||
|
final aliceMessage = await aliceSession.encryptToJid(
|
||||||
|
bobJid,
|
||||||
|
null,
|
||||||
|
newSessions: [
|
||||||
|
await (await bobSession.getDevice()).toBundle(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(aliceMessage.encryptedKeys.length, 1);
|
||||||
|
expect(aliceMessage.ciphertext, null);
|
||||||
|
|
||||||
|
// Alice sends the message to Bob
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// Bob decrypts it
|
||||||
|
final bobMessage = await bobSession.decryptMessage(
|
||||||
|
aliceMessage.ciphertext,
|
||||||
|
aliceJid,
|
||||||
|
(await aliceSession.getDevice()).id,
|
||||||
|
aliceMessage.encryptedKeys,
|
||||||
|
);
|
||||||
|
expect(bobMessage, null);
|
||||||
|
|
||||||
|
// This call must not cause an exception
|
||||||
|
bobSession.getRatchet(aliceJid, (await aliceSession.getDevice()).id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user