diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X25519.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X25519.cs new file mode 100644 index 00000000000000..ae315aef7cf8ec --- /dev/null +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X25519.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Security.Cryptography; + +internal static partial class Interop +{ + internal static partial class AndroidCrypto + { + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519IsSupported")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool X25519IsSupported(); + + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519DestroyKey")] + internal static partial void X25519DestroyKey(IntPtr key); + + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519GenerateKey")] + private static partial int X25519GenerateKeyNative( + out SafeX25519PublicKeyHandle publicKey, + out SafeX25519PrivateKeyHandle privateKey); + + internal static void X25519GenerateKey( + out SafeX25519PublicKeyHandle publicKey, + out SafeX25519PrivateKeyHandle privateKey) + { + const int Success = 1; + + int result = X25519GenerateKeyNative(out publicKey, out privateKey); + + if (result != Success) + { + publicKey.Dispose(); + privateKey.Dispose(); + throw new CryptographicException(); + } + } + + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519ExportSubjectPublicKeyInfo")] + private static partial int X25519ExportSubjectPublicKeyInfoNative( + SafeX25519PublicKeyHandle publicKey, + Span buffer, + int bufferLength, + out int bytesWritten); + + internal static bool X25519TryExportSubjectPublicKeyInfo( + SafeX25519PublicKeyHandle publicKey, + Span buffer, + out int bytesWritten) + { + const int Success = 1; + const int InsufficientBuffer = -1; + + int result = X25519ExportSubjectPublicKeyInfoNative( + publicKey, + buffer, + buffer.Length, + out bytesWritten); + + return result switch + { + Success => true, + InsufficientBuffer => false, + _ => throw new CryptographicException(), + }; + } + + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519ExportPkcs8PrivateKey")] + private static partial int X25519ExportPkcs8PrivateKeyNative( + SafeX25519PrivateKeyHandle privateKey, + Span buffer, + int bufferLength, + out int bytesWritten); + + internal static bool X25519TryExportPkcs8PrivateKey( + SafeX25519PrivateKeyHandle privateKey, + Span buffer, + out int bytesWritten) + { + const int Success = 1; + const int InsufficientBuffer = -1; + + int result = X25519ExportPkcs8PrivateKeyNative( + privateKey, + buffer, + buffer.Length, + out bytesWritten); + + return result switch + { + Success => true, + InsufficientBuffer => false, + _ => throw new CryptographicException(), + }; + } + + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519ImportSubjectPublicKeyInfo")] + private static partial SafeX25519PublicKeyHandle X25519ImportSubjectPublicKeyInfoNative( + ReadOnlySpan buffer, + int bufferLength); + + internal static SafeX25519PublicKeyHandle X25519ImportSubjectPublicKeyInfo(ReadOnlySpan spki) + { + SafeX25519PublicKeyHandle handle = X25519ImportSubjectPublicKeyInfoNative(spki, spki.Length); + + if (handle.IsInvalid) + { + handle.Dispose(); + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + return handle; + } + + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519ImportPkcs8PrivateKey")] + private static partial SafeX25519PrivateKeyHandle X25519ImportPkcs8PrivateKeyNative( + ReadOnlySpan buffer, + int bufferLength); + + internal static SafeX25519PrivateKeyHandle X25519ImportPkcs8PrivateKey(ReadOnlySpan pkcs8) + { + SafeX25519PrivateKeyHandle handle = X25519ImportPkcs8PrivateKeyNative(pkcs8, pkcs8.Length); + + if (handle.IsInvalid) + { + handle.Dispose(); + throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); + } + + return handle; + } + + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519DeriveSecret")] + private static partial int X25519DeriveSecretNative( + SafeX25519PrivateKeyHandle privateKey, + SafeX25519PublicKeyHandle publicKey, + Span destination, + int destinationLength); + + internal static void X25519DeriveSecret( + SafeX25519PrivateKeyHandle privateKey, + SafeX25519PublicKeyHandle publicKey, + Span destination) + { + const int Success = 1; + + int result = X25519DeriveSecretNative(privateKey, publicKey, destination, destination.Length); + + if (result != Success) + { + throw new CryptographicException(); + } + } + + [LibraryImport(Libraries.AndroidCryptoNative, EntryPoint = "AndroidCryptoNative_X25519DeriveSecretWithSubjectPublicKeyInfo")] + private static partial int X25519DeriveSecretWithSubjectPublicKeyInfoNative( + SafeX25519PrivateKeyHandle privateKey, + ReadOnlySpan subjectPublicKeyInfo, + int subjectPublicKeyInfoLength, + Span destination, + int destinationLength); + + internal static void X25519DeriveSecretWithSubjectPublicKeyInfo( + SafeX25519PrivateKeyHandle privateKey, + ReadOnlySpan subjectPublicKeyInfo, + Span destination) + { + const int Success = 1; + + int result = X25519DeriveSecretWithSubjectPublicKeyInfoNative( + privateKey, + subjectPublicKeyInfo, + subjectPublicKeyInfo.Length, + destination, + destination.Length); + + if (result != Success) + { + throw new CryptographicException(); + } + } + } +} + +namespace System.Security.Cryptography +{ + internal sealed class SafeX25519PublicKeyHandle : SafeHandle + { + public SafeX25519PublicKeyHandle() + : base(IntPtr.Zero, ownsHandle: true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.AndroidCrypto.X25519DestroyKey(handle); + SetHandle(IntPtr.Zero); + return true; + } + } + + internal sealed class SafeX25519PrivateKeyHandle : SafeHandle + { + public SafeX25519PrivateKeyHandle() + : base(IntPtr.Zero, ownsHandle: true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + Interop.AndroidCrypto.X25519DestroyKey(handle); + SetHandle(IntPtr.Zero); + return true; + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.cs b/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.cs index 109f25afc5096c..31f1a3ffbbc205 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.cs @@ -53,7 +53,8 @@ internal static void ReadSubjectPublicKeyInfo( internal static ReadOnlySpan ReadSubjectPublicKeyInfo( string[] validOids, ReadOnlySpan source, - out int bytesRead) + out int bytesRead, + bool permitParameters = true) { ValueSubjectPublicKeyInfoAsn spki; int read; @@ -70,7 +71,8 @@ internal static ReadOnlySpan ReadSubjectPublicKeyInfo( throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } - if (Array.IndexOf(validOids, spki.Algorithm.Algorithm) < 0) + if (Array.IndexOf(validOids, spki.Algorithm.Algorithm) < 0 || + (!permitParameters && spki.Algorithm.HasParameters)) { throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); } @@ -130,7 +132,8 @@ internal static void ReadPkcs8( internal static ReadOnlySpan ReadPkcs8( string[] validOids, ReadOnlySpan source, - out int bytesRead) + out int bytesRead, + bool permitParameters = true) { try { @@ -138,7 +141,8 @@ internal static ReadOnlySpan ReadPkcs8( int read = reader.PeekEncodedValue().Length; ValuePrivateKeyInfoAsn.Decode(ref reader, out ValuePrivateKeyInfoAsn privateKeyInfo); - if (Array.IndexOf(validOids, privateKeyInfo.PrivateKeyAlgorithm.Algorithm) < 0) + if (Array.IndexOf(validOids, privateKeyInfo.PrivateKeyAlgorithm.Algorithm) < 0 || + (!permitParameters && privateKeyInfo.PrivateKeyAlgorithm.HasParameters)) { throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey); } diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 344b9f60f008ed..360895a759854f 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -1208,6 +1208,8 @@ Link="Common\Interop\Android\System.Security.Cryptography.Native.Android\Interop.Random.cs" /> + - + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs index 9a32a2191a87dc..32fa23d7d46df9 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellman.cs @@ -22,7 +22,7 @@ namespace System.Security.Cryptography /// public abstract class X25519DiffieHellman : IDisposable { - private static readonly string[] s_knownOids = [Oids.X25519]; + private protected static readonly string[] s_knownOids = [Oids.X25519]; private bool _disposed; @@ -41,8 +41,11 @@ public abstract class X25519DiffieHellman : IDisposable /// public const int PublicKeySizeInBytes = 32; + // Pre-encoded PKCS#8 for X25519 is 48 bytes: 16 byte preamble + 32 byte private key. + private protected const int Pkcs8SizeInBytes = 16 + PrivateKeySizeInBytes; + // Pre-encoded SPKI for X25519 is 44 bytes: 12 byte preamble + 32 byte public key. - private const int SpkiSizeInBytes = 12 + PublicKeySizeInBytes; + private protected const int SpkiSizeInBytes = 12 + PublicKeySizeInBytes; /// /// Gets a value that indicates whether the algorithm is supported on the current platform. @@ -1427,7 +1430,12 @@ protected virtual void Dispose(bool disposing) { } - private bool TryExportSubjectPublicKeyInfoCore(Span destination, out int bytesWritten) + private protected static bool TryWriteSubjectPublicKeyInfo( + Span destination, + TState state, + Action> writer, + out int bytesWritten) + where TState : allows ref struct { // Pre-encoded SubjectPublicKeyInfo for X25519 (RFC 8410): ReadOnlySpan spkiPreamble = @@ -1446,11 +1454,20 @@ private bool TryExportSubjectPublicKeyInfoCore(Span destination, out int b } spkiPreamble.CopyTo(destination); - ExportPublicKeyCore(destination.Slice(spkiPreamble.Length, PublicKeySizeInBytes)); + writer(state, destination.Slice(spkiPreamble.Length, PublicKeySizeInBytes)); bytesWritten = SpkiSizeInBytes; return true; } + private bool TryExportSubjectPublicKeyInfoCore(Span destination, out int bytesWritten) + { + return TryWriteSubjectPublicKeyInfo( + destination, + this, + static (self, buffer) => self.ExportPublicKeyCore(buffer), + out bytesWritten); + } + private TResult ExportPkcs8PrivateKeyCallback(Func, TResult> func) { // A PKCS#8 X25519 PrivateKeyInfo has an ASN.1 overhead of 16 bytes, assuming no attributes. @@ -1479,6 +1496,20 @@ private TResult ExportPkcs8PrivateKeyCallback(Func, } private protected bool TryExportPkcs8PrivateKeyImpl(Span destination, out int bytesWritten) + { + return TryWritePkcs8PrivateKey( + destination, + this, + static (self, buffer) => self.ExportPrivateKeyCore(buffer), + out bytesWritten); + } + + private protected static bool TryWritePkcs8PrivateKey( + Span destination, + TState state, + Action> writer, + out int bytesWritten) + where TState : allows ref struct { // Pre-encoded PKCS#8 PrivateKeyInfo for X25519 (RFC 8410): ReadOnlySpan pkcs8Preamble = @@ -1490,9 +1521,9 @@ private protected bool TryExportPkcs8PrivateKeyImpl(Span destination, out 0x04, 0x20, // OCTET STRING (32 bytes) ]; - int pkcs8SizeInBytes = pkcs8Preamble.Length + PrivateKeySizeInBytes; + Debug.Assert(pkcs8Preamble.Length + PrivateKeySizeInBytes == Pkcs8SizeInBytes); - if (destination.Length < pkcs8SizeInBytes) + if (destination.Length < Pkcs8SizeInBytes) { bytesWritten = 0; return false; @@ -1503,8 +1534,8 @@ private protected bool TryExportPkcs8PrivateKeyImpl(Span destination, out try { - ExportPrivateKey(privateKeyBuffer); - bytesWritten = pkcs8SizeInBytes; + writer(state, privateKeyBuffer); + bytesWritten = Pkcs8SizeInBytes; return true; } catch diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Android.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Android.cs new file mode 100644 index 00000000000000..ffa4dbc8dbb514 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X25519DiffieHellmanImplementation.Android.cs @@ -0,0 +1,308 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Formats.Asn1; + +namespace System.Security.Cryptography +{ + internal sealed class X25519DiffieHellmanImplementation : X25519DiffieHellman + { + private static readonly Lazy s_basePointHandle = new(static () => + { + ReadOnlySpan basePoint = + [ + 9, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + return ImportPublicKeyAsHandle(basePoint); + }); + + private readonly SafeX25519PublicKeyHandle _publicKey; + private readonly SafeX25519PrivateKeyHandle? _privateKey; + + internal static new bool IsSupported { get; } = Interop.AndroidCrypto.X25519IsSupported(); + + private X25519DiffieHellmanImplementation( + SafeX25519PublicKeyHandle publicKey, + SafeX25519PrivateKeyHandle? privateKey) + { + _publicKey = publicKey; + _privateKey = privateKey; + } + + protected override void DeriveRawSecretAgreementCore(X25519DiffieHellman otherParty, Span destination) + { + Debug.Assert(destination.Length == SecretAgreementSizeInBytes); + ThrowIfPrivateNeeded(); + + if (otherParty is X25519DiffieHellmanImplementation otherImpl) + { + DeriveRawSecretAgreementCore(_privateKey, otherImpl._publicKey, destination); + } + else + { + unsafe + { + Span otherPublicKey = stackalloc byte[PublicKeySizeInBytes]; + otherParty.ExportPublicKey(otherPublicKey); + DeriveRawSecretAgreementCore(otherPublicKey, destination); + } + } + } + + protected override void DeriveRawSecretAgreementCore(ReadOnlySpan otherPartyPublicKey, Span destination) + { + Debug.Assert(otherPartyPublicKey.Length == PublicKeySizeInBytes); + Debug.Assert(destination.Length == SecretAgreementSizeInBytes); + ThrowIfPrivateNeeded(); + + unsafe + { + Span spki = stackalloc byte[SpkiSizeInBytes]; + + bool encoded = TryWriteSubjectPublicKeyInfo( + spki, + otherPartyPublicKey, + static (source, buffer) => source.CopyTo(buffer), + out int written); + + // SPKI encoding is either right or wrong, there aren't "optional" things that can be written down. So it + // should be precisely sized. + if (!encoded || written != SpkiSizeInBytes) + { + throw new CryptographicException(); + } + + Interop.AndroidCrypto.X25519DeriveSecretWithSubjectPublicKeyInfo(_privateKey, spki, destination); + } + } + + protected override void ExportPrivateKeyCore(Span destination) + { + Debug.Assert(destination.Length == PrivateKeySizeInBytes); + ThrowIfPrivateNeeded(); + + // PKCS#8 keys are not strictly deterministic in size because they could have attributes as "metadata" + // attached. A minimally encoded PKCS#8 private key is going to be 48 bytes. 512 bytes is 10x more space + // than needed, but anything larger than that we won't attempt to process. + scoped Span pkcs8Buffer; + + unsafe + { + pkcs8Buffer = stackalloc byte[512]; + } + + if (!Interop.AndroidCrypto.X25519TryExportPkcs8PrivateKey(_privateKey, pkcs8Buffer, out int written)) + { + Debug.Fail($"X25519 PKCS#8 PrivateKeyInfo did not fit in {pkcs8Buffer.Length} bytes."); + throw new CryptographicException(SR.Argument_DestinationTooShort); + } + + try + { + ReadOnlySpan privateKeyContents = KeyFormatHelper.ReadPkcs8( + s_knownOids, + pkcs8Buffer.Slice(0, written), + out int bytesRead, + permitParameters: false); + + Debug.Assert(bytesRead == written); + + ValueAsnReader reader = new(privateKeyContents, AsnEncodingRules.BER); + + if (reader.TryReadPrimitiveOctetString(out ReadOnlySpan privateKey)) + { + if (privateKey.Length != PrivateKeySizeInBytes) + { + throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + } + + privateKey.CopyTo(destination); + } + else + { + byte[] allocatedPrivateKey = reader.ReadOctetString(); + + try + { + if (allocatedPrivateKey.Length != PrivateKeySizeInBytes) + { + throw new CryptographicException(SR.Argument_PrivateKeyWrongSizeForAlgorithm); + } + + allocatedPrivateKey.CopyTo(destination); + } + finally + { + CryptographicOperations.ZeroMemory(allocatedPrivateKey); + } + } + + reader.ThrowIfNotEmpty(); + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + finally + { + CryptographicOperations.ZeroMemory(pkcs8Buffer.Slice(0, written)); + } + } + + protected override void ExportPublicKeyCore(Span destination) + { + Debug.Assert(destination.Length == PublicKeySizeInBytes); + + + scoped Span spkiBuffer; + + unsafe + { + spkiBuffer = stackalloc byte[SpkiSizeInBytes]; + } + + // A SPKI has no wiggle room - they are DER, we expect no algorithm parameters, etc. Either it is exactly + // the right size, or it's wrong. + if (!Interop.AndroidCrypto.X25519TryExportSubjectPublicKeyInfo(_publicKey, spkiBuffer, out int written) + || written != SpkiSizeInBytes) + { + Debug.Fail($"X25519 SubjectPublicKeyInfo did not fit in {spkiBuffer.Length} bytes or wrote the incorrect amount."); + throw new CryptographicException(); + } + + ReadOnlySpan key = KeyFormatHelper.ReadSubjectPublicKeyInfo( + s_knownOids, + spkiBuffer, + out int read, + permitParameters: false); + + Debug.Assert(read == SpkiSizeInBytes); + key.CopyTo(destination); + } + + protected override bool TryExportPkcs8PrivateKeyCore(Span destination, out int bytesWritten) + { + ThrowIfPrivateNeeded(); + return TryExportPkcs8PrivateKeyImpl(destination, out bytesWritten); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _publicKey.Dispose(); + _privateKey?.Dispose(); + } + + base.Dispose(disposing); + } + + internal static X25519DiffieHellmanImplementation GenerateKeyImpl() + { + Interop.AndroidCrypto.X25519GenerateKey( + out SafeX25519PublicKeyHandle publicKey, + out SafeX25519PrivateKeyHandle privateKey); + + return new X25519DiffieHellmanImplementation(publicKey, privateKey); + } + + internal static X25519DiffieHellmanImplementation ImportPrivateKeyImpl(ReadOnlySpan source) + { + Debug.Assert(source.Length == PrivateKeySizeInBytes); + + unsafe + { + Span pkcs8 = stackalloc byte[Pkcs8SizeInBytes]; + + bool encoded = TryWritePkcs8PrivateKey( + pkcs8, + source, + static (source, buffer) => source.CopyTo(buffer), + out int written); + + Debug.Assert(encoded); + Debug.Assert(written == Pkcs8SizeInBytes); + + SafeX25519PrivateKeyHandle privateKey = Interop.AndroidCrypto.X25519ImportPkcs8PrivateKey(pkcs8.Slice(0, written)); + + try + { + // Android's native implementation gives us two handles, one for the private key and one for the + // public key when generating a key pair. When importing a private key, it only gives us a handle + // representing the private key back. This makes it difficult to export the public key out of a private + // key handle since the export on the handle doesn't specify whether you want the public or private key. + // To recover the public key from the private key, we do X25519(9, key). This is how the public key + // is computed per RFC7748, section 6.1. + Span publicKeyBytes = stackalloc byte[PublicKeySizeInBytes]; + DeriveRawSecretAgreementCore(privateKey, s_basePointHandle.Value, publicKeyBytes); + SafeX25519PublicKeyHandle publicKey = ImportPublicKeyAsHandle(publicKeyBytes); + return new X25519DiffieHellmanImplementation(publicKey, privateKey); + } + catch + { + privateKey.Dispose(); + throw; + } + finally + { + CryptographicOperations.ZeroMemory(pkcs8); + } + } + } + + internal static X25519DiffieHellmanImplementation ImportPublicKeyImpl(ReadOnlySpan source) + { + Debug.Assert(source.Length == PublicKeySizeInBytes); + return new X25519DiffieHellmanImplementation(ImportPublicKeyAsHandle(source), privateKey: null); + } + + [MemberNotNull(nameof(_privateKey))] + private void ThrowIfPrivateNeeded() + { + if (_privateKey is null) + { + throw new CryptographicException(SR.Cryptography_CSP_NoPrivateKey); + } + } + + private static void DeriveRawSecretAgreementCore( + SafeX25519PrivateKeyHandle currentParty, + SafeX25519PublicKeyHandle otherParty, + Span destination) + { + Debug.Assert(destination.Length == SecretAgreementSizeInBytes); + Interop.AndroidCrypto.X25519DeriveSecret(currentParty, otherParty, destination); + } + + private static SafeX25519PublicKeyHandle ImportPublicKeyAsHandle(ReadOnlySpan source) + { + scoped Span spki; + + unsafe + { + spki = stackalloc byte[SpkiSizeInBytes]; + } + + bool encoded = TryWriteSubjectPublicKeyInfo( + spki, + source, + static (source, buffer) => source.CopyTo(buffer), + out int written); + + // SPKI encoding is either right or wrong, there aren't "optional" things that can be written down. So it + // should be precisely sized. + if (!encoded || written != SpkiSizeInBytes) + { + throw new CryptographicException(); + } + + return Interop.AndroidCrypto.X25519ImportSubjectPublicKeyInfo(spki); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs index fa00b438fbc569..b0cac751d4367e 100644 --- a/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X25519DiffieHellmanImplementationTests.cs @@ -25,6 +25,7 @@ public static void IsSupported_AgreesWithPlatform() bool expectedSupported = PlatformDetection.IsWindows10OrLater || PlatformDetection.IsApplePlatform || + OperatingSystem.IsAndroidVersionAtLeast(33) || PlatformDetection.IsOpenSslSupported; // X25519 is in OpenSSL 1.1.0 and .NET's floor is 1.1.1. Assert.Equal(expectedSupported, X25519DiffieHellman.IsSupported); diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt b/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt index 9b30cdeeacd90c..6cf63c72359927 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt +++ b/src/native/libs/System.Security.Cryptography.Native.Android/CMakeLists.txt @@ -27,6 +27,7 @@ set(NATIVECRYPTO_SOURCES pal_ssl.c pal_sslstream.c pal_trust_manager.c + pal_x25519.c pal_x509.c pal_x509chain.c pal_x509store.c diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c index 81f11ab25c2ea7..7f0b4d0e49f620 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.c @@ -155,6 +155,10 @@ jclass g_PrivateKeyEntryClass; jmethodID g_PrivateKeyEntryGetCertificate; jmethodID g_PrivateKeyEntryGetPrivateKey; +// java/security/Key +jclass g_KeyClass; +jmethodID g_KeyGetEncoded; + // java/security/KeyStore$TrustedCertificateEntry jclass g_TrustedCertificateEntryClass; jmethodID g_TrustedCertificateEntryGetTrustedCertificate; @@ -873,6 +877,9 @@ jint AndroidCryptoNative_InitLibraryOnLoad (JavaVM *vm, void *reserved) g_PrivateKeyEntryGetCertificate = GetMethod(env, false, g_PrivateKeyEntryClass, "getCertificate", "()Ljava/security/cert/Certificate;"); g_PrivateKeyEntryGetPrivateKey = GetMethod(env, false, g_PrivateKeyEntryClass, "getPrivateKey", "()Ljava/security/PrivateKey;"); + g_KeyClass = GetClassGRef(env, "java/security/Key"); + g_KeyGetEncoded = GetMethod(env, false, g_KeyClass, "getEncoded", "()[B"); + g_TrustedCertificateEntryClass = GetClassGRef(env, "java/security/KeyStore$TrustedCertificateEntry"); g_TrustedCertificateEntryGetTrustedCertificate = GetMethod(env, false, g_TrustedCertificateEntryClass, "getTrustedCertificate", "()Ljava/security/cert/Certificate;"); diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h index 9285c123525a27..4b77d0c24367b0 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_jni.h @@ -245,6 +245,10 @@ extern jclass g_PrivateKeyEntryClass; extern jmethodID g_PrivateKeyEntryGetCertificate; extern jmethodID g_PrivateKeyEntryGetPrivateKey; +// java/security/Key +extern jclass g_KeyClass; +extern jmethodID g_KeyGetEncoded; + // java/security/KeyStore$TrustedCertificateEntry extern jclass g_TrustedCertificateEntryClass; extern jmethodID g_TrustedCertificateEntryGetTrustedCertificate; diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_x25519.c b/src/native/libs/System.Security.Cryptography.Native.Android/pal_x25519.c new file mode 100644 index 00000000000000..c04977b2742951 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_x25519.c @@ -0,0 +1,370 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "pal_x25519.h" +#include "pal_misc.h" + +#include + +static jobject ImportSubjectPublicKeyInfo(JNIEnv* env, const uint8_t* buffer, int32_t bufferLength); +static int32_t ExportEncodedKey(jobject key, uint8_t* buffer, int32_t bufferLength, int32_t* bytesWritten); + +int32_t AndroidCryptoNative_X25519IsSupported(void) +{ + JNIEnv* env = GetJNIEnv(); + int32_t ret = FAIL; + + INIT_LOCALS(loc, algorithmName, keyPairGenerator, keyPair); + + loc[algorithmName] = make_java_string(env, "XDH"); + loc[keyPairGenerator] = (*env)->CallStaticObjectMethod(env, g_keyPairGenClass, g_keyPairGenGetInstanceMethod, loc[algorithmName]); + + if (TryClearJNIExceptions(env)) + { + goto cleanup; + } + + // Generating a key pair exercises the full provider path, which catches cases where + // getInstance succeeds on a stub provider but actual key generation is not implemented. + loc[keyPair] = (*env)->CallObjectMethod(env, loc[keyPairGenerator], g_keyPairGenGenKeyPairMethod); + + if (TryClearJNIExceptions(env)) + { + goto cleanup; + } + + if (loc[keyPair] != NULL) + { + ret = SUCCESS; + } + +cleanup: + RELEASE_LOCALS(loc, env); + return ret; +} + +void AndroidCryptoNative_X25519DestroyKey(jobject key) +{ + if (key) + { + JNIEnv* env = GetJNIEnv(); + + if ((*env)->IsInstanceOf(env, key, g_DestroyableClass)) + { + (*env)->CallVoidMethod(env, key, g_destroy); + (void)TryClearJNIExceptions(env); + } + + ReleaseGRef(env, key); + } +} + +int32_t AndroidCryptoNative_X25519GenerateKey(jobject* publicKey, jobject* privateKey) +{ + abort_if_invalid_pointer_argument(publicKey); + abort_if_invalid_pointer_argument(privateKey); + + *publicKey = NULL; + *privateKey = NULL; + + JNIEnv* env = GetJNIEnv(); + int32_t ret = FAIL; + + INIT_LOCALS(loc, algorithmName, keyPairGenerator, keyPair, pubKey, privKey); + + loc[algorithmName] = make_java_string(env, "XDH"); + loc[keyPairGenerator] = (*env)->CallStaticObjectMethod(env, g_keyPairGenClass, g_keyPairGenGetInstanceMethod, loc[algorithmName]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[keyPair] = (*env)->CallObjectMethod(env, loc[keyPairGenerator], g_keyPairGenGenKeyPairMethod); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (loc[keyPair] == NULL) + { + goto cleanup; + } + + loc[pubKey] = (*env)->CallObjectMethod(env, loc[keyPair], g_keyPairGetPublicMethod); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[privKey] = (*env)->CallObjectMethod(env, loc[keyPair], g_keyPairGetPrivateMethod); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (loc[pubKey] == NULL || loc[privKey] == NULL) + { + goto cleanup; + } + + *publicKey = ToGRef(env, loc[pubKey]); + loc[pubKey] = NULL; + + if (CheckJNIExceptions(env) || *publicKey == NULL) + { + goto cleanup; + } + + *privateKey = ToGRef(env, loc[privKey]); + loc[privKey] = NULL; + + if (CheckJNIExceptions(env) || *privateKey == NULL) + { + AndroidCryptoNative_X25519DestroyKey(*publicKey); + *publicKey = NULL; + goto cleanup; + } + + ret = SUCCESS; + +cleanup: + RELEASE_LOCALS(loc, env); + return ret; +} + +int32_t AndroidCryptoNative_X25519ExportSubjectPublicKeyInfo( + jobject publicKey, + uint8_t* buffer, + int32_t bufferLength, + int32_t* bytesWritten) +{ + return ExportEncodedKey(publicKey, buffer, bufferLength, bytesWritten); +} + +int32_t AndroidCryptoNative_X25519ExportPkcs8PrivateKey( + jobject privateKey, + uint8_t* buffer, + int32_t bufferLength, + int32_t* bytesWritten) +{ + return ExportEncodedKey(privateKey, buffer, bufferLength, bytesWritten); +} + +jobject AndroidCryptoNative_X25519ImportSubjectPublicKeyInfo(const uint8_t* buffer, int32_t bufferLength) +{ + abort_if_invalid_pointer_argument(buffer); + abort_if_negative_integer_argument(bufferLength); + + JNIEnv* env = GetJNIEnv(); + jobject publicKey = ImportSubjectPublicKeyInfo(env, buffer, bufferLength); + jobject ret = ToGRef(env, publicKey); + + if (CheckJNIExceptions(env) || ret == NULL) + { + ret = NULL; + } + + return ret; +} + +jobject AndroidCryptoNative_X25519ImportPkcs8PrivateKey(const uint8_t* buffer, int32_t bufferLength) +{ + abort_if_invalid_pointer_argument(buffer); + abort_if_negative_integer_argument(bufferLength); + + JNIEnv* env = GetJNIEnv(); + jobject ret = NULL; + + INIT_LOCALS(loc, algorithmName, keyFactory, pkcs8Bytes, keySpec, privateKey); + + loc[algorithmName] = make_java_string(env, "XDH"); + loc[keyFactory] = (*env)->CallStaticObjectMethod(env, g_KeyFactoryClass, g_KeyFactoryGetInstanceMethod, loc[algorithmName]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[pkcs8Bytes] = make_java_byte_array(env, bufferLength); + (*env)->SetByteArrayRegion(env, loc[pkcs8Bytes], 0, bufferLength, (const jbyte*)buffer); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[keySpec] = (*env)->NewObject(env, g_PKCS8EncodedKeySpec, g_PKCS8EncodedKeySpecCtor, loc[pkcs8Bytes]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + jbyte* pkcs8Elements = (*env)->GetByteArrayElements(env, loc[pkcs8Bytes], NULL); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (pkcs8Elements != NULL) + { + memset(pkcs8Elements, 0, (size_t)bufferLength); + (*env)->ReleaseByteArrayElements(env, loc[pkcs8Bytes], pkcs8Elements, 0); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + } + + loc[privateKey] = (*env)->CallObjectMethod(env, loc[keyFactory], g_KeyFactoryGenPrivateMethod, loc[keySpec]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (loc[privateKey] == NULL) + { + goto cleanup; + } + + ret = ToGRef(env, loc[privateKey]); + loc[privateKey] = NULL; + + if (CheckJNIExceptions(env) || ret == NULL) + { + AndroidCryptoNative_X25519DestroyKey(ret); + ret = NULL; + goto cleanup; + } + +cleanup: + RELEASE_LOCALS(loc, env); + return ret; +} + +int32_t AndroidCryptoNative_X25519DeriveSecret( + jobject privateKey, + jobject publicKey, + uint8_t* destination, + int32_t destinationLength) +{ + abort_if_invalid_pointer_argument(privateKey); + abort_if_invalid_pointer_argument(publicKey); + abort_if_invalid_pointer_argument(destination); + abort_if_negative_integer_argument(destinationLength); + + JNIEnv* env = GetJNIEnv(); + int32_t ret = FAIL; + + INIT_LOCALS(loc, algorithmName, keyAgreement, phaseResult, secret); + + loc[algorithmName] = make_java_string(env, "XDH"); + loc[keyAgreement] = (*env)->CallStaticObjectMethod(env, g_KeyAgreementClass, g_KeyAgreementGetInstance, loc[algorithmName]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + (*env)->CallVoidMethod(env, loc[keyAgreement], g_KeyAgreementInit, privateKey); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[phaseResult] = (*env)->CallObjectMethod(env, loc[keyAgreement], g_KeyAgreementDoPhase, publicKey, JNI_TRUE); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[secret] = (jbyteArray)(*env)->CallObjectMethod(env, loc[keyAgreement], g_KeyAgreementGenerateSecret); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (loc[secret] == NULL) + { + goto cleanup; + } + + jsize secretLen = (*env)->GetArrayLength(env, loc[secret]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (secretLen != destinationLength) + { + goto cleanup; + } + + (*env)->GetByteArrayRegion(env, loc[secret], 0, secretLen, (jbyte*)destination); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + ret = SUCCESS; + +cleanup: + RELEASE_LOCALS(loc, env); + return ret; +} + +int32_t AndroidCryptoNative_X25519DeriveSecretWithSubjectPublicKeyInfo( + jobject privateKey, + const uint8_t* buffer, + int32_t bufferLength, + uint8_t* destination, + int32_t destinationLength) +{ + abort_if_invalid_pointer_argument(privateKey); + abort_if_invalid_pointer_argument(buffer); + abort_if_invalid_pointer_argument(destination); + abort_if_negative_integer_argument(bufferLength); + abort_if_negative_integer_argument(destinationLength); + + JNIEnv* env = GetJNIEnv(); + int32_t ret = FAIL; + + INIT_LOCALS(loc, publicKey); + + loc[publicKey] = ImportSubjectPublicKeyInfo(env, buffer, bufferLength); + + if (loc[publicKey] == NULL) + { + goto cleanup; + } + + ret = AndroidCryptoNative_X25519DeriveSecret(privateKey, loc[publicKey], destination, destinationLength); + +cleanup: + RELEASE_LOCALS(loc, env); + return ret; +} + +static int32_t ExportEncodedKey(jobject key, uint8_t* buffer, int32_t bufferLength, int32_t* bytesWritten) +{ + abort_if_invalid_pointer_argument(key); + abort_if_invalid_pointer_argument(buffer); + abort_if_invalid_pointer_argument(bytesWritten); + abort_if_negative_integer_argument(bufferLength); + + *bytesWritten = 0; + + JNIEnv* env = GetJNIEnv(); + int32_t ret = FAIL; + + INIT_LOCALS(loc, encoded); + + loc[encoded] = (jbyteArray)(*env)->CallObjectMethod(env, key, g_KeyGetEncoded); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (loc[encoded] == NULL) + { + goto cleanup; + } + + jsize encodedLen = (*env)->GetArrayLength(env, loc[encoded]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (encodedLen > bufferLength) + { + *bytesWritten = (int32_t)encodedLen; + ret = INSUFFICIENT_BUFFER; + goto cleanup; + } + + (*env)->GetByteArrayRegion(env, loc[encoded], 0, encodedLen, (jbyte*)buffer); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + *bytesWritten = (int32_t)encodedLen; + ret = SUCCESS; + +cleanup: + RELEASE_LOCALS(loc, env); + return ret; +} + +static jobject ImportSubjectPublicKeyInfo(JNIEnv* env, const uint8_t* buffer, int32_t bufferLength) +{ + jobject ret = NULL; + + INIT_LOCALS(loc, algorithmName, keyFactory, spkiBytes, keySpec, publicKey); + + loc[algorithmName] = make_java_string(env, "XDH"); + loc[keyFactory] = (*env)->CallStaticObjectMethod(env, g_KeyFactoryClass, g_KeyFactoryGetInstanceMethod, loc[algorithmName]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[spkiBytes] = make_java_byte_array(env, bufferLength); + (*env)->SetByteArrayRegion(env, loc[spkiBytes], 0, bufferLength, (const jbyte*)buffer); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[keySpec] = (*env)->NewObject(env, g_X509EncodedKeySpecClass, g_X509EncodedKeySpecCtor, loc[spkiBytes]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + loc[publicKey] = (*env)->CallObjectMethod(env, loc[keyFactory], g_KeyFactoryGenPublicMethod, loc[keySpec]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + if (loc[publicKey] == NULL) + { + goto cleanup; + } + + ret = loc[publicKey]; + loc[publicKey] = NULL; + +cleanup: + RELEASE_LOCALS(loc, env); + return ret; +} diff --git a/src/native/libs/System.Security.Cryptography.Native.Android/pal_x25519.h b/src/native/libs/System.Security.Cryptography.Native.Android/pal_x25519.h new file mode 100644 index 00000000000000..8a38c8931eb402 --- /dev/null +++ b/src/native/libs/System.Security.Cryptography.Native.Android/pal_x25519.h @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma once + +#include "pal_compiler.h" +#include "pal_jni.h" +#include "pal_types.h" + +PALEXPORT int32_t AndroidCryptoNative_X25519IsSupported(void); + +PALEXPORT void AndroidCryptoNative_X25519DestroyKey(jobject key); + +PALEXPORT int32_t AndroidCryptoNative_X25519GenerateKey(jobject* publicKey, jobject* privateKey); + +PALEXPORT int32_t AndroidCryptoNative_X25519ExportSubjectPublicKeyInfo( + jobject publicKey, + uint8_t* buffer, + int32_t bufferLength, + int32_t* bytesWritten); + +PALEXPORT int32_t AndroidCryptoNative_X25519ExportPkcs8PrivateKey( + jobject privateKey, + uint8_t* buffer, + int32_t bufferLength, + int32_t* bytesWritten); + +PALEXPORT jobject AndroidCryptoNative_X25519ImportSubjectPublicKeyInfo(const uint8_t* buffer, int32_t bufferLength); +PALEXPORT jobject AndroidCryptoNative_X25519ImportPkcs8PrivateKey(const uint8_t* buffer, int32_t bufferLength); + +PALEXPORT int32_t AndroidCryptoNative_X25519DeriveSecret( + jobject privateKey, + jobject publicKey, + uint8_t* destination, + int32_t destinationLength); + +PALEXPORT int32_t AndroidCryptoNative_X25519DeriveSecretWithSubjectPublicKeyInfo( + jobject privateKey, + const uint8_t* buffer, + int32_t bufferLength, + uint8_t* destination, + int32_t destinationLength);