22 Commits

Author SHA1 Message Date
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
21 changed files with 844 additions and 135 deletions

3
.gitignore vendored
View File

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

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

@@ -20,3 +20,19 @@
- Add `isRatchetAcknowledged` - Add `isRatchetAcknowledged`
- Ratchets that are created due to accepting a kex are now unacknowledged - 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.2.0 version: ^0.3.1
# [...] # [...]
# [...] # [...]

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

@@ -94,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(

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,
); );
@@ -68,6 +66,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,7 @@ 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 /// Returns the id attribute of our own device. This is just a short-hand for
@@ -176,16 +172,18 @@ class OmemoSessionManager {
Future<void> _addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async { 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(
@@ -193,7 +191,7 @@ class OmemoSessionManager {
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,
); );
@@ -297,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
@@ -314,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(
@@ -347,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();
} }
@@ -385,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)]!;
@@ -402,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
@@ -441,6 +481,24 @@ 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 {
@@ -496,6 +554,12 @@ class OmemoSessionManager {
_eventStreamController.add(DeviceModifiedEvent(_device)); _eventStreamController.add(DeviceModifiedEvent(_device));
}); });
} }
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)]!;
@@ -503,42 +567,6 @@ class OmemoSessionManager {
@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

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

View File

@@ -16,4 +16,7 @@ abstract class TrustManager {
/// Mark the device with id [deviceId] of Jid [jid] as enabled if [enabled] is true or as disabled /// Mark the device with id [deviceId] of Jid [jid] as enabled if [enabled] is true or as disabled
/// if [enabled] is false. /// if [enabled] is false.
Future<void> setEnabled(String jid, int deviceId, bool enabled); Future<void> setEnabled(String jid, int deviceId, bool enabled);
/// Serialize the trust manager to JSON.
Future<Map<String, dynamic>> toJson();
} }

View File

@@ -8,29 +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,
enablementCache = {}, Map<RatchetMapKey, bool>? enablementCache,
devices = {}, Map<String, List<int>>? devices,
_lock = Lock(); }) : 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 /// The cache for mapping a RatchetMapKey to whether it is enabled or not
@visibleForTesting
@protected @protected
final Map<RatchetMapKey, bool> enablementCache; 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;
@@ -141,17 +164,56 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
// Commit the state // Commit the state
await commitState(); 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)]!;
} }
@@ -161,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

@@ -17,4 +17,7 @@ class NeverTrustingTrustManager extends TrustManager {
@override @override
Future<void> setEnabled(String jid, int deviceId, bool enabled) async {} 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.2.1 version: 0.3.1
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,6 +11,7 @@ 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
logging: ^1.0.2
meta: ^1.7.0 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';
@@ -220,7 +258,7 @@ void main() {
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';
@@ -614,4 +652,145 @@ void main() {
expect(await aliceRatchet1.equals(aliceRatchet2), false); expect(await aliceRatchet1.equals(aliceRatchet2), false);
expect(await bobRatchet1.equals(bobRatchet2), 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);
@@ -59,7 +64,7 @@ void main() {
aliceMessage.encryptedKeys, aliceMessage.encryptedKeys,
); );
final aliceOld = aliceSession.getRatchet(bobJid, await bobSession.getDeviceId()); 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);
@@ -84,9 +89,11 @@ void main() {
); );
// 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);
}); });
} }