feat(core): Make SCRAM SASL2 aware

This commit is contained in:
PapaTutuWawa 2023-03-31 21:09:16 +02:00
parent 7ab3f4f0d9
commit 478b5b8770
4 changed files with 61 additions and 36 deletions

View File

@ -268,7 +268,8 @@ class XmppConnection {
/// Register a list of negotiator with the connection. /// Register a list of negotiator with the connection.
Future<void> registerFeatureNegotiators( Future<void> registerFeatureNegotiators(
List<XmppFeatureNegotiatorBase> negotiators) async { List<XmppFeatureNegotiatorBase> negotiators,
) async {
for (final negotiator in negotiators) { for (final negotiator in negotiators) {
_log.finest('Registering ${negotiator.id}'); _log.finest('Registering ${negotiator.id}');
negotiator.register( negotiator.register(

View File

@ -4,7 +4,6 @@ import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/errors.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/sasl/nonza.dart';
import 'package:moxxmpp/src/negotiators/sasl2.dart'; import 'package:moxxmpp/src/negotiators/sasl2.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
@ -58,7 +57,6 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
} else { } else {
final tag = nonza.tag; final tag = nonza.tag;
if (tag == 'success') { if (tag == 'success') {
await attributes.sendEvent(AuthenticationSuccessEvent());
attributes.setAuthenticated(); attributes.setAuthenticated();
return const Result(NegotiatorState.done); return const Result(NegotiatorState.done);
} else { } else {
@ -90,7 +88,8 @@ class SaslPlainNegotiator extends Sasl2AuthenticationNegotiator {
Future<String> getRawStep(String input) async { Future<String> getRawStep(String input) async {
final settings = attributes.getConnectionSettings(); final settings = attributes.getConnectionSettings();
return base64.encode( return base64.encode(
utf8.encode('\u0000${settings.jid.local}\u0000${settings.password}')); utf8.encode('\u0000${settings.jid.local}\u0000${settings.password}'),
);
} }
@override @override

View File

@ -8,8 +8,8 @@ import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/errors.dart'; import 'package:moxxmpp/src/negotiators/sasl/errors.dart';
import 'package:moxxmpp/src/negotiators/sasl/kv.dart'; import 'package:moxxmpp/src/negotiators/sasl/kv.dart';
import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
import 'package:moxxmpp/src/negotiators/sasl/nonza.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/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/types/result.dart';
import 'package:random_string/random_string.dart'; import 'package:random_string/random_string.dart';
@ -95,7 +95,7 @@ enum ScramState { preSent, initialMessageSent, challengeResponseSent, error }
const gs2Header = 'n,,'; const gs2Header = 'n,,';
class SaslScramNegotiator extends SaslNegotiator { class SaslScramNegotiator extends Sasl2AuthenticationNegotiator {
// NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing // NOTE: NEVER, and I mean, NEVER set clientNonce or initalMessageNoGS2. They are just there for testing
SaslScramNegotiator( SaslScramNegotiator(
int priority, int priority,
@ -236,20 +236,9 @@ class SaslScramNegotiator extends SaslNegotiator {
) async { ) async {
switch (_scramState) { switch (_scramState) {
case ScramState.preSent: case ScramState.preSent:
if (clientNonce == null || clientNonce == '') {
clientNonce = randomAlphaNumeric(
40,
provider: CoreRandomProvider.from(Random.secure()),
);
}
initialMessageNoGS2 =
'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
_scramState = ScramState.initialMessageSent;
attributes.sendNonza( attributes.sendNonza(
SaslScramAuthNonza( SaslScramAuthNonza(
body: base64.encode(utf8.encode(gs2Header + initialMessageNoGS2)), body: await getRawStep(''),
type: hashType, type: hashType,
), ),
redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(), redact: SaslScramAuthNonza(body: '******', type: hashType).toXml(),
@ -266,12 +255,8 @@ class SaslScramNegotiator extends SaslNegotiator {
); );
} }
final challengeBase64 = nonza.innerText();
final response = await calculateChallengeResponse(challengeBase64);
final responseBase64 = base64.encode(utf8.encode(response));
_scramState = ScramState.challengeResponseSent;
attributes.sendNonza( attributes.sendNonza(
SaslScramResponseNonza(body: responseBase64), SaslScramResponseNonza(body: await getRawStep(nonza.innerText())),
redact: SaslScramResponseNonza(body: '******').toXml(), redact: SaslScramResponseNonza(body: '******').toXml(),
); );
return const Result(NegotiatorState.ready); return const Result(NegotiatorState.ready);
@ -314,4 +299,43 @@ class SaslScramNegotiator extends SaslNegotiator {
super.reset(); super.reset();
} }
@override
Future<String> getRawStep(String input) async {
switch (_scramState) {
case ScramState.preSent:
if (clientNonce == null || clientNonce == '') {
clientNonce = randomAlphaNumeric(
40,
provider: CoreRandomProvider.from(Random.secure()),
);
}
initialMessageNoGS2 =
'n=${attributes.getConnectionSettings().jid.local},r=$clientNonce';
_scramState = ScramState.initialMessageSent;
return base64.encode(utf8.encode(gs2Header + initialMessageNoGS2));
case ScramState.initialMessageSent:
final challengeBase64 = input;
final response = await calculateChallengeResponse(challengeBase64);
final responseBase64 = base64.encode(utf8.encode(response));
_scramState = ScramState.challengeResponseSent;
return responseBase64;
case ScramState.challengeResponseSent:
case ScramState.error:
return '';
}
}
@override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
return [];
}
@override
Future<void> onSasl2Success(XMLNode response) async {
state = NegotiatorState.done;
}
} }

View File

@ -1,4 +1,3 @@
import 'package:moxxmpp/src/events.dart';
import 'package:moxxmpp/src/namespaces.dart'; import 'package:moxxmpp/src/namespaces.dart';
import 'package:moxxmpp/src/negotiators/namespaces.dart'; import 'package:moxxmpp/src/negotiators/namespaces.dart';
import 'package:moxxmpp/src/negotiators/negotiator.dart'; import 'package:moxxmpp/src/negotiators/negotiator.dart';
@ -6,15 +5,14 @@ import 'package:moxxmpp/src/negotiators/sasl/negotiator.dart';
import 'package:moxxmpp/src/stringxml.dart'; import 'package:moxxmpp/src/stringxml.dart';
import 'package:moxxmpp/src/types/result.dart'; import 'package:moxxmpp/src/types/result.dart';
typedef Sasl2FeaturesReceivedCallback = Future<List<XMLNode>> Function(XMLNode); /// A special type of [XmppFeatureNegotiatorBase] that is aware of SASL2.
abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase { abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
Sasl2FeatureNegotiator( Sasl2FeatureNegotiator(
int priority, super.priority,
bool sendStreamHeaderWhenDone, super.sendStreamHeaderWhenDone,
String negotiatingXmlns, super.negotiatingXmlns,
String id, super.id,
) : super(priority, sendStreamHeaderWhenDone, negotiatingXmlns, id); );
/// Called by the SASL2 negotiator when we received the SASL2 stream features /// Called by the SASL2 negotiator when we received the SASL2 stream features
/// [sasl2Features]. The return value is a list of XML elements that should be /// [sasl2Features]. The return value is a list of XML elements that should be
@ -26,10 +24,10 @@ abstract class Sasl2FeatureNegotiator extends XmppFeatureNegotiatorBase {
Future<void> onSasl2Success(XMLNode response); Future<void> onSasl2Success(XMLNode response);
} }
/// A special type of [SaslNegotiator] that is aware of SASL2.
abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator
implements Sasl2FeatureNegotiator { implements Sasl2FeatureNegotiator {
Sasl2AuthenticationNegotiator(int priority, String id, String mechanismName) Sasl2AuthenticationNegotiator(super.priority, super.id, super.mechanismName);
: super(priority, id, mechanismName);
/// Perform a SASL step with [input] as the already parsed input data. Returns /// Perform a SASL step with [input] as the already parsed input data. Returns
/// the base64-encoded response data. /// the base64-encoded response data.
@ -59,8 +57,10 @@ class UserAgent {
final String? device; final String? device;
XMLNode toXml() { XMLNode toXml() {
assert(id != null || software != null || device != null, assert(
'A completely empty user agent makes no sense'); id != null || software != null || device != null,
'A completely empty user agent makes no sense',
);
return XMLNode( return XMLNode(
tag: 'user-agent', tag: 'user-agent',
attributes: id != null attributes: id != null
@ -130,7 +130,8 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
@override @override
Future<Result<NegotiatorState, NegotiatorError>> negotiate( Future<Result<NegotiatorState, NegotiatorError>> negotiate(
XMLNode nonza) async { XMLNode nonza,
) async {
switch (_sasl2State) { switch (_sasl2State) {
case Sasl2State.idle: case Sasl2State.idle:
final sasl2 = nonza.firstTag('authentication', xmlns: sasl2Xmlns)!; final sasl2 = nonza.firstTag('authentication', xmlns: sasl2Xmlns)!;