1
+ import 'dart:convert' ;
2
+ import 'dart:math' ;
1
3
import 'dart:typed_data' ;
2
4
3
- import 'package:sasl_scram/sasl_scram .dart' ;
5
+ import 'package:crypto/crypto .dart' ;
4
6
5
7
import '../buffer.dart' ;
6
8
import '../exceptions.dart' ;
7
9
import '../messages/client_messages.dart' ;
8
10
import '../messages/server_messages.dart' ;
9
11
import 'auth.dart' ;
10
12
13
+ final _random = Random .secure ();
14
+
11
15
/// Structure for SASL Authenticator
12
16
class PostgresSaslAuthenticator extends PostgresAuthenticator {
13
- final SaslAuthenticator authenticator ;
17
+ PostgresSaslAuthenticator ( super .connection) ;
14
18
15
- PostgresSaslAuthenticator (super .connection, this .authenticator);
19
+ late final _authenticator = _ScramSha256Authenticator (
20
+ username: connection.username ?? '' ,
21
+ password: connection.password ?? '' ,
22
+ );
16
23
17
24
@override
18
25
void onMessage (AuthenticationMessage message) {
19
- ClientMessage msg;
26
+ ClientMessage ? msg;
20
27
switch (message.type) {
21
28
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' );
30
32
break ;
31
33
case AuthenticationMessageType .saslContinue:
32
- final bytesToSend = authenticator. handleMessage (
33
- SaslMessageType . AuthenticationSASLContinue ,
34
+ // Server sends server-first-message
35
+ final bytesToSend = _authenticator. processServerFirstMessage (
34
36
message.bytes,
35
37
);
36
- if (bytesToSend == null ) {
37
- throw PgException ('KindSASLContinue: No bytes to send' );
38
- }
39
38
msg = SaslClientLastMessage (bytesToSend);
40
39
break ;
41
40
case AuthenticationMessageType .saslFinal:
42
- authenticator.handleMessage (
43
- SaslMessageType .AuthenticationSASLFinal ,
44
- message.bytes,
45
- );
41
+ // Server sends server-final-message
42
+ _authenticator.verifyServerFinalMessage (message.bytes);
46
43
return ;
47
44
default :
48
45
throw PgException (
@@ -53,6 +50,159 @@ class PostgresSaslAuthenticator extends PostgresAuthenticator {
53
50
}
54
51
}
55
52
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
+
56
206
class SaslClientFirstMessage extends ClientMessage {
57
207
final Uint8List bytesToSendToServer;
58
208
final String mechanismName;
0 commit comments