8000 Support `caching_sha2_password` authentication mode · jasync-sql/jasync-sql@b68c826 · GitHub
[go: up one dir, main page]

Skip to content

Commit b68c826

Browse files
Support caching_sha2_password authentication mode
1 parent 6acef36 commit b68c826

File tree

18 files changed

+212
-82
lines changed

18 files changed

+212
-82
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ postgresql-async/out/*
2020
mysql-async/target/*
2121
pool-async/target/*
2222
postgis-jasync/target/*
23+
r2dbc-mysql/target/*
2324
.rvmrc
2425
.ruby-version
2526
.ruby-gemset

mysql-async/src/main/java/com/github/jasync/sql/db/mysql/codec/MySQLConnectionHandler.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.github.jasync.sql.db.Configuration
44
import com.github.jasync.sql.db.exceptions.DatabaseException
55
import com.github.jasync.sql.db.general.MutableResultSet
66
import com.github.jasync.sql.db.mysql.binary.BinaryRowDecoder
7+
import com.github.jasync.sql.db.mysql.encoder.auth.AuthenticationMethod
78
import com.github.jasync.sql.db.mysql.message.client.AuthenticationSwitchResponse
89
import com.github.jasync.sql.db.mysql.message.client.CapabilityRequestMessage
910
import com.github.jasync.sql.db.mysql.message.client.CloseStatementMessage
@@ -13,6 +14,7 @@ import com.github.jasync.sql.db.mysql.message.client.PreparedStatementPrepareMes
1314
import com.github.jasync.sql.db.mysql.message.client.QueryMessage
1415
import com.github.jasync.sql.db.mysql.message.client.QuitMessage
1516
import com.github.jasync.sql.db.mysql.message.client.SendLongDataMessage
17+
import com.github.jasync.sql.db.mysql.message.server.AuthMoreDataMessage
1618
import com.github.jasync.sql.db.mysql.message.server.AuthenticationSwitchRequest
1719
import com.github.jasync.sql.db.mysql.message.server.BinaryRowMessage
1820
import com.github.jasync.sql.db.mysql.message.server.ColumnDefini A93C tionMessage
@@ -23,6 +25,7 @@ import com.github.jasync.sql.db.mysql.message.server.OkMessage
2325
import com.github.jasync.sql.db.mysql.message.server.PreparedStatementPrepareResponse
2426
import com.github.jasync.sql.db.mysql.message.server.ResultSetRowMessage
2527
import com.github.jasync.sql.db.mysql.message.server.ServerMessage
28+
import com.github.jasync.sql.db.mysql.util.CapabilityFlag
2629
import com.github.jasync.sql.db.mysql.util.CharsetMapper
2730
import com.github.jasync.sql.db.util.ExecutorServiceUtils
2831
import com.github.jasync.sql.db.util.FP
@@ -72,6 +75,7 @@ class MySQLConnectionHandler(
7275
private val parsedStatements = HashMap<String, PreparedStatementHolder>()
7376
private val binaryRowDecoder = BinaryRowDecoder()
7477

78+
private var sslEstablished: Boolean = false
7579
private var currentPreparedStatementHolder: PreparedStatementHolder? = null
7680
private var currentPreparedStatement: PreparedStatement? = null
7781
private var currentQuery: MutableResultSet<ColumnDefinitionMessage>? = null
@@ -127,6 +131,20 @@ class MySQLConnectionHandler(
127131
ServerMessage.EOF -> {
128132
this.handleEOF(message)
129133
}
134+
ServerMessage.AuthMoreData -> {
135+
val m = message as AuthMoreDataMessage
136+
137+
if (!m.isSuccess()) {
138+
if (!sslEstablished) {
139+
throw IllegalStateException(
140+
"Full authentication mode for ${AuthenticationMethod.CachingSha2} requires SSL"
141+
)
142+
}
143+
144+
val request = AuthenticationSwitchRequest(AuthenticationMethod.CachingSha2, null)
145+
handlerDelegate.switchAuthentication(request)
146+
}
147+
}
130148
ServerMessage.ColumnDefinition -> {
131149
val m = message as ColumnDefinitionMessage
132150

@@ -278,6 +296,7 @@ class MySQLConnectionHandler(
278296
fun write(message: CapabilityRequestMessage): ChannelFuture = writeAndHandleError(message)
279297

280298
fun write(message: HandshakeResponseMessage): ChannelFuture {
299+
sslEstablished = message.header.flags.contains(CapabilityFlag.CLIENT_SSL)
281300
decoder.hasDoneHandshake = true
282301
return writeAndHandleError(message)
283302
}

mysql-async/src/main/java/com/github/jasync/sql/db/mysql/codec/MySQLFrameDecoder.kt

Lines changed: 9 additions & 0 deletions
< 10000 tr class="diff-line-row">
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.github.jasync.sql.db.mysql.codec
33
import com.github.jasync.sql.db.exceptions.BufferNotFullyConsumedException
44
import com.github.jasync.sql.db.exceptions.NegativeMessageSizeException
55
import com.github.jasync.sql.db.exceptions.ParserNotAvailableException
6+
import com.github.jasync.sql.db.mysql.decoder.AuthMoreDataDecoder
67
import com.github.jasync.sql.db.mysql.decoder.AuthenticationSwitchRequestDecoder
78
import com.github.jasync.sql.db.mysql.decoder.ColumnDefinitionDecoder
89
import com.github.jasync.sql.db.mysql.decoder.ColumnProcessingFinishedDecoder
@@ -39,6 +40,7 @@ class MySQLFrameDecoder(val charset: Charset, private val connectionId: String)
3940
private val handshakeDecoder = HandshakeV10Decoder()
4041
private val errorDecoder = ErrorDecoder(charset)
4142
private val okDecoder = OkDecoder(charset)
43+
private val authMoreDataDecoder = AuthMoreDataDecoder()
4244
private val columnDecoder = ColumnDefinitionDecoder(charset, DecoderRegistry(charset))
4345
private val authenticationSwitchRequestDecoder = AuthenticationSwitchRequestDecoder(charset)
4446
private val rowDecoder = ResultSetRowDecoder()
@@ -161,6 +163,13 @@ class MySQLFrameDecoder(val charset: Charset, private val connectionId: String)
161163
}
162164
}
163165
}
166+
ServerMessage.AuthMoreData -> {
167+
if (!isInQuery) {
168+
this.authMoreDataDecoder
169+
} else {
170+
null
171+
}
172+
}
164173
else -> {
165174
if (this.isInQuery) {
166175
null
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.github.jasync.sql.db.mysql.decoder
2+
3+
import com.github.jasync.sql.db.mysql.message.server.AuthMoreDataMessage
4+
import com.github.jasync.sql.db.mysql.message.server.ServerMessage
5+
import io.netty.buffer.ByteBuf
6+
7+
class AuthMoreDataDecoder : MessageDecoder {
8+
override fun decode(buffer: ByteBuf): ServerMessage {
9+
return AuthMoreDataMessage(
10+
data = buffer.readByte(),
11+
)
12+
}
13+
}

mysql-async/src/main/java/com/github/jasync/sql/db/mysql/decoder/AuthenticationSwitchRequestDecoder.kt

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,14 @@ package com.github.jasync.sql.db.mysql.decoder
33
import com.github.jasync.sql.db.mysql.message.server.AuthenticationSwitchRequest
44
import com.github.jasync.sql.db.mysql.message.server.ServerMessage
55
import com.github.jasync.sql.db.util.readCString
6+
import com.github.jasync.sql.db.util.readUntilEOF
67
import io.netty.buffer.ByteBuf
7-
import io.netty.buffer.ByteBufUtil
88
import java.nio.charset.Charset
99

1010
class AuthenticationSwitchRequestDecoder(val charset: Charset) : MessageDecoder {
1111
override fun decode(buffer: ByteBuf): ServerMessage {
1212
val method = buffer.readCString(charset)
13-
val bytes: Int = buffer.readableBytes()
14-
val terminal = 0.toByte()
15-
val salt = if (bytes > 0 && buffer.getByte(buffer.writerIndex() - 1) == terminal) ByteBufUtil.getBytes(
16-
buffer,
17-
buffer.readerIndex(),
18-
bytes - 1
19-
) else ByteBufUtil.getBytes(buffer)
20-
return AuthenticationSwitchRequest(method, salt)
13+
val seed = buffer.readUntilEOF(charset).toByteArray(charset)
14+
return AuthenticationSwitchRequest(method, seed)
2115
}
2216
}

mysql-async/src/main/java/com/github/jasync/sql/db/mysql/encoder/auth/AuthenticationMethod.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import java.nio.charset.Charset
44

55
interface AuthenticationMethod {
66

7-
fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray): ByteArray
7+
fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray?): ByteArray
88

99
companion object {
10-
val Native = "mysql_native_password"
11-
val Old = "mysql_old_password"
10+
const val CachingSha2 = "caching_sha2_password"
11+
const val Native = "mysql_native_password"
12+
const val Old = "mysql_old_password"
13+
const val Sha256 = "sha256_password"
1214

1315
val Availables = mapOf(
16+
CachingSha2 to CachingSha2PasswordAuthentication,
1417
Native to MySQLNativePasswordAuthentication,
15-
Old to OldPasswordAuthentication
18+
Old to OldPasswordAuthentication,
19+
Sha256 to Sha256PasswordAuthentication,
1620
)
1721
}
1822
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.github.jasync.sql.db.mysql.encoder.auth
2+
3+
import com.github.jasync.sql.db.util.length
4+
import java.nio.charset.Charset
5+
import java.security.MessageDigest
6+
import kotlin.experimental.xor
7+
8+
object AuthenticationScrambler {
9+
10+
fun scramble411(
11+
algorithm: String,
12+
password: String,
13+
charset: Charset,
14+
seed: ByteArray,
15+
seedFirst: Boolean,
16+
): ByteArray {
17+
val messageDigest = MessageDigest.getInstance(algorithm)
18+
val initialDigest = messageDigest.digest(password.toByteArray(charset))
19+
20+
messageDigest.reset()
21+
22+
val finalDigest = messageDigest.digest(initialDigest)
23+
24+
messageDigest.reset()
25+
26+
if (seedFirst) {
27+
messageDigest.update(seed)
28+
messageDigest.update(finalDigest)
29+
} else {
30+
messageDigest.update(finalDigest)
31+
messageDigest.update(seed)
32+
}
33+
34+
val result = messageDigest.digest()
35+
var counter = 0
36+
37+
while (counter < result.length) {
38+
result[counter] = (result[counter] xor initialDigest[counter])
39+
counter += 1
40+
}
41+
42+
return result
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.jasync.sql.db.mysql.encoder.auth
2+
3+
import java.nio.charset.Charset
4+
5+
object CachingSha2PasswordAuthentication : AuthenticationMethod {
6+
7+
private val EmptyArray = ByteArray(0)
8+
9+
override fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray?): ByteArray {
10+
return if (password != null) {
11+
if (seed != null) {
12+
// Fast authentication mode. Requires seed, but not SSL.
13+
AuthenticationScrambler.scramble411("SHA-256", password, charset, seed, false)
14+
} else {
15+
// Full authentication mode.
16+
// Since this sends the plaintext password, SSL is required.
17+
// Without SSL, the server always rejects the password.
18+
Sha256PasswordAuthentication.generateAuthentication(charset, password, null)
19+
}
20+
} else {
21+
EmptyArray
22+
}
23+
}
24+
}
Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,18 @@
11
package com.github.jasync.sql.db.mysql.encoder.auth
22

3-
import com.github.jasync.sql.db.util.length
43
import java.nio.charset.Charset
5-
import java.security.MessageDigest
6-
import kotlin.experimental.xor
74

85
object MySQLNativePasswordAuthentication : AuthenticationMethod {
96

10-
val EmptyArray = ByteArray(0)
7+
private val EmptyArray = ByteArray(0)
118

12-
override fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray): ByteArray {
9+
override fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray?): ByteArray {
10+
requireNotNull(seed) { "Seed should not be null" }
1311

1412
return if (password != null) {
15-
scramble411(charset, password, seed)
13+
AuthenticationScrambler.scramble411("SHA-1", password, charset, seed, true)
1614
} else {
1715
EmptyArray
1816
}
1917
}
20-
21-
private fun scramble411(charset: Charset, password: String, seed: ByteArray): ByteArray {
22-
23-
val messageDigest = MessageDigest.getInstance("SHA-1")
24-
val initialDigest = messageDigest.digest(password.toByteArray(charset))
25-
26-
messageDigest.reset()
27-
28-
val finalDigest = messageDigest.digest(initialDigest)
29-
30-
messageDigest.reset()
31-
32-
messageDigest.update(seed)
33-
messageDigest.update(finalDigest)
34-
35-
val result = messageDigest.digest()
36-
var counter = 0
37-
38-
while (counter < result.length) {
39-
result[counter] = (result[counter] xor initialDigest[counter])
40-
counter += 1
41-
}
42-
43-
return result
44-
}
4518
}

mysql-async/src/main/java/com/github/jasync/sql/db/mysql/encoder/auth/OldPasswordAuthentication.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import kotlin.math.floor
77
@Suppress("RedundantExplicitType", "UNUSED_VALUE", "VARIABLE_WITH_REDUNDANT_INITIALIZER")
88
object OldPasswordAuthentication : AuthenticationMethod {
99

10-
val EmptyArray = ByteArray(0)
10+
private val EmptyArray = ByteArray(0)
11+
12+
override fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray?): ByteArray {
13+
requireNotNull(seed) { "Seed should not be null" }
1114

12-
override fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray): ByteArray {
1315
return when {
14-
password != null && password.isNotEmpty() -> {
16+
!password.isNullOrEmpty() -> {
1517
newCrypt(charset, password, String(seed, charset))
1618
}
1719
else -> EmptyArray
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.github.jasync.sql.db.mysql.encoder.auth
2+
3+
import com.github.jasync.sql.db.util.length
4+
import java.nio.charset.Charset
5+
6+
object Sha256PasswordAuthentication : AuthenticationMethod {
7+
8+
private val EmptyArray = ByteArray(0)
9+
10+
override fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray?): ByteArray {
11+
return if (password != null) {
12+
val bytes = password.toByteArray(charset)
13+
val result = ByteArray(bytes.length + 1)
14+
System.arraycopy(bytes, 0, result, 0, bytes.length)
15+
result
16+
} else {
17+
EmptyArray
18+
}
19+
}
20+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.github.jasync.sql.db.mysql.message.server
2+
3+
data class AuthMoreDataMessage(
4+
val data: Byte,
5+
) : ServerMessage(AuthMoreData) {
6+
fun isSuccess(): Boolean {
7+
return data == 3.toByte()
8+
}
9+
}

mysql-async/src/main/java/com/github/jasync/sql/db/mysql/message/server/AuthenticationSwitchRequest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ package com.github.jasync.sql.db.mysql.message.server
22

33
data class AuthenticationSwitchRequest(
44
val method: String,
5-
val seed: ByteArray
5+
val seed: ByteArray?,
66
) : ServerMessage(ServerMessage.EOF)

mysql-async/src/main/java/com/github/jasync/sql/db/mysql/message/server/ServerMessage.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ abstract class ServerMessage(override val kind: Int) : KindedMessage {
88
const val ServerProtocolVersion = 10
99
const val Error = -1
1010
const val Ok = 0
11+
const val AuthMoreData = 1
1112
const val EOF = -2
1213

1314
// these messages don't actually exist

0 commit comments

Comments
 (0)
0