Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 3.5.8

- Upgraded SDK constraints and lints.
- New SASL authenticator (reduce dependencies that haven't been updated for a while).
- Supporting more URL-based connection-string parameters (mostly for pool).
- Optimized `StackTrace` capture [#432](https://github.com/isoos/postgresql-dart/pull/432) by [gmpassos](https://github.com/gmpassos).

Expand Down
12 changes: 1 addition & 11 deletions lib/src/auth/auth.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import 'package:crypto/crypto.dart';
import 'package:sasl_scram/sasl_scram.dart';

import '../../messages.dart';
import 'clear_text_authenticator.dart';
import 'md5_authenticator.dart';
Expand Down Expand Up @@ -41,14 +38,7 @@ PostgresAuthenticator createAuthenticator(
case AuthenticationScheme.md5:
return MD5Authenticator(connection);
case AuthenticationScheme.scramSha256:
final credentials = UsernamePasswordCredential(
username: connection.username,
password: connection.password,
);
return PostgresSaslAuthenticator(
connection,
ScramAuthenticator('SCRAM-SHA-256', sha256, credentials),
);
return PostgresSaslAuthenticator(connection);
case AuthenticationScheme.clear:
return ClearAuthenticator(connection);
}
Expand Down
192 changes: 171 additions & 21 deletions lib/src/auth/sasl_authenticator.dart
Original file line number Diff line number Diff line change
@@ -1,48 +1,45 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';

import 'package:sasl_scram/sasl_scram.dart';
import 'package:crypto/crypto.dart';

import '../buffer.dart';
import '../exceptions.dart';
import '../messages/client_messages.dart';
import '../messages/server_messages.dart';
import 'auth.dart';

final _random = Random.secure();

/// Structure for SASL Authenticator
class PostgresSaslAuthenticator extends PostgresAuthenticator {
final SaslAuthenticator authenticator;
PostgresSaslAuthenticator(super.connection);

PostgresSaslAuthenticator(super.connection, this.authenticator);
late final _authenticator = _ScramSha256Authenticator(
username: connection.username ?? '',
password: connection.password ?? '',
);

@override
void onMessage(AuthenticationMessage message) {
ClientMessage msg;
ClientMessage? msg;
switch (message.type) {
case AuthenticationMessageType.sasl:
final bytesToSend = authenticator.handleMessage(
SaslMessageType.AuthenticationSASL,
message.bytes,
);
if (bytesToSend == null) {
throw PgException('KindSASL: No bytes to send');
}
msg = SaslClientFirstMessage(bytesToSend, authenticator.mechanism.name);
// Server sends list of supported mechanisms
final bytesToSend = _authenticator.generateClientFirstMessage();
msg = SaslClientFirstMessage(bytesToSend, 'SCRAM-SHA-256');
break;
case AuthenticationMessageType.saslContinue:
final bytesToSend = authenticator.handleMessage(
SaslMessageType.AuthenticationSASLContinue,
// Server sends server-first-message
final bytesToSend = _authenticator.processServerFirstMessage(
message.bytes,
);
if (bytesToSend == null) {
throw PgException('KindSASLContinue: No bytes to send');
}
msg = SaslClientLastMessage(bytesToSend);
break;
case AuthenticationMessageType.saslFinal:
authenticator.handleMessage(
SaslMessageType.AuthenticationSASLFinal,
message.bytes,
);
// Server sends server-final-message
_authenticator.verifyServerFinalMessage(message.bytes);
return;
default:
throw PgException(
Expand All @@ -53,6 +50,159 @@ class PostgresSaslAuthenticator extends PostgresAuthenticator {
}
}

/// SCRAM-SHA-256 authenticator implementation
class _ScramSha256Authenticator {
final String username;
final String password;

late String _clientNonce;
late String _clientFirstMessageBare;
String? _serverNonce;
String? _salt;
int? _iterations;
String? _authMessage;

_ScramSha256Authenticator({required this.username, required this.password});

/// Generate client-first-message
Uint8List generateClientFirstMessage() {
_clientNonce = base64.encode(
List<int>.generate(24, (_) => _random.nextInt(256)),
);

final encodedUsername = username
.replaceAll('=', '=3D')
.replaceAll(',', '=2C');
_clientFirstMessageBare = 'n=$encodedUsername,r=$_clientNonce';

// client-first-message: GS2 header + client-first-message-bare
// GS2 header: "n,," (no channel binding)
final clientFirstMessage = 'n,,$_clientFirstMessageBare';

return utf8.encode(clientFirstMessage);
}

/// Process server-first-message and generate client-final-message
Uint8List processServerFirstMessage(Uint8List serverFirstMessageBytes) {
final serverFirstMessage = utf8.decode(serverFirstMessageBytes);

// Parse server-first-message: r=<nonce>,s=<salt>,i=<iteration-count>
final parts = _parseMessage(serverFirstMessage);

_serverNonce = parts['r'];
_salt = parts['s'];
_iterations = int.parse(parts['i'] ?? '0');

if (_serverNonce == null || !_serverNonce!.startsWith(_clientNonce)) {
throw PgException('Server nonce does not start with client nonce');
}

// Build client-final-message-without-proof
final channelBinding = 'c=${base64.encode(utf8.encode('n,,'))}';
final clientFinalMessageWithoutProof = '$channelBinding,r=$_serverNonce';

// Calculate auth message
_authMessage =
'$_clientFirstMessageBare,$serverFirstMessage,$clientFinalMessageWithoutProof';

// Calculate client proof
final saltedPassword = _hi(
utf8.encode(password),
base64.decode(_salt!),
_iterations!,
);

final clientKey = _hmac(saltedPassword, utf8.encode('Client Key'));
final storedKey = sha256.convert(clientKey).bytes;
final clientSignature = _hmac(storedKey, utf8.encode(_authMessage!));

final clientProof = Uint8List(clientKey.length);
for (var i = 0; i < clientKey.length; i++) {
clientProof[i] = clientKey[i] ^ clientSignature[i];
}

// Build client-final-message
final clientFinalMessage =
'$clientFinalMessageWithoutProof,p=${base64.encode(clientProof)}';

return Uint8List.fromList(utf8.encode(clientFinalMessage));
}

/// Verify server-final-message
void verifyServerFinalMessage(Uint8List serverFinalMessageBytes) {
final serverFinalMessage = utf8.decode(serverFinalMessageBytes);

// Parse server-final-message: v=<verifier> or e=<error>
final parts = _parseMessage(serverFinalMessage);

if (parts.containsKey('e')) {
throw PgException('SCRAM authentication failed: ${parts['e']}');
}

final serverSignatureB64 = parts['v'];
if (serverSignatureB64 == null) {
throw PgException('Server final message missing verifier');
}

// Calculate expected server signature
final saltedPassword = _hi(
utf8.encode(password),
base64.decode(_salt!),
_iterations!,
);

final serverKey = _hmac(saltedPassword, utf8.encode('Server Key'));
final serverSignature = _hmac(serverKey, utf8.encode(_authMessage!));

// Verify server signature
final expectedSignature = base64.encode(serverSignature);
if (serverSignatureB64 != expectedSignature) {
throw PgException('Server signature verification failed');
}
}

/// Parse SASL message into key-value pairs
Map<String, String> _parseMessage(String message) {
final result = <String, String>{};
final parts = message.split(',');

for (final part in parts) {
final index = part.indexOf('=');
if (index > 0) {
final key = part.substring(0, index);
final value = part.substring(index + 1);
result[key] = value;
}
}

return result;
}

/// HMAC-SHA256
List<int> _hmac(List<int> key, List<int> message) {
final hmacSha256 = Hmac(sha256, key);
return hmacSha256.convert(message).bytes;
}

/// PBKDF2 (Hi function): HMAC iterated i times
List<int> _hi(List<int> password, List<int> salt, int iterations) {
// First iteration: HMAC(password, salt + INT(1))
final saltWithCount = [...salt, 0, 0, 0, 1];
var u = _hmac(password, saltWithCount);
final result = List<int>.from(u);

// Remaining iterations
for (var i = 1; i < iterations; i++) {
u = _hmac(password, u);
for (var j = 0; j < result.length; j++) {
result[j] ^= u[j];
}
}

return result;
}
}

class SaslClientFirstMessage extends ClientMessage {
final Uint8List bytesToSendToServer;
final String mechanismName;
Expand Down
1 change: 0 additions & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ dependencies:
buffer: ^1.2.3
crypto: ^3.0.6
collection: ^1.19.1
sasl_scram: ^0.1.1
stack_trace: ^1.12.1
stream_channel: ^2.1.4
async: ^2.12.0
Expand Down