diff --git a/lib/omemo_dart.dart b/lib/omemo_dart.dart index 06b49bf..9bc83cd 100644 --- a/lib/omemo_dart.dart +++ b/lib/omemo_dart.dart @@ -5,4 +5,4 @@ export 'src/double_ratchet.dart'; export 'src/errors.dart'; export 'src/helpers.dart'; export 'src/key.dart'; -export 'src/x3dh.dart'; +export 'src/x3dh/x3dh.dart'; diff --git a/lib/src/crypto.dart b/lib/src/crypto.dart new file mode 100644 index 0000000..77c36f3 --- /dev/null +++ b/lib/src/crypto.dart @@ -0,0 +1,24 @@ +import 'package:cryptography/cryptography.dart'; +import 'package:omemo_dart/src/key.dart'; + +/// Performs X25519 with [kp] and [pk]. If [identityKey] is set, then +/// it indicates which of [kp] ([identityKey] == 1) or [pk] ([identityKey] == 2) +/// is the identity key. This is needed since the identity key pair/public key is +/// an Ed25519 key, but we need them as X25519 keys for DH. +Future> omemoDH(OmemoKeyPair kp, OmemoPublicKey pk, int identityKey) async { + var ckp = kp; + var cpk = pk; + + if (identityKey == 1) { + ckp = await kp.toCurve25519(); + } else if (identityKey == 2) { + cpk = await pk.toCurve25519(); + } + + final shared = await Cryptography.instance.x25519().sharedSecretKey( + keyPair: await ckp.asKeyPair(), + remotePublicKey: cpk.asPublicKey(), + ); + + return shared.extractBytes(); +} diff --git a/lib/src/double_ratchet.dart b/lib/src/double_ratchet.dart index 55ac590..f9dd156 100644 --- a/lib/src/double_ratchet.dart +++ b/lib/src/double_ratchet.dart @@ -1,12 +1,12 @@ import 'package:cryptography/cryptography.dart'; import 'package:meta/meta.dart'; import 'package:omemo_dart/protobuf/schema.pb.dart'; +import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/double_ratchet/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/key.dart'; -import 'package:omemo_dart/src/x3dh.dart'; /// Amount of messages we may skip per session const maxSkip = 1000; @@ -78,7 +78,7 @@ class OmemoDoubleRatchet { static Future initiateNewSession(OmemoPublicKey spk, List sk, List ad) async { final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final dhr = spk; - final rk = await kdfRk(sk, await dh(dhs, dhr, 0)); + final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0)); final cks = rk; return OmemoDoubleRatchet( @@ -148,11 +148,11 @@ class OmemoDoubleRatchet { nr = 0; dhr = OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519); - final newRk = await kdfRk(rk, await dh(dhs, dhr!, 0)); + final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0)); rk = newRk; ckr = newRk; dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); - final newNewRk = await kdfRk(rk, await dh(dhs, dhr!, 0)); + final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0)); rk = newNewRk; cks = newNewRk; } diff --git a/lib/src/x3dh.dart b/lib/src/x3dh.dart index f674382..a81d895 100644 --- a/lib/src/x3dh.dart +++ b/lib/src/x3dh.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:math'; import 'package:cryptography/cryptography.dart'; import 'package:omemo_dart/src/bundle.dart'; +import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/helpers.dart'; import 'package:omemo_dart/src/key.dart'; @@ -47,28 +48,6 @@ Future> sig(OmemoKeyPair keyPair, List message) async { return signature.bytes; } -/// Performs X25519 with [kp] and [pk]. If [identityKey] is set, then -/// it indicates which of [kp] ([identityKey] == 1) or [pk] ([identityKey] == 2) -/// is the identity key. This is needed since the identity key pair/public key is -/// an Ed25519 key, but we need them as X25519 keys for DH. -Future> dh(OmemoKeyPair kp, OmemoPublicKey pk, int identityKey) async { - var ckp = kp; - var cpk = pk; - - if (identityKey == 1) { - ckp = await kp.toCurve25519(); - } else if (identityKey == 2) { - cpk = await pk.toCurve25519(); - } - - final shared = await Cryptography.instance.x25519().sharedSecretKey( - keyPair: await ckp.asKeyPair(), - remotePublicKey: cpk.asPublicKey(), - ); - - return shared.extractBytes(); -} - /// Derive a secret from the key material [km]. Future> kdf(List km) async { final f = List.filled(32, 0xFF); @@ -113,10 +92,10 @@ Future x3dhFromBundle(OmemoBundle bundle, OmemoKeyPair ik) asyn final opkId = bundle.opksEncoded.keys.elementAt(opkIndex); final opk = bundle.getOpk(opkId); - final dh1 = await dh(ik, bundle.spk, 1); - final dh2 = await dh(ek, bundle.ik, 2); - final dh3 = await dh(ek, bundle.spk, 0); - final dh4 = await dh(ek, opk, 0); + final dh1 = await omemoDH(ik, bundle.spk, 1); + final dh2 = await omemoDH(ek, bundle.ik, 2); + final dh3 = await omemoDH(ek, bundle.spk, 0); + final dh4 = await omemoDH(ek, opk, 0); final sk = await kdf(concat([dh1, dh2, dh3, dh4])); final ad = concat([ @@ -130,10 +109,10 @@ Future x3dhFromBundle(OmemoBundle bundle, OmemoKeyPair ik) asyn /// Bob builds the X3DH shared secret from the inital message [msg], the SPK [spk], the /// OPK [opk] that was selected by Alice and our IK [ik]. Returns the shared secret. Future x3dhFromInitialMessage(X3DHMessage msg, OmemoKeyPair spk, OmemoKeyPair opk, OmemoKeyPair ik) async { - final dh1 = await dh(spk, msg.ik, 2); - final dh2 = await dh(ik, msg.ek, 1); - final dh3 = await dh(spk, msg.ek, 0); - final dh4 = await dh(opk, msg.ek, 0); + final dh1 = await omemoDH(spk, msg.ik, 2); + final dh2 = await omemoDH(ik, msg.ek, 1); + final dh3 = await omemoDH(spk, msg.ek, 0); + final dh4 = await omemoDH(opk, msg.ek, 0); final sk = await kdf(concat([dh1, dh2, dh3, dh4])); final ad = concat([ diff --git a/lib/src/x3dh/x3dh.dart b/lib/src/x3dh/x3dh.dart new file mode 100644 index 0000000..f674382 --- /dev/null +++ b/lib/src/x3dh/x3dh.dart @@ -0,0 +1,145 @@ +import 'dart:convert'; +import 'dart:math'; +import 'package:cryptography/cryptography.dart'; +import 'package:omemo_dart/src/bundle.dart'; +import 'package:omemo_dart/src/errors.dart'; +import 'package:omemo_dart/src/helpers.dart'; +import 'package:omemo_dart/src/key.dart'; + +/// The overarching assumption is that we use Ed25519 keys for the identity keys +const omemoX3DHInfoString = 'OMEMO X3DH'; + +/// Performed by Alice +class X3DHAliceResult { + + const X3DHAliceResult(this.ek, this.sk, this.opkId, this.ad); + final OmemoKeyPair ek; + final List sk; + final String opkId; + final List ad; +} + +/// Received by Bob +class X3DHMessage { + + const X3DHMessage(this.ik, this.ek, this.opkId); + final OmemoPublicKey ik; + final OmemoPublicKey ek; + final String opkId; +} + +class X3DHBobResult { + + const X3DHBobResult(this.sk, this.ad); + final List sk; + final List ad; +} + +/// Sign [message] using the keypair [keyPair]. Note that [keyPair] must be +/// a Ed25519 keypair. +Future> sig(OmemoKeyPair keyPair, List message) async { + assert(keyPair.type == KeyPairType.ed25519, 'Signature keypair must be Ed25519'); + final signature = await Ed25519().sign( + message, + keyPair: await keyPair.asKeyPair(), + ); + + return signature.bytes; +} + +/// Performs X25519 with [kp] and [pk]. If [identityKey] is set, then +/// it indicates which of [kp] ([identityKey] == 1) or [pk] ([identityKey] == 2) +/// is the identity key. This is needed since the identity key pair/public key is +/// an Ed25519 key, but we need them as X25519 keys for DH. +Future> dh(OmemoKeyPair kp, OmemoPublicKey pk, int identityKey) async { + var ckp = kp; + var cpk = pk; + + if (identityKey == 1) { + ckp = await kp.toCurve25519(); + } else if (identityKey == 2) { + cpk = await pk.toCurve25519(); + } + + final shared = await Cryptography.instance.x25519().sharedSecretKey( + keyPair: await ckp.asKeyPair(), + remotePublicKey: cpk.asPublicKey(), + ); + + return shared.extractBytes(); +} + +/// Derive a secret from the key material [km]. +Future> kdf(List km) async { + final f = List.filled(32, 0xFF); + final input = List.empty(growable: true) + ..addAll(f) + ..addAll(km); + + final algorithm = Hkdf( + hmac: Hmac(Sha256()), + outputLength: 32, + ); + final output = await algorithm.deriveKey( + secretKey: SecretKey(input), + nonce: List.filled(32, 0x00), + info: utf8.encode(omemoX3DHInfoString), + ); + + return output.extractBytes(); +} + +/// Alice builds a session with Bob using his bundle [bundle] and Alice's identity key +/// pair [ik]. +Future x3dhFromBundle(OmemoBundle bundle, OmemoKeyPair ik) async { + // Check the signature first + final signatureValue = await Ed25519().verify( + await bundle.spk.getBytes(), + signature: Signature( + bundle.spkSignature, + publicKey: bundle.ik.asPublicKey(), + ), + ); + + if (!signatureValue) { + throw InvalidSignatureException(); + } + + // Generate EK + final ek = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); + + final random = Random.secure(); + final opkIndex = random.nextInt(bundle.opksEncoded.length); + final opkId = bundle.opksEncoded.keys.elementAt(opkIndex); + final opk = bundle.getOpk(opkId); + + final dh1 = await dh(ik, bundle.spk, 1); + final dh2 = await dh(ek, bundle.ik, 2); + final dh3 = await dh(ek, bundle.spk, 0); + final dh4 = await dh(ek, opk, 0); + + final sk = await kdf(concat([dh1, dh2, dh3, dh4])); + final ad = concat([ + await ik.pk.getBytes(), + await bundle.ik.getBytes(), + ]); + + return X3DHAliceResult(ek, sk, opkId, ad); +} + +/// Bob builds the X3DH shared secret from the inital message [msg], the SPK [spk], the +/// OPK [opk] that was selected by Alice and our IK [ik]. Returns the shared secret. +Future x3dhFromInitialMessage(X3DHMessage msg, OmemoKeyPair spk, OmemoKeyPair opk, OmemoKeyPair ik) async { + final dh1 = await dh(spk, msg.ik, 2); + final dh2 = await dh(ik, msg.ek, 1); + final dh3 = await dh(spk, msg.ek, 0); + final dh4 = await dh(opk, msg.ek, 0); + + final sk = await kdf(concat([dh1, dh2, dh3, dh4])); + final ad = concat([ + await msg.ik.getBytes(), + await ik.pk.getBytes(), + ]); + + return X3DHBobResult(sk, ad); +}