import 'package:cryptography/cryptography.dart'; import 'package:hex/hex.dart'; import 'package:meta/meta.dart'; import 'package:moxlib/moxlib.dart'; import 'package:omemo_dart/src/common/constants.dart'; import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/double_ratchet/kdf.dart'; import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/helpers.dart'; import 'package:omemo_dart/src/keys.dart'; import 'package:omemo_dart/src/protobuf/schema.pb.dart'; @immutable class SkippedKey { const SkippedKey(this.dh, this.n); /// The DH public key for which we skipped a message key. final OmemoPublicKey dh; /// The associated number of the message key we skipped. final int n; @override bool operator ==(Object other) { return other is SkippedKey && other.dh == dh && other.n == n; } @override int get hashCode => dh.hashCode ^ n.hashCode; } @immutable class KeyExchangeData { const KeyExchangeData( this.pkId, this.spkId, this.ik, this.ek, ); /// The id of the used OPK. final int pkId; /// The id of the used SPK. final int spkId; /// The ephemeral key used while the key exchange. final OmemoPublicKey ek; /// The identity key used in the key exchange. final OmemoPublicKey ik; } class OmemoDoubleRatchet { OmemoDoubleRatchet( this.dhs, // DHs this.dhr, // DHr this.rk, // RK this.cks, // CKs this.ckr, // CKr this.ns, // Ns this.nr, // Nr this.pn, // Pn this.ik, this.sessionAd, this.mkSkipped, // MKSKIPPED this.acknowledged, this.kex, ); /// Sending DH keypair OmemoKeyPair dhs; /// Receiving Public key OmemoPublicKey? dhr; /// 32 byte Root Key List rk; /// Sending and receiving Chain Keys List? cks; List? ckr; /// Sending and receiving message numbers int ns; int nr; /// Previous sending chain number int pn; /// The IK public key from the chat partner. Not used for the actual encryption but /// for verification purposes final OmemoPublicKey ik; /// Associated data for this ratchet. final List sessionAd; /// List of skipped message keys. final Map> mkSkipped; /// The key exchange that was used for initiating the session. final KeyExchangeData kex; /// Indicates whether we received an empty OMEMO message after building a session with /// 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 initiateNewSession( OmemoPublicKey spk, int spkId, OmemoPublicKey ik, OmemoPublicKey ownIk, OmemoPublicKey ek, List sk, List ad, int pkId, ) async { final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final rk = await kdfRk(sk, await omemoDH(dhs, spk, 0)); return OmemoDoubleRatchet( dhs, spk, List.from(rk), List.from(rk), null, 0, 0, 0, ik, ad, {}, false, KeyExchangeData( pkId, spkId, ownIk, ek, ), ); } /// Create an OMEMO session that was not initiated by the caller using the used Signed /// 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 acceptNewSession( OmemoKeyPair spk, int spkId, OmemoPublicKey ik, int pkId, OmemoPublicKey ek, List sk, List ad, ) async { return OmemoDoubleRatchet( spk, null, sk, null, null, 0, 0, 0, ik, ad, {}, true, KeyExchangeData( pkId, spkId, ik, ek, ), ); } /// Performs a single ratchet step in case we received a new /// public key in [header]. Future _dhRatchet(OMEMOMessage header) async { pn = ns; ns = 0; nr = 0; dhr = OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519); final newRk1 = await kdfRk( rk, await omemoDH( dhs, dhr!, 0, ), ); rk = List.from(newRk1); ckr = List.from(newRk1); dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final newRk2 = await kdfRk( rk, await omemoDH( dhs, dhr!, 0, ), ); rk = List.from(newRk2); cks = List.from(newRk2); } /// Skip (and keep track of) message keys until our receive counter is /// equal to [until]. If we would skip too many messages, returns /// a [SkippingTooManyKeysError]. If not, returns null. Future _skipMessageKeys(int until) async { if (nr + maxSkip < until) { return SkippingTooManyKeysError(); } if (ckr != null) { while (nr < until) { final newCkr = await kdfCk(ckr!, kdfCkNextChainKey); final mk = await kdfCk(ckr!, kdfCkNextMessageKey); ckr = newCkr; mkSkipped[SkippedKey(dhr!, nr)] = mk; nr++; } } return null; } /// Decrypt [ciphertext] using keys derived from the message key [mk]. Also computes the /// HMAC from the [OMEMOMessage] embedded in [message]. /// /// If the computed HMAC does not match the HMAC in [message], returns /// [InvalidMessageHMACError]. If it matches, returns the decrypted /// payload. Future>> _decrypt( OMEMOAuthenticatedMessage message, List ciphertext, List mk, ) async { final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString); final hmacInput = concat([sessionAd, message.message]); final hmacResult = await truncatedHmac(hmacInput, keys.authenticationKey); if (!listsEqual(hmacResult, message.mac)) { return Result(InvalidMessageHMACError()); } final plaintext = await aes256CbcDecrypt(ciphertext, keys.encryptionKey, keys.iv); if (plaintext.isType()) { return Result(plaintext.get()); } return Result(plaintext.get>()); } /// Checks whether we could decrypt the payload in [header] with a skipped key. If yes, /// attempts to decrypt it. If not, returns null. /// /// If the decryption is successful, returns the plaintext payload. If an error occurs, like /// an [InvalidMessageHMACError], that is returned instead. Future?>> _trySkippedMessageKeys( OMEMOAuthenticatedMessage message, OMEMOMessage header, ) async { final key = SkippedKey( OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519), header.n, ); if (mkSkipped.containsKey(key)) { final mk = mkSkipped[key]!; mkSkipped.remove(key); return _decrypt(message, header.ciphertext, mk); } return const Result(null); } /// Decrypt the payload (deeply) embedded in [message]. /// /// If everything goes well, returns the plaintext payload. If an error occurs, that /// is returned instead. Future>> ratchetDecrypt( OMEMOAuthenticatedMessage message, ) async { final header = OMEMOMessage.fromBuffer(message.message); // Try skipped keys final plaintextRaw = await _trySkippedMessageKeys(message, header); if (plaintextRaw.isType()) { // Propagate the error return Result(plaintextRaw.get()); } final plaintext = plaintextRaw.get?>(); if (plaintext != null) { return Result(plaintext); } if (dhr == null || !listsEqual(header.dhPub, await dhr!.getBytes())) { final skipResult1 = await _skipMessageKeys(header.pn); if (skipResult1 != null) { return Result(skipResult1); } await _dhRatchet(header); } final skipResult2 = await _skipMessageKeys(header.n); if (skipResult2 != null) { return Result(skipResult2); } final ck = await kdfCk(ckr!, kdfCkNextChainKey); final mk = await kdfCk(ckr!, kdfCkNextMessageKey); ckr = ck; nr++; return _decrypt(message, header.ciphertext, mk); } /// Encrypt the payload [plaintext] using the double ratchet session. Future ratchetEncrypt(List plaintext) async { // Advance the ratchet final ck = await kdfCk(cks!, kdfCkNextChainKey); final mk = await kdfCk(cks!, kdfCkNextMessageKey); cks = ck; // Generate encryption, authentication key and IV final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString); final ciphertext = await aes256CbcEncrypt(plaintext, keys.encryptionKey, keys.iv); // Fill-in the header and serialize it here so we do it only once final header = OMEMOMessage() ..dhPub = await dhs.pk.getBytes() ..pn = pn ..n = ns ..ciphertext = ciphertext; final headerBytes = header.writeToBuffer(); // Increment the send counter ns++; final newAd = concat([sessionAd, headerBytes]); final hmac = await truncatedHmac(newAd, keys.authenticationKey); return OMEMOAuthenticatedMessage() ..mac = hmac ..message = headerBytes; } /// Returns a copy of the ratchet. OmemoDoubleRatchet clone() { return OmemoDoubleRatchet( dhs, dhr, rk, cks != null ? List.from(cks!) : null, ckr != null ? List.from(ckr!) : null, ns, nr, pn, ik, sessionAd, Map>.from(mkSkipped), acknowledged, kex, ); } /// Computes the fingerprint of the double ratchet, according to /// XEP-0384. Future get fingerprint async { final curveKey = await ik.toCurve25519(); return HEX.encode( await curveKey.getBytes(), ); } @visibleForTesting Future equals(OmemoDoubleRatchet other) async { final dhrMatch = dhr == null ? other.dhr == null : // ignore: invalid_use_of_visible_for_testing_member 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 final dhsMatch = await dhs.equals(other.dhs); // ignore: invalid_use_of_visible_for_testing_member final ikMatch = await ik.equals(other.ik); return dhsMatch && ikMatch && dhrMatch && listsEqual(rk, other.rk) && cksMatch && ckrMatch && ns == other.ns && nr == other.nr && pn == other.pn && listsEqual(sessionAd, other.sessionAd); } }