diff --git a/lib/src/crypto.dart b/lib/src/crypto.dart index c599dad..7666faa 100644 --- a/lib/src/crypto.dart +++ b/lib/src/crypto.dart @@ -32,6 +32,19 @@ class HkdfKeyResult { final List iv; } +/// cryptography _really_ wants to check the MAC output from AES-256-CBC. Since +/// we don't have it, we need the MAC check to always "pass". +class NoMacSecretBox extends SecretBox { + NoMacSecretBox(super.cipherText, { required super.nonce }) : super(mac: Mac.empty); + + @override + Future checkMac({ + required MacAlgorithm macAlgorithm, + required SecretKey secretKey, + required List aad, + }) async {} +} + /// OMEMO 0.8.3 often derives the three keys for encryption, authentication and the IV from /// some input using HKDF-SHA-256. As such, this is a helper function that already provides /// those three keys from [input] and the info string [info]. @@ -65,6 +78,21 @@ Future> aes256CbcEncrypt(List plaintext, List key, List return result.cipherText; } +/// A small helper function to make AES-256-CBC easier. Decrypt [ciphertext] using [key] as +/// the encryption key and [iv] as the IV. Returns the ciphertext. +Future> aes256CbcDecrypt(List ciphertext, List key, List iv) async { + final algorithm = AesCbc.with256bits( + macAlgorithm: MacAlgorithm.empty, + ); + return algorithm.decrypt( + NoMacSecretBox( + ciphertext, + nonce: iv, + ), + secretKey: SecretKey(key), + ); +} + /// OMEMO often uses the output of a HMAC-SHA-256 truncated to its first 16 bytes. /// Calculate the HMAC-SHA-256 of [input] using the authentication key [key] and /// truncate the output to 16 bytes. diff --git a/lib/src/errors.dart b/lib/src/errors.dart index 801f264..8398381 100644 --- a/lib/src/errors.dart +++ b/lib/src/errors.dart @@ -3,7 +3,8 @@ class InvalidSignatureException implements Exception { String errMsg() => 'The signature of the SPK does not match the provided signature'; } -/// Triggered by the Double Ratchet if the computet HMAC does not match the attached HMAC. +/// Triggered by the Double Ratchet if the computed HMAC does not match the attached HMAC. +/// Triggered by the Session Manager if the computed HMAC does not match the attached HMAC. class InvalidMessageHMACException implements Exception { String errMsg() => 'The computed HMAC does not match the provided HMAC'; } @@ -13,3 +14,13 @@ class InvalidMessageHMACException implements Exception { class SkippingTooManyMessagesException implements Exception { String errMsg() => 'Skipping messages would cause a skip bigger than MAXSKIP'; } + +/// Triggered by the Session Manager if the message key is not encrypted for the device. +class NotEncryptedForDeviceException implements Exception { + String errMsg() => 'Not encrypted for this device'; +} + +/// Triggered by the Session Manager when there is no key for decrypting the message. +class NoDecryptionKeyException implements Exception { + String errMsg() => 'No key available for decrypting the message'; +} diff --git a/lib/src/omemo/sessionmanager.dart b/lib/src/omemo/sessionmanager.dart index 866b72e..c83c5d7 100644 --- a/lib/src/omemo/sessionmanager.dart +++ b/lib/src/omemo/sessionmanager.dart @@ -1,6 +1,9 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; +import 'package:omemo_dart/protobuf/schema.pb.dart'; import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart'; +import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/helpers.dart'; import 'package:omemo_dart/src/omemo/device.dart'; import 'package:synchronized/synchronized.dart'; @@ -20,6 +23,13 @@ class EncryptionResult { final Map> encryptedKeys; } +class EncryptedKey { + + const EncryptedKey(this.rid, this.value); + final String rid; + final String value; +} + class OmemoSessionManager { OmemoSessionManager(this.device) : _ratchetMap = {}, _deviceMap = {}, _lock = Lock(); @@ -92,4 +102,42 @@ class OmemoSessionManager { encryptedKeys, ); } + + /// Attempt to decrypt [ciphertext]. [keys] refers to the elements inside the + /// 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 + /// element. + Future decryptMessage(List ciphertext, String senderJid, String senderDeviceId, List keys) async { + // Try to find a session we can decrypt with. + final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id); + if (rawKey == null) { + throw NotEncryptedForDeviceException(); + } + + final devices = _deviceMap[senderJid]; + if (devices == null) { + throw NoDecryptionKeyException(); + } + if (!devices.contains(senderDeviceId)) { + throw NoDecryptionKeyException(); + } + + final decodedRawKey = base64.decode(rawKey.value); + final authMessage = OMEMOAuthenticatedMessage.fromBuffer(decodedRawKey); + final message = OMEMOMessage.fromBuffer(authMessage.message); + + final ratchet = _ratchetMap[senderDeviceId]!; + final keyAndHmac = await ratchet.ratchetDecrypt(message, message.ciphertext); + final key = keyAndHmac.sublist(0, 32); + final hmac = keyAndHmac.sublist(32, 48); + final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString); + + final computedHmac = await truncatedHmac(ciphertext, derivedKeys.authenticationKey); + if (!listsEqual(hmac, computedHmac)) { + throw InvalidMessageHMACException(); + } + + final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv); + return utf8.decode(plaintext); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 09f2b5e..88f1f10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,6 +6,7 @@ environment: sdk: '>=2.17.0 <3.0.0' dependencies: + collection: ^1.16.0 cryptography: ^2.0.5 pinenacl: ^0.5.1 protobuf: ^2.1.0