feat: Handle empty OMEMO messages

This commit is contained in:
PapaTutuWawa 2022-08-08 14:59:46 +02:00
parent 8c1a78e360
commit 9f986c3369
2 changed files with 73 additions and 18 deletions

View File

@ -28,7 +28,7 @@ class EncryptionResult {
const EncryptionResult(this.ciphertext, this.encryptedKeys);
/// 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
/// for the ratchet with said device Id
@ -186,25 +186,35 @@ class OmemoSessionManager {
}
/// 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);
}
/// 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].
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);
// Generate the key and encrypt the plaintext
final key = generateRandomBytes(32);
final keys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
final ciphertext = await aes256CbcEncrypt(
utf8.encode(plaintext),
keys.encryptionKey,
keys.iv,
);
final hmac = await truncatedHmac(ciphertext, keys.authenticationKey);
final concatKey = concat([key, hmac]);
var ciphertext = const <int>[];
var keyPayload = const <int>[];
if (plaintext != null) {
// Generate the key and encrypt the plaintext
final key = generateRandomBytes(32);
final keys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
ciphertext = await aes256CbcEncrypt(
utf8.encode(plaintext),
keys.encryptionKey,
keys.iv,
);
final hmac = await truncatedHmac(ciphertext, keys.authenticationKey);
keyPayload = concat([key, hmac]);
} else {
keyPayload = List<int>.filled(32, 0x0);
}
final kex = <int, OmemoKeyExchange>{};
if (newSessions != null) {
@ -219,14 +229,14 @@ class OmemoSessionManager {
for (final deviceId in _deviceMap[jid]!) {
final ratchetKey = RatchetMapKey(jid, deviceId);
final ratchet = _ratchetMap[ratchetKey]!;
final ciphertext = (await ratchet.ratchetEncrypt(concatKey)).ciphertext;
final ciphertext = (await ratchet.ratchetEncrypt(keyPayload)).ciphertext;
// Commit the ratchet
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
if (kex.isNotEmpty && kex.containsKey(deviceId)) {
final k = kex[deviceId]!
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
encryptedKeys.add(
EncryptedKey(
jid,
@ -250,7 +260,7 @@ class OmemoSessionManager {
});
return EncryptionResult(
ciphertext,
plaintext != null ? ciphertext : null,
encryptedKeys,
);
}
@ -259,7 +269,12 @@ 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.
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.
var device = await getDevice();
final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id);
@ -314,6 +329,11 @@ class OmemoSessionManager {
_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 hmac = keyAndHmac!.sublist(32, 48);
final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
@ -322,7 +342,7 @@ class OmemoSessionManager {
if (!listsEqual(hmac, computedHmac)) {
throw InvalidMessageHMACException();
}
final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv);
return utf8.decode(plaintext);
}

View File

@ -186,4 +186,39 @@ void main() {
);
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);
});
}