20 Commits

Author SHA1 Message Date
1e7f66ccc3 release: Bump to 0.3.0 2022-09-15 13:51:18 +02:00
0480e9156f fix: Fix occurence of not using synchronized's return 2022-09-15 13:46:52 +02:00
96d9c55c87 fix: Make fromJson* functions work when reading JSON from a String 2022-09-15 13:41:33 +02:00
49c847a96b feat: Remove toJson and fromJson from OmemoSessionManager 2022-09-15 13:22:50 +02:00
cf5331a026 feat: Introduce logging for logging purposes 2022-09-15 13:17:30 +02:00
c1d8073af0 refactor: Remove BTBV's loadState method 2022-09-15 13:06:14 +02:00
438012d8f8 fix: Hopefully fix all tests being flaky
It seems that the varint encoding function would not work for
some integers as input. This should in theory fix this issue. Since
the SPK IDs are randomly between 0 and 2**32 - 1, it makes sense that
the tests fail only sometimes.
2022-09-14 23:50:54 +02:00
79704da99c fix: Fix issues with the maps being unmodifiable 2022-09-14 22:02:50 +02:00
4341797f14 fix: Commit the restored ratchet 2022-09-14 22:02:35 +02:00
c5c579810e fix: Restore the ratchets in case of an error
This means that if the ratchet fails to decrypt a message, from the
outside it will be as if that one message had never been received.
Thus, the ratchet can be used normally. This is to guard against
messages that are received again.
2022-09-14 21:58:41 +02:00
8991599a0b feat: Allow removing all ratchets for a given Jid 2022-09-11 17:26:54 +02:00
dad938b0e1 feat: Allow initializing the BTBV trust manager in the constructor 2022-09-11 13:43:07 +02:00
ff52c82039 feat: Help with serializing and deserializing the BTVT manager 2022-09-11 13:33:45 +02:00
12e09947f6 feat: Implement enabling and disabling devices 2022-09-11 12:34:31 +02:00
d530358359 chore: Lower meta's minimum version 2022-09-09 17:40:16 +02:00
0e370a8e19 refactor: Use synchronized's return 2022-09-09 17:39:28 +02:00
b6aa28e1ee release: Bump version 2022-08-19 17:00:44 +02:00
2e10842c54 feat: Make accepted ratchets unacknowledged by default 2022-08-19 16:59:24 +02:00
0e2af1f2a3 feat: Add a function to check if a ratchet is acknowledged 2022-08-19 16:58:23 +02:00
80e1b20f27 fix: Fix crash when calling getUnacknowledgedRatchets for a new Jid 2022-08-18 16:21:53 +02:00
15 changed files with 537 additions and 148 deletions

View File

@@ -15,3 +15,18 @@
- Add convenience functions `getDeviceId` and `getDeviceBundle`
- Creating a new ratchet with an id for which we already have a ratchet will now overwrite the old ratchet
- Ratchet now carry an "acknowledged" attribute
## 0.2.1
- Add `isRatchetAcknowledged`
- Ratchets that are created due to accepting a kex are now unacknowledged
## 0.3.0
- Implement enabling and disabling ratchets via the TrustManager interface
- Fix deserialization of the various objects
- Remove the BTBV TrustManager's loadState method. Just use the constructor
- Allow removing all ratchets for a given Jid
- If an error occurs while decrypting the message, the ratchet will now be reset to its prior state
- Fix a bug within the Varint encoding function. This should fix some occasional UnknownSignedPrekeyExceptions
- Remove OmemoSessionManager's toJson and fromJson. Use toJsonWithoutSessions and fromJsonWithoutSessions. Restoring sessions is not out-of-scope for that function

View File

@@ -94,11 +94,20 @@ class OmemoDoubleRatchet {
]
}
*/
final mkSkipped = <SkippedKey, List<int>>{};
for (final entry in data['mkskipped']! as List<Map<String, dynamic>>) {
final key = SkippedKey.fromJson(entry);
mkSkipped[key] = base64.decode(entry['key']! as String);
}
// NOTE: Dart has some issues with just casting a List<dynamic> to List<Map<...>>, as
// such we need to convert the items by hand.
final mkSkipped = Map<SkippedKey, List<int>>.fromEntries(
(data['mkskipped']! as List<dynamic>).map<MapEntry<SkippedKey, List<int>>>(
(entry) {
final map = entry as Map<String, dynamic>;
final key = SkippedKey.fromJson(map);
return MapEntry(
key,
base64.decode(map['key']! as String),
);
},
),
);
return OmemoDoubleRatchet(
OmemoKeyPair.fromBytes(
@@ -197,7 +206,7 @@ class OmemoDoubleRatchet {
ik,
ad,
{},
true,
false,
);
}

View File

@@ -49,14 +49,23 @@ class Device {
]
}
*/
final opks = <int, OmemoKeyPair>{};
for (final opk in data['opks']! as List<Map<String, dynamic>>) {
opks[opk['id']! as int] = OmemoKeyPair.fromBytes(
base64.decode(opk['public']! as String),
base64.decode(opk['private']! as String),
// NOTE: Dart has some issues with just casting a List<dynamic> to List<Map<...>>, as
// such we need to convert the items by hand.
final opks = Map<int, OmemoKeyPair>.fromEntries(
(data['opks']! as List<dynamic>).map<MapEntry<int, OmemoKeyPair>>(
(opk) {
final map = opk as Map<String, dynamic>;
return MapEntry(
map['id']! as int,
OmemoKeyPair.fromBytes(
base64.decode(map['public']! as String),
base64.decode(map['private']! as String),
KeyPairType.x25519,
),
);
},
),
);
}
return Device(
data['jid']! as String,

View File

@@ -4,9 +4,24 @@ import 'package:meta/meta.dart';
class RatchetMapKey {
const RatchetMapKey(this.jid, this.deviceId);
factory RatchetMapKey.fromJsonKey(String key) {
final parts = key.split(':');
final deviceId = int.parse(parts.first);
return RatchetMapKey(
parts.sublist(1).join(':'),
deviceId,
);
}
final String jid;
final int deviceId;
String toJsonKey() {
return '$deviceId:$jid';
}
@override
bool operator ==(Object other) {
return other is RatchetMapKey && jid == other.jid && deviceId == other.deviceId;

View File

@@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:cryptography/cryptography.dart';
import 'package:hex/hex.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:omemo_dart/src/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart';
@@ -31,31 +32,28 @@ class OmemoSessionManager {
OmemoSessionManager(this._device, this._deviceMap, this._ratchetMap, this._trustManager)
: _lock = Lock(),
_deviceLock = Lock(),
_eventStreamController = StreamController<OmemoEvent>.broadcast();
/// Deserialise the OmemoSessionManager from JSON data [data].
factory OmemoSessionManager.fromJson(Map<String, dynamic> data, TrustManager trustManager) {
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;
}
return OmemoSessionManager(
Device.fromJson(data['device']! as Map<String, dynamic>),
data['devices']! as Map<String, List<int>>,
ratchetMap,
trustManager,
);
}
_eventStreamController = StreamController<OmemoEvent>.broadcast(),
_log = Logger('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) {
factory OmemoSessionManager.fromJsonWithoutSessions(
Map<String, dynamic> data,
Map<RatchetMapKey, OmemoDoubleRatchet> ratchetMap,
TrustManager trustManager,
) {
// NOTE: Dart has some issues with just casting a List<dynamic> to List<Map<...>>, as
// such we need to convert the items by hand.
return OmemoSessionManager(
Device.fromJson(data['device']! as Map<String, dynamic>),
data['devices']! as Map<String, List<int>>,
(data['devices']! as Map<String, dynamic>).map<String, List<int>>(
(key, value) {
return MapEntry(
key,
(value as List<dynamic>).map<int>((i) => i as int).toList(),
);
}
),
ratchetMap,
trustManager,
);
@@ -69,6 +67,9 @@ class OmemoSessionManager {
return OmemoSessionManager(device, {}, {}, trustManager);
}
/// Logging
Logger _log;
/// Lock for _ratchetMap and _bundleMap
final Lock _lock;
@@ -96,12 +97,7 @@ class OmemoSessionManager {
/// Returns our own device.
Future<Device> getDevice() async {
Device? dev;
await _deviceLock.synchronized(() async {
dev = _device;
});
return dev!;
return _deviceLock.synchronized(() => _device);
}
/// Returns the id attribute of our own device. This is just a short-hand for
@@ -176,16 +172,18 @@ class OmemoSessionManager {
Future<void> _addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async {
// Pick the correct SPK
final device = await getDevice();
OmemoKeyPair? spk;
if (kex.spkId == device.spkId) {
spk = device.spk;
} else if (kex.spkId == device.oldSpkId) {
spk = device.oldSpk;
} else {
throw UnknownSignedPrekeyException();
final spk = await _lock.synchronized(() async {
if (kex.spkId == _device.spkId) {
return _device.spk;
} else if (kex.spkId == _device.oldSpkId) {
return _device.oldSpk;
}
assert(spk != null, 'The used SPK must be found');
return null;
});
if (spk == null) {
throw UnknownSignedPrekeyException();
}
final kexResult = await x3dhFromInitialMessage(
X3DHMessage(
@@ -193,7 +191,7 @@ class OmemoSessionManager {
OmemoPublicKey.fromBytes(kex.ek!, KeyPairType.x25519),
kex.pkId!,
),
spk!,
spk,
device.opks.values.elementAt(kex.pkId!),
device.ik,
);
@@ -254,6 +252,9 @@ class OmemoSessionManager {
if (plaintext != null) {
// Only encrypt to devices that are trusted
if (!(await _trustManager.isTrusted(jid, deviceId))) continue;
// Onyl encrypt to devices that are enabled
if (!(await _trustManager.isEnabled(jid, deviceId))) continue;
}
final ratchetKey = RatchetMapKey(jid, deviceId);
@@ -294,6 +295,25 @@ class OmemoSessionManager {
);
}
/// In case a decryption error occurs, the Double Ratchet spec says to just restore
/// the ratchet to its old state. As such, this function restores the ratchet at
/// [mapKey] with [oldRatchet].
Future<void> _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) async {
await _lock.synchronized(() {
_log.finest('Restoring ratchet ${mapKey.jid}:${mapKey.deviceId}');
_ratchetMap[mapKey] = oldRatchet;
// Commit the ratchet
_eventStreamController.add(
RatchetModifiedEvent(
mapKey.jid,
mapKey.deviceId,
oldRatchet,
),
);
});
}
/// Attempt to decrypt [ciphertext]. [keys] refers to the <key /> elements inside the
/// <keys /> element with a "jid" attribute matching our own. [senderJid] refers to the
/// bare Jid of the sender. [senderDeviceId] refers to the "sid" attribute of the
@@ -311,9 +331,15 @@ class OmemoSessionManager {
throw NotEncryptedForDeviceException();
}
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
final decodedRawKey = base64.decode(rawKey.value);
OmemoAuthenticatedMessage authMessage;
OmemoDoubleRatchet? oldRatchet;
if (rawKey.kex) {
// If the ratchet already existed, we store it. If it didn't, oldRatchet will stay
// null.
oldRatchet = await _getRatchet(ratchetKey);
// TODO(PapaTutuWawa): Only do this when we should
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
await _addSessionFromKeyExchange(
@@ -344,31 +370,38 @@ class OmemoSessionManager {
}
final message = OmemoMessage.fromBuffer(authMessage.message!);
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
List<int>? keyAndHmac;
await _lock.synchronized(() async {
final ratchet = _ratchetMap[ratchetKey]!;
// We can guarantee that the ratchet exists at this point in time
final ratchet = (await _getRatchet(ratchetKey))!;
oldRatchet ??= ratchet ;
try {
if (rawKey.kex) {
keyAndHmac = await ratchet.ratchetDecrypt(message, authMessage.writeToBuffer());
} else {
keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey);
}
} on InvalidMessageHMACException {
await _restoreRatchet(ratchetKey, oldRatchet);
rethrow;
}
// Commit the ratchet
_eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet));
});
// Empty OMEMO messages should just have the key decrypted and/or session set up.
if (ciphertext == null) {
return null;
}
final key = keyAndHmac!.sublist(0, 32);
final hmac = keyAndHmac!.sublist(32, 48);
final key = keyAndHmac.sublist(0, 32);
final hmac = keyAndHmac.sublist(32, 48);
final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
final computedHmac = await truncatedHmac(ciphertext, derivedKeys.authenticationKey);
if (!listsEqual(hmac, computedHmac)) {
// TODO(PapaTutuWawa): I am unsure if we should restore the ratchet here
await _restoreRatchet(ratchetKey, oldRatchet);
throw InvalidMessageHMACException();
}
@@ -415,13 +448,7 @@ class OmemoSessionManager {
/// Returns the device map, i.e. the mapping of bare Jid to its device identifiers
/// we have built sessions with.
Future<Map<String, List<int>>> getDeviceMap() async {
Map<String, List<int>>? map;
await _lock.synchronized(() async {
map = _deviceMap;
});
return map!;
return _lock.synchronized(() => _deviceMap);
}
/// Removes the ratchet identified by [jid] and [deviceId] from the session manager.
@@ -444,20 +471,45 @@ class OmemoSessionManager {
});
}
/// Removes all ratchets for Jid [jid]. Triggers a DeviceMapModified event at the end and an
/// RatchetRemovedEvent for each ratchet.
Future<void> removeAllRatchets(String jid) async {
await _lock.synchronized(() async {
for (final deviceId in _deviceMap[jid]!) {
// Remove the ratchet
_ratchetMap.remove(RatchetMapKey(jid, deviceId));
// Commit it
_eventStreamController.add(RatchetRemovedEvent(jid, deviceId));
}
// Remove the device from jid
_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 {
Future<List<int>?> getUnacknowledgedRatchets(String jid) async {
return _lock.synchronized(() async {
final ret = List<int>.empty(growable: true);
final devices = _deviceMap[jid];
if (devices == null) return null;
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;
});
}
/// Returns true if the ratchet for [jid] with device identifier [deviceId] is
/// acknowledged. Returns false if not.
Future<bool> isRatchetAcknowledged(String jid, int deviceId) async {
return _lock.synchronized(() => _ratchetMap[RatchetMapKey(jid, deviceId)]!.acknowledged);
}
/// Mark the ratchet for device [deviceId] from [jid] as acked.
@@ -493,48 +545,18 @@ class OmemoSessionManager {
});
}
Future<OmemoDoubleRatchet?> _getRatchet(RatchetMapKey key) async {
return _lock.synchronized(() async {
return _ratchetMap[key];
});
}
@visibleForTesting
OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!;
@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': { ... },
},
...
],
}
*/
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,
};
}
/// Serialise the entire session manager into a JSON object.
Future<Map<String, dynamic>> toJsonWithoutSessions() async {
/*

View File

@@ -46,21 +46,18 @@ List<int> encodeVarint(int i) {
assert(i >= 0, "Two's complement is not implemented");
final ret = List<int>.empty(growable: true);
var j = 0;
while (true) {
// Thanks to https://github.com/hathibelagal-dev/LEB128 for the trick with toRadixString!
final numSevenBlocks = (i.toRadixString(2).length / 7).ceil();
for (var j = 0; j < numSevenBlocks; j++) {
// The 7 LSB of the byte we're creating
final x = (i & (lsb7Mask << j * 7)) >> j * 7;
// The next bits
final next = i & (lsb7Mask << (j + 1) * 7);
if (next == 0) {
if (j == numSevenBlocks - 1) {
// If we were to shift further, we only get zero, so we're at the end
ret.add(x);
break;
} else {
// We still have at least one bit more to go, so set the MSB to 1
ret.add(x + msb);
j++;
}
}

View File

@@ -11,4 +11,13 @@ class AlwaysTrustingTrustManager extends TrustManager {
@override
Future<void> onNewSession(String jid, int deviceId) async {}
@override
Future<bool> isEnabled(String jid, int deviceId) async => true;
@override
Future<void> setEnabled(String jid, int deviceId, bool enabled) async {}
@override
Future<Map<String, dynamic>> toJson() async => <String, dynamic>{};
}

View File

@@ -8,4 +8,15 @@ abstract class TrustManager {
/// Called by the OmemoSessionManager when a new session has been built. Should set
/// a default trust state to [jid]'s device with identifier [deviceId].
Future<void> onNewSession(String jid, int deviceId);
/// Return true if the device with id [deviceId] of Jid [jid] should be used for encryption.
/// If not, return false.
Future<bool> isEnabled(String jid, int deviceId);
/// Mark the device with id [deviceId] of Jid [jid] as enabled if [enabled] is true or as disabled
/// if [enabled] is false.
Future<void> setEnabled(String jid, int deviceId, bool enabled);
/// Serialize the trust manager to JSON.
Future<Map<String, dynamic>> toJson();
}

View File

@@ -8,24 +8,52 @@ import 'package:synchronized/synchronized.dart';
/// - blindTrust: The fingerprint is not verified using OOB means
/// - verified: The fingerprint has been verified using OOB means
enum BTBVTrustState {
notTrusted,
blindTrust,
verified,
notTrusted, // = 1
blindTrust, // = 2
verified, // = 3
}
int _trustToInt(BTBVTrustState state) {
switch (state) {
case BTBVTrustState.notTrusted: return 1;
case BTBVTrustState.blindTrust: return 2;
case BTBVTrustState.verified: return 3;
}
}
BTBVTrustState _trustFromInt(int i) {
switch (i) {
case 1: return BTBVTrustState.notTrusted;
case 2: return BTBVTrustState.blindTrust;
case 3: return BTBVTrustState.verified;
default: return BTBVTrustState.notTrusted;
}
}
/// A TrustManager that implements the idea of Blind Trust Before Verification.
/// See https://gultsch.de/trust.html for more details.
abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
BlindTrustBeforeVerificationTrustManager()
: trustCache = {},
devices = {},
BlindTrustBeforeVerificationTrustManager({
Map<RatchetMapKey, BTBVTrustState>? trustCache,
Map<RatchetMapKey, bool>? enablementCache,
Map<String, List<int>>? devices,
}) : trustCache = trustCache ?? {},
enablementCache = enablementCache ?? {},
devices = devices ?? {},
_lock = Lock();
/// The cache for Mapping a RatchetMapKey to its trust state
/// The cache for mapping a RatchetMapKey to its trust state
@visibleForTesting
@protected
final Map<RatchetMapKey, BTBVTrustState> trustCache;
/// The cache for mapping a RatchetMapKey to whether it is enabled or not
@visibleForTesting
@protected
final Map<RatchetMapKey, bool> enablementCache;
/// Mapping of Jids to their device identifiers
@visibleForTesting
@protected
final Map<String, List<int>> devices;
@@ -74,10 +102,13 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
@override
Future<void> onNewSession(String jid, int deviceId) async {
await _lock.synchronized(() async {
final key = RatchetMapKey(jid, deviceId);
if (_hasAtLeastOneVerifiedDevice(jid)) {
trustCache[RatchetMapKey(jid, deviceId)] = BTBVTrustState.notTrusted;
trustCache[key] = BTBVTrustState.notTrusted;
enablementCache[key] = false;
} else {
trustCache[RatchetMapKey(jid, deviceId)] = BTBVTrustState.blindTrust;
trustCache[key] = BTBVTrustState.blindTrust;
enablementCache[key] = true;
}
if (devices.containsKey(jid)) {
@@ -114,16 +145,75 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
});
}
@override
Future<bool> isEnabled(String jid, int deviceId) async {
return _lock.synchronized(() async {
final value = enablementCache[RatchetMapKey(jid, deviceId)];
if (value == null) return false;
return value;
});
}
@override
Future<void> setEnabled(String jid, int deviceId, bool enabled) async {
await _lock.synchronized(() async {
enablementCache[RatchetMapKey(jid, deviceId)] = enabled;
});
// Commit the state
await commitState();
}
@override
Future<Map<String, dynamic>> toJson() async {
return {
'devices': devices,
'trust': trustCache.map((key, value) => MapEntry(
key.toJsonKey(), _trustToInt(value),
),),
'enable': enablementCache.map((key, value) => MapEntry(key.toJsonKey(), value)),
};
}
/// From a serialized version of a BTBV trust manager, extract the device list.
/// NOTE: This is needed as Dart cannot just cast a List<dynamic> to List<int> and so on.
static Map<String, List<int>> deviceListFromJson(Map<String, dynamic> json) {
return (json['devices']! as Map<String, dynamic>).map<String, List<int>>(
(key, value) => MapEntry(
key,
(value as List<dynamic>).map<int>((i) => i as int).toList(),
),
);
}
/// From a serialized version of a BTBV trust manager, extract the trust cache.
/// NOTE: This is needed as Dart cannot just cast a List<dynamic> to List<int> and so on.
static Map<RatchetMapKey, BTBVTrustState> trustCacheFromJson(Map<String, dynamic> json) {
return (json['trust']! as Map<String, dynamic>).map<RatchetMapKey, BTBVTrustState>(
(key, value) => MapEntry(
RatchetMapKey.fromJsonKey(key),
_trustFromInt(value as int),
),
);
}
/// From a serialized version of a BTBV trust manager, extract the enable cache.
/// NOTE: This is needed as Dart cannot just cast a List<dynamic> to List<int> and so on.
static Map<RatchetMapKey, bool> enableCacheFromJson(Map<String, dynamic> json) {
return (json['enable']! as Map<String, dynamic>).map<RatchetMapKey, bool>(
(key, value) => MapEntry(
RatchetMapKey.fromJsonKey(key),
value as bool,
),
);
}
/// Called when the state of the trust manager has been changed. Allows the user to
/// commit the trust state to persistent storage.
@visibleForOverriding
Future<void> commitState();
/// Called when the user wants to restore the state of the trust manager. The format
/// and actual storage mechanism is left to the user.
@visibleForOverriding
Future<void> loadState();
@visibleForTesting
BTBVTrustState getDeviceTrust(String jid, int deviceId) => trustCache[RatchetMapKey(jid, deviceId)]!;
}
@@ -133,7 +223,4 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
class MemoryBTBVTrustManager extends BlindTrustBeforeVerificationTrustManager {
@override
Future<void> commitState() async {}
@override
Future<void> loadState() async {}
}

View File

@@ -11,4 +11,13 @@ class NeverTrustingTrustManager extends TrustManager {
@override
Future<void> onNewSession(String jid, int deviceId) async {}
@override
Future<bool> isEnabled(String jid, int deviceId) async => true;
@override
Future<void> setEnabled(String jid, int deviceId, bool enabled) async {}
@override
Future<Map<String, dynamic>> toJson() async => <String, dynamic>{};
}

View File

@@ -1,6 +1,6 @@
name: omemo_dart
description: An XMPP library independent OMEMO library
version: 0.2.0
version: 0.3.0
homepage: https://github.com/PapaTutuWawa/omemo_dart
publish_to: https://git.polynom.me/api/packages/PapaTutuWawa/pub
@@ -11,7 +11,8 @@ dependencies:
collection: ^1.16.0
cryptography: ^2.0.5
hex: ^0.2.0
meta: ^1.8.0
logging: ^1.0.2
meta: ^1.7.0
pinenacl: ^0.5.1
synchronized: ^3.0.0+2

View File

@@ -1,9 +1,17 @@
import 'package:logging/logging.dart';
import 'package:omemo_dart/omemo_dart.dart';
import 'package:omemo_dart/src/trust/always.dart';
import 'package:omemo_dart/src/trust/never.dart';
import 'package:test/test.dart';
void main() {
Logger.root
..level = Level.ALL
..onRecord.listen((record) {
// ignore: avoid_print
print('${record.level.name}: ${record.message}');
});
test('Test using OMEMO sessions with only one device per user', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
@@ -220,7 +228,7 @@ void main() {
expect(messagePlaintext, aliceMessage2);
});
test('Test using sending empty OMEMO messages', () async {
test('Test sending empty OMEMO messages', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
@@ -539,7 +547,7 @@ void main() {
// Alice marks the ratchet as acknowledged
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
expect(
(await aliceSession.getUnacknowledgedRatchets(bobJid)).isEmpty,
(await aliceSession.getUnacknowledgedRatchets(bobJid))!.isEmpty,
true,
);
});
@@ -614,4 +622,145 @@ void main() {
expect(await aliceRatchet1.equals(aliceRatchet2), false);
expect(await bobRatchet1.equals(bobRatchet2), false);
});
test('Test receiving an old message that contains a KEX', () 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: 2,
);
// Alice sends Bob a message
final msg1 = await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
// Bob responds
final msg2 = await bobSession.encryptToJid(
aliceJid,
'Hello!',
);
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
// Due to some issue with the transport protocol, the first message Bob received is
// received again
try {
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
expect(true, false);
} on InvalidMessageHMACException {
// NOOP
}
final msg3 = await aliceSession.encryptToJid(
bobJid,
'Are you okay?',
);
final result = await bobSession.decryptMessage(
msg3.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg3.encryptedKeys,
);
expect(result, 'Are you okay?');
});
test('Test receiving an old message that does not contain a KEX', () 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: 2,
);
// Alice sends Bob a message
final msg1 = await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
// Bob responds
final msg2 = await bobSession.encryptToJid(
aliceJid,
'Hello!',
);
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
// Due to some issue with the transport protocol, the first message Alice received is
// received again.
try {
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
expect(true, false);
} catch (_) {
// NOOP
}
final msg3 = await aliceSession.encryptToJid(
bobJid,
'Are you okay?',
);
final result = await bobSession.decryptMessage(
msg3.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg3.encryptedKeys,
);
expect(result, 'Are you okay?');
});
}

View File

@@ -48,6 +48,10 @@ void main() {
<int>[172, 2],
);
});
test('Test some special cases', () {
expect(decodeVarint(encodeVarint(1042464893), 0).n, 1042464893);
});
});
group('OMEMOMessage', () {
@@ -170,5 +174,13 @@ void main() {
expect(decoded.message!.mac, <int>[5, 6, 8, 0]);
expect(decoded.message!.message, <int>[4, 5, 7, 3, 2]);
});
test('Test decoding an issue', () {
/*
final data = 'CAAQfRogc2GwslU219dUkrMHNM4KdZRmuFnBTae+bQaJ+55IsAMiII7aZKj2sUpb6xR/3Ari7WZUmKFV0G6czUc4NMvjKDBaKnwKEM2ZpI8X3TgcxhxwENANnlsSaAgAEAAaICy8T9WPgLb7RdYd8/4JkrLF0RahEkC3ZaEfk5jw3dsLIkBMILzLyByweLgF4lCn0oNea+kbdrFr6rY7r/7WyI8hXEQz38QpnN+jyGGwC7Ga0dq70WuyqE7VpiFArQwqZh2G';
final kex = OmemoKeyExchange.fromBuffer(base64Decode(data));
expect(kex.spkId!, 1042464893);
*/
});
});
}

View File

@@ -1,7 +1,12 @@
import 'dart:convert';
import 'package:omemo_dart/omemo_dart.dart';
import 'package:omemo_dart/src/trust/always.dart';
import 'package:test/test.dart';
Map<String, dynamic> jsonify(Map<String, dynamic> map) {
return jsonDecode(jsonEncode(map)) as Map<String, dynamic>;
}
void main() {
test('Test serialising and deserialising the Device', () async {
// Generate a random session
@@ -11,7 +16,7 @@ void main() {
opkAmount: 1,
);
final oldDevice = await oldSession.getDevice();
final serialised = await oldDevice.toJson();
final serialised = jsonify(await oldDevice.toJson());
final newDevice = Device.fromJson(serialised);
expect(await oldDevice.equals(newDevice), true);
@@ -25,7 +30,7 @@ void main() {
opkAmount: 1,
);
final oldDevice = await (await oldSession.getDevice()).replaceSignedPrekey();
final serialised = await oldDevice.toJson();
final serialised = jsonify(await oldDevice.toJson());
final newDevice = Device.fromJson(serialised);
expect(await oldDevice.equals(newDevice), true);
@@ -59,7 +64,7 @@ void main() {
aliceMessage.encryptedKeys,
);
final aliceOld = aliceSession.getRatchet(bobJid, await bobSession.getDeviceId());
final aliceSerialised = await aliceOld.toJson();
final aliceSerialised = jsonify(await aliceOld.toJson());
final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised);
expect(await aliceOld.equals(aliceNew), true);
@@ -84,9 +89,11 @@ void main() {
);
// Serialise and deserialise
final serialised = await oldSession.toJson();
final newSession = OmemoSessionManager.fromJson(
final serialised = jsonify(await oldSession.toJsonWithoutSessions());
final newSession = OmemoSessionManager.fromJsonWithoutSessions(
serialised,
// NOTE: At this point, we don't care about this attribute
{},
AlwaysTrustingTrustManager(),
);
@@ -94,14 +101,47 @@ void main() {
final newDevice = await newSession.getDevice();
expect(await oldDevice.equals(newDevice), true);
expect(await oldSession.getDeviceMap(), await newSession.getDeviceMap());
});
expect(oldSession.getRatchetMap().length, newSession.getRatchetMap().length);
for (final session in oldSession.getRatchetMap().entries) {
expect(newSession.getRatchetMap().containsKey(session.key), true);
test('Test serializing and deserializing RatchetMapKey', () {
const test1 = RatchetMapKey('user@example.org', 1234);
final result1 = RatchetMapKey.fromJsonKey(test1.toJsonKey());
expect(result1.jid, test1.jid);
expect(result1.deviceId, test1.deviceId);
final oldRatchet = oldSession.getRatchetMap()[session.key]!;
final newRatchet = newSession.getRatchetMap()[session.key]!;
expect(await oldRatchet.equals(newRatchet), true);
}
const test2 = RatchetMapKey('user@example.org/hallo:welt', 3333);
final result2 = RatchetMapKey.fromJsonKey(test2.toJsonKey());
expect(result2.jid, test2.jid);
expect(result2.deviceId, test2.deviceId);
});
test('Test serializing and deserializing the components of the BTBV manager', () async {
// Caroline's BTBV manager
final btbv = MemoryBTBVTrustManager();
// Example data
const aliceJid = 'alice@some.server';
const bobJid = 'bob@other.server';
await btbv.onNewSession(aliceJid, 1);
await btbv.setDeviceTrust(aliceJid, 1, BTBVTrustState.verified);
await btbv.onNewSession(aliceJid, 2);
await btbv.onNewSession(bobJid, 3);
await btbv.onNewSession(bobJid, 4);
final serialized = jsonify(await btbv.toJson());
final deviceList = BlindTrustBeforeVerificationTrustManager.deviceListFromJson(
serialized,
);
expect(btbv.devices, deviceList);
final trustCache = BlindTrustBeforeVerificationTrustManager.trustCacheFromJson(
serialized,
);
expect(btbv.trustCache, trustCache);
final enableCache = BlindTrustBeforeVerificationTrustManager.enableCacheFromJson(
serialized,
);
expect(btbv.enablementCache, enableCache);
});
}

View File

@@ -12,6 +12,7 @@ void main() {
// Caroline starts a chat a device from Alice
await btbv.onNewSession(aliceJid, 1);
expect(await btbv.isTrusted(aliceJid, 1), true);
expect(await btbv.isEnabled(aliceJid, 1), true);
// Caroline meets with Alice and verifies her fingerprint
await btbv.setDeviceTrust(aliceJid, 1, BTBVTrustState.verified);
@@ -21,6 +22,7 @@ void main() {
await btbv.onNewSession(aliceJid, 2);
expect(await btbv.isTrusted(aliceJid, 2), false);
expect(btbv.getDeviceTrust(aliceJid, 2), BTBVTrustState.notTrusted);
expect(await btbv.isEnabled(aliceJid, 2), false);
// Caronline starts a chat with Bob but since they live far apart, Caroline cannot
// verify his fingerprint.
@@ -32,5 +34,7 @@ void main() {
expect(await btbv.isTrusted(bobJid, 4), true);
expect(btbv.getDeviceTrust(bobJid, 3), BTBVTrustState.blindTrust);
expect(btbv.getDeviceTrust(bobJid, 4), BTBVTrustState.blindTrust);
expect(await btbv.isEnabled(bobJid, 3), true);
expect(await btbv.isEnabled(bobJid, 4), true);
});
}