Merge pull request #32 from PapaTutuWawa/rework

Rework omemo_dart to finally enable (hopefully) stable E2EE in Moxxy.
This commit is contained in:
PapaTutuWawa 2023-06-18 21:25:36 +02:00 committed by GitHub
commit 96ca643d26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 3634 additions and 4540 deletions

View File

@ -54,3 +54,14 @@
## 0.4.3 ## 0.4.3
- Fix bug that causes ratchets to be unable to decrypt anything after receiving a heartbeat with a completely new session - Fix bug that causes ratchets to be unable to decrypt anything after receiving a heartbeat with a completely new session
## 0.5.0
This version is a complete rework of omemo_dart!
- Removed events from `OmemoManager`
- Removed `OmemoSessionManager`
- Removed serialization/deserialization code
- Replace exceptions with errors inside a result type
- Ratchets and trust data is now loaded and cached on demand
- Accessing the trust manager must happen via `withTrustManager`

View File

@ -28,22 +28,32 @@ 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.4.3 version: ^0.5.0
# [...] # [...]
# [...] # [...]
``` ```
## Contributing ### Example
Due to issues with `protobuf`, `omemo_dart` reimplements the Protobuf encoding for the required This repository includes a documented ["example"](./example/omemo_dart_example.dart) that explains the basic usage of the library while
OMEMO messages. As such, `protobuf` is only a dependency for testing that the serialisation and leaving out the XMPP specific bits. For a more functional and integrated example, see the `omemo_client.dart` example from
deserialisation of the custom implementation. In order to run tests, you need the Protbuf [moxxmpp](https://codeberg.org/moxxy/moxxmpp).
compiler. After that, making sure that
the [Dart Protobuf compiler addon](https://pub.dev/packages/protoc_plugin) and the ### Persistence
Protobuf compiler itself is in your PATH,
run `protoc -I=./protobuf/ --dart_out=lib/protobuf/ ./protobuf/schema.proto` in the By default, `omemo_dart` uses in-memory implementations for everything. For a real-world application, this is unsuitable as OMEMO devices would be constantly added.
repository's root to generate the real Protobuf bindings. In order to allow persistence, your application needs to keep track of the following:
- The `OmemoDevice` assigned to the `OmemoManager`
- `JID -> [int]`: The device list for each JID
- `(JID, device) -> Ratchet`: The actual ratchet
If you also use the `BlindTrustBeforeVerificationTrustManager`, you additionally need to keep track of:
- `(JID, device) -> (int, bool)`: The trust level and the enablement state
## Contributing
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

@ -9,8 +9,5 @@ linter:
analyzer: analyzer:
exclude: exclude:
- "lib/protobuf/*.dart" - "lib/src/protobuf/*.dart"
# TODO: Remove once OmemoSessionManager is gone
- "test/omemo_test.dart"
- "example/omemo_dart_example.dart" - "example/omemo_dart_example.dart"
- "test/serialisation_test.dart"

View File

@ -35,6 +35,10 @@ void main() async {
// This needs to be wired into your XMPP library's OMEMO implementation. // This needs to be wired into your XMPP library's OMEMO implementation.
// For simplicity, we use an empty function and imagine it works. // For simplicity, we use an empty function and imagine it works.
(jid) async {}, (jid) async {},
// This function is called whenever our own device bundle has to be republished to our PEP node.
// This needs to be wired into your XMPP library's OMEMO implementation.
// For simplicity, we use an empty function and imagine it works.
(device) async {},
); );
// Alice now wants to chat with Bob at his bare Jid "bob@other.server". To make things // Alice now wants to chat with Bob at his bare Jid "bob@other.server". To make things
@ -42,7 +46,7 @@ void main() async {
// request it using PEP and then convert the device bundle into a OmemoBundle object. // request it using PEP and then convert the device bundle into a OmemoBundle object.
final bobManager = OmemoManager( final bobManager = OmemoManager(
await OmemoDevice.generateNewDevice(bobJid), await OmemoDevice.generateNewDevice(bobJid),
MemoryBTBVTrustManager(), BlindTrustBeforeVerificationTrustManager(),
(result, recipient) async => {}, (result, recipient) async => {},
(jid) async => [], (jid) async => [],
(jid, id) async => null, (jid, id) async => null,
@ -145,6 +149,11 @@ void main() async {
/// The text of the <payload /> element, if it exists. If not, then the message might be /// The text of the <payload /> element, if it exists. If not, then the message might be
/// a hearbeat, where no payload is sent. In that case, use null. /// a hearbeat, where no payload is sent. In that case, use null.
payload, payload,
/// Since we did not receive this message due to a catch-up mechanism, like MAM, we
/// set this to false. If we, however, did use a catch-up mechanism, we must set this
/// to true to prevent the OPKs from being replaced.
false,
), ),
); );

View File

@ -8,9 +8,10 @@ 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/errors.dart';
export 'src/omemo/fingerprint.dart'; export 'src/omemo/fingerprint.dart';
export 'src/omemo/omemomanager.dart'; export 'src/omemo/omemo.dart';
export 'src/omemo/ratchet_data.dart';
export 'src/omemo/ratchet_map_key.dart'; export 'src/omemo/ratchet_map_key.dart';
export 'src/omemo/stanza.dart'; export 'src/omemo/stanza.dart';
export 'src/trust/base.dart'; export 'src/trust/base.dart';

View File

@ -0,0 +1,11 @@
/// The overarching assumption is that we use Ed25519 keys for the identity keys
const omemoX3DHInfoString = 'OMEMO X3DH';
/// The info used for when encrypting the AES key for the actual payload.
const omemoPayloadInfoString = 'OMEMO Payload';
/// Info string for ENCRYPT
const encryptHkdfInfoString = 'OMEMO Message Key Material';
/// Amount of messages we may skip per session
const maxSkip = 1000;

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:moxlib/moxlib.dart';
import 'package:omemo_dart/src/errors.dart';
import 'package:omemo_dart/src/keys.dart'; import 'package:omemo_dart/src/keys.dart';
/// Performs X25519 with [kp] and [pk]. If [identityKey] is set, then /// Performs X25519 with [kp] and [pk]. If [identityKey] is set, then
@ -92,7 +94,7 @@ Future<List<int>> aes256CbcEncrypt(
/// A small helper function to make AES-256-CBC easier. Decrypt [ciphertext] using [key] as /// A small helper function to make AES-256-CBC easier. Decrypt [ciphertext] using [key] as
/// the encryption key and [iv] as the IV. Returns the ciphertext. /// the encryption key and [iv] as the IV. Returns the ciphertext.
Future<List<int>> aes256CbcDecrypt( Future<Result<MalformedCiphertextError, List<int>>> aes256CbcDecrypt(
List<int> ciphertext, List<int> ciphertext,
List<int> key, List<int> key,
List<int> iv, List<int> iv,
@ -100,13 +102,19 @@ Future<List<int>> aes256CbcDecrypt(
final algorithm = AesCbc.with256bits( final algorithm = AesCbc.with256bits(
macAlgorithm: MacAlgorithm.empty, macAlgorithm: MacAlgorithm.empty,
); );
return algorithm.decrypt( try {
NoMacSecretBox( return Result(
ciphertext, await algorithm.decrypt(
nonce: iv, NoMacSecretBox(
), ciphertext,
secretKey: SecretKey(key), nonce: iv,
); ),
secretKey: SecretKey(key),
),
);
} catch (ex) {
return Result(MalformedCiphertextError(ex));
}
} }
/// OMEMO often uses the output of a HMAC-SHA-256 truncated to its first 16 bytes. /// OMEMO often uses the output of a HMAC-SHA-256 truncated to its first 16 bytes.

View File

@ -1,60 +0,0 @@
import 'package:omemo_dart/src/crypto.dart';
import 'package:omemo_dart/src/errors.dart';
import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart';
import 'package:omemo_dart/src/protobuf/omemo_message.dart';
/// Info string for ENCRYPT
const encryptHkdfInfoString = 'OMEMO Message Key Material';
/// Signals ENCRYPT function as specified by OMEMO 0.8.3.
/// Encrypt [plaintext] using the message key [mk], given associated_data [associatedData]
/// and the AD output from the X3DH [sessionAd].
Future<List<int>> encrypt(
List<int> mk,
List<int> plaintext,
List<int> associatedData,
List<int> sessionAd,
) async {
// Generate encryption, authentication key and IV
final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString);
final ciphertext =
await aes256CbcEncrypt(plaintext, keys.encryptionKey, keys.iv);
final header =
OmemoMessage.fromBuffer(associatedData.sublist(sessionAd.length))
..ciphertext = ciphertext;
final headerBytes = header.writeToBuffer();
final hmacInput = concat([sessionAd, headerBytes]);
final hmacResult = await truncatedHmac(hmacInput, keys.authenticationKey);
final message = OmemoAuthenticatedMessage()
..mac = hmacResult
..message = headerBytes;
return message.writeToBuffer();
}
/// Signals DECRYPT function as specified by OMEMO 0.8.3.
/// Decrypt [ciphertext] with the message key [mk], given the associated_data [associatedData]
/// and the AD output from the X3DH.
Future<List<int>> decrypt(
List<int> mk,
List<int> ciphertext,
List<int> associatedData,
List<int> sessionAd,
) async {
// Generate encryption, authentication key and IV
final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString);
// Assumption ciphertext is a OMEMOAuthenticatedMessage
final message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
final header = OmemoMessage.fromBuffer(message.message!);
final hmacInput = concat([sessionAd, header.writeToBuffer()]);
final hmacResult = await truncatedHmac(hmacInput, keys.authenticationKey);
if (!listsEqual(hmacResult, message.mac!)) {
throw InvalidMessageHMACException();
}
return aes256CbcDecrypt(header.ciphertext!, keys.encryptionKey, keys.iv);
}

View File

@ -1,47 +1,24 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:hex/hex.dart'; import 'package:hex/hex.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:moxlib/moxlib.dart';
import 'package:omemo_dart/src/common/constants.dart';
import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/kdf.dart'; import 'package:omemo_dart/src/double_ratchet/kdf.dart';
import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/errors.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/protobuf/omemo_message.dart'; import 'package:omemo_dart/src/protobuf/schema.pb.dart';
/// Amount of messages we may skip per session
const maxSkip = 1000;
class RatchetStep {
const RatchetStep(this.header, this.ciphertext);
final OmemoMessage header;
final List<int> ciphertext;
}
@immutable @immutable
class SkippedKey { class SkippedKey {
const SkippedKey(this.dh, this.n); const SkippedKey(this.dh, this.n);
factory SkippedKey.fromJson(Map<String, dynamic> data) { /// The DH public key for which we skipped a message key.
return SkippedKey(
OmemoPublicKey.fromBytes(
base64.decode(data['public']! as String),
KeyPairType.x25519,
),
data['n']! as int,
);
}
final OmemoPublicKey dh; final OmemoPublicKey dh;
final int n;
Future<Map<String, dynamic>> toJson() async { /// The associated number of the message key we skipped.
return { final int n;
'public': base64.encode(await dh.getBytes()),
'n': n,
};
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@ -52,6 +29,28 @@ class SkippedKey {
int get hashCode => dh.hashCode ^ n.hashCode; int get hashCode => dh.hashCode ^ n.hashCode;
} }
@immutable
class KeyExchangeData {
const KeyExchangeData(
this.pkId,
this.spkId,
this.ik,
this.ek,
);
/// The id of the used OPK.
final int pkId;
/// The id of the used SPK.
final int spkId;
/// The ephemeral key used while the key exchange.
final OmemoPublicKey ek;
/// The identity key used in the key exchange.
final OmemoPublicKey ik;
}
class OmemoDoubleRatchet { class OmemoDoubleRatchet {
OmemoDoubleRatchet( OmemoDoubleRatchet(
this.dhs, // DHs this.dhs, // DHs
@ -66,77 +65,9 @@ class OmemoDoubleRatchet {
this.sessionAd, this.sessionAd,
this.mkSkipped, // MKSKIPPED this.mkSkipped, // MKSKIPPED
this.acknowledged, this.acknowledged,
this.kexTimestamp,
this.kex, this.kex,
); );
factory OmemoDoubleRatchet.fromJson(Map<String, dynamic> data) {
/*
{
'dhs': 'base/64/encoded',
'dhs_pub': 'base/64/encoded',
'dhr': null | 'base/64/encoded',
'rk': 'base/64/encoded',
'cks': null | 'base/64/encoded',
'ckr': null | 'base/64/encoded',
'ns': 0,
'nr': 0,
'pn': 0,
'ik_pub': null | 'base/64/encoded',
'session_ad': 'base/64/encoded',
'acknowledged': true | false,
'kex_timestamp': int,
'kex': 'base/64/encoded',
'mkskipped': [
{
'key': 'base/64/encoded',
'public': 'base/64/encoded',
'n': 0
}, ...
]
}
*/
// NOTE: Dart has some issues with just casting a List<dynamic> to List<Map<...>>, as
// such we need to convert the items by hand.
final mkSkipped = Map<SkippedKey, List<int>>.fromEntries(
(data['mkskipped']! as List<dynamic>)
.map<MapEntry<SkippedKey, List<int>>>(
(entry) {
final map = entry as Map<String, dynamic>;
final key = SkippedKey.fromJson(map);
return MapEntry(
key,
base64.decode(map['key']! as String),
);
},
),
);
return OmemoDoubleRatchet(
OmemoKeyPair.fromBytes(
base64.decode(data['dhs_pub']! as String),
base64.decode(data['dhs']! as String),
KeyPairType.x25519,
),
decodeKeyIfNotNull(data, 'dhr', KeyPairType.x25519),
base64.decode(data['rk']! as String),
base64DecodeIfNotNull(data, 'cks'),
base64DecodeIfNotNull(data, 'ckr'),
data['ns']! as int,
data['nr']! as int,
data['pn']! as int,
OmemoPublicKey.fromBytes(
base64.decode(data['ik_pub']! as String),
KeyPairType.ed25519,
),
base64.decode(data['session_ad']! as String),
mkSkipped,
data['acknowledged']! as bool,
data['kex_timestamp']! as int,
data['kex'] as String?,
);
}
/// Sending DH keypair /// Sending DH keypair
OmemoKeyPair dhs; OmemoKeyPair dhs;
@ -161,16 +92,14 @@ class OmemoDoubleRatchet {
/// for verification purposes /// for verification purposes
final OmemoPublicKey ik; final OmemoPublicKey ik;
/// Associated data for this ratchet.
final List<int> sessionAd; final List<int> sessionAd;
/// List of skipped message keys.
final Map<SkippedKey, List<int>> mkSkipped; final Map<SkippedKey, List<int>> mkSkipped;
/// The point in time at which we performed the kex exchange to create this ratchet.
/// Precision is milliseconds since epoch.
int kexTimestamp;
/// The key exchange that was used for initiating the session. /// The key exchange that was used for initiating the session.
final String? kex; final KeyExchangeData kex;
/// Indicates whether we received an empty OMEMO message after building a session with /// Indicates whether we received an empty OMEMO message after building a session with
/// the device. /// the device.
@ -181,21 +110,22 @@ class OmemoDoubleRatchet {
/// a X3DH. [ik] refers to Bob's (the receiver's) IK public key. /// a X3DH. [ik] refers to Bob's (the receiver's) IK public key.
static Future<OmemoDoubleRatchet> initiateNewSession( static Future<OmemoDoubleRatchet> initiateNewSession(
OmemoPublicKey spk, OmemoPublicKey spk,
int spkId,
OmemoPublicKey ik, OmemoPublicKey ik,
OmemoPublicKey ownIk,
OmemoPublicKey ek,
List<int> sk, List<int> sk,
List<int> ad, List<int> ad,
int timestamp, int pkId,
) async { ) async {
final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final dhr = spk; final rk = await kdfRk(sk, await omemoDH(dhs, spk, 0));
final rk = await kdfRk(sk, await omemoDH(dhs, dhr, 0));
final cks = rk;
return OmemoDoubleRatchet( return OmemoDoubleRatchet(
dhs, dhs,
dhr, spk,
rk, List.from(rk),
cks, List.from(rk),
null, null,
0, 0,
0, 0,
@ -204,8 +134,12 @@ class OmemoDoubleRatchet {
ad, ad,
{}, {},
false, false,
timestamp, KeyExchangeData(
'', pkId,
spkId,
ownIk,
ek,
),
); );
} }
@ -215,10 +149,12 @@ class OmemoDoubleRatchet {
/// Alice's (the initiator's) IK public key. /// Alice's (the initiator's) IK public key.
static Future<OmemoDoubleRatchet> acceptNewSession( static Future<OmemoDoubleRatchet> acceptNewSession(
OmemoKeyPair spk, OmemoKeyPair spk,
int spkId,
OmemoPublicKey ik, OmemoPublicKey ik,
int pkId,
OmemoPublicKey ek,
List<int> sk, List<int> sk,
List<int> ad, List<int> ad,
int kexTimestamp,
) async { ) async {
return OmemoDoubleRatchet( return OmemoDoubleRatchet(
spk, spk,
@ -233,72 +169,52 @@ class OmemoDoubleRatchet {
ad, ad,
{}, {},
true, true,
kexTimestamp, KeyExchangeData(
null, pkId,
spkId,
ik,
ek,
),
); );
} }
Future<Map<String, dynamic>> toJson() async { /// Performs a single ratchet step in case we received a new
final mkSkippedSerialised = /// public key in [header].
List<Map<String, dynamic>>.empty(growable: true); Future<void> _dhRatchet(OMEMOMessage header) async {
for (final entry in mkSkipped.entries) { pn = ns;
final result = await entry.key.toJson(); ns = 0;
result['key'] = base64.encode(entry.value); nr = 0;
dhr = OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519);
mkSkippedSerialised.add(result); final newRk1 = await kdfRk(
} rk,
await omemoDH(
return { dhs,
'dhs': base64.encode(await dhs.sk.getBytes()), dhr!,
'dhs_pub': base64.encode(await dhs.pk.getBytes()), 0,
'dhr': dhr != null ? base64.encode(await dhr!.getBytes()) : null, ),
'rk': base64.encode(rk),
'cks': cks != null ? base64.encode(cks!) : null,
'ckr': ckr != null ? base64.encode(ckr!) : null,
'ns': ns,
'nr': nr,
'pn': pn,
'ik_pub': base64.encode(await ik.getBytes()),
'session_ad': base64.encode(sessionAd),
'mkskipped': mkSkippedSerialised,
'acknowledged': acknowledged,
'kex_timestamp': kexTimestamp,
'kex': kex,
};
}
/// Returns the OMEMO compatible fingerprint of the ratchet session.
Future<String> getOmemoFingerprint() async {
final curveKey = await ik.toCurve25519();
return HEX.encode(await curveKey.getBytes());
}
Future<List<int>?> _trySkippedMessageKeys(
OmemoMessage header,
List<int> ciphertext,
) async {
final key = SkippedKey(
OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519),
header.n!,
); );
if (mkSkipped.containsKey(key)) { rk = List.from(newRk1);
final mk = mkSkipped[key]!; ckr = List.from(newRk1);
mkSkipped.remove(key);
return decrypt( dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
mk, final newRk2 = await kdfRk(
ciphertext, rk,
concat([sessionAd, header.writeToBuffer()]), await omemoDH(
sessionAd, dhs,
); dhr!,
} 0,
),
return null; );
rk = List.from(newRk2);
cks = List.from(newRk2);
} }
Future<void> _skipMessageKeys(int until) async { /// Skip (and keep track of) message keys until our receive counter is
/// equal to [until]. If we would skip too many messages, returns
/// a [SkippingTooManyKeysError]. If not, returns null.
Future<OmemoError?> _skipMessageKeys(int until) async {
if (nr + maxSkip < until) { if (nr + maxSkip < until) {
throw SkippingTooManyMessagesException(); return SkippingTooManyKeysError();
} }
if (ckr != null) { if (ckr != null) {
@ -306,88 +222,140 @@ class OmemoDoubleRatchet {
final newCkr = await kdfCk(ckr!, kdfCkNextChainKey); final newCkr = await kdfCk(ckr!, kdfCkNextChainKey);
final mk = await kdfCk(ckr!, kdfCkNextMessageKey); final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
ckr = newCkr; ckr = newCkr;
mkSkipped[SkippedKey(dhr!, nr)] = mk; mkSkipped[SkippedKey(dhr!, nr)] = mk;
nr++; nr++;
} }
} }
return null;
} }
Future<void> _dhRatchet(OmemoMessage header) async { /// Decrypt [ciphertext] using keys derived from the message key [mk]. Also computes the
pn = ns; /// HMAC from the [OMEMOMessage] embedded in [message].
ns = 0;
nr = 0;
dhr = OmemoPublicKey.fromBytes(header.dhPub!, KeyPairType.x25519);
final newRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
rk = List.from(newRk);
ckr = List.from(newRk);
dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
final newNewRk = await kdfRk(rk, await omemoDH(dhs, dhr!, 0));
rk = List.from(newNewRk);
cks = List.from(newNewRk);
}
/// Encrypt [plaintext] using the Double Ratchet.
Future<RatchetStep> ratchetEncrypt(List<int> plaintext) async {
final newCks = await kdfCk(cks!, kdfCkNextChainKey);
final mk = await kdfCk(cks!, kdfCkNextMessageKey);
cks = newCks;
final header = OmemoMessage()
..dhPub = await dhs.pk.getBytes()
..pn = pn
..n = ns;
ns++;
return RatchetStep(
header,
await encrypt(
mk,
plaintext,
concat([sessionAd, header.writeToBuffer()]),
sessionAd,
),
);
}
/// Decrypt a [ciphertext] that was sent with the header [header] using the Double
/// Ratchet. Returns the decrypted (raw) plaintext.
/// ///
/// Throws an SkippingTooManyMessagesException if too many messages were to be skipped. /// If the computed HMAC does not match the HMAC in [message], returns
Future<List<int>> ratchetDecrypt( /// [InvalidMessageHMACError]. If it matches, returns the decrypted
OmemoMessage header, /// payload.
Future<Result<OmemoError, List<int>>> _decrypt(
OMEMOAuthenticatedMessage message,
List<int> ciphertext, List<int> ciphertext,
List<int> mk,
) async { ) async {
// Check if we skipped too many messages final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString);
final plaintext = await _trySkippedMessageKeys(header, ciphertext);
if (plaintext != null) { final hmacInput = concat([sessionAd, message.message]);
return plaintext; final hmacResult = await truncatedHmac(hmacInput, keys.authenticationKey);
if (!listsEqual(hmacResult, message.mac)) {
return Result(InvalidMessageHMACError());
} }
final dhPubMatches = listsEqual( final plaintext =
header.dhPub!, await aes256CbcDecrypt(ciphertext, keys.encryptionKey, keys.iv);
(await dhr?.getBytes()) ?? <int>[], if (plaintext.isType<MalformedCiphertextError>()) {
return Result(plaintext.get<MalformedCiphertextError>());
}
return Result(plaintext.get<List<int>>());
}
/// Checks whether we could decrypt the payload in [header] with a skipped key. If yes,
/// attempts to decrypt it. If not, returns null.
///
/// If the decryption is successful, returns the plaintext payload. If an error occurs, like
/// an [InvalidMessageHMACError], that is returned instead.
Future<Result<OmemoError, List<int>?>> _trySkippedMessageKeys(
OMEMOAuthenticatedMessage message,
OMEMOMessage header,
) async {
final key = SkippedKey(
OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519),
header.n,
); );
if (!dhPubMatches) { if (mkSkipped.containsKey(key)) {
await _skipMessageKeys(header.pn!); final mk = mkSkipped[key]!;
mkSkipped.remove(key);
return _decrypt(message, header.ciphertext, mk);
}
return const Result(null);
}
/// Decrypt the payload (deeply) embedded in [message].
///
/// If everything goes well, returns the plaintext payload. If an error occurs, that
/// is returned instead.
Future<Result<OmemoError, List<int>>> ratchetDecrypt(
OMEMOAuthenticatedMessage message,
) async {
final header = OMEMOMessage.fromBuffer(message.message);
// Try skipped keys
final plaintextRaw = await _trySkippedMessageKeys(message, header);
if (plaintextRaw.isType<OmemoError>()) {
// Propagate the error
return Result(plaintextRaw.get<OmemoError>());
}
final plaintext = plaintextRaw.get<List<int>?>();
if (plaintext != null) {
return Result(plaintext);
}
if (dhr == null || !listsEqual(header.dhPub, await dhr!.getBytes())) {
final skipResult1 = await _skipMessageKeys(header.pn);
if (skipResult1 != null) {
return Result(skipResult1);
}
await _dhRatchet(header); await _dhRatchet(header);
} }
await _skipMessageKeys(header.n!); final skipResult2 = await _skipMessageKeys(header.n);
final newCkr = await kdfCk(ckr!, kdfCkNextChainKey); if (skipResult2 != null) {
return Result(skipResult2);
}
final ck = await kdfCk(ckr!, kdfCkNextChainKey);
final mk = await kdfCk(ckr!, kdfCkNextMessageKey); final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
ckr = newCkr; ckr = ck;
nr++; nr++;
return decrypt( return _decrypt(message, header.ciphertext, mk);
mk,
ciphertext,
concat([sessionAd, header.writeToBuffer()]),
sessionAd,
);
} }
/// Encrypt the payload [plaintext] using the double ratchet session.
Future<OMEMOAuthenticatedMessage> ratchetEncrypt(List<int> plaintext) async {
// Advance the ratchet
final ck = await kdfCk(cks!, kdfCkNextChainKey);
final mk = await kdfCk(cks!, kdfCkNextMessageKey);
cks = ck;
// Generate encryption, authentication key and IV
final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString);
final ciphertext =
await aes256CbcEncrypt(plaintext, keys.encryptionKey, keys.iv);
// Fill-in the header and serialize it here so we do it only once
final header = OMEMOMessage()
..dhPub = await dhs.pk.getBytes()
..pn = pn
..n = ns
..ciphertext = ciphertext;
final headerBytes = header.writeToBuffer();
// Increment the send counter
ns++;
final newAd = concat([sessionAd, headerBytes]);
final hmac = await truncatedHmac(newAd, keys.authenticationKey);
return OMEMOAuthenticatedMessage()
..mac = hmac
..message = headerBytes;
}
/// Returns a copy of the ratchet.
OmemoDoubleRatchet clone() { OmemoDoubleRatchet clone() {
return OmemoDoubleRatchet( return OmemoDoubleRatchet(
dhs, dhs,
@ -402,27 +370,16 @@ class OmemoDoubleRatchet {
sessionAd, sessionAd,
Map<SkippedKey, List<int>>.from(mkSkipped), Map<SkippedKey, List<int>>.from(mkSkipped),
acknowledged, acknowledged,
kexTimestamp,
kex, kex,
); );
} }
OmemoDoubleRatchet cloneWithKex(String kex) { /// Computes the fingerprint of the double ratchet, according to
return OmemoDoubleRatchet( /// XEP-0384.
dhs, Future<String> get fingerprint async {
dhr, final curveKey = await ik.toCurve25519();
rk, return HEX.encode(
cks != null ? List<int>.from(cks!) : null, await curveKey.getBytes(),
ckr != null ? List<int>.from(ckr!) : null,
ns,
nr,
pn,
ik,
sessionAd,
Map<SkippedKey, List<int>>.from(mkSkipped),
acknowledged,
kexTimestamp,
kex,
); );
} }
@ -454,7 +411,6 @@ class OmemoDoubleRatchet {
ns == other.ns && ns == other.ns &&
nr == other.nr && nr == other.nr &&
pn == other.pn && pn == other.pn &&
listsEqual(sessionAd, other.sessionAd) && listsEqual(sessionAd, other.sessionAd);
kexTimestamp == other.kexTimestamp;
} }
} }

View File

@ -8,7 +8,7 @@ const kdfRkInfoString = 'OMEMO Root Chain';
const kdfCkNextMessageKey = 0x01; const kdfCkNextMessageKey = 0x01;
const kdfCkNextChainKey = 0x02; const kdfCkNextChainKey = 0x02;
/// Signals KDF_CK function as specified by OMEMO 0.8.0. /// Signals KDF_CK function as specified by OMEMO 0.8.3.
Future<List<int>> kdfCk(List<int> ck, int constant) async { Future<List<int>> kdfCk(List<int> ck, int constant) async {
final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32); final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32);
final result = await hkdf.deriveKey( final result = await hkdf.deriveKey(
@ -19,7 +19,7 @@ Future<List<int>> kdfCk(List<int> ck, int constant) async {
return result.extractBytes(); return result.extractBytes();
} }
/// Signals KDF_RK function as specified by OMEMO 0.8.0. /// Signals KDF_RK function as specified by OMEMO 0.8.3.
Future<List<int>> kdfRk(List<int> rk, List<int> dhOut) async { Future<List<int>> kdfRk(List<int> rk, List<int> dhOut) async {
final algorithm = Hkdf( final algorithm = Hkdf(
hmac: Hmac(Sha256()), hmac: Hmac(Sha256()),

View File

@ -1,61 +1,39 @@
abstract class OmemoException {} abstract class OmemoError {}
/// Triggered during X3DH if the signature if the SPK does verify to the actual SPK. /// Triggered during X3DH if the signature if the SPK does verify to the actual SPK.
class InvalidSignatureException extends OmemoException implements Exception { class InvalidKeyExchangeSignatureError extends OmemoError {}
String errMsg() =>
'The signature of the SPK does not match the provided signature';
}
/// Triggered by the Double Ratchet if the computed HMAC does not match the attached HMAC. /// Triggered by the Double Ratchet if the computed HMAC does not match the attached HMAC.
/// Triggered by the Session Manager if the computed HMAC does not match the attached HMAC. class InvalidMessageHMACError extends OmemoError {}
class InvalidMessageHMACException extends OmemoException implements Exception {
String errMsg() => 'The computed HMAC does not match the provided HMAC';
}
/// Triggered by the Double Ratchet if skipping messages would cause skipping more than /// Triggered by the Double Ratchet if skipping messages would cause skipping more than
/// MAXSKIP messages /// MAXSKIP messages
class SkippingTooManyMessagesException extends OmemoException class SkippingTooManyKeysError extends OmemoError {}
implements Exception {
String errMsg() => 'Skipping messages would cause a skip bigger than MAXSKIP';
}
/// Triggered by the Session Manager if the message key is not encrypted for the device. /// Triggered by the Session Manager if the message key is not encrypted for the device.
class NotEncryptedForDeviceException extends OmemoException class NotEncryptedForDeviceError extends OmemoError {}
implements Exception {
String errMsg() => 'Not encrypted for this device';
}
/// Triggered by the Session Manager when there is no key for decrypting the message.
class NoDecryptionKeyException extends OmemoException implements Exception {
String errMsg() => 'No key available for decrypting the message';
}
/// Triggered by the Session Manager when the identifier of the used Signed Prekey /// Triggered by the Session Manager when the identifier of the used Signed Prekey
/// is neither the current SPK's identifier nor the old one's. /// is neither the current SPK's identifier nor the old one's.
class UnknownSignedPrekeyException extends OmemoException implements Exception { class UnknownSignedPrekeyError extends OmemoError {}
String errMsg() => 'Unknown Signed Prekey used.';
}
/// Triggered by the Session Manager when the received Key Exchange message does not meet
/// the requirement that a key exchange, given that the ratchet already exists, must be
/// sent after its creation.
class InvalidKeyExchangeException extends OmemoException implements Exception {
String errMsg() => 'The key exchange was sent before the last kex finished';
}
/// Triggered by the Session Manager when a message's sequence number is smaller than we
/// expect it to be.
class MessageAlreadyDecryptedException extends OmemoException
implements Exception {
String errMsg() => 'The message has already been decrypted';
}
/// Triggered by the OmemoManager when we could not encrypt a message as we have /// Triggered by the OmemoManager when we could not encrypt a message as we have
/// no key material available. That happens, for example, when we want to create a /// no key material available. That happens, for example, when we want to create a
/// ratchet session with a JID we had no session with but fetching the device bundle /// ratchet session with a JID we had no session with but fetching the device bundle
/// failed. /// failed.
class NoKeyMaterialAvailableException extends OmemoException class NoKeyMaterialAvailableError extends OmemoError {}
implements Exception {
String errMsg() => /// A non-key-exchange message was received that was encrypted for our device, but we have no ratchet with
'No key material available to create a ratchet session with'; /// the device that sent the message.
class NoSessionWithDeviceError extends OmemoError {}
/// Caused when the AES-256 CBC decryption failed.
class MalformedCiphertextError extends OmemoError {
MalformedCiphertextError(this.ex);
/// The exception that was raised while decryption.
final Object ex;
} }
/// Caused by an empty <key /> element
class MalformedEncryptedKeyError extends OmemoError {}

View File

@ -1,7 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:cryptography/cryptography.dart';
import 'package:omemo_dart/src/keys.dart';
/// Flattens [inputs] and concatenates the elements. /// Flattens [inputs] and concatenates the elements.
List<int> concat(List<List<int>> inputs) { List<int> concat(List<List<int>> inputs) {
@ -43,41 +41,35 @@ int generateRandom32BitNumber() {
return Random.secure().nextInt(4294967295 /*pow(2, 32) - 1*/); return Random.secure().nextInt(4294967295 /*pow(2, 32) - 1*/);
} }
OmemoPublicKey? decodeKeyIfNotNull( /// Describes the differences between two lists in terms of its items.
Map<String, dynamic> map, class ListDiff<T> {
String key, ListDiff(this.added, this.removed);
KeyPairType type,
) {
if (map[key] == null) return null;
return OmemoPublicKey.fromBytes( /// The items that were added.
base64.decode(map[key]! as String), final List<T> added;
type,
); /// The items that were removed.
final List<T> removed;
} }
List<int>? base64DecodeIfNotNull(Map<String, dynamic> map, String key) { extension AppendToListOrCreateExtension<K, V> on Map<K, List<V>> {
if (map[key] == null) return null; /// Create or append [value] to the list identified with key [key].
void appendOrCreate(K key, V value, {bool checkExistence = false}) {
return base64.decode(map[key]! as String); if (containsKey(key)) {
if (!checkExistence) {
this[key]!.add(value);
}
if (!this[key]!.contains(value)) {
this[key]!.add(value);
}
} else {
this[key] = [value];
}
}
} }
String? base64EncodeIfNotNull(List<int>? bytes) { extension StringFromBase64Extension on String {
if (bytes == null) return null; /// Base64-decode this string. Useful for doing `someString?.fromBase64()` instead
/// of `someString != null ? base64Decode(someString) : null`.
return base64.encode(bytes); List<int> fromBase64() => base64Decode(this);
}
OmemoKeyPair? decodeKeyPairIfNotNull(String? pk, String? sk, KeyPairType type) {
if (pk == null || sk == null) return null;
return OmemoKeyPair.fromBytes(
base64.decode(pk),
base64.decode(sk),
type,
);
}
int getTimestamp() {
return DateTime.now().millisecondsSinceEpoch;
} }

View File

@ -1,2 +0,0 @@
/// The info used for when encrypting the AES key for the actual payload.
const omemoPayloadInfoString = 'OMEMO Payload';

View File

@ -3,7 +3,15 @@ import 'package:omemo_dart/src/errors.dart';
@immutable @immutable
class DecryptionResult { class DecryptionResult {
const DecryptionResult(this.payload, this.error); const DecryptionResult(this.payload, this.usedOpkId, this.error);
/// The decrypted payload or null, if it was an empty OMEMO message.
final String? payload; final String? payload;
final OmemoException? error;
/// In case a key exchange has been performed: The id of the used OPK. Useful for
/// replacing the OPK after a message catch-up.
final int? usedOpkId;
/// The error that occurred during decryption or null, if no error occurred.
final OmemoError? error;
} }

View File

@ -22,76 +22,6 @@ class OmemoDevice {
this.opks, this.opks,
); );
/// Deserialize the Device
factory OmemoDevice.fromJson(Map<String, dynamic> data) {
// NOTE: We use the way OpenSSH names their keys, meaning that ik is the Identity
// Keypair's private key, while ik_pub refers to the Identity Keypair's public
// key.
/*
{
'jid': 'alice@...',
'id': 123,
'ik': 'base/64/encoded',
'ik_pub': 'base/64/encoded',
'spk': 'base/64/encoded',
'spk_pub': 'base/64/encoded',
'spk_id': 123,
'spk_sig': 'base/64/encoded',
'old_spk': 'base/64/encoded',
'old_spk_pub': 'base/64/encoded',
'old_spk_id': 122,
'opks': [
{
'id': 0,
'public': 'base/64/encoded',
'private': 'base/64/encoded'
}, ...
]
}
*/
// NOTE: Dart has some issues with just casting a List<dynamic> to List<Map<...>>, as
// such we need to convert the items by hand.
final opks = Map<int, OmemoKeyPair>.fromEntries(
(data['opks']! as List<dynamic>).map<MapEntry<int, OmemoKeyPair>>(
(opk) {
final map = opk as Map<String, dynamic>;
return MapEntry(
map['id']! as int,
OmemoKeyPair.fromBytes(
base64.decode(map['public']! as String),
base64.decode(map['private']! as String),
KeyPairType.x25519,
),
);
},
),
);
return OmemoDevice(
data['jid']! as String,
data['id']! as int,
OmemoKeyPair.fromBytes(
base64.decode(data['ik_pub']! as String),
base64.decode(data['ik']! as String),
KeyPairType.ed25519,
),
OmemoKeyPair.fromBytes(
base64.decode(data['spk_pub']! as String),
base64.decode(data['spk']! as String),
KeyPairType.x25519,
),
data['spk_id']! as int,
base64.decode(data['spk_sig']! as String),
decodeKeyPairIfNotNull(
data['old_spk_pub'] as String?,
data['old_spk'] as String?,
KeyPairType.x25519,
),
data['old_spk_id'] as int?,
opks,
);
}
/// Generate a completely new device, i.e. cryptographic identity. /// Generate a completely new device, i.e. cryptographic identity.
static Future<OmemoDevice> generateNewDevice( static Future<OmemoDevice> generateNewDevice(
String jid, { String jid, {
@ -105,7 +35,16 @@ class OmemoDevice {
final opks = <int, OmemoKeyPair>{}; final opks = <int, OmemoKeyPair>{};
for (var i = 0; i < opkAmount; i++) { for (var i = 0; i < opkAmount; i++) {
opks[i] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); // Generate unique ids for each key
while (true) {
final opkId = generateRandom32BitNumber();
if (opks.containsKey(opkId)) {
continue;
}
opks[opkId] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
break;
}
} }
return OmemoDevice(jid, id, ik, spk, spkId, signature, null, null, opks); return OmemoDevice(jid, id, ik, spk, spkId, signature, null, null, opks);
@ -142,7 +81,18 @@ class OmemoDevice {
/// a new Device object that copies over everything but replaces said key. /// a new Device object that copies over everything but replaces said key.
@internal @internal
Future<OmemoDevice> replaceOnetimePrekey(int id) async { Future<OmemoDevice> replaceOnetimePrekey(int id) async {
opks[id] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519); opks.remove(id);
// Generate a new unique id for the OPK.
while (true) {
final newId = generateRandom32BitNumber();
if (opks.containsKey(newId)) {
continue;
}
opks[newId] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
break;
}
return OmemoDevice( return OmemoDevice(
jid, jid,
@ -221,34 +171,6 @@ class OmemoDevice {
return HEX.encode(await curveKey.getBytes()); return HEX.encode(await curveKey.getBytes());
} }
/// Serialise the device information.
Future<Map<String, dynamic>> toJson() async {
/// Serialise the OPKs
final serialisedOpks = List<Map<String, dynamic>>.empty(growable: true);
for (final entry in opks.entries) {
serialisedOpks.add({
'id': entry.key,
'public': base64.encode(await entry.value.pk.getBytes()),
'private': base64.encode(await entry.value.sk.getBytes()),
});
}
return {
'jid': jid,
'id': id,
'ik': base64.encode(await ik.sk.getBytes()),
'ik_pub': base64.encode(await ik.pk.getBytes()),
'spk': base64.encode(await spk.sk.getBytes()),
'spk_pub': base64.encode(await spk.pk.getBytes()),
'spk_id': spkId,
'spk_sig': base64.encode(spkSignature),
'old_spk': base64EncodeIfNotNull(await oldSpk?.sk.getBytes()),
'old_spk_pub': base64EncodeIfNotNull(await oldSpk?.pk.getBytes()),
'old_spk_id': oldSpkId,
'opks': serialisedOpks,
};
}
@visibleForTesting @visibleForTesting
Future<bool> equals(OmemoDevice other) async { Future<bool> equals(OmemoDevice other) async {
var opksMatch = true; var opksMatch = true;

View File

@ -1,12 +1,23 @@
import 'dart:convert';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
/// EncryptedKey is the intermediary format of a <key /> element in the OMEMO message's /// EncryptedKey is the intermediary format of a <key /> element in the OMEMO message's
/// <keys /> header. /// <keys /> header.
@immutable @immutable
class EncryptedKey { class EncryptedKey {
const EncryptedKey(this.jid, this.rid, this.value, this.kex); const EncryptedKey(this.rid, this.value, this.kex);
final String jid;
/// The id of the device the key is encrypted for.
final int rid; final int rid;
/// The base64-encoded payload.
final String value; final String value;
/// Flag indicating whether the payload is a OMEMOKeyExchange (true) or
/// an OMEMOAuthenticatedMessage (false).
final bool kex; final bool kex;
/// The base64-decoded payload.
List<int> get data => base64Decode(value);
} }

View File

@ -1,7 +1,6 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:omemo_dart/src/errors.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/ratchet_map_key.dart'; import 'package:omemo_dart/src/omemo/errors.dart';
@immutable @immutable
class EncryptionResult { class EncryptionResult {
@ -9,7 +8,7 @@ class EncryptionResult {
this.ciphertext, this.ciphertext,
this.encryptedKeys, this.encryptedKeys,
this.deviceEncryptionErrors, this.deviceEncryptionErrors,
this.jidEncryptionErrors, this.canSend,
); );
/// The actual message that was encrypted. /// The actual message that was encrypted.
@ -17,17 +16,12 @@ class EncryptionResult {
/// Mapping of the device Id to the key for decrypting ciphertext, encrypted /// Mapping of the device Id to the key for decrypting ciphertext, encrypted
/// for the ratchet with said device Id. /// for the ratchet with said device Id.
final List<EncryptedKey> encryptedKeys; final Map<String, List<EncryptedKey>> encryptedKeys;
/// Mapping of a ratchet map keys to a possible exception. /// Mapping of a JID to
final Map<RatchetMapKey, OmemoException> deviceEncryptionErrors; final Map<String, List<EncryptToJidError>> deviceEncryptionErrors;
/// Mapping of a JID to a possible exception. /// A flag indicating that the message could be sent like that, i.e. we were able
final Map<String, OmemoException> jidEncryptionErrors; /// to encrypt to at-least one device per recipient.
final bool canSend;
/// True if the encryption was a success. This means that we could encrypt for
/// at least one ratchet.
bool isSuccess(int numberOfRecipients) =>
encryptedKeys.isNotEmpty &&
jidEncryptionErrors.length < numberOfRecipients;
} }

12
lib/src/omemo/errors.dart Normal file
View File

@ -0,0 +1,12 @@
import 'package:omemo_dart/src/errors.dart';
/// Returned on encryption, if encryption failed for some reason.
class EncryptToJidError extends OmemoError {
EncryptToJidError(this.device, this.error);
/// The device the error occurred with
final int? device;
/// The actual error.
final OmemoError error;
}

View File

@ -1,44 +0,0 @@
import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart';
import 'package:omemo_dart/src/omemo/device.dart';
abstract class OmemoEvent {}
/// Triggered when a ratchet has been modified
class RatchetModifiedEvent extends OmemoEvent {
RatchetModifiedEvent(
this.jid,
this.deviceId,
this.ratchet,
this.added,
this.replaced,
);
final String jid;
final int deviceId;
final OmemoDoubleRatchet ratchet;
/// Indicates whether the ratchet has just been created (true) or just modified (false).
final bool added;
/// Indicates whether the ratchet has been replaced (true) or not.
final bool replaced;
}
/// 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
class DeviceListModifiedEvent extends OmemoEvent {
DeviceListModifiedEvent(this.list);
final Map<String, List<int>> list;
}
/// Triggered by the OmemoSessionManager when our own device bundle was modified
/// and thus should be republished.
class DeviceModifiedEvent extends OmemoEvent {
DeviceModifiedEvent(this.device);
final OmemoDevice device;
}

1040
lib/src/omemo/omemo.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,854 +0,0 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:cryptography/cryptography.dart';
import 'package:hex/hex.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:omemo_dart/src/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart';
import 'package:omemo_dart/src/errors.dart';
import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/keys.dart';
import 'package:omemo_dart/src/omemo/bundle.dart';
import 'package:omemo_dart/src/omemo/constants.dart';
import 'package:omemo_dart/src/omemo/decryption_result.dart';
import 'package:omemo_dart/src/omemo/device.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/events.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/stanza.dart';
import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart';
import 'package:omemo_dart/src/protobuf/omemo_key_exchange.dart';
import 'package:omemo_dart/src/protobuf/omemo_message.dart';
import 'package:omemo_dart/src/trust/base.dart';
import 'package:omemo_dart/src/x3dh/x3dh.dart';
import 'package:synchronized/synchronized.dart';
class _InternalDecryptionResult {
const _InternalDecryptionResult(
this.ratchetCreated,
this.ratchetReplaced,
this.payload,
) : assert(
!ratchetCreated || !ratchetReplaced,
'Ratchet must be either replaced or created',
);
final bool ratchetCreated;
final bool ratchetReplaced;
final String? payload;
}
class OmemoManager {
OmemoManager(
this._device,
this._trustManager,
this.sendEmptyOmemoMessageImpl,
this.fetchDeviceListImpl,
this.fetchDeviceBundleImpl,
this.subscribeToDeviceListNodeImpl,
);
final Logger _log = Logger('OmemoManager');
/// Functions for connecting with the OMEMO library
/// Send an empty OMEMO:2 message using the encrypted payload @result to
/// @recipientJid.
final Future<void> Function(EncryptionResult result, String recipientJid)
sendEmptyOmemoMessageImpl;
/// Fetch the list of device ids associated with @jid. If the device list cannot be
/// fetched, return null.
final Future<List<int>?> Function(String jid) fetchDeviceListImpl;
/// Fetch the device bundle for the device with id @id of jid. If it cannot be fetched, return null.
final Future<OmemoBundle?> Function(String jid, int id) fetchDeviceBundleImpl;
/// Subscribe to the device list PEP node of @jid.
final Future<void> Function(String jid) subscribeToDeviceListNodeImpl;
/// Map bare JID to its known devices
Map<String, List<int>> _deviceList = {};
/// Map bare JIDs to whether we already requested the device list once
final Map<String, bool> _deviceListRequested = {};
/// Map bare a ratchet key to its ratchet. Note that this is also locked by
/// _ratchetCriticalSectionLock.
Map<RatchetMapKey, OmemoDoubleRatchet> _ratchetMap = {};
/// Map bare JID to whether we already tried to subscribe to the device list node.
final Map<String, bool> _subscriptionMap = {};
/// For preventing a race condition in encryption/decryption
final Map<String, Queue<Completer<void>>> _ratchetCriticalSectionQueue = {};
final Lock _ratchetCriticalSectionLock = Lock();
/// The OmemoManager's trust management
final TrustManager _trustManager;
TrustManager get trustManager => _trustManager;
/// Our own keys...
final Lock _deviceLock = Lock();
// ignore: prefer_final_fields
OmemoDevice _device;
/// The event bus of the session manager
final StreamController<OmemoEvent> _eventStreamController =
StreamController<OmemoEvent>.broadcast();
Stream<OmemoEvent> get eventStream => _eventStreamController.stream;
/// Enter the critical section for performing cryptographic operations on the ratchets
Future<void> _enterRatchetCriticalSection(String jid) async {
final completer = await _ratchetCriticalSectionLock.synchronized(() {
if (_ratchetCriticalSectionQueue.containsKey(jid)) {
final c = Completer<void>();
_ratchetCriticalSectionQueue[jid]!.addLast(c);
return c;
}
_ratchetCriticalSectionQueue[jid] = Queue();
return null;
});
if (completer != null) {
await completer.future;
}
}
/// Leave the critical section for the ratchets.
Future<void> _leaveRatchetCriticalSection(String jid) async {
await _ratchetCriticalSectionLock.synchronized(() {
if (_ratchetCriticalSectionQueue.containsKey(jid)) {
if (_ratchetCriticalSectionQueue[jid]!.isEmpty) {
_ratchetCriticalSectionQueue.remove(jid);
} else {
_ratchetCriticalSectionQueue[jid]!.removeFirst().complete();
}
}
});
}
Future<String?> _decryptAndVerifyHmac(
List<int>? ciphertext,
List<int> keyAndHmac,
) async {
// Empty OMEMO messages should just have the key decrypted and/or session set up.
if (ciphertext == null) {
return null;
}
final key = keyAndHmac.sublist(0, 32);
final hmac = keyAndHmac.sublist(32, 48);
final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
final computedHmac =
await truncatedHmac(ciphertext, derivedKeys.authenticationKey);
if (!listsEqual(hmac, computedHmac)) {
throw InvalidMessageHMACException();
}
return utf8.decode(
await aes256CbcDecrypt(
ciphertext,
derivedKeys.encryptionKey,
derivedKeys.iv,
),
);
}
/// Add a session [ratchet] with the [deviceId] to the internal tracking state.
/// NOTE: Must be called from within the ratchet critical section.
void _addSession(String jid, int deviceId, OmemoDoubleRatchet ratchet) {
// Add the bundle Id
if (!_deviceList.containsKey(jid)) {
_deviceList[jid] = [deviceId];
// Commit the device map
_eventStreamController.add(DeviceListModifiedEvent(_deviceList));
} else {
// Prevent having the same device multiple times in the list
if (!_deviceList[jid]!.contains(deviceId)) {
_deviceList[jid]!.add(deviceId);
// Commit the device map
_eventStreamController.add(DeviceListModifiedEvent(_deviceList));
}
}
// Add the ratchet session
final key = RatchetMapKey(jid, deviceId);
_ratchetMap[key] = ratchet;
// Commit the ratchet
_eventStreamController
.add(RatchetModifiedEvent(jid, deviceId, ratchet, true, false));
}
/// Build a new session with the user at [jid] with the device [deviceId] using data
/// from the key exchange [kex]. In case [kex] contains an unknown Signed Prekey
/// identifier an UnknownSignedPrekeyException will be thrown.
Future<OmemoDoubleRatchet> _addSessionFromKeyExchange(
String jid,
int deviceId,
OmemoKeyExchange kex,
) async {
// Pick the correct SPK
final device = await getDevice();
OmemoKeyPair spk;
if (kex.spkId == _device.spkId) {
spk = _device.spk;
} else if (kex.spkId == _device.oldSpkId) {
spk = _device.oldSpk!;
} else {
throw UnknownSignedPrekeyException();
}
final kexResult = await x3dhFromInitialMessage(
X3DHMessage(
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
OmemoPublicKey.fromBytes(kex.ek!, KeyPairType.x25519),
kex.pkId!,
),
spk,
device.opks.values.elementAt(kex.pkId!),
device.ik,
);
final ratchet = await OmemoDoubleRatchet.acceptNewSession(
spk,
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
kexResult.sk,
kexResult.ad,
getTimestamp(),
);
// Notify the trust manager
await trustManager.onNewSession(jid, deviceId);
return ratchet;
}
/// Create a ratchet session initiated by Alice to the user with Jid [jid] and the device
/// [deviceId] from the bundle [bundle].
@visibleForTesting
Future<OmemoKeyExchange> addSessionFromBundle(
String jid,
int deviceId,
OmemoBundle bundle,
) async {
final device = await getDevice();
final kexResult = await x3dhFromBundle(
bundle,
device.ik,
);
final ratchet = await OmemoDoubleRatchet.initiateNewSession(
bundle.spk,
bundle.ik,
kexResult.sk,
kexResult.ad,
getTimestamp(),
);
await _trustManager.onNewSession(jid, deviceId);
_addSession(jid, deviceId, ratchet);
return OmemoKeyExchange()
..pkId = kexResult.opkId
..spkId = bundle.spkId
..ik = await device.ik.pk.getBytes()
..ek = await kexResult.ek.pk.getBytes();
}
/// 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].
/// NOTE: Must be called from within the ratchet critical section
void _restoreRatchet(RatchetMapKey mapKey, OmemoDoubleRatchet oldRatchet) {
_log.finest(
'Restoring ratchet ${mapKey.jid}:${mapKey.deviceId} to ${oldRatchet.nr}',
);
_ratchetMap[mapKey] = oldRatchet;
// Commit the ratchet
_eventStreamController.add(
RatchetModifiedEvent(
mapKey.jid,
mapKey.deviceId,
oldRatchet,
false,
false,
),
);
}
/// Attempt to decrypt [ciphertext]. [keys] refers to the <key /> elements inside the
/// <keys /> element with a "jid" attribute matching our own. [senderJid] refers to the
/// bare Jid of the sender. [senderDeviceId] refers to the "sid" attribute of the
/// <encrypted /> element.
/// [timestamp] refers to the time the message was sent. This might be either what the
/// server tells you via "XEP-0203: Delayed Delivery" or the point in time at which
/// you received the stanza, if no Delayed Delivery element was found.
///
/// If the received message is an empty OMEMO message, i.e. there is no <payload />
/// element, then [ciphertext] must be set to null. In this case, this function
/// will return null as there is no message to be decrypted. This, however, is used
/// to set up sessions or advance the ratchets.
Future<_InternalDecryptionResult> _decryptMessage(
List<int>? ciphertext,
String senderJid,
int senderDeviceId,
List<EncryptedKey> keys,
int timestamp,
) async {
// Try to find a session we can decrypt with.
var device = await getDevice();
final rawKey = keys.firstWhereOrNull((key) => key.rid == device.id);
if (rawKey == null) {
throw NotEncryptedForDeviceException();
}
final decodedRawKey = base64.decode(rawKey.value);
List<int>? keyAndHmac;
OmemoAuthenticatedMessage authMessage;
OmemoMessage? message;
// If the ratchet already existed, we store it. If it didn't, oldRatchet will stay
// null.
final ratchetKey = RatchetMapKey(senderJid, senderDeviceId);
final oldRatchet = getRatchet(ratchetKey)?.clone();
if (rawKey.kex) {
final kex = OmemoKeyExchange.fromBuffer(decodedRawKey);
authMessage = kex.message!;
message = OmemoMessage.fromBuffer(authMessage.message!);
// Guard against old key exchanges
if (oldRatchet != null) {
_log.finest(
'KEX for existent ratchet ${ratchetKey.toJsonKey()}. ${oldRatchet.kexTimestamp} > $timestamp: ${oldRatchet.kexTimestamp > timestamp}',
);
if (oldRatchet.kexTimestamp > timestamp) {
throw InvalidKeyExchangeException();
}
}
final r =
await _addSessionFromKeyExchange(senderJid, senderDeviceId, kex);
// Try to decrypt with the new ratchet r
try {
keyAndHmac =
await r.ratchetDecrypt(message, authMessage.writeToBuffer());
final result = await _decryptAndVerifyHmac(ciphertext, keyAndHmac);
// Add the new ratchet
_addSession(senderJid, senderDeviceId, r);
// Replace the OPK
await _deviceLock.synchronized(() async {
device = await device.replaceOnetimePrekey(kex.pkId!);
// Commit the device
_eventStreamController.add(DeviceModifiedEvent(device));
});
// Commit the ratchet
_eventStreamController.add(
RatchetModifiedEvent(
senderJid,
senderDeviceId,
r,
oldRatchet == null,
oldRatchet != null,
),
);
return _InternalDecryptionResult(
oldRatchet == null,
oldRatchet != null,
result,
);
} catch (ex) {
_log.finest('Kex failed due to $ex. Not proceeding with kex.');
}
} else {
authMessage = OmemoAuthenticatedMessage.fromBuffer(decodedRawKey);
message = OmemoMessage.fromBuffer(authMessage.message!);
}
final devices = _deviceList[senderJid];
if (devices?.contains(senderDeviceId) != true) {
throw NoDecryptionKeyException();
}
// TODO(PapaTutuWawa): When receiving a message that is not an OMEMOKeyExchange from a device there is no session with, clients SHOULD create a session with that device and notify it about the new session by responding with an empty OMEMO message as per Sending a message.
// We can guarantee that the ratchet exists at this point in time
final ratchet = getRatchet(ratchetKey)!;
try {
if (rawKey.kex) {
keyAndHmac =
await ratchet.ratchetDecrypt(message, authMessage.writeToBuffer());
} else {
keyAndHmac = await ratchet.ratchetDecrypt(message, decodedRawKey);
}
} catch (_) {
_restoreRatchet(ratchetKey, oldRatchet!);
rethrow;
}
// Commit the ratchet
_eventStreamController.add(
RatchetModifiedEvent(
senderJid,
senderDeviceId,
ratchet,
false,
false,
),
);
try {
return _InternalDecryptionResult(
false,
false,
await _decryptAndVerifyHmac(ciphertext, keyAndHmac),
);
} catch (_) {
_restoreRatchet(ratchetKey, oldRatchet!);
rethrow;
}
}
/// Returns, if it exists, the ratchet associated with [key].
/// NOTE: Must be called from within the ratchet critical section.
@visibleForTesting
OmemoDoubleRatchet? getRatchet(RatchetMapKey key) => _ratchetMap[key];
/// Figure out what bundles we have to still build a session with.
Future<List<OmemoBundle>> _fetchNewBundles(String jid) async {
// Check if we already requested the device list for [jid]
List<int> bundlesToFetch;
if (!_deviceListRequested.containsKey(jid) ||
!_deviceList.containsKey(jid)) {
// We don't have an up-to-date version of the device list
final newDeviceList = await fetchDeviceListImpl(jid);
if (newDeviceList == null) return [];
_deviceList[jid] = newDeviceList;
bundlesToFetch = newDeviceList.where((id) {
return !_ratchetMap.containsKey(RatchetMapKey(jid, id)) ||
_deviceList[jid]?.contains(id) == false;
}).toList();
// Trigger an event with the new device list
_eventStreamController.add(DeviceListModifiedEvent(_deviceList));
} else {
// We already have an up-to-date version of the device list
bundlesToFetch = _deviceList[jid]!
.where((id) => !_ratchetMap.containsKey(RatchetMapKey(jid, id)))
.toList();
}
if (bundlesToFetch.isNotEmpty) {
_log.finest('Fetching bundles $bundlesToFetch for $jid');
}
final device = await getDevice();
final newBundles = List<OmemoBundle>.empty(growable: true);
for (final id in bundlesToFetch) {
if (jid == device.jid && id == device.id) continue;
final bundle = await fetchDeviceBundleImpl(jid, id);
if (bundle != null) newBundles.add(bundle);
}
return newBundles;
}
/// Encrypt the key [plaintext] for all known bundles of the Jids in [jids]. Returns a
/// map that maps the device Id to the ciphertext of [plaintext].
///
/// If [plaintext] is null, then the result will be an empty OMEMO message, i.e. one that
/// does not contain a <payload /> element. This means that the ciphertext attribute of
/// the result will be null as well.
/// NOTE: Must be called within the ratchet critical section
Future<EncryptionResult> _encryptToJids(
List<String> jids,
String? plaintext,
) async {
final encryptedKeys = List<EncryptedKey>.empty(growable: true);
var ciphertext = const <int>[];
var keyPayload = const <int>[];
if (plaintext != null) {
// Generate the key and encrypt the plaintext
final key = generateRandomBytes(32);
final keys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
ciphertext = await aes256CbcEncrypt(
utf8.encode(plaintext),
keys.encryptionKey,
keys.iv,
);
final hmac = await truncatedHmac(ciphertext, keys.authenticationKey);
keyPayload = concat([key, hmac]);
} else {
keyPayload = List<int>.filled(32, 0x0);
}
final kex = <RatchetMapKey, OmemoKeyExchange>{};
for (final jid in jids) {
for (final newSession in await _fetchNewBundles(jid)) {
kex[RatchetMapKey(jid, newSession.id)] = await addSessionFromBundle(
newSession.jid,
newSession.id,
newSession,
);
}
}
// We assume that the user already checked if the session exists
final deviceEncryptionErrors = <RatchetMapKey, OmemoException>{};
final jidEncryptionErrors = <String, OmemoException>{};
for (final jid in jids) {
final devices = _deviceList[jid];
if (devices == null) {
_log.severe('Device list does not exist for $jid.');
jidEncryptionErrors[jid] = NoKeyMaterialAvailableException();
continue;
}
if (!_subscriptionMap.containsKey(jid)) {
unawaited(subscribeToDeviceListNodeImpl(jid));
_subscriptionMap[jid] = true;
}
for (final deviceId in devices) {
// Empty OMEMO messages are allowed to bypass trust
if (plaintext != null) {
// Only encrypt to devices that are trusted
if (!(await _trustManager.isTrusted(jid, deviceId))) continue;
// Only encrypt to devices that are enabled
if (!(await _trustManager.isEnabled(jid, deviceId))) continue;
}
final ratchetKey = RatchetMapKey(jid, deviceId);
var ratchet = _ratchetMap[ratchetKey];
if (ratchet == null) {
_log.severe('Ratchet ${ratchetKey.toJsonKey()} does not exist.');
deviceEncryptionErrors[ratchetKey] =
NoKeyMaterialAvailableException();
continue;
}
final ciphertext =
(await ratchet.ratchetEncrypt(keyPayload)).ciphertext;
if (kex.containsKey(ratchetKey)) {
// The ratchet did not exist
final k = kex[ratchetKey]!
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
final buffer = base64.encode(k.writeToBuffer());
encryptedKeys.add(
EncryptedKey(
jid,
deviceId,
buffer,
true,
),
);
ratchet = ratchet.cloneWithKex(buffer);
_ratchetMap[ratchetKey] = ratchet;
} else if (!ratchet.acknowledged) {
// The ratchet exists but is not acked
if (ratchet.kex != null) {
final oldKex =
OmemoKeyExchange.fromBuffer(base64.decode(ratchet.kex!))
..message = OmemoAuthenticatedMessage.fromBuffer(ciphertext);
encryptedKeys.add(
EncryptedKey(
jid,
deviceId,
base64.encode(oldKex.writeToBuffer()),
true,
),
);
} else {
// The ratchet is not acked but we don't have the old key exchange
_log.warning(
'Ratchet for $jid:$deviceId is not acked but the kex attribute is null',
);
encryptedKeys.add(
EncryptedKey(
jid,
deviceId,
base64.encode(ciphertext),
false,
),
);
}
} else {
// The ratchet exists and is acked
encryptedKeys.add(
EncryptedKey(
jid,
deviceId,
base64.encode(ciphertext),
false,
),
);
}
// Commit the ratchet
_eventStreamController
.add(RatchetModifiedEvent(jid, deviceId, ratchet, false, false));
}
}
return EncryptionResult(
plaintext != null ? ciphertext : null,
encryptedKeys,
deviceEncryptionErrors,
jidEncryptionErrors,
);
}
/// Call when receiving an OMEMO:2 encrypted stanza. Will handle everything and
/// decrypt it.
Future<DecryptionResult> onIncomingStanza(OmemoIncomingStanza stanza) async {
await _enterRatchetCriticalSection(stanza.bareSenderJid);
if (!_subscriptionMap.containsKey(stanza.bareSenderJid)) {
unawaited(subscribeToDeviceListNodeImpl(stanza.bareSenderJid));
_subscriptionMap[stanza.bareSenderJid] = true;
}
final ratchetKey =
RatchetMapKey(stanza.bareSenderJid, stanza.senderDeviceId);
final _InternalDecryptionResult result;
try {
result = await _decryptMessage(
stanza.payload != null ? base64.decode(stanza.payload!) : null,
stanza.bareSenderJid,
stanza.senderDeviceId,
stanza.keys,
stanza.timestamp,
);
} on OmemoException catch (ex) {
await _leaveRatchetCriticalSection(stanza.bareSenderJid);
return DecryptionResult(
null,
ex,
);
}
// Check if the ratchet is acked
final ratchet = getRatchet(ratchetKey);
assert(
ratchet != null,
'We decrypted the message, so the ratchet must exist',
);
if (ratchet!.acknowledged) {
// Ratchet is acknowledged
if (ratchet.nr > 53 || result.ratchetCreated || result.ratchetReplaced) {
await sendEmptyOmemoMessageImpl(
await _encryptToJids(
[stanza.bareSenderJid],
null,
),
stanza.bareSenderJid,
);
}
// Ratchet is acked
await _leaveRatchetCriticalSection(stanza.bareSenderJid);
return DecryptionResult(
result.payload,
null,
);
} else {
// Ratchet is not acked.
// Mark as acked and send an empty OMEMO message.
await ratchetAcknowledged(
stanza.bareSenderJid,
stanza.senderDeviceId,
enterCriticalSection: false,
);
await sendEmptyOmemoMessageImpl(
await _encryptToJids(
[stanza.bareSenderJid],
null,
),
stanza.bareSenderJid,
);
await _leaveRatchetCriticalSection(stanza.bareSenderJid);
return DecryptionResult(
result.payload,
null,
);
}
}
/// Call when sending out an encrypted stanza. Will handle everything and
/// encrypt it.
Future<EncryptionResult> onOutgoingStanza(OmemoOutgoingStanza stanza) async {
_log.finest('Waiting to enter critical section');
await _enterRatchetCriticalSection(stanza.recipientJids.first);
_log.finest('Entered critical section');
final result = _encryptToJids(
stanza.recipientJids,
stanza.payload,
);
await _leaveRatchetCriticalSection(stanza.recipientJids.first);
return result;
}
// Sends a hearbeat message as specified by XEP-0384 to [jid].
Future<void> sendOmemoHeartbeat(String jid) async {
// TODO(Unknown): Include some error handling
final result = await _encryptToJids(
[jid],
null,
);
await sendEmptyOmemoMessageImpl(result, jid);
}
/// Mark the ratchet for device [deviceId] from [jid] as acked.
Future<void> ratchetAcknowledged(
String jid,
int deviceId, {
bool enterCriticalSection = true,
}) async {
if (enterCriticalSection) await _enterRatchetCriticalSection(jid);
final key = RatchetMapKey(jid, deviceId);
if (_ratchetMap.containsKey(key)) {
final ratchet = _ratchetMap[key]!..acknowledged = true;
// Commit it
_eventStreamController
.add(RatchetModifiedEvent(jid, deviceId, ratchet, false, false));
} else {
_log.severe(
'Attempted to acknowledge ratchet ${key.toJsonKey()}, even though it does not exist',
);
}
if (enterCriticalSection) await _leaveRatchetCriticalSection(jid);
}
/// 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() async {
await _deviceLock.synchronized(() async {
_device = await OmemoDevice.generateNewDevice(_device.jid);
// Commit it
_eventStreamController.add(DeviceModifiedEvent(_device));
});
}
/// Returns the device used for encryption and decryption.
Future<OmemoDevice> getDevice() => _deviceLock.synchronized(() => _device);
/// Returns the id of the device used for encryption and decryption.
Future<int> getDeviceId() async => (await getDevice()).id;
/// Directly aquire the current device as a OMEMO device bundle.
Future<OmemoBundle> getDeviceBundle() async => (await getDevice()).toBundle();
/// Directly aquire the current device's fingerprint.
Future<String> getDeviceFingerprint() async =>
(await getDevice()).getFingerprint();
/// Returns the fingerprints for all devices of [jid] that we have a session with.
/// If there are not sessions with [jid], then returns null.
Future<List<DeviceFingerprint>?> getFingerprintsForJid(String jid) async {
if (!_deviceList.containsKey(jid)) return null;
await _enterRatchetCriticalSection(jid);
final fingerprintKeys = _deviceList[jid]!
.map((id) => RatchetMapKey(jid, id))
.where((key) => _ratchetMap.containsKey(key));
final fingerprints = List<DeviceFingerprint>.empty(growable: true);
for (final key in fingerprintKeys) {
final curveKey = await _ratchetMap[key]!.ik.toCurve25519();
fingerprints.add(
DeviceFingerprint(
key.deviceId,
HEX.encode(await curveKey.getBytes()),
),
);
}
await _leaveRatchetCriticalSection(jid);
return fingerprints;
}
/// Ensures that the device list is fetched again on the next message sending.
void onNewConnection() {
_deviceListRequested.clear();
_subscriptionMap.clear();
}
/// Sets the device list for [jid] to [devices]. Triggers a DeviceListModifiedEvent.
void onDeviceListUpdate(String jid, List<int> devices) {
_deviceList[jid] = devices;
_deviceListRequested[jid] = true;
// Trigger an event
_eventStreamController.add(DeviceListModifiedEvent(_deviceList));
}
void initialize(
Map<RatchetMapKey, OmemoDoubleRatchet> ratchetMap,
Map<String, List<int>> deviceList,
) {
_deviceList = deviceList;
_ratchetMap = ratchetMap;
}
/// Removes all ratchets for JID [jid]. This also removes all trust decisions for
/// [jid] from the trust manager. This function triggers a RatchetRemovedEvent for
/// every removed ratchet and a DeviceListModifiedEvent afterwards. Behaviour for
/// the trust manager is dependent on its implementation.
Future<void> removeAllRatchets(String jid) async {
await _enterRatchetCriticalSection(jid);
for (final deviceId in _deviceList[jid]!) {
// Remove the ratchet and commit it
_ratchetMap.remove(RatchetMapKey(jid, deviceId));
_eventStreamController.add(RatchetRemovedEvent(jid, deviceId));
}
// Remove the devices from the device list cache and commit it
_deviceList.remove(jid);
_deviceListRequested.remove(jid);
_eventStreamController.add(DeviceListModifiedEvent(_deviceList));
// Remove trust decisions
await _trustManager.removeTrustDecisionsForJid(jid);
await _leaveRatchetCriticalSection(jid);
}
/// Replaces the internal device with [newDevice]. Does not trigger an event.
Future<void> replaceDevice(OmemoDevice newDevice) async {
await _deviceLock.synchronized(() {
_device = newDevice;
});
}
}

100
lib/src/omemo/queue.dart Normal file
View File

@ -0,0 +1,100 @@
import 'dart:async';
import 'dart:collection';
import 'package:meta/meta.dart';
import 'package:synchronized/synchronized.dart';
extension UtilAllMethodsList<T> on List<T> {
void removeAll(List<T> values) {
for (final value in values) {
remove(value);
}
}
bool containsAll(List<T> values) {
for (final value in values) {
if (!contains(value)) {
return false;
}
}
return true;
}
}
class _RatchetAccessQueueEntry {
_RatchetAccessQueueEntry(
this.jids,
this.completer,
);
final List<String> jids;
final Completer<void> completer;
}
class RatchetAccessQueue {
final Queue<_RatchetAccessQueueEntry> _queue = Queue();
@visibleForTesting
final List<String> runningOperations = List<String>.empty(growable: true);
final Lock lock = Lock();
bool canBypass(List<String> jids) {
for (final jid in jids) {
if (runningOperations.contains(jid)) {
return false;
}
}
return true;
}
Future<void> enterCriticalSection(List<String> jids) async {
final completer = await lock.synchronized<Completer<void>?>(() {
if (canBypass(jids)) {
runningOperations.addAll(jids);
return null;
}
final completer = Completer<void>();
_queue.add(
_RatchetAccessQueueEntry(
jids,
completer,
),
);
return completer;
});
await completer?.future;
}
Future<void> leaveCriticalSection(List<String> jids) async {
await lock.synchronized(() {
runningOperations.removeAll(jids);
while (_queue.isNotEmpty) {
if (canBypass(_queue.first.jids)) {
final head = _queue.removeFirst();
runningOperations.addAll(head.jids);
head.completer.complete();
} else {
break;
}
}
});
}
Future<T> synchronized<T>(
List<String> jids,
Future<T> Function() function,
) async {
await enterCriticalSection(jids);
final result = await function();
await leaveCriticalSection(jids);
return result;
}
}

View File

@ -0,0 +1,26 @@
import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart';
class OmemoRatchetData {
const OmemoRatchetData(
this.jid,
this.id,
this.ratchet,
this.added,
this.replaced,
);
/// The JID we have the ratchet with.
final String jid;
/// The device id we have the ratchet with.
final int id;
/// The actual double ratchet to commit.
final OmemoDoubleRatchet ratchet;
/// Indicates whether the ratchet has just been created (true) or just modified (false).
final bool added;
/// Indicates whether the ratchet has been replaced (true) or not.
final bool replaced;
}

View File

@ -5,9 +5,9 @@ class OmemoIncomingStanza {
const OmemoIncomingStanza( const OmemoIncomingStanza(
this.bareSenderJid, this.bareSenderJid,
this.senderDeviceId, this.senderDeviceId,
this.timestamp,
this.keys, this.keys,
this.payload, this.payload,
this.isCatchup,
); );
/// The bare JID of the sender of the stanza. /// The bare JID of the sender of the stanza.
@ -16,14 +16,14 @@ class OmemoIncomingStanza {
/// The device ID of the sender. /// The device ID of the sender.
final int senderDeviceId; final int senderDeviceId;
/// The timestamp when the stanza was received. /// The included encrypted keys for our own JID
final int timestamp;
/// The included encrypted keys
final List<EncryptedKey> keys; final List<EncryptedKey> keys;
/// The string payload included in the <encrypted /> element. /// The string payload included in the <encrypted /> element.
final String? payload; final String? payload;
/// Flag indicating whether the message was received due to a catchup.
final bool isCatchup;
} }
/// Describes a stanza that is to be sent out /// Describes a stanza that is to be sent out
@ -37,5 +37,5 @@ class OmemoOutgoingStanza {
final List<String> recipientJids; final List<String> recipientJids;
/// The serialised XML data that should be encrypted. /// The serialised XML data that should be encrypted.
final String payload; final String? payload;
} }

View File

@ -1,38 +0,0 @@
import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/protobuf/protobuf.dart';
class OmemoAuthenticatedMessage {
OmemoAuthenticatedMessage();
factory OmemoAuthenticatedMessage.fromBuffer(List<int> data) {
var i = 0;
// required bytes mac = 1;
if (data[0] != fieldId(1, fieldTypeByteArray)) {
throw Exception();
}
final mac = data.sublist(2, i + 2 + data[1]);
i += data[1] + 2;
if (data[i] != fieldId(2, fieldTypeByteArray)) {
throw Exception();
}
final message = data.sublist(i + 2, i + 2 + data[i + 1]);
return OmemoAuthenticatedMessage()
..mac = mac
..message = message;
}
List<int>? mac;
List<int>? message;
List<int> writeToBuffer() {
return concat([
[fieldId(1, fieldTypeByteArray), mac!.length],
mac!,
[fieldId(2, fieldTypeByteArray), message!.length],
message!,
]);
}
}

View File

@ -1,71 +0,0 @@
import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart';
import 'package:omemo_dart/src/protobuf/protobuf.dart';
class OmemoKeyExchange {
OmemoKeyExchange();
factory OmemoKeyExchange.fromBuffer(List<int> data) {
var i = 0;
if (data[i] != fieldId(1, fieldTypeUint32)) {
throw Exception();
}
var decoded = decodeVarint(data, 1);
final pkId = decoded.n;
i += decoded.length + 1;
if (data[i] != fieldId(2, fieldTypeUint32)) {
throw Exception();
}
decoded = decodeVarint(data, i + 1);
final spkId = decoded.n;
i += decoded.length + 1;
if (data[i] != fieldId(3, fieldTypeByteArray)) {
throw Exception();
}
final ik = data.sublist(i + 2, i + 2 + data[i + 1]);
i += 2 + data[i + 1];
if (data[i] != fieldId(4, fieldTypeByteArray)) {
throw Exception();
}
final ek = data.sublist(i + 2, i + 2 + data[i + 1]);
i += 2 + data[i + 1];
if (data[i] != fieldId(5, fieldTypeByteArray)) {
throw Exception();
}
final message = OmemoAuthenticatedMessage.fromBuffer(data.sublist(i + 2));
return OmemoKeyExchange()
..pkId = pkId
..spkId = spkId
..ik = ik
..ek = ek
..message = message;
}
int? pkId;
int? spkId;
List<int>? ik;
List<int>? ek;
OmemoAuthenticatedMessage? message;
List<int> writeToBuffer() {
final msg = message!.writeToBuffer();
return concat([
[fieldId(1, fieldTypeUint32)],
encodeVarint(pkId!),
[fieldId(2, fieldTypeUint32)],
encodeVarint(spkId!),
[fieldId(3, fieldTypeByteArray), ik!.length],
ik!,
[fieldId(4, fieldTypeByteArray), ek!.length],
ek!,
[fieldId(5, fieldTypeByteArray), msg.length],
msg,
]);
}
}

View File

@ -1,75 +0,0 @@
import 'package:omemo_dart/src/helpers.dart';
import 'package:omemo_dart/src/protobuf/protobuf.dart';
class OmemoMessage {
OmemoMessage();
factory OmemoMessage.fromBuffer(List<int> data) {
var i = 0;
// required uint32 n = 1;
if (data[0] != fieldId(1, fieldTypeUint32)) {
throw Exception();
}
var decode = decodeVarint(data, 1);
final n = decode.n;
i += decode.length + 1;
// required uint32 pn = 2;
if (data[i] != fieldId(2, fieldTypeUint32)) {
throw Exception();
}
decode = decodeVarint(data, i + 1);
final pn = decode.n;
i += decode.length + 1;
// required bytes dh_pub = 3;
if (data[i] != fieldId(3, fieldTypeByteArray)) {
throw Exception();
}
final dhPub = data.sublist(i + 2, i + 2 + data[i + 1]);
i += 2 + data[i + 1];
// optional bytes ciphertext = 4;
List<int>? ciphertext;
if (i < data.length) {
if (data[i] != fieldId(4, fieldTypeByteArray)) {
throw Exception();
}
ciphertext = data.sublist(i + 2, i + 2 + data[i + 1]);
}
return OmemoMessage()
..n = n
..pn = pn
..dhPub = dhPub
..ciphertext = ciphertext;
}
int? n;
int? pn;
List<int>? dhPub;
List<int>? ciphertext;
List<int> writeToBuffer() {
final data = concat([
[fieldId(1, fieldTypeUint32)],
encodeVarint(n!),
[fieldId(2, fieldTypeUint32)],
encodeVarint(pn!),
[fieldId(3, fieldTypeByteArray), dhPub!.length],
dhPub!,
]);
if (ciphertext != null) {
return concat([
data,
[fieldId(4, fieldTypeByteArray), ciphertext!.length],
ciphertext!,
]);
}
return data;
}
}

View File

@ -1,64 +0,0 @@
/// Masks the 7 LSB
const lsb7Mask = 0x7F;
/// Constant for setting the MSB
const msb = 1 << 7;
/// Field types
const fieldTypeUint32 = 0;
const fieldTypeByteArray = 2;
int fieldId(int number, int type) {
return (number << 3) | type;
}
class VarintDecode {
const VarintDecode(this.n, this.length);
final int n;
final int length;
}
/// Decode a Varint that begins at [input]'s index [offset].
VarintDecode decodeVarint(List<int> input, int offset) {
// The return value
var n = 0;
// The byte offset counter
var i = 0;
// Iterate until the MSB of the byte is 0
while (true) {
// Mask only the 7 LSB and "move" them accordingly
n += (input[offset + i] & lsb7Mask) << (7 * i);
// Break if we reached the end
if (input[offset + i] & 1 << 7 == 0) {
break;
}
i++;
}
return VarintDecode(n, i + 1);
}
// Encodes the integer [i] into a Varint.
List<int> encodeVarint(int i) {
assert(i >= 0, "Two's complement is not implemented");
final ret = List<int>.empty(growable: true);
// Thanks to https://github.com/hathibelagal-dev/LEB128 for the trick with toRadixString!
final numSevenBlocks = (i.toRadixString(2).length / 7).ceil();
for (var j = 0; j < numSevenBlocks; j++) {
// The 7 LSB of the byte we're creating
final x = (i & (lsb7Mask << j * 7)) >> j * 7;
if (j == numSevenBlocks - 1) {
// If we were to shift further, we only get zero, so we're at the end
ret.add(x);
} else {
// We still have at least one bit more to go, so set the MSB to 1
ret.add(x + msb);
}
}
return ret;
}

View File

@ -22,5 +22,5 @@ class AlwaysTrustingTrustManager extends TrustManager {
Future<void> removeTrustDecisionsForJid(String jid) async {} Future<void> removeTrustDecisionsForJid(String jid) async {}
@override @override
Future<Map<String, dynamic>> toJson() async => <String, dynamic>{}; Future<void> loadTrustData(String jid) async {}
} }

View File

@ -1,3 +1,5 @@
import 'package:meta/meta.dart';
/// The base class for managing trust in OMEMO sessions. /// The base class for managing trust in OMEMO sessions.
// ignore: one_member_abstracts // ignore: one_member_abstracts
abstract class TrustManager { abstract class TrustManager {
@ -7,6 +9,7 @@ abstract class TrustManager {
/// Called by the OmemoSessionManager when a new session has been built. Should set /// Called by the OmemoSessionManager when a new session has been built. Should set
/// a default trust state to [jid]'s device with identifier [deviceId]. /// a default trust state to [jid]'s device with identifier [deviceId].
@internal
Future<void> onNewSession(String jid, int deviceId); Future<void> onNewSession(String jid, int deviceId);
/// Return true if the device with id [deviceId] of Jid [jid] should be used for encryption. /// Return true if the device with id [deviceId] of Jid [jid] should be used for encryption.
@ -17,9 +20,14 @@ abstract class TrustManager {
/// 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();
/// Removes all trust decisions for [jid]. /// Removes all trust decisions for [jid].
@internal
Future<void> removeTrustDecisionsForJid(String jid); Future<void> removeTrustDecisionsForJid(String jid);
// ignore: comment_references
/// Called from within the [OmemoManager].
/// Loads the trust data for the JID [jid] from persistent storage
/// into the internal cache, if applicable.
@internal
Future<void> loadTrustData(String jid);
} }

View File

@ -1,71 +1,116 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:omemo_dart/src/helpers.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/trust/base.dart'; import 'package:omemo_dart/src/trust/base.dart';
import 'package:synchronized/synchronized.dart';
@immutable
class BTBVTrustData {
const BTBVTrustData(
this.jid,
this.device,
this.state,
this.enabled,
this.trusted,
);
/// The JID in question.
final String jid;
/// The device (ratchet) in question.
final int device;
/// The trust state of the ratchet.
final BTBVTrustState state;
/// Flag indicating whether the ratchet is enabled (true) or not (false).
final bool enabled;
/// Flag indicating whether the ratchet is trusted. For loading and commiting a ratchet, this field
/// contains an arbitrary value.
/// When using [BlindTrustBeforeVerificationTrustManager.getDevicesTrust], this flag will be true if
/// the ratchet is trusted and false if not.
final bool trusted;
}
/// A callback for when a trust decision is to be commited to persistent storage.
typedef BTBVTrustCommitCallback = Future<void> Function(BTBVTrustData data);
/// A stub-implementation of [BTBVTrustCommitCallback].
Future<void> btbvCommitStub(BTBVTrustData _) async {}
/// A callback for when all trust decisions for a JID should be removed from persistent storage.
typedef BTBVRemoveTrustForJidCallback = Future<void> Function(String jid);
/// A stub-implementation of [BTBVRemoveTrustForJidCallback].
Future<void> btbvRemoveTrustStub(String _) async {}
/// A callback for when trust data should be loaded.
typedef BTBVLoadDataCallback = Future<List<BTBVTrustData>> Function(String jid);
/// A stub-implementation for [BTBVLoadDataCallback].
Future<List<BTBVTrustData>> btbvLoadDataStub(String _) async => [];
/// Every device is in either of those two trust states: /// Every device is in either of those two trust states:
/// - notTrusted: The device is absolutely not trusted /// - notTrusted: The device is absolutely not trusted
/// - 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, // = 1 notTrusted(1),
blindTrust, // = 2 blindTrust(2),
verified, // = 3 verified(3);
}
int _trustToInt(BTBVTrustState state) { const BTBVTrustState(this.value);
switch (state) {
case BTBVTrustState.notTrusted:
return 1;
case BTBVTrustState.blindTrust:
return 2;
case BTBVTrustState.verified:
return 3;
}
}
BTBVTrustState _trustFromInt(int i) { factory BTBVTrustState.fromInt(int value) {
switch (i) { switch (value) {
case 1: case 1:
return BTBVTrustState.notTrusted; return BTBVTrustState.notTrusted;
case 2: case 2:
return BTBVTrustState.blindTrust; return BTBVTrustState.blindTrust;
case 3: case 3:
return BTBVTrustState.verified; return BTBVTrustState.verified;
default: // TODO(Unknown): Should we handle this better?
return BTBVTrustState.notTrusted; default:
return BTBVTrustState.notTrusted;
}
} }
/// The value backing the trust state.
final int value;
} }
/// 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 { class BlindTrustBeforeVerificationTrustManager extends TrustManager {
BlindTrustBeforeVerificationTrustManager({ BlindTrustBeforeVerificationTrustManager({
Map<RatchetMapKey, BTBVTrustState>? trustCache, this.loadData = btbvLoadDataStub,
Map<RatchetMapKey, bool>? enablementCache, this.commit = btbvCommitStub,
Map<String, List<int>>? devices, this.removeTrust = btbvRemoveTrustStub,
}) : 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 @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 @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 @visibleForTesting
@protected @protected
final Map<String, List<int>> devices; final Map<String, List<int>> devices = {};
/// The lock for devices and trustCache /// Callback for loading trust data.
final Lock _lock; final BTBVLoadDataCallback loadData;
/// Callback for commiting trust data to persistent storage.
final BTBVTrustCommitCallback commit;
/// Callback for removing trust data for a JID.
final BTBVRemoveTrustForJidCallback removeTrust;
/// 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
@ -80,69 +125,72 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
@override @override
Future<bool> isTrusted(String jid, int deviceId) async { Future<bool> isTrusted(String jid, int deviceId) async {
var returnValue = false; final trustCacheValue = trustCache[RatchetMapKey(jid, deviceId)];
await _lock.synchronized(() async { if (trustCacheValue == BTBVTrustState.notTrusted) {
final trustCacheValue = trustCache[RatchetMapKey(jid, deviceId)]; return false;
if (trustCacheValue == BTBVTrustState.notTrusted) { } else if (trustCacheValue == BTBVTrustState.verified) {
returnValue = false; // The key is verified, so it's safe.
return; return true;
} else if (trustCacheValue == BTBVTrustState.verified) { } else {
// The key is verified, so it's safe. if (_hasAtLeastOneVerifiedDevice(jid)) {
returnValue = true; // Do not trust if there is at least one device with full trust
return; return false;
} else { } else {
if (_hasAtLeastOneVerifiedDevice(jid)) { // We have not verified a key from [jid], so it is blind trust all the way.
// Do not trust if there is at least one device with full trust return true;
returnValue = false;
return;
} else {
// We have not verified a key from [jid], so it is blind trust all the way.
returnValue = true;
return;
}
} }
}); }
return returnValue;
} }
@override @override
Future<void> onNewSession(String jid, int deviceId) async { Future<void> onNewSession(String jid, int deviceId) async {
await _lock.synchronized(() async { final key = RatchetMapKey(jid, deviceId);
final key = RatchetMapKey(jid, deviceId); if (_hasAtLeastOneVerifiedDevice(jid)) {
if (_hasAtLeastOneVerifiedDevice(jid)) { trustCache[key] = BTBVTrustState.notTrusted;
trustCache[key] = BTBVTrustState.notTrusted; enablementCache[key] = false;
enablementCache[key] = false; } else {
} else { trustCache[key] = BTBVTrustState.blindTrust;
trustCache[key] = BTBVTrustState.blindTrust; enablementCache[key] = true;
enablementCache[key] = true; }
}
if (devices.containsKey(jid)) { // Append to the device list
devices[jid]!.add(deviceId); devices.appendOrCreate(jid, deviceId, checkExistence: true);
} else {
devices[jid] = List<int>.from([deviceId]);
}
// Commit the state // Commit the state
await commitState(); await commit(
}); BTBVTrustData(
jid,
deviceId,
trustCache[key]!,
enablementCache[key]!,
false,
),
);
} }
/// Returns a mapping from the device identifiers of [jid] to their trust state. If /// Returns a mapping from the device identifiers of [jid] to their trust state. If
/// there are no devices known for [jid], then an empty map is returned. /// there are no devices known for [jid], then an empty map is returned.
Future<Map<int, BTBVTrustState>> getDevicesTrust(String jid) async { Future<Map<int, BTBVTrustData>> getDevicesTrust(String jid) async {
return _lock.synchronized(() async { final map = <int, BTBVTrustData>{};
final map = <int, BTBVTrustState>{};
if (!devices.containsKey(jid)) return map; if (!devices.containsKey(jid)) return map;
for (final deviceId in devices[jid]!) { for (final deviceId in devices[jid]!) {
map[deviceId] = trustCache[RatchetMapKey(jid, deviceId)]!; final key = RatchetMapKey(jid, deviceId);
if (!trustCache.containsKey(key) || !enablementCache.containsKey(key)) {
continue;
} }
return map; map[deviceId] = BTBVTrustData(
}); jid,
deviceId,
trustCache[key]!,
enablementCache[key]!,
await isTrusted(jid, deviceId),
);
}
return map;
} }
/// Sets the trust of [jid]'s device with identifier [deviceId] to [state]. /// Sets the trust of [jid]'s device with identifier [deviceId] to [state].
@ -151,108 +199,71 @@ abstract class BlindTrustBeforeVerificationTrustManager extends TrustManager {
int deviceId, int deviceId,
BTBVTrustState state, BTBVTrustState state,
) async { ) async {
await _lock.synchronized(() async { final key = RatchetMapKey(jid, deviceId);
trustCache[RatchetMapKey(jid, deviceId)] = state; trustCache[key] = state;
// Commit the state // Commit the state
await commitState(); await commit(
}); BTBVTrustData(
jid,
deviceId,
state,
enablementCache[key]!,
false,
),
);
} }
@override @override
Future<bool> isEnabled(String jid, int deviceId) async { Future<bool> isEnabled(String jid, int deviceId) async {
return _lock.synchronized(() async { final value = enablementCache[RatchetMapKey(jid, deviceId)];
final value = enablementCache[RatchetMapKey(jid, deviceId)];
if (value == null) return false; if (value == null) return false;
return value; return value;
});
} }
@override @override
Future<void> setEnabled(String jid, int deviceId, bool enabled) async { Future<void> setEnabled(String jid, int deviceId, bool enabled) async {
await _lock.synchronized(() async { final key = RatchetMapKey(jid, deviceId);
enablementCache[RatchetMapKey(jid, deviceId)] = enabled; enablementCache[key] = enabled;
});
// Commit the state // Commit the state
await commitState(); await commit(
} BTBVTrustData(
jid,
@override deviceId,
Future<Map<String, dynamic>> toJson() async { trustCache[key]!,
return { enabled,
'devices': devices, false,
'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,
), ),
); );
} }
@override @override
Future<void> removeTrustDecisionsForJid(String jid) async { Future<void> removeTrustDecisionsForJid(String jid) async {
await _lock.synchronized(() async { // Clear the caches
devices.remove(jid); for (final device in devices[jid]!) {
await commitState(); final key = RatchetMapKey(jid, device);
}); trustCache.remove(key);
enablementCache.remove(key);
}
devices.remove(jid);
// Commit the state
await removeTrust(jid);
} }
/// Called when the state of the trust manager has been changed. Allows the user to @override
/// commit the trust state to persistent storage. Future<void> loadTrustData(String jid) async {
@visibleForOverriding for (final result in await loadData(jid)) {
Future<void> commitState(); final key = RatchetMapKey(jid, result.device);
trustCache[key] = result.state;
enablementCache[key] = result.enabled;
devices.appendOrCreate(jid, result.device, checkExistence: true);
}
}
@visibleForTesting @visibleForTesting
BTBVTrustState getDeviceTrust(String jid, int deviceId) => BTBVTrustState getDeviceTrust(String jid, int deviceId) =>
trustCache[RatchetMapKey(jid, 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 {}
}

View File

@ -22,5 +22,5 @@ class NeverTrustingTrustManager extends TrustManager {
Future<void> removeTrustDecisionsForJid(String jid) async {} Future<void> removeTrustDecisionsForJid(String jid) async {}
@override @override
Future<Map<String, dynamic>> toJson() async => <String, dynamic>{}; Future<void> loadTrustData(String jid) async {}
} }

View File

@ -1,15 +1,14 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:moxlib/moxlib.dart';
import 'package:omemo_dart/src/common/constants.dart';
import 'package:omemo_dart/src/crypto.dart'; import 'package:omemo_dart/src/crypto.dart';
import 'package:omemo_dart/src/errors.dart'; import 'package:omemo_dart/src/errors.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';
/// The overarching assumption is that we use Ed25519 keys for the identity keys
const omemoX3DHInfoString = 'OMEMO X3DH';
/// Performed by Alice /// Performed by Alice
class X3DHAliceResult { class X3DHAliceResult {
const X3DHAliceResult(this.ek, this.sk, this.opkId, this.ad); const X3DHAliceResult(this.ek, this.sk, this.opkId, this.ad);
@ -70,7 +69,8 @@ Future<List<int>> kdf(List<int> km) async {
/// Alice builds a session with Bob using his bundle [bundle] and Alice's identity key /// Alice builds a session with Bob using his bundle [bundle] and Alice's identity key
/// pair [ik]. /// pair [ik].
Future<X3DHAliceResult> x3dhFromBundle( Future<Result<InvalidKeyExchangeSignatureError, X3DHAliceResult>>
x3dhFromBundle(
OmemoBundle bundle, OmemoBundle bundle,
OmemoKeyPair ik, OmemoKeyPair ik,
) async { ) async {
@ -84,7 +84,7 @@ Future<X3DHAliceResult> x3dhFromBundle(
); );
if (!signatureValue) { if (!signatureValue) {
throw InvalidSignatureException(); return Result(InvalidKeyExchangeSignatureError());
} }
// Generate EK // Generate EK
@ -106,7 +106,7 @@ Future<X3DHAliceResult> x3dhFromBundle(
await bundle.ik.getBytes(), await bundle.ik.getBytes(),
]); ]);
return X3DHAliceResult(ek, sk, opkId, ad); return Result(X3DHAliceResult(ek, sk, opkId, ad));
} }
/// Bob builds the X3DH shared secret from the inital message [msg], the SPK [spk], the /// Bob builds the X3DH shared secret from the inital message [msg], the SPK [spk], the

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.4.3 version: 0.5.0
homepage: https://github.com/PapaTutuWawa/omemo_dart homepage: https://github.com/PapaTutuWawa/omemo_dart
publish_to: https://git.polynom.me/api/packages/PapaTutuWawa/pub publish_to: https://git.polynom.me/api/packages/PapaTutuWawa/pub
@ -13,12 +13,15 @@ dependencies:
hex: ^0.2.0 hex: ^0.2.0
logging: ^1.0.2 logging: ^1.0.2
meta: ^1.7.0 meta: ^1.7.0
moxlib:
version: ^0.2.0
hosted: https://git.polynom.me/api/packages/Moxxy/pub
pinenacl: ^0.5.1 pinenacl: ^0.5.1
protobuf: ^2.1.0
protoc_plugin: ^20.0.1
synchronized: ^3.0.0+2 synchronized: ^3.0.0+2
dev_dependencies: dev_dependencies:
lints: ^2.0.0 lints: ^2.0.0
protobuf: ^2.1.0
protoc_plugin: ^20.0.1
test: ^1.21.0 test: ^1.21.0
very_good_analysis: ^3.0.1 very_good_analysis: ^3.0.1

View File

@ -1,39 +1,10 @@
// ignore_for_file: avoid_print
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'package:cryptography/cryptography.dart'; import 'package:cryptography/cryptography.dart';
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart';
import 'package:omemo_dart/protobuf/schema.pb.dart';
import 'package:omemo_dart/src/double_ratchet/crypto.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
test('Test encrypting and decrypting', () async {
final sessionAd = List<int>.filled(32, 0x0);
final mk = List<int>.filled(32, 0x1);
final plaintext = utf8.encode('Hallo');
final header = OMEMOMessage()
..n = 0
..pn = 0
..dhPub = List<int>.empty();
final asd = concat([sessionAd, header.writeToBuffer()]);
final ciphertext = await encrypt(
mk,
plaintext,
asd,
sessionAd,
);
final decrypted = await decrypt(
mk,
ciphertext,
asd,
sessionAd,
);
expect(decrypted, plaintext);
});
test('Test the Double Ratchet', () async { test('Test the Double Ratchet', () async {
// Generate keys // Generate keys
const bobJid = 'bob@other.example.server'; const bobJid = 'bob@other.example.server';
@ -57,7 +28,8 @@ void main() {
); );
// Alice does X3DH // Alice does X3DH
final resultAlice = await x3dhFromBundle(bundleBob, ikAlice); final resultAliceRaw = await x3dhFromBundle(bundleBob, ikAlice);
final resultAlice = resultAliceRaw.get<X3DHAliceResult>();
// Alice sends the inital message to Bob // Alice sends the inital message to Bob
// ... // ...
@ -74,23 +46,28 @@ void main() {
ikBob, ikBob,
); );
print('X3DH key exchange done'); log('X3DH key exchange done');
// Alice and Bob now share sk as a common secret and ad // Alice and Bob now share sk as a common secret and ad
// Build a session // Build a session
final alicesRatchet = await OmemoDoubleRatchet.initiateNewSession( final alicesRatchet = await OmemoDoubleRatchet.initiateNewSession(
spkBob.pk, spkBob.pk,
bundleBob.spkId,
ikBob.pk, ikBob.pk,
ikAlice.pk,
resultAlice.ek.pk,
resultAlice.sk, resultAlice.sk,
resultAlice.ad, resultAlice.ad,
0, resultAlice.opkId,
); );
final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession( final bobsRatchet = await OmemoDoubleRatchet.acceptNewSession(
spkBob, spkBob,
bundleBob.spkId,
ikAlice.pk, ikAlice.pk,
2,
resultAlice.ek.pk,
resultBob.sk, resultBob.sk,
resultBob.ad, resultBob.ad,
0,
); );
expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd); expect(alicesRatchet.sessionAd, bobsRatchet.sessionAd);
@ -98,40 +75,42 @@ void main() {
for (var i = 0; i < 100; i++) { for (var i = 0; i < 100; i++) {
final messageText = 'Hello, dear $i'; final messageText = 'Hello, dear $i';
log('${i + 1}/100');
if (i.isEven) { if (i.isEven) {
// Alice encrypts a message // Alice encrypts a message
final aliceRatchetResult = final aliceRatchetResult =
await alicesRatchet.ratchetEncrypt(utf8.encode(messageText)); await alicesRatchet.ratchetEncrypt(utf8.encode(messageText));
print('Alice sent the message'); log('Alice sent the message');
// Alice sends it to Bob // Alice sends it to Bob
// ... // ...
// Bob tries to decrypt it // Bob tries to decrypt it
final bobRatchetResult = await bobsRatchet.ratchetDecrypt( final bobRatchetResult = await bobsRatchet.ratchetDecrypt(
aliceRatchetResult.header, aliceRatchetResult,
aliceRatchetResult.ciphertext,
); );
print('Bob decrypted the message'); log('Bob decrypted the message');
expect(utf8.encode(messageText), bobRatchetResult); expect(bobRatchetResult.isType<List<int>>(), true);
expect(bobRatchetResult.get<List<int>>(), utf8.encode(messageText));
} else { } else {
// Bob sends a message to Alice // Bob sends a message to Alice
final bobRatchetResult = final bobRatchetResult =
await bobsRatchet.ratchetEncrypt(utf8.encode(messageText)); await bobsRatchet.ratchetEncrypt(utf8.encode(messageText));
print('Bob sent the message'); log('Bob sent the message');
// Bobs sends it to Alice // Bobs sends it to Alice
// ... // ...
// Alice tries to decrypt it // Alice tries to decrypt it
final aliceRatchetResult = await alicesRatchet.ratchetDecrypt( final aliceRatchetResult = await alicesRatchet.ratchetDecrypt(
bobRatchetResult.header, bobRatchetResult,
bobRatchetResult.ciphertext,
); );
print('Alice decrypted the message'); log('Alice decrypted the message');
expect(utf8.encode(messageText), aliceRatchetResult); expect(aliceRatchetResult.isType<List<int>>(), true);
expect(aliceRatchetResult.get<List<int>>(), utf8.encode(messageText));
expect(utf8.encode(messageText), aliceRatchetResult.get<List<int>>());
} }
} }
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,186 +0,0 @@
import 'package:omemo_dart/protobuf/schema.pb.dart';
import 'package:omemo_dart/src/protobuf/omemo_authenticated_message.dart';
import 'package:omemo_dart/src/protobuf/omemo_key_exchange.dart';
import 'package:omemo_dart/src/protobuf/omemo_message.dart';
import 'package:omemo_dart/src/protobuf/protobuf.dart';
import 'package:test/test.dart';
void main() {
group('Base 128 Varints', () {
test('Test simple parsing of Varints', () {
expect(
decodeVarint(<int>[1], 0).n,
1,
);
expect(
decodeVarint(<int>[1], 0).length,
1,
);
expect(
decodeVarint(<int>[0x96, 0x01, 0x00], 0).n,
150,
);
expect(
decodeVarint(<int>[0x96, 0x01, 0x00], 0).length,
2,
);
expect(
decodeVarint(<int>[172, 2, 0x8], 0).n,
300,
);
expect(
decodeVarint(<int>[172, 2, 0x8], 0).length,
2,
);
});
test('Test encoding Varints', () {
expect(
encodeVarint(1),
<int>[1],
);
expect(
encodeVarint(150),
<int>[0x96, 0x01],
);
expect(
encodeVarint(300),
<int>[172, 2],
);
});
test('Test some special cases', () {
expect(decodeVarint(encodeVarint(1042464893), 0).n, 1042464893);
});
});
group('OMEMOMessage', () {
test('Decode a OMEMOMessage', () {
final pbMessage = OMEMOMessage()
..n = 1
..pn = 5
..dhPub = <int>[1, 2, 3]
..ciphertext = <int>[4, 5, 6];
final serial = pbMessage.writeToBuffer();
final msg = OmemoMessage.fromBuffer(serial);
expect(msg.n, 1);
expect(msg.pn, 5);
expect(msg.dhPub, <int>[1, 2, 3]);
expect(msg.ciphertext, <int>[4, 5, 6]);
});
test('Decode a OMEMOMessage without ciphertext', () {
final pbMessage = OMEMOMessage()
..n = 1
..pn = 5
..dhPub = <int>[1, 2, 3];
final serial = pbMessage.writeToBuffer();
final msg = OmemoMessage.fromBuffer(serial);
expect(msg.n, 1);
expect(msg.pn, 5);
expect(msg.dhPub, <int>[1, 2, 3]);
expect(msg.ciphertext, null);
});
test('Encode a OMEMOMessage', () {
final m = OmemoMessage()
..n = 1
..pn = 5
..dhPub = <int>[1, 2, 3]
..ciphertext = <int>[4, 5, 6];
final serial = m.writeToBuffer();
final msg = OMEMOMessage.fromBuffer(serial);
expect(msg.n, 1);
expect(msg.pn, 5);
expect(msg.dhPub, <int>[1, 2, 3]);
expect(msg.ciphertext, <int>[4, 5, 6]);
});
test('Encode a OMEMOMessage without ciphertext', () {
final m = OmemoMessage()
..n = 1
..pn = 5
..dhPub = <int>[1, 2, 3];
final serial = m.writeToBuffer();
final msg = OMEMOMessage.fromBuffer(serial);
expect(msg.n, 1);
expect(msg.pn, 5);
expect(msg.dhPub, <int>[1, 2, 3]);
expect(msg.ciphertext, <int>[]);
});
});
group('OMEMOAuthenticatedMessage', () {
test('Test encoding a message', () {
final msg = OmemoAuthenticatedMessage()
..mac = <int>[1, 2, 3]
..message = <int>[4, 5, 6];
final decoded = OMEMOAuthenticatedMessage.fromBuffer(msg.writeToBuffer());
expect(decoded.mac, <int>[1, 2, 3]);
expect(decoded.message, <int>[4, 5, 6]);
});
test('Test decoding a message', () {
final msg = OMEMOAuthenticatedMessage()
..mac = <int>[1, 2, 3]
..message = <int>[4, 5, 6];
final bytes = msg.writeToBuffer();
final decoded = OmemoAuthenticatedMessage.fromBuffer(bytes);
expect(decoded.mac, <int>[1, 2, 3]);
expect(decoded.message, <int>[4, 5, 6]);
});
});
group('OMEMOKeyExchange', () {
test('Test encoding a message', () {
final authMessage = OmemoAuthenticatedMessage()
..mac = <int>[5, 6, 8, 0]
..message = <int>[4, 5, 7, 3, 2];
final message = OmemoKeyExchange()
..pkId = 698
..spkId = 245
..ik = <int>[1, 4, 6]
..ek = <int>[4, 6, 7, 80]
..message = authMessage;
final kex = OMEMOKeyExchange.fromBuffer(message.writeToBuffer());
expect(kex.pkId, 698);
expect(kex.spkId, 245);
expect(kex.ik, <int>[1, 4, 6]);
expect(kex.ek, <int>[4, 6, 7, 80]);
expect(kex.message.mac, <int>[5, 6, 8, 0]);
expect(kex.message.message, <int>[4, 5, 7, 3, 2]);
});
test('Test decoding a message', () {
final message = OMEMOAuthenticatedMessage()
..mac = <int>[5, 6, 8, 0]
..message = <int>[4, 5, 7, 3, 2];
final kex = OMEMOKeyExchange()
..pkId = 698
..spkId = 245
..ik = <int>[1, 4, 6]
..ek = <int>[4, 6, 7, 80]
..message = message;
final decoded = OmemoKeyExchange.fromBuffer(kex.writeToBuffer());
expect(decoded.pkId, 698);
expect(decoded.spkId, 245);
expect(decoded.ik, <int>[1, 4, 6]);
expect(decoded.ek, <int>[4, 6, 7, 80]);
expect(decoded.message!.mac, <int>[5, 6, 8, 0]);
expect(decoded.message!.message, <int>[4, 5, 7, 3, 2]);
});
test('Test decoding an issue', () {
/*
final data = 'CAAQfRogc2GwslU219dUkrMHNM4KdZRmuFnBTae+bQaJ+55IsAMiII7aZKj2sUpb6xR/3Ari7WZUmKFV0G6czUc4NMvjKDBaKnwKEM2ZpI8X3TgcxhxwENANnlsSaAgAEAAaICy8T9WPgLb7RdYd8/4JkrLF0RahEkC3ZaEfk5jw3dsLIkBMILzLyByweLgF4lCn0oNea+kbdrFr6rY7r/7WyI8hXEQz38QpnN+jyGGwC7Ga0dq70WuyqE7VpiFArQwqZh2G';
final kex = OmemoKeyExchange.fromBuffer(base64Decode(data));
expect(kex.spkId!, 1042464893);
*/
});
});
}

62
test/queue_test.dart Normal file
View File

@ -0,0 +1,62 @@
import 'dart:async';
import 'package:omemo_dart/src/omemo/queue.dart';
import 'package:test/test.dart';
Future<void> testMethod(
RatchetAccessQueue queue,
List<String> data,
int duration,
) async {
await queue.enterCriticalSection(data);
await Future<void>.delayed(Duration(seconds: duration));
await queue.leaveCriticalSection(data);
}
void main() {
test('Test blocking due to conflicts', () async {
final queue = RatchetAccessQueue();
unawaited(testMethod(queue, ['a', 'b', 'c'], 5));
unawaited(testMethod(queue, ['a'], 4));
await Future<void>.delayed(const Duration(seconds: 1));
expect(
queue.runningOperations.containsAll(['a', 'b', 'c']),
isTrue,
);
expect(queue.runningOperations.length, 3);
await Future<void>.delayed(const Duration(seconds: 4));
expect(
queue.runningOperations.containsAll(['a']),
isTrue,
);
expect(queue.runningOperations.length, 1);
await Future<void>.delayed(const Duration(seconds: 4));
expect(queue.runningOperations.length, 0);
});
test('Test not blocking due to no conflicts', () async {
final queue = RatchetAccessQueue();
unawaited(testMethod(queue, ['a', 'b'], 5));
unawaited(testMethod(queue, ['c'], 5));
unawaited(testMethod(queue, ['d'], 5));
await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.runningOperations.length, 4);
expect(
queue.runningOperations.containsAll([
'a',
'b',
'c',
'd',
]),
isTrue,
);
});
}

View File

@ -1,155 +0,0 @@
import 'dart:convert';
import 'package:omemo_dart/omemo_dart.dart';
import 'package:omemo_dart/src/trust/always.dart';
import 'package:test/test.dart';
Map<String, dynamic> jsonify(Map<String, dynamic> map) {
return jsonDecode(jsonEncode(map)) as Map<String, dynamic>;
}
void main() {
test('Test serialising and deserialising the Device', () async {
// Generate a random session
final oldSession = await OmemoSessionManager.generateNewIdentity(
'user@test.server',
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final oldDevice = await oldSession.getDevice();
final serialised = jsonify(await oldDevice.toJson());
final newDevice = OmemoDevice.fromJson(serialised);
expect(await oldDevice.equals(newDevice), true);
});
test('Test serialising and deserialising the Device after rotating the SPK',
() async {
// Generate a random session
final oldSession = await OmemoSessionManager.generateNewIdentity(
'user@test.server',
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final oldDevice =
await (await oldSession.getDevice()).replaceSignedPrekey();
final serialised = jsonify(await oldDevice.toJson());
final newDevice = OmemoDevice.fromJson(serialised);
expect(await oldDevice.equals(newDevice), true);
});
test('Test serialising and deserialising the OmemoDoubleRatchet', () async {
// Generate a random ratchet
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
final aliceSession = await OmemoSessionManager.generateNewIdentity(
aliceJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
bobJid,
AlwaysTrustingTrustManager(),
opkAmount: 1,
);
final aliceMessage = await aliceSession.encryptToJid(
bobJid,
'Hello Bob!',
newSessions: [
await bobSession.getDeviceBundle(),
],
);
await bobSession.decryptMessage(
aliceMessage.ciphertext,
aliceJid,
await aliceSession.getDeviceId(),
aliceMessage.encryptedKeys,
getTimestamp(),
);
final aliceOld =
aliceSession.getRatchet(bobJid, await bobSession.getDeviceId());
final aliceSerialised = jsonify(await aliceOld.toJson());
final aliceNew = OmemoDoubleRatchet.fromJson(aliceSerialised);
expect(await aliceOld.equals(aliceNew), true);
});
test('Test serialising and deserialising the OmemoSessionManager', () async {
// Generate a random session
final oldSession = await OmemoSessionManager.generateNewIdentity(
'a@server',
AlwaysTrustingTrustManager(),
opkAmount: 4,
);
final bobSession = await OmemoSessionManager.generateNewIdentity(
'b@other.server',
AlwaysTrustingTrustManager(),
opkAmount: 4,
);
await oldSession.addSessionFromBundle(
'bob@localhost',
await bobSession.getDeviceId(),
await bobSession.getDeviceBundle(),
);
// Serialise and deserialise
final serialised = jsonify(await oldSession.toJsonWithoutSessions());
final newSession = OmemoSessionManager.fromJsonWithoutSessions(
serialised,
// NOTE: At this point, we don't care about this attribute
{},
AlwaysTrustingTrustManager(),
);
final oldDevice = await oldSession.getDevice();
final newDevice = await newSession.getDevice();
expect(await oldDevice.equals(newDevice), true);
expect(await oldSession.getDeviceMap(), await newSession.getDeviceMap());
});
test('Test serializing and deserializing RatchetMapKey', () {
const test1 = RatchetMapKey('user@example.org', 1234);
final result1 = RatchetMapKey.fromJsonKey(test1.toJsonKey());
expect(result1.jid, test1.jid);
expect(result1.deviceId, test1.deviceId);
const test2 = RatchetMapKey('user@example.org/hallo:welt', 3333);
final result2 = RatchetMapKey.fromJsonKey(test2.toJsonKey());
expect(result2.jid, test2.jid);
expect(result2.deviceId, test2.deviceId);
});
test('Test serializing and deserializing the components of the BTBV manager',
() async {
// Caroline's BTBV manager
final btbv = MemoryBTBVTrustManager();
// Example data
const aliceJid = 'alice@some.server';
const bobJid = 'bob@other.server';
await btbv.onNewSession(aliceJid, 1);
await btbv.setDeviceTrust(aliceJid, 1, BTBVTrustState.verified);
await btbv.onNewSession(aliceJid, 2);
await btbv.onNewSession(bobJid, 3);
await btbv.onNewSession(bobJid, 4);
final serialized = jsonify(await btbv.toJson());
final deviceList =
BlindTrustBeforeVerificationTrustManager.deviceListFromJson(
serialized,
);
expect(btbv.devices, deviceList);
final trustCache =
BlindTrustBeforeVerificationTrustManager.trustCacheFromJson(
serialized,
);
expect(btbv.trustCache, trustCache);
final enableCache =
BlindTrustBeforeVerificationTrustManager.enableCacheFromJson(
serialized,
);
expect(btbv.enablementCache, enableCache);
});
}

View File

@ -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 = MemoryBTBVTrustManager(); final btbv = BlindTrustBeforeVerificationTrustManager();
// Example data // Example data
const aliceJid = 'alice@some.server'; const aliceJid = 'alice@some.server';
const bobJid = 'bob@other.server'; const bobJid = 'bob@other.server';

View File

@ -26,7 +26,8 @@ void main() {
); );
// Alice does X3DH // Alice does X3DH
final resultAlice = await x3dhFromBundle(bundleBob, ikAlice); final resultAliceRaw = await x3dhFromBundle(bundleBob, ikAlice);
final resultAlice = resultAliceRaw.get<X3DHAliceResult>();
// Alice sends the inital message to Bob // Alice sends the inital message to Bob
// ... // ...
@ -68,18 +69,7 @@ void main() {
); );
// Alice does X3DH // Alice does X3DH
var exception = false; final result = await x3dhFromBundle(bundleBob, ikAlice);
try { expect(result.isType<InvalidKeyExchangeSignatureError>(), isTrue);
await x3dhFromBundle(bundleBob, ikAlice);
} catch (e) {
exception = true;
expect(
e is InvalidSignatureException,
true,
reason: 'Expected InvalidSignatureException, but got $e',
);
}
expect(exception, true, reason: 'Expected test failure');
}); });
} }