From fdc3985a8d07910a80f1c6cb0fb422b1b3893000 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 5 Aug 2022 18:14:10 +0200 Subject: [PATCH] feat: Serialise the Double Ratchet --- lib/src/double_ratchet/double_ratchet.dart | 97 +++++++++++++++++++++- lib/src/double_ratchet/helpers.dart | 18 ++++ lib/src/omemo/sessionmanager.dart | 7 +- test/serialisation_test.dart | 48 ++++++++++- 4 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 lib/src/double_ratchet/helpers.dart diff --git a/lib/src/double_ratchet/double_ratchet.dart b/lib/src/double_ratchet/double_ratchet.dart index f0d2746..de3e113 100644 --- a/lib/src/double_ratchet/double_ratchet.dart +++ b/lib/src/double_ratchet/double_ratchet.dart @@ -1,7 +1,9 @@ +import 'dart:convert'; import 'package:cryptography/cryptography.dart'; import 'package:meta/meta.dart'; import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/double_ratchet/crypto.dart'; +import 'package:omemo_dart/src/double_ratchet/helpers.dart'; import 'package:omemo_dart/src/double_ratchet/kdf.dart'; import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/helpers.dart'; @@ -22,9 +24,27 @@ class RatchetStep { class SkippedKey { const SkippedKey(this.dh, this.n); + + factory SkippedKey.fromJson(Map data) { + return SkippedKey( + OmemoPublicKey.fromBytes( + base64.decode(data['public']! as String), + KeyPairType.x25519, + ), + data['n']! as int, + ); + } + final OmemoPublicKey dh; final int n; + Future> toJson() async { + return { + 'public': base64.encode(await dh.getBytes()), + 'n': n, + }; + } + @override bool operator ==(Object other) { return other is SkippedKey && other.dh == dh && other.n == n; @@ -46,8 +66,55 @@ class OmemoDoubleRatchet { this.nr, // Nr this.pn, // Pn this.sessionAd, + this.mkSkipped, ); - + + factory OmemoDoubleRatchet.fromJson(Map data) { + /* + { + 'dhs': 'base/64/encoded', + 'dhs_pub': 'base/64/encoded', + 'dhr': null | 'base/64/encoded', + 'rk': 'base/64/encoded', + 'cks': null | 'base/64/encoded', + 'ckr': null | 'base/64/encoded', + 'ns': 0, + 'nr': 0, + 'pn': 0, + 'session_ad': 'base/64/encoded', + 'mkskipped': [ + { + 'key': 'base/64/encoded', + 'public': 'base/64/encoded', + 'n': 0 + }, ... + ] + } + */ + final mkSkipped = >{}; + for (final entry in data['mkskipped']! as List>) { + final key = SkippedKey.fromJson(entry); + mkSkipped[key] = base64.decode(entry['key']! as String); + } + + return OmemoDoubleRatchet( + OmemoKeyPair.fromBytes( + base64.decode(data['dhs_pub']! as String), + base64.decode(data['dhs']! as String), + KeyPairType.x25519, + ), + decodeKeyIfNotNull(data, 'dhr', KeyPairType.x25519), + base64.decode(data['rk']! as String), + base64DecodeIfNotNull(data, 'cks'), + base64DecodeIfNotNull(data, 'ckr'), + data['ns']! as int, + data['nr']! as int, + data['pn']! as int, + base64.decode(data['session_ad']! as String), + mkSkipped, + ); + } + /// Sending DH keypair OmemoKeyPair dhs; @@ -70,7 +137,7 @@ class OmemoDoubleRatchet { final List sessionAd; - final Map> mkSkipped = {}; + 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 @@ -91,6 +158,7 @@ class OmemoDoubleRatchet { 0, 0, ad, + {}, ); } @@ -108,8 +176,33 @@ class OmemoDoubleRatchet { 0, 0, ad, + {}, ); } + + Future> toJson() async { + final mkSkippedSerialised = List>.empty(growable: true); + for (final entry in mkSkipped.entries) { + final result = await entry.key.toJson(); + result['key'] = base64.encode(entry.value); + + mkSkippedSerialised.add(result); + } + + return { + 'dhs': base64.encode(await dhs.sk.getBytes()), + 'dhs_pub': base64.encode(await dhs.pk.getBytes()), + 'dhr': dhr != null ? base64.encode(await dhr!.getBytes()) : null, + 'rk': base64.encode(rk), + 'cks': cks != null ? base64.encode(cks!) : null, + 'ckr': ckr != null ? base64.encode(ckr!) : null, + 'ns': ns, + 'nr': nr, + 'pn': pn, + 'session_ad': base64.encode(sessionAd), + 'mkskipped': mkSkippedSerialised, + }; + } Future?> _trySkippedMessageKeys(OmemoMessage header, List ciphertext) async { final key = SkippedKey( diff --git a/lib/src/double_ratchet/helpers.dart b/lib/src/double_ratchet/helpers.dart new file mode 100644 index 0000000..7b7a2b2 --- /dev/null +++ b/lib/src/double_ratchet/helpers.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; +import 'package:cryptography/cryptography.dart'; +import 'package:omemo_dart/src/keys.dart'; + +OmemoPublicKey? decodeKeyIfNotNull(Map map, String key, KeyPairType type) { + if (map[key] == null) return null; + + return OmemoPublicKey.fromBytes( + base64.decode(map[key]! as String), + type, + ); +} + +List? base64DecodeIfNotNull(Map map, String key) { + if (map[key] == null) return null; + + return base64.decode(map[key]! as String); +} diff --git a/lib/src/omemo/sessionmanager.dart b/lib/src/omemo/sessionmanager.dart index fae4bed..d6ca264 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:meta/meta.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'; @@ -58,8 +59,9 @@ class OmemoSessionManager { /// Lock for _ratchetMap and _bundleMap final Lock _lock; - + /// Mapping of the Device Id to its OMEMO session + // TODO(PapaTutuWawa): Make this map use a tuple (Jid, Id) as a key final Map _ratchetMap; /// Mapping of a bare Jid to its Device Ids @@ -274,4 +276,7 @@ class OmemoSessionManager { final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv); return utf8.decode(plaintext); } + + @visibleForTesting + OmemoDoubleRatchet getRatchet(int deviceId) => _ratchetMap[deviceId]!; } diff --git a/test/serialisation_test.dart b/test/serialisation_test.dart index ac4955b..986aafc 100644 --- a/test/serialisation_test.dart +++ b/test/serialisation_test.dart @@ -2,7 +2,7 @@ import 'package:omemo_dart/omemo_dart.dart'; import 'package:test/test.dart'; void main() { - test('Test serialising and deserialising Device', () async { + test('Test serialising and deserialising the Device', () async { // Generate a random session final oldSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); final oldDevice = await oldSession.getDevice(); @@ -21,4 +21,50 @@ void main() { expect(await newDevice.opks[entry.key]!.equals(entry.value), true); } }); + + test('Test serialising and deserialising the OmemoDoubleRatchet', () async { + // Generate a random ratchet + const aliceJid = 'alice@server.example'; + const bobJid = 'bob@other.server.example'; + final aliceSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); + final bobSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 1); + final aliceMessage = await aliceSession.encryptToJid( + bobJid, + 'Hello Bob!', + newSessions: [ + await (await bobSession.getDevice()).toBundle(), + ], + ); + await bobSession.decryptMessage( + aliceMessage.ciphertext, + aliceJid, + (await aliceSession.getDevice()).id, + aliceMessage.encryptedKeys, + ); + final aliceOld = aliceSession.getRatchet((await bobSession.getDevice()).id); + final aliceSerialised = await aliceOld.toJson(); + final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised); + + expect(await aliceOld.dhs.equals(aliceNew.dhs), true); + if (aliceOld.dhr == null) { + expect(aliceNew.dhr, null); + } else { + expect(await aliceOld.dhr!.equals(aliceNew.dhr!), true); + } + expect(listsEqual(aliceOld.rk, aliceNew.rk), true); + if (aliceOld.cks == null) { + expect(aliceNew.cks, null); + } else { + expect(listsEqual(aliceOld.cks!, aliceNew.cks!), true); + } + if (aliceOld.ckr == null) { + expect(aliceNew.ckr, null); + } else { + expect(listsEqual(aliceOld.ckr!, aliceNew.ckr!), true); + } + expect(aliceOld.ns, aliceNew.ns); + expect(aliceOld.nr, aliceNew.nr); + expect(aliceOld.pn, aliceNew.pn); + expect(listsEqual(aliceOld.sessionAd, aliceNew.sessionAd), true); + }); }