39 Commits

Author SHA1 Message Date
afad5056c0 release: Bump version to 0.3.2
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2022-11-05 13:50:45 +01:00
c68471349a fix: Reuse old key exchange when the ratchet is unacked
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-10-22 12:41:41 +02:00
1472624b1d fix: Use stanza receival timestamps to guard against stale kex messages
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-10-02 19:23:58 +02:00
0826d043d5 feat: Attempt to detect already decrypted messages 2022-10-02 17:03:39 +02:00
2aa3674c4b fix: Fix receiving an old key exchange breaking decryption
This was mostly caused by Dart not copying values but referencing
them. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.

We know make some assumptions about received key exchanges, so this
needs some field testing.
2022-10-02 14:56:20 +02:00
7c3a9a75df chore: Let pub ignore the protobuf build artifacts
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-09-29 15:24:33 +02:00
bc6f98bcd8 release: Version 0.3.1
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2022-09-29 15:24:33 +02:00
a107dfad87 ci: Fix issue with duplicate naming
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2022-09-28 13:18:39 +02:00
96771cf317 feat: Allow getting the device's fingerprint 2022-09-25 12:50:59 +02:00
bea433e377 fix: React to all decryption errors with ratchet restoration 2022-09-25 11:35:12 +02:00
47948fa6ea build: Add Woodpecker CI 2022-09-22 21:33:22 +02:00
a23dd30eee chore: Commit the protobuf artifacts 2022-09-18 15:56:09 +02:00
b4c14a9769 fix: Guard against a crash in the critical section 2022-09-15 23:33:07 +02:00
b69acdd936 fix: Fix replaceOnetimePrekey mutating the device's id 2022-09-15 23:31:56 +02:00
3d8c82fe5b docs: Update README 2022-09-15 13:52:29 +02:00
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
f68e45af26 docs: Update README 2022-08-18 15:35:07 +02:00
345596923e release: Bump version to 0.2.0 2022-08-18 15:33:49 +02:00
d5d4aa9014 feat: Add getDeviceId and getDeviceBundle 2022-08-18 15:30:31 +02:00
ee7b09bdb0 feat: Ratchets should overwrite each other 2022-08-18 15:20:32 +02:00
27 changed files with 1433 additions and 251 deletions

3
.gitignore vendored
View File

@@ -12,6 +12,3 @@ pubspec.lock
# NixOS # NixOS
.direnv .direnv
.envrc .envrc
# Protobuf build artifacts
lib/protobuf/*.dart

1
.pubignore Normal file
View File

@@ -0,0 +1 @@
lib/protobuf

11
.woodpecker.yml Normal file
View File

@@ -0,0 +1,11 @@
pipeline:
lint:
image: dart:2.18.1
commands:
- dart pub get
- dart analyze --fatal-infos --fatal-warnings
test:
image: dart:2.18.1
commands:
- dart pub get
- dart test

View File

@@ -9,3 +9,30 @@
- Fix bug with the Double Ratchet causing only the initial message to be decryptable - Fix bug with the Double Ratchet causing only the initial message to be decryptable
- Expose `getDeviceMap` as a developer usable function - Expose `getDeviceMap` as a developer usable function
## 0.2.0
- 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
## 0.3.1
- Fix a bug that caused the device's id to change when replacing a OPK
- Every decryption failure now causes the ratchet to be restored to a pre-decryption state
- Add method to get the device's fingerprint

View File

@@ -28,7 +28,7 @@ Include `omemo_dart` in your `pubspec.yaml` like this:
dependencies: dependencies:
omemo_dart: omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.1.0 version: ^0.3.1
# [...] # [...]
# [...] # [...]

View File

@@ -65,7 +65,7 @@ void main() async {
// a new session. Let's also assume that Bob only has one device. We may, however, // a new session. Let's also assume that Bob only has one device. We may, however,
// add more bundles to newSessions, if we know of more. // add more bundles to newSessions, if we know of more.
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
@@ -119,6 +119,8 @@ void main() async {
aliceDevice.id, aliceDevice.id,
// The deserialised keys // The deserialised keys
keys, keys,
// Since the message was not delayed, we use the current time
DateTime.now().millisecondsSinceEpoch,
); );
// All Bob has to do now is replace the OMEMO wrapper element // All Bob has to do now is replace the OMEMO wrapper element

263
lib/protobuf/schema.pb.dart Normal file
View File

@@ -0,0 +1,263 @@
///
// Generated code. Do not modify.
// source: schema.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class OMEMOMessage extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'OMEMOMessage', createEmptyInstance: create)
..a<$core.int>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'n', $pb.PbFieldType.QU3)
..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'pn', $pb.PbFieldType.QU3)
..a<$core.List<$core.int>>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'dhPub', $pb.PbFieldType.QY)
..a<$core.List<$core.int>>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ciphertext', $pb.PbFieldType.OY)
;
OMEMOMessage._() : super();
factory OMEMOMessage({
$core.int? n,
$core.int? pn,
$core.List<$core.int>? dhPub,
$core.List<$core.int>? ciphertext,
}) {
final _result = create();
if (n != null) {
_result.n = n;
}
if (pn != null) {
_result.pn = pn;
}
if (dhPub != null) {
_result.dhPub = dhPub;
}
if (ciphertext != null) {
_result.ciphertext = ciphertext;
}
return _result;
}
factory OMEMOMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory OMEMOMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
OMEMOMessage clone() => OMEMOMessage()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
OMEMOMessage copyWith(void Function(OMEMOMessage) updates) => super.copyWith((message) => updates(message as OMEMOMessage)) as OMEMOMessage; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static OMEMOMessage create() => OMEMOMessage._();
OMEMOMessage createEmptyInstance() => create();
static $pb.PbList<OMEMOMessage> createRepeated() => $pb.PbList<OMEMOMessage>();
@$core.pragma('dart2js:noInline')
static OMEMOMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<OMEMOMessage>(create);
static OMEMOMessage? _defaultInstance;
@$pb.TagNumber(1)
$core.int get n => $_getIZ(0);
@$pb.TagNumber(1)
set n($core.int v) { $_setUnsignedInt32(0, v); }
@$pb.TagNumber(1)
$core.bool hasN() => $_has(0);
@$pb.TagNumber(1)
void clearN() => clearField(1);
@$pb.TagNumber(2)
$core.int get pn => $_getIZ(1);
@$pb.TagNumber(2)
set pn($core.int v) { $_setUnsignedInt32(1, v); }
@$pb.TagNumber(2)
$core.bool hasPn() => $_has(1);
@$pb.TagNumber(2)
void clearPn() => clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get dhPub => $_getN(2);
@$pb.TagNumber(3)
set dhPub($core.List<$core.int> v) { $_setBytes(2, v); }
@$pb.TagNumber(3)
$core.bool hasDhPub() => $_has(2);
@$pb.TagNumber(3)
void clearDhPub() => clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get ciphertext => $_getN(3);
@$pb.TagNumber(4)
set ciphertext($core.List<$core.int> v) { $_setBytes(3, v); }
@$pb.TagNumber(4)
$core.bool hasCiphertext() => $_has(3);
@$pb.TagNumber(4)
void clearCiphertext() => clearField(4);
}
class OMEMOAuthenticatedMessage extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'OMEMOAuthenticatedMessage', createEmptyInstance: create)
..a<$core.List<$core.int>>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'mac', $pb.PbFieldType.QY)
..a<$core.List<$core.int>>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'message', $pb.PbFieldType.QY)
;
OMEMOAuthenticatedMessage._() : super();
factory OMEMOAuthenticatedMessage({
$core.List<$core.int>? mac,
$core.List<$core.int>? message,
}) {
final _result = create();
if (mac != null) {
_result.mac = mac;
}
if (message != null) {
_result.message = message;
}
return _result;
}
factory OMEMOAuthenticatedMessage.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory OMEMOAuthenticatedMessage.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
OMEMOAuthenticatedMessage clone() => OMEMOAuthenticatedMessage()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
OMEMOAuthenticatedMessage copyWith(void Function(OMEMOAuthenticatedMessage) updates) => super.copyWith((message) => updates(message as OMEMOAuthenticatedMessage)) as OMEMOAuthenticatedMessage; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static OMEMOAuthenticatedMessage create() => OMEMOAuthenticatedMessage._();
OMEMOAuthenticatedMessage createEmptyInstance() => create();
static $pb.PbList<OMEMOAuthenticatedMessage> createRepeated() => $pb.PbList<OMEMOAuthenticatedMessage>();
@$core.pragma('dart2js:noInline')
static OMEMOAuthenticatedMessage getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<OMEMOAuthenticatedMessage>(create);
static OMEMOAuthenticatedMessage? _defaultInstance;
@$pb.TagNumber(1)
$core.List<$core.int> get mac => $_getN(0);
@$pb.TagNumber(1)
set mac($core.List<$core.int> v) { $_setBytes(0, v); }
@$pb.TagNumber(1)
$core.bool hasMac() => $_has(0);
@$pb.TagNumber(1)
void clearMac() => clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get message => $_getN(1);
@$pb.TagNumber(2)
set message($core.List<$core.int> v) { $_setBytes(1, v); }
@$pb.TagNumber(2)
$core.bool hasMessage() => $_has(1);
@$pb.TagNumber(2)
void clearMessage() => clearField(2);
}
class OMEMOKeyExchange extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'OMEMOKeyExchange', createEmptyInstance: create)
..a<$core.int>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'pkId', $pb.PbFieldType.QU3)
..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'spkId', $pb.PbFieldType.QU3)
..a<$core.List<$core.int>>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ik', $pb.PbFieldType.QY)
..a<$core.List<$core.int>>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'ek', $pb.PbFieldType.QY)
..aQM<OMEMOAuthenticatedMessage>(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'message', subBuilder: OMEMOAuthenticatedMessage.create)
;
OMEMOKeyExchange._() : super();
factory OMEMOKeyExchange({
$core.int? pkId,
$core.int? spkId,
$core.List<$core.int>? ik,
$core.List<$core.int>? ek,
OMEMOAuthenticatedMessage? message,
}) {
final _result = create();
if (pkId != null) {
_result.pkId = pkId;
}
if (spkId != null) {
_result.spkId = spkId;
}
if (ik != null) {
_result.ik = ik;
}
if (ek != null) {
_result.ek = ek;
}
if (message != null) {
_result.message = message;
}
return _result;
}
factory OMEMOKeyExchange.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r);
factory OMEMOKeyExchange.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
'Will be removed in next major version')
OMEMOKeyExchange clone() => OMEMOKeyExchange()..mergeFromMessage(this);
@$core.Deprecated(
'Using this can add significant overhead to your binary. '
'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
'Will be removed in next major version')
OMEMOKeyExchange copyWith(void Function(OMEMOKeyExchange) updates) => super.copyWith((message) => updates(message as OMEMOKeyExchange)) as OMEMOKeyExchange; // ignore: deprecated_member_use
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static OMEMOKeyExchange create() => OMEMOKeyExchange._();
OMEMOKeyExchange createEmptyInstance() => create();
static $pb.PbList<OMEMOKeyExchange> createRepeated() => $pb.PbList<OMEMOKeyExchange>();
@$core.pragma('dart2js:noInline')
static OMEMOKeyExchange getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<OMEMOKeyExchange>(create);
static OMEMOKeyExchange? _defaultInstance;
@$pb.TagNumber(1)
$core.int get pkId => $_getIZ(0);
@$pb.TagNumber(1)
set pkId($core.int v) { $_setUnsignedInt32(0, v); }
@$pb.TagNumber(1)
$core.bool hasPkId() => $_has(0);
@$pb.TagNumber(1)
void clearPkId() => clearField(1);
@$pb.TagNumber(2)
$core.int get spkId => $_getIZ(1);
@$pb.TagNumber(2)
set spkId($core.int v) { $_setUnsignedInt32(1, v); }
@$pb.TagNumber(2)
$core.bool hasSpkId() => $_has(1);
@$pb.TagNumber(2)
void clearSpkId() => clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get ik => $_getN(2);
@$pb.TagNumber(3)
set ik($core.List<$core.int> v) { $_setBytes(2, v); }
@$pb.TagNumber(3)
$core.bool hasIk() => $_has(2);
@$pb.TagNumber(3)
void clearIk() => clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get ek => $_getN(3);
@$pb.TagNumber(4)
set ek($core.List<$core.int> v) { $_setBytes(3, v); }
@$pb.TagNumber(4)
$core.bool hasEk() => $_has(3);
@$pb.TagNumber(4)
void clearEk() => clearField(4);
@$pb.TagNumber(5)
OMEMOAuthenticatedMessage get message => $_getN(4);
@$pb.TagNumber(5)
set message(OMEMOAuthenticatedMessage v) { setField(5, v); }
@$pb.TagNumber(5)
$core.bool hasMessage() => $_has(4);
@$pb.TagNumber(5)
void clearMessage() => clearField(5);
@$pb.TagNumber(5)
OMEMOAuthenticatedMessage ensureMessage() => $_ensure(4);
}

View File

@@ -0,0 +1,7 @@
///
// Generated code. Do not modify.
// source: schema.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name

View File

@@ -0,0 +1,48 @@
///
// Generated code. Do not modify.
// source: schema.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
import 'dart:core' as $core;
import 'dart:convert' as $convert;
import 'dart:typed_data' as $typed_data;
@$core.Deprecated('Use oMEMOMessageDescriptor instead')
const OMEMOMessage$json = const {
'1': 'OMEMOMessage',
'2': const [
const {'1': 'n', '3': 1, '4': 2, '5': 13, '10': 'n'},
const {'1': 'pn', '3': 2, '4': 2, '5': 13, '10': 'pn'},
const {'1': 'dh_pub', '3': 3, '4': 2, '5': 12, '10': 'dhPub'},
const {'1': 'ciphertext', '3': 4, '4': 1, '5': 12, '10': 'ciphertext'},
],
};
/// Descriptor for `OMEMOMessage`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List oMEMOMessageDescriptor = $convert.base64Decode('CgxPTUVNT01lc3NhZ2USDAoBbhgBIAIoDVIBbhIOCgJwbhgCIAIoDVICcG4SFQoGZGhfcHViGAMgAigMUgVkaFB1YhIeCgpjaXBoZXJ0ZXh0GAQgASgMUgpjaXBoZXJ0ZXh0');
@$core.Deprecated('Use oMEMOAuthenticatedMessageDescriptor instead')
const OMEMOAuthenticatedMessage$json = const {
'1': 'OMEMOAuthenticatedMessage',
'2': const [
const {'1': 'mac', '3': 1, '4': 2, '5': 12, '10': 'mac'},
const {'1': 'message', '3': 2, '4': 2, '5': 12, '10': 'message'},
],
};
/// Descriptor for `OMEMOAuthenticatedMessage`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List oMEMOAuthenticatedMessageDescriptor = $convert.base64Decode('ChlPTUVNT0F1dGhlbnRpY2F0ZWRNZXNzYWdlEhAKA21hYxgBIAIoDFIDbWFjEhgKB21lc3NhZ2UYAiACKAxSB21lc3NhZ2U=');
@$core.Deprecated('Use oMEMOKeyExchangeDescriptor instead')
const OMEMOKeyExchange$json = const {
'1': 'OMEMOKeyExchange',
'2': const [
const {'1': 'pk_id', '3': 1, '4': 2, '5': 13, '10': 'pkId'},
const {'1': 'spk_id', '3': 2, '4': 2, '5': 13, '10': 'spkId'},
const {'1': 'ik', '3': 3, '4': 2, '5': 12, '10': 'ik'},
const {'1': 'ek', '3': 4, '4': 2, '5': 12, '10': 'ek'},
const {'1': 'message', '3': 5, '4': 2, '5': 11, '6': '.OMEMOAuthenticatedMessage', '10': 'message'},
],
};
/// Descriptor for `OMEMOKeyExchange`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List oMEMOKeyExchangeDescriptor = $convert.base64Decode('ChBPTUVNT0tleUV4Y2hhbmdlEhMKBXBrX2lkGAEgAigNUgRwa0lkEhUKBnNwa19pZBgCIAIoDVIFc3BrSWQSDgoCaWsYAyACKAxSAmlrEg4KAmVrGAQgAigMUgJlaxI0CgdtZXNzYWdlGAUgAigLMhouT01FTU9BdXRoZW50aWNhdGVkTWVzc2FnZVIHbWVzc2FnZQ==');

View File

@@ -0,0 +1,9 @@
///
// Generated code. Do not modify.
// source: schema.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
export 'schema.pb.dart';

View File

@@ -68,6 +68,8 @@ class OmemoDoubleRatchet {
this.sessionAd, this.sessionAd,
this.mkSkipped, // MKSKIPPED this.mkSkipped, // MKSKIPPED
this.acknowledged, this.acknowledged,
this.kexTimestamp,
this.kex,
); );
factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) { factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) {
@@ -82,9 +84,11 @@ class OmemoDoubleRatchet {
'ns': 0, 'ns': 0,
'nr': 0, 'nr': 0,
'pn': 0, 'pn': 0,
'ik_pub': 'base/64/encoded', 'ik_pub': null | 'base/64/encoded',
'session_ad': 'base/64/encoded', 'session_ad': 'base/64/encoded',
'acknowledged': true | false, 'acknowledged': true | false,
'kex_timestamp': int,
'kex': 'base/64/encoded',
'mkskipped': [ 'mkskipped': [
{ {
'key': 'base/64/encoded', 'key': 'base/64/encoded',
@@ -94,11 +98,20 @@ class OmemoDoubleRatchet {
] ]
} }
*/ */
final mkSkipped = <SkippedKey, List<int>>{}; // NOTE: Dart has some issues with just casting a List<dynamic> to List<Map<...>>, as
for (final entry in data['mkskipped']! as List<Map<String, dynamic>>) { // such we need to convert the items by hand.
final key = SkippedKey.fromJson(entry); final mkSkipped = Map<SkippedKey, List<int>>.fromEntries(
mkSkipped[key] = base64.decode(entry['key']! as String); (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( return OmemoDoubleRatchet(
OmemoKeyPair.fromBytes( OmemoKeyPair.fromBytes(
@@ -120,6 +133,8 @@ class OmemoDoubleRatchet {
base64.decode(data['session_ad']! as String), base64.decode(data['session_ad']! as String),
mkSkipped, mkSkipped,
data['acknowledged']! as bool, data['acknowledged']! as bool,
data['kex_timestamp']! as int,
data['kex'] as String?,
); );
} }
@@ -151,6 +166,13 @@ class OmemoDoubleRatchet {
final Map<SkippedKey, List<int>> mkSkipped; final Map<SkippedKey, List<int>> mkSkipped;
/// The point in time at which we performed the kex exchange to create this ratchet.
/// Precision is milliseconds since epoch.
int kexTimestamp;
/// The key exchange that was used for initiating the session.
final String? kex;
/// Indicates whether we received an empty OMEMO message after building a session with /// Indicates whether we received an empty OMEMO message after building a session with
/// the device. /// the device.
bool acknowledged; bool acknowledged;
@@ -158,7 +180,7 @@ class OmemoDoubleRatchet {
/// Create an OMEMO session using the Signed Pre Key [spk], the shared secret [sk] that /// 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 /// 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. /// a X3DH. [ik] refers to Bob's (the receiver's) IK public key.
static Future<OmemoDoubleRatchet> initiateNewSession(OmemoPublicKey spk, OmemoPublicKey ik, List<int> sk, List<int> ad) async { static Future<OmemoDoubleRatchet> initiateNewSession(OmemoPublicKey spk, OmemoPublicKey ik, List<int> sk, List<int> ad, int timestamp) async {
final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final dhr = spk; final dhr = spk;
final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0)); final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0));
@@ -177,6 +199,8 @@ class OmemoDoubleRatchet {
ad, ad,
{}, {},
false, false,
timestamp,
'',
); );
} }
@@ -184,7 +208,7 @@ class OmemoDoubleRatchet {
/// Pre Key keypair [spk], the shared secret [sk] that was obtained through a X3DH and /// Pre Key keypair [spk], the shared secret [sk] that was obtained through a X3DH and
/// the associated data [ad] that was also obtained through a X3DH. [ik] refers to /// the associated data [ad] that was also obtained through a X3DH. [ik] refers to
/// Alice's (the initiator's) IK public key. /// Alice's (the initiator's) IK public key.
static Future<OmemoDoubleRatchet> acceptNewSession(OmemoKeyPair spk, OmemoPublicKey ik, List<int> sk, List<int> ad) async { static Future<OmemoDoubleRatchet> acceptNewSession(OmemoKeyPair spk, OmemoPublicKey ik, List<int> sk, List<int> ad, int kexTimestamp) async {
return OmemoDoubleRatchet( return OmemoDoubleRatchet(
spk, spk,
null, null,
@@ -197,7 +221,9 @@ class OmemoDoubleRatchet {
ik, ik,
ad, ad,
{}, {},
true, false,
kexTimestamp,
null,
); );
} }
@@ -224,6 +250,8 @@ class OmemoDoubleRatchet {
'session_ad': base64.encode(sessionAd), 'session_ad': base64.encode(sessionAd),
'mkskipped': mkSkippedSerialised, 'mkskipped': mkSkippedSerialised,
'acknowledged': acknowledged, 'acknowledged': acknowledged,
'kex_timestamp': kexTimestamp,
'kex': kex,
}; };
} }
@@ -259,18 +287,18 @@ class OmemoDoubleRatchet {
} }
Future<void> _dhRatchet(OmemoMessage header) async { Future<void> _dhRatchet(OmemoMessage header) async {
pn = header.n!; pn = ns;
ns = 0; ns = 0;
nr = 0; nr = 0;
dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519); dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519);
final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0)); final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
rk = newRk; rk = List.from(newRk);
ckr = newRk; ckr = List.from(newRk);
dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0)); final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
rk = newNewRk; rk = List.from(newNewRk);
cks = newNewRk; cks = List.from(newNewRk);
} }
/// Encrypt [plaintext] using the Double Ratchet. /// Encrypt [plaintext] using the Double Ratchet.
@@ -304,8 +332,8 @@ class OmemoDoubleRatchet {
} }
final dhPubMatches = listsEqual( final dhPubMatches = listsEqual(
header.dhPub ?? <int>[], header.dhPub!,
await dhr?.getBytes() ?? <int>[], (await dhr?.getBytes()) ?? <int>[],
); );
if (!dhPubMatches) { if (!dhPubMatches) {
await _skipMessageKeys(header.pn!); await _skipMessageKeys(header.pn!);
@@ -321,12 +349,64 @@ class OmemoDoubleRatchet {
return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd); return decrypt(mk, ciphertext, concat([sessionAd, header.writeToBuffer()]), sessionAd);
} }
OmemoDoubleRatchet clone() {
return OmemoDoubleRatchet(
dhs,
dhr,
rk,
cks != null ?
List<int>.from(cks!) :
null,
ckr != null ?
List<int>.from(ckr!) :
null,
ns,
nr,
pn,
ik,
sessionAd,
Map<SkippedKey, List<int>>.from(mkSkipped),
acknowledged,
kexTimestamp,
kex,
);
}
OmemoDoubleRatchet cloneWithKex(String kex) {
return OmemoDoubleRatchet(
dhs,
dhr,
rk,
cks != null ?
List<int>.from(cks!) :
null,
ckr != null ?
List<int>.from(ckr!) :
null,
ns,
nr,
pn,
ik,
sessionAd,
Map<SkippedKey, List<int>>.from(mkSkipped),
acknowledged,
kexTimestamp,
kex,
);
}
@visibleForTesting @visibleForTesting
Future<bool> equals(OmemoDoubleRatchet other) async { Future<bool> equals(OmemoDoubleRatchet other) async {
// ignore: invalid_use_of_visible_for_testing_member final dhrMatch = dhr == null ?
final dhrMatch = dhr == null ? other.dhr == null : await dhr!.equals(other.dhr!); other.dhr == null :
final ckrMatch = ckr == null ? other.ckr == null : listsEqual(ckr!, other.ckr!); // ignore: invalid_use_of_visible_for_testing_member
final cksMatch = cks == null ? other.cks == null : listsEqual(cks!, other.cks!); other.dhr != null && await dhr!.equals(other.dhr!);
final ckrMatch = ckr == null ?
other.ckr == null :
other.ckr != null && listsEqual(ckr!, other.ckr!);
final cksMatch = cks == null ?
other.cks == null :
other.cks != null && listsEqual(cks!, other.cks!);
// ignore: invalid_use_of_visible_for_testing_member // ignore: invalid_use_of_visible_for_testing_member
final dhsMatch = await dhs.equals(other.dhs); final dhsMatch = await dhs.equals(other.dhs);
@@ -342,6 +422,7 @@ class OmemoDoubleRatchet {
ns == other.ns && ns == other.ns &&
nr == other.nr && nr == other.nr &&
pn == other.pn && pn == other.pn &&
listsEqual(sessionAd, other.sessionAd); listsEqual(sessionAd, other.sessionAd) &&
kexTimestamp == other.kexTimestamp;
} }
} }

View File

@@ -30,3 +30,16 @@ class NoDecryptionKeyException implements Exception {
class UnknownSignedPrekeyException implements Exception { class UnknownSignedPrekeyException implements Exception {
String errMsg() => 'Unknown Signed Prekey used.'; String errMsg() => 'Unknown Signed Prekey used.';
} }
/// Triggered by the Session Manager when the received Key Exchange message does not meet
/// the requirement that a key exchange, given that the ratchet already exists, must be
/// sent after its creation.
class InvalidKeyExchangeException implements Exception {
String errMsg() => 'The key exchange was sent before the last kex finished';
}
/// Triggered by the Session Manager when a message's sequence number is smaller than we
/// expect it to be.
class MessageAlreadyDecryptedException implements Exception {
String errMsg() => 'The message has already been decrypted';
}

View File

@@ -73,3 +73,7 @@ OmemoKeyPair? decodeKeyPairIfNotNull(String? pk, String? sk, KeyPairType type) {
type, type,
); );
} }
int getTimestamp() {
return DateTime.now().millisecondsSinceEpoch;
}

View File

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

View File

@@ -4,9 +4,24 @@ import 'package:meta/meta.dart';
class RatchetMapKey { class RatchetMapKey {
const RatchetMapKey(this.jid, this.deviceId); 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 String jid;
final int deviceId; final int deviceId;
String toJsonKey() {
return '$deviceId:$jid';
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is RatchetMapKey && jid == other.jid && deviceId == other.deviceId; 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:collection/collection.dart';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:hex/hex.dart'; import 'package:hex/hex.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/double_ratchet.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) OmemoSessionManager(this._device, this._deviceMap, this._ratchetMap, this._trustManager)
: _lock = Lock(), : _lock = Lock(),
_deviceLock = Lock(), _deviceLock = Lock(),
_eventStreamController = StreamController<OmemoEvent>.broadcast(); _eventStreamController = StreamController<OmemoEvent>.broadcast(),
_log = Logger('OmemoSessionManager');
/// 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,
);
}
/// Deserialise the OmemoSessionManager from JSON data [data] that does not contain /// Deserialise the OmemoSessionManager from JSON data [data] that does not contain
/// the ratchet sessions. /// 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( return OmemoSessionManager(
Device.fromJson(data['device']! as Map<String, dynamic>), 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, ratchetMap,
trustManager, trustManager,
); );
@@ -69,6 +67,9 @@ class OmemoSessionManager {
return OmemoSessionManager(device, {}, {}, trustManager); return OmemoSessionManager(device, {}, {}, trustManager);
} }
/// Logging
Logger _log;
/// Lock for _ratchetMap and _bundleMap /// Lock for _ratchetMap and _bundleMap
final Lock _lock; final Lock _lock;
@@ -96,12 +97,19 @@ class OmemoSessionManager {
/// Returns our own device. /// Returns our own device.
Future<Device> getDevice() async { Future<Device> getDevice() async {
Device? dev; return _deviceLock.synchronized(() => _device);
await _deviceLock.synchronized(() async { }
dev = _device;
});
return dev!; /// Returns the id attribute of our own device. This is just a short-hand for
/// ```await (session.getDevice()).id```.
Future<int> getDeviceId() async {
return _deviceLock.synchronized(() => _device.id);
}
/// Returns the device as an OmemoBundle. This is just a short-hand for
/// ```await (await session.getDevice()).toBundle()```.
Future<OmemoBundle> getDeviceBundle() async {
return _deviceLock.synchronized(() async => _device.toBundle());
} }
/// Add a session [ratchet] with the [deviceId] to the internal tracking state. /// Add a session [ratchet] with the [deviceId] to the internal tracking state.
@@ -110,21 +118,22 @@ class OmemoSessionManager {
// Add the bundle Id // Add the bundle Id
if (!_deviceMap.containsKey(jid)) { if (!_deviceMap.containsKey(jid)) {
_deviceMap[jid] = [deviceId]; _deviceMap[jid] = [deviceId];
} else {
_deviceMap[jid]!.add(deviceId);
}
// Commit the device map // Commit the device map
_eventStreamController.add(DeviceMapModifiedEvent(_deviceMap)); _eventStreamController.add(DeviceMapModifiedEvent(_deviceMap));
} else {
// Prevent having the same device multiple times in the list
if (!_deviceMap[jid]!.contains(deviceId)) {
_deviceMap[jid]!.add(deviceId);
// Commit the device map
_eventStreamController.add(DeviceMapModifiedEvent(_deviceMap));
}
}
// Add the ratchet session // Add the ratchet session
final key = RatchetMapKey(jid, deviceId); final key = RatchetMapKey(jid, deviceId);
if (!_ratchetMap.containsKey(key)) { _ratchetMap[key] = ratchet;
_ratchetMap[key] = ratchet;
} else {
// TODO(PapaTutuWawa): What do we do now?
throw Exception();
}
// Commit the ratchet // Commit the ratchet
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet)); _eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
@@ -145,6 +154,7 @@ class OmemoSessionManager {
bundle.ik, bundle.ik,
kexResult.sk, kexResult.sk,
kexResult.ad, kexResult.ad,
getTimestamp(),
); );
await _trustManager.onNewSession(jid, deviceId); await _trustManager.onNewSession(jid, deviceId);
@@ -160,27 +170,29 @@ class OmemoSessionManager {
/// Build a new session with the user at [jid] with the device [deviceId] using data /// Build a new session with the user at [jid] with the device [deviceId] using data
/// from the key exchange [kex]. In case [kex] contains an unknown Signed Prekey /// from the key exchange [kex]. In case [kex] contains an unknown Signed Prekey
/// identifier an UnknownSignedPrekeyException will be thrown. /// identifier an UnknownSignedPrekeyException will be thrown.
Future<void> _addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async { Future<OmemoDoubleRatchet> _addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async {
// Pick the correct SPK // Pick the correct SPK
final device = await getDevice(); final device = await getDevice();
OmemoKeyPair? spk; final spk = await _lock.synchronized(() async {
if (kex.spkId == device.spkId) { if (kex.spkId == _device.spkId) {
spk = device.spk; return _device.spk;
} else if (kex.spkId == device.oldSpkId) { } else if (kex.spkId == _device.oldSpkId) {
spk = device.oldSpk; return _device.oldSpk;
} else { }
return null;
});
if (spk == null) {
throw UnknownSignedPrekeyException(); throw UnknownSignedPrekeyException();
} }
assert(spk != null, 'The used SPK must be found');
final kexResult = await x3dhFromInitialMessage( final kexResult = await x3dhFromInitialMessage(
X3DHMessage( X3DHMessage(
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519), OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
OmemoPublicKey.fromBytes(kex.ek!, KeyPairType.x25519), OmemoPublicKey.fromBytes(kex.ek!, KeyPairType.x25519),
kex.pkId!, kex.pkId!,
), ),
spk!, spk,
device.opks.values.elementAt(kex.pkId!), device.opks.values.elementAt(kex.pkId!),
device.ik, device.ik,
); );
@@ -189,10 +201,10 @@ class OmemoSessionManager {
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519), OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
kexResult.sk, kexResult.sk,
kexResult.ad, kexResult.ad,
getTimestamp(),
); );
await _trustManager.onNewSession(jid, deviceId); return ratchet;
await _addSession(jid, deviceId, ratchet);
} }
/// Like [encryptToJids] but only for one Jid [jid]. /// Like [encryptToJids] but only for one Jid [jid].
@@ -229,7 +241,11 @@ class OmemoSessionManager {
final kex = <int, OmemoKeyExchange>{}; final kex = <int, OmemoKeyExchange>{};
if (newSessions != null) { if (newSessions != null) {
for (final newSession in newSessions) { for (final newSession in newSessions) {
kex[newSession.id] = await addSessionFromBundle(newSession.jid, newSession.id, newSession); kex[newSession.id] = await addSessionFromBundle(
newSession.jid,
newSession.id,
newSession,
);
} }
} }
@@ -241,27 +257,59 @@ class OmemoSessionManager {
if (plaintext != null) { if (plaintext != null) {
// Only encrypt to devices that are trusted // Only encrypt to devices that are trusted
if (!(await _trustManager.isTrusted(jid, deviceId))) continue; 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); final ratchetKey = RatchetMapKey(jid, deviceId);
final ratchet = _ratchetMap[ratchetKey]!; var ratchet = _ratchetMap[ratchetKey]!;
final ciphertext = (await ratchet.ratchetEncrypt(keyPayload)).ciphertext; final ciphertext = (await ratchet.ratchetEncrypt(keyPayload)).ciphertext;
// Commit the ratchet
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
if (kex.isNotEmpty && kex.containsKey(deviceId)) { if (kex.isNotEmpty && kex.containsKey(deviceId)) {
// The ratchet did not exist
final k = kex[deviceId]! final k = kex[deviceId]!
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext); ..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
final buffer = base64.encode(k.writeToBuffer());
encryptedKeys.add( encryptedKeys.add(
EncryptedKey( EncryptedKey(
jid, jid,
deviceId, deviceId,
base64.encode(k.writeToBuffer()), buffer,
true, true,
), ),
); );
ratchet = ratchet.cloneWithKex(buffer);
_ratchetMap[ratchetKey] = ratchet;
} else if (!ratchet.acknowledged) {
// The ratchet exists but is not acked
if (ratchet.kex != null) {
final oldKex = OmemoKeyExchange.fromBuffer(base64.decode(ratchet.kex!))
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
encryptedKeys.add(
EncryptedKey(
jid,
deviceId,
base64.encode(oldKex.writeToBuffer()),
true,
),
);
} else {
// The ratchet is not acked but we don't have the old key exchange
_log.warning('Ratchet for $jid:$deviceId is not acked but the kex attribute is null');
encryptedKeys.add(
EncryptedKey(
jid,
deviceId,
base64.encode(ciphertext),
false,
),
);
}
} else { } else {
// The ratchet exists and is acked
encryptedKeys.add( encryptedKeys.add(
EncryptedKey( EncryptedKey(
jid, jid,
@@ -271,6 +319,9 @@ class OmemoSessionManager {
), ),
); );
} }
// Commit the ratchet
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
} }
} }
}); });
@@ -281,16 +332,57 @@ 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} to ${oldRatchet.nr}');
_ratchetMap[mapKey] = oldRatchet;
// Commit the ratchet
_eventStreamController.add(
RatchetModifiedEvent(
mapKey.jid,
mapKey.deviceId,
oldRatchet,
),
);
});
}
Future<String?> _decryptAndVerifyHmac(List<int>? ciphertext, List<int> keyAndHmac) async {
// 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 derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
final computedHmac = await truncatedHmac(ciphertext, derivedKeys.authenticationKey);
if (!listsEqual(hmac, computedHmac)) {
throw InvalidMessageHMACException();
}
return utf8.decode(
await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv),
);
}
/// Attempt to decrypt [ciphertext]. [keys] refers to the <key /> elements inside the /// 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 /// <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 /// bare Jid of the sender. [senderDeviceId] refers to the "sid" attribute of the
/// <encrypted /> element. /// <encrypted /> element.
/// [timestamp] refers to the time the message was sent. This might be either what the
/// server tells you via "XEP-0203: Delayed Delivery" or the point in time at which
/// you received the stanza, if no Delayed Delivery element was found.
/// ///
/// If the received message is an empty OMEMO message, i.e. there is no <payload /> /// If the received message is an empty OMEMO message, i.e. there is no <payload />
/// element, then [ciphertext] must be set to null. In this case, this function /// element, then [ciphertext] must be set to null. In this case, this function
/// will return null as there is no message to be decrypted. This, however, is used /// will return null as there is no message to be decrypted. This, however, is used
/// to set up sessions or advance the ratchets. /// to set up sessions or advance the ratchets.
Future<String?> decryptMessage(List<int>? ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys) async { Future<String?> decryptMessage(List<int>? ciphertext, String senderJid, int senderDeviceId, List<EncryptedKey> keys, int timestamp) async {
// Try to find a session we can decrypt with. // Try to find a session we can decrypt with.
var device = await getDevice(); var device = await getDevice();
final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id); final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id);
@@ -298,20 +390,57 @@ class OmemoSessionManager {
throw NotEncryptedForDeviceException(); throw NotEncryptedForDeviceException();
} }
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
final decodedRawKey = base64.decode(rawKey.value); final decodedRawKey = base64.decode(rawKey.value);
List<int>? keyAndHmac;
OmemoAuthenticatedMessage authMessage; OmemoAuthenticatedMessage authMessage;
OmemoDoubleRatchet? oldRatchet;
OmemoMessage? message;
if (rawKey.kex) { if (rawKey.kex) {
// TODO(PapaTutuWawa): Only do this when we should // If the ratchet already existed, we store it. If it didn't, oldRatchet will stay
// null.
final oldRatchet = (await _getRatchet(ratchetKey))?.clone();
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey); final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
await _addSessionFromKeyExchange(
senderJid,
senderDeviceId,
kex,
);
authMessage = kex.message!; authMessage = kex.message!;
message = OmemoMessage.fromBuffer(authMessage.message!);
// Guard against old key exchanges
if (oldRatchet != null) {
_log.finest('KEX for existent ratchet. ${oldRatchet.pn}');
if (oldRatchet.kexTimestamp > timestamp) {
throw InvalidKeyExchangeException();
}
// Try to decrypt it
try {
final decrypted = await oldRatchet.ratchetDecrypt(message, authMessage.writeToBuffer());
// Commit the ratchet
_eventStreamController.add(
RatchetModifiedEvent(
senderJid,
senderDeviceId,
oldRatchet,
),
);
final plaintext = await _decryptAndVerifyHmac(
ciphertext,
decrypted,
);
await _addSession(senderJid, senderDeviceId, oldRatchet);
return plaintext;
} catch (_) {
_log.finest('Failed to use old ratchet with KEX for existing ratchet');
}
}
final r = await _addSessionFromKeyExchange(senderJid, senderDeviceId, kex);
await _trustManager.onNewSession(senderJid, senderDeviceId);
await _addSession(senderJid, senderDeviceId, r);
// Replace the OPK // Replace the OPK
// TODO(PapaTutuWawa): Replace the OPK when we know that the KEX worked
await _deviceLock.synchronized(() async { await _deviceLock.synchronized(() async {
device = await device.replaceOnetimePrekey(kex.pkId!); device = await device.replaceOnetimePrekey(kex.pkId!);
@@ -320,6 +449,7 @@ class OmemoSessionManager {
}); });
} else { } else {
authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey); authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey);
message = OmemoMessage.fromBuffer(authMessage.message!);
} }
final devices = _deviceMap[senderJid]; final devices = _deviceMap[senderJid];
@@ -330,37 +460,30 @@ class OmemoSessionManager {
throw NoDecryptionKeyException(); throw NoDecryptionKeyException();
} }
final message = OmemoMessage.fromBuffer(authMessage.message!); // We can guarantee that the ratchet exists at this point in time
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId); final ratchet = (await _getRatchet(ratchetKey))!;
List<int>? keyAndHmac; oldRatchet ??= ratchet.clone();
await _lock.synchronized(() async {
final ratchet = _ratchetMap[ratchetKey]!; try {
if (rawKey.kex) { if (rawKey.kex) {
keyAndHmac = await ratchet.ratchetDecrypt(message, authMessage.writeToBuffer()); keyAndHmac = await ratchet.ratchetDecrypt(message, authMessage.writeToBuffer());
} else { } else {
keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey); keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey);
} }
} catch (_) {
// Commit the ratchet await _restoreRatchet(ratchetKey, oldRatchet);
_eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet)); rethrow;
});
// 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); // Commit the ratchet
final hmac = keyAndHmac!.sublist(32, 48); _eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet));
final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
final computedHmac = await truncatedHmac(ciphertext, derivedKeys.authenticationKey); try {
if (!listsEqual(hmac, computedHmac)) { return _decryptAndVerifyHmac(ciphertext, keyAndHmac);
throw InvalidMessageHMACException(); } catch (_) {
await _restoreRatchet(ratchetKey, oldRatchet);
rethrow;
} }
final plaintext = await aes256CbcDecrypt(ciphertext, derivedKeys.encryptionKey, derivedKeys.iv);
return utf8.decode(plaintext);
} }
/// Returns the list of hex-encoded fingerprints we have for sessions with [jid]. /// Returns the list of hex-encoded fingerprints we have for sessions with [jid].
@@ -369,7 +492,7 @@ class OmemoSessionManager {
await _lock.synchronized(() async { await _lock.synchronized(() async {
// Get devices for jid // Get devices for jid
final devices = _deviceMap[jid]!; final devices = _deviceMap[jid] ?? [];
for (final deviceId in devices) { for (final deviceId in devices) {
final ratchet = _ratchetMap[RatchetMapKey(jid, deviceId)]!; final ratchet = _ratchetMap[RatchetMapKey(jid, deviceId)]!;
@@ -386,6 +509,16 @@ class OmemoSessionManager {
return fingerprints; return fingerprints;
} }
/// Returns the hex-encoded fingerprint of the current device.
Future<DeviceFingerprint> getHexFingerprintForDevice() async {
final device = await getDevice();
return DeviceFingerprint(
device.id,
HEX.encode(await device.ik.pk.getBytes()),
);
}
/// Replaces the Signed Prekey and its signature in our own device bundle. Triggers /// Replaces the Signed Prekey and its signature in our own device bundle. Triggers
/// a DeviceModifiedEvent when done. /// a DeviceModifiedEvent when done.
/// See https://xmpp.org/extensions/xep-0384.html#protocol-key_exchange under the point /// See https://xmpp.org/extensions/xep-0384.html#protocol-key_exchange under the point
@@ -402,13 +535,7 @@ class OmemoSessionManager {
/// Returns the device map, i.e. the mapping of bare Jid to its device identifiers /// Returns the device map, i.e. the mapping of bare Jid to its device identifiers
/// we have built sessions with. /// we have built sessions with.
Future<Map<String, List<int>>> getDeviceMap() async { Future<Map<String, List<int>>> getDeviceMap() async {
Map<String, List<int>>? map; return _lock.synchronized(() => _deviceMap);
await _lock.synchronized(() async {
map = _deviceMap;
});
return map!;
} }
/// Removes the ratchet identified by [jid] and [deviceId] from the session manager. /// Removes the ratchet identified by [jid] and [deviceId] from the session manager.
@@ -431,20 +558,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. /// 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. /// we have not yet received an empty OMEMO message from.
Future<List<int>> getUnacknowledgedRatchets(String jid) async { Future<List<int>?> getUnacknowledgedRatchets(String jid) async {
final ret = List<int>.empty(growable: true); 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) { for (final device in devices) {
final ratchet = _ratchetMap[RatchetMapKey(jid, device)]!; final ratchet = _ratchetMap[RatchetMapKey(jid, device)]!;
if (!ratchet.acknowledged) ret.add(device); if (!ratchet.acknowledged) ret.add(device);
} }
});
return ret; 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. /// Mark the ratchet for device [deviceId] from [jid] as acked.
@@ -480,48 +632,18 @@ class OmemoSessionManager {
}); });
} }
Future<OmemoDoubleRatchet?> _getRatchet(RatchetMapKey key) async {
return _lock.synchronized(() async {
return _ratchetMap[key];
});
}
@visibleForTesting @visibleForTesting
OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!; OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!;
@visibleForTesting @visibleForTesting
Map<RatchetMapKey, OmemoDoubleRatchet> getRatchetMap() => _ratchetMap; 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. /// Serialise the entire session manager into a JSON object.
Future<Map<String, dynamic>> toJsonWithoutSessions() async { 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"); assert(i >= 0, "Two's complement is not implemented");
final ret = List<int>.empty(growable: true); final ret = List<int>.empty(growable: true);
var j = 0; // Thanks to https://github.com/hathibelagal-dev/LEB128 for the trick with toRadixString!
while (true) { final numSevenBlocks = (i.toRadixString(2).length / 7).ceil();
for (var j = 0; j < numSevenBlocks; j++) {
// The 7 LSB of the byte we're creating // The 7 LSB of the byte we're creating
final x = (i & (lsb7Mask << j * 7)) >> j * 7; 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 // If we were to shift further, we only get zero, so we're at the end
ret.add(x); ret.add(x);
break;
} else { } else {
// We still have at least one bit more to go, so set the MSB to 1 // We still have at least one bit more to go, so set the MSB to 1
ret.add(x + msb); ret.add(x + msb);
j++;
} }
} }

View File

@@ -11,4 +11,13 @@ class AlwaysTrustingTrustManager extends TrustManager {
@override @override
Future<void> onNewSession(String jid, int deviceId) async {} 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 /// Called by the OmemoSessionManager when a new session has been built. Should set
/// a default trust state to [jid]'s device with identifier [deviceId]. /// a default trust state to [jid]'s device with identifier [deviceId].
Future<void> onNewSession(String jid, int 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 /// - blindTrust: The fingerprint is not verified using OOB means
/// - verified: The fingerprint has been verified using OOB means /// - verified: The fingerprint has been verified using OOB means
enum BTBVTrustState { enum BTBVTrustState {
notTrusted, notTrusted, // = 1
blindTrust, blindTrust, // = 2
verified, 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. /// A TrustManager that implements the idea of Blind Trust Before Verification.
/// See https://gultsch.de/trust.html for more details. /// See https://gultsch.de/trust.html for more details.
abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager { abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
BlindTrustBeforeVerificationTrustManager() BlindTrustBeforeVerificationTrustManager({
: trustCache = {}, Map<RatchetMapKey, BTBVTrustState>? trustCache,
devices = {}, Map<RatchetMapKey, bool>? enablementCache,
_lock = Lock(); 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 @protected
final Map<RatchetMapKey, BTBVTrustState> trustCache; 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 /// Mapping of Jids to their device identifiers
@visibleForTesting
@protected @protected
final Map<String, List<int>> devices; final Map<String, List<int>> devices;
@@ -74,10 +102,13 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
@override @override
Future<void> onNewSession(String jid, int deviceId) async { Future<void> onNewSession(String jid, int deviceId) async {
await _lock.synchronized(() async { await _lock.synchronized(() async {
final key = RatchetMapKey(jid, deviceId);
if (_hasAtLeastOneVerifiedDevice(jid)) { if (_hasAtLeastOneVerifiedDevice(jid)) {
trustCache[RatchetMapKey(jid, deviceId)] = BTBVTrustState.notTrusted; trustCache[key] = BTBVTrustState.notTrusted;
enablementCache[key] = false;
} else { } else {
trustCache[RatchetMapKey(jid, deviceId)] = BTBVTrustState.blindTrust; trustCache[key] = BTBVTrustState.blindTrust;
enablementCache[key] = true;
} }
if (devices.containsKey(jid)) { 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 /// Called when the state of the trust manager has been changed. Allows the user to
/// commit the trust state to persistent storage. /// commit the trust state to persistent storage.
@visibleForOverriding @visibleForOverriding
Future<void> commitState(); 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 @visibleForTesting
BTBVTrustState getDeviceTrust(String jid, int deviceId) => trustCache[RatchetMapKey(jid, deviceId)]!; BTBVTrustState getDeviceTrust(String jid, int deviceId) => trustCache[RatchetMapKey(jid, deviceId)]!;
} }
@@ -133,7 +223,4 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
class MemoryBTBVTrustManager extends BlindTrustBeforeVerificationTrustManager { class MemoryBTBVTrustManager extends BlindTrustBeforeVerificationTrustManager {
@override @override
Future<void> commitState() async {} Future<void> commitState() async {}
@override
Future<void> loadState() async {}
} }

View File

@@ -11,4 +11,13 @@ class NeverTrustingTrustManager extends TrustManager {
@override @override
Future<void> onNewSession(String jid, int deviceId) async {} 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 name: omemo_dart
description: An XMPP library independent OMEMO library description: An XMPP library independent OMEMO library
version: 0.1.3 version: 0.3.2
homepage: https://github.com/PapaTutuWawa/omemo_dart homepage: https://github.com/PapaTutuWawa/omemo_dart
publish_to: https://git.polynom.me/api/packages/PapaTutuWawa/pub publish_to: https://git.polynom.me/api/packages/PapaTutuWawa/pub
@@ -11,7 +11,8 @@ dependencies:
collection: ^1.16.0 collection: ^1.16.0
cryptography: ^2.0.5 cryptography: ^2.0.5
hex: ^0.2.0 hex: ^0.2.0
meta: ^1.8.0 logging: ^1.0.2
meta: ^1.7.0
pinenacl: ^0.5.1 pinenacl: ^0.5.1
synchronized: ^3.0.0+2 synchronized: ^3.0.0+2

View File

@@ -84,12 +84,14 @@ void main() {
ikBob.pk, ikBob.pk,
resultAlice.sk, resultAlice.sk,
resultAlice.ad, resultAlice.ad,
0,
); );
final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession( final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession(
spkBob, spkBob,
ikAlice.pk, ikAlice.pk,
resultBob.sk, resultBob.sk,
resultBob.ad, resultBob.ad,
0,
); );
expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd); expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd);

View File

@@ -1,9 +1,47 @@
import 'package:logging/logging.dart';
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart';
import 'package:omemo_dart/src/trust/always.dart'; import 'package:omemo_dart/src/trust/always.dart';
import 'package:omemo_dart/src/trust/never.dart'; import 'package:omemo_dart/src/trust/never.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
Logger.root
..level = Level.ALL
..onRecord.listen((record) {
// ignore: avoid_print
print('${record.level.name}: ${record.message}');
});
test('Test replacing a onetime prekey', () async {
const aliceJid = 'alice@server.example';
final device = await Device.generateNewDevice(aliceJid, opkAmount: 1);
final newDevice = await device.replaceOnetimePrekey(0);
expect(device.jid, newDevice.jid);
expect(device.id, newDevice.id);
var opksMatch = true;
if (newDevice.opks.length != device.opks.length) {
opksMatch = false;
} else {
for (final entry in device.opks.entries) {
final m = await newDevice.opks[entry.key]?.equals(entry.value) ?? false;
if (!m) opksMatch = false;
}
}
expect(opksMatch, true);
expect(await device.ik.equals(newDevice.ik), true);
expect(await device.spk.equals(newDevice.spk), true);
final oldSpkMatch = device.oldSpk != null ?
await device.oldSpk!.equals(newDevice.oldSpk!) :
newDevice.oldSpk == null;
expect(oldSpkMatch, true);
expect(listsEqual(device.spkSignature, newDevice.spkSignature), true);
});
test('Test using OMEMO sessions with only one device per user', () async { test('Test using OMEMO sessions with only one device per user', () async {
const aliceJid = 'alice@server.example'; const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example'; const bobJid = 'bob@other.server.example';
@@ -38,7 +76,7 @@ void main() {
bobJid, bobJid,
messagePlaintext, messagePlaintext,
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
expect(aliceMessage.encryptedKeys.length, 1); expect(aliceMessage.encryptedKeys.length, 1);
@@ -50,8 +88,9 @@ void main() {
final bobMessage = await bobSession.decryptMessage( final bobMessage = await bobSession.decryptMessage(
aliceMessage.ciphertext, aliceMessage.ciphertext,
aliceJid, aliceJid,
(await aliceSession.getDevice()).id, await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
// The ratchet should be modified two times: Once for when the ratchet is created and // The ratchet should be modified two times: Once for when the ratchet is created and
@@ -67,6 +106,10 @@ void main() {
false, false,
); );
// Ratchets are acked
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
// Bob responds to Alice // Bob responds to Alice
const bobResponseText = 'Oh, hello Alice!'; const bobResponseText = 'Oh, hello Alice!';
final bobResponseMessage = await bobSession.encryptToJid( final bobResponseMessage = await bobSession.encryptToJid(
@@ -81,8 +124,9 @@ void main() {
final aliceReceivedMessage = await aliceSession.decryptMessage( final aliceReceivedMessage = await aliceSession.decryptMessage(
bobResponseMessage.ciphertext, bobResponseMessage.ciphertext,
bobJid, bobJid,
(await bobSession.getDevice()).id, await bobSession.getDeviceId(),
bobResponseMessage.encryptedKeys, bobResponseMessage.encryptedKeys,
0,
); );
expect(bobResponseText, aliceReceivedMessage); expect(bobResponseText, aliceReceivedMessage);
}); });
@@ -115,8 +159,8 @@ void main() {
bobJid, bobJid,
messagePlaintext, messagePlaintext,
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
await (await bobSession2.getDevice()).toBundle(), await bobSession2.getDeviceBundle(),
], ],
); );
expect(aliceMessage.encryptedKeys.length, 2); expect(aliceMessage.encryptedKeys.length, 2);
@@ -130,11 +174,16 @@ void main() {
final bobMessage = await bobSession.decryptMessage( final bobMessage = await bobSession.decryptMessage(
aliceMessage.ciphertext, aliceMessage.ciphertext,
aliceJid, aliceJid,
(await aliceSession.getDevice()).id, await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
// Ratchets are acked
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
// Bob responds to Alice // Bob responds to Alice
const bobResponseText = 'Oh, hello Alice!'; const bobResponseText = 'Oh, hello Alice!';
final bobResponseMessage = await bobSession.encryptToJid( final bobResponseMessage = await bobSession.encryptToJid(
@@ -149,8 +198,9 @@ void main() {
final aliceReceivedMessage = await aliceSession.decryptMessage( final aliceReceivedMessage = await aliceSession.decryptMessage(
bobResponseMessage.ciphertext, bobResponseMessage.ciphertext,
bobJid, bobJid,
(await bobSession.getDevice()).id, await bobSession.getDeviceId(),
bobResponseMessage.encryptedKeys, bobResponseMessage.encryptedKeys,
0,
); );
expect(bobResponseText, aliceReceivedMessage); expect(bobResponseText, aliceReceivedMessage);
@@ -192,8 +242,8 @@ void main() {
[bobJid, aliceJid], [bobJid, aliceJid],
messagePlaintext, messagePlaintext,
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
await (await aliceSession2.getDevice()).toBundle(), await aliceSession2.getDeviceBundle(),
], ],
); );
expect(aliceMessage.encryptedKeys.length, 2); expect(aliceMessage.encryptedKeys.length, 2);
@@ -205,8 +255,9 @@ void main() {
final bobMessage = await bobSession.decryptMessage( final bobMessage = await bobSession.decryptMessage(
aliceMessage.ciphertext, aliceMessage.ciphertext,
aliceJid, aliceJid,
(await aliceSession1.getDevice()).id, await aliceSession1.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
@@ -214,13 +265,14 @@ void main() {
final aliceMessage2 = await aliceSession2.decryptMessage( final aliceMessage2 = await aliceSession2.decryptMessage(
aliceMessage.ciphertext, aliceMessage.ciphertext,
aliceJid, aliceJid,
(await aliceSession1.getDevice()).id, await aliceSession1.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, aliceMessage2); expect(messagePlaintext, aliceMessage2);
}); });
test('Test using sending empty OMEMO messages', () async { test('Test sending empty OMEMO messages', () async {
const aliceJid = 'alice@server.example'; const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example'; const bobJid = 'bob@other.server.example';
@@ -241,7 +293,7 @@ void main() {
bobJid, bobJid,
null, null,
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
expect(aliceMessage.encryptedKeys.length, 1); expect(aliceMessage.encryptedKeys.length, 1);
@@ -254,13 +306,14 @@ void main() {
final bobMessage = await bobSession.decryptMessage( final bobMessage = await bobSession.decryptMessage(
aliceMessage.ciphertext, aliceMessage.ciphertext,
aliceJid, aliceJid,
(await aliceSession.getDevice()).id, await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(bobMessage, null); expect(bobMessage, null);
// This call must not cause an exception // This call must not cause an exception
bobSession.getRatchet(aliceJid, (await aliceSession.getDevice()).id); bobSession.getRatchet(aliceJid, await aliceSession.getDeviceId());
}); });
test('Test rotating the Signed Prekey', () async { test('Test rotating the Signed Prekey', () async {
@@ -316,7 +369,7 @@ void main() {
bobJid, bobJid,
messagePlaintext, messagePlaintext,
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
expect(aliceMessage.encryptedKeys.length, 1); expect(aliceMessage.encryptedKeys.length, 1);
@@ -331,8 +384,9 @@ void main() {
final bobMessage = await bobSession.decryptMessage( final bobMessage = await bobSession.decryptMessage(
aliceMessage.ciphertext, aliceMessage.ciphertext,
aliceJid, aliceJid,
(await aliceSession.getDevice()).id, await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
}); });
@@ -358,7 +412,7 @@ void main() {
bobJid, bobJid,
null, null,
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
@@ -387,7 +441,7 @@ void main() {
bobJid, bobJid,
'Hello Bob!', 'Hello Bob!',
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
@@ -397,10 +451,15 @@ void main() {
await bobSession.decryptMessage( await bobSession.decryptMessage(
aliceMessage.ciphertext, aliceMessage.ciphertext,
aliceJid, aliceJid,
(await aliceSession.getDevice()).id, await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
0,
); );
// Ratchets are acked
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
for (var i = 0; i < 100; i++) { for (var i = 0; i < 100; i++) {
final messageText = 'Test Message #$i'; final messageText = 'Test Message #$i';
// Bob responds to Alice // Bob responds to Alice
@@ -416,8 +475,9 @@ void main() {
final aliceReceivedMessage = await aliceSession.decryptMessage( final aliceReceivedMessage = await aliceSession.decryptMessage(
bobResponseMessage.ciphertext, bobResponseMessage.ciphertext,
bobJid, bobJid,
(await bobSession.getDevice()).id, await bobSession.getDeviceId(),
bobResponseMessage.encryptedKeys, bobResponseMessage.encryptedKeys,
0,
); );
expect(messageText, aliceReceivedMessage); expect(messageText, aliceReceivedMessage);
} }
@@ -448,14 +508,14 @@ void main() {
bobJid, bobJid,
'Hallo Welt', 'Hallo Welt',
newSessions: [ newSessions: [
await (await bobSession1.getDevice()).toBundle(), await bobSession1.getDeviceBundle(),
await (await bobSession2.getDevice()).toBundle(), await bobSession2.getDeviceBundle(),
], ],
); );
// One of those two sessions is broken, so Alice removes the session2 ratchet // One of those two sessions is broken, so Alice removes the session2 ratchet
final id1 = (await bobSession1.getDevice()).id; final id1 = await bobSession1.getDeviceId();
final id2 = (await bobSession2.getDevice()).id; final id2 = await bobSession2.getDeviceId();
await aliceSession.removeRatchet(bobJid, id1); await aliceSession.removeRatchet(bobJid, id1);
final map = aliceSession.getRatchetMap(); final map = aliceSession.getRatchetMap();
@@ -485,12 +545,12 @@ void main() {
bobJid, bobJid,
'Hallo Welt', 'Hallo Welt',
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
// One of those two sessions is broken, so Alice removes the session2 ratchet // One of those two sessions is broken, so Alice removes the session2 ratchet
final id = (await bobSession.getDevice()).id; final id = await bobSession.getDeviceId();
await aliceSession.removeRatchet(bobJid, id); await aliceSession.removeRatchet(bobJid, id);
final map = aliceSession.getRatchetMap(); final map = aliceSession.getRatchetMap();
@@ -520,13 +580,13 @@ void main() {
bobJid, bobJid,
'Hallo Welt', 'Hallo Welt',
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
expect( expect(
await aliceSession.getUnacknowledgedRatchets(bobJid), await aliceSession.getUnacknowledgedRatchets(bobJid),
[ [
(await bobSession.getDevice()).id, await bobSession.getDeviceId(),
], ],
); );
@@ -537,10 +597,350 @@ void main() {
// ... // ...
// Alice marks the ratchet as acknowledged // Alice marks the ratchet as acknowledged
await aliceSession.ratchetAcknowledged(bobJid, (await bobSession.getDevice()).id); await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
expect( expect(
(await aliceSession.getUnacknowledgedRatchets(bobJid)).isEmpty, (await aliceSession.getUnacknowledgedRatchets(bobJid))!.isEmpty,
true, true,
); );
}); });
test('Test overwriting sessions', () 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,
0,
);
final aliceRatchet1 = aliceSession.getRatchet(
bobJid,
await bobSession.getDeviceId(),
);
final bobRatchet1 = bobSession.getRatchet(
aliceJid,
await aliceSession.getDeviceId(),
);
// Alice is impatient and immediately sends another message before the original one
// can be acknowledged by Bob
final msg2 = await aliceSession.encryptToJid(
bobJid,
"Why don't you answer?",
newSessions: [
await bobSession.getDeviceBundle(),
],
);
await bobSession.decryptMessage(
msg2.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg2.encryptedKeys,
getTimestamp(),
);
final aliceRatchet2 = aliceSession.getRatchet(
bobJid,
await bobSession.getDeviceId(),
);
final bobRatchet2 = bobSession.getRatchet(
aliceJid,
await aliceSession.getDeviceId(),
);
// Both should only have one ratchet
expect(aliceSession.getRatchetMap().length, 1);
expect(bobSession.getRatchetMap().length, 1);
// The ratchets should both be different
expect(await aliceRatchet1.equals(aliceRatchet2), false);
expect(await bobRatchet1.equals(bobRatchet2), false);
});
test('Test resending key exchanges', () 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(),
],
);
// The first message should be a kex message
expect(msg1.encryptedKeys.first.kex, true);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
0,
);
// Alice is impatient and immediately sends another message before the original one
// can be acknowledged by Bob
final msg2 = await aliceSession.encryptToJid(
bobJid,
"Why don't you answer?",
);
expect(msg2.encryptedKeys.first.kex, true);
await bobSession.decryptMessage(
msg2.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg2.encryptedKeys,
getTimestamp(),
);
});
test('Test receiving old messages including 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,
);
final bobsReceivedMessages = List<EncryptionResult>.empty(growable: true);
final bobsReceivedMessagesTimestamps = List<int>.empty(growable: true);
// Alice sends Bob a message
final msg1 = await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
bobsReceivedMessages.add(msg1);
final t1 = getTimestamp();
bobsReceivedMessagesTimestamps.add(t1);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
t1,
);
// Ratchets are acked
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
// Bob responds
final msg2 = await bobSession.encryptToJid(
aliceJid,
'Hello!',
);
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
getTimestamp(),
);
// Send some messages between the two
for (var i = 0; i < 100; i++) {
final msg = await aliceSession.encryptToJid(
bobJid,
'Hello $i',
);
bobsReceivedMessages.add(msg);
final t = getTimestamp();
bobsReceivedMessagesTimestamps.add(t);
final result = await bobSession.decryptMessage(
msg.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg.encryptedKeys,
t,
);
expect(result, 'Hello $i');
}
// Due to some issue with the transport protocol, the messages to Bob are received
// again.
final ratchetPreError = bobSession
.getRatchet(aliceJid, await aliceSession.getDeviceId())
.clone();
var invalidKex = 0;
var errorCounter = 0;
for (var i = 0; i < bobsReceivedMessages.length; i++) {
final msg = bobsReceivedMessages[i];
try {
await bobSession.decryptMessage(
msg.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg.encryptedKeys,
bobsReceivedMessagesTimestamps[i],
);
expect(true, false);
} on InvalidMessageHMACException catch (_) {
errorCounter++;
} on InvalidKeyExchangeException catch (_) {
invalidKex++;
}
}
final ratchetPostError = bobSession
.getRatchet(aliceJid, await aliceSession.getDeviceId())
.clone();
// The 100 messages including the initial KEX message
expect(invalidKex, 1);
expect(errorCounter, 100);
expect(await ratchetPreError.equals(ratchetPostError), true);
final msg3 = await aliceSession.encryptToJid(
bobJid,
'Are you okay?',
);
final result = await bobSession.decryptMessage(
msg3.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg3.encryptedKeys,
104,
);
expect(result, 'Are you okay?');
});
test("Test ignoring a new KEX when we haven't acket it yet", () 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
final msg1 = await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
expect(msg1.encryptedKeys.first.kex, true);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
getTimestamp(),
);
// Alice sends another message before the ack can reach us
final msg2 = await aliceSession.encryptToJid(
bobJid,
'ANSWER ME!',
);
expect(msg2.encryptedKeys.first.kex, true);
await bobSession.decryptMessage(
msg2.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg2.encryptedKeys,
getTimestamp(),
);
// Now the acks reach us
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
await bobSession.ratchetAcknowledged(aliceJid, await aliceSession.getDeviceId());
// Alice sends another message
final msg3 = await aliceSession.encryptToJid(
bobJid,
"You read the message, didn't you?",
);
expect(msg3.encryptedKeys.first.kex, false);
await bobSession.decryptMessage(
msg3.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg3.encryptedKeys,
getTimestamp(),
);
for (var i = 0; i < 100; i++) {
final messageText = 'Test Message #$i';
// Bob responds to Alice
final bobResponseMessage = await bobSession.encryptToJid(
aliceJid,
messageText,
);
// Bob sends the message to Alice
// ...
// Alice decrypts it
final aliceReceivedMessage = await aliceSession.decryptMessage(
bobResponseMessage.ciphertext,
bobJid,
await bobSession.getDeviceId(),
bobResponseMessage.encryptedKeys,
0,
);
expect(messageText, aliceReceivedMessage);
}
});
} }

View File

@@ -48,6 +48,10 @@ void main() {
<int>[172, 2], <int>[172, 2],
); );
}); });
test('Test some special cases', () {
expect(decodeVarint(encodeVarint(1042464893), 0).n, 1042464893);
});
}); });
group('OMEMOMessage', () { group('OMEMOMessage', () {
@@ -170,5 +174,13 @@ void main() {
expect(decoded.message!.mac, <int>[5, 6, 8, 0]); expect(decoded.message!.mac, <int>[5, 6, 8, 0]);
expect(decoded.message!.message, <int>[4, 5, 7, 3, 2]); 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/omemo_dart.dart';
import 'package:omemo_dart/src/trust/always.dart'; import 'package:omemo_dart/src/trust/always.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
Map<String, dynamic> jsonify(Map<String, dynamic> map) {
return jsonDecode(jsonEncode(map)) as Map<String, dynamic>;
}
void main() { void main() {
test('Test serialising and deserialising the Device', () async { test('Test serialising and deserialising the Device', () async {
// Generate a random session // Generate a random session
@@ -11,7 +16,7 @@ void main() {
opkAmount: 1, opkAmount: 1,
); );
final oldDevice = await oldSession.getDevice(); final oldDevice = await oldSession.getDevice();
final serialised = await oldDevice.toJson(); final serialised = jsonify(await oldDevice.toJson());
final newDevice = Device.fromJson(serialised); final newDevice = Device.fromJson(serialised);
expect(await oldDevice.equals(newDevice), true); expect(await oldDevice.equals(newDevice), true);
@@ -25,7 +30,7 @@ void main() {
opkAmount: 1, opkAmount: 1,
); );
final oldDevice = await (await oldSession.getDevice()).replaceSignedPrekey(); final oldDevice = await (await oldSession.getDevice()).replaceSignedPrekey();
final serialised = await oldDevice.toJson(); final serialised = jsonify(await oldDevice.toJson());
final newDevice = Device.fromJson(serialised); final newDevice = Device.fromJson(serialised);
expect(await oldDevice.equals(newDevice), true); expect(await oldDevice.equals(newDevice), true);
@@ -49,17 +54,18 @@ void main() {
bobJid, bobJid,
'Hello Bob!', 'Hello Bob!',
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
await bobSession.decryptMessage( await bobSession.decryptMessage(
aliceMessage.ciphertext, aliceMessage.ciphertext,
aliceJid, aliceJid,
(await aliceSession.getDevice()).id, await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
getTimestamp(),
); );
final aliceOld = aliceSession.getRatchet(bobJid, (await bobSession.getDevice()).id); final aliceOld = aliceSession.getRatchet(bobJid, await bobSession.getDeviceId());
final aliceSerialised = await aliceOld.toJson(); final aliceSerialised = jsonify(await aliceOld.toJson());
final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised); final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised);
expect(await aliceOld.equals(aliceNew), true); expect(await aliceOld.equals(aliceNew), true);
@@ -79,14 +85,16 @@ void main() {
); );
await oldSession.addSessionFromBundle( await oldSession.addSessionFromBundle(
'bob@localhost', 'bob@localhost',
(await bobSession.getDevice()).id, await bobSession.getDeviceId(),
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
); );
// Serialise and deserialise // Serialise and deserialise
final serialised = await oldSession.toJson(); final serialised = jsonify(await oldSession.toJsonWithoutSessions());
final newSession = OmemoSessionManager.fromJson( final newSession = OmemoSessionManager.fromJsonWithoutSessions(
serialised, serialised,
// NOTE: At this point, we don't care about this attribute
{},
AlwaysTrustingTrustManager(), AlwaysTrustingTrustManager(),
); );
@@ -94,14 +102,47 @@ void main() {
final newDevice = await newSession.getDevice(); final newDevice = await newSession.getDevice();
expect(await oldDevice.equals(newDevice), true); expect(await oldDevice.equals(newDevice), true);
expect(await oldSession.getDeviceMap(), await newSession.getDeviceMap()); expect(await oldSession.getDeviceMap(), await newSession.getDeviceMap());
});
expect(oldSession.getRatchetMap().length, newSession.getRatchetMap().length); test('Test serializing and deserializing RatchetMapKey', () {
for (final session in oldSession.getRatchetMap().entries) { const test1 = RatchetMapKey('user@example.org', 1234);
expect(newSession.getRatchetMap().containsKey(session.key), true); final result1 = RatchetMapKey.fromJsonKey(test1.toJsonKey());
expect(result1.jid, test1.jid);
expect(result1.deviceId, test1.deviceId);
final oldRatchet = oldSession.getRatchetMap()[session.key]!; const test2 = RatchetMapKey('user@example.org/hallo:welt', 3333);
final newRatchet = newSession.getRatchetMap()[session.key]!; final result2 = RatchetMapKey.fromJsonKey(test2.toJsonKey());
expect(await oldRatchet.equals(newRatchet), true); 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 // Caroline starts a chat a device from Alice
await btbv.onNewSession(aliceJid, 1); await btbv.onNewSession(aliceJid, 1);
expect(await btbv.isTrusted(aliceJid, 1), true); expect(await btbv.isTrusted(aliceJid, 1), true);
expect(await btbv.isEnabled(aliceJid, 1), true);
// Caroline meets with Alice and verifies her fingerprint // Caroline meets with Alice and verifies her fingerprint
await btbv.setDeviceTrust(aliceJid, 1, BTBVTrustState.verified); await btbv.setDeviceTrust(aliceJid, 1, BTBVTrustState.verified);
@@ -21,6 +22,7 @@ void main() {
await btbv.onNewSession(aliceJid, 2); await btbv.onNewSession(aliceJid, 2);
expect(await btbv.isTrusted(aliceJid, 2), false); expect(await btbv.isTrusted(aliceJid, 2), false);
expect(btbv.getDeviceTrust(aliceJid, 2), BTBVTrustState.notTrusted); 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 // Caronline starts a chat with Bob but since they live far apart, Caroline cannot
// verify his fingerprint. // verify his fingerprint.
@@ -32,5 +34,7 @@ void main() {
expect(await btbv.isTrusted(bobJid, 4), true); expect(await btbv.isTrusted(bobJid, 4), true);
expect(btbv.getDeviceTrust(bobJid, 3), BTBVTrustState.blindTrust); expect(btbv.getDeviceTrust(bobJid, 3), BTBVTrustState.blindTrust);
expect(btbv.getDeviceTrust(bobJid, 4), BTBVTrustState.blindTrust); expect(btbv.getDeviceTrust(bobJid, 4), BTBVTrustState.blindTrust);
expect(await btbv.isEnabled(bobJid, 3), true);
expect(await btbv.isEnabled(bobJid, 4), true);
}); });
} }