???: Move code around
This commit is contained in:
		
							parent
							
								
									d3c8d813a9
								
							
						
					
					
						commit
						4e3e20f08c
					
				@ -1,6 +1,8 @@
 | 
			
		||||
library omemo_dart;
 | 
			
		||||
 | 
			
		||||
export 'src/bundle.dart';
 | 
			
		||||
export 'src/double_ratchet.dart';
 | 
			
		||||
export 'src/errors.dart';
 | 
			
		||||
export 'src/helpers.dart';
 | 
			
		||||
export 'src/key.dart';
 | 
			
		||||
export 'src/x3dh.dart';
 | 
			
		||||
 | 
			
		||||
@ -1,281 +1,196 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'package:cryptography/cryptography.dart';
 | 
			
		||||
import 'package:meta/meta.dart';
 | 
			
		||||
import 'package:omemo_dart/protobuf/schema.pb.dart';
 | 
			
		||||
import 'package:omemo_dart/src/bundle.dart';
 | 
			
		||||
import 'package:omemo_dart/src/double_ratchet/crypto.dart';
 | 
			
		||||
import 'package:omemo_dart/src/double_ratchet/kdf.dart';
 | 
			
		||||
import 'package:omemo_dart/src/helpers.dart';
 | 
			
		||||
import 'package:omemo_dart/src/key.dart';
 | 
			
		||||
import 'package:omemo_dart/src/x3dh.dart';
 | 
			
		||||
 | 
			
		||||
class OmemoRatchetStepResult {
 | 
			
		||||
/// Amount of messages we may skip per session
 | 
			
		||||
const maxSkip = 1000;
 | 
			
		||||
 | 
			
		||||
  const OmemoRatchetStepResult(this.header, this.cipherText);
 | 
			
		||||
  final List<int> header;
 | 
			
		||||
  final List<int> cipherText;
 | 
			
		||||
class RatchetStep {
 | 
			
		||||
 | 
			
		||||
  const RatchetStep(this.header, this.ciphertext);
 | 
			
		||||
  final OMEMOMessage header;
 | 
			
		||||
  final List<int> ciphertext;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class OmemoEncryptionResult {
 | 
			
		||||
@immutable
 | 
			
		||||
class SkippedKey {
 | 
			
		||||
 | 
			
		||||
  const OmemoEncryptionResult(this.cipherText, this.keys);
 | 
			
		||||
  /// The encrypted plaintext
 | 
			
		||||
  final List<int> cipherText;
 | 
			
		||||
  /// Mapping between Device id and the key to decrypt cipherText;
 | 
			
		||||
  final Map<String, List<int>> keys;
 | 
			
		||||
  const SkippedKey(this.dh, this.n);
 | 
			
		||||
  final OmemoPublicKey dh;
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// The session state of one party
 | 
			
		||||
class AliceOmemoSession {
 | 
			
		||||
class OmemoDoubleRatchet {
 | 
			
		||||
 | 
			
		||||
  AliceOmemoSession(
 | 
			
		||||
    this.dhs,
 | 
			
		||||
    this.dhr,
 | 
			
		||||
    this.ek,
 | 
			
		||||
    this.rk,
 | 
			
		||||
    this.cks,
 | 
			
		||||
    this.ckr,
 | 
			
		||||
    this.ns,
 | 
			
		||||
    this.nr,
 | 
			
		||||
    this.pn,
 | 
			
		||||
    // this.skippedMessages,
 | 
			
		||||
    this.ad,
 | 
			
		||||
  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.sessionAd,
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  /// The Diffie-Hellman sending key pair
 | 
			
		||||
  final OmemoKeyPair dhs;
 | 
			
		||||
     
 | 
			
		||||
  /// Sending DH keypair
 | 
			
		||||
  OmemoKeyPair dhs;
 | 
			
		||||
 | 
			
		||||
  /// The Diffie-Hellman receiving key pair
 | 
			
		||||
  final OmemoPublicKey dhr;
 | 
			
		||||
  /// Receiving Public key
 | 
			
		||||
  OmemoPublicKey? dhr;
 | 
			
		||||
 | 
			
		||||
  /// The EK used by X3DH
 | 
			
		||||
  final OmemoKeyPair ek;
 | 
			
		||||
  
 | 
			
		||||
  /// The Root Key
 | 
			
		||||
  /// 32 byte Root Key
 | 
			
		||||
  List<int> rk;
 | 
			
		||||
 | 
			
		||||
  /// Sending Chain Key
 | 
			
		||||
  List<int> cks;
 | 
			
		||||
 | 
			
		||||
  /// Receiving Chain Key
 | 
			
		||||
  /// Sending and receiving Chain Keys
 | 
			
		||||
  List<int>? cks;
 | 
			
		||||
  List<int>? ckr;
 | 
			
		||||
 | 
			
		||||
  /// Message number for sending
 | 
			
		||||
  /// Sending and receiving message numbers
 | 
			
		||||
  int ns;
 | 
			
		||||
 | 
			
		||||
  /// Message number for receiving
 | 
			
		||||
  int nr;
 | 
			
		||||
 | 
			
		||||
  /// Number of messages in the previous sending chain
 | 
			
		||||
  /// Previous sending chain number
 | 
			
		||||
  int pn;
 | 
			
		||||
  
 | 
			
		||||
  /// The associated data from the X3DH
 | 
			
		||||
  final List<int> ad;
 | 
			
		||||
 | 
			
		||||
  // TODO(PapaTutuWawa): Track skipped over message keys
 | 
			
		||||
  final List<int> sessionAd;
 | 
			
		||||
 | 
			
		||||
  static Future<AliceOmemoSession> newSession(OmemoBundle bundle, OmemoKeyPair ik) async {
 | 
			
		||||
    // TODO(PapaTutuWawa): Error handling
 | 
			
		||||
    final x3dhResult = await x3dhFromBundle(bundle, ik);
 | 
			
		||||
  final Map<SkippedKey, List<int>> mkSkipped = {};
 | 
			
		||||
 | 
			
		||||
  /// This is performed by the initiating entity
 | 
			
		||||
  static Future<OmemoDoubleRatchet> initiateNewSession(OmemoPublicKey spk, List<int> sk, List<int> ad) async {
 | 
			
		||||
    final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
 | 
			
		||||
    final dhr = bundle.ik;
 | 
			
		||||
    final ek = x3dhResult.ek;
 | 
			
		||||
    final sk = x3dhResult.sk;
 | 
			
		||||
    final kdfRkResult = await kdfRk(sk, await dh(dhs, dhr, 2));
 | 
			
		||||
    
 | 
			
		||||
    return AliceOmemoSession(
 | 
			
		||||
    final dhr = spk;
 | 
			
		||||
    final rk  = await kdfRk(sk, await dh(dhs, dhr, 0));
 | 
			
		||||
    final cks = rk;
 | 
			
		||||
 | 
			
		||||
    return OmemoDoubleRatchet(
 | 
			
		||||
      dhs,
 | 
			
		||||
      dhr,
 | 
			
		||||
      ek,
 | 
			
		||||
      kdfRkResult.rk,
 | 
			
		||||
      kdfRkResult.ck,
 | 
			
		||||
      rk,
 | 
			
		||||
      cks,
 | 
			
		||||
      null,
 | 
			
		||||
      0,
 | 
			
		||||
      0,
 | 
			
		||||
      0,
 | 
			
		||||
      x3dhResult.ad,
 | 
			
		||||
      ad,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// The associated_data parameter is implicit as it belongs to the session
 | 
			
		||||
  Future<List<int>> _encrypt(List<int> mk, List<int> plaintext, List<int> associatedData) async {
 | 
			
		||||
    final algorithm = Hkdf(
 | 
			
		||||
      hmac: Hmac(Sha256()),
 | 
			
		||||
      outputLength: 80,
 | 
			
		||||
  /// This is performed by the accepting entity
 | 
			
		||||
  static Future<OmemoDoubleRatchet> acceptNewSession(OmemoKeyPair spk, List<int> sk, List<int> ad) async {
 | 
			
		||||
    final dhs = spk;
 | 
			
		||||
    return OmemoDoubleRatchet(
 | 
			
		||||
      dhs,
 | 
			
		||||
      null,
 | 
			
		||||
      sk,
 | 
			
		||||
      null,
 | 
			
		||||
      null,
 | 
			
		||||
      0,
 | 
			
		||||
      0,
 | 
			
		||||
      0,
 | 
			
		||||
      ad,
 | 
			
		||||
    );
 | 
			
		||||
    final hkdfResult = await algorithm.deriveKey(
 | 
			
		||||
      secretKey: SecretKey(mk),
 | 
			
		||||
      nonce: List<int>.filled(32, 0x00),
 | 
			
		||||
      info: utf8.encode(encryptHkdfInfoString),
 | 
			
		||||
    );
 | 
			
		||||
    final bytes = await hkdfResult.extractBytes();
 | 
			
		||||
 | 
			
		||||
    final encKey = bytes.sublist(0, 32);
 | 
			
		||||
    final authKey = bytes.sublist(32, 64);
 | 
			
		||||
    final iv = bytes.sublist(64, 82);
 | 
			
		||||
 | 
			
		||||
    // TODO(PapaTutuWawa): Remove once done
 | 
			
		||||
    assert(encKey.length == 32);
 | 
			
		||||
    assert(authKey.length == 32);
 | 
			
		||||
    assert(iv.length == 16);
 | 
			
		||||
 | 
			
		||||
    // 32 = 256 / 8
 | 
			
		||||
    final encodedPlaintext = pkcs7padding(plaintext, 32);
 | 
			
		||||
 | 
			
		||||
    final aesAlgorithm = AesCbc.with256bits(
 | 
			
		||||
      macAlgorithm: Hmac.sha256(),
 | 
			
		||||
    );
 | 
			
		||||
    final secretBox = await aesAlgorithm.encrypt(
 | 
			
		||||
      encodedPlaintext,
 | 
			
		||||
      secretKey: SecretKey(encKey),
 | 
			
		||||
      nonce: iv,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final ad_ = associatedData.sublist(0, ad.length);
 | 
			
		||||
    final message = OMEMOMessage.fromBuffer(associatedData.sublist(ad.length))
 | 
			
		||||
      ..ciphertext = secretBox.cipherText;
 | 
			
		||||
    final messageBytes = message.writeToBuffer();
 | 
			
		||||
 | 
			
		||||
    final input = concat([ad_, messageBytes]);
 | 
			
		||||
    final authBytes = (await Hmac.sha256().calculateMac(
 | 
			
		||||
      input,
 | 
			
		||||
      secretKey: SecretKey(authKey),
 | 
			
		||||
    )).bytes.sublist(0, 16);
 | 
			
		||||
 | 
			
		||||
    final authenticatedMessage = OMEMOAuthenticatedMessage()
 | 
			
		||||
      ..mac = authBytes
 | 
			
		||||
      ..message = messageBytes;
 | 
			
		||||
 | 
			
		||||
    return authenticatedMessage.writeToBuffer();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<int>> ratchetStep(List<int> plaintext) async {
 | 
			
		||||
    final kdfResult = await kdfCk(cks);
 | 
			
		||||
    final message = OMEMOMessage()
 | 
			
		||||
      ..dhPub = await dhs.pk.getBytes()
 | 
			
		||||
      ..pn = pn
 | 
			
		||||
      ..n = ns;
 | 
			
		||||
    final header = message.writeToBuffer();
 | 
			
		||||
    
 | 
			
		||||
    cks = kdfResult.ck;
 | 
			
		||||
    ns++;
 | 
			
		||||
 | 
			
		||||
    return _encrypt(
 | 
			
		||||
      kdfResult.mk,
 | 
			
		||||
      plaintext,
 | 
			
		||||
      concat([ad, header]),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<OmemoEncryptionResult> encryptForSessions(List<AliceOmemoSession> sessions, String plaintext) async {
 | 
			
		||||
  // TODO(PapaTutuWawa): Generate random data
 | 
			
		||||
  final key = List<int>.filled(32, 0x0);
 | 
			
		||||
  final algorithm = Hkdf(
 | 
			
		||||
    hmac: Hmac(Sha256()),
 | 
			
		||||
    outputLength: 80,
 | 
			
		||||
  );
 | 
			
		||||
  final result = await algorithm.deriveKey(
 | 
			
		||||
    secretKey: SecretKey(key),
 | 
			
		||||
    nonce: List<int>.filled(32, 0x0),
 | 
			
		||||
    info: utf8.encode(encryptionHkdfInfoString),
 | 
			
		||||
  );
 | 
			
		||||
  final bytes = await result.extractBytes();
 | 
			
		||||
 | 
			
		||||
  final encKey = bytes.sublist(0, 32);
 | 
			
		||||
  final authKey = bytes.sublist(32, 64);
 | 
			
		||||
  final iv = bytes.sublist(64, 80);
 | 
			
		||||
 | 
			
		||||
  final encodedPlaintext = pkcs7padding(utf8.encode(plaintext), 32);
 | 
			
		||||
  final aesAlgorithm = AesCbc.with256bits(
 | 
			
		||||
    macAlgorithm: Hmac.sha256(),
 | 
			
		||||
  );
 | 
			
		||||
  final secretBox = await aesAlgorithm.encrypt(
 | 
			
		||||
    encodedPlaintext,
 | 
			
		||||
    secretKey: SecretKey(encKey),
 | 
			
		||||
    nonce: iv,
 | 
			
		||||
  );
 | 
			
		||||
  final hmac = (await Hmac.sha256().calculateMac(
 | 
			
		||||
    secretBox.cipherText,
 | 
			
		||||
    secretKey: SecretKey(authKey),
 | 
			
		||||
  )).bytes.sublist(0, 16);
 | 
			
		||||
 | 
			
		||||
  final keyData = concat([encKey, hmac]);
 | 
			
		||||
 | 
			
		||||
  final keyMap = <String, List<int>>{};
 | 
			
		||||
  for (final session in sessions) {
 | 
			
		||||
    final ratchetKey = await session.ratchetStep(keyData);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return OmemoEncryptionResult(
 | 
			
		||||
    secretBox.cipherText,
 | 
			
		||||
    keyMap,
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Result of the KDF_RK function from the Double Ratchet spec.
 | 
			
		||||
class KdfRkResult {
 | 
			
		||||
 | 
			
		||||
  const KdfRkResult(this.rk, this.ck);
 | 
			
		||||
  /// 32 byte Root Key
 | 
			
		||||
  final List<int> rk;
 | 
			
		||||
 | 
			
		||||
  /// 32 byte Chain Key
 | 
			
		||||
  final List<int> ck;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Result of the KDF_CK function from the Double Ratchet spec.
 | 
			
		||||
class KdfCkResult {
 | 
			
		||||
 | 
			
		||||
  const KdfCkResult(this.ck, this.mk);
 | 
			
		||||
  /// 32 byte Chain Key
 | 
			
		||||
  final List<int> ck;
 | 
			
		||||
 | 
			
		||||
  /// 32 byte Message Key
 | 
			
		||||
  final List<int> mk;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Amount of messages we may skip per session
 | 
			
		||||
const maxSkip = 1000;
 | 
			
		||||
 | 
			
		||||
/// Info string for KDF_RK
 | 
			
		||||
const kdfRkInfoString = 'OMEMO Root Chain';
 | 
			
		||||
 | 
			
		||||
/// Info string for ENCRYPT
 | 
			
		||||
const encryptHkdfInfoString = 'OMEMO Message Key Material';
 | 
			
		||||
 | 
			
		||||
/// Info string for encrypting a message
 | 
			
		||||
const encryptionHkdfInfoString = 'OMEMO Payload';
 | 
			
		||||
 | 
			
		||||
/// Flags for KDF_CK
 | 
			
		||||
const kdfCkNextMessageKey = 0x01;
 | 
			
		||||
const kdfCkNextChainKey = 0x02;
 | 
			
		||||
 | 
			
		||||
Future<KdfRkResult> kdfRk(List<int> rk, List<int> dhOut) async {
 | 
			
		||||
  final algorithm = Hkdf(
 | 
			
		||||
    hmac: Hmac(Sha256()),
 | 
			
		||||
    outputLength: 32,
 | 
			
		||||
  );
 | 
			
		||||
  final result = await algorithm.deriveKey(
 | 
			
		||||
    secretKey: SecretKey(dhOut),
 | 
			
		||||
    nonce: rk,
 | 
			
		||||
    info: utf8.encode(kdfRkInfoString),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // TODO(PapaTutuWawa): Does the rk in the tuple (rk, ck) refer to the input rk?
 | 
			
		||||
  return KdfRkResult(rk, await result.extractBytes());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Future<KdfCkResult> kdfCk(List<int> ck) async {
 | 
			
		||||
  final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32);
 | 
			
		||||
  final newCk = await hkdf.deriveKey(
 | 
			
		||||
    secretKey: SecretKey(ck),
 | 
			
		||||
    nonce: [kdfCkNextChainKey],
 | 
			
		||||
  );
 | 
			
		||||
  final mk = await hkdf.deriveKey(
 | 
			
		||||
    secretKey: SecretKey(ck),
 | 
			
		||||
    nonce: [kdfCkNextMessageKey],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return KdfCkResult(
 | 
			
		||||
    await newCk.extractBytes(),
 | 
			
		||||
    await mk.extractBytes(),
 | 
			
		||||
  );
 | 
			
		||||
  Future<RatchetStep> ratchetEncrypt(List<int> plaintext) async {
 | 
			
		||||
    final newCks = await kdfCk(cks!, kdfCkNextChainKey);
 | 
			
		||||
    final mk = await kdfCk(cks!, kdfCkNextMessageKey);
 | 
			
		||||
 | 
			
		||||
    cks = newCks;
 | 
			
		||||
    final header = OMEMOMessage()
 | 
			
		||||
      ..n = ns
 | 
			
		||||
      ..pn = pn
 | 
			
		||||
      ..dhPub = await dhs.pk.getBytes();
 | 
			
		||||
 | 
			
		||||
    ns++;
 | 
			
		||||
 | 
			
		||||
    return RatchetStep(
 | 
			
		||||
      header,
 | 
			
		||||
      await encrypt(mk, plaintext, concat([sessionAd, header.writeToBuffer()]), sessionAd),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<int>?> trySkippedMessageKeys(OMEMOMessage header, List<int> ciphertext) async {
 | 
			
		||||
    final key = SkippedKey(
 | 
			
		||||
      // TODO(PapaTutuWawa): Is this correct
 | 
			
		||||
      OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.ed25519),
 | 
			
		||||
      header.n,
 | 
			
		||||
    );
 | 
			
		||||
    if (mkSkipped.containsKey(key)) {
 | 
			
		||||
      final mk = mkSkipped[key]!;
 | 
			
		||||
      mkSkipped.remove(key);
 | 
			
		||||
 | 
			
		||||
      return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> skipMessageKeys(int until) async {
 | 
			
		||||
    if (nr + maxSkip < until) {
 | 
			
		||||
      // TODO(PapaTutuWawa): Custom exception
 | 
			
		||||
      throw Exception();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> dhRatchet(OMEMOMessage header) async {
 | 
			
		||||
    pn = header.n;
 | 
			
		||||
    ns = 0;
 | 
			
		||||
    nr = 0;
 | 
			
		||||
    dhr = OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.ed25519);
 | 
			
		||||
 | 
			
		||||
    final newRk = await kdfRk(rk, await dh(dhs, dhr!, 2));
 | 
			
		||||
    rk = newRk;
 | 
			
		||||
    ckr = newRk;
 | 
			
		||||
    dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
 | 
			
		||||
    final newNewRk = await kdfRk(rk, await dh(dhs, dhr!, 2));
 | 
			
		||||
    rk = newNewRk;
 | 
			
		||||
    cks = newNewRk;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  Future<List<int>> ratchetDecrypt(OMEMOMessage header, List<int> ciphertext) async {
 | 
			
		||||
    // Check if we skipped too many messages
 | 
			
		||||
    final plaintext = await trySkippedMessageKeys(header, ciphertext);
 | 
			
		||||
    if (plaintext != null) {
 | 
			
		||||
      return plaintext;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (header.dhPub != await dhr?.getBytes()) {
 | 
			
		||||
      await skipMessageKeys(header.pn);
 | 
			
		||||
      await dhRatchet(header);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await skipMessageKeys(header.n);
 | 
			
		||||
    final newCkr = await kdfCk(ckr!, kdfCkNextChainKey);
 | 
			
		||||
    final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
 | 
			
		||||
    ckr = newCkr;
 | 
			
		||||
    nr++;
 | 
			
		||||
 | 
			
		||||
    return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										109
									
								
								lib/src/double_ratchet/crypto.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								lib/src/double_ratchet/crypto.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'package:cryptography/cryptography.dart';
 | 
			
		||||
import 'package:omemo_dart/protobuf/schema.pb.dart';
 | 
			
		||||
import 'package:omemo_dart/src/helpers.dart';
 | 
			
		||||
 | 
			
		||||
/// Info string for ENCRYPT
 | 
			
		||||
const encryptHkdfInfoString = 'OMEMO Message Key Material';
 | 
			
		||||
 | 
			
		||||
/// 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<void> checkMac({
 | 
			
		||||
    required MacAlgorithm macAlgorithm,
 | 
			
		||||
    required SecretKey secretKey,
 | 
			
		||||
    required List<int> aad,
 | 
			
		||||
  }) async {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Signals ENCRYPT function as specified by OMEMO 0.8.0.
 | 
			
		||||
/// Encrypt [plaintext] using the message key [mk], given associated_data [associatedData]
 | 
			
		||||
/// and the AD output from the X3DH [sessionAd].
 | 
			
		||||
Future<List<int>> encrypt(List<int> mk, List<int> plaintext, List<int> associatedData, List<int> sessionAd) async {
 | 
			
		||||
  final hkdf = Hkdf(
 | 
			
		||||
    hmac: Hmac(Sha256()),
 | 
			
		||||
    outputLength: 80,
 | 
			
		||||
  );
 | 
			
		||||
  final hkdfResult = await hkdf.deriveKey(
 | 
			
		||||
    secretKey: SecretKey(mk),
 | 
			
		||||
    nonce: List<int>.filled(32, 0x0),
 | 
			
		||||
    info: utf8.encode(encryptHkdfInfoString),
 | 
			
		||||
  );
 | 
			
		||||
  final hkdfBytes = await hkdfResult.extractBytes();
 | 
			
		||||
 | 
			
		||||
  // Split hkdfBytes into encryption, authentication key and IV
 | 
			
		||||
  final encryptionKey = hkdfBytes.sublist(0, 32);
 | 
			
		||||
  final authenticationKey = hkdfBytes.sublist(32, 64);
 | 
			
		||||
  final iv = hkdfBytes.sublist(64, 80);
 | 
			
		||||
 | 
			
		||||
  final aesResult = await AesCbc.with256bits(
 | 
			
		||||
    macAlgorithm: MacAlgorithm.empty,
 | 
			
		||||
  ).encrypt(
 | 
			
		||||
    plaintext,
 | 
			
		||||
    secretKey: SecretKey(encryptionKey),
 | 
			
		||||
    nonce: iv,
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  final header = OMEMOMessage.fromBuffer(associatedData.sublist(sessionAd.length))
 | 
			
		||||
    ..ciphertext = aesResult.cipherText;
 | 
			
		||||
  final headerBytes = header.writeToBuffer();
 | 
			
		||||
  final hmacInput = concat([sessionAd, headerBytes]);
 | 
			
		||||
  final hmacResult = (await Hmac.sha256().calculateMac(
 | 
			
		||||
    hmacInput,
 | 
			
		||||
    secretKey: SecretKey(authenticationKey),
 | 
			
		||||
  )).bytes.sublist(0, 16);
 | 
			
		||||
  
 | 
			
		||||
  final message = OMEMOAuthenticatedMessage()
 | 
			
		||||
    ..mac = hmacResult
 | 
			
		||||
    ..message = headerBytes;
 | 
			
		||||
  return message.writeToBuffer();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Signals DECRYPT function as specified by OMEMO 0.8.0.
 | 
			
		||||
/// Decrypt [ciphertext] with the message key [mk], given the associated_data [associatedData]
 | 
			
		||||
/// and the AD output from the X3DH.
 | 
			
		||||
Future<List<int>> decrypt(List<int> mk, List<int> ciphertext, List<int> associatedData, List<int> sessionAd) async {
 | 
			
		||||
  // Generate the keys and iv from mk
 | 
			
		||||
  final hkdf = Hkdf(
 | 
			
		||||
    hmac: Hmac(Sha256()),
 | 
			
		||||
    outputLength: 80,
 | 
			
		||||
  );
 | 
			
		||||
  final hkdfResult = await hkdf.deriveKey(
 | 
			
		||||
    secretKey: SecretKey(mk),
 | 
			
		||||
    nonce: List<int>.filled(32, 0x0),
 | 
			
		||||
    info: utf8.encode(encryptHkdfInfoString),
 | 
			
		||||
  );
 | 
			
		||||
  final hkdfBytes = await hkdfResult.extractBytes();
 | 
			
		||||
 | 
			
		||||
  // Split hkdfBytes into encryption, authentication key and IV
 | 
			
		||||
  final encryptionKey = hkdfBytes.sublist(0, 32);
 | 
			
		||||
  final authenticationKey = hkdfBytes.sublist(32, 64);
 | 
			
		||||
  final iv = hkdfBytes.sublist(64, 80);
 | 
			
		||||
  
 | 
			
		||||
  // Assumption ciphertext is a OMEMOAuthenticatedMessage
 | 
			
		||||
  final message = OMEMOAuthenticatedMessage.fromBuffer(ciphertext);
 | 
			
		||||
  final header = OMEMOMessage.fromBuffer(message.message);
 | 
			
		||||
 | 
			
		||||
  final hmacInput = concat([sessionAd, header.writeToBuffer()]);
 | 
			
		||||
  final hmacResult = (await Hmac.sha256().calculateMac(
 | 
			
		||||
    hmacInput,
 | 
			
		||||
    secretKey: SecretKey(authenticationKey),
 | 
			
		||||
  )).bytes.sublist(0, 16);
 | 
			
		||||
 | 
			
		||||
  // TODO(PapaTutuWawa): Check the HMAC result
 | 
			
		||||
  
 | 
			
		||||
  final plaintext = await AesCbc.with256bits(
 | 
			
		||||
    macAlgorithm: MacAlgorithm.empty,
 | 
			
		||||
  ).decrypt(
 | 
			
		||||
    NoMacSecretBox(
 | 
			
		||||
      header.ciphertext,
 | 
			
		||||
      nonce: iv,
 | 
			
		||||
    ),
 | 
			
		||||
    secretKey: SecretKey(encryptionKey),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return plaintext;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								lib/src/double_ratchet/kdf.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								lib/src/double_ratchet/kdf.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'package:cryptography/cryptography.dart';
 | 
			
		||||
 | 
			
		||||
/// Info string for KDF_RK
 | 
			
		||||
const kdfRkInfoString = 'OMEMO Root Chain';
 | 
			
		||||
 | 
			
		||||
/// Flags for KDF_CK
 | 
			
		||||
const kdfCkNextMessageKey = 0x01;
 | 
			
		||||
const kdfCkNextChainKey = 0x02;
 | 
			
		||||
 | 
			
		||||
/// Signals KDF_CK function as specified by OMEMO 0.8.0.
 | 
			
		||||
Future<List<int>> kdfCk(List<int> ck, int constant) async {
 | 
			
		||||
  final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32);
 | 
			
		||||
  final result = await hkdf.deriveKey(
 | 
			
		||||
    secretKey: SecretKey(ck),
 | 
			
		||||
    nonce: [constant],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return result.extractBytes();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Signals KDF_RK function as specified by OMEMO 0.8.0.
 | 
			
		||||
Future<List<int>> kdfRk(List<int> rk, List<int> dhOut) async {
 | 
			
		||||
  final algorithm = Hkdf(
 | 
			
		||||
    hmac: Hmac(Sha256()),
 | 
			
		||||
    outputLength: 32,
 | 
			
		||||
  );
 | 
			
		||||
  final result = await algorithm.deriveKey(
 | 
			
		||||
    secretKey: SecretKey(dhOut),
 | 
			
		||||
    nonce: rk,
 | 
			
		||||
    info: utf8.encode(kdfRkInfoString),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return result.extractBytes();
 | 
			
		||||
}
 | 
			
		||||
@ -59,6 +59,9 @@ Future<List<int>> dh(OmemoKeyPair kp, OmemoPublicKey pk, int identityKey) async
 | 
			
		||||
    ckp = await kp.toCurve25519();
 | 
			
		||||
  } else if (identityKey == 2) {
 | 
			
		||||
    cpk = await pk.toCurve25519();
 | 
			
		||||
  } else if (identityKey == 3) {
 | 
			
		||||
    ckp = await kp.toCurve25519();
 | 
			
		||||
    cpk = await pk.toCurve25519();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  final shared = await Cryptography.instance.x25519().sharedSecretKey(
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										109
									
								
								test/double_ratchet_test.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								test/double_ratchet_test.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'package:cryptography/cryptography.dart';
 | 
			
		||||
import 'package:omemo_dart/omemo_dart.dart';
 | 
			
		||||
import 'package:omemo_dart/protobuf/schema.pb.dart';
 | 
			
		||||
import 'package:omemo_dart/src/double_ratchet/crypto.dart';
 | 
			
		||||
import 'package:test/test.dart';
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  test('Test encrypting and decrypting', () async {
 | 
			
		||||
    final sessionAd = List<int>.filled(32, 0x0);
 | 
			
		||||
    final mk = List<int>.filled(32, 0x1);
 | 
			
		||||
    final plaintext = utf8.encode('Hallo');
 | 
			
		||||
    final header = OMEMOMessage()
 | 
			
		||||
      ..n = 0
 | 
			
		||||
      ..pn = 0
 | 
			
		||||
      ..dhPub = List<int>.empty();
 | 
			
		||||
    final asd = concat([sessionAd, header.writeToBuffer()]);
 | 
			
		||||
      
 | 
			
		||||
    final ciphertext = await encrypt(
 | 
			
		||||
      mk,
 | 
			
		||||
      plaintext,
 | 
			
		||||
      asd,
 | 
			
		||||
      sessionAd,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    final decrypted = await decrypt(
 | 
			
		||||
      mk,
 | 
			
		||||
      ciphertext,
 | 
			
		||||
      asd,
 | 
			
		||||
      sessionAd,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(decrypted, plaintext);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
  test('Test the Double Ratchet', () async {
 | 
			
		||||
    // Generate keys
 | 
			
		||||
    final ikAlice = await OmemoKeyPair.generateNewPair(KeyPairType.ed25519);
 | 
			
		||||
    final ikBob = await OmemoKeyPair.generateNewPair(KeyPairType.ed25519);
 | 
			
		||||
    final spkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
 | 
			
		||||
    final opkBob = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
 | 
			
		||||
    final bundleBob = OmemoBundle(
 | 
			
		||||
      '1',
 | 
			
		||||
      await spkBob.pk.asBase64(),
 | 
			
		||||
      '3',
 | 
			
		||||
      base64Encode(
 | 
			
		||||
        await sig(ikBob, await spkBob.pk.getBytes()),
 | 
			
		||||
      ),
 | 
			
		||||
      //'Q5in+/L4kJixEX692h6mJkPMyp4I3SlQ84L0E7ipPzqfPHOMiraUlqG2vG/O8wvFjLsKYZpPBraga9IvwhqVDA==',
 | 
			
		||||
      await ikBob.pk.asBase64(),
 | 
			
		||||
      {
 | 
			
		||||
        '2': await opkBob.pk.asBase64(),
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    // Alice does X3DH
 | 
			
		||||
    final resultAlice = await x3dhFromBundle(bundleBob, ikAlice);
 | 
			
		||||
 | 
			
		||||
    // Alice sends the inital message to Bob
 | 
			
		||||
    // ...
 | 
			
		||||
    
 | 
			
		||||
    // Bob does X3DH
 | 
			
		||||
    final resultBob = await x3dhFromInitialMessage(
 | 
			
		||||
      X3DHMessage(
 | 
			
		||||
        ikAlice.pk,
 | 
			
		||||
        resultAlice.ek.pk,
 | 
			
		||||
        '2',
 | 
			
		||||
      ),
 | 
			
		||||
      spkBob,
 | 
			
		||||
      opkBob,
 | 
			
		||||
      ikBob,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    print('X3DH key exchange done');
 | 
			
		||||
 | 
			
		||||
    // Alice and Bob now share sk as a common secret and ad
 | 
			
		||||
    final alicesRatchet = await OmemoDoubleRatchet.initiateNewSession(
 | 
			
		||||
      spkBob.pk,
 | 
			
		||||
      resultAlice.sk,
 | 
			
		||||
      resultAlice.ad,
 | 
			
		||||
    );
 | 
			
		||||
    final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession(
 | 
			
		||||
      spkBob,
 | 
			
		||||
      resultBob.sk,
 | 
			
		||||
      resultBob.ad,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd);
 | 
			
		||||
    //expect(await alicesRatchet.dhr.getBytes(), await ikBob.pk.getBytes());
 | 
			
		||||
    
 | 
			
		||||
    // Alice encrypts a message
 | 
			
		||||
    final aliceRatchetResult = await alicesRatchet.ratchetEncrypt(utf8.encode('Hello Bob'));
 | 
			
		||||
    print('Alice sent the message');
 | 
			
		||||
 | 
			
		||||
    // Alice sends it to Bob
 | 
			
		||||
    // ...
 | 
			
		||||
 | 
			
		||||
    // Bob tries to decrypt it
 | 
			
		||||
    final bobRatchetResult = await bobsRatchet.ratchetDecrypt(
 | 
			
		||||
      aliceRatchetResult.header,
 | 
			
		||||
      aliceRatchetResult.ciphertext,
 | 
			
		||||
    );
 | 
			
		||||
    print('Bob decrypted the message');
 | 
			
		||||
 | 
			
		||||
    expect(utf8.encode('Hello Bob'), bobRatchetResult);
 | 
			
		||||
  });
 | 
			
		||||
  */
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user