1+ import 'dart:convert' ;
2+ import 'dart:math' ;
13import 'dart:typed_data' ;
24
3- import 'package:sasl_scram/sasl_scram .dart' ;
5+ import 'package:crypto/crypto .dart' ;
46
57import '../buffer.dart' ;
68import '../exceptions.dart' ;
79import '../messages/client_messages.dart' ;
810import '../messages/server_messages.dart' ;
911import 'auth.dart' ;
1012
13+ final _random = Random .secure ();
14+
1115/// Structure for SASL Authenticator
1216class PostgresSaslAuthenticator extends PostgresAuthenticator {
13- final SaslAuthenticator authenticator ;
17+ PostgresSaslAuthenticator ( super .connection) ;
1418
15- PostgresSaslAuthenticator (super .connection, this .authenticator);
19+ late final _authenticator = _ScramSha256Authenticator (
20+ username: connection.username ?? '' ,
21+ password: connection.password ?? '' ,
22+ );
1623
1724 @override
1825 void onMessage (AuthenticationMessage message) {
19- ClientMessage msg;
26+ ClientMessage ? msg;
2027 switch (message.type) {
2128 case AuthenticationMessageType .sasl:
22- final bytesToSend = authenticator.handleMessage (
23- SaslMessageType .AuthenticationSASL ,
24- message.bytes,
25- );
26- if (bytesToSend == null ) {
27- throw PgException ('KindSASL: No bytes to send' );
28- }
29- msg = SaslClientFirstMessage (bytesToSend, authenticator.mechanism.name);
29+ // Server sends list of supported mechanisms
30+ final bytesToSend = _authenticator.generateClientFirstMessage ();
31+ msg = SaslClientFirstMessage (bytesToSend, 'SCRAM-SHA-256' );
3032 break ;
3133 case AuthenticationMessageType .saslContinue:
32- final bytesToSend = authenticator. handleMessage (
33- SaslMessageType . AuthenticationSASLContinue ,
34+ // Server sends server-first-message
35+ final bytesToSend = _authenticator. processServerFirstMessage (
3436 message.bytes,
3537 );
36- if (bytesToSend == null ) {
37- throw PgException ('KindSASLContinue: No bytes to send' );
38- }
3938 msg = SaslClientLastMessage (bytesToSend);
4039 break ;
4140 case AuthenticationMessageType .saslFinal:
42- authenticator.handleMessage (
43- SaslMessageType .AuthenticationSASLFinal ,
44- message.bytes,
45- );
41+ // Server sends server-final-message
42+ _authenticator.verifyServerFinalMessage (message.bytes);
4643 return ;
4744 default :
4845 throw PgException (
@@ -53,6 +50,159 @@ class PostgresSaslAuthenticator extends PostgresAuthenticator {
5350 }
5451}
5552
53+ /// SCRAM-SHA-256 authenticator implementation
54+ class _ScramSha256Authenticator {
55+ final String username;
56+ final String password;
57+
58+ late String _clientNonce;
59+ late String _clientFirstMessageBare;
60+ String ? _serverNonce;
61+ String ? _salt;
62+ int ? _iterations;
63+ String ? _authMessage;
64+
65+ _ScramSha256Authenticator ({required this .username, required this .password});
66+
67+ /// Generate client-first-message
68+ Uint8List generateClientFirstMessage () {
69+ _clientNonce = base64.encode (
70+ List <int >.generate (24 , (_) => _random.nextInt (256 )),
71+ );
72+
73+ final encodedUsername = username
74+ .replaceAll ('=' , '=3D' )
75+ .replaceAll (',' , '=2C' );
76+ _clientFirstMessageBare = 'n=$encodedUsername ,r=$_clientNonce ' ;
77+
78+ // client-first-message: GS2 header + client-first-message-bare
79+ // GS2 header: "n,," (no channel binding)
80+ final clientFirstMessage = 'n,,$_clientFirstMessageBare ' ;
81+
82+ return utf8.encode (clientFirstMessage);
83+ }
84+
85+ /// Process server-first-message and generate client-final-message
86+ Uint8List processServerFirstMessage (Uint8List serverFirstMessageBytes) {
87+ final serverFirstMessage = utf8.decode (serverFirstMessageBytes);
88+
89+ // Parse server-first-message: r=<nonce>,s=<salt>,i=<iteration-count>
90+ final parts = _parseMessage (serverFirstMessage);
91+
92+ _serverNonce = parts['r' ];
93+ _salt = parts['s' ];
94+ _iterations = int .parse (parts['i' ] ?? '0' );
95+
96+ if (_serverNonce == null || ! _serverNonce! .startsWith (_clientNonce)) {
97+ throw PgException ('Server nonce does not start with client nonce' );
98+ }
99+
100+ // Build client-final-message-without-proof
101+ final channelBinding = 'c=${base64 .encode (utf8 .encode ('n,,' ))}' ;
102+ final clientFinalMessageWithoutProof = '$channelBinding ,r=$_serverNonce ' ;
103+
104+ // Calculate auth message
105+ _authMessage =
106+ '$_clientFirstMessageBare ,$serverFirstMessage ,$clientFinalMessageWithoutProof ' ;
107+
108+ // Calculate client proof
109+ final saltedPassword = _hi (
110+ utf8.encode (password),
111+ base64.decode (_salt! ),
112+ _iterations! ,
113+ );
114+
115+ final clientKey = _hmac (saltedPassword, utf8.encode ('Client Key' ));
116+ final storedKey = sha256.convert (clientKey).bytes;
117+ final clientSignature = _hmac (storedKey, utf8.encode (_authMessage! ));
118+
119+ final clientProof = Uint8List (clientKey.length);
120+ for (var i = 0 ; i < clientKey.length; i++ ) {
121+ clientProof[i] = clientKey[i] ^ clientSignature[i];
122+ }
123+
124+ // Build client-final-message
125+ final clientFinalMessage =
126+ '$clientFinalMessageWithoutProof ,p=${base64 .encode (clientProof )}' ;
127+
128+ return Uint8List .fromList (utf8.encode (clientFinalMessage));
129+ }
130+
131+ /// Verify server-final-message
132+ void verifyServerFinalMessage (Uint8List serverFinalMessageBytes) {
133+ final serverFinalMessage = utf8.decode (serverFinalMessageBytes);
134+
135+ // Parse server-final-message: v=<verifier> or e=<error>
136+ final parts = _parseMessage (serverFinalMessage);
137+
138+ if (parts.containsKey ('e' )) {
139+ throw PgException ('SCRAM authentication failed: ${parts ['e' ]}' );
140+ }
141+
142+ final serverSignatureB64 = parts['v' ];
143+ if (serverSignatureB64 == null ) {
144+ throw PgException ('Server final message missing verifier' );
145+ }
146+
147+ // Calculate expected server signature
148+ final saltedPassword = _hi (
149+ utf8.encode (password),
150+ base64.decode (_salt! ),
151+ _iterations! ,
152+ );
153+
154+ final serverKey = _hmac (saltedPassword, utf8.encode ('Server Key' ));
155+ final serverSignature = _hmac (serverKey, utf8.encode (_authMessage! ));
156+
157+ // Verify server signature
158+ final expectedSignature = base64.encode (serverSignature);
159+ if (serverSignatureB64 != expectedSignature) {
160+ throw PgException ('Server signature verification failed' );
161+ }
162+ }
163+
164+ /// Parse SASL message into key-value pairs
165+ Map <String , String > _parseMessage (String message) {
166+ final result = < String , String > {};
167+ final parts = message.split (',' );
168+
169+ for (final part in parts) {
170+ final index = part.indexOf ('=' );
171+ if (index > 0 ) {
172+ final key = part.substring (0 , index);
173+ final value = part.substring (index + 1 );
174+ result[key] = value;
175+ }
176+ }
177+
178+ return result;
179+ }
180+
181+ /// HMAC-SHA256
182+ List <int > _hmac (List <int > key, List <int > message) {
183+ final hmacSha256 = Hmac (sha256, key);
184+ return hmacSha256.convert (message).bytes;
185+ }
186+
187+ /// PBKDF2 (Hi function): HMAC iterated i times
188+ List <int > _hi (List <int > password, List <int > salt, int iterations) {
189+ // First iteration: HMAC(password, salt + INT(1))
190+ final saltWithCount = [...salt, 0 , 0 , 0 , 1 ];
191+ var u = _hmac (password, saltWithCount);
192+ final result = List <int >.from (u);
193+
194+ // Remaining iterations
195+ for (var i = 1 ; i < iterations; i++ ) {
196+ u = _hmac (password, u);
197+ for (var j = 0 ; j < result.length; j++ ) {
198+ result[j] ^ = u[j];
199+ }
200+ }
201+
202+ return result;
203+ }
204+ }
205+
56206class SaslClientFirstMessage extends ClientMessage {
57207 final Uint8List bytesToSendToServer;
58208 final String mechanismName;
0 commit comments