From dd96e840d4a8a38d663b429ebbed65831851725e Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Mon, 8 Aug 2022 14:26:25 +0200 Subject: [PATCH] feat: Allow calculating fingerprints --- lib/omemo_dart.dart | 1 + lib/src/double_ratchet/double_ratchet.dart | 33 +++++++++++++++++----- lib/src/omemo/fingerprint.dart | 19 +++++++++++++ lib/src/omemo/sessionmanager.dart | 28 +++++++++++++++++- pubspec.yaml | 1 + test/double_ratchet_test.dart | 2 ++ test/omemo_test.dart | 10 +++++++ 7 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 lib/src/omemo/fingerprint.dart diff --git a/lib/omemo_dart.dart b/lib/omemo_dart.dart index 2ba852d..1d92313 100644 --- a/lib/omemo_dart.dart +++ b/lib/omemo_dart.dart @@ -7,5 +7,6 @@ export 'src/helpers.dart'; export 'src/keys.dart'; export 'src/omemo/bundle.dart'; export 'src/omemo/device.dart'; +export 'src/omemo/fingerprint.dart'; export 'src/omemo/sessionmanager.dart'; export 'src/x3dh/x3dh.dart'; diff --git a/lib/src/double_ratchet/double_ratchet.dart b/lib/src/double_ratchet/double_ratchet.dart index fcfd666..0469446 100644 --- a/lib/src/double_ratchet/double_ratchet.dart +++ b/lib/src/double_ratchet/double_ratchet.dart @@ -65,8 +65,9 @@ class OmemoDoubleRatchet { this.ns, // Ns this.nr, // Nr this.pn, // Pn + this.ik, this.sessionAd, - this.mkSkipped, + this.mkSkipped, // MKSKIPPED ); factory OmemoDoubleRatchet.fromJson(Map data) { @@ -81,6 +82,7 @@ class OmemoDoubleRatchet { 'ns': 0, 'nr': 0, 'pn': 0, + 'ik_pub': 'base/64/encoded', 'session_ad': 'base/64/encoded', 'mkskipped': [ { @@ -110,6 +112,10 @@ class OmemoDoubleRatchet { data['ns']! as int, data['nr']! as int, data['pn']! as int, + OmemoPublicKey.fromBytes( + base64.decode(data['ik_pub']! as String), + KeyPairType.ed25519, + ), base64.decode(data['session_ad']! as String), mkSkipped, ); @@ -135,14 +141,18 @@ class OmemoDoubleRatchet { /// 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; + final List sessionAd; final Map> mkSkipped; /// 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. - static Future initiateNewSession(OmemoPublicKey spk, List sk, List ad) async { + /// a X3DH. [ik] refers to Bob's (the receiver's) IK public key. + static Future initiateNewSession(OmemoPublicKey spk, OmemoPublicKey ik, List sk, List ad) async { final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final dhr = spk; final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0)); @@ -157,6 +167,7 @@ class OmemoDoubleRatchet { 0, 0, 0, + ik, ad, {}, ); @@ -164,8 +175,9 @@ class OmemoDoubleRatchet { /// 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. - static Future acceptNewSession(OmemoKeyPair spk, List sk, List ad) async { + /// 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, OmemoPublicKey ik, List sk, List ad) async { return OmemoDoubleRatchet( spk, null, @@ -175,6 +187,7 @@ class OmemoDoubleRatchet { 0, 0, 0, + ik, ad, {}, ); @@ -199,6 +212,7 @@ class OmemoDoubleRatchet { 'ns': ns, 'nr': nr, 'pn': pn, + 'ik_pub': base64.encode(await ik.getBytes()), 'session_ad': base64.encode(sessionAd), 'mkskipped': mkSkippedSerialised, }; @@ -300,9 +314,14 @@ class OmemoDoubleRatchet { final dhrMatch = dhr == null ? other.dhr == null : await dhr!.equals(other.dhr!); final ckrMatch = ckr == null ? other.ckr == null : listsEqual(ckr!, other.ckr!); final cksMatch = cks == null ? other.cks == null : listsEqual(cks!, other.cks!); - + // ignore: invalid_use_of_visible_for_testing_member - return await dhs.equals(other.dhs) && + 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 && diff --git a/lib/src/omemo/fingerprint.dart b/lib/src/omemo/fingerprint.dart new file mode 100644 index 0000000..7c50326 --- /dev/null +++ b/lib/src/omemo/fingerprint.dart @@ -0,0 +1,19 @@ +import 'package:meta/meta.dart'; + +@immutable +class DeviceFingerprint { + + const DeviceFingerprint(this.deviceId, this.fingerprint); + final String fingerprint; + final int deviceId; + + @override + bool operator ==(Object other) { + return other is DeviceFingerprint && + fingerprint == other.fingerprint && + deviceId == other.deviceId; + } + + @override + int get hashCode => fingerprint.hashCode ^ deviceId.hashCode; +} diff --git a/lib/src/omemo/sessionmanager.dart b/lib/src/omemo/sessionmanager.dart index a7484bc..13db63e 100644 --- a/lib/src/omemo/sessionmanager.dart +++ b/lib/src/omemo/sessionmanager.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:cryptography/cryptography.dart'; +import 'package:hex/hex.dart'; import 'package:meta/meta.dart'; import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart'; @@ -11,6 +12,7 @@ import 'package:omemo_dart/src/helpers.dart'; import 'package:omemo_dart/src/keys.dart'; import 'package:omemo_dart/src/omemo/bundle.dart'; import 'package:omemo_dart/src/omemo/device.dart'; +import 'package:omemo_dart/src/omemo/fingerprint.dart'; import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart'; import 'package:omemo_dart/src/protobuf/omemo_key_exchange.dart'; import 'package:omemo_dart/src/protobuf/omemo_message.dart'; @@ -19,7 +21,6 @@ import 'package:synchronized/synchronized.dart'; /// The info used for when encrypting the AES key for the actual payload. const omemoPayloadInfoString = 'OMEMO Payload'; - @immutable class EncryptionResult { @@ -158,6 +159,7 @@ class OmemoSessionManager { ); final ratchet = await OmemoDoubleRatchet.initiateNewSession( bundle.spk, + bundle.ik, kexResult.sk, kexResult.ad, ); @@ -187,6 +189,7 @@ class OmemoSessionManager { ); final ratchet = await OmemoDoubleRatchet.acceptNewSession( device.spk, + OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519), kexResult.sk, kexResult.ad, ); @@ -327,6 +330,29 @@ class OmemoSessionManager { return utf8.decode(plaintext); } + /// Returns the list of hex-encoded fingerprints we have for sessions with [jid]. + Future> getHexFingerprintsForJid(String jid) async { + final fingerprints = List.empty(growable: true); + + await _lock.synchronized(() async { + // Get devices for jid + final devices = _deviceMap[jid]!; + + for (final deviceId in devices) { + final ratchet = _ratchetMap[RatchetMapKey(jid, deviceId)]!; + + fingerprints.add( + DeviceFingerprint( + deviceId, + HEX.encode(await ratchet.ik.getBytes()), + ), + ); + } + }); + + return fingerprints; + } + @visibleForTesting OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!; diff --git a/pubspec.yaml b/pubspec.yaml index 9533ac3..5e3dbeb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: collection: ^1.16.0 cryptography: ^2.0.5 + hex: ^0.2.0 pinenacl: ^0.5.1 synchronized: ^3.0.0+2 diff --git a/test/double_ratchet_test.dart b/test/double_ratchet_test.dart index 0953a5f..4dcbeaa 100644 --- a/test/double_ratchet_test.dart +++ b/test/double_ratchet_test.dart @@ -79,11 +79,13 @@ void main() { // Build a session final alicesRatchet = await OmemoDoubleRatchet.initiateNewSession( spkBob.pk, + ikBob.pk, resultAlice.sk, resultAlice.ad, ); final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession( spkBob, + ikAlice.pk, resultBob.sk, resultBob.ad, ); diff --git a/test/omemo_test.dart b/test/omemo_test.dart index 5654f3a..fcf8bd7 100644 --- a/test/omemo_test.dart +++ b/test/omemo_test.dart @@ -132,5 +132,15 @@ void main() { bobResponseMessage.encryptedKeys, ); expect(bobResponseText, aliceReceivedMessage); + + // Alice checks the fingerprints + final fingerprints = await aliceSession.getHexFingerprintsForJid(bobJid); + // Check that they the fingerprints are correct + expect(fingerprints.length, 2); + expect(fingerprints[0] != fingerprints[1], true); + // Check that those two calls do not throw an exception + aliceSession + ..getRatchet(bobJid, fingerprints[0].deviceId) + ..getRatchet(bobJid, fingerprints[1].deviceId); }); }