Skip to content

Commit 9a7d13a

Browse files
committed
Added RSA encryption feature
Signed-off-by: S V <vats02581@gmail.com>
1 parent 8b47ca5 commit 9a7d13a

File tree

9 files changed

+225
-12
lines changed

9 files changed

+225
-12
lines changed

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,21 @@ final class InitFlow {
126126
* @param password the password of the {@code user}.
127127
* @param compressionAlgorithms the list of compression algorithms.
128128
* @param zstdCompressionLevel the zstd compression level.
129+
* @param serverRSAPublicKeyFile the local file path of the MySQL server's public key
129130
* @return a {@link Mono} that indicates the initialization is done, or an error if the initialization failed.
130131
*/
131132
static Mono<Void> initHandshake(Client client, SslMode sslMode, String database, String user,
132-
@Nullable CharSequence password, Set<CompressionAlgorithm> compressionAlgorithms, int zstdCompressionLevel) {
133+
@Nullable CharSequence password, Set<CompressionAlgorithm> compressionAlgorithms, int zstdCompressionLevel,
134+
@Nullable String serverRSAPublicKeyFile) {
133135
return client.exchange(new HandshakeExchangeable(
134136
client,
135137
sslMode,
136138
database,
137139
user,
138140
password,
139141
compressionAlgorithms,
140-
zstdCompressionLevel
142+
zstdCompressionLevel,
143+
serverRSAPublicKeyFile
141144
)).then();
142145
}
143146

@@ -511,9 +514,12 @@ final class HandshakeExchangeable extends FluxExchangeable<Void> {
511514

512515
private boolean sslCompleted;
513516

517+
@Nullable
518+
private String serverRSAPublicKeyFile;
519+
514520
HandshakeExchangeable(Client client, SslMode sslMode, String database, String user,
515521
@Nullable CharSequence password, Set<CompressionAlgorithm> compressions,
516-
int zstdCompressionLevel) {
522+
int zstdCompressionLevel, @Nullable String serverRSAPublicKeyFile) {
517523
this.client = client;
518524
this.sslMode = sslMode;
519525
this.database = database;
@@ -522,6 +528,7 @@ final class HandshakeExchangeable extends FluxExchangeable<Void> {
522528
this.compressions = compressions;
523529
this.zstdCompressionLevel = zstdCompressionLevel;
524530
this.sslCompleted = sslMode == SslMode.TUNNEL;
531+
this.serverRSAPublicKeyFile = serverRSAPublicKeyFile;
525532
}
526533

527534
@Override
@@ -605,6 +612,11 @@ private AuthResponse createAuthResponse(String phase) {
605612
MySqlAuthProvider authProvider = getAndNextProvider();
606613

607614
if (authProvider.isSslNecessary() && !sslCompleted) {
615+
if (serverRSAPublicKeyFile != null && sslMode.equals(SslMode.DISABLED)) {
616+
return new AuthResponse(MySqlAuthProvider.rsaEncryption(authProvider.authentication(
617+
password, salt, client.getContext().getClientCollation()), serverRSAPublicKeyFile,
618+
client.getContext().getServerVersion(), salt));
619+
}
608620
throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), phase), CLI_SPECIFIC);
609621
}
610622

@@ -709,12 +721,17 @@ private MySqlAuthProvider getAndNextProvider() {
709721
private HandshakeResponse createHandshakeResponse(Capability capability) {
710722
MySqlAuthProvider authProvider = getAndNextProvider();
711723

712-
if (authProvider.isSslNecessary() && !sslCompleted) {
713-
throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), "handshake"),
714-
CLI_SPECIFIC);
724+
if (authProvider.isSslNecessary() && !sslCompleted && serverRSAPublicKeyFile == null) {
725+
throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), "handshake"), CLI_SPECIFIC);
715726
}
716727

717728
byte[] authorization = authProvider.authentication(password, salt, client.getContext().getClientCollation());
729+
730+
if (authProvider.isSslNecessary() && !sslCompleted && sslMode.equals(SslMode.DISABLED)) {
731+
authorization = MySqlAuthProvider.rsaEncryption(authorization, serverRSAPublicKeyFile,
732+
client.getContext().getServerVersion(), salt);
733+
}
734+
718735
String authType = authProvider.getType();
719736

720737
if (MySqlAuthProvider.NO_AUTH_PROVIDER.equals(authType)) {

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ public final class MySqlConnectionConfiguration {
136136

137137
private final boolean tinyInt1isBit;
138138

139+
@Nullable
140+
private final String serverRSAPublicKeyFile;
141+
139142
private MySqlConnectionConfiguration(
140143
boolean isHost, String domain, int port, MySqlSslConfiguration ssl,
141144
boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout,
@@ -153,7 +156,7 @@ private MySqlConnectionConfiguration(
153156
Extensions extensions, @Nullable Publisher<String> passwordPublisher,
154157
@Nullable AddressResolverGroup<?> resolver,
155158
boolean metrics,
156-
boolean tinyInt1isBit) {
159+
boolean tinyInt1isBit, @Nullable String serverRSAPublicKeyFile) {
157160
this.isHost = isHost;
158161
this.domain = domain;
159162
this.port = port;
@@ -185,6 +188,7 @@ private MySqlConnectionConfiguration(
185188
this.resolver = resolver;
186189
this.metrics = metrics;
187190
this.tinyInt1isBit = tinyInt1isBit;
191+
this.serverRSAPublicKeyFile = serverRSAPublicKeyFile;
188192
}
189193

190194
/**
@@ -328,6 +332,11 @@ boolean isTinyInt1isBit() {
328332
return tinyInt1isBit;
329333
}
330334

335+
@Nullable
336+
String getServerRSAPublicKeyFile() {
337+
return serverRSAPublicKeyFile;
338+
}
339+
331340
@Override
332341
public boolean equals(Object o) {
333342
if (this == o) {
@@ -367,7 +376,8 @@ public boolean equals(Object o) {
367376
Objects.equals(passwordPublisher, that.passwordPublisher) &&
368377
Objects.equals(resolver, that.resolver) &&
369378
metrics == that.metrics &&
370-
tinyInt1isBit == that.tinyInt1isBit;
379+
tinyInt1isBit == that.tinyInt1isBit &&
380+
Objects.equals(serverRSAPublicKeyFile, that.serverRSAPublicKeyFile);
371381
}
372382

373383
@Override
@@ -382,7 +392,8 @@ public int hashCode() {
382392
loadLocalInfilePath, localInfileBufferSize,
383393
queryCacheSize, prepareCacheSize,
384394
compressionAlgorithms, zstdCompressionLevel,
385-
loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit);
395+
loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit,
396+
serverRSAPublicKeyFile);
386397
}
387398

388399
@Override
@@ -418,7 +429,8 @@ private String buildCommonToStringPart() {
418429
", passwordPublisher=" + passwordPublisher +
419430
", resolver=" + resolver +
420431
", metrics=" + metrics +
421-
", tinyInt1isBit=" + tinyInt1isBit;
432+
", tinyInt1isBit=" + tinyInt1isBit +
433+
", serverRSAPublicKeyFile=" + serverRSAPublicKeyFile;
422434
}
423435

424436
/**
@@ -522,6 +534,9 @@ public static final class Builder {
522534

523535
private boolean tinyInt1isBit = true;
524536

537+
@Nullable
538+
private String serverRSAPublicKeyFile;
539+
525540
/**
526541
* Builds an immutable {@link MySqlConnectionConfiguration} with current options.
527542
*
@@ -556,7 +571,8 @@ public MySqlConnectionConfiguration build() {
556571
loadLocalInfilePath,
557572
localInfileBufferSize, queryCacheSize, prepareCacheSize,
558573
compressionAlgorithms, zstdCompressionLevel, loopResources,
559-
Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit);
574+
Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit,
575+
serverRSAPublicKeyFile);
560576
}
561577

562578
/**
@@ -1234,6 +1250,21 @@ public Builder tinyInt1isBit(boolean tinyInt1isBit) {
12341250
return this;
12351251
}
12361252

1253+
/**
1254+
* Option to configure the database server's RSA Public Key file path on the local system if RSA encryption
1255+
* is desired such as when using caching_sha2_password authentication type while SSLMode is DISABLED. If
1256+
* serverRSAPublicKeyFile not null and SSLMode is not DISABLED, SSL encryption takes precedence.
1257+
*
1258+
* @param serverRSAPublicKeyFile the local file path of the database server's RSA Public Key file or
1259+
* {@code null} when RSA encryption not desired
1260+
* @return this {@link Builder}
1261+
* @since 1.4.2
1262+
*/
1263+
public Builder serverRSAPublicKeyFile(@Nullable String serverRSAPublicKeyFile) {
1264+
this.serverRSAPublicKeyFile = serverRSAPublicKeyFile;
1265+
return this;
1266+
}
1267+
12371268
private SslMode requireSslMode() {
12381269
SslMode sslMode = this.sslMode;
12391270

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ private static Mono<MySqlConnection> getMySqlConnection(
162162
user,
163163
password,
164164
configuration.getCompressionAlgorithms(),
165-
configuration.getZstdCompressionLevel()
165+
configuration.getZstdCompressionLevel(),
166+
configuration.getServerRSAPublicKeyFile()
166167
).then(InitFlow.initSession(
167168
client,
168169
sessionDb,

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,17 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr
341341
*/
342342
public static final Option<Boolean> TINY_INT_1_IS_BIT = Option.valueOf("tinyInt1isBit");
343343

344+
/**
345+
* Option to configure the database server's RSA Public Key file path on the local system if RSA encryption
346+
* is desired such as when using caching_sha2_password authentication type while
347+
* {@link io.asyncer.r2dbc.mysql.constant.SslMode} is DISABLED. If serverRSAPublicKeyFile not
348+
* {@code null} and {@link io.asyncer.r2dbc.mysql.constant.SslMode} is not DISABLED, SSL encryption
349+
* takes precedence.
350+
*
351+
* @since 1.4.2
352+
*/
353+
public static final Option<String> SERVER_RSA_PUBLIC_KEY_FILE = Option.valueOf("serverRSAPublicKeyFile");
354+
344355
@Override
345356
public ConnectionFactory create(ConnectionFactoryOptions options) {
346357
requireNonNull(options, "connectionFactoryOptions must not be null");
@@ -438,6 +449,8 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) {
438449
.to(builder::metrics);
439450
mapper.optional(TINY_INT_1_IS_BIT).asBoolean()
440451
.to(builder::tinyInt1isBit);
452+
mapper.optional(SERVER_RSA_PUBLIC_KEY_FILE).asString()
453+
.to(builder::serverRSAPublicKeyFile);
441454

442455
return builder.build();
443456
}

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,17 @@ private static byte[] allBytesXor(byte[] left, byte[] right) {
113113
return left;
114114
}
115115

116+
static byte[] rotatingXor(byte[] password, byte[] seedBytes) {
117+
int seedLength = seedBytes.length;
118+
int passwordLength = password.length;
119+
byte[] buffer = new byte[passwordLength];
120+
121+
for (int i = 0; i < passwordLength; i++) {
122+
buffer[i] = (byte) (password[i] ^ seedBytes[i % seedLength]);
123+
}
124+
125+
return buffer;
126+
}
127+
116128
private AuthUtils() { }
117129
}

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,30 @@
1616

1717
package io.asyncer.r2dbc.mysql.authentication;
1818

19+
import io.asyncer.r2dbc.mysql.ServerVersion;
1920
import io.asyncer.r2dbc.mysql.collation.CharCollation;
2021
import io.r2dbc.spi.R2dbcPermissionDeniedException;
2122
import org.jetbrains.annotations.Nullable;
2223

2324
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
2425

26+
import java.io.IOException;
27+
import java.nio.charset.Charset;
28+
import java.nio.file.Files;
29+
import java.nio.file.Paths;
30+
import java.security.InvalidKeyException;
31+
import java.security.KeyFactory;
32+
import java.security.NoSuchAlgorithmException;
33+
import java.security.interfaces.RSAPublicKey;
34+
import java.security.spec.InvalidKeySpecException;
35+
import java.security.spec.X509EncodedKeySpec;
36+
import java.util.Base64;
37+
38+
import javax.crypto.BadPaddingException;
39+
import javax.crypto.Cipher;
40+
import javax.crypto.IllegalBlockSizeException;
41+
import javax.crypto.NoSuchPaddingException;
42+
2543
/**
2644
* An abstraction of the MySQL authorization plugin provider for connection phase. More information for MySQL
2745
* authentication type:
@@ -124,4 +142,57 @@ static MySqlAuthProvider build(String type) {
124142
* @return the next provider
125143
*/
126144
MySqlAuthProvider next();
145+
146+
/**
147+
* Encrypts data with the RSA Public Key of MySQL server
148+
* @param bytesToEncrypt the data to encrypt
149+
* @param serverRSAPublicKeyFile the file path on the local system of the database server's RSA Public Key
150+
* @param serverVersion the version of the MySQL server
151+
* @param seed the seed bytes for rotating XOR obfuscation
152+
* @return the encrypted bytes
153+
*/
154+
static byte[] rsaEncryption(byte[] bytesToEncrypt, String serverRsaPublicKeyFile, ServerVersion serverVersion,
155+
byte[] seed) {
156+
try {
157+
bytesToEncrypt = AuthUtils.rotatingXor(bytesToEncrypt, seed);
158+
159+
String key = new String(Files.readAllBytes(Paths.get(serverRsaPublicKeyFile)), Charset.defaultCharset());
160+
161+
int startIndex = key.indexOf("-----BEGIN PUBLIC KEY-----") + 26;
162+
int endIndex = key.indexOf("-----END PUBLIC KEY-----");
163+
key = key.substring(startIndex, endIndex);
164+
String publicKeyPEM = key.replaceAll(System.lineSeparator(), "");
165+
166+
byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
167+
168+
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
169+
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
170+
RSAPublicKey pk = (RSAPublicKey) keyFactory.generatePublic(keySpec);
171+
172+
Cipher cipher;
173+
if (serverVersion.isGreaterThanOrEqualTo(ServerVersion.create(8, 0, 5))) {
174+
cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
175+
} else {
176+
cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
177+
}
178+
cipher.init(Cipher.ENCRYPT_MODE, pk);
179+
return cipher.doFinal(bytesToEncrypt);
180+
} catch (IOException e) {
181+
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
182+
} catch (NoSuchAlgorithmException e) {
183+
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
184+
} catch (InvalidKeySpecException e) {
185+
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
186+
} catch (NoSuchPaddingException e) {
187+
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
188+
} catch (InvalidKeyException e) {
189+
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
190+
} catch (IllegalBlockSizeException e) {
191+
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
192+
} catch (BadPaddingException e) {
193+
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
194+
} catch (IndexOutOfBoundsException e) {
195+
throw new IllegalArgumentException(e.getLocalizedMessage(), e);
196+
}
197+
}
127198
}

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ private static MySqlConnectionConfiguration filledUp() {
282282
.lockWaitTimeout(Duration.ofSeconds(5))
283283
.statementTimeout(Duration.ofSeconds(10))
284284
.autodetectExtensions(false)
285+
.serverRSAPublicKeyFile("/path/to/mysql/serverRSAPublicKey.pem")
285286
.build();
286287
}
287288
}

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.METRICS;
5555
import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.PASSWORD_PUBLISHER;
5656
import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.RESOLVER;
57+
import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.SERVER_RSA_PUBLIC_KEY_FILE;
5758
import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.USE_SERVER_PREPARE_STATEMENT;
5859
import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT;
5960
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
@@ -530,6 +531,18 @@ void sessionVariables(String input, List<String> expected) {
530531
assertThat(MySqlConnectionFactoryProvider.setup(options).getSessionVariables()).isEqualTo(expected);
531532
}
532533

534+
@Test
535+
void validServerRSAPublicKeyFile() {
536+
ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
537+
.option(DRIVER, "mysql")
538+
.option(HOST, "127.0.0.1")
539+
.option(USER, "root")
540+
.option(SERVER_RSA_PUBLIC_KEY_FILE, "/path/to/mysql/serverRSAPublicKey.pem")
541+
.build();
542+
543+
assertThat(MySqlConnectionFactoryProvider.setup(options).getServerRSAPublicKeyFile()).isEqualTo("/path/to/mysql/serverRSAPublicKey.pem");
544+
}
545+
533546
static Stream<Arguments> sessionVariables() {
534547
return Stream.of(
535548
Arguments.of("", Collections.emptyList()),

0 commit comments

Comments
 (0)