Skip to content

Commit 027a4b9

Browse files
committed
Use RSA public key encryption for non-SSL connections.
1 parent 159cc5a commit 027a4b9

File tree

8 files changed

+205
-9
lines changed

8 files changed

+205
-9
lines changed

.travis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ env:
88
FEATURES=Json,StoredProcedures,Sha256Password
99
- IMAGE=percona:5.7
1010
NAME=percona
11-
FEATURES=Json,StoredProcedures,Sha256Password
11+
FEATURES=Json,StoredProcedures,Sha256Password,OpenSsl
1212
- IMAGE=mariadb:10.3
1313
NAME=mariadb
14-
FEATURES=StoredProcedures
14+
FEATURES=StoredProcedures,OpenSsl
1515

1616
before_install:
1717
- .ci/docker-run.sh $IMAGE $NAME 3307 $FEATURES

src/MySqlConnector/MySqlConnector.csproj

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<AssemblyTitle>Async MySQL Connector</AssemblyTitle>
88
<VersionPrefix>0.21.0</VersionPrefix>
99
<Authors>Bradley Grainger;Caleb Lloyd</Authors>
10-
<TargetFrameworks>net451;netstandard1.3</TargetFrameworks>
10+
<TargetFrameworks>net451;net46;netstandard1.3</TargetFrameworks>
1111
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1212
<AssemblyName>MySqlConnector</AssemblyName>
1313
<PackageId>MySqlConnector</PackageId>
@@ -28,11 +28,20 @@
2828
<Reference Include="System.Transactions" />
2929
</ItemGroup>
3030

31+
<ItemGroup Condition=" '$(TargetFramework)' == 'net46' ">
32+
<PackageReference Include="System.Buffers" Version="4.0.0" />
33+
<PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.0.0" />
34+
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.0.0" />
35+
<Reference Include="System.Data" />
36+
<Reference Include="System.Transactions" />
37+
</ItemGroup>
38+
3139
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
3240
<PackageReference Include="System.Buffers" Version="4.3.0" />
3341
<PackageReference Include="System.Data.Common" Version="4.3.0" />
3442
<PackageReference Include="System.Net.NameResolution" Version="4.3.0" />
3543
<PackageReference Include="System.Net.Security" Version="4.3.0" />
44+
<PackageReference Include="System.Security.Cryptography.Algorithms" Version="4.3.0" />
3645
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.3.0" />
3746
</ItemGroup>
3847

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace MySql.Data.Serialization
2+
{
3+
internal class AuthenticationMoreDataPayload
4+
{
5+
public byte[] Data { get; }
6+
7+
public const byte Signature = 0x01;
8+
9+
public static AuthenticationMoreDataPayload Create(PayloadData payload)
10+
{
11+
var reader = new ByteArrayReader(payload.ArraySegment);
12+
reader.ReadByte(Signature);
13+
return new AuthenticationMoreDataPayload(reader.ReadByteString(reader.BytesRemaining));
14+
}
15+
16+
private AuthenticationMoreDataPayload(byte[] data) => Data = data;
17+
}
18+
}

src/MySqlConnector/Serialization/MySqlSession.cs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,14 +302,52 @@ private async Task SwitchAuthenticationAsync(PayloadData payload, string passwor
302302
break;
303303

304304
case "sha256_password":
305-
if (!m_isSecureConnection)
306-
throw new NotImplementedException("Authentication method '{0}' requires a secure connection.".FormatInvariant(switchRequest.Name));
307-
308-
// send plaintext password with a NUL terminator
305+
// add NUL terminator to password
309306
var passwordBytes = Encoding.UTF8.GetBytes(password);
310307
Array.Resize(ref passwordBytes, passwordBytes.Length + 1);
311-
payload = new PayloadData(new ArraySegment<byte>(passwordBytes));
312-
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
308+
309+
if (!m_isSecureConnection && passwordBytes.Length > 1)
310+
{
311+
#if NET451
312+
throw new MySqlException("Authentication method '{0}' requires a secure connection (prior to .NET 4.6).".FormatInvariant(switchRequest.Name));
313+
#else
314+
// request the RSA public key
315+
await SendReplyAsync(new PayloadData(new ArraySegment<byte>(new byte[] { 0x01 }, 0, 1)), ioBehavior, cancellationToken).ConfigureAwait(false);
316+
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
317+
var publicKeyPayload = AuthenticationMoreDataPayload.Create(payload);
318+
var publicKey = Encoding.ASCII.GetString(publicKeyPayload.Data);
319+
320+
// load the RSA public key
321+
RSA rsa;
322+
try
323+
{
324+
rsa = Utility.DecodeX509PublicKey(publicKey);
325+
}
326+
catch (Exception ex)
327+
{
328+
throw new MySqlException("Couldn't load server's RSA public key; try using a secure connection instead.", ex);
329+
}
330+
331+
using (rsa)
332+
{
333+
// XOR the password bytes with the challenge
334+
AuthPluginData = Utility.TrimZeroByte(switchRequest.Data);
335+
for (int i = 0; i < passwordBytes.Length; i++)
336+
passwordBytes[i] ^= AuthPluginData[i % AuthPluginData.Length];
337+
338+
// encrypt with RSA public key
339+
var encryptedPassword = rsa.Encrypt(passwordBytes, RSAEncryptionPadding.OaepSHA1);
340+
payload = new PayloadData(new ArraySegment<byte>(encryptedPassword));
341+
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
342+
}
343+
#endif
344+
}
345+
else
346+
{
347+
// send plaintext password
348+
payload = new PayloadData(new ArraySegment<byte>(passwordBytes));
349+
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
350+
}
313351
break;
314352

315353
default:

src/MySqlConnector/Utility.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
22
using System.Globalization;
33
using System.IO;
4+
using System.Linq;
45
using System.Runtime.InteropServices;
6+
using System.Security.Cryptography;
57
using System.Text;
68
using System.Threading.Tasks;
79

@@ -25,6 +27,109 @@ public static string FormatInvariant(this string format, params object[] args) =
2527
public static string GetString(this Encoding encoding, ArraySegment<byte> arraySegment) =>
2628
encoding.GetString(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
2729

30+
/// <summary>
31+
/// Loads a RSA public key from a PEM string. Taken from <a href="https://stackoverflow.com/a/32243171/23633">Stack Overflow</a>.
32+
/// </summary>
33+
/// <param name="publicKey">The public key, in PEM format.</param>
34+
/// <returns>An RSA public key, or <c>null</c> on failure.</returns>
35+
public static RSA DecodeX509PublicKey(string publicKey)
36+
{
37+
var x509Key = Convert.FromBase64String(publicKey.Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", ""));
38+
39+
// encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
40+
byte[] seqOid = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
41+
42+
// --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------
43+
using (var stream = new MemoryStream(x509Key))
44+
using (var reader = new BinaryReader(stream)) //wrap Memory Stream with BinaryReader for easy reading
45+
{
46+
var temp = reader.ReadUInt16();
47+
switch (temp)
48+
{
49+
case 0x8130:
50+
reader.ReadByte(); //advance 1 byte
51+
break;
52+
case 0x8230:
53+
reader.ReadInt16(); //advance 2 bytes
54+
break;
55+
default:
56+
throw new FormatException("Expected 0x8130 or 0x8230 but read {0:X4}".FormatInvariant(temp));
57+
}
58+
59+
var seq = reader.ReadBytes(15);
60+
if (!seq.SequenceEqual(seqOid)) //make sure Sequence for OID is correct
61+
throw new FormatException("Expected RSA OID but read {0}".FormatInvariant(BitConverter.ToString(seq)));
62+
63+
temp = reader.ReadUInt16();
64+
if (temp == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81)
65+
reader.ReadByte(); //advance 1 byte
66+
else if (temp == 0x8203)
67+
reader.ReadInt16(); //advance 2 bytes
68+
else
69+
throw new FormatException("Expected 0x8130 or 0x8230 but read {0:X4}".FormatInvariant(temp));
70+
71+
var bt = reader.ReadByte();
72+
if (bt != 0x00) //expect null byte next
73+
throw new FormatException("Expected 0x00 but read {0:X2}".FormatInvariant(bt));
74+
75+
temp = reader.ReadUInt16();
76+
if (temp == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
77+
reader.ReadByte(); //advance 1 byte
78+
else if (temp == 0x8230)
79+
reader.ReadInt16(); //advance 2 bytes
80+
else
81+
throw new FormatException("Expected 0x8130 or 0x8230 but read {0:X4}".FormatInvariant(temp));
82+
83+
temp = reader.ReadUInt16();
84+
byte lowbyte;
85+
byte highbyte = 0x00;
86+
87+
if (temp == 0x8102)
88+
{
89+
//data read as little endian order (actual data order for Integer is 02 81)
90+
lowbyte = reader.ReadByte(); // read next bytes which is bytes in modulus
91+
}
92+
else if (temp == 0x8202)
93+
{
94+
highbyte = reader.ReadByte(); //advance 2 bytes
95+
lowbyte = reader.ReadByte();
96+
}
97+
else
98+
{
99+
throw new FormatException("Expected 0x8102 or 0x8202 but read {0:X4}".FormatInvariant(temp));
100+
}
101+
102+
var modulusSize = highbyte * 256 + lowbyte;
103+
104+
var firstbyte = reader.ReadByte();
105+
reader.BaseStream.Seek(-1, SeekOrigin.Current);
106+
107+
if (firstbyte == 0x00)
108+
{
109+
//if first byte (highest order) of modulus is zero, don't include it
110+
reader.ReadByte(); //skip this null byte
111+
modulusSize -= 1; //reduce modulus buffer size by 1
112+
}
113+
114+
var modulus = reader.ReadBytes(modulusSize); //read the modulus bytes
115+
116+
if (reader.ReadByte() != 0x02) //expect an Integer for the exponent data
117+
throw new FormatException("Expected 0x02");
118+
int exponentSize = reader.ReadByte(); // should only need one byte for actual exponent data (for all useful values)
119+
var exponent = reader.ReadBytes(exponentSize);
120+
121+
// ------- create RSACryptoServiceProvider instance and initialize with public key -----
122+
var rsa = RSA.Create();
123+
var rsaKeyInfo = new RSAParameters
124+
{
125+
Modulus = modulus,
126+
Exponent = exponent
127+
};
128+
rsa.ImportParameters(rsaKeyInfo);
129+
return rsa;
130+
}
131+
}
132+
28133
/// <summary>
29134
/// Returns a new <see cref="ArraySegment{T}"/> that starts at index <paramref name="index"/> into <paramref name="arraySegment"/>.
30135
/// </summary>
@@ -55,6 +160,13 @@ public static Task<T> TaskFromException<T>(Exception exception)
55160
public static Task<T> TaskFromException<T>(Exception exception) => Task.FromException<T>(exception);
56161
#endif
57162

163+
public static byte[] TrimZeroByte(byte[] value)
164+
{
165+
if (value[value.Length - 1] == 0)
166+
Array.Resize(ref value, value.Length - 1);
167+
return value;
168+
}
169+
58170
#if !NETSTANDARD1_3
59171
public static bool TryGetBuffer(this MemoryStream memoryStream, out ArraySegment<byte> buffer)
60172
{

tests/SideBySide/ConnectAsync.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,16 @@ public async Task Sha256WithoutSecureConnection()
233233
var csb = AppConfig.CreateSha256ConnectionStringBuilder();
234234
csb.SslMode = MySqlSslMode.None;
235235
using (var connection = new MySqlConnection(csb.ConnectionString))
236+
{
237+
#if BASELINE || NET451
236238
await Assert.ThrowsAsync<NotImplementedException>(() => connection.OpenAsync());
239+
#else
240+
if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.OpenSsl))
241+
await connection.OpenAsync();
242+
else
243+
await Assert.ThrowsAsync<MySqlException>(() => connection.OpenAsync());
244+
#endif
245+
}
237246
}
238247

239248
readonly DatabaseFixture m_database;

tests/SideBySide/ConnectSync.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,16 @@ public void Sha256WithoutSecureConnection()
340340
var csb = AppConfig.CreateSha256ConnectionStringBuilder();
341341
csb.SslMode = MySqlSslMode.None;
342342
using (var connection = new MySqlConnection(csb.ConnectionString))
343+
{
344+
#if BASELINE || NET451
343345
Assert.Throws<NotImplementedException>(() => connection.Open());
346+
#else
347+
if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.OpenSsl))
348+
connection.Open();
349+
else
350+
Assert.Throws<MySqlException>(() => connection.Open());
351+
#endif
352+
}
344353
}
345354

346355
readonly DatabaseFixture m_database;

tests/SideBySide/ServerFeatures.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ public enum ServerFeatures
99
Json = 1,
1010
StoredProcedures = 2,
1111
Sha256Password = 4,
12+
OpenSsl = 8,
1213
}
1314
}

0 commit comments

Comments
 (0)