Skip to content

Commit 0b9ea46

Browse files
Implement password encryption using an RSA public key (#373)
* Implement password encryption using an RSA public key * Add README
1 parent 23fdcaa commit 0b9ea46

27 files changed

+390
-69
lines changed

db-async-common/src/main/java/com/github/jasync/sql/db/Configuration.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import io.netty.channel.nio.NioEventLoopGroup
1010
import io.netty.util.CharsetUtil
1111
import mu.KotlinLogging
1212
import java.nio.charset.Charset
13+
import java.nio.file.Path
1314
import java.time.Duration
1415
import java.util.concurrent.CompletionStage
1516
import java.util.concurrent.Executor
@@ -43,6 +44,7 @@ private val logger = KotlinLogging.logger {}
4344
* @param currentSchema optional database schema - postgresql only.
4445
* @param socketPath path to unix domain socket file (on the local machine)
4546
* @param credentialsProvider a credential provider used to inject credentials on demand
47+
* @param rsaPublicKey path to the RSA public key, used for password encryption over unsafe connections
4648
*
4749
*/
4850
class Configuration @JvmOverloads constructor(
@@ -63,7 +65,8 @@ class Configuration @JvmOverloads constructor(
6365
val executionContext: Executor = ExecutorServiceUtils.CommonPool,
6466
val currentSchema: String? = null,
6567
val socketPath: String? = null,
66-
val credentialsProvider: CredentialsProvider? = null
68+
val credentialsProvider: CredentialsProvider? = null,
69+
val rsaPublicKey: Path? = null,
6770
) {
6871
init {
6972
if (socketPath != null && eventLoopGroup is NioEventLoopGroup) {
@@ -96,6 +99,7 @@ class Configuration @JvmOverloads constructor(
9699
currentSchema: String? = null,
97100
socketPath: String? = null,
98101
credentialsProvider: CredentialsProvider? = null,
102+
rsaPublicKey: Path? = null,
99103
): Configuration {
100104
return Configuration(
101105
username = username ?: this.username,
@@ -116,6 +120,7 @@ class Configuration @JvmOverloads constructor(
116120
currentSchema = currentSchema ?: this.currentSchema,
117121
socketPath = socketPath ?: this.socketPath,
118122
credentialsProvider = credentialsProvider ?: this.credentialsProvider,
123+
rsaPublicKey = rsaPublicKey ?: this.rsaPublicKey,
119124
)
120125
}
121126

@@ -143,6 +148,7 @@ class Configuration @JvmOverloads constructor(
143148
if (currentSchema != other.currentSchema) return false
144149
if (socketPath != other.socketPath) return false
145150
if (credentialsProvider != other.credentialsProvider) return false
151+
if (rsaPublicKey != other.rsaPublicKey) return false
146152

147153
return true
148154
}
@@ -166,11 +172,12 @@ class Configuration @JvmOverloads constructor(
166172
result = 31 * result + (currentSchema?.hashCode() ?: 0)
167173
result = 31 * result + (socketPath?.hashCode() ?: 0)
168174
result = 31 * result + (credentialsProvider?.hashCode() ?: 0)
175+
result = 31 * result + (rsaPublicKey?.hashCode() ?: 0)
169176
return result
170177
}
171178

172179
override fun toString(): String {
173-
return "Configuration(username='$username', host='$host', port=$port, password=****, database=$database, ssl=$ssl, charset=$charset, maximumMessageSize=$maximumMessageSize, allocator=$allocator, connectionTimeout=$connectionTimeout, queryTimeout=$queryTimeout, applicationName=$applicationName, interceptors=$interceptors, eventLoopGroup=$eventLoopGroup, executionContext=$executionContext, currentSchema=$currentSchema, socketPath=$socketPath, credentialsProvider=$credentialsProvider)"
180+
return "Configuration(username='$username', host='$host', port=$port, password=****, database=$database, ssl=$ssl, charset=$charset, maximumMessageSize=$maximumMessageSize, allocator=$allocator, connectionTimeout=$connectionTimeout, queryTimeout=$queryTimeout, applicationName=$applicationName, interceptors=$interceptors, eventLoopGroup=$eventLoopGroup, executionContext=$executionContext, currentSchema=$currentSchema, socketPath=$socketPath, credentialsProvider=$credentialsProvider, rsaPublicKey=$rsaPublicKey)"
174181
}
175182
}
176183

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

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import com.github.jasync.sql.db.exceptions.InsufficientParametersException
1313
import com.github.jasync.sql.db.interceptor.PreparedStatementParams
1414
import com.github.jasync.sql.db.mysql.codec.MySQLConnectionHandler
1515
import com.github.jasync.sql.db.mysql.codec.MySQLHandlerDelegate
16+
import com.github.jasync.sql.db.mysql.encoder.auth.AuthenticationMethod
1617
import com.github.jasync.sql.db.mysql.exceptions.MySQLException
1718
import com.github.jasync.sql.db.mysql.message.client.AuthenticationSwitchResponse
1819
import com.github.jasync.sql.db.mysql.message.client.CapabilityRequestMessage
1920
import com.github.jasync.sql.db.mysql.message.client.HandshakeResponseMessage
21+
import com.github.jasync.sql.db.mysql.message.server.AuthMoreDataMessage
2022
import com.github.jasync.sql.db.mysql.message.server.AuthenticationSwitchRequest
2123
import com.github.jasync.sql.db.mysql.message.server.EOFMessage
2224
import com.github.jasync.sql.db.mysql.message.server.ErrorMessage
@@ -93,6 +95,8 @@ class MySQLConnection @JvmOverloads constructor(
9395
private var connected = false
9496
private var lastException: Throwable? = null
9597
private var serverVersion: Version? = null
98+
private var authenticationMethod: String? = null
99+
private var authenticationSeed: ByteArray? = null
96100

97101
object StatusFlags {
98102
// https://dev.mysql.com/doc/internals/en/status-flags.html
@@ -259,6 +263,8 @@ class MySQLConnection @JvmOverloads constructor(
259263
override fun onHandshake(message: HandshakeMessage) {
260264
this.serverVersion = parseVersion(message.serverVersion)
261265
this.serverStatus = message.statusFlags
266+
this.authenticationMethod = message.authenticationMethod
267+
this.authenticationSeed = message.seed
262268

263269
val switchToSsl = when (this.configuration.ssl.mode) {
264270
SSLConfiguration.Mode.Disable -> false
@@ -298,7 +304,9 @@ class MySQLConnection @JvmOverloads constructor(
298304
message.authenticationMethod,
299305
database = configuration.database,
300306
password = configuration.password,
301-
appName = configuration.applicationName
307+
appName = configuration.applicationName,
308+
sslConfiguration = configuration.ssl,
309+
rsaPublicKey = configuration.rsaPublicKey,
302310
)
303311

304312
if (!switchToSsl) {
@@ -336,7 +344,37 @@ class MySQLConnection @JvmOverloads constructor(
336344
}
337345

338346
override fun switchAuthentication(message: AuthenticationSwitchRequest) {
339-
this.connectionHandler.write(AuthenticationSwitchResponse(configuration.password, message))
347+
val response = AuthenticationSwitchResponse(
348+
configuration.password,
349+
configuration.ssl,
350+
configuration.rsaPublicKey,
351+
message
352+
)
353+
354+
this.connectionHandler.write(response)
355+
}
356+
357+
override fun onAuthMoreData(message: AuthMoreDataMessage) {
358+
if (message.isSuccess()) {
359+
// Do nothing. This message will be followed by an `OkMessage`.
360+
return
361+
}
362+
363+
if (authenticationMethod != AuthenticationMethod.CachingSha2) {
364+
throw IllegalStateException(
365+
"AuthMoreDataMessage is only supported for '${AuthenticationMethod.CachingSha2}' method"
366+
)
367+
}
368+
369+
val request = AuthenticationSwitchRequest(AuthenticationMethod.Sha256, authenticationSeed!!)
370+
val response = AuthenticationSwitchResponse(
371+
configuration.password,
372+
configuration.ssl,
373+
configuration.rsaPublicKey,
374+
request
375+
)
376+
377+
this.connectionHandler.write(response)
340378
}
341379

342380
override fun sendQueryDirect(query: String): CompletableFuture<QueryResult> {

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ 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
87
import com.github.jasync.sql.db.mysql.message.client.AuthenticationSwitchResponse
98
import com.github.jasync.sql.db.mysql.message.client.CapabilityRequestMessage
109
import com.github.jasync.sql.db.mysql.message.client.CloseStatementMessage
@@ -132,18 +131,7 @@ class MySQLConnectionHandler(
132131
this.handleEOF(message)
133132
}
134133
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-
}
134+
handlerDelegate.onAuthMoreData(message as AuthMoreDataMessage)
147135
}
148136
ServerMessage.ColumnDefinition -> {
149137
val m = message as ColumnDefinitionMessage

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.github.jasync.sql.db.mysql.codec
22

33
import com.github.jasync.sql.db.ResultSet
4+
import com.github.jasync.sql.db.mysql.message.server.AuthMoreDataMessage
45
import com.github.jasync.sql.db.mysql.message.server.AuthenticationSwitchRequest
56
import com.github.jasync.sql.db.mysql.message.server.EOFMessage
67
import com.github.jasync.sql.db.mysql.message.server.ErrorMessage
@@ -18,5 +19,6 @@ interface MySQLHandlerDelegate {
1819
fun connected(ctx: ChannelHandlerContext)
1920
fun onResultSet(resultSet: ResultSet, message: EOFMessage)
2021
fun switchAuthentication(message: AuthenticationSwitchRequest)
22+
fun onAuthMoreData(message: AuthMoreDataMessage)
2123
fun unregistered()
2224
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,13 @@ class AuthenticationSwitchResponseEncoder(val charset: Charset) : MessageEncoder
2020

2121
val buffer = ByteBufferUtils.packetBuffer()
2222

23-
val bytes =
24-
authenticator.generateAuthentication(charset, switch.password, switch.request.seed)
23+
val bytes = authenticator.generateAuthentication(
24+
charset,
25+
switch.password,
26+
switch.request.seed,
27+
switch.sslConfiguration,
28+
switch.rsaPublicKey,
29+
)
2530
buffer.writeBytes(bytes)
2631

2732
return buffer

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class HandshakeResponseEncoder(private val charset: Charset, private val headerE
3131
val authenticator = this.authenticationMethods.getOrElse(
3232
method
3333
) { throw UnsupportedAuthenticationMethodException(method) }
34-
val bytes = authenticator.generateAuthentication(charset, m.password, m.seed)
34+
val bytes = authenticator.generateAuthentication(charset, m.password, m.seed, m.sslConfiguration, m.rsaPublicKey)
3535
buffer.writeByte(bytes.length)
3636
buffer.writeBytes(bytes)
3737
} else {

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
package com.github.jasync.sql.db.mysql.encoder.auth
22

3+
import com.github.jasync.sql.db.SSLConfiguration
34
import java.nio.charset.Charset
5+
import java.nio.file.Path
46

57
interface AuthenticationMethod {
68

7-
fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray?): ByteArray
9+
fun generateAuthentication(
10+
charset: Charset,
11+
password: String?,
12+
seed: ByteArray,
13+
sslConfiguration: SSLConfiguration,
14+
rsaPublicKey: Path?,
15+
): ByteArray
816

917
companion object {
1018
const val CachingSha2 = "caching_sha2_password"

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
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
54
import java.security.MessageDigest
65
import kotlin.experimental.xor
@@ -32,11 +31,8 @@ object AuthenticationScrambler {
3231
}
3332

3433
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
34+
for ((index, byte) in result.withIndex()) {
35+
result[index] = byte xor initialDigest[index]
4036
}
4137

4238
return result

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
package com.github.jasync.sql.db.mysql.encoder.auth
22

3+
import com.github.jasync.sql.db.SSLConfiguration
34
import java.nio.charset.Charset
5+
import java.nio.file.Path
46

57
object CachingSha2PasswordAuthentication : AuthenticationMethod {
68

79
private val EmptyArray = ByteArray(0)
810

9-
override fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray?): ByteArray {
11+
override fun generateAuthentication(
12+
charset: Charset,
13+
password: String?,
14+
seed: ByteArray,
15+
sslConfiguration: SSLConfiguration,
16+
rsaPublicKey: Path?,
17+
): ByteArray {
1018
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-
}
19+
AuthenticationScrambler.scramble411("SHA-256", password, charset, seed, false)
2020
} else {
2121
EmptyArray
2222
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package com.github.jasync.sql.db.mysql.encoder.auth
22

3+
import com.github.jasync.sql.db.SSLConfiguration
34
import java.nio.charset.Charset
5+
import java.nio.file.Path
46

57
object MySQLNativePasswordAuthentication : AuthenticationMethod {
68

79
private val EmptyArray = ByteArray(0)
810

9-
override fun generateAuthentication(charset: Charset, password: String?, seed: ByteArray?): ByteArray {
10-
requireNotNull(seed) { "Seed should not be null" }
11-
11+
override fun generateAuthentication(
12+
charset: Charset,
13+
password: String?,
14+
seed: ByteArray,
15+
sslConfiguration: SSLConfiguration,
16+
rsaPublicKey: Path?,
17+
): ByteArray {
1218
return if (password != null) {
1319
AuthenticationScrambler.scramble411("SHA-1", password, charset, seed, true)
1420
} else {

0 commit comments

Comments
 (0)