From 2e60e9841e75b53d08484ddbd2ad21f63c458da2 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Fri, 31 Mar 2023 19:02:57 +0200 Subject: [PATCH] feat(xep): Begin work on SASL2 --- packages/moxxmpp/lib/moxxmpp.dart | 1 + packages/moxxmpp/lib/src/connection.dart | 9 +- packages/moxxmpp/lib/src/namespaces.dart | 3 + .../lib/src/negotiators/namespaces.dart | 1 + .../lib/src/negotiators/negotiator.dart | 7 + .../lib/src/negotiators/sasl/plain.dart | 8 + .../moxxmpp/lib/src/negotiators/sasl2.dart | 161 ++++++++++++++++++ packages/moxxmpp/test/xeps/xep_0030_test.dart | 2 +- packages/moxxmpp/test/xeps/xep_0060_test.dart | 11 +- packages/moxxmpp/test/xeps/xep_0198_test.dart | 10 +- packages/moxxmpp/test/xeps/xep_0388_test.dart | 69 ++++++++ packages/moxxmpp/test/xmpp_test.dart | 29 ++-- 12 files changed, 282 insertions(+), 29 deletions(-) create mode 100644 packages/moxxmpp/lib/src/negotiators/sasl2.dart create mode 100644 packages/moxxmpp/test/xeps/xep_0388_test.dart diff --git a/packages/moxxmpp/lib/moxxmpp.dart b/packages/moxxmpp/lib/moxxmpp.dart index e8583ad..2f74fca 100644 --- a/packages/moxxmpp/lib/moxxmpp.dart +++ b/packages/moxxmpp/lib/moxxmpp.dart @@ -23,6 +23,7 @@ export 'package:moxxmpp/src/negotiators/sasl/errors.dart'; export 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; export 'package:moxxmpp/src/negotiators/sasl/plain.dart'; export 'package:moxxmpp/src/negotiators/sasl/scram.dart'; +export 'package:moxxmpp/src/negotiators/sasl2.dart'; export 'package:moxxmpp/src/negotiators/starttls.dart'; export 'package:moxxmpp/src/ping.dart'; export 'package:moxxmpp/src/presence.dart'; diff --git a/packages/moxxmpp/lib/src/connection.dart b/packages/moxxmpp/lib/src/connection.dart index 1f1f673..6dd47b4 100644 --- a/packages/moxxmpp/lib/src/connection.dart +++ b/packages/moxxmpp/lib/src/connection.dart @@ -254,7 +254,8 @@ class XmppConnection { } /// Register a list of negotiator with the connection. - void registerFeatureNegotiators(List negotiators) { + Future registerFeatureNegotiators( + List negotiators) async { for (final negotiator in negotiators) { _log.finest('Registering ${negotiator.id}'); negotiator.register( @@ -273,6 +274,10 @@ class XmppConnection { } _log.finest('Negotiators registered'); + + for (final negotiator in _featureNegotiators.values) { + await negotiator.postRegisterCallback(); + } } /// Reset all registered negotiators. @@ -399,7 +404,7 @@ class XmppConnection { // Close the socket _socket.close(); - + if (!error.isRecoverable()) { // We cannot recover this error _log.severe( diff --git a/packages/moxxmpp/lib/src/namespaces.dart b/packages/moxxmpp/lib/src/namespaces.dart index d49330b..f7a11eb 100644 --- a/packages/moxxmpp/lib/src/namespaces.dart +++ b/packages/moxxmpp/lib/src/namespaces.dart @@ -116,6 +116,9 @@ const omemoBundlesXmlns = 'urn:xmpp:omemo:2:bundles'; // XEP-0385 const simsXmlns = 'urn:xmpp:sims:1'; +// XEP-0388 +const sasl2Xmlns = 'urn:xmpp:sasl:2'; + // XEP-0420 const sceXmlns = 'urn:xmpp:sce:1'; diff --git a/packages/moxxmpp/lib/src/negotiators/namespaces.dart b/packages/moxxmpp/lib/src/negotiators/namespaces.dart index 00595b7..1605329 100644 --- a/packages/moxxmpp/lib/src/negotiators/namespaces.dart +++ b/packages/moxxmpp/lib/src/negotiators/namespaces.dart @@ -7,3 +7,4 @@ const rosterNegotiator = 'im.moxxmpp.core.roster'; const resourceBindingNegotiator = 'im.moxxmpp.core.resource'; const streamManagementNegotiator = 'im.moxxmpp.xeps.sm'; const startTlsNegotiator = 'im.moxxmpp.core.starttls'; +const sasl2Negotiator = 'org.moxxmpp.sasl.sasl2'; diff --git a/packages/moxxmpp/lib/src/negotiators/negotiator.dart b/packages/moxxmpp/lib/src/negotiators/negotiator.dart index 3594403..a251192 100644 --- a/packages/moxxmpp/lib/src/negotiators/negotiator.dart +++ b/packages/moxxmpp/lib/src/negotiators/negotiator.dart @@ -1,3 +1,4 @@ +import 'package:meta/meta.dart'; import 'package:moxlib/moxlib.dart'; import 'package:moxxmpp/src/errors.dart'; import 'package:moxxmpp/src/events.dart'; @@ -120,5 +121,11 @@ abstract class XmppFeatureNegotiatorBase { state = NegotiatorState.ready; } + @protected NegotiatorAttributes get attributes => _attributes; + + /// Run after all negotiators are registered. Useful for registering callbacks against + /// other negotiators. + @visibleForOverriding + Future postRegisterCallback() async {} } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart index 8ee4512..eb78979 100644 --- a/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart +++ b/packages/moxxmpp/lib/src/negotiators/sasl/plain.dart @@ -6,6 +6,7 @@ import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/sasl/errors.dart'; import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; import 'package:moxxmpp/src/negotiators/sasl/nonza.dart'; +import 'package:moxxmpp/src/negotiators/sasl2.dart'; import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/types/result.dart'; @@ -76,4 +77,11 @@ class SaslPlainNegotiator extends SaslNegotiator { super.reset(); } + + @override + Future postRegisterCallback() async { + attributes + .getNegotiatorById(sasl2Negotiator) + ?.registerSaslNegotiator(this); + } } diff --git a/packages/moxxmpp/lib/src/negotiators/sasl2.dart b/packages/moxxmpp/lib/src/negotiators/sasl2.dart new file mode 100644 index 0000000..694c7f6 --- /dev/null +++ b/packages/moxxmpp/lib/src/negotiators/sasl2.dart @@ -0,0 +1,161 @@ +import 'package:moxxmpp/src/namespaces.dart'; +import 'package:moxxmpp/src/negotiators/namespaces.dart'; +import 'package:moxxmpp/src/negotiators/negotiator.dart'; +import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart'; +import 'package:moxxmpp/src/stringxml.dart'; +import 'package:moxxmpp/src/types/result.dart'; + +typedef Sasl2FeaturesReceivedCallback = Future> Function(XMLNode); + +class NoSASLMechanismSelectedError extends NegotiatorError { + @override + bool isRecoverable() => false; +} + +/// A data class describing the user agent. See https://dyn.eightysoft.de/final/xep-0388.html#initiation +class UserAgent { + const UserAgent({ + this.id, + this.software, + this.device, + }); + + /// The identifier of the software/device combo connecting. SHOULD be a UUIDv4. + final String? id; + + /// The software's name that's connecting at the moment. + final String? software; + + /// The name of the device. + final String? device; + + XMLNode toXml() { + assert(id != null || software != null || device != null, + 'A completely empty user agent makes no sense'); + return XMLNode( + tag: 'user-agent', + attributes: id != null + ? { + 'id': id, + } + : {}, + children: [ + if (software != null) + XMLNode( + tag: 'software', + text: software, + ), + if (device != null) + XMLNode( + tag: 'device', + text: device, + ), + ], + ); + } +} + +enum Sasl2State { + // No request has been sent yet. + idle, +} + +/// A negotiator that implements XEP-0388 SASL2. Alone, it does nothing. Has to be +/// registered with other negotiators that register themselves against this one. +class Sasl2Negotiator extends XmppFeatureNegotiatorBase { + Sasl2Negotiator({ + this.userAgent, + }) : super(100, false, sasl2Xmlns, sasl2Negotiator); + + /// The user agent data that will be sent to the server when authenticating. + final UserAgent? userAgent; + + /// List of callbacks that are registered against us. Will be called once we get + /// SASL2 features. + final List _featureCallbacks = + List.empty(growable: true); + + /// List of SASL negotiators, sorted by their priority. The higher the priority, the + /// lower its index. + final List _saslNegotiators = + List.empty(growable: true); + + /// The state the SASL2 negotiator is currently in. + Sasl2State _sasl2State = Sasl2State.idle; + + /// The SASL negotiator that will negotiate authentication. + SaslNegotiator? _currentSaslNegotiator; + + void registerSaslNegotiator(SaslNegotiator negotiator) { + _saslNegotiators + ..add(negotiator) + ..sort((a, b) => b.priority.compareTo(a.priority)); + } + + void registerFeaturesCallback(Sasl2FeaturesReceivedCallback callback) { + _featureCallbacks.add(callback); + } + + @override + Future> negotiate( + XMLNode nonza) async { + switch (_sasl2State) { + case Sasl2State.idle: + final sasl2 = nonza.firstTag('authentication', xmlns: sasl2Xmlns)!; + final mechanisms = XMLNode.xmlns( + tag: 'mechanisms', + xmlns: saslXmlns, + children: sasl2.children.where((c) => c.tag == 'mechanism').toList(), + ); + for (final negotiator in _saslNegotiators) { + if (negotiator.matchesFeature([mechanisms])) { + _currentSaslNegotiator = negotiator; + break; + } + } + + // We must have a SASL negotiator by now + if (_currentSaslNegotiator == null) { + return Result(NoSASLMechanismSelectedError()); + } + + // Collect additional data by interested negotiators + final children = List.empty(growable: true); + for (final callback in _featureCallbacks) { + children.addAll( + await callback(sasl2), + ); + } + + // Build the authenticate nonza + final authenticate = XMLNode.xmlns( + tag: 'authenticate', + xmlns: sasl2Xmlns, + attributes: { + 'mechanism': _currentSaslNegotiator!.mechanismName, + }, + children: [ + if (userAgent != null) userAgent!.toXml(), + + // TODO: Get the initial response + XMLNode( + tag: 'initial-response', + ), + ...children, + ], + ); + attributes.sendNonza(authenticate); + return const Result(NegotiatorState.ready); + } + + return const Result(NegotiatorState.ready); + } + + @override + void reset() { + _currentSaslNegotiator = null; + _sasl2State = Sasl2State.idle; + + super.reset(); + } +} diff --git a/packages/moxxmpp/test/xeps/xep_0030_test.dart b/packages/moxxmpp/test/xeps/xep_0030_test.dart index 6f15953..4f3b903 100644 --- a/packages/moxxmpp/test/xeps/xep_0030_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0030_test.dart @@ -84,7 +84,7 @@ void main() { DiscoManager([]), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), SaslScramNegotiator(10, '', '', ScramHashType.sha512), ResourceBindingNegotiator(), diff --git a/packages/moxxmpp/test/xeps/xep_0060_test.dart b/packages/moxxmpp/test/xeps/xep_0060_test.dart index c52a328..a8c84a1 100644 --- a/packages/moxxmpp/test/xeps/xep_0060_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0060_test.dart @@ -169,12 +169,11 @@ void main() { MessageManager(), RosterManager(TestingRosterStateManager(null, [])), ]); - connection - ..registerFeatureNegotiators([ - SaslPlainNegotiator(), - ResourceBindingNegotiator(), - ]) - ..setConnectionSettings(TestingManagerHolder.settings); + await connection.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + ]); + connection.setConnectionSettings(TestingManagerHolder.settings); await connection.connect( waitUntilLogin: true, ); diff --git a/packages/moxxmpp/test/xeps/xep_0198_test.dart b/packages/moxxmpp/test/xeps/xep_0198_test.dart index 66e9349..770fece 100644 --- a/packages/moxxmpp/test/xeps/xep_0198_test.dart +++ b/packages/moxxmpp/test/xeps/xep_0198_test.dart @@ -298,7 +298,7 @@ void main() { CarbonsManager()..forceEnable(), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), @@ -423,7 +423,7 @@ void main() { CarbonsManager()..forceEnable(), //EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), @@ -580,7 +580,7 @@ void main() { DiscoManager([]), StreamManagementManager(), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), @@ -674,7 +674,7 @@ void main() { DiscoManager([]), StreamManagementManager(), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), @@ -765,7 +765,7 @@ void main() { DiscoManager([]), StreamManagementManager(), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ResourceBindingNegotiator(), StreamManagementNegotiator(), diff --git a/packages/moxxmpp/test/xeps/xep_0388_test.dart b/packages/moxxmpp/test/xeps/xep_0388_test.dart new file mode 100644 index 0000000..e381aac --- /dev/null +++ b/packages/moxxmpp/test/xeps/xep_0388_test.dart @@ -0,0 +1,69 @@ +import 'package:moxxmpp/moxxmpp.dart'; +import 'package:test/test.dart'; +import '../helpers/logging.dart'; +import '../helpers/xmpp.dart'; + +void main() { + initLogger(); + + test('Test simple SASL2 negotiation', () async { + final fakeSocket = StubTCPSocket([ + StringExpectation( + "", + ''' + + + + PLAIN + + + PLAIN + + ''', + ), + StanzaExpectation( + "moxxmppPapaTutuWawa's awesome deviceAHBvbHlub21kaXZpc2lvbgBhYWFh", + '', + ), + ]); + final conn = XmppConnection( + TestingReconnectionPolicy(), + AlwaysConnectedConnectivityManager(), + fakeSocket, + )..setConnectionSettings( + ConnectionSettings( + jid: JID.fromString('polynomdivision@test.server'), + password: 'aaaa', + useDirectTLS: true, + ), + ); + await conn.registerManagers([ + PresenceManager(), + RosterManager(TestingRosterStateManager('', [])), + DiscoManager([]), + ]); + await conn.registerFeatureNegotiators([ + SaslPlainNegotiator(), + ResourceBindingNegotiator(), + Sasl2Negotiator( + userAgent: const UserAgent( + id: 'd4565fa7-4d72-4749-b3d3-740edbf87770', + software: 'moxxmpp', + device: "PapaTutuWawa's awesome device", + ), + ), + ]); + + final result = await conn.connect( + waitUntilLogin: true, + shouldReconnect: false, + enableReconnectOnSuccess: false, + ); + expect(result.isType(), false); + }); +} diff --git a/packages/moxxmpp/test/xmpp_test.dart b/packages/moxxmpp/test/xmpp_test.dart index a20b982..829f101 100644 --- a/packages/moxxmpp/test/xmpp_test.dart +++ b/packages/moxxmpp/test/xmpp_test.dart @@ -140,7 +140,7 @@ void main() { StreamManagementManager(), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), SaslScramNegotiator(10, '', '', ScramHashType.sha512), ResourceBindingNegotiator(), @@ -195,7 +195,7 @@ void main() { DiscoManager([]), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([ + await conn.registerFeatureNegotiators([ SaslPlainNegotiator(), ]); @@ -254,7 +254,7 @@ void main() { DiscoManager([]), EntityCapabilitiesManager('http://moxxmpp.example'), ]); - conn.registerFeatureNegotiators([SaslPlainNegotiator()]); + await conn.registerFeatureNegotiators([SaslPlainNegotiator()]); conn.asBroadcastStream().listen((event) { if (event is AuthenticationFailedEvent && @@ -407,18 +407,17 @@ void main() { RosterManager(TestingRosterStateManager('', [])), DiscoManager([]), ]); - conn - ..registerFeatureNegotiators([ - // SaslPlainNegotiator(), - ResourceBindingNegotiator(), - ]) - ..setConnectionSettings( - ConnectionSettings( - jid: JID.fromString('testuser@example.org'), - password: 'abc123', - useDirectTLS: false, - ), - ); + await conn.registerFeatureNegotiators([ + // SaslPlainNegotiator(), + ResourceBindingNegotiator(), + ]); + conn.setConnectionSettings( + ConnectionSettings( + jid: JID.fromString('testuser@example.org'), + password: 'abc123', + useDirectTLS: false, + ), + ); final result = await conn.connect( waitUntilLogin: true,