feat: Allow serialising and deserialising OmemoSessionManager

This commit is contained in:
PapaTutuWawa 2022-08-06 12:24:26 +02:00
parent 3b8ccfaccf
commit 859f25d867
4 changed files with 144 additions and 38 deletions

View File

@ -293,4 +293,23 @@ class OmemoDoubleRatchet {
return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
}
@visibleForTesting
Future<bool> 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);
}
}

View File

@ -166,4 +166,31 @@ class Device {
'opks': serialisedOpks,
};
}
@visibleForTesting
Future<bool> 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;
}
}

View File

@ -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<OmemoEvent>.broadcast();
/// Deserialise the OmemoSessionManager from JSON data [data].
factory OmemoSessionManager.fromJson(Map<String, dynamic> data) {
final ratchetMap = <RatchetMapKey, OmemoDoubleRatchet>{};
for (final rawRatchet in data['sessions']! as List<Map<String, dynamic>>) {
final key = RatchetMapKey(rawRatchet['jid']! as String, rawRatchet['deviceId']! as int);
final ratchet = OmemoDoubleRatchet.fromJson(rawRatchet['ratchet']! as Map<String, dynamic>);
ratchetMap[key] = ratchet;
}
// TODO(PapaTutuWawa): Handle Trust behaviour
return OmemoSessionManager(
Device.fromJson(data['device']! as Map<String, dynamic>),
data['devices']! as Map<String, List<int>>,
ratchetMap,
);
}
/// Generate a new cryptographic identity.
static Future<OmemoSessionManager> 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<OmemoKeyExchange> _addSessionFromBundle(String jid, int deviceId, OmemoBundle bundle) async {
@visibleForTesting
Future<OmemoKeyExchange> 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 = <int, OmemoKeyExchange>{};
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<String, List<int>> getDeviceMap() => _deviceMap;
@visibleForTesting
Map<RatchetMapKey, OmemoDoubleRatchet> getRatchetMap() => _ratchetMap;
/// Serialise the entire session manager into a JSON object.
Future<Map<String, dynamic>> toJson() async {
/*
{
'devices': {
'alice@...': [1, 2, ...],
'bob@...': [1],
...
},
'device': { ... },
'sessions': [
{
'jid': 'alice@...',
'deviceId': 1,
'ratchet': { ... },
},
...
],
'trust': { ... }
}
*/
final sessions = List<Map<String, dynamic>>.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': <String, dynamic>{},
};
}
}

View File

@ -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);
});
}