File: Internal\Cryptography\Pal\Windows\DecryptorPalWindows.Decrypt.cs
Web Access
Project: src\src\runtime\src\libraries\System.Security.Cryptography.Pkcs\src\System.Security.Cryptography.Pkcs.csproj (System.Security.Cryptography.Pkcs)
// 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.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;

using Microsoft.Win32.SafeHandles;

using static Interop.Crypt32;

namespace Internal.Cryptography.Pal.Windows
{
    internal sealed partial class DecryptorPalWindows : DecryptorPal
    {
        public sealed override unsafe ContentInfo? TryDecrypt(
            RecipientInfo recipientInfo,
            X509Certificate2? cert,
            AsymmetricAlgorithm? privateKey,
            X509Certificate2Collection originatorCerts,
            X509Certificate2Collection extraStore,
            out Exception? exception)
        {
            Debug.Assert((cert != null) ^ (privateKey != null));

            if (privateKey != null)
            {
                RSA? key = privateKey as RSA;

                if (key == null)
                {
                    exception = new CryptographicException(SR.Cryptography_Cms_Ktri_RSARequired);
                    return null;
                }

                ContentInfo contentInfo = _hCryptMsg.GetContentInfo();
                byte[]? cek = AnyOS.ManagedPkcsPal.ManagedKeyTransPal.DecryptCekCore(
                    cert,
                    key,
                    recipientInfo.EncryptedKey,
                    recipientInfo.KeyEncryptionAlgorithm.Oid.Value,
                    recipientInfo.KeyEncryptionAlgorithm.Parameters,
                    out exception);

                // Pin CEK to prevent it from getting copied during heap compaction.
                fixed (byte* pinnedCek = cek)
                {
                    try
                    {
                        if (exception != null)
                        {
                            return null;
                        }

                        return AnyOS.ManagedPkcsPal.ManagedDecryptorPal.TryDecryptCore(
                            cek!,
                            contentInfo.ContentType.Value!,
                            contentInfo.Content,
                            _contentEncryptionAlgorithm,
                            out exception);
                    }
                    finally
                    {
                        if (cek != null)
                        {
                            Array.Clear(cek, 0, cek.Length);
                        }
                    }
                }
            }

            Debug.Assert(recipientInfo != null);
            Debug.Assert(cert != null);
            Debug.Assert(originatorCerts != null);
            Debug.Assert(extraStore != null);

            CryptKeySpec keySpec;
            exception = TryGetKeySpecForCertificate(cert, out keySpec);
            if (exception != null)
                return null;

            // .NET Framework compat: We pass false for "silent" here (thus allowing crypto providers to display UI.)
            const bool Silent = false;
            // Note: Using CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG rather than CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG
            // because wrapping an NCrypt wrapper over CAPI keys unconditionally causes some legacy features
            // (such as RC4 support) to break.
            const bool PreferNCrypt = false;
            using (SafeProvOrNCryptKeyHandle? hKey = PkcsPalWindows.GetCertificatePrivateKey(cert, Silent, PreferNCrypt, out _, out exception))
            {
                if (hKey == null)
                    return null;

                RecipientInfoType type = recipientInfo.Type;
                switch (type)
                {
                    case RecipientInfoType.KeyTransport:
                        exception = TryDecryptTrans((KeyTransRecipientInfo)recipientInfo, hKey, keySpec);
                        break;

                    case RecipientInfoType.KeyAgreement:
                        exception = TryDecryptAgree((KeyAgreeRecipientInfo)recipientInfo, hKey, keySpec, originatorCerts, extraStore);
                        break;

                    default:
                        // Since only the framework can construct RecipientInfo's, we're at fault if we get here. So it's okay to assert and throw rather than
                        // returning to the caller.
                        Debug.Fail($"Unexpected RecipientInfoType: {type}");
                        throw new NotSupportedException();
                }

                if (exception != null)
                    return null;

                // If we got here, we successfully decrypted. Return the decrypted content.
                return _hCryptMsg.GetContentInfo();
            }
        }

        private static CryptographicException? TryGetKeySpecForCertificate(X509Certificate2 cert, out CryptKeySpec keySpec)
        {
            using (SafeCertContextHandle hCertContext = cert.CreateCertContextHandle())
            {
                int cbSize = 0;

                if (Interop.Crypt32.CertGetCertificateContextProperty(
                    hCertContext,
                    CertContextPropId.CERT_NCRYPT_KEY_HANDLE_PROP_ID,
                    null,
                    ref cbSize))
                {
                    keySpec = CryptKeySpec.CERT_NCRYPT_KEY_SPEC;
                    return null;
                }

                if (!Interop.Crypt32.CertGetCertificateContextProperty(hCertContext, CertContextPropId.CERT_KEY_PROV_INFO_PROP_ID, null, ref cbSize))
                {
                    ErrorCode errorCode = (ErrorCode)(Marshal.GetLastPInvokeError());
                    keySpec = default(CryptKeySpec);
                    return errorCode.ToCryptographicException();
                }

                byte[] pData = new byte[cbSize];
                unsafe
                {
                    fixed (byte* pvData = pData)
                    {
                        if (!Interop.Crypt32.CertGetCertificateContextProperty(hCertContext, CertContextPropId.CERT_KEY_PROV_INFO_PROP_ID, pData, ref cbSize))
                        {
                            ErrorCode errorCode = (ErrorCode)(Marshal.GetLastPInvokeError());
                            keySpec = default(CryptKeySpec);
                            return errorCode.ToCryptographicException();
                        }

                        CRYPT_KEY_PROV_INFO* pCryptKeyProvInfo = (CRYPT_KEY_PROV_INFO*)pvData;
                        keySpec = pCryptKeyProvInfo->dwKeySpec;
                        return null;
                    }
                }
            }
        }

        private unsafe CryptographicException? TryDecryptTrans(KeyTransRecipientInfo recipientInfo, SafeProvOrNCryptKeyHandle hKey, CryptKeySpec keySpec)
        {
            KeyTransRecipientInfoPalWindows pal = (KeyTransRecipientInfoPalWindows)(recipientInfo.Pal);

            bool keyAddRefd = false;

            try
            {
                CMSG_CTRL_DECRYPT_PARA decryptPara;
                decryptPara.cbSize = sizeof(CMSG_CTRL_DECRYPT_PARA);
                hKey.DangerousAddRef(ref keyAddRefd);
                decryptPara.hKey = hKey.DangerousGetHandle();
                decryptPara.dwKeySpec = keySpec;
                decryptPara.dwRecipientIndex = pal.Index;

                bool success = Interop.Crypt32.CryptMsgControl(_hCryptMsg, 0, MsgControlType.CMSG_CTRL_DECRYPT, ref decryptPara);
                if (!success)
                    return Marshal.GetHRForLastWin32Error().ToCryptographicException();

                return null;
            }
            finally
            {
                if (keyAddRefd)
                {
                    hKey.DangerousRelease();
                }
            }
        }

        private Exception? TryDecryptAgree(KeyAgreeRecipientInfo keyAgreeRecipientInfo, SafeProvOrNCryptKeyHandle hKey, CryptKeySpec keySpec, X509Certificate2Collection originatorCerts, X509Certificate2Collection extraStore)
        {
            unsafe
            {
                KeyAgreeRecipientInfoPalWindows pal = (KeyAgreeRecipientInfoPalWindows)(keyAgreeRecipientInfo.Pal);
                return pal.WithCmsgCmsRecipientInfo<Exception?>(
                    delegate (CMSG_KEY_AGREE_RECIPIENT_INFO* pKeyAgreeRecipientInfo)
                    {
                        bool keyAddRefd = false;
                        try
                        {
                            CMSG_CTRL_KEY_AGREE_DECRYPT_PARA decryptPara = default(CMSG_CTRL_KEY_AGREE_DECRYPT_PARA);
                            decryptPara.cbSize = sizeof(CMSG_CTRL_KEY_AGREE_DECRYPT_PARA);
                            hKey.DangerousAddRef(ref keyAddRefd);
                            decryptPara.hProv = hKey.DangerousGetHandle();
                            decryptPara.dwKeySpec = keySpec;
                            decryptPara.pKeyAgree = pKeyAgreeRecipientInfo;
                            decryptPara.dwRecipientIndex = pal.Index;
                            decryptPara.dwRecipientEncryptedKeyIndex = pal.SubIndex;
                            CMsgKeyAgreeOriginatorChoice originatorChoice = pKeyAgreeRecipientInfo->dwOriginatorChoice;
                            switch (originatorChoice)
                            {
                                case CMsgKeyAgreeOriginatorChoice.CMSG_KEY_AGREE_ORIGINATOR_CERT:
                                    {
                                        X509Certificate2Collection candidateCerts = new X509Certificate2Collection();
                                        candidateCerts.AddRange(PkcsHelpers.GetStoreCertificates(StoreName.AddressBook, StoreLocation.CurrentUser, openExistingOnly: true));
                                        candidateCerts.AddRange(PkcsHelpers.GetStoreCertificates(StoreName.AddressBook, StoreLocation.LocalMachine, openExistingOnly: true));
                                        candidateCerts.AddRange(originatorCerts);
                                        candidateCerts.AddRange(extraStore);
                                        SubjectIdentifier originatorId = pKeyAgreeRecipientInfo->OriginatorCertId.ToSubjectIdentifier();
                                        X509Certificate2? originatorCert = candidateCerts.TryFindMatchingCertificate(originatorId);
                                        if (originatorCert == null)
                                            return ErrorCode.CRYPT_E_NOT_FOUND.ToCryptographicException();
                                        using (SafeCertContextHandle hCertContext = originatorCert.CreateCertContextHandle())
                                        {
                                            CERT_CONTEXT* pOriginatorCertContext = hCertContext.DangerousGetCertContext();
                                            decryptPara.OriginatorPublicKey = pOriginatorCertContext->pCertInfo->SubjectPublicKeyInfo.PublicKey;

                                            // Do not factor this call out of the switch statement as leaving this "using" block will free up
                                            // native memory that decryptPara points to.
                                            return TryExecuteDecryptAgree(ref decryptPara);
                                        }
                                    }

                                case CMsgKeyAgreeOriginatorChoice.CMSG_KEY_AGREE_ORIGINATOR_PUBLIC_KEY:
                                    {
                                        decryptPara.OriginatorPublicKey = pKeyAgreeRecipientInfo->OriginatorPublicKeyInfo.PublicKey;
                                        return TryExecuteDecryptAgree(ref decryptPara);
                                    }

                                default:
                                    return new CryptographicException(SR.Format(SR.Cryptography_Cms_Invalid_Originator_Identifier_Choice, originatorChoice));
                            }
                        }
                        finally
                        {
                            if (keyAddRefd)
                            {
                                hKey.DangerousRelease();
                            }
                        }
                    });
            }
        }

        private CryptographicException? TryExecuteDecryptAgree(ref CMSG_CTRL_KEY_AGREE_DECRYPT_PARA decryptPara)
        {
            if (!Interop.Crypt32.CryptMsgControl(_hCryptMsg, 0, MsgControlType.CMSG_CTRL_KEY_AGREE_DECRYPT, ref decryptPara))
            {
                ErrorCode errorCode = (ErrorCode)(Marshal.GetHRForLastWin32Error());
                return errorCode.ToCryptographicException();
            }
            return null;
        }
    }
}