diff --git a/.ci/config/config.compression.json b/.ci/config/config.compression.json
index 09326f1f6..448eb1b2b 100644
--- a/.ci/config/config.compression.json
+++ b/.ci/config/config.compression.json
@@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
- "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
+ "UnsupportedFeatures": "Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
diff --git a/.ci/config/config.json b/.ci/config/config.json
index bc38f605a..e32c72260 100644
--- a/.ci/config/config.json
+++ b/.ci/config/config.json
@@ -4,7 +4,7 @@
"SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
"PasswordlessUser": "no_password",
"SecondaryDatabase": "testdb2",
- "UnsupportedFeatures": "Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
+ "UnsupportedFeatures": "Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,ZeroDateTime",
"MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
"MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
}
diff --git a/.ci/docker-run.sh b/.ci/docker-run.sh
index 7a791e21d..1247f47c2 100755
--- a/.ci/docker-run.sh
+++ b/.ci/docker-run.sh
@@ -79,6 +79,12 @@ for i in `seq 1 120`; do
if [ $? -ne 0 ]; then exit $?; fi
fi
+ if [[ $OMIT_FEATURES != *"ParsecAuthentication"* ]]; then
+ echo "Installing auth_parsec component"
+ docker exec mysql bash -c "$MYSQL -uroot -ptest < /etc/mysql/conf.d/init_parsec.sql"
+ if [ $? -ne 0 ]; then exit $?; fi
+ fi
+
if [[ $OMIT_FEATURES != *"QueryAttributes"* ]]; then
echo "Installing query_attributes component"
docker exec mysql $MYSQL -uroot -ptest -e "INSTALL COMPONENT 'file://component_query_attributes';"
diff --git a/.ci/server/init_parsec.sql b/.ci/server/init_parsec.sql
new file mode 100644
index 000000000..85c8d6894
--- /dev/null
+++ b/.ci/server/init_parsec.sql
@@ -0,0 +1,3 @@
+INSTALL SONAME 'auth_parsec';
+CREATE USER 'parsec-user'@'%' IDENTIFIED via parsec using PASSWORD('P@rs3c-Pa55');
+GRANT ALL PRIVILEGES ON *.* TO 'parsec-user'@'%';
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 25fe3b765..9f75afc5a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,6 +9,7 @@
+
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 58e7b9801..d7cba324e 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -51,7 +51,7 @@ jobs:
arguments: 'tests\IntegrationTests\IntegrationTests.csproj -c MySqlData'
testRunTitle: 'MySql.Data integration tests'
env:
- DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,TlsFingerprintValidation,UnixDomainSocket'
+ DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,StreamingResults,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=root;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600'
DATA__CERTIFICATESPATH: '$(Build.Repository.LocalPath)\.ci\server\certs\'
DATA__MYSQLBULKLOADERLOCALCSVFILE: '$(Build.Repository.LocalPath)\tests\TestData\LoadData_UTF8_BOM_Unix.CSV'
@@ -120,7 +120,7 @@ jobs:
arguments: '-c Release --no-restore -p:TestTfmsInParallel=false'
testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net481/net9.0', 'No SSL') }}
env:
- DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket'
+ DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True;UseCompression=True'
- job: windows_integration_tests_2
@@ -158,7 +158,7 @@ jobs:
arguments: '-c Release --no-restore -p:TestTfmsInParallel=false'
testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net8.0', 'No SSL') }}
env:
- DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket'
+ DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket'
DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True'
- job: linux_integration_tests
@@ -171,27 +171,27 @@ jobs:
'MySQL 8.0':
image: 'mysql:8.0'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
- unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
+ unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MySQL 8.4':
image: 'mysql:8.4'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
- unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
+ unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MySQL 9.3':
image: 'mysql:9.3'
connectionStringExtra: 'AllowPublicKeyRetrieval=True'
- unsupportedFeatures: 'Ed25519,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
+ unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,ZeroDateTime'
'MariaDB 10.6':
image: 'mariadb:10.6'
connectionStringExtra: ''
- unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
+ unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
'MariaDB 10.11':
image: 'mariadb:10.11'
connectionStringExtra: ''
- unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
+ unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin'
'MariaDB 11.4':
image: 'mariadb:11.4'
connectionStringExtra: ''
- unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin,Redirection'
+ unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Sha256Password,Tls11,UuidToBin,Redirection'
'MariaDB 11.6':
image: 'mariadb:11.6'
connectionStringExtra: ''
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs
new file mode 100644
index 000000000..c637b6f98
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Chaos.NaCl
+{
+ internal static class CryptoBytes
+ {
+ public static void Wipe(byte[] data)
+ {
+ if (data == null)
+ throw new ArgumentNullException("data");
+ InternalWipe(data, 0, data.Length);
+ }
+
+ // Secure wiping is hard
+ // * the GC can move around and copy memory
+ // Perhaps this can be avoided by using unmanaged memory or by fixing the position of the array in memory
+ // * Swap files and error dumps can contain secret information
+ // It seems possible to lock memory in RAM, no idea about error dumps
+ // * Compiler could optimize out the wiping if it knows that data won't be read back
+ // I hope this is enough, suppressing inlining
+ // but perhaps `RtlSecureZeroMemory` is needed
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ internal static void InternalWipe(byte[] data, int offset, int count)
+ {
+ Array.Clear(data, offset, count);
+ }
+
+ // shallow wipe of structs
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ internal static void InternalWipe(ref T data)
+ where T : struct
+ {
+ data = default(T);
+ }
+ }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs
new file mode 100644
index 000000000..f3a0b7011
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Security.Cryptography;
+using Chaos.NaCl.Internal.Ed25519Ref10;
+
+namespace Chaos.NaCl
+{
+ internal static class Ed25519
+ {
+ public static readonly int PublicKeySizeInBytes = 32;
+ public static readonly int SignatureSizeInBytes = 64;
+ public static readonly int ExpandedPrivateKeySizeInBytes = 32 * 2;
+ public static readonly int PrivateKeySeedSizeInBytes = 32;
+ public static readonly int SharedKeySizeInBytes = 32;
+
+ public static void Sign(ArraySegment signature, ArraySegment message, ArraySegment expandedPrivateKey)
+ {
+ if (signature.Array == null)
+ throw new ArgumentNullException("signature.Array");
+ if (signature.Count != SignatureSizeInBytes)
+ throw new ArgumentException("signature.Count");
+ if (expandedPrivateKey.Array == null)
+ throw new ArgumentNullException("expandedPrivateKey.Array");
+ if (expandedPrivateKey.Count != ExpandedPrivateKeySizeInBytes)
+ throw new ArgumentException("expandedPrivateKey.Count");
+ if (message.Array == null)
+ throw new ArgumentNullException("message.Array");
+ Ed25519Operations.crypto_sign2(signature.Array, signature.Offset, message.Array, message.Offset, message.Count, expandedPrivateKey.Array, expandedPrivateKey.Offset);
+ }
+
+ public static byte[] Sign(byte[] message, byte[] expandedPrivateKey)
+ {
+ var signature = new byte[SignatureSizeInBytes];
+ Sign(new ArraySegment(signature), new ArraySegment(message), new ArraySegment(expandedPrivateKey));
+ return signature;
+ }
+
+ public static void KeyPairFromSeed(out byte[] publicKey, out byte[] expandedPrivateKey, byte[] privateKeySeed)
+ {
+ if (privateKeySeed == null)
+ throw new ArgumentNullException("privateKeySeed");
+ if (privateKeySeed.Length != PrivateKeySeedSizeInBytes)
+ throw new ArgumentException("privateKeySeed");
+ var pk = new byte[PublicKeySizeInBytes];
+ var sk = new byte[ExpandedPrivateKeySizeInBytes];
+ Ed25519Operations.crypto_sign_keypair(pk, 0, sk, 0, privateKeySeed, 0);
+ publicKey = pk;
+ expandedPrivateKey = sk;
+ }
+ }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs
index 9ae034b6e..abeaca869 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs
@@ -1,52 +1,63 @@
-namespace Chaos.NaCl.Internal.Ed25519Ref10;
+using System;
-/*
-ge means group element.
-
-Here the group is the set of pairs (x,y) of field elements (see fe.h)
-satisfying -x^2 + y^2 = 1 + d x^2y^2
-where d = -121665/121666.
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+ /*
+ ge means group element.
-Representations:
- ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z
- ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT
- ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
- ge_precomp (Duif): (y+x,y-x,2dxy)
-*/
+ Here the group is the set of pairs (x,y) of field elements (see fe.h)
+ satisfying -x^2 + y^2 = 1 + d x^2y^2
+ where d = -121665/121666.
-internal struct GroupElementP2
-{
- public FieldElement X;
- public FieldElement Y;
- public FieldElement Z;
-} ;
+ Representations:
+ ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z
+ ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT
+ ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
+ ge_precomp (Duif): (y+x,y-x,2dxy)
+ */
-internal struct GroupElementP3
-{
- public FieldElement X;
- public FieldElement Y;
- public FieldElement Z;
- public FieldElement T;
-} ;
+ internal struct GroupElementP2
+ {
+ public FieldElement X;
+ public FieldElement Y;
+ public FieldElement Z;
+ } ;
-internal struct GroupElementP1P1
-{
- public FieldElement X;
- public FieldElement Y;
- public FieldElement Z;
- public FieldElement T;
-} ;
+ internal struct GroupElementP3
+ {
+ public FieldElement X;
+ public FieldElement Y;
+ public FieldElement Z;
+ public FieldElement T;
+ } ;
-internal struct GroupElementPreComp
-{
- public FieldElement yplusx;
- public FieldElement yminusx;
- public FieldElement xy2d;
+ internal struct GroupElementP1P1
+ {
+ public FieldElement X;
+ public FieldElement Y;
+ public FieldElement Z;
+ public FieldElement T;
+ } ;
- public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d)
+ internal struct GroupElementPreComp
+ {
+ public FieldElement yplusx;
+ public FieldElement yminusx;
+ public FieldElement xy2d;
+
+ public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d)
+ {
+ this.yplusx = yplusx;
+ this.yminusx = yminusx;
+ this.xy2d = xy2d;
+ }
+ } ;
+
+ internal struct GroupElementCached
{
- this.yplusx = yplusx;
- this.yminusx = yminusx;
- this.xy2d = xy2d;
- }
-} ;
+ public FieldElement YplusX;
+ public FieldElement YminusX;
+ public FieldElement Z;
+ public FieldElement T2d;
+ } ;
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs
new file mode 100644
index 000000000..03676e5cf
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs
@@ -0,0 +1,50 @@
+using System;
+
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+ internal static partial class LookupTables
+ {
+ internal static readonly GroupElementPreComp[] Base2 = new GroupElementPreComp[]{
+ new GroupElementPreComp(
+ new FieldElement( 25967493,-14356035,29566456,3660896,-12694345,4014787,27544626,-11754271,-6079156,2047605 ),
+ new FieldElement( -12545711,934262,-2722910,3049990,-727428,9406986,12720692,5043384,19500929,-15469378 ),
+ new FieldElement( -8738181,4489570,9688441,-14785194,10184609,-12363380,29287919,11864899,-24514362,-4438546 )
+ ),
+ new GroupElementPreComp(
+ new FieldElement( 15636291,-9688557,24204773,-7912398,616977,-16685262,27787600,-14772189,28944400,-1550024 ),
+ new FieldElement( 16568933,4717097,-11556148,-1102322,15682896,-11807043,16354577,-11775962,7689662,11199574 ),
+ new FieldElement( 30464156,-5976125,-11779434,-15670865,23220365,15915852,7512774,10017326,-17749093,-9920357 )
+ ),
+ new GroupElementPreComp(
+ new FieldElement( 10861363,11473154,27284546,1981175,-30064349,12577861,32867885,14515107,-15438304,10819380 ),
+ new FieldElement( 4708026,6336745,20377586,9066809,-11272109,6594696,-25653668,12483688,-12668491,5581306 ),
+ new FieldElement( 19563160,16186464,-29386857,4097519,10237984,-4348115,28542350,13850243,-23678021,-15815942 )
+ ),
+ new GroupElementPreComp(
+ new FieldElement( 5153746,9909285,1723747,-2777874,30523605,5516873,19480852,5230134,-23952439,-15175766 ),
+ new FieldElement( -30269007,-3463509,7665486,10083793,28475525,1649722,20654025,16520125,30598449,7715701 ),
+ new FieldElement( 28881845,14381568,9657904,3680757,-20181635,7843316,-31400660,1370708,29794553,-1409300 )
+ ),
+ new GroupElementPreComp(
+ new FieldElement( -22518993,-6692182,14201702,-8745502,-23510406,8844726,18474211,-1361450,-13062696,13821877 ),
+ new FieldElement( -6455177,-7839871,3374702,-4740862,-27098617,-10571707,31655028,-7212327,18853322,-14220951 ),
+ new FieldElement( 4566830,-12963868,-28974889,-12240689,-7602672,-2830569,-8514358,-10431137,2207753,-3209784 )
+ ),
+ new GroupElementPreComp(
+ new FieldElement( -25154831,-4185821,29681144,7868801,-6854661,-9423865,-12437364,-663000,-31111463,-16132436 ),
+ new FieldElement( 25576264,-2703214,7349804,-11814844,16472782,9300885,3844789,15725684,171356,6466918 ),
+ new FieldElement( 23103977,13316479,9739013,-16149481,817875,-15038942,8965339,-14088058,-30714912,16193877 )
+ ),
+ new GroupElementPreComp(
+ new FieldElement( -33521811,3180713,-2394130,14003687,-16903474,-16270840,17238398,4729455,-18074513,9256800 ),
+ new FieldElement( -25182317,-4174131,32336398,5036987,-21236817,11360617,22616405,9761698,-19827198,630305 ),
+ new FieldElement( -13720693,2639453,-24237460,-7406481,9494427,-5774029,-6554551,-15960994,-2449256,-14291300 )
+ ),
+ new GroupElementPreComp(
+ new FieldElement( -3151181,-5046075,9282714,6866145,-31907062,-863023,-18940575,15033784,25105118,-7894876 ),
+ new FieldElement( -24326370,15950226,-31801215,-14592823,-11662737,-5090925,1573892,-2625887,2198790,-15804619 ),
+ new FieldElement( -3099351,10324967,-2241613,7453183,-5446979,-2735503,-13812022,-16236442,-32461234,-12290683 )
+ )
+ };
+ }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs
new file mode 100644
index 000000000..037efcfd1
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Security.Cryptography;
+
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+ internal static partial class Ed25519Operations
+ {
+ public static void crypto_sign_keypair(byte[] pk, int pkoffset, byte[] sk, int skoffset, byte[] seed, int seedoffset)
+ {
+ GroupElementP3 A;
+ int i;
+
+ Array.Copy(seed, seedoffset, sk, skoffset, 32);
+#if NET5_0_OR_GREATER
+ byte[] h = SHA512.HashData(sk.AsSpan(skoffset, 32));
+#else
+ using var hash = SHA512.Create();
+ byte[] h = hash.ComputeHash(sk, skoffset, 32);
+#endif
+ ScalarOperations.sc_clamp(h, 0);
+
+ GroupOperations.ge_scalarmult_base(out A, h, 0);
+ GroupOperations.ge_p3_tobytes(pk, pkoffset, ref A);
+
+ for (i = 0; i < 32; ++i) sk[skoffset + 32 + i] = pk[pkoffset + i];
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+ CryptographicOperations.ZeroMemory(h);
+#else
+ CryptoBytes.Wipe(h);
+#endif
+ }
+ }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs
new file mode 100644
index 000000000..a56625557
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Security.Cryptography;
+
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+ internal static partial class Ed25519Operations
+ {
+ public static void crypto_sign2(
+ byte[] sig, int sigoffset,
+ byte[] m, int moffset, int mlen,
+ byte[] sk, int skoffset)
+ {
+ byte[] az;
+ byte[] r;
+ byte[] hram;
+ GroupElementP3 R;
+ using (var hasher = SHA512.Create())
+ {
+ az = hasher.ComputeHash(sk, skoffset, 32);
+ ScalarOperations.sc_clamp(az, 0);
+
+ hasher.Initialize();
+ hasher.TransformBlock(az, 32, 32, null, 0);
+ hasher.TransformFinalBlock(m, moffset, mlen);
+ r = hasher.Hash;
+
+ ScalarOperations.sc_reduce(r);
+ GroupOperations.ge_scalarmult_base(out R, r, 0);
+ GroupOperations.ge_p3_tobytes(sig, sigoffset, ref R);
+
+ hasher.Initialize();
+ hasher.TransformBlock(sig, sigoffset, 32, null, 0);
+ hasher.TransformBlock(sk, skoffset + 32, 32, null, 0);
+ hasher.TransformFinalBlock(m, moffset, mlen);
+ hram = hasher.Hash;
+
+ ScalarOperations.sc_reduce(hram);
+ var s = new byte[32];//todo: remove allocation
+ Array.Copy(sig, sigoffset + 32, s, 0, 32);
+ ScalarOperations.sc_muladd(s, hram, az, r);
+ Array.Copy(s, 0, sig, sigoffset + 32, 32);
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+ CryptographicOperations.ZeroMemory(s);
+#else
+ CryptoBytes.Wipe(s);
+#endif
+ }
+ }
+ }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs
new file mode 100644
index 000000000..fb8b50122
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+ internal static partial class LookupTables
+ {
+ internal static FieldElement sqrtm1 = new FieldElement(-32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482);
+ }
+}
\ No newline at end of file
diff --git a/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml b/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml
index 738dbc799..e66567aca 100644
--- a/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml
+++ b/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml
@@ -1,5 +1,5 @@
-
+
PKV006
diff --git a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
index f2ea62c3e..b7f718b17 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
@@ -10,7 +10,9 @@ namespace MySqlConnector.Authentication.Ed25519;
/// Provides an implementation of the client_ed25519 authentication plugin for MariaDB.
///
/// See Authentication Plugin - ed25519.
-public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin2
+#pragma warning disable CS0618 // Type or member is obsolete
+public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin3, IAuthenticationPlugin2
+#pragma warning restore CS0618 // Type or member is obsolete
{
///
/// Registers the Ed25519 authentication plugin with MySqlConnector. You must call this method once before
@@ -32,7 +34,7 @@ public static void Install()
///
public byte[] CreateResponse(string password, ReadOnlySpan authenticationData)
{
- CreateResponseAndHash(password, authenticationData, out _, out var authenticationResponse);
+ CreateResponseAndPasswordHash(password, authenticationData, out var authenticationResponse, out _);
return authenticationResponse;
}
@@ -41,11 +43,20 @@ public byte[] CreateResponse(string password, ReadOnlySpan authenticationD
///
public byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData)
{
- CreateResponseAndHash(password, authenticationData, out var passwordHash, out _);
+ CreateResponseAndPasswordHash(password, authenticationData, out _, out var passwordHash);
return passwordHash;
}
- private static void CreateResponseAndHash(string password, ReadOnlySpan authenticationData, out byte[] passwordHash, out byte[] authenticationResponse)
+ ///
+ /// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
+ ///
+ /// The client's password.
+ /// The authentication data supplied by the server; this is the auth method data
+ /// from the Authentication
+ /// Method Switch Request Packet.
+ /// The authentication response.
+ /// The authentication-method-specific hash of the client's password.
+ public void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
{
// Java reference: https://github.com/MariaDB/mariadb-connector-j/blob/master/src/main/java/org/mariadb/jdbc/internal/com/send/authentication/Ed25519PasswordPlugin.java
// C reference: https://github.com/MariaDB/server/blob/592fe954ef82be1bc08b29a8e54f7729eb1e1343/plugin/auth_ed25519/ref10/sign.c#L7
@@ -77,8 +88,12 @@ private static void CreateResponseAndHash(string password, ReadOnlySpan au
az[31] |= 64;
*/
+#if NET5_0_OR_GREATER
+ byte[] az = SHA512.HashData(passwordBytes);
+#else
using var sha512 = SHA512.Create();
byte[] az = sha512.ComputeHash(passwordBytes);
+#endif
ScalarOperations.sc_clamp(az, 0);
/*** Java
@@ -104,7 +119,11 @@ private static void CreateResponseAndHash(string password, ReadOnlySpan au
byte[] sm = new byte[64 + authenticationData.Length];
authenticationData.CopyTo(sm.AsSpan().Slice(64));
Buffer.BlockCopy(az, 32, sm, 32, 32);
+#if NET5_0_OR_GREATER
+ byte[] nonce = SHA512.HashData(sm.AsSpan(32, authenticationData.Length + 32));
+#else
byte[] nonce = sha512.ComputeHash(sm, 32, authenticationData.Length + 32);
+#endif
/*** Java
ScalarOps scalar = new ScalarOps();
@@ -162,7 +181,11 @@ private static void CreateResponseAndHash(string password, ReadOnlySpan au
return 0;
*/
+#if NET5_0_OR_GREATER
+ var hram = SHA512.HashData(sm);
+#else
var hram = sha512.ComputeHash(sm);
+#endif
ScalarOperations.sc_reduce(hram);
var temp = new byte[32];
ScalarOperations.sc_muladd(temp, hram, az, nonce);
diff --git a/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj b/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj
index bd2122bb3..37349bfd2 100644
--- a/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj
+++ b/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj
@@ -1,22 +1,30 @@
- net462;netstandard2.0
+ net462;net472;netstandard2.0;netstandard2.1;net6.0
MySqlConnector Ed25519 Authentication Plugin
- Implements the client_ed25519 authentication plugin for MariaDB.
+ Implements the client_ed25519 and parsec authentication plugins for MariaDB.
Copyright 2019–2024 Bradley Grainger
Bradley Grainger
README.md
- mariadb;mysqlconnector;authentication;ed25519
- SA1001;SA1002;SA1005;SA1011;SA1012;SA1021;SA1025;SA1106;SA1107;SA1111;SA1119;SA1121;SA1300;SA1307;SA1312;SA1401;SA1413;SA1501;SA1505;SA1507;SA1508;SA1512;SA1518;SA1601
+ mariadb;mysqlconnector;authentication;ed25519;parsec
+ CA1305;CA1507;CA1802;CA2208;CS0649;IDE0049;SA1001;SA1002;SA1005;SA1011;SA1012;SA1021;SA1025;SA1028;SA1106;SA1107;SA1111;SA1119;SA1121;SA1124;SA1137;SA1214;SA1300;SA1307;SA1309;SA1312;SA1313;SA1401;SA1413;SA1501;SA1505;SA1507;SA1508;SA1509;SA1512;SA1515;SA1518;SA1520;SA1601
+
+
+
+
+
+
+
+
-
+
diff --git a/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs
new file mode 100644
index 000000000..d10011216
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+
+namespace MySqlConnector.Authentication.Ed25519;
+
+///
+/// Provides an implementation of the Parsec authentication plugin for MariaDB.
+///
+public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin3
+{
+ ///
+ /// Registers the Parsec authentication plugin with MySqlConnector. You must call this method once before
+ /// opening a connection that uses Parsec authentication.
+ ///
+ public static void Install()
+ {
+ if (Interlocked.CompareExchange(ref s_isInstalled, 1, 0) == 0)
+ AuthenticationPlugins.Register(new ParsecAuthenticationPlugin());
+ }
+
+ ///
+ /// Gets the authentication plugin name.
+ ///
+ public string Name => "parsec";
+
+ ///
+ /// Creates the authentication response.
+ ///
+ public byte[] CreateResponse(string password, ReadOnlySpan authenticationData)
+ {
+ CreateResponseAndPasswordHash(password, authenticationData, out var response, out _);
+ return response;
+ }
+
+ ///
+ /// Creates the authentication response.
+ ///
+ public void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
+ {
+ // first 32 bytes are server scramble
+ var serverScramble = authenticationData.Slice(0, 32);
+
+ // generate client scramble
+#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+ Span clientScramble = stackalloc byte[32];
+ RandomNumberGenerator.Fill(clientScramble);
+#else
+ var clientScramble = new byte[32];
+ using var randomNumberGenerator = RandomNumberGenerator.Create();
+ randomNumberGenerator.GetBytes(clientScramble);
+#endif
+
+ // parse extended salt from remaining authentication data and verify format
+ var extendedSalt = authenticationData.Slice(32);
+ if (extendedSalt[0] != (byte) 'P')
+ throw new ArgumentException("Invalid extended salt", nameof(authenticationData));
+ if (extendedSalt[1] is not (>= 0 and <= 3))
+ throw new ArgumentException("Invalid iteration count", nameof(authenticationData));
+
+ var iterationCount = 1024 << extendedSalt[1];
+ var salt = extendedSalt.Slice(2);
+
+ // derive private key using PBKDF2-SHA512
+ byte[] privateKeySeed;
+#if NET6_0_OR_GREATER
+ privateKeySeed = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32);
+#elif NET472_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+ using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt.ToArray(), iterationCount, HashAlgorithmName.SHA512))
+ privateKeySeed = pbkdf2.GetBytes(32);
+#else
+ privateKeySeed = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2(
+ password, salt.ToArray(), Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf.HMACSHA512,
+ iterationCount, numBytesRequested: 32);
+#endif
+ Chaos.NaCl.Ed25519.KeyPairFromSeed(out var publicKey, out var privateKey, privateKeySeed);
+
+ // generate Ed25519 keypair and sign concatenated scrambles
+ var message = new byte[serverScramble.Length + clientScramble.Length];
+ serverScramble.CopyTo(message);
+ clientScramble.CopyTo(message.AsSpan(serverScramble.Length));
+
+ var signature = Chaos.NaCl.Ed25519.Sign(message, privateKey);
+
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+ CryptographicOperations.ZeroMemory(privateKey);
+#endif
+
+ // return client scramble followed by signature
+ authenticationResponse = new byte[clientScramble.Length + signature.Length];
+ clientScramble.CopyTo(authenticationResponse.AsSpan());
+ signature.CopyTo(authenticationResponse.AsSpan(clientScramble.Length));
+
+ // "password hash" for parsec is the extended salt followed by the public key
+ passwordHash = [(byte) 'P', (byte) iterationCount, .. salt, .. publicKey];
+ }
+
+ private ParsecAuthenticationPlugin()
+ {
+ }
+
+ private static int s_isInstalled;
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/docs/README.md b/src/MySqlConnector.Authentication.Ed25519/docs/README.md
index 0dd5a9af7..b2d794296 100644
--- a/src/MySqlConnector.Authentication.Ed25519/docs/README.md
+++ b/src/MySqlConnector.Authentication.Ed25519/docs/README.md
@@ -1,7 +1,13 @@
## About
-This package implements the `client_ed25519` [authentication plugin for MariaDB](https://mariadb.com/kb/en/authentication-plugin-ed25519/).
+This package implements the following authentication plugins for MariaDB:
+
+* [`client_ed25519`](https://mariadb.com/kb/en/authentication-plugin-ed25519/).
+* [PARSEC](https://mariadb.com/kb/en/authentication-plugin-parsec/)
## How to Use
-Call `Ed25519AuthenticationPlugin.Install()` from your application startup code to enable it.
+Call either of the following methods from your application startup code to enable the corresponding authentication plugin:
+
+* `Ed25519AuthenticationPlugin.Install()`
+* `ParsecAuthenticationPlugin.Install()`
diff --git a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
index 6dfface4e..bc432c0d1 100644
--- a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
+++ b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
@@ -24,6 +24,7 @@ public interface IAuthenticationPlugin
///
/// is an extension to that returns a hash of the client's password.
///
+[Obsolete("Use IAuthenticationPlugin3 instead.")]
public interface IAuthenticationPlugin2 : IAuthenticationPlugin
{
///
@@ -36,3 +37,21 @@ public interface IAuthenticationPlugin2 : IAuthenticationPlugin
/// The authentication-method-specific hash of the client's password.
byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData);
}
+
+///
+/// is an extension to that also returns a hash of the client's password.
+///
+/// If an authentication plugin supports this interface, the base method will not be called.
+public interface IAuthenticationPlugin3 : IAuthenticationPlugin
+{
+ ///
+ /// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
+ ///
+ /// The client's password.
+ /// The authentication data supplied by the server; this is the auth method data
+ /// from the Authentication
+ /// Method Switch Request Packet.
+ /// The authentication response.
+ /// The authentication-method-specific hash of the client's password.
+ void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash);
+}
diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs
index 67e5a438e..23efbfb44 100644
--- a/src/MySqlConnector/Core/ServerSession.cs
+++ b/src/MySqlConnector/Core/ServerSession.cs
@@ -448,13 +448,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
var initialHandshake = InitialHandshakePayload.Create(payload.Span);
// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
- m_currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
+ var currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
(initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" :
"mysql_native_password";
- Log.ServerSentAuthPluginName(m_logger, Id, m_currentAuthenticationMethod);
- if (m_currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
+ Log.ServerSentAuthPluginName(m_logger, Id, currentAuthenticationMethod);
+ if (currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
{
- Log.UnsupportedAuthenticationMethod(m_logger, Id, m_currentAuthenticationMethod);
+ Log.UnsupportedAuthenticationMethod(m_logger, Id, currentAuthenticationMethod);
throw new NotSupportedException($"Authentication method '{initialHandshake.AuthPluginName}' is not supported.");
}
@@ -528,11 +528,17 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
if (m_supportsConnectionAttributes && cs.ConnectionAttributes is null)
cs.ConnectionAttributes = CreateConnectionAttributes(cs.ApplicationName);
+ var password = GetPassword(cs, connection);
+
// send a caching_sha2_password response if the server advertised support in the initial handshake
var useCachingSha2 = initialHandshake.AuthPluginName == "caching_sha2_password";
+ byte[] authenticationResponse;
+ if (useCachingSha2)
+ authenticationResponse = AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(initialHandshake.AuthPluginData.AsSpan()), password);
+ else
+ AuthenticationUtility.CreateResponseAndPasswordHash(password, initialHandshake.AuthPluginData, out authenticationResponse, out m_passwordHash);
- var password = GetPassword(cs, connection);
- using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, useCachingSha2, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
+ using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, authenticationResponse, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
await SendReplyAsync(handshakeResponsePayload, ioBehavior, cancellationToken).ConfigureAwait(false);
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -553,7 +559,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
}
else if (!m_isSecureConnection && password.Length != 0)
{
- var publicKey = await GetRsaPublicKeyAsync(m_currentAuthenticationMethod, cs, ioBehavior, cancellationToken).ConfigureAwait(false);
+ var publicKey = await GetRsaPublicKeyAsync(currentAuthenticationMethod, cs, ioBehavior, cancellationToken).ConfigureAwait(false);
payload = await SendEncryptedPasswordAsync(AuthPluginData, publicKey, password, ioBehavior, cancellationToken).ConfigureAwait(false);
}
else
@@ -583,7 +589,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
// there is no shared secret that can be used to validate the certificate
Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors);
}
- else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password))
+ else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20)))
{
Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors);
ignoreCertificateError = true;
@@ -649,36 +655,20 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
///
/// The validation hash received from the server.
/// The auth plugin data from the initial handshake.
- /// The user's password.
/// true if the validation hash matches the locally-computed value; otherwise, false.
- private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge, string password)
+ private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge)
{
// expect 0x01 followed by 64 hex characters giving a SHA2 hash
if (validationHash?.Length != 65 || validationHash[0] != 1)
return false;
- byte[]? passwordHashResult = null;
- switch (m_currentAuthenticationMethod)
- {
- case "mysql_native_password":
- passwordHashResult = AuthenticationUtility.HashPassword([], password, onlyHashPassword: true);
- break;
-
- case "client_ed25519":
- AuthenticationPlugins.TryGetPlugin(m_currentAuthenticationMethod, out var ed25519Plugin);
- if (ed25519Plugin is IAuthenticationPlugin2 plugin2)
- passwordHashResult = plugin2.CreatePasswordHash(password, challenge);
- break;
- }
- if (passwordHashResult is null)
+ // the authentication plugin must have provided a password hash (via IAuthenticationPlugin3) that we saved for future use
+ if (m_passwordHash is null)
return false;
- Span combined = stackalloc byte[32 + challenge.Length + passwordHashResult.Length];
- passwordHashResult.CopyTo(combined);
- challenge.CopyTo(combined[passwordHashResult.Length..]);
- m_remoteCertificateSha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length)..]);
-
+ // hash password hash || scramble || certificate thumbprint
Span hashBytes = stackalloc byte[32];
+ Span combined = [.. m_passwordHash, .. challenge, .. m_remoteCertificateSha2Thumbprint!];
#if NET5_0_OR_GREATER
SHA256.TryHashData(combined, hashBytes, out _);
#else
@@ -827,8 +817,8 @@ public async Task TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
DatabaseOverride = null;
}
var password = GetPassword(cs, connection);
- var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData!, password);
- using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, hashedPassword, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
+ AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
+ using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, nativeResponse, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
await SendAsync(changeUserPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
@@ -872,13 +862,12 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs,
// if the server didn't support the hashed password; rehash with the new challenge
var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span);
Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name);
- m_currentAuthenticationMethod = switchRequest.Name;
switch (switchRequest.Name)
{
case "mysql_native_password":
AuthPluginData = switchRequest.Data;
- var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, password);
- payload = new(hashedPassword);
+ AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
+ payload = new(nativeResponse);
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -931,9 +920,26 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs,
throw new NotSupportedException("'MySQL Server is requesting the insecure pre-4.1 auth mechanism (mysql_old_password). The user password must be upgraded; see https://dev.mysql.com/doc/refman/5.7/en/account-upgrades.html.");
case "client_ed25519":
- if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin))
+ if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin) || ed25519Plugin is not IAuthenticationPlugin3 ed25519Plugin3)
throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call Ed25519AuthenticationPlugin.Install to use client_ed25519 authentication.");
- payload = new(ed25519Plugin.CreateResponse(password, switchRequest.Data));
+ ed25519Plugin3.CreateResponseAndPasswordHash(password, switchRequest.Data, out var ed25519Response, out m_passwordHash);
+ payload = new(ed25519Response);
+ await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+ return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+
+ case "parsec":
+ if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin) || parsecPlugin is not IAuthenticationPlugin3 parsecPlugin3)
+ throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call ParsecAuthenticationPlugin.Install to use parsec authentication.");
+ payload = new([]);
+ await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+ payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+
+ Span combinedData = stackalloc byte[switchRequest.Data.Length + payload.Span.Length];
+ switchRequest.Data.CopyTo(combinedData);
+ payload.Span.CopyTo(combinedData.Slice(switchRequest.Data.Length));
+
+ parsecPlugin3.CreateResponseAndPasswordHash(password, combinedData, out var parsecResponse, out m_passwordHash);
+ payload = new(parsecResponse);
await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -2201,7 +2207,7 @@ protected override void OnStatementBegin(int index)
private PayloadData m_setNamesPayload;
private byte[]? m_pipelinedResetConnectionBytes;
private Dictionary? m_preparedStatements;
- private string? m_currentAuthenticationMethod;
+ private byte[]? m_passwordHash;
private byte[]? m_remoteCertificateSha2Thumbprint;
private SslPolicyErrors m_sslPolicyErrors;
}
diff --git a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
index fdc5f2352..d817e0290 100644
--- a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
@@ -56,14 +56,12 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s
public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, CompressionMethod compressionMethod, CharacterSet characterSet) =>
CreateCapabilitiesPayload(serverCapabilities, cs, compressionMethod, characterSet, ProtocolCapabilities.Ssl).ToPayloadData();
- public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, string password, bool useCachingSha2, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes)
+ public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, byte[] authenticationResponse, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes)
{
// TODO: verify server capabilities
var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, compressionMethod, characterSet);
writer.WriteNullTerminatedString(cs.UserID);
- var authenticationResponse = useCachingSha2 ? AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(handshake.AuthPluginData.AsSpan()), password) :
- AuthenticationUtility.CreateAuthenticationResponse(handshake.AuthPluginData, password);
writer.Write((byte) authenticationResponse.Length);
writer.Write(authenticationResponse);
@@ -71,7 +69,7 @@ public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSe
writer.WriteNullTerminatedString(cs.Database);
if ((handshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0)
- writer.Write(useCachingSha2 ? "caching_sha2_password\0"u8 : "mysql_native_password\0"u8);
+ writer.Write(handshake.AuthPluginName == "caching_sha2_password" ? "caching_sha2_password\0"u8 : "mysql_native_password\0"u8);
if (connectionAttributes is not null)
writer.Write(connectionAttributes);
diff --git a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
index 659d2350d..d1f325682 100644
--- a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
+++ b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
@@ -24,24 +24,26 @@ public static byte[] GetNullTerminatedPasswordBytes(string password)
return passwordBytes;
}
- public static byte[] CreateAuthenticationResponse(ReadOnlySpan challenge, string password) =>
- string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password, onlyHashPassword: false);
-
///
/// Hashes a password with the "Secure Password Authentication" method.
///
- /// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).
/// The password to hash.
- /// If true, is ignored and only the twice-hashed password
- /// is returned, instead of performing the full "secure password authentication" algorithm that XORs the hashed password against
- /// a hash derived from the challenge.
- /// A 20-byte password hash.
+ /// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).
+ /// The authentication response.
+ /// The twice-hashed password.
/// See Secure Password Authentication.
#if NET5_0_OR_GREATER
[SkipLocalsInit]
#endif
- public static byte[] HashPassword(ReadOnlySpan challenge, string password, bool onlyHashPassword)
+ public static void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
{
+ if (string.IsNullOrEmpty(password))
+ {
+ authenticationResponse = [];
+ passwordHash = [];
+ return;
+ }
+
#if !NET5_0_OR_GREATER
using var sha1 = SHA1.Create();
#endif
@@ -58,10 +60,9 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password,
sha1.TryComputeHash(passwordBytes, hashedPassword, out _);
sha1.TryComputeHash(hashedPassword, combined[20..], out _);
#endif
- if (onlyHashPassword)
- return combined[20..].ToArray();
+ passwordHash = combined[20..].ToArray();
- challenge[..20].CopyTo(combined);
+ authenticationData[..20].CopyTo(combined);
Span xorBytes = stackalloc byte[20];
#if NET5_0_OR_GREATER
SHA1.TryHashData(combined, xorBytes, out _);
@@ -71,7 +72,7 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password,
for (var i = 0; i < hashedPassword.Length; i++)
hashedPassword[i] ^= xorBytes[i];
- return hashedPassword.ToArray();
+ authenticationResponse = hashedPassword.ToArray();
}
public static byte[] CreateScrambleResponse(ReadOnlySpan nonce, string password) =>
diff --git a/tests/IntegrationTests/ConnectAsync.cs b/tests/IntegrationTests/ConnectAsync.cs
index ce31fdf55..03e69afde 100644
--- a/tests/IntegrationTests/ConnectAsync.cs
+++ b/tests/IntegrationTests/ConnectAsync.cs
@@ -1,7 +1,4 @@
using System.Security.Authentication;
-#if !MYSQL_DATA
-using MySqlConnector.Authentication.Ed25519;
-#endif
namespace IntegrationTests;
@@ -429,7 +426,7 @@ public async Task CachingSha2WithoutSecureConnection()
[SkippableFact(ServerFeatures.Ed25519)]
public async Task Ed25519Authentication()
{
- Ed25519AuthenticationPlugin.Install();
+ MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install();
var csb = AppConfig.CreateConnectionStringBuilder();
csb.UserID = "ed25519user";
@@ -442,7 +439,7 @@ public async Task Ed25519Authentication()
[SkippableFact(ServerFeatures.Ed25519)]
public async Task MultiAuthentication()
{
- Ed25519AuthenticationPlugin.Install();
+ MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install();
var csb = AppConfig.CreateConnectionStringBuilder();
csb.UserID = "multiAuthUser";
csb.Password = "secret";
@@ -450,6 +447,18 @@ public async Task MultiAuthentication()
using var connection = new MySqlConnection(csb.ConnectionString);
await connection.OpenAsync();
}
+
+ [SkippableFact(ServerFeatures.ParsecAuthentication)]
+ public async Task Parsec()
+ {
+ MySqlConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install();
+ var csb = AppConfig.CreateConnectionStringBuilder();
+ csb.UserID = "parsec-user";
+ csb.Password = "P@rs3c-Pa55";
+ csb.Database = null;
+ using var connection = new MySqlConnection(csb.ConnectionString);
+ await connection.OpenAsync();
+ }
#endif
// To create a MariaDB GSSAPI user for a current user
diff --git a/tests/IntegrationTests/ServerFeatures.cs b/tests/IntegrationTests/ServerFeatures.cs
index 120b541bf..f78c4829d 100644
--- a/tests/IntegrationTests/ServerFeatures.cs
+++ b/tests/IntegrationTests/ServerFeatures.cs
@@ -45,4 +45,9 @@ public enum ServerFeatures
/// Server provides hash of TLS certificate in first OK packet.
///
TlsFingerprintValidation = 0x100_0000,
+
+ ///
+ /// Server supports the 'parsec' authentication plugin.
+ ///
+ ParsecAuthentication = 0x200_0000,
}
diff --git a/tests/IntegrationTests/SslTests.cs b/tests/IntegrationTests/SslTests.cs
index b27742e02..be0dcf3da 100644
--- a/tests/IntegrationTests/SslTests.cs
+++ b/tests/IntegrationTests/SslTests.cs
@@ -249,6 +249,20 @@ public async Task ConnectZeroConfigurationSslEd25519()
using var connection = new MySqlConnection(csb.ConnectionString);
await connection.OpenAsync();
}
+
+ [SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.ParsecAuthentication)]
+ public async Task ConnectZeroConfigurationSslParsec()
+ {
+ MySqlConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install();
+ var csb = AppConfig.CreateConnectionStringBuilder();
+ csb.CertificateFile = null;
+ csb.SslMode = MySqlSslMode.VerifyFull;
+ csb.SslCa = "";
+ csb.UserID = "parsec-user";
+ csb.Password = "P@rs3c-Pa55";
+ using var connection = new MySqlConnection(csb.ConnectionString);
+ await connection.OpenAsync();
+ }
#endif
[SkippableFact(ConfigSettings.RequiresSsl)]