39 Commits

Author SHA1 Message Date
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
73613e266f feat: Allow regerating a device's id 2022-08-18 15:08:05 +02:00
0a03483aaf feat: Allow regenerating one's device identity 2022-08-18 15:02:17 +02:00
fda06cef55 feat: Implement acknowledging ratchet sessions 2022-08-16 14:02:04 +02:00
800b53b11f style: Fix linter warnings 2022-08-16 13:46:08 +02:00
5a097e4d2a feat: Allow removing a ratchet session 2022-08-16 13:27:21 +02:00
710b3c9497 feat: Allow serialising and deserialising without the ratchets 2022-08-16 12:57:16 +02:00
f540a80ec2 docs: Remove trust manager serialization from OmemoSessionManager 2022-08-16 12:54:15 +02:00
25 changed files with 1264 additions and 157 deletions

3
.gitignore vendored
View File

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

View File

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

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,24 @@
- 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

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.0
# [...] # [...]
# [...] # [...]

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(),
], ],
); );

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

@@ -67,6 +67,7 @@ class OmemoDoubleRatchet {
this.ik, this.ik,
this.sessionAd, this.sessionAd,
this.mkSkipped, // MKSKIPPED this.mkSkipped, // MKSKIPPED
this.acknowledged,
); );
factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) { factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) {
@@ -83,6 +84,7 @@ class OmemoDoubleRatchet {
'pn': 0, 'pn': 0,
'ik_pub': 'base/64/encoded', 'ik_pub': 'base/64/encoded',
'session_ad': 'base/64/encoded', 'session_ad': 'base/64/encoded',
'acknowledged': true | false,
'mkskipped': [ 'mkskipped': [
{ {
'key': 'base/64/encoded', 'key': 'base/64/encoded',
@@ -92,11 +94,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(
@@ -117,6 +128,7 @@ class OmemoDoubleRatchet {
), ),
base64.decode(data['session_ad']! as String), base64.decode(data['session_ad']! as String),
mkSkipped, mkSkipped,
data['acknowledged']! as bool,
); );
} }
@@ -148,6 +160,10 @@ class OmemoDoubleRatchet {
final Map<SkippedKey, List<int>> mkSkipped; final Map<SkippedKey, List<int>> mkSkipped;
/// Indicates whether we received an empty OMEMO message after building a session with
/// the device.
bool acknowledged;
/// Create an OMEMO session using the Signed Pre Key [spk], the shared secret [sk] that /// 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.
@@ -169,6 +185,7 @@ class OmemoDoubleRatchet {
ik, ik,
ad, ad,
{}, {},
false,
); );
} }
@@ -189,6 +206,7 @@ class OmemoDoubleRatchet {
ik, ik,
ad, ad,
{}, {},
false,
); );
} }
@@ -214,6 +232,7 @@ class OmemoDoubleRatchet {
'ik_pub': base64.encode(await ik.getBytes()), 'ik_pub': base64.encode(await ik.getBytes()),
'session_ad': base64.encode(sessionAd), 'session_ad': base64.encode(sessionAd),
'mkskipped': mkSkippedSerialised, 'mkskipped': mkSkippedSerialised,
'acknowledged': acknowledged,
}; };
} }

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,
@@ -163,6 +172,23 @@ class Device {
); );
} }
/// Returns a new device that is equal to this one with the exception that the new
/// device's id is a new number between 0 and 2**32 - 1.
@internal
Device withNewId() {
return Device(
jid,
generateRandom32BitNumber(),
ik,
spk,
spkId,
spkSignature,
oldSpk,
oldSpkId,
opks,
);
}
/// Converts this device into an OmemoBundle that could be used for publishing. /// Converts this device into an OmemoBundle that could be used for publishing.
Future<OmemoBundle> toBundle() async { Future<OmemoBundle> toBundle() async {
final encodedOpks = <int, String>{}; final encodedOpks = <int, String>{};

View File

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

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,20 +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;
}
/// Deserialise the OmemoSessionManager from JSON data [data] that does not contain
/// the ratchet sessions.
factory OmemoSessionManager.fromJsonWithoutSessions(
Map<String, dynamic> data,
Map<RatchetMapKey, OmemoDoubleRatchet> ratchetMap,
TrustManager trustManager,
) {
// 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,
); );
@@ -58,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;
@@ -83,13 +95,21 @@ class OmemoSessionManager {
/// A stream that receives events regarding the session /// A stream that receives events regarding the session
Stream<OmemoEvent> get eventStream => _eventStreamController.stream; Stream<OmemoEvent> get eventStream => _eventStreamController.stream;
/// 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.
@@ -98,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));
@@ -151,24 +172,26 @@ class OmemoSessionManager {
Future<void> _addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async { Future<void> _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,
); );
@@ -229,6 +252,9 @@ 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);
@@ -269,6 +295,25 @@ class OmemoSessionManager {
); );
} }
/// In case a decryption error occurs, the Double Ratchet spec says to just restore
/// the ratchet to its old state. As such, this function restores the ratchet at
/// [mapKey] with [oldRatchet].
Future<void> _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) async {
await _lock.synchronized(() {
_log.finest('Restoring ratchet ${mapKey.jid}:${mapKey.deviceId}');
_ratchetMap[mapKey] = oldRatchet;
// Commit the ratchet
_eventStreamController.add(
RatchetModifiedEvent(
mapKey.jid,
mapKey.deviceId,
oldRatchet,
),
);
});
}
/// Attempt to decrypt [ciphertext]. [keys] refers to the <key /> elements inside the /// 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
@@ -286,9 +331,15 @@ class OmemoSessionManager {
throw NotEncryptedForDeviceException(); throw NotEncryptedForDeviceException();
} }
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
final decodedRawKey = base64.decode(rawKey.value); final decodedRawKey = base64.decode(rawKey.value);
OmemoAuthenticatedMessage authMessage; OmemoAuthenticatedMessage authMessage;
OmemoDoubleRatchet? oldRatchet;
if (rawKey.kex) { if (rawKey.kex) {
// If the ratchet already existed, we store it. If it didn't, oldRatchet will stay
// null.
oldRatchet = await _getRatchet(ratchetKey);
// TODO(PapaTutuWawa): Only do this when we should // TODO(PapaTutuWawa): Only do this when we should
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey); final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
await _addSessionFromKeyExchange( await _addSessionFromKeyExchange(
@@ -319,31 +370,38 @@ class OmemoSessionManager {
} }
final message = OmemoMessage.fromBuffer(authMessage.message!); final message = OmemoMessage.fromBuffer(authMessage.message!);
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
List<int>? keyAndHmac; List<int>? keyAndHmac;
await _lock.synchronized(() async { // We can guarantee that the ratchet exists at this point in time
final ratchet = _ratchetMap[ratchetKey]!; final ratchet = (await _getRatchet(ratchetKey))!;
oldRatchet ??= ratchet ;
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 (_) {
await _restoreRatchet(ratchetKey, oldRatchet);
rethrow;
}
// Commit the ratchet // Commit the ratchet
_eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet)); _eventStreamController.add(RatchetModifiedEvent(senderJid, senderDeviceId, ratchet));
});
// Empty OMEMO messages should just have the key decrypted and/or session set up. // Empty OMEMO messages should just have the key decrypted and/or session set up.
if (ciphertext == null) { if (ciphertext == null) {
return null; return null;
} }
final key = keyAndHmac!.sublist(0, 32); final key = keyAndHmac.sublist(0, 32);
final hmac = keyAndHmac!.sublist(32, 48); final hmac = keyAndHmac.sublist(32, 48);
final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString); final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
final computedHmac = await truncatedHmac(ciphertext, derivedKeys.authenticationKey); final computedHmac = await truncatedHmac(ciphertext, derivedKeys.authenticationKey);
if (!listsEqual(hmac, computedHmac)) { if (!listsEqual(hmac, computedHmac)) {
// TODO(PapaTutuWawa): I am unsure if we should restore the ratchet here
await _restoreRatchet(ratchetKey, oldRatchet);
throw InvalidMessageHMACException(); throw InvalidMessageHMACException();
} }
@@ -357,7 +415,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)]!;
@@ -374,6 +432,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
@@ -390,13 +458,107 @@ 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);
}
/// Removes the ratchet identified by [jid] and [deviceId] from the session manager.
/// Also triggers events for commiting the new device map to storage and removing
/// the old ratchet.
Future<void> removeRatchet(String jid, int deviceId) async {
await _lock.synchronized(() async { await _lock.synchronized(() async {
map = _deviceMap; // Remove the ratchet
}); _ratchetMap.remove(RatchetMapKey(jid, deviceId));
// Commit it
_eventStreamController.add(RatchetRemovedEvent(jid, deviceId));
return map!; // Remove the device from jid
_deviceMap[jid]!.remove(deviceId);
if (_deviceMap[jid]!.isEmpty) {
_deviceMap.remove(jid);
}
// Commit it
_eventStreamController.add(DeviceMapModifiedEvent(_deviceMap));
});
}
/// Removes all ratchets for Jid [jid]. Triggers a DeviceMapModified event at the end and an
/// RatchetRemovedEvent for each ratchet.
Future<void> removeAllRatchets(String jid) async {
await _lock.synchronized(() async {
for (final deviceId in _deviceMap[jid]!) {
// Remove the ratchet
_ratchetMap.remove(RatchetMapKey(jid, deviceId));
// Commit it
_eventStreamController.add(RatchetRemovedEvent(jid, deviceId));
}
// Remove the device from jid
_deviceMap.remove(jid);
// Commit it
_eventStreamController.add(DeviceMapModifiedEvent(_deviceMap));
});
}
/// Returns the list of device identifiers belonging to [jid] that are yet unacked, i.e.
/// we have not yet received an empty OMEMO message from.
Future<List<int>?> getUnacknowledgedRatchets(String jid) async {
return _lock.synchronized(() async {
final ret = List<int>.empty(growable: true);
final devices = _deviceMap[jid];
if (devices == null) return null;
for (final device in devices) {
final ratchet = _ratchetMap[RatchetMapKey(jid, device)]!;
if (!ratchet.acknowledged) ret.add(device);
}
return ret;
});
}
/// Returns true if the ratchet for [jid] with device identifier [deviceId] is
/// acknowledged. Returns false if not.
Future<bool> isRatchetAcknowledged(String jid, int deviceId) async {
return _lock.synchronized(() => _ratchetMap[RatchetMapKey(jid, deviceId)]!.acknowledged);
}
/// Mark the ratchet for device [deviceId] from [jid] as acked.
Future<void> ratchetAcknowledged(String jid, int deviceId) async {
await _lock.synchronized(() async {
final ratchet = _ratchetMap[RatchetMapKey(jid, deviceId)]!
..acknowledged = true;
// Commit it
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
});
}
/// Generates an entirely new device. May be useful when the user wants to reset their cryptographic
/// identity. Triggers an event to commit it to storage.
Future<void> regenerateDevice({ int opkAmount = 100 }) async {
await _deviceLock.synchronized(() async {
_device = await Device.generateNewDevice(_device.jid, opkAmount: opkAmount);
// Commit it
_eventStreamController.add(DeviceModifiedEvent(_device));
});
}
/// Make our device have a new identifier. Only useful before publishing it as a bundle
/// to make sure that our device has a id that is account unique.
Future<void> regenerateDeviceId() async {
await _deviceLock.synchronized(() async {
_device = _device.withNewId();
// Commit it
_eventStreamController.add(DeviceModifiedEvent(_device));
});
}
Future<OmemoDoubleRatchet?> _getRatchet(RatchetMapKey key) async {
return _lock.synchronized(() async {
return _ratchetMap[key];
});
} }
@visibleForTesting @visibleForTesting
@@ -406,7 +568,7 @@ class OmemoSessionManager {
Map<RatchetMapKey, OmemoDoubleRatchet> getRatchetMap() => _ratchetMap; Map<RatchetMapKey, OmemoDoubleRatchet> getRatchetMap() => _ratchetMap;
/// Serialise the entire session manager into a JSON object. /// Serialise the entire session manager into a JSON object.
Future<Map<String, dynamic>> toJson() async { Future<Map<String, dynamic>> toJsonWithoutSessions() async {
/* /*
{ {
'devices': { 'devices': {
@@ -415,32 +577,12 @@ class OmemoSessionManager {
... ...
}, },
'device': { ... }, 'device': { ... },
'sessions': [
{
'jid': 'alice@...',
'deviceId': 1,
'ratchet': { ... },
},
...
],
'trust': { ... }
} }
*/ */
final sessions = List<Map<String, dynamic>>.empty(growable: true);
for (final entry in _ratchetMap.entries) {
sessions.add({
'jid': entry.key.jid,
'deviceId': entry.key.deviceId,
'ratchet': await entry.value.toJson(),
});
}
return { return {
'devices': _deviceMap, 'devices': _deviceMap,
'device': await (await getDevice()).toJson(), 'device': await (await getDevice()).toJson(),
'sessions': sessions,
// TODO(PapaTutuWawa): Implement
'trust': <String, dynamic>{},
}; };
} }
} }

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.0
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

@@ -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,7 +88,7 @@ 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,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
@@ -81,7 +119,7 @@ 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,
); );
expect(bobResponseText, aliceReceivedMessage); expect(bobResponseText, aliceReceivedMessage);
@@ -115,8 +153,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,7 +168,7 @@ 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,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
@@ -149,7 +187,7 @@ 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,
); );
expect(bobResponseText, aliceReceivedMessage); expect(bobResponseText, aliceReceivedMessage);
@@ -192,8 +230,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,7 +243,7 @@ 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,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
@@ -214,13 +252,13 @@ 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,
); );
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 +279,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 +292,13 @@ 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,
); );
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 +354,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,7 +369,7 @@ 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,
); );
expect(messagePlaintext, bobMessage); expect(messagePlaintext, bobMessage);
@@ -358,7 +396,7 @@ void main() {
bobJid, bobJid,
null, null,
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
@@ -387,7 +425,7 @@ void main() {
bobJid, bobJid,
'Hello Bob!', 'Hello Bob!',
newSessions: [ newSessions: [
await (await bobSession.getDevice()).toBundle(), await bobSession.getDeviceBundle(),
], ],
); );
@@ -397,7 +435,7 @@ 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,
); );
@@ -416,10 +454,343 @@ 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,
); );
expect(messageText, aliceReceivedMessage); expect(messageText, aliceReceivedMessage);
} }
}); });
group('Test removing a ratchet', () {
test('Test removing a ratchet when the user has multiple', () async {
const aliceJid = 'alice@server.local';
const bobJid = 'bob@some.server.local';
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession1 = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession2 = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
// Alice sends a message to those two Bobs
await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession1.getDeviceBundle(),
await bobSession2.getDeviceBundle(),
],
);
// One of those two sessions is broken, so Alice removes the session2 ratchet
final id1 = await bobSession1.getDeviceId();
final id2 = await bobSession2.getDeviceId();
await aliceSession.removeRatchet(bobJid, id1);
final map = aliceSession.getRatchetMap();
expect(map.containsKey(RatchetMapKey(bobJid, id1)), false);
expect(map.containsKey(RatchetMapKey(bobJid, id2)), true);
final deviceMap = await aliceSession.getDeviceMap();
expect(deviceMap.containsKey(bobJid), true);
expect(deviceMap[bobJid], [id2]);
});
test('Test removing a ratchet when the user has only one', () async {
const aliceJid = 'alice@server.local';
const bobJid = 'bob@some.server.local';
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
// Alice sends a message to those two Bobs
await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
// One of those two sessions is broken, so Alice removes the session2 ratchet
final id = await bobSession.getDeviceId();
await aliceSession.removeRatchet(bobJid, id);
final map = aliceSession.getRatchetMap();
expect(map.containsKey(RatchetMapKey(bobJid, id)), false);
final deviceMap = await aliceSession.getDeviceMap();
expect(deviceMap.containsKey(bobJid), false);
});
});
test('Test acknowledging a ratchet', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
// Alice sends Bob a message
await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
expect(
await aliceSession.getUnacknowledgedRatchets(bobJid),
[
await bobSession.getDeviceId(),
],
);
// Bob sends alice an empty message
// ...
// Alice decrypts it
// ...
// Alice marks the ratchet as acknowledged
await aliceSession.ratchetAcknowledged(bobJid, await bobSession.getDeviceId());
expect(
(await aliceSession.getUnacknowledgedRatchets(bobJid))!.isEmpty,
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,
);
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,
);
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 receiving an old message that contains a KEX', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 2,
);
// Alice sends Bob a message
final msg1 = await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
// Bob responds
final msg2 = await bobSession.encryptToJid(
aliceJid,
'Hello!',
);
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
// Due to some issue with the transport protocol, the first message Bob received is
// received again
try {
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
expect(true, false);
} on InvalidMessageHMACException {
// NOOP
}
final msg3 = await aliceSession.encryptToJid(
bobJid,
'Are you okay?',
);
final result = await bobSession.decryptMessage(
msg3.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg3.encryptedKeys,
);
expect(result, 'Are you okay?');
});
test('Test receiving an old message that does not contain a KEX', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 2,
);
// Alice sends Bob a message
final msg1 = await aliceSession.encryptToJid(
bobJid,
'Hallo Welt',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
await bobSession.decryptMessage(
msg1.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg1.encryptedKeys,
);
// Bob responds
final msg2 = await bobSession.encryptToJid(
aliceJid,
'Hello!',
);
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
// Due to some issue with the transport protocol, the first message Alice received is
// received again.
try {
await aliceSession.decryptMessage(
msg2.ciphertext,
bobJid,
await bobSession.getDeviceId(),
msg2.encryptedKeys,
);
expect(true, false);
} catch (_) {
// NOOP
}
final msg3 = await aliceSession.encryptToJid(
bobJid,
'Are you okay?',
);
final result = await bobSession.decryptMessage(
msg3.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
msg3.encryptedKeys,
);
expect(result, 'Are you okay?');
});
} }

View File

@@ -48,6 +48,10 @@ void main() {
<int>[172, 2], <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,17 @@ 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,
); );
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 +84,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 +101,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);
}); });
} }