28 Commits

Author SHA1 Message Date
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
44ab31aebb release: Bump version to 0.1.3 2022-08-11 12:06:02 +02:00
e4f1d7d4b0 docs: Fix indent in README 2022-08-11 12:04:40 +02:00
7600804aa1 test: Uncomment those tests 2022-08-11 12:02:33 +02:00
a4589b6e09 feat: Allow access to the device map 2022-08-11 12:02:21 +02:00
d0986a4608 fix: Fix ratchet only working for one message 2022-08-11 11:57:33 +02:00
683a76cc80 test: Add test for sending multiple OMEMO messages 2022-08-10 17:24:35 +02:00
dad707f71d release: Sigh, bump to 0.1.2 2022-08-09 16:40:41 +02:00
419be8af4d Revert "meta: Lower Dart SDK requirement"
This reverts commit fafc4f2320.
2022-08-09 16:40:19 +02:00
fafc4f2320 meta: Lower Dart SDK requirement 2022-08-09 16:32:59 +02:00
fc842fe000 docs: Add note about using omemo_dart 2022-08-09 16:05:05 +02:00
e3de50e0c7 release: Make pub happy 2022-08-09 16:02:20 +02:00
234c2088b9 docs: Add notes on protobuf 2022-08-09 16:00:12 +02:00
ad3f1d4579 release: Publish to polynom.me 2022-08-09 15:51:22 +02:00
5bec3b4587 docs: Add note about events to the example 2022-08-09 15:49:40 +02:00
6e7b8e3905 docs: Add an example 2022-08-09 15:48:26 +02:00
cb43bbb112 fix: Allow empty OMEMO messages to bypass trust 2022-08-09 14:45:04 +02:00
fa16f97113 refactor: Move events to lib/src/omemo 2022-08-08 19:11:27 +02:00
5158c32c3d feat: Make BlindTrustBeforeVerificationTrustManager abstract 2022-08-08 18:58:53 +02:00
23 changed files with 752 additions and 404 deletions

3
.gitignore vendored
View File

@@ -12,3 +12,6 @@ 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]

View File

@@ -1,3 +1,17 @@
## 1.0.0 ## 0.1.0
- Initial version. - Initial version
- Implement the Double Ratchet, X3DH and OMEMO specific bits
- Add a Blind-Trust-Before-Verification TrustManager
- Supported OMEMO version: 0.8.3
## 0.1.3
- Fix bug with the Double Ratchet causing only the initial message to be decryptable
- 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

View File

View File

@@ -18,11 +18,32 @@ the stanza format of your preferred XMPP library yourself.
- **Please note that this library has not been audited for its security! Use at your own risk!** - **Please note that this library has not been audited for its security! Use at your own risk!**
- This library is not tested with other implementations of OMEMO 0.8.3 as I do not know of any client implementing spec compliant OMEMO 0.8.3. It does, however, work with itself. - This library is not tested with other implementations of OMEMO 0.8.3 as I do not know of any client implementing spec compliant OMEMO 0.8.3. It does, however, work with itself.
## Usage
Include `omemo_dart` in your `pubspec.yaml` like this:
```yaml
# [...]
dependencies:
omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
version: ^0.1.0
# [...]
# [...]
```
## Contributing ## Contributing
Due to issues with `protobuf`, `omemo_dart` reimplements the Protobuf encoding for the required Due to issues with `protobuf`, `omemo_dart` reimplements the Protobuf encoding for the required
OMEMO messages. As such, `protobuf` is only a dependency for testing that the serialisation and OMEMO messages. As such, `protobuf` is only a dependency for testing that the serialisation and
deserialisation of the custom implementation. deserialisation of the custom implementation. In order to run tests, you need the Protbuf
compiler. After that, making sure that
the [Dart Protobuf compiler addon](https://pub.dev/packages/protoc_plugin) and the
Protobuf compiler itself is in your PATH,
run `protoc -I=./protobuf/ --dart_out=lib/protobuf/ ./protobuf/schema.proto` in the
repository's root to generate the real Protobuf bindings.
When submitting a PR, please run the linter using `dart analyze` and make sure that all When submitting a PR, please run the linter using `dart analyze` and make sure that all
tests still pass using `dart test`. tests still pass using `dart test`.

View File

@@ -1,5 +1,155 @@
//import 'package:omemo_dart/omemo_dart.dart'; import 'dart:convert';
import 'package:omemo_dart/omemo_dart.dart';
void main() { /// This example aims to demonstrate how omemo_dart is used. Since omemo_dart is not
// TODO(PapaTutuWawa): Currently NOOP /// dependent on any XMPP library, you need to convert stanzas to the appropriate
/// intermediary format and back.
void main() async {
const aliceJid = 'alice@some.server';
const bobJid = 'bob@other.serve';
// You are Alice and want to begin using OMEMO, so you first create a SessionManager
final aliceSession = await OmemoSessionManager.generateNewIdentity(
// The bare Jid of Alice as a String
aliceJid,
// The trust manager we want to use. In this case, we use the provided one that
// implements "Blind Trust Before Verification". To make things simpler, we keep
// no persistent data and can thus use the MemoryBTBVTrustManager. If we wanted to keep
// the state, we would have to override BlindTrustBeforeVerificationTrustManager.
MemoryBTBVTrustManager(),
// Here we specify how many Onetime Prekeys we want to have. XEP-0384 recommends around
// 100 OPKs, so let's generate 100. The parameter defaults to 100.
//opkAmount: 100,
);
// Alice now wants to chat with Bob at his bare Jid "bob@other.server". To make things
// simple, we just generate the identity bundle ourselves. In the real world, we would
// request it using PEP and then convert the device bundle into a OmemoBundle object.
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
MemoryBTBVTrustManager(),
// Just for illustrative purposes
opkAmount: 1,
);
// Alice prepares to send the message to Bob, so she builds the message stanza and
// collects all the children of the stanza that should be encrypted into a string.
const aliceMessageStanzaBody = '''
<body>Hello Bob, it's me, Alice!</body>
<super-secret-element xmlns='super-secret-element' />
''';
// Since OMEMO 0.8.3 mandates usage of XEP-0420: Stanza Content Encryption, we have to
// wrap our acual payload - aliceMessageStanzaBody - into an SCE envelope. Note that
// the rpad element must contain a random string. See XEP-0420 for recommendations.
// OMEMO makes the <time /> element optional, but let's use for this example.
const envelope = '''
<envelope xmlns='urn:xmpp:sce:1'>
<content>
$aliceMessageStanzaBody
</content>
<rpad>s0m3-r4nd0m-b9t3s</rpad>
<from jid='$aliceJid' />
<time stamp='1969-07-20T21:56:15-05:00' />
</envelope>
''';
// Since Alice has no open session with Bob, we need to tell the session manager to build
// it when sending the message.
final message = await aliceSession.encryptToJid(
// The bare receiver Jid
bobJid,
// The envelope we want to encrypt
envelope,
// Since this is the first time Alice contacts Bob from this device, we need to create
// 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.
newSessions: [
await bobSession.getDeviceBundle(),
],
);
// Alice now builds the actual message stanza for Bob
final payload = base64.encode(message.ciphertext!);
final aliceDevice = await aliceSession.getDevice();
// ignore: unused_local_variable
final bobDevice = await bobSession.getDevice();
// Since we know we have just one key for Bob, we take a shortcut. However, in the real
// world, we have to serialise every EncryptedKey to a <key /> element and group them
// per Jid.
final key = message.encryptedKeys[0];
// Note that the key's "kex" attribute refers to key.kex. It just means that the
// encrypted key also contains the required data for Bob to build a session with Alice.
// ignore: unused_local_variable
final aliceStanza = '''
<message from='$aliceJid/device1' to='$bobJid/device2'>
<encrypted xmlns='urn:xmpp:omemo:2'>
<header sid='${aliceDevice.id}'>
<keys jid='$bobJid'>
<key rid='${key.rid} kex='true'>
${key.value}
</key>
</keys>
</header>
<payload>
$payload
</payload>
</encrypted>
</message>
''';
// Alice can now send this message to Bob using our preferred XMPP library.
// ...
// Bob now receives an OMEMO encrypted message from Alice and wants to decrypt it.
// Since we have just one key, let's just deserialise the one key by hand.
final keys = [
EncryptedKey(bobJid, key.rid, key.value, true),
];
// Bob extracts the payload and attempts to decrypt it.
// ignore: unused_local_variable
final bobMessage = await bobSession.decryptMessage(
// base64 decode the payload
base64.decode(payload),
// Specify the Jid of the sender
aliceJid,
// Specify the device identifier of the sender (the "sid" attribute of <header />)
aliceDevice.id,
// The deserialised keys
keys,
);
// All Bob has to do now is replace the OMEMO wrapper element
// <encrypted xmlns='urn:xmpp:omemo:2' />) with the content of the <content /> element
// of the envelope we just decrypted.
// Bob now has a session with Alice and can send encrypted message to her.
// Since they both used the BlindTrustBeforeVerificationTrustManager, they currently
// use blind trust, meaning that both Alice and Bob accept new devices without any
// hesitation. If Alice, however, decides to verify one of Bob's devices and sets
// it as verified using
// ```
// await aliceSession.trustManager.setDeviceTrust(bobJid, bobDevice.id, BTBVTrustState.verified)
// ```
// then Alice's OmemoSessionManager won't encrypt to new devices unless they are also
// verified. To prevent user confusion, you should check if every device is trusted
// before sending the message and ask the user for a trust decision.
// If you want to make the BlindTrustBeforeVerificationTrustManager persistent, then
// you need to subclass it and override the `Future<void> commitState()` and
// `Future<void> loadState()` functions. commitState is called everytime the internal
// state gets changed. loadState never gets automatically called but is more of a
// function for the user to restore the trust manager. In those functions you have
// access to `ratchetMap`, which maps a `RatchetMapKey` - essentially a tuple consisting
// of a bare Jid and the device identifier - to the trust state, and `devices` which
// maps a bare Jid to its device identifiers.
// To make the entire OmemoSessionManager persistent, you have two options:
// - use the provided `toJson()` and `fromJson()` functions. They, however, serialise
// and deserialise *ALL* known sessions, so it might be slow.
// - subscribe to the session manager's `eventStream`. There, events get triggered
// everytime a ratchet changes, our own device changes or the internal ratchet map
// gets changed. This give finer control over the the serialisation. The session
// manager can then be restored using its constructor. For a list of events, see
// lib/src/omemo/events.dart.
} }

View File

@@ -2,14 +2,15 @@ library omemo_dart;
export 'src/double_ratchet/double_ratchet.dart'; export 'src/double_ratchet/double_ratchet.dart';
export 'src/errors.dart'; export 'src/errors.dart';
export 'src/events.dart';
export 'src/helpers.dart'; export 'src/helpers.dart';
export 'src/keys.dart'; export 'src/keys.dart';
export 'src/omemo/bundle.dart'; export 'src/omemo/bundle.dart';
export 'src/omemo/device.dart'; export 'src/omemo/device.dart';
export 'src/omemo/encrypted_key.dart'; export 'src/omemo/encrypted_key.dart';
export 'src/omemo/encryption_result.dart'; export 'src/omemo/encryption_result.dart';
export 'src/omemo/events.dart';
export 'src/omemo/fingerprint.dart'; export 'src/omemo/fingerprint.dart';
export 'src/omemo/ratchet_map_key.dart';
export 'src/omemo/sessionmanager.dart'; export 'src/omemo/sessionmanager.dart';
export 'src/trust/base.dart'; export 'src/trust/base.dart';
export 'src/trust/btbv.dart'; export 'src/trust/btbv.dart';

0
lib/protobuf/.gitkeep Normal file
View File

View File

@@ -1,263 +0,0 @@
///
// 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

@@ -1,7 +0,0 @@
///
// 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

@@ -1,48 +0,0 @@
///
// 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

@@ -1,9 +0,0 @@
///
// 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',
@@ -117,6 +119,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 +151,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 +176,7 @@ class OmemoDoubleRatchet {
ik, ik,
ad, ad,
{}, {},
false,
); );
} }
@@ -189,6 +197,7 @@ class OmemoDoubleRatchet {
ik, ik,
ad, ad,
{}, {},
true,
); );
} }
@@ -214,6 +223,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,
}; };
} }
@@ -293,7 +303,11 @@ class OmemoDoubleRatchet {
return plaintext; return plaintext;
} }
if (header.dhPub != await dhr?.getBytes()) { final dhPubMatches = listsEqual(
header.dhPub ?? <int>[],
await dhr?.getBytes() ?? <int>[],
);
if (!dhPubMatches) {
await _skipMessageKeys(header.pn!); await _skipMessageKeys(header.pn!);
await _dhRatchet(header); await _dhRatchet(header);
} }

View File

@@ -163,6 +163,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

@@ -1,6 +1,5 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
@internal
@immutable @immutable
class RatchetMapKey { class RatchetMapKey {

View File

@@ -7,13 +7,13 @@ 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';
import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/errors.dart';
import 'package:omemo_dart/src/events.dart';
import 'package:omemo_dart/src/helpers.dart'; import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/keys.dart'; import 'package:omemo_dart/src/keys.dart';
import 'package:omemo_dart/src/omemo/bundle.dart'; import 'package:omemo_dart/src/omemo/bundle.dart';
import 'package:omemo_dart/src/omemo/device.dart'; import 'package:omemo_dart/src/omemo/device.dart';
import 'package:omemo_dart/src/omemo/encrypted_key.dart'; import 'package:omemo_dart/src/omemo/encrypted_key.dart';
import 'package:omemo_dart/src/omemo/encryption_result.dart'; import 'package:omemo_dart/src/omemo/encryption_result.dart';
import 'package:omemo_dart/src/omemo/events.dart';
import 'package:omemo_dart/src/omemo/fingerprint.dart'; import 'package:omemo_dart/src/omemo/fingerprint.dart';
import 'package:omemo_dart/src/omemo/ratchet_map_key.dart'; import 'package:omemo_dart/src/omemo/ratchet_map_key.dart';
import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart'; import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart';
@@ -50,6 +50,17 @@ class OmemoSessionManager {
); );
} }
/// Deserialise the OmemoSessionManager from JSON data [data] that does not contain
/// the ratchet sessions.
factory OmemoSessionManager.fromJsonWithoutSessions(Map<String, dynamic> data, Map<RatchetMapKey, OmemoDoubleRatchet> ratchetMap, TrustManager trustManager) {
return OmemoSessionManager(
Device.fromJson(data['device']! as Map<String, dynamic>),
data['devices']! as Map<String, List<int>>,
ratchetMap,
trustManager,
);
}
/// Generate a new cryptographic identity. /// Generate a new cryptographic identity.
static Future<OmemoSessionManager> generateNewIdentity(String jid, TrustManager trustManager, { int opkAmount = 100 }) async { static Future<OmemoSessionManager> generateNewIdentity(String jid, TrustManager trustManager, { int opkAmount = 100 }) async {
assert(opkAmount > 0, 'opkAmount must be bigger than 0.'); assert(opkAmount > 0, 'opkAmount must be bigger than 0.');
@@ -78,10 +89,12 @@ class OmemoSessionManager {
/// The trust manager /// The trust manager
final TrustManager _trustManager; final TrustManager _trustManager;
TrustManager get trustManager => _trustManager;
/// 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; Device? dev;
await _deviceLock.synchronized(() async { await _deviceLock.synchronized(() async {
@@ -91,27 +104,40 @@ class OmemoSessionManager {
return dev!; 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.
Future<void> _addSession(String jid, int deviceId, OmemoDoubleRatchet ratchet) async { Future<void> _addSession(String jid, int deviceId, OmemoDoubleRatchet ratchet) async {
await _lock.synchronized(() async { await _lock.synchronized(() async {
// 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));
@@ -224,8 +250,11 @@ class OmemoSessionManager {
// We assume that the user already checked if the session exists // We assume that the user already checked if the session exists
for (final jid in jids) { for (final jid in jids) {
for (final deviceId in _deviceMap[jid]!) { for (final deviceId in _deviceMap[jid]!) {
// Empty OMEMO messages are allowed to bypass trust
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;
}
final ratchetKey = RatchetMapKey(jid, deviceId); final ratchetKey = RatchetMapKey(jid, deviceId);
final ratchet = _ratchetMap[ratchetKey]!; final ratchet = _ratchetMap[ratchetKey]!;
@@ -383,11 +412,89 @@ class OmemoSessionManager {
}); });
} }
@visibleForTesting /// Returns the device map, i.e. the mapping of bare Jid to its device identifiers
OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!; /// we have built sessions with.
Future<Map<String, List<int>>> getDeviceMap() async {
Map<String, List<int>>? map;
await _lock.synchronized(() async {
map = _deviceMap;
});
return map!;
}
/// Removes the ratchet identified by [jid] and [deviceId] from the session manager.
/// Also triggers events for commiting the new device map to storage and removing
/// the old ratchet.
Future<void> removeRatchet(String jid, int deviceId) async {
await _lock.synchronized(() async {
// Remove the ratchet
_ratchetMap.remove(RatchetMapKey(jid, deviceId));
// Commit it
_eventStreamController.add(RatchetRemovedEvent(jid, deviceId));
// Remove the device from jid
_deviceMap[jid]!.remove(deviceId);
if (_deviceMap[jid]!.isEmpty) {
_deviceMap.remove(jid);
}
// Commit it
_eventStreamController.add(DeviceMapModifiedEvent(_deviceMap));
});
}
/// Returns the list of device identifiers belonging to [jid] that are yet unacked, i.e.
/// we have not yet received an empty OMEMO message from.
Future<List<int>> getUnacknowledgedRatchets(String jid) async {
final ret = List<int>.empty(growable: true);
await _lock.synchronized(() async {
final devices = _deviceMap[jid]!;
for (final device in devices) {
final ratchet = _ratchetMap[RatchetMapKey(jid, device)]!;
if (!ratchet.acknowledged) ret.add(device);
}
});
return ret;
}
/// Mark the ratchet for device [deviceId] from [jid] as acked.
Future<void> ratchetAcknowledged(String jid, int deviceId) async {
await _lock.synchronized(() async {
final ratchet = _ratchetMap[RatchetMapKey(jid, deviceId)]!
..acknowledged = true;
// Commit it
_eventStreamController.add(RatchetModifiedEvent(jid, deviceId, ratchet));
});
}
/// 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));
});
}
@visibleForTesting @visibleForTesting
Map<String, List<int>> getDeviceMap() => _deviceMap; OmemoDoubleRatchet getRatchet(String jid, int deviceId) => _ratchetMap[RatchetMapKey(jid, deviceId)]!;
@visibleForTesting @visibleForTesting
Map<RatchetMapKey, OmemoDoubleRatchet> getRatchetMap() => _ratchetMap; Map<RatchetMapKey, OmemoDoubleRatchet> getRatchetMap() => _ratchetMap;
@@ -410,7 +517,6 @@ class OmemoSessionManager {
}, },
... ...
], ],
'trust': { ... }
} }
*/ */
@@ -426,8 +532,25 @@ class OmemoSessionManager {
'devices': _deviceMap, 'devices': _deviceMap,
'device': await (await getDevice()).toJson(), 'device': await (await getDevice()).toJson(),
'sessions': sessions, 'sessions': sessions,
// TODO(PapaTutuWawa): Implement };
'trust': <String, dynamic>{}, }
/// Serialise the entire session manager into a JSON object.
Future<Map<String, dynamic>> toJsonWithoutSessions() async {
/*
{
'devices': {
'alice@...': [1, 2, ...],
'bob@...': [1],
...
},
'device': { ... },
}
*/
return {
'devices': _deviceMap,
'device': await (await getDevice()).toJson(),
}; };
} }
} }

View File

@@ -13,29 +13,33 @@ enum BTBVTrustState {
verified, verified,
} }
class BlindTrustBeforeVerificationTrustManager extends TrustManager { /// A TrustManager that implements the idea of Blind Trust Before Verification.
/// See https://gultsch.de/trust.html for more details.
abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
BlindTrustBeforeVerificationTrustManager() BlindTrustBeforeVerificationTrustManager()
: _trustCache = {}, : trustCache = {},
_devices = {}, devices = {},
_lock = Lock(); _lock = Lock();
/// The cache for Mapping a RatchetMapKey to its trust state /// The cache for Mapping a RatchetMapKey to its trust state
final Map<RatchetMapKey, BTBVTrustState> _trustCache; @protected
final Map<RatchetMapKey, BTBVTrustState> trustCache;
/// Mapping of Jids to their device identifiers /// Mapping of Jids to their device identifiers
final Map<String, List<int>> _devices; @protected
final Map<String, List<int>> devices;
/// The lock for _devices and _trustCache /// The lock for devices and trustCache
final Lock _lock; final Lock _lock;
/// Returns true if [jid] has at least one device that is verified. If not, returns false. /// Returns true if [jid] has at least one device that is verified. If not, returns false.
/// Note that this function accesses _devices and _trustCache, which requires that the /// Note that this function accesses devices and trustCache, which requires that the
/// lock for those two maps (_lock) has been aquired before calling. /// lock for those two maps (_lock) has been aquired before calling.
bool _hasAtLeastOneVerifiedDevice(String jid) { bool _hasAtLeastOneVerifiedDevice(String jid) {
if (!_devices.containsKey(jid)) return false; if (!devices.containsKey(jid)) return false;
return _devices[jid]!.any((id) { return devices[jid]!.any((id) {
return _trustCache[RatchetMapKey(jid, id)]! == BTBVTrustState.verified; return trustCache[RatchetMapKey(jid, id)]! == BTBVTrustState.verified;
}); });
} }
@@ -43,7 +47,7 @@ class BlindTrustBeforeVerificationTrustManager extends TrustManager {
Future<bool> isTrusted(String jid, int deviceId) async { Future<bool> isTrusted(String jid, int deviceId) async {
var returnValue = false; var returnValue = false;
await _lock.synchronized(() async { await _lock.synchronized(() async {
final trustCacheValue = _trustCache[RatchetMapKey(jid, deviceId)]; final trustCacheValue = trustCache[RatchetMapKey(jid, deviceId)];
if (trustCacheValue == BTBVTrustState.notTrusted) { if (trustCacheValue == BTBVTrustState.notTrusted) {
returnValue = false; returnValue = false;
return; return;
@@ -71,16 +75,19 @@ class BlindTrustBeforeVerificationTrustManager extends TrustManager {
Future<void> onNewSession(String jid, int deviceId) async { Future<void> onNewSession(String jid, int deviceId) async {
await _lock.synchronized(() async { await _lock.synchronized(() async {
if (_hasAtLeastOneVerifiedDevice(jid)) { if (_hasAtLeastOneVerifiedDevice(jid)) {
_trustCache[RatchetMapKey(jid, deviceId)] = BTBVTrustState.notTrusted; trustCache[RatchetMapKey(jid, deviceId)] = BTBVTrustState.notTrusted;
} else { } else {
_trustCache[RatchetMapKey(jid, deviceId)] = BTBVTrustState.blindTrust; trustCache[RatchetMapKey(jid, deviceId)] = BTBVTrustState.blindTrust;
} }
if (_devices.containsKey(jid)) { if (devices.containsKey(jid)) {
_devices[jid]!.add(deviceId); devices[jid]!.add(deviceId);
} else { } else {
_devices[jid] = List<int>.from([deviceId]); devices[jid] = List<int>.from([deviceId]);
} }
// Commit the state
await commitState();
}); });
} }
@@ -89,8 +96,8 @@ class BlindTrustBeforeVerificationTrustManager extends TrustManager {
final map = <int, BTBVTrustState>{}; final map = <int, BTBVTrustState>{};
await _lock.synchronized(() async { await _lock.synchronized(() async {
for (final deviceId in _devices[jid]!) { for (final deviceId in devices[jid]!) {
map[deviceId] = _trustCache[RatchetMapKey(jid, deviceId)]!; map[deviceId] = trustCache[RatchetMapKey(jid, deviceId)]!;
} }
}); });
@@ -100,10 +107,33 @@ class BlindTrustBeforeVerificationTrustManager extends TrustManager {
/// Sets the trust of [jid]'s device with identifier [deviceId] to [state]. /// Sets the trust of [jid]'s device with identifier [deviceId] to [state].
Future<void> setDeviceTrust(String jid, int deviceId, BTBVTrustState state) async { Future<void> setDeviceTrust(String jid, int deviceId, BTBVTrustState state) async {
await _lock.synchronized(() async { await _lock.synchronized(() async {
_trustCache[RatchetMapKey(jid, deviceId)] = state; trustCache[RatchetMapKey(jid, deviceId)] = state;
// Commit the state
await commitState();
}); });
} }
/// Called when the state of the trust manager has been changed. Allows the user to
/// commit the trust state to persistent storage.
@visibleForOverriding
Future<void> commitState();
/// Called when the user wants to restore the state of the trust manager. The format
/// and actual storage mechanism is left to the user.
@visibleForOverriding
Future<void> loadState();
@visibleForTesting @visibleForTesting
BTBVTrustState getDeviceTrust(String jid, int deviceId) => _trustCache[RatchetMapKey(jid, deviceId)]!; BTBVTrustState getDeviceTrust(String jid, int deviceId) => trustCache[RatchetMapKey(jid, deviceId)]!;
}
/// A BTBV TrustManager that does not commit its state to persistent storage. Well suited
/// for testing.
class MemoryBTBVTrustManager extends BlindTrustBeforeVerificationTrustManager {
@override
Future<void> commitState() async {}
@override
Future<void> loadState() async {}
} }

14
lib/src/trust/never.dart Normal file
View File

@@ -0,0 +1,14 @@
import 'package:meta/meta.dart';
import 'package:omemo_dart/src/trust/base.dart';
/// Only use for testing!
/// An implementation of TrustManager that never trusts any device and thus
/// has no internal state.
@visibleForTesting
class NeverTrustingTrustManager extends TrustManager {
@override
Future<bool> isTrusted(String jid, int deviceId) async => false;
@override
Future<void> onNewSession(String jid, int deviceId) async {}
}

View File

@@ -1,6 +1,8 @@
name: omemo_dart name: omemo_dart
description: An XMPP library independent OMEMO library description: An XMPP library independent OMEMO library
version: 0.1.0 version: 0.2.0
homepage: https://github.com/PapaTutuWawa/omemo_dart
publish_to: https://git.polynom.me/api/packages/PapaTutuWawa/pub
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=2.17.0 <3.0.0'
@@ -9,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
meta: ^1.8.0
pinenacl: ^0.5.1 pinenacl: ^0.5.1
synchronized: ^3.0.0+2 synchronized: ^3.0.0+2

View File

@@ -1,12 +1,12 @@
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:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
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';
// Alice and Bob generate their sessions // Alice and Bob generate their sessions
var deviceModified = false; var deviceModified = false;
var ratchetModified = 0; var ratchetModified = 0;
@@ -38,7 +38,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 +50,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 +81,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 +115,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 +130,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 +149,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 +192,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 +205,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,7 +214,7 @@ 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);
@@ -241,7 +241,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 +254,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 +316,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,9 +331,287 @@ 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);
}); });
test('Test trust bypassing with empty OMEMO messages', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
NeverTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
NeverTrustingTrustManager(),
opkAmount: 1,
);
// Alice encrypts an empty message for Bob
final aliceMessage = await aliceSession.encryptToJid(
bobJid,
null,
newSessions: [
await bobSession.getDeviceBundle(),
],
);
// Despite Alice not trusting Bob's device, we should have encrypted it for his
// untrusted device.
expect(aliceMessage.encryptedKeys.length, 1);
});
test('Test by sending multiple messages back and forth', () 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 encrypts a message for Bob
final aliceMessage = await aliceSession.encryptToJid(
bobJid,
'Hello Bob!',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
// Alice sends the message to Bob
// ...
await bobSession.decryptMessage(
aliceMessage.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys,
);
for (var i = 0; i < 100; i++) {
final messageText = 'Test Message #$i';
// Bob responds to Alice
final bobResponseMessage = await bobSession.encryptToJid(
aliceJid,
messageText,
);
// Bob sends the message to Alice
// ...
// Alice decrypts it
final aliceReceivedMessage = await aliceSession.decryptMessage(
bobResponseMessage.ciphertext,
bobJid,
await bobSession.getDeviceId(),
bobResponseMessage.encryptedKeys,
);
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);
});
} }

View File

@@ -49,16 +49,16 @@ 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 = await aliceOld.toJson();
final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised); final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised);
@@ -79,8 +79,8 @@ 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
@@ -93,7 +93,7 @@ void main() {
final oldDevice = await oldSession.getDevice(); final oldDevice = await oldSession.getDevice();
final newDevice = await newSession.getDevice(); final newDevice = await newSession.getDevice();
expect(await oldDevice.equals(newDevice), true); expect(await oldDevice.equals(newDevice), true);
expect(oldSession.getDeviceMap(), newSession.getDeviceMap()); expect(await oldSession.getDeviceMap(), await newSession.getDeviceMap());
expect(oldSession.getRatchetMap().length, newSession.getRatchetMap().length); expect(oldSession.getRatchetMap().length, newSession.getRatchetMap().length);
for (final session in oldSession.getRatchetMap().entries) { for (final session in oldSession.getRatchetMap().entries) {

View File

@@ -4,7 +4,7 @@ import 'package:test/test.dart';
void main() { void main() {
test('Test the Blind Trust Before Verification TrustManager', () async { test('Test the Blind Trust Before Verification TrustManager', () async {
// Caroline's BTBV manager // Caroline's BTBV manager
final btbv = BlindTrustBeforeVerificationTrustManager(); final btbv = MemoryBTBVTrustManager();
// Example data // Example data
const aliceJid = 'alice@some.server'; const aliceJid = 'alice@some.server';
const bobJid = 'bob@other.server'; const bobJid = 'bob@other.server';