feat(xep): Implement an OMEMO example client
This commit is contained in:
parent
29a5417b31
commit
c2f62e2967
@ -30,24 +30,26 @@ 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(
|
||||||
Stanza.message(
|
StanzaDetails(
|
||||||
to: stanza.from,
|
Stanza.message(
|
||||||
children: [
|
to: stanza.from,
|
||||||
XMLNode(
|
children: [
|
||||||
tag: 'body',
|
XMLNode(
|
||||||
text: 'Hello, ${stanza.from}! You said "$bodyText"',
|
tag: 'body',
|
||||||
),
|
text: 'Hello, ${stanza.from}! You said "$bodyText"',
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
awaitable: false,
|
||||||
),
|
),
|
||||||
awaitable: false,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return state.copyWith(done: true);
|
return state..done = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
140
examples_dart/bin/omemo_client.dart
Normal file
140
examples_dart/bin/omemo_client.dart
Normal 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();
|
||||||
|
}
|
@ -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:
|
||||||
|
2
integration_tests/create_users.sh
Normal file
2
integration_tests/create_users.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
prosodyctl --config ./prosody.cfg.lua register testuser1 localhost abc123
|
||||||
|
prosodyctl --config ./prosody.cfg.lua register testuser2 localhost abc123
|
@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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()),
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
))!;
|
))!;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 encryptToJids = [
|
||||||
|
toJid.toString(),
|
||||||
|
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
|
||||||
|
];
|
||||||
final result = await om.onOutgoingStanza(
|
final result = await om.onOutgoingStanza(
|
||||||
OmemoOutgoingStanza(
|
omemo.OmemoOutgoingStanza(
|
||||||
[
|
encryptToJids,
|
||||||
toJid.toString(),
|
|
||||||
if (carbonsEnabled) getAttributes().getFullJID().toBare().toString(),
|
|
||||||
],
|
|
||||||
_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());
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user