5 Commits

5 changed files with 220 additions and 6 deletions

View File

@@ -5,6 +5,6 @@ line-length=72
[title-trailing-punctuation]
[title-hard-tab]
[title-match-regex]
regex=^(feat|fix|test|release|chore|security|docs|refactor):.*$
regex=^(feat|fix|test|release|chore|security|docs|refactor|style):.*$
[body-trailing-whitespace]
[body-first-line-empty]

View File

@@ -67,6 +67,7 @@ class OmemoDoubleRatchet {
this.ik,
this.sessionAd,
this.mkSkipped, // MKSKIPPED
this.acknowledged,
);
factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) {
@@ -83,6 +84,7 @@ class OmemoDoubleRatchet {
'pn': 0,
'ik_pub': 'base/64/encoded',
'session_ad': 'base/64/encoded',
'acknowledged': true | false,
'mkskipped': [
{
'key': 'base/64/encoded',
@@ -117,6 +119,7 @@ class OmemoDoubleRatchet {
),
base64.decode(data['session_ad']! as String),
mkSkipped,
data['acknowledged']! as bool,
);
}
@@ -148,6 +151,10 @@ class OmemoDoubleRatchet {
final Map<SkippedKey, List<int>> mkSkipped;
/// Indicates whether we received an empty OMEMO message after building a session with
/// the device.
bool acknowledged;
/// 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. [ik] refers to Bob's (the receiver's) IK public key.
@@ -169,6 +176,7 @@ class OmemoDoubleRatchet {
ik,
ad,
{},
false,
);
}
@@ -189,6 +197,7 @@ class OmemoDoubleRatchet {
ik,
ad,
{},
true,
);
}
@@ -214,6 +223,7 @@ class OmemoDoubleRatchet {
'ik_pub': base64.encode(await ik.getBytes()),
'session_ad': base64.encode(sessionAd),
'mkskipped': mkSkippedSerialised,
'acknowledged': acknowledged,
};
}

View File

@@ -12,6 +12,14 @@ class RatchetModifiedEvent extends OmemoEvent {
final OmemoDoubleRatchet ratchet;
}
/// Triggered when a ratchet has been removed and should be removed from storage.
class RatchetRemovedEvent extends OmemoEvent {
RatchetRemovedEvent(this.jid, this.deviceId);
final String jid;
final int deviceId;
}
/// Triggered when the device map has been modified
class DeviceMapModifiedEvent extends OmemoEvent {

View File

@@ -50,6 +50,17 @@ class OmemoSessionManager {
);
}
/// Deserialise the OmemoSessionManager from JSON data [data] that does not contain
/// the ratchet sessions.
factory OmemoSessionManager.fromJsonWithoutSessions(Map<String, dynamic> data, Map<RatchetMapKey, OmemoDoubleRatchet> ratchetMap, TrustManager trustManager) {
return OmemoSessionManager(
Device.fromJson(data['device']! as Map<String, dynamic>),
data['devices']! as Map<String, List<int>>,
ratchetMap,
trustManager,
);
}
/// Generate a new cryptographic identity.
static Future<OmemoSessionManager> generateNewIdentity(String jid, TrustManager trustManager, { int opkAmount = 100 }) async {
assert(opkAmount > 0, 'opkAmount must be bigger than 0.');
@@ -83,6 +94,7 @@ class OmemoSessionManager {
/// A stream that receives events regarding the session
Stream<OmemoEvent> get eventStream => _eventStreamController.stream;
/// Returns our own device.
Future<Device> getDevice() async {
Device? dev;
await _deviceLock.synchronized(() async {
@@ -399,6 +411,53 @@ class OmemoSessionManager {
return map!;
}
/// Removes the ratchet identified by [jid] and [deviceId] from the session manager.
/// Also triggers events for commiting the new device map to storage and removing
/// the old ratchet.
Future<void> removeRatchet(String jid, int deviceId) async {
await _lock.synchronized(() async {
// Remove the ratchet
_ratchetMap.remove(RatchetMapKey(jid, deviceId));
// Commit it
_eventStreamController.add(RatchetRemovedEvent(jid, deviceId));
// Remove the device from jid
_deviceMap[jid]!.remove(deviceId);
if (_deviceMap[jid]!.isEmpty) {
_deviceMap.remove(jid);
}
// Commit it
_eventStreamController.add(DeviceMapModifiedEvent(_deviceMap));
});
}
/// Returns the list of device identifiers belonging to [jid] that are yet unacked, i.e.
/// we have not yet received an empty OMEMO message from.
Future<List<int>> getUnacknowledgedRatchets(String jid) async {
final ret = List<int>.empty(growable: true);
await _lock.synchronized(() async {
final devices = _deviceMap[jid]!;
for (final device in devices) {
final ratchet = _ratchetMap[RatchetMapKey(jid, device)]!;
if (!ratchet.acknowledged) ret.add(device);
}
});
return ret;
}
/// Mark the ratchet for device [deviceId] from [jid] as acked.
Future<void> ratchetAcknowledged(String jid, int deviceId) async {
await _lock.synchronized(() async {
final ratchet = _ratchetMap[RatchetMapKey(jid, deviceId)]!
..acknowledged = true;
// Commit it
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
});
}
@visibleForTesting
OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!;
@@ -423,7 +482,6 @@ class OmemoSessionManager {
},
...
],
'trust': { ... }
}
*/
@@ -439,8 +497,25 @@ class OmemoSessionManager {
'devices': _deviceMap,
'device': await (await getDevice()).toJson(),
'sessions': sessions,
// TODO(PapaTutuWawa): Implement
'trust': <String, dynamic>{},
};
}
/// Serialise the entire session manager into a JSON object.
Future<Map<String, dynamic>> toJsonWithoutSessions() async {
/*
{
'devices': {
'alice@...': [1, 2, ...],
'bob@...': [1],
...
},
'device': { ... },
}
*/
return {
'devices': _deviceMap,
'device': await (await getDevice()).toJson(),
};
}
}

View File

@@ -422,4 +422,125 @@ void main() {
expect(messageText, aliceReceivedMessage);
}
});
group('Test removing a ratchet', () {
test('Test removing a ratchet when the user has multiple', () async {
const aliceJid = 'alice@server.local';
const bobJid = 'bob@some.server.local';
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession1 = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession2 = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
// Alice sends a message to those two Bobs
await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await (await bobSession1.getDevice()).toBundle(),
await (await bobSession2.getDevice()).toBundle(),
],
);
// One of those two sessions is broken, so Alice removes the session2 ratchet
final id1 = (await bobSession1.getDevice()).id;
final id2 = (await bobSession2.getDevice()).id;
await aliceSession.removeRatchet(bobJid, id1);
final map = aliceSession.getRatchetMap();
expect(map.containsKey(RatchetMapKey(bobJid, id1)), false);
expect(map.containsKey(RatchetMapKey(bobJid, id2)), true);
final deviceMap = await aliceSession.getDeviceMap();
expect(deviceMap.containsKey(bobJid), true);
expect(deviceMap[bobJid], [id2]);
});
test('Test removing a ratchet when the user has only one', () async {
const aliceJid = 'alice@server.local';
const bobJid = 'bob@some.server.local';
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
// Alice sends a message to those two Bobs
await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await (await bobSession.getDevice()).toBundle(),
],
);
// One of those two sessions is broken, so Alice removes the session2 ratchet
final id = (await bobSession.getDevice()).id;
await aliceSession.removeRatchet(bobJid, id);
final map = aliceSession.getRatchetMap();
expect(map.containsKey(RatchetMapKey(bobJid, id)), false);
final deviceMap = await aliceSession.getDeviceMap();
expect(deviceMap.containsKey(bobJid), false);
});
});
test('Test acknowledging a ratchet', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
// Alice sends Bob a message
await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await (await bobSession.getDevice()).toBundle(),
],
);
expect(
await aliceSession.getUnacknowledgedRatchets(bobJid),
[
(await bobSession.getDevice()).id,
],
);
// Bob sends alice an empty message
// ...
// Alice decrypts it
// ...
// Alice marks the ratchet as acknowledged
await aliceSession.ratchetAcknowledged(bobJid, (await bobSession.getDevice()).id);
expect(
(await aliceSession.getUnacknowledgedRatchets(bobJid)).isEmpty,
true,
);
});
}