Skip to content

Commit 98d8659

Browse files
authored
Merge branch 'master' into 164-numeric-datatype
2 parents a960900 + 81ab97f commit 98d8659

12 files changed

+218
-75
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Unreleased
44

55
- Support for type `numeric` / `decimal` ([#7](https://github.com/isoos/postgresql-dart/pull/7), [#9](https://github.com/isoos/postgresql-dart/pull/9)).
6+
- Support SASL / SCRAM-SHA-256 Authentication, [#6](https://github.com/isoos/postgresql-dart/pull/6).
67

78
## 2.3.2
89

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![CI](https://github.com/isoos/postgresql-dart/actions/workflows/dart.yml/badge.svg)](https://github.com/isoos/postgresql-dart/actions/workflows/dart.yml)
44

5-
A library for connecting to and querying PostgreSQL databases.
5+
A library for connecting to and querying PostgreSQL databases (see [Postgres Protocol](https://www.postgresql.org/docs/13/protocol-overview.html)).
66

77
This driver uses the more efficient and secure extended query format of the PostgreSQL protocol.
88

lib/src/auth/auth.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'package:crypto/crypto.dart';
2+
import 'package:sasl_scram/sasl_scram.dart';
3+
4+
import '../../postgres.dart';
5+
import '../server_messages.dart';
6+
import 'md5_authenticator.dart';
7+
import 'sasl_authenticator.dart';
8+
9+
enum AuthenticationScheme { MD5, SCRAM_SHA_256 }
10+
11+
abstract class PostgresAuthenticator {
12+
static String? name;
13+
late final PostgreSQLConnection connection;
14+
15+
PostgresAuthenticator(this.connection);
16+
17+
void onMessage(AuthenticationMessage message);
18+
}
19+
20+
PostgresAuthenticator createAuthenticator(PostgreSQLConnection connection, AuthenticationScheme authenticationScheme) {
21+
switch (authenticationScheme) {
22+
case AuthenticationScheme.MD5:
23+
return MD5Authenticator(connection);
24+
case AuthenticationScheme.SCRAM_SHA_256:
25+
final credentials = UsernamePasswordCredential(username: connection.username, password: connection.password);
26+
return PostgresSaslAuthenticator(connection, ScramAuthenticator('SCRAM-SHA-256', sha256, credentials));
27+
default:
28+
throw PostgreSQLException("Authenticator wasn't specified");
29+
}
30+
}

lib/src/auth/md5_authenticator.dart

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:buffer/buffer.dart';
2+
import 'package:crypto/crypto.dart';
3+
4+
import '../../postgres.dart';
5+
import '../client_messages.dart';
6+
import '../server_messages.dart';
7+
import '../utf8_backed_string.dart';
8+
import 'auth.dart';
9+
10+
class MD5Authenticator extends PostgresAuthenticator {
11+
static final String name = 'MD5';
12+
13+
MD5Authenticator(PostgreSQLConnection connection) : super(connection);
14+
15+
@override
16+
void onMessage(AuthenticationMessage message) {
17+
final reader = ByteDataReader()..add(message.bytes);
18+
final salt = reader.read(4, copy: true);
19+
20+
final authMessage = AuthMD5Message(connection.username!, connection.password!, salt);
21+
22+
connection.socket!.add(authMessage.asBytes());
23+
}
24+
}
25+
26+
class AuthMD5Message extends ClientMessage {
27+
UTF8BackedString? _hashedAuthString;
28+
29+
AuthMD5Message(String username, String password, List<int> saltBytes) {
30+
final passwordHash = md5.convert('$password$username'.codeUnits).toString();
31+
final saltString = String.fromCharCodes(saltBytes);
32+
final md5Hash = md5.convert('$passwordHash$saltString'.codeUnits).toString();
33+
_hashedAuthString = UTF8BackedString('md5$md5Hash');
34+
}
35+
36+
@override
37+
void applyToBuffer(ByteDataWriter buffer) {
38+
buffer.writeUint8(ClientMessage.PasswordIdentifier);
39+
final length = 5 + _hashedAuthString!.utf8Length;
40+
buffer.writeUint32(length);
41+
_hashedAuthString!.applyToBuffer(buffer);
42+
}
43+
}

lib/src/auth/sasl_authenticator.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import 'dart:typed_data';
2+
3+
import 'package:buffer/buffer.dart';
4+
import 'package:sasl_scram/sasl_scram.dart';
5+
6+
import '../../postgres.dart';
7+
import '../client_messages.dart';
8+
import '../server_messages.dart';
9+
import '../utf8_backed_string.dart';
10+
import 'auth.dart';
11+
12+
/// Structure for SASL Authenticator
13+
class PostgresSaslAuthenticator extends PostgresAuthenticator {
14+
final SaslAuthenticator authenticator;
15+
16+
PostgresSaslAuthenticator(PostgreSQLConnection connection, this.authenticator) : super(connection);
17+
18+
@override
19+
void onMessage(AuthenticationMessage message) {
20+
ClientMessage msg;
21+
switch (message.type) {
22+
case AuthenticationMessage.KindSASL:
23+
final bytesToSend = authenticator.handleMessage(SaslMessageType.AuthenticationSASL, message.bytes);
24+
if (bytesToSend == null) throw PostgreSQLException('KindSASL: No bytes to send');
25+
msg = SaslClientFirstMessage(bytesToSend, authenticator.mechanism.name);
26+
break;
27+
case AuthenticationMessage.KindSASLContinue:
28+
final bytesToSend = authenticator.handleMessage(SaslMessageType.AuthenticationSASLContinue, message.bytes);
29+
if (bytesToSend == null) throw PostgreSQLException('KindSASLContinue: No bytes to send');
30+
msg = SaslClientLastMessage(bytesToSend);
31+
break;
32+
case AuthenticationMessage.KindSASLFinal:
33+
authenticator.handleMessage(SaslMessageType.AuthenticationSASLFinal, message.bytes);
34+
return;
35+
default:
36+
throw PostgreSQLException('Unsupported authentication type ${message.type}, closing connection.');
37+
}
38+
connection.socket!.add(msg.asBytes());
39+
}
40+
}
41+
42+
class SaslClientFirstMessage extends ClientMessage {
43+
Uint8List bytesToSendToServer;
44+
String mechanismName;
45+
46+
SaslClientFirstMessage(this.bytesToSendToServer, this.mechanismName);
47+
48+
@override
49+
void applyToBuffer(ByteDataWriter buffer) {
50+
buffer.writeUint8(ClientMessage.PasswordIdentifier);
51+
52+
final utf8CachedMechanismName = UTF8BackedString(mechanismName);
53+
54+
final msgLength = bytesToSendToServer.length;
55+
// No Identifier bit + 4 byte counts (for whole length) + mechanism bytes + zero byte + 4 byte counts (for msg length) + msg bytes
56+
final length = 4 + utf8CachedMechanismName.utf8Length + 1 + 4 + msgLength;
57+
58+
buffer.writeUint32(length);
59+
utf8CachedMechanismName.applyToBuffer(buffer);
60+
61+
// do not add the msg byte count for whatever reason
62+
buffer.writeUint32(msgLength);
63+
buffer.write(bytesToSendToServer);
64+
}
65+
}
66+
67+
class SaslClientLastMessage extends ClientMessage {
68+
Uint8List bytesToSendToServer;
69+
70+
SaslClientLastMessage(this.bytesToSendToServer);
71+
72+
@override
73+
void applyToBuffer(ByteDataWriter buffer) {
74+
buffer.writeUint8(ClientMessage.PasswordIdentifier);
75+
76+
// No Identifier bit + 4 byte counts (for msg length) + msg bytes
77+
final length = 4 + bytesToSendToServer.length;
78+
79+
buffer.writeUint32(length);
80+
buffer.write(bytesToSendToServer);
81+
}
82+
}

lib/src/client_messages.dart

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import 'dart:typed_data';
22

33
import 'package:buffer/buffer.dart';
4-
import 'package:crypto/crypto.dart';
54

65
import 'constants.dart';
76
import 'query.dart';
@@ -13,13 +12,13 @@ abstract class ClientMessage {
1312

1413
static const int ProtocolVersion = 196608;
1514

16-
static const int BindIdentifier = 66;
17-
static const int DescribeIdentifier = 68;
18-
static const int ExecuteIdentifier = 69;
19-
static const int ParseIdentifier = 80;
20-
static const int QueryIdentifier = 81;
21-
static const int SyncIdentifier = 83;
22-
static const int PasswordIdentifier = 112;
15+
static const int BindIdentifier = 66; // B
16+
static const int DescribeIdentifier = 68; // D
17+
static const int ExecuteIdentifier = 69; // E
18+
static const int ParseIdentifier = 80; //P
19+
static const int QueryIdentifier = 81; // Q
20+
static const int SyncIdentifier = 83; // S
21+
static const int PasswordIdentifier = 112; //p
2322

2423
void applyToBuffer(ByteDataWriter buffer);
2524

@@ -36,11 +35,6 @@ abstract class ClientMessage {
3635
}
3736
}
3837

39-
void _applyStringToBuffer(UTF8BackedString string, ByteDataWriter buffer) {
40-
buffer.write(string.utf8Bytes);
41-
buffer.writeInt8(0);
42-
}
43-
4438
class StartupMessage extends ClientMessage {
4539
final UTF8BackedString? _username;
4640
final UTF8BackedString _databaseName;
@@ -66,42 +60,22 @@ class StartupMessage extends ClientMessage {
6660

6761
if (_username != null) {
6862
buffer.write(UTF8ByteConstants.user);
69-
_applyStringToBuffer(_username!, buffer);
63+
_username!.applyToBuffer(buffer);
7064
}
7165

7266
buffer.write(UTF8ByteConstants.database);
73-
_applyStringToBuffer(_databaseName, buffer);
67+
_databaseName.applyToBuffer(buffer);
7468

7569
buffer.write(UTF8ByteConstants.clientEncoding);
7670
buffer.write(UTF8ByteConstants.utf8);
7771

7872
buffer.write(UTF8ByteConstants.timeZone);
79-
_applyStringToBuffer(_timeZone, buffer);
73+
_timeZone.applyToBuffer(buffer);
8074

8175
buffer.writeInt8(0);
8276
}
8377
}
8478

85-
class AuthMD5Message extends ClientMessage {
86-
UTF8BackedString? _hashedAuthString;
87-
88-
AuthMD5Message(String username, String password, List<int> saltBytes) {
89-
final passwordHash = md5.convert('$password$username'.codeUnits).toString();
90-
final saltString = String.fromCharCodes(saltBytes);
91-
final md5Hash =
92-
md5.convert('$passwordHash$saltString'.codeUnits).toString();
93-
_hashedAuthString = UTF8BackedString('md5$md5Hash');
94-
}
95-
96-
@override
97-
void applyToBuffer(ByteDataWriter buffer) {
98-
buffer.writeUint8(ClientMessage.PasswordIdentifier);
99-
final length = 5 + _hashedAuthString!.utf8Length;
100-
buffer.writeUint32(length);
101-
_applyStringToBuffer(_hashedAuthString!, buffer);
102-
}
103-
}
104-
10579
class QueryMessage extends ClientMessage {
10680
final UTF8BackedString _queryString;
10781

@@ -113,7 +87,7 @@ class QueryMessage extends ClientMessage {
11387
buffer.writeUint8(ClientMessage.QueryIdentifier);
11488
final length = 5 + _queryString.utf8Length;
11589
buffer.writeUint32(length);
116-
_applyStringToBuffer(_queryString, buffer);
90+
_queryString.applyToBuffer(buffer);
11791
}
11892
}
11993

@@ -131,8 +105,8 @@ class ParseMessage extends ClientMessage {
131105
final length = 8 + _statement.utf8Length + _statementName.utf8Length;
132106
buffer.writeUint32(length);
133107
// Name of prepared statement
134-
_applyStringToBuffer(_statementName, buffer);
135-
_applyStringToBuffer(_statement, buffer); // Query string
108+
_statementName.applyToBuffer(buffer);
109+
_statement.applyToBuffer(buffer); // Query string
136110
buffer.writeUint16(0);
137111
}
138112
}
@@ -149,7 +123,7 @@ class DescribeMessage extends ClientMessage {
149123
final length = 6 + _statementName.utf8Length;
150124
buffer.writeUint32(length);
151125
buffer.writeUint8(83);
152-
_applyStringToBuffer(_statementName, buffer); // Name of prepared statement
126+
_statementName.applyToBuffer(buffer); // Name of prepared statement
153127
}
154128
}
155129

@@ -193,7 +167,7 @@ class BindMessage extends ClientMessage {
193167
// Name of portal - currently unnamed portal.
194168
buffer.writeUint8(0);
195169
// Name of prepared statement.
196-
_applyStringToBuffer(_statementName, buffer);
170+
_statementName.applyToBuffer(buffer);
197171

198172
// OK, if we have no specified types at all, we can use 0. If we have all specified types, we can use 1. If we have a mix, we have to individually
199173
// call out each type.

lib/src/connection.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:io';
66
import 'dart:typed_data';
77

88
import 'package:buffer/buffer.dart';
9+
import 'auth/auth.dart';
910

1011
import 'client_messages.dart';
1112
import 'execution_context.dart';
@@ -118,7 +119,6 @@ class PostgreSQLConnection extends Object
118119
late int _processID;
119120
// ignore: unused_field
120121
late int _secretKey;
121-
late List<int> _salt;
122122

123123
bool _hasConnectedPreviously = false;
124124
late _PostgreSQLConnectionState _connectionState;
@@ -129,6 +129,8 @@ class PostgreSQLConnection extends Object
129129
@override
130130
PostgreSQLConnection get _connection => this;
131131

132+
Socket? get socket => _socket;
133+
132134
/// Establishes a connection with a PostgreSQL database.
133135
///
134136
/// This method will return a [Future] that completes when the connection is established. Queries can be executed
@@ -246,8 +248,7 @@ class PostgreSQLConnection extends Object
246248
_connectionState = newState;
247249
_connectionState.connection = this;
248250

249-
_connectionState = _connectionState.onEnter();
250-
_connectionState.connection = this;
251+
_transitionToState(_connectionState.onEnter());
251252
}
252253

253254
Future _close([dynamic error, StackTrace? trace]) async {
@@ -292,7 +293,7 @@ class PostgreSQLConnection extends Object
292293
originalSocket.listen((data) {
293294
if (data.length != 1) {
294295
sslCompleter.completeError(PostgreSQLException(
295-
'Could not initalize SSL connection, received unknown byte stream.'));
296+
'Could not initialize SSL connection, received unknown byte stream.'));
296297
return;
297298
}
298299

0 commit comments

Comments
 (0)