feat(xep): Implement an OMEMO example client

This commit is contained in:
PapaTutuWawa 2023-06-17 21:28:54 +02:00
parent 29a5417b31
commit c2f62e2967
18 changed files with 285 additions and 89 deletions

View File

@ -30,11 +30,12 @@ class EchoMessageManager extends XmppManagerBase {
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final body = stanza.firstTag('body'); final body = stanza.firstTag('body');
if (body == null) return state.copyWith(done: true); if (body == null) return state..done = true;
final bodyText = body.innerText(); final bodyText = body.innerText();
await getAttributes().sendStanza( await getAttributes().sendStanza(
StanzaDetails(
Stanza.message( Stanza.message(
to: stanza.from, to: stanza.from,
children: [ children: [
@ -45,9 +46,10 @@ class EchoMessageManager extends XmppManagerBase {
], ],
), ),
awaitable: false, awaitable: false,
),
); );
return state.copyWith(done: true); return state..done = true;
} }
} }

View File

@ -0,0 +1,140 @@
import 'package:args/args.dart';
import 'package:chalkdart/chalk.dart';
import 'package:cli_repl/cli_repl.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxmpp_socket_tcp/moxxmpp_socket_tcp.dart';
import 'package:omemo_dart/omemo_dart.dart' as omemo;
class TestingOmemoManager extends BaseOmemoManager {
TestingOmemoManager(this._encryptToJid);
final JID _encryptToJid;
late omemo.OmemoManager manager;
@override
Future<omemo.OmemoManager> getOmemoManager() async {
return manager;
}
@override
Future<bool> shouldEncryptStanza(JID toJid, Stanza stanza) async {
return toJid.toBare() == _encryptToJid;
}
}
class TestingTCPSocketWrapper extends TCPSocketWrapper {
@override
bool onBadCertificate(dynamic certificate, String domain) {
return true;
}
}
void main(List<String> args) async {
// Set up logging
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// ignore: avoid_print
print(
'[${record.level.name}] (${record.loggerName}) ${record.time}: ${record.message}',
);
});
final parser = ArgParser()
..addOption('jid')
..addOption('password')
..addOption('host')
..addOption('port')
..addOption('to');
final options = parser.parse(args);
// Connect
final jid = JID.fromString(options['jid']! as String);
final to = JID.fromString(options['to']! as String).toBare();
final portString = options['port'] as String?;
final connection = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
ClientToServerNegotiator(),
TestingTCPSocketWrapper(),
)..connectionSettings = ConnectionSettings(
jid: jid,
password: options['password']! as String,
host: options['host'] as String?,
port: portString != null ? int.parse(portString) : null,
);
// Generate OMEMO data
final moxxmppOmemo = TestingOmemoManager(to);
final omemoManager = omemo.OmemoManager(
await omemo.OmemoDevice.generateNewDevice(jid.toString(), opkAmount: 5),
omemo.BlindTrustBeforeVerificationTrustManager(),
moxxmppOmemo.sendEmptyMessageImpl,
moxxmppOmemo.fetchDeviceList,
moxxmppOmemo.fetchDeviceBundle,
moxxmppOmemo.subscribeToDeviceListImpl,
);
moxxmppOmemo.manager = omemoManager;
final deviceId = await omemoManager.getDeviceId();
Logger.root.info('Our device id: $deviceId');
// Register the managers and negotiators
await connection.registerManagers([
PresenceManager(),
DiscoManager([]),
PubSubManager(),
MessageManager(),
moxxmppOmemo,
]);
await connection.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
StartTlsNegotiator(),
SaslScramNegotiator(10, '', '', ScramHashType.sha1),
]);
// Set up event handlers
connection.asBroadcastStream().listen((event) {
if (event is MessageEvent) {
Logger.root.info(event.id);
Logger.root.info(event.extensions.keys.toList());
final body = event.encryptionError != null
? chalk.red('Failed to decrypt message: ${event.encryptionError}')
: chalk.green(event.get<MessageBodyData>()?.body ?? '');
print('[${event.from.toString()}] ' + body);
}
});
// Connect
Logger.root.info('Connecting...');
final result = await connection.connect(shouldReconnect: false, waitUntilLogin: true);
if (!result.isType<bool>()) {
Logger.root.severe('Authentication failed!');
return;
}
Logger.root.info('Connected.');
// Publish our bundle
Logger.root.info('Publishing bundle');
final device = await moxxmppOmemo.manager.getDevice();
final omemoResult = await moxxmppOmemo.publishBundle(await device.toBundle());
if (!omemoResult.isType<bool>()) {
Logger.root.severe('Failed to publish OMEMO bundle: ${omemoResult.get<OmemoError>()}');
return;
}
final repl = Repl(prompt: '> ');
await for (final line in repl.runAsync()) {
await connection.getManagerById<MessageManager>(messageManager)!.sendMessage(
to,
TypedMap<StanzaHandlerExtension>.fromList([
MessageBodyData(line),
]),
);
}
// Disconnect
await connection.disconnect();
}

View File

@ -7,6 +7,9 @@ environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=2.18.0 <3.0.0'
dependencies: dependencies:
args: 2.4.1
chalkdart: 2.0.9
cli_repl: 0.2.3
logging: ^1.0.2 logging: ^1.0.2
moxxmpp: moxxmpp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
@ -14,6 +17,8 @@ dependencies:
moxxmpp_socket_tcp: moxxmpp_socket_tcp:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: 0.3.1 version: 0.3.1
omemo_dart:
path: ../../../Personal/omemo_dart
dependency_overrides: dependency_overrides:
moxxmpp: moxxmpp:

View File

@ -0,0 +1,2 @@
prosodyctl --config ./prosody.cfg.lua register testuser1 localhost abc123
prosodyctl --config ./prosody.cfg.lua register testuser2 localhost abc123

View File

@ -479,6 +479,7 @@ class XmppConnection {
newStanza, newStanza,
TypedMap(), TypedMap(),
encrypted: details.encrypted, encrypted: details.encrypted,
shouldEncrypt: details.shouldEncrypt,
forceEncryption: details.forceEncryption, forceEncryption: details.forceEncryption,
), ),
); );
@ -736,6 +737,13 @@ class XmppConnection {
: ''; : '';
_log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}'); _log.finest('<== $prefix${incomingPreHandlers.stanza.toXml()}');
if (incomingPreHandlers.skip) {
_log.fine(
'Not processing stanza (${incomingPreHandlers.stanza.tag}, ${incomingPreHandlers.stanza.id}) due to skip=true.',
);
return;
}
final awaited = await _stanzaAwaiter.onData( final awaited = await _stanzaAwaiter.onData(
incomingPreHandlers.stanza, incomingPreHandlers.stanza,
connectionSettings.jid.toBare(), connectionSettings.jid.toBare(),
@ -753,6 +761,7 @@ class XmppConnection {
incomingPreHandlers.stanza, incomingPreHandlers.stanza,
incomingPreHandlers.extensions, incomingPreHandlers.extensions,
encrypted: incomingPreHandlers.encrypted, encrypted: incomingPreHandlers.encrypted,
encryptionError: incomingPreHandlers.encryptionError,
cancelReason: incomingPreHandlers.cancelReason, cancelReason: incomingPreHandlers.cancelReason,
), ),
); );

View File

@ -70,9 +70,9 @@ class MessageEvent extends XmppEvent {
MessageEvent( MessageEvent(
this.from, this.from,
this.to, this.to,
this.id,
this.encrypted, this.encrypted,
this.extensions, { this.extensions, {
this.id,
this.type, this.type,
this.error, this.error,
this.encryptionError, this.encryptionError,
@ -85,7 +85,7 @@ class MessageEvent extends XmppEvent {
final JID to; final JID to;
/// The id attribute of the message. /// The id attribute of the message.
final String id; final String? id;
/// The type attribute of the message. /// The type attribute of the message.
final String? type; final String? type;

View File

@ -13,12 +13,20 @@ class StanzaHandlerData {
this.encryptionError, this.encryptionError,
this.encrypted = false, this.encrypted = false,
this.forceEncryption = false, this.forceEncryption = false,
this.shouldEncrypt = true,
this.skip = false,
}); });
/// Indicates to the runner that processing is now done. This means that all /// Indicates to the runner that processing is now done. This means that all
/// pre-processing is done and no other handlers should be consulted. /// pre-processing is done and no other handlers should be consulted.
bool done; bool done;
/// Only useful in combination with [done] = true: When [skip] is set to true and
/// this [StanzaHandlerData] object is returned from a IncomingPreStanzaHandler, then
/// moxxmpp will skip checking whether the stanza was awaited and will not run any actual
/// IncomingStanzaHandler callbacks.
bool skip;
/// Indicates to the runner that processing is to be cancelled and no further handlers /// Indicates to the runner that processing is to be cancelled and no further handlers
/// should run. The stanza also will not be sent. /// should run. The stanza also will not be sent.
bool cancel; bool cancel;
@ -33,7 +41,7 @@ class StanzaHandlerData {
/// absolutely necessary, e.g. with Message Carbons or OMEMO. /// absolutely necessary, e.g. with Message Carbons or OMEMO.
Stanza stanza; Stanza stanza;
/// Whether the stanza was received encrypted /// Whether the stanza is already encrypted
bool encrypted; bool encrypted;
// If true, forces the encryption manager to encrypt to the JID, even if it // If true, forces the encryption manager to encrypt to the JID, even if it
@ -42,6 +50,10 @@ class StanzaHandlerData {
// to the JID anyway. // to the JID anyway.
bool forceEncryption; bool forceEncryption;
/// Flag indicating whether a E2EE implementation should encrypt the stanza (true)
/// or not (false).
bool shouldEncrypt;
/// Additional data from other managers. /// Additional data from other managers.
final TypedMap<StanzaHandlerExtension> extensions; final TypedMap<StanzaHandlerExtension> extensions;
} }

View File

@ -73,16 +73,23 @@ class MessageManager extends XmppManagerBase {
Future<bool> isSupported() async => true; Future<bool> isSupported() async => true;
Future<StanzaHandlerData> _onMessage( Future<StanzaHandlerData> _onMessage(
Stanza _, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final body = stanza.firstTag('body');
if (body != null) {
state.extensions.set(
MessageBodyData(body.innerText()),
);
}
getAttributes().sendEvent( getAttributes().sendEvent(
MessageEvent( MessageEvent(
JID.fromString(state.stanza.attributes['from']! as String), JID.fromString(state.stanza.attributes['from']! as String),
JID.fromString(state.stanza.attributes['to']! as String), JID.fromString(state.stanza.attributes['to']! as String),
state.stanza.attributes['id']! as String,
state.encrypted, state.encrypted,
state.extensions, state.extensions,
id: state.stanza.attributes['id'] as String?,
type: state.stanza.attributes['type'] as String?, type: state.stanza.attributes['type'] as String?,
error: StanzaError.fromStanza(state.stanza), error: StanzaError.fromStanza(state.stanza),
encryptionError: state.encryptionError, encryptionError: state.encryptionError,

View File

@ -7,6 +7,7 @@ class StanzaDetails {
this.stanza, { this.stanza, {
this.addId = true, this.addId = true,
this.awaitable = true, this.awaitable = true,
this.shouldEncrypt = true,
this.encrypted = false, this.encrypted = false,
this.forceEncryption = false, this.forceEncryption = false,
this.bypassQueue = false, this.bypassQueue = false,
@ -22,9 +23,16 @@ class StanzaDetails {
/// Track the stanza to allow awaiting its response. /// Track the stanza to allow awaiting its response.
final bool awaitable; final bool awaitable;
final bool forceEncryption;
/// Flag indicating whether the stanza that is sent is already encrypted (true)
/// or not (false). This is only useful for E2EE implementations that have to
/// send heartbeats that must bypass themselves.
final bool encrypted; final bool encrypted;
final bool forceEncryption; /// Tells an E2EE implementation, if available, to encrypt the stanza (true) or
/// ignore the stanza (false).
final bool shouldEncrypt;
/// Bypasses being put into the queue. Useful for sending stanzas that must go out /// Bypasses being put into the queue. Useful for sending stanzas that must go out
/// now, where it's okay if it does not get sent. /// now, where it's okay if it does not get sent.

View File

@ -50,16 +50,12 @@ class AsyncStanzaQueue {
@visibleForTesting @visibleForTesting
Queue<StanzaQueueEntry> get queue => _queue; Queue<StanzaQueueEntry> get queue => _queue;
@visibleForTesting
bool get isRunning => _running;
/// Adds a job [entry] to the queue. /// Adds a job [entry] to the queue.
Future<void> enqueueStanza(StanzaQueueEntry entry) async { Future<void> enqueueStanza(StanzaQueueEntry entry) async {
await _lock.synchronized(() async { await _lock.synchronized(() async {
_queue.add(entry); _queue.add(entry);
if (!_running && _queue.isNotEmpty && await _canSendCallback()) { if (_queue.isNotEmpty && await _canSendCallback()) {
_running = true;
unawaited( unawaited(
_runJob(_queue.removeFirst()), _runJob(_queue.removeFirst()),
); );

View File

@ -20,4 +20,6 @@ class TypedMap<B> {
/// Return the object of type [T] from the map, if it has been stored. /// Return the object of type [T] from the map, if it has been stored.
T? get<T>() => _data[T] as T?; T? get<T>() => _data[T] as T?;
Iterable<Object> get keys => _data.keys;
} }

View File

@ -255,7 +255,7 @@ class DiscoManager extends XmppManagerBase {
Future<Result<DiscoError, DiscoInfo>> discoInfoQuery( Future<Result<DiscoError, DiscoInfo>> discoInfoQuery(
JID entity, { JID entity, {
String? node, String? node,
bool shouldEncrypt = true, bool shouldEncrypt = false,
bool shouldCache = true, bool shouldCache = true,
}) async { }) async {
DiscoInfo? info; DiscoInfo? info;
@ -294,7 +294,7 @@ class DiscoManager extends XmppManagerBase {
final stanza = (await getAttributes().sendStanza( final stanza = (await getAttributes().sendStanza(
StanzaDetails( StanzaDetails(
buildDiscoInfoQueryStanza(entity, node), buildDiscoInfoQueryStanza(entity, node),
encrypted: !shouldEncrypt, shouldEncrypt: shouldEncrypt,
), ),
))!; ))!;
final query = stanza.firstTag('query'); final query = stanza.firstTag('query');
@ -325,7 +325,7 @@ class DiscoManager extends XmppManagerBase {
Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery( Future<Result<DiscoError, List<DiscoItem>>> discoItemsQuery(
JID entity, { JID entity, {
String? node, String? node,
bool shouldEncrypt = true, bool shouldEncrypt = false,
}) async { }) async {
final key = DiscoCacheKey(entity, node); final key = DiscoCacheKey(entity, node);
final future = await _discoItemsTracker.waitFor(key); final future = await _discoItemsTracker.waitFor(key);

View File

@ -202,6 +202,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -245,6 +246,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -329,6 +331,7 @@ class PubSubManager extends XmppManagerBase {
) )
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
if (result.attributes['type'] != 'result') { if (result.attributes['type'] != 'result') {
@ -419,6 +422,7 @@ class PubSubManager extends XmppManagerBase {
) )
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -471,6 +475,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -521,6 +526,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
if (form.attributes['type'] != 'result') { if (form.attributes['type'] != 'result') {
@ -550,6 +556,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
if (submit.attributes['type'] != 'result') { if (submit.attributes['type'] != 'result') {
@ -580,6 +587,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;
@ -624,6 +632,7 @@ class PubSubManager extends XmppManagerBase {
), ),
], ],
), ),
shouldEncrypt: false,
), ),
))!; ))!;

View File

@ -16,6 +16,6 @@ class OmemoEncryptionError {
const OmemoEncryptionError(this.jids, this.devices); const OmemoEncryptionError(this.jids, this.devices);
/// See omemo_dart's EncryptionResult for info on these fields. /// See omemo_dart's EncryptionResult for info on these fields.
final Map<String, OmemoException> jids; final Map<String, OmemoError> jids;
final Map<RatchetMapKey, OmemoException> devices; final Map<RatchetMapKey, OmemoError> devices;
} }

View File

@ -24,7 +24,7 @@ import 'package:moxxmpp/src/xeps/xep_0384/crypto.dart';
import 'package:moxxmpp/src/xeps/xep_0384/errors.dart'; import 'package:moxxmpp/src/xeps/xep_0384/errors.dart';
import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart'; import 'package:moxxmpp/src/xeps/xep_0384/helpers.dart';
import 'package:moxxmpp/src/xeps/xep_0384/types.dart'; import 'package:moxxmpp/src/xeps/xep_0384/types.dart';
import 'package:omemo_dart/omemo_dart.dart'; import 'package:omemo_dart/omemo_dart.dart' as omemo;
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
const _doNotEncryptList = [ const _doNotEncryptList = [
@ -113,7 +113,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
// Tell the OmemoManager // Tell the OmemoManager
(await getOmemoManager()).onDeviceListUpdate(jid.toString(), ids); await (await getOmemoManager()).onDeviceListUpdate(jid.toString(), ids);
// Generate an event // Generate an event
getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids)); getAttributes().sendEvent(OmemoDeviceListUpdatedEvent(jid, ids));
@ -121,13 +121,13 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
@visibleForOverriding @visibleForOverriding
Future<OmemoManager> getOmemoManager(); Future<omemo.OmemoManager> getOmemoManager();
/// Wrapper around using getSessionManager and then calling getDeviceId on it. /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId(); Future<int> _getDeviceId() async => (await getOmemoManager()).getDeviceId();
/// Wrapper around using getSessionManager and then calling getDeviceId on it. /// Wrapper around using getSessionManager and then calling getDeviceId on it.
Future<OmemoBundle> _getDeviceBundle() async { Future<omemo.OmemoBundle> _getDeviceBundle() async {
final om = await getOmemoManager(); final om = await getOmemoManager();
final device = await om.getDevice(); final device = await om.getDevice();
return device.toBundle(); return device.toBundle();
@ -199,53 +199,43 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
XMLNode _buildEncryptedElement( XMLNode _buildEncryptedElement(
EncryptionResult result, omemo.EncryptionResult result,
String recipientJid, String recipientJid,
int deviceId, int deviceId,
) { ) {
final keyElements = <String, List<XMLNode>>{}; final keyElements = <String, List<XMLNode>>{};
for (final key in result.encryptedKeys) { for (final keys in result.encryptedKeys.entries) {
final keyElement = XMLNode( keyElements[keys.key] = keys.value.map((ek) => XMLNode(
tag: 'key', tag: 'key',
attributes: <String, String>{ attributes: {
'rid': '${key.rid}', 'rid': ek.rid.toString(),
'kex': key.kex ? 'true' : 'false', if (ek.kex)
'kex': 'true',
}, },
text: key.value, text: ek.value,
); ),).toList();
if (keyElements.containsKey(key.jid)) {
keyElements[key.jid]!.add(keyElement);
} else {
keyElements[key.jid] = [keyElement];
}
} }
final keysElements = keyElements.entries.map((entry) { final keysElements = keyElements.entries.map((entry) {
return XMLNode( return XMLNode(
tag: 'keys', tag: 'keys',
attributes: <String, String>{ attributes: {
'jid': entry.key, 'jid': entry.key,
}, },
children: entry.value, children: entry.value,
); );
}).toList(); }).toList();
var payloadElement = <XMLNode>[];
if (result.ciphertext != null) {
payloadElement = [
XMLNode(
tag: 'payload',
text: base64.encode(result.ciphertext!),
),
];
}
return XMLNode.xmlns( return XMLNode.xmlns(
tag: 'encrypted', tag: 'encrypted',
xmlns: omemoXmlns, xmlns: omemoXmlns,
children: [ children: [
...payloadElement, if (result.ciphertext != null)
XMLNode(
tag: 'payload',
text: base64Encode(result.ciphertext!),
),
XMLNode( XMLNode(
tag: 'header', tag: 'header',
attributes: <String, String>{ attributes: <String, String>{
@ -259,7 +249,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// For usage with omemo_dart's OmemoManager. /// For usage with omemo_dart's OmemoManager.
Future<void> sendEmptyMessageImpl( Future<void> sendEmptyMessageImpl(
EncryptionResult result, omemo.EncryptionResult result,
String toJid, String toJid,
) async { ) async {
await getAttributes().sendStanza( await getAttributes().sendStanza(
@ -301,17 +291,22 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
/// For usage with omemo_dart's OmemoManager /// For usage with omemo_dart's OmemoManager
Future<OmemoBundle?> fetchDeviceBundle(String jid, int id) async { Future<omemo.OmemoBundle?> fetchDeviceBundle(String jid, int id) async {
final result = await retrieveDeviceBundle(JID.fromString(jid), id); final result = await retrieveDeviceBundle(JID.fromString(jid), id);
if (result.isType<OmemoError>()) return null; if (result.isType<OmemoError>()) return null;
return result.get<OmemoBundle>(); return result.get<omemo.OmemoBundle>();
} }
Future<StanzaHandlerData> _onOutgoingStanza( Future<StanzaHandlerData> _onOutgoingStanza(
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
if (!state.shouldEncrypt) {
logger.finest('Not encrypting since state.shouldEncrypt is false');
return state;
}
if (state.encrypted) { if (state.encrypted) {
logger.finest('Not encrypting since state.encrypted is true'); logger.finest('Not encrypting since state.encrypted is true');
return state; return state;
@ -352,29 +347,31 @@ abstract class BaseOmemoManager extends XmppManagerBase {
?.isEnabled ?? ?.isEnabled ??
false; false;
final om = await getOmemoManager(); final om = await getOmemoManager();
final result = await om.onOutgoingStanza( final encryptToJids = [
OmemoOutgoingStanza(
[
toJid.toString(), toJid.toString(),
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(), if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
], ];
final result = await om.onOutgoingStanza(
omemo.OmemoOutgoingStanza(
encryptToJids,
_buildEnvelope(toEncrypt, toJid.toString()), _buildEnvelope(toEncrypt, toJid.toString()),
), ),
); );
logger.finest('Encryption done'); logger.finest('Encryption done');
if (!result.isSuccess(2)) { if (!result.canSend) {
return state return state
..cancel = true ..cancel = true
// If we have no device list for toJid, then the contact most likely does not // If we have no device list for toJid, then the contact most likely does not
// support OMEMO:2 // support OMEMO:2
..cancelReason = result.jidEncryptionErrors[toJid.toString()] ..cancelReason = result.deviceEncryptionErrors[toJid.toString()]!.first.error
is NoKeyMaterialAvailableException is omemo.NoKeyMaterialAvailableError
? OmemoNotSupportedForContactException() ? OmemoNotSupportedForContactException()
: UnknownOmemoError() : UnknownOmemoError()
..encryptionError = OmemoEncryptionError( // TODO
result.jidEncryptionErrors, ..encryptionError = const OmemoEncryptionError(
result.deviceEncryptionErrors, {},
{},
); );
} }
@ -411,20 +408,19 @@ abstract class BaseOmemoManager extends XmppManagerBase {
Stanza stanza, Stanza stanza,
StanzaHandlerData state, StanzaHandlerData state,
) async { ) async {
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns);
if (encrypted == null) return state;
if (stanza.from == null) return state; if (stanza.from == null) return state;
final encrypted = stanza.firstTag('encrypted', xmlns: omemoXmlns)!;
final fromJid = JID.fromString(stanza.from!).toBare(); final fromJid = JID.fromString(stanza.from!).toBare();
final header = encrypted.firstTag('header')!; final header = encrypted.firstTag('header')!;
final payloadElement = encrypted.firstTag('payload'); final payloadElement = encrypted.firstTag('payload');
final keys = List<EncryptedKey>.empty(growable: true); // TODO: Only extract our own keys by the JID attribute
final keys = List<omemo.EncryptedKey>.empty(growable: true);
for (final keysElement in header.findTags('keys')) { for (final keysElement in header.findTags('keys')) {
final jid = keysElement.attributes['jid']! as String; final jid = keysElement.attributes['jid']! as String;
for (final key in keysElement.findTags('key')) { for (final key in keysElement.findTags('key')) {
keys.add( keys.add(
EncryptedKey( omemo.EncryptedKey(
jid,
int.parse(key.attributes['rid']! as String), int.parse(key.attributes['rid']! as String),
key.innerText(), key.innerText(),
key.attributes['kex'] == 'true', key.attributes['kex'] == 'true',
@ -438,7 +434,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
final om = await getOmemoManager(); final om = await getOmemoManager();
final result = await om.onIncomingStanza( final result = await om.onIncomingStanza(
OmemoIncomingStanza( omemo.OmemoIncomingStanza(
fromJid.toString(), fromJid.toString(),
sid, sid,
state.extensions state.extensions
@ -448,6 +444,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
DateTime.now().millisecondsSinceEpoch, DateTime.now().millisecondsSinceEpoch,
keys, keys,
payloadElement?.innerText(), payloadElement?.innerText(),
false,
), ),
); );
@ -464,6 +461,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
.toList(); .toList();
} }
logger.finest('Got payload: ${result.payload != null}');
if (result.payload != null) { if (result.payload != null) {
XMLNode envelope; XMLNode envelope;
try { try {
@ -481,6 +479,8 @@ abstract class BaseOmemoManager extends XmppManagerBase {
// Do not add forbidden elements from the envelope // Do not add forbidden elements from the envelope
envelopeChildren.where(shouldEncryptElement), envelopeChildren.where(shouldEncryptElement),
); );
logger.finest('Adding children: ${envelopeChildren.map((c) => c.tag)}');
} else { } else {
logger.warning('Invalid envelope element: No <content /> element'); logger.warning('Invalid envelope element: No <content /> element');
} }
@ -490,6 +490,15 @@ abstract class BaseOmemoManager extends XmppManagerBase {
} }
} }
// Ignore heartbeat messages
if (stanza.tag == 'message' && encrypted.firstTag('payload') == null) {
logger.finest('Received empty OMEMO message. Ending processing early.');
return state
..encrypted = true
..skip = true
..done = true;
}
return state return state
..encrypted = true ..encrypted = true
..stanza = Stanza( ..stanza = Stanza(
@ -532,7 +541,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieve all device bundles for the JID [jid]. /// Retrieve all device bundles for the JID [jid].
/// ///
/// On success, returns a list of devices. On failure, returns am OmemoError. /// On success, returns a list of devices. On failure, returns am OmemoError.
Future<Result<OmemoError, List<OmemoBundle>>> retrieveDeviceBundles( Future<Result<OmemoError, List<omemo.OmemoBundle>>> retrieveDeviceBundles(
JID jid, JID jid,
) async { ) async {
// TODO(Unknown): Should we query the device list first? // TODO(Unknown): Should we query the device list first?
@ -553,7 +562,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// Retrieves a bundle from entity [jid] with the device id [deviceId]. /// Retrieves a bundle from entity [jid] with the device id [deviceId].
/// ///
/// On success, returns the device bundle. On failure, returns an OmemoError. /// On success, returns the device bundle. On failure, returns an OmemoError.
Future<Result<OmemoError, OmemoBundle>> retrieveDeviceBundle( Future<Result<OmemoError, omemo.OmemoBundle>> retrieveDeviceBundle(
JID jid, JID jid,
int deviceId, int deviceId,
) async { ) async {
@ -569,7 +578,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// nodes. /// nodes.
/// ///
/// On success, returns true. On failure, returns an OmemoError. /// On success, returns true. On failure, returns an OmemoError.
Future<Result<OmemoError, bool>> publishBundle(OmemoBundle bundle) async { Future<Result<OmemoError, bool>> publishBundle(omemo.OmemoBundle bundle) async {
final attrs = getAttributes(); final attrs = getAttributes();
final pm = attrs.getManagerById<PubSubManager>(pubsubManager)!; final pm = attrs.getManagerById<PubSubManager>(pubsubManager)!;
final bareJid = attrs.getFullJID().toBare(); final bareJid = attrs.getFullJID().toBare();
@ -642,7 +651,7 @@ abstract class BaseOmemoManager extends XmppManagerBase {
/// On failure, returns an OmemoError. /// On failure, returns an OmemoError.
Future<Result<OmemoError, bool>> supportsOmemo(JID jid) async { Future<Result<OmemoError, bool>> supportsOmemo(JID jid) async {
final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!; final dm = getAttributes().getManagerById<DiscoManager>(discoManager)!;
final items = await dm.discoItemsQuery(jid.toBare()); final items = await dm.discoItemsQuery(jid.toBare(), shouldEncrypt: false);
if (items.isType<DiscoError>()) return Result(UnknownOmemoError()); if (items.isType<DiscoError>()) return Result(UnknownOmemoError());

View File

@ -18,10 +18,9 @@ dependencies:
meta: ^1.7.0 meta: ^1.7.0
moxlib: moxlib:
hosted: https://git.polynom.me/api/packages/Moxxy/pub hosted: https://git.polynom.me/api/packages/Moxxy/pub
version: ^0.1.5 version: ^0.2.0
omemo_dart: omemo_dart:
hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub path: ../../../../Personal/omemo_dart
version: ^0.4.3
random_string: ^2.3.1 random_string: ^2.3.1
saslprep: ^1.0.2 saslprep: ^1.0.2
synchronized: ^3.0.0+2 synchronized: ^3.0.0+2

View File

@ -30,7 +30,6 @@ void main() {
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.queue.length, 2); expect(queue.queue.length, 2);
expect(queue.isRunning, false);
}); });
test('Test sending', () async { test('Test sending', () async {
@ -58,7 +57,6 @@ void main() {
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.queue.length, 0); expect(queue.queue.length, 0);
expect(queue.isRunning, false);
}); });
test('Test partial sending and resuming', () async { test('Test partial sending and resuming', () async {
@ -89,12 +87,10 @@ void main() {
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.queue.length, 1); expect(queue.queue.length, 1);
expect(queue.isRunning, false);
canRun = true; canRun = true;
await queue.restart(); await queue.restart();
await Future<void>.delayed(const Duration(seconds: 1)); await Future<void>.delayed(const Duration(seconds: 1));
expect(queue.queue.length, 0); expect(queue.queue.length, 0);
expect(queue.isRunning, false);
}); });
} }