From 859f25d8670f6aa0e33a4f2abd20e086076879b8 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Sat, 6 Aug 2022 12:24:26 +0200 Subject: [PATCH] feat: Allow serialising and deserialising OmemoSessionManager --- lib/src/double_ratchet/double_ratchet.dart | 19 ++++++ lib/src/omemo/device.dart | 27 ++++++++ lib/src/omemo/sessionmanager.dart | 75 ++++++++++++++++++++-- test/serialisation_test.dart | 61 +++++++++--------- 4 files changed, 144 insertions(+), 38 deletions(-) diff --git a/lib/src/double_ratchet/double_ratchet.dart b/lib/src/double_ratchet/double_ratchet.dart index de3e113..fcfd666 100644 --- a/lib/src/double_ratchet/double_ratchet.dart +++ b/lib/src/double_ratchet/double_ratchet.dart @@ -293,4 +293,23 @@ class OmemoDoubleRatchet { return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd); } + + @visibleForTesting + Future equals(OmemoDoubleRatchet other) async { + // ignore: invalid_use_of_visible_for_testing_member + 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) && + dhrMatch && + listsEqual(rk, other.rk) && + cksMatch && + ckrMatch && + ns == other.ns && + nr == other.nr && + pn == other.pn && + listsEqual(sessionAd, other.sessionAd); + } } diff --git a/lib/src/omemo/device.dart b/lib/src/omemo/device.dart index 132634f..e9df85d 100644 --- a/lib/src/omemo/device.dart +++ b/lib/src/omemo/device.dart @@ -166,4 +166,31 @@ class Device { 'opks': serialisedOpks, }; } + + @visibleForTesting + Future equals(Device other) async { + var opksMatch = true; + if (opks.length != other.opks.length) { + opksMatch = false; + } else { + for (final entry in opks.entries) { + // ignore: invalid_use_of_visible_for_testing_member + final matches = await other.opks[entry.key]?.equals(entry.value) ?? false; + if (!matches) { + opksMatch = false; + } + } + } + + // ignore: invalid_use_of_visible_for_testing_member + final ikMatch = await ik.equals(other.ik); + // ignore: invalid_use_of_visible_for_testing_member + final spkMatch = await spk.equals(other.spk); + return id == other.id && + ikMatch && + spkMatch && + listsEqual(spkSignature, other.spkSignature) && + spkId == other.spkId && + opksMatch; + } } diff --git a/lib/src/omemo/sessionmanager.dart b/lib/src/omemo/sessionmanager.dart index 15ae4dc..d025827 100644 --- a/lib/src/omemo/sessionmanager.dart +++ b/lib/src/omemo/sessionmanager.dart @@ -60,19 +60,34 @@ class RatchetMapKey { class OmemoSessionManager { - OmemoSessionManager(this._device) - : _ratchetMap = {}, - _deviceMap = {}, - _lock = Lock(), + OmemoSessionManager(this._device, this._deviceMap, this._ratchetMap) + : _lock = Lock(), _deviceLock = Lock(), _eventStreamController = StreamController.broadcast(); + /// Deserialise the OmemoSessionManager from JSON data [data]. + factory OmemoSessionManager.fromJson(Map data) { + final ratchetMap = {}; + for (final rawRatchet in data['sessions']! as List>) { + final key = RatchetMapKey(rawRatchet['jid']! as String, rawRatchet['deviceId']! as int); + final ratchet = OmemoDoubleRatchet.fromJson(rawRatchet['ratchet']! as Map); + ratchetMap[key] = ratchet; + } + + // TODO(PapaTutuWawa): Handle Trust behaviour + return OmemoSessionManager( + Device.fromJson(data['device']! as Map), + data['devices']! as Map>, + ratchetMap, + ); + } + /// Generate a new cryptographic identity. static Future generateNewIdentity({ int opkAmount = 100 }) async { assert(opkAmount > 0, 'opkAmount must be bigger than 0.'); final device = await Device.generateNewDevice(opkAmount: opkAmount); - return OmemoSessionManager(device); + return OmemoSessionManager(device, {}, {}); } /// Lock for _ratchetMap and _bundleMap @@ -128,7 +143,8 @@ class OmemoSessionManager { /// Create a ratchet session initiated by Alice to the user with Jid [jid] and the device /// [deviceId] from the bundle [bundle]. - Future _addSessionFromBundle(String jid, int deviceId, OmemoBundle bundle) async { + @visibleForTesting + Future addSessionFromBundle(String jid, int deviceId, OmemoBundle bundle) async { final device = await getDevice(); final kexResult = await x3dhFromBundle( bundle, @@ -191,7 +207,7 @@ class OmemoSessionManager { final kex = {}; if (newSessions != null) { for (final newSession in newSessions) { - kex[newSession.id] = await _addSessionFromBundle(jid, newSession.id, newSession); + kex[newSession.id] = await addSessionFromBundle(jid, newSession.id, newSession); } } @@ -299,4 +315,49 @@ class OmemoSessionManager { @visibleForTesting OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!; + + @visibleForTesting + Map> getDeviceMap() => _deviceMap; + + @visibleForTesting + Map getRatchetMap() => _ratchetMap; + + /// Serialise the entire session manager into a JSON object. + Future> toJson() async { + /* + { + 'devices': { + 'alice@...': [1, 2, ...], + 'bob@...': [1], + ... + }, + 'device': { ... }, + 'sessions': [ + { + 'jid': 'alice@...', + 'deviceId': 1, + 'ratchet': { ... }, + }, + ... + ], + 'trust': { ... } + } + */ + + final sessions = List>.empty(growable: true); + for (final entry in _ratchetMap.entries) { + sessions.add({ + 'jid': entry.key.jid, + 'deviceId': entry.key.deviceId, + 'ratchet': await entry.value.toJson(), + }); + } + return { + 'devices': _deviceMap, + 'device': await (await getDevice()).toJson(), + 'sessions': sessions, + // TODO(PapaTutuWawa): Implement + 'trust': {}, + }; + } } diff --git a/test/serialisation_test.dart b/test/serialisation_test.dart index 503b1b3..16b9af1 100644 --- a/test/serialisation_test.dart +++ b/test/serialisation_test.dart @@ -9,17 +9,7 @@ void main() { final serialised = await oldDevice.toJson(); final newDevice = Device.fromJson(serialised); - expect(oldDevice.id, newDevice.id); - expect(await oldDevice.ik.equals(newDevice.ik), true); - expect(await oldDevice.spk.equals(newDevice.spk), true); - expect(listsEqual(oldDevice.spkSignature, newDevice.spkSignature), true); - expect(oldDevice.spkId, newDevice.spkId); - - // Check the Ontime-Prekeys - expect(oldDevice.opks.length, newDevice.opks.length); - for (final entry in oldDevice.opks.entries) { - expect(await newDevice.opks[entry.key]!.equals(entry.value), true); - } + expect(await oldDevice.equals(newDevice), true); }); test('Test serialising and deserialising the OmemoDoubleRatchet', () async { @@ -45,26 +35,35 @@ void main() { 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(await aliceOld.equals(aliceNew), true); + }); + + test('Test serialising and deserialising the OmemoSessionManager', () async { + // Generate a random session + final oldSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 4); + final bobSession = await OmemoSessionManager.generateNewIdentity(opkAmount: 4); + await oldSession.addSessionFromBundle( + 'bob@localhost', + (await bobSession.getDevice()).id, + await (await bobSession.getDevice()).toBundle(), + ); + + // Serialise and deserialise + final serialised = await oldSession.toJson(); + final newSession = OmemoSessionManager.fromJson(serialised); + + final oldDevice = await oldSession.getDevice(); + final newDevice = await newSession.getDevice(); + expect(await oldDevice.equals(newDevice), true); + expect(oldSession.getDeviceMap(), newSession.getDeviceMap()); + + expect(oldSession.getRatchetMap().length, newSession.getRatchetMap().length); + for (final session in oldSession.getRatchetMap().entries) { + expect(newSession.getRatchetMap().containsKey(session.key), true); + + final oldRatchet = oldSession.getRatchetMap()[session.key]!; + final newRatchet = newSession.getRatchetMap()[session.key]!; + expect(await oldRatchet.equals(newRatchet), 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); }); }