Skip to content

Commit 2172483

Browse files
Marat GainullinMarat Gainullin
authored andcommitted
SASL SCRAM implementation started
1 parent b537fae commit 2172483

File tree

10 files changed

+174
-25
lines changed

10 files changed

+174
-25
lines changed

src/main/java/com/github/pgasync/io/backend/AuthenticationDecoder.java

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
package com.github.pgasync.io.backend;
1616

1717
import com.github.pgasync.io.Decoder;
18+
import com.github.pgasync.io.IO;
1819
import com.github.pgasync.message.backend.Authentication;
1920

2021
import java.nio.ByteBuffer;
2122
import java.nio.charset.Charset;
2223

2324
/**
2425
* See <a href="www.postgresql.org/docs/9.3/static/protocol-message-formats.html">Postgres message formats</a>
25-
*
26+
*
2627
* <pre>
2728
* AuthenticationOk (B)
2829
* Byte1('R')
@@ -31,7 +32,7 @@
3132
* Length of message contents in bytes, including self.
3233
* Int32(0)
3334
* Specifies that the authentication was successful.
34-
*
35+
*
3536
* AuthenticationMD5Password (B)
3637
* Byte1('R')
3738
* Identifies the message as an authentication request.
@@ -42,14 +43,22 @@
4243
* Byte4
4344
* The salt to use when encrypting the password.
4445
* </pre>
45-
*
46+
*
4647
* @author Antti Laisi
4748
*/
4849
public class AuthenticationDecoder implements Decoder<Authentication> {
4950

5051
private static final int OK = 0;
52+
private static final int SASL_FINAL = 12;
53+
private static final int SASL_CONTINUE = 11;
54+
private static final int SASL = 10;
55+
private static final int AUTHENTICATION_SSPI = 9;
56+
private static final int AUTHENTICATION_GSS = 7;
57+
private static final int AUTHENTICATION_SCM_CREDENTIAL = 6;
5158
private static final int PASSWORD_MD5_CHALLENGE = 5;
5259
private static final int CLEARTEXT_PASSWORD = 3;
60+
private static final int AUTHENTICATION_KERBEROS_V5 = 2;
61+
public static final String AUTHENTICATION_IS_NOT_SUPPORTED = " authentication is not supported";
5362

5463
@Override
5564
public byte getMessageId() {
@@ -60,14 +69,40 @@ public byte getMessageId() {
6069
public Authentication read(ByteBuffer buffer, Charset encoding) {
6170
int type = buffer.getInt();
6271
switch (type) {
63-
case OK:
64-
return new Authentication(true, null);
6572
case CLEARTEXT_PASSWORD:
66-
return new Authentication(false, null);
73+
return Authentication.CLEAR_TEXT;
6774
case PASSWORD_MD5_CHALLENGE:
6875
byte[] salt = new byte[4];
6976
buffer.get(salt);
70-
return new Authentication(false, salt);
77+
return new Authentication(false, false, salt);
78+
case SASL:
79+
boolean scramSha256Met = false;
80+
String sasl = IO.getCString(buffer, encoding);
81+
while (!sasl.isEmpty()) {
82+
if ("SCRAM-SHA-256".equals(sasl)) {
83+
scramSha256Met = true;
84+
}
85+
sasl = IO.getCString(buffer, encoding);
86+
}
87+
if (scramSha256Met) {
88+
return Authentication.SCRAM_SHA_256;
89+
} else {
90+
throw new UnsupportedOperationException("The server doesn't support " + Authentication.SUPPORTED_SASL + " SASL mechanism");
91+
}
92+
case AUTHENTICATION_SSPI:
93+
throw new UnsupportedOperationException(AUTHENTICATION_IS_NOT_SUPPORTED);
94+
case AUTHENTICATION_GSS:
95+
throw new UnsupportedOperationException("AuthenticationGss" + AUTHENTICATION_IS_NOT_SUPPORTED);
96+
case AUTHENTICATION_SCM_CREDENTIAL:
97+
throw new UnsupportedOperationException("AuthenticationScmCredential" + AUTHENTICATION_IS_NOT_SUPPORTED);
98+
case AUTHENTICATION_KERBEROS_V5:
99+
throw new UnsupportedOperationException("AuthenticationKerberosV5" + AUTHENTICATION_IS_NOT_SUPPORTED);
100+
case OK:
101+
return Authentication.OK;
102+
case SASL_CONTINUE:
103+
return Authentication.OK;
104+
case SASL_FINAL:
105+
return Authentication.OK;
71106
default:
72107
throw new UnsupportedOperationException("Unsupported authentication type: " + type);
73108
}

src/main/java/com/github/pgasync/io/frontend/FIndicatorsEncoder.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
package com.github.pgasync.io.frontend;
1616

1717
import com.github.pgasync.io.Encoder;
18+
import com.github.pgasync.io.IO;
19+
import com.github.pgasync.message.backend.Authentication;
1820
import com.github.pgasync.message.frontend.FIndicators;
1921

2022
import java.nio.ByteBuffer;
@@ -46,6 +48,9 @@ public void write(FIndicators msg, ByteBuffer buffer, Charset encoding) {
4648
case SYNC:
4749
sync(buffer);
4850
break;
51+
case SASL_INITIAL:
52+
saslInitial(buffer, encoding);
53+
break;
4954
default:
5055
throw new IllegalStateException(msg.name());
5156
}
@@ -56,4 +61,12 @@ void sync(ByteBuffer buffer) {
5661
buffer.putInt(4);
5762
}
5863

64+
void saslInitial(ByteBuffer buffer, Charset encoding) {
65+
buffer.put((byte) 'p');
66+
buffer.putInt(0);
67+
IO.putCString(buffer, Authentication.SUPPORTED_SASL, encoding);
68+
buffer.putInt(-1);
69+
buffer.putInt(1, buffer.position() - 1);
70+
}
71+
5972
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.github.pgasync.io.frontend;
2+
3+
import com.github.pgasync.io.IO;
4+
import com.github.pgasync.message.frontend.SASLInitialResponse;
5+
6+
import java.nio.ByteBuffer;
7+
import java.nio.charset.Charset;
8+
import java.nio.charset.StandardCharsets;
9+
import java.security.SecureRandom;
10+
11+
public class SASLInitialResponseEncoder extends SkipableEncoder<SASLInitialResponse> {
12+
13+
private static String saslPrep(String value){
14+
// TODO: Implement me
15+
return value;
16+
}
17+
18+
@Override
19+
protected byte getMessageId() {
20+
return 'p';
21+
}
22+
23+
@Override
24+
protected void writeBody(SASLInitialResponse msg, ByteBuffer buffer, Charset encoding) {
25+
IO.putCString(buffer, msg.getSaslMechanism(), encoding);
26+
StringBuilder clientFirstMessage = new StringBuilder();
27+
if (msg.getChannelBindingType() != null && !msg.getChannelBindingType().isBlank()) {
28+
clientFirstMessage.append("p=").append(msg.getChannelBindingType()).append(",");
29+
} else {
30+
clientFirstMessage.append("n,");
31+
}
32+
clientFirstMessage.append(",");// It could be 'clientFirstMessage.append("a=").append(msg.getUsername()).append(",");' but it is not needed unless we are using impersonate techniques.
33+
clientFirstMessage.append("n=,"); // Postgres expects the username here to be an empty string because of the startup message.
34+
clientFirstMessage.append("r=").append(msg.getNonce());
35+
byte[] clientFirstMessageContent = clientFirstMessage.toString().getBytes(encoding); // RFC dictates us to use UTF-8 here
36+
buffer.putInt(clientFirstMessageContent.length);
37+
buffer.put(clientFirstMessageContent);
38+
}
39+
40+
@Override
41+
public Class<SASLInitialResponse> getMessageType() {
42+
return SASLInitialResponse.class;
43+
}
44+
}

src/main/java/com/github/pgasync/message/backend/Authentication.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,18 @@
2323
*/
2424
public class Authentication implements Message {
2525

26+
public static final Authentication OK = new Authentication(true, false, null);
27+
public static final Authentication CLEAR_TEXT = new Authentication(false, false, null);
28+
public static final Authentication SCRAM_SHA_256 = new Authentication(false, true, null);
29+
public static final String SUPPORTED_SASL = "SCRAM-SHA-256";
30+
2631
private final boolean success;
32+
private final boolean scramSha256;
2733
private final byte[] md5salt;
2834

29-
public Authentication(boolean success, byte[] md5salt) {
35+
public Authentication(boolean success, boolean scramSha256, byte[] md5salt) {
3036
this.success = success;
37+
this.scramSha256 = scramSha256;
3138
this.md5salt = md5salt;
3239
}
3340

@@ -39,8 +46,12 @@ public boolean isAuthenticationOk() {
3946
return success;
4047
}
4148

49+
public boolean isScramSha256() {
50+
return scramSha256;
51+
}
52+
4253
@Override
4354
public String toString() {
44-
return String.format("Authentication(success=%s, md5salt=%s)", success, md5salt != null ? printHexBinary(md5salt) : null);
55+
return String.format("Authentication(success=%s, md5salt=%s, scramSha256=%s)", success, md5salt != null ? printHexBinary(md5salt) : null, scramSha256);
4556
}
4657
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.github.pgasync.message.backend;
2+
3+
import com.github.pgasync.message.Message;
4+
5+
public class AuthenticationSaslContinue implements Message {
6+
7+
private final byte[] saslData;
8+
9+
public AuthenticationSaslContinue(byte[] saslData) {
10+
this.saslData = saslData;
11+
}
12+
13+
public byte[] getSaslData() {
14+
return saslData;
15+
}
16+
}

src/main/java/com/github/pgasync/message/backend/SSLHandshake.java renamed to src/main/java/com/github/pgasync/message/backend/SslHandshake.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
/**
66
* @author Marat Gainullin
77
*/
8-
public enum SSLHandshake implements Message {
8+
public enum SslHandshake implements Message {
99
INSTANCE
1010
}

src/main/java/com/github/pgasync/message/frontend/FIndicators.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
* @author Marat Gainullin
2121
*/
2222
public enum FIndicators implements Message {
23-
SYNC
23+
SYNC,
24+
SASL_INITIAL
2425
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.github.pgasync.message.frontend;
2+
3+
import com.github.pgasync.message.Message;
4+
5+
/**
6+
* it is a bit weired to have a username both here and in a {@link StartupMessage}.
7+
* But according to the SCRAM specification in RFC5802 we have to have it :(
8+
*/
9+
public class SASLInitialResponse implements Message {
10+
11+
private final String saslMechanism;
12+
private final String channelBindingType;
13+
private final String username;
14+
private final String nonce;
15+
16+
public SASLInitialResponse(String saslMechanism, String channelBindingType, String username, String nonce) {
17+
this.saslMechanism = saslMechanism;
18+
this.channelBindingType = channelBindingType;
19+
this.username = username;
20+
this.nonce = nonce;
21+
}
22+
23+
public String getUsername() {
24+
return username;
25+
}
26+
27+
public String getChannelBindingType() {
28+
return channelBindingType;
29+
}
30+
31+
public String getSaslMechanism() {
32+
return saslMechanism;
33+
}
34+
35+
public String getNonce() {
36+
return nonce;
37+
}
38+
}

src/test/java/com/github/pgasync/netty/NettyMessageEncoder.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,7 @@
1515
package com.github.pgasync.netty;
1616

1717
import com.github.pgasync.io.Encoder;
18-
import com.github.pgasync.io.frontend.BindEncoder;
19-
import com.github.pgasync.io.frontend.CloseEncoder;
20-
import com.github.pgasync.io.frontend.DescribeEncoder;
21-
import com.github.pgasync.io.frontend.ExecuteEncoder;
22-
import com.github.pgasync.io.frontend.FIndicatorsEncoder;
23-
import com.github.pgasync.io.frontend.ParseEncoder;
24-
import com.github.pgasync.io.frontend.PasswordMessageEncoder;
25-
import com.github.pgasync.io.frontend.QueryEncoder;
26-
import com.github.pgasync.io.frontend.SSLRequestEncoder;
27-
import com.github.pgasync.io.frontend.StartupMessageEncoder;
28-
import com.github.pgasync.io.frontend.TerminateEncoder;
18+
import com.github.pgasync.io.frontend.*;
2919
import com.github.pgasync.message.Message;
3020
import io.netty.buffer.ByteBuf;
3121
import io.netty.channel.ChannelHandlerContext;
@@ -54,6 +44,7 @@ public class NettyMessageEncoder extends MessageToByteEncoder<Message> {
5444
new BindEncoder(),
5545
new DescribeEncoder(),
5646
new ExecuteEncoder(),
47+
new SASLInitialResponseEncoder(),
5748
new CloseEncoder(),
5849
new FIndicatorsEncoder(),
5950
new TerminateEncoder()

src/test/java/com/github/pgasync/netty/NettyPgProtocolStream.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import com.github.pgasync.PgProtocolStream;
1818
import com.github.pgasync.message.Message;
19-
import com.github.pgasync.message.backend.SSLHandshake;
19+
import com.github.pgasync.message.backend.SslHandshake;
2020
import com.github.pgasync.message.frontend.SSLRequest;
2121
import com.github.pgasync.message.frontend.StartupMessage;
2222
import com.github.pgasync.message.frontend.Terminate;
@@ -76,7 +76,7 @@ public CompletableFuture<Message> connect(StartupMessage startup) {
7676
.thenApply(this::send)
7777
.thenCompose(Function.identity())
7878
.thenApply(message -> {
79-
if (message == SSLHandshake.INSTANCE) {
79+
if (message == SslHandshake.INSTANCE) {
8080
return send(startup);
8181
} else {
8282
return CompletableFuture.completedFuture(message);
@@ -171,7 +171,7 @@ public void channelActive(ChannelHandlerContext context) {
171171
@Override
172172
public void userEventTriggered(ChannelHandlerContext context, Object evt) {
173173
if (evt instanceof SslHandshakeCompletionEvent && ((SslHandshakeCompletionEvent) evt).isSuccess()) {
174-
gotMessage(SSLHandshake.INSTANCE);
174+
gotMessage(SslHandshake.INSTANCE);
175175
}
176176
}
177177

0 commit comments

Comments
 (0)