File: System\Security\Cryptography\CngHelpers.cs
Web Access
Project: src\src\runtime\src\libraries\System.Security.Cryptography\src\System.Security.Cryptography.csproj (System.Security.Cryptography)
// 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.IO;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.Win32.SafeHandles;

using BCRYPT_RSAKEY_BLOB = Interop.BCrypt.BCRYPT_RSAKEY_BLOB;
using ErrorCode = Interop.NCrypt.ErrorCode;
using KeyBlobMagicNumber = Interop.BCrypt.KeyBlobMagicNumber;

namespace System.Security.Cryptography
{
    internal static partial class CngHelpers
    {
        private static readonly CngKeyBlobFormat s_cipherKeyBlobFormat = new CngKeyBlobFormat(Interop.NCrypt.NCRYPT_CIPHER_KEY_BLOB);

        /// <summary>
        /// Retrieve a well-known CNG dword property. (Note: .NET Framework compat: this helper likes to return special values
        /// rather than throw exceptions for missing or ill-formatted property values. Only use it for well-known properties that
        /// are unlikely to be ill-formatted.)
        /// </summary>
        public static int GetPropertyAsDword(this SafeNCryptHandle ncryptHandle, string propertyName, CngPropertyOptions options) =>
            GetPropertyAsPrimitive<int>(ncryptHandle, propertyName, options);

        /// <summary>
        /// Retrieve a well-known CNG pointer property. (Note: .NET Framework compat: this helper likes to return special values
        /// rather than throw exceptions for missing or ill-formatted property values. Only use it for well-known properties that
        /// are unlikely to be ill-formatted.)
        /// </summary>
        internal static IntPtr GetPropertyAsIntPtr(this SafeNCryptHandle ncryptHandle, string propertyName, CngPropertyOptions options) =>
            GetPropertyAsPrimitive<IntPtr>(ncryptHandle, propertyName, options);

        private static unsafe T GetPropertyAsPrimitive<T>(SafeNCryptHandle ncryptHandle, string propertyName, CngPropertyOptions options)
            where T : unmanaged
        {
            T value;

            ErrorCode errorCode = Interop.NCrypt.NCryptGetProperty(
                ncryptHandle,
                propertyName,
                &value,
                sizeof(T),
                out _,
                options);

            if (errorCode == ErrorCode.NTE_NOT_FOUND)
            {
                return default;
            }

            if (errorCode != ErrorCode.ERROR_SUCCESS)
            {
                throw errorCode.ToCryptographicException();
            }

            return value;
        }

        /// <summary>
        /// Note! This can and likely will throw if the algorithm was given a hardware-based key.
        /// </summary>
        internal static byte[] GetSymmetricKeyDataIfExportable(this CngKey cngKey, string algorithm)
        {
            const int SizeOf_NCRYPT_KEY_BLOB_HEADER =
                sizeof(int) + sizeof(int) + sizeof(int) + sizeof(int);

            byte[] keyBlob = cngKey.Export(s_cipherKeyBlobFormat);
            using (MemoryStream ms = new MemoryStream(keyBlob))
            {
                using (BinaryReader br = new BinaryReader(ms, Encoding.Unicode))
                {
                    // Read NCRYPT_KEY_BLOB_HEADER
                    int cbSize = br.ReadInt32();                      // NCRYPT_KEY_BLOB_HEADER.cbSize
                    if (cbSize != SizeOf_NCRYPT_KEY_BLOB_HEADER)
                        throw new CryptographicException(SR.Cryptography_KeyBlobParsingError);

                    int ncryptMagic = br.ReadInt32();                 // NCRYPT_KEY_BLOB_HEADER.dwMagic
                    if (ncryptMagic != Interop.NCrypt.NCRYPT_CIPHER_KEY_BLOB_MAGIC)
                        throw new CryptographicException(SR.Cryptography_KeyBlobParsingError);

                    int cbAlgName = br.ReadInt32();                   // NCRYPT_KEY_BLOB_HEADER.cbAlgName

                    br.ReadInt32();                                   // NCRYPT_KEY_BLOB_HEADER.cbKey

                    string algorithmName = new string(br.ReadChars((cbAlgName / 2) - 1));
                    if (algorithmName != algorithm)
                        throw new CryptographicException(SR.Format(SR.Cryptography_CngKeyWrongAlgorithm, algorithmName, algorithm));

                    char nullTerminator = br.ReadChar();
                    if (nullTerminator != 0)
                        throw new CryptographicException(SR.Cryptography_KeyBlobParsingError);

                    // Read BCRYPT_KEY_DATA_BLOB_HEADER
                    int bcryptMagic = br.ReadInt32();                 // BCRYPT_KEY_DATA_BLOB_HEADER.dwMagic
                    if (bcryptMagic != Interop.BCrypt.BCRYPT_KEY_DATA_BLOB_HEADER.BCRYPT_KEY_DATA_BLOB_MAGIC)
                        throw new CryptographicException(SR.Cryptography_KeyBlobParsingError);

                    int dwVersion = br.ReadInt32();                   // BCRYPT_KEY_DATA_BLOB_HEADER.dwVersion
                    if (dwVersion != Interop.BCrypt.BCRYPT_KEY_DATA_BLOB_HEADER.BCRYPT_KEY_DATA_BLOB_VERSION1)
                        throw new CryptographicException(SR.Cryptography_KeyBlobParsingError);

                    int keyLength = br.ReadInt32();                   // BCRYPT_KEY_DATA_BLOB_HEADER.cbKeyData
                    byte[] key = br.ReadBytes(keyLength);
                    return key;
                }
            }
        }

        internal static unsafe ArraySegment<byte> ToBCryptBlob(this in RSAParameters parameters)
        {
            if (parameters.Exponent == null || parameters.Modulus == null)
                throw new CryptographicException(SR.Cryptography_InvalidRsaParameters);

            bool includePrivate;
            if (parameters.D == null)
            {
                includePrivate = false;

                if (parameters.P != null ||
                    parameters.DP != null ||
                    parameters.Q != null ||
                    parameters.DQ != null ||
                    parameters.InverseQ != null)
                {
                    throw new CryptographicException(SR.Cryptography_InvalidRsaParameters);
                }
            }
            else
            {
                includePrivate = true;

                if (parameters.P == null ||
                    parameters.DP == null ||
                    parameters.Q == null ||
                    parameters.DQ == null ||
                    parameters.InverseQ == null)
                {
                    throw new CryptographicException(SR.Cryptography_InvalidRsaParameters);
                }

                // Half, rounded up.
                int halfModulusLength = (parameters.Modulus.Length + 1) / 2;

                // The same checks are done by RSACryptoServiceProvider on import (when building the key blob)
                // Historically RSACng let CNG handle this (reporting NTE_NOT_SUPPORTED), but on RS1 CNG let the
                // import succeed, then on private key use (e.g. signing) it would report NTE_INVALID_PARAMETER.
                //
                // Doing the check here prevents the state in RS1 where the Import succeeds, but corrupts the key,
                // and makes for a friendlier exception message.
                if (parameters.D.Length != parameters.Modulus.Length ||
                    parameters.P.Length != halfModulusLength ||
                    parameters.Q.Length != halfModulusLength ||
                    parameters.DP.Length != halfModulusLength ||
                    parameters.DQ.Length != halfModulusLength ||
                    parameters.InverseQ.Length != halfModulusLength)
                {
                    throw new CryptographicException(SR.Cryptography_InvalidRsaParameters);
                }
            }

            //
            // We need to build a key blob structured as follows:
            //
            //     BCRYPT_RSAKEY_BLOB   header
            //     byte[cbPublicExp]    publicExponent      - Exponent
            //     byte[cbModulus]      modulus             - Modulus
            //     -- Only if "includePrivate" is true --
            //     byte[cbPrime1]       prime1              - P
            //     byte[cbPrime2]       prime2              - Q
            //     ------------------
            //

            int blobSize = sizeof(BCRYPT_RSAKEY_BLOB) +
                            parameters.Exponent.Length +
                            parameters.Modulus.Length;
            if (includePrivate)
            {
                blobSize += parameters.P!.Length +
                            parameters.Q!.Length;
            }

            byte[] rsaBlob = CryptoPool.Rent(blobSize);

            fixed (byte* pRsaBlob = &rsaBlob[0])
            {
                // Build the header
                BCRYPT_RSAKEY_BLOB* pBcryptBlob = (BCRYPT_RSAKEY_BLOB*)pRsaBlob;
                pBcryptBlob->Magic = includePrivate ? KeyBlobMagicNumber.BCRYPT_RSAPRIVATE_MAGIC : KeyBlobMagicNumber.BCRYPT_RSAPUBLIC_MAGIC;
                pBcryptBlob->BitLength = parameters.Modulus.Length * 8;
                pBcryptBlob->cbPublicExp = parameters.Exponent.Length;
                pBcryptBlob->cbModulus = parameters.Modulus.Length;

                if (includePrivate)
                {
                    pBcryptBlob->cbPrime1 = parameters.P!.Length;
                    pBcryptBlob->cbPrime2 = parameters.Q!.Length;
                }
                else
                {
                    pBcryptBlob->cbPrime1 = pBcryptBlob->cbPrime2 = 0;
                }

                int offset = sizeof(BCRYPT_RSAKEY_BLOB);

                Interop.BCrypt.Emit(rsaBlob, ref offset, parameters.Exponent);
                Interop.BCrypt.Emit(rsaBlob, ref offset, parameters.Modulus);

                if (includePrivate)
                {
                    Interop.BCrypt.Emit(rsaBlob, ref offset, parameters.P!);
                    Interop.BCrypt.Emit(rsaBlob, ref offset, parameters.Q!);
                }

                // We better have computed the right allocation size above!
                Debug.Assert(offset == blobSize);
            }

            return new ArraySegment<byte>(rsaBlob, 0, blobSize);
        }

        internal static void FromBCryptBlob(
            this ref RSAParameters rsaParams,
            ReadOnlySpan<byte> rsaBlob,
            bool includePrivateParameters)
        {
            //
            // We now have a buffer laid out as follows:
            //     BCRYPT_RSAKEY_BLOB   header
            //     byte[cbPublicExp]    publicExponent      - Exponent
            //     byte[cbModulus]      modulus             - Modulus
            //     -- Private only --
            //     byte[cbPrime1]       prime1              - P
            //     byte[cbPrime2]       prime2              - Q
            //     byte[cbPrime1]       exponent1           - DP
            //     byte[cbPrime2]       exponent2           - DQ
            //     byte[cbPrime1]       coefficient         - InverseQ
            //     byte[cbModulus]      privateExponent     - D
            //

            unsafe
            {
                // Fail-fast if a rogue provider gave us a blob that isn't even the size of the blob header.
                if (rsaBlob.Length < sizeof(BCRYPT_RSAKEY_BLOB))
                    throw ErrorCode.E_FAIL.ToCryptographicException();

                fixed (byte* pRsaBlob = &rsaBlob[0])
                {
                    KeyBlobMagicNumber magic = (KeyBlobMagicNumber)Unsafe.ReadUnaligned<int>(pRsaBlob);

                    // Check the magic value in the key blob header. If the blob does not have the required magic,
                    // then throw a CryptographicException.
                    CheckMagicValueOfKey(magic, includePrivateParameters);

                    BCRYPT_RSAKEY_BLOB* pBcryptBlob = (BCRYPT_RSAKEY_BLOB*)pRsaBlob;

                    int offset = sizeof(BCRYPT_RSAKEY_BLOB);

                    // Read out the exponent
                    rsaParams.Exponent = Interop.BCrypt.Consume(rsaBlob, ref offset, pBcryptBlob->cbPublicExp);
                    rsaParams.Modulus = Interop.BCrypt.Consume(rsaBlob, ref offset, pBcryptBlob->cbModulus);

                    if (includePrivateParameters)
                    {
                        rsaParams.P = Interop.BCrypt.Consume(rsaBlob, ref offset, pBcryptBlob->cbPrime1);
                        rsaParams.Q = Interop.BCrypt.Consume(rsaBlob, ref offset, pBcryptBlob->cbPrime2);
                        rsaParams.DP = Interop.BCrypt.Consume(rsaBlob, ref offset, pBcryptBlob->cbPrime1);
                        rsaParams.DQ = Interop.BCrypt.Consume(rsaBlob, ref offset, pBcryptBlob->cbPrime2);
                        rsaParams.InverseQ = Interop.BCrypt.Consume(rsaBlob, ref offset, pBcryptBlob->cbPrime1);
                        rsaParams.D = Interop.BCrypt.Consume(rsaBlob, ref offset, pBcryptBlob->cbModulus);
                    }
                }
            }

            static void CheckMagicValueOfKey(KeyBlobMagicNumber magic, bool includePrivateParameters)
            {
                if (includePrivateParameters)
                {
                    if (magic != KeyBlobMagicNumber.BCRYPT_RSAPRIVATE_MAGIC && magic != KeyBlobMagicNumber.BCRYPT_RSAFULLPRIVATE_MAGIC)
                    {
                        throw new CryptographicException(SR.Cryptography_NotValidPrivateKey);
                    }
                }
                else
                {
                    if (magic != KeyBlobMagicNumber.BCRYPT_RSAPUBLIC_MAGIC)
                    {
                        // Private key magic is permissible too since the public key can be derived from the private key blob.
                        if (magic != KeyBlobMagicNumber.BCRYPT_RSAPRIVATE_MAGIC && magic != KeyBlobMagicNumber.BCRYPT_RSAFULLPRIVATE_MAGIC)
                        {
                            throw new CryptographicException(SR.Cryptography_NotValidPublicOrPrivateKey);
                        }
                    }
                }
            }
        }
    }
}