File: System\Security\Cryptography\Pkcs\EnvelopedCms.cs
Web Access
Project: src\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.Security.Cryptography.X509Certificates;
 
using Internal.Cryptography;
 
namespace System.Security.Cryptography.Pkcs
{
    public sealed class EnvelopedCms
    {
        //
        // Constructors
        //
 
        public EnvelopedCms()
            : this(new ContentInfo(Array.Empty<byte>()))
        {
        }
 
        public EnvelopedCms(ContentInfo contentInfo)
            : this(contentInfo, new AlgorithmIdentifier(Oids.Aes256CbcOid.CopyOid()))
        {
        }
 
        public EnvelopedCms(ContentInfo contentInfo, AlgorithmIdentifier encryptionAlgorithm)
        {
            if (contentInfo is null)
            {
                throw new ArgumentNullException(nameof(contentInfo));
            }
            if (encryptionAlgorithm is null)
            {
                throw new ArgumentNullException(nameof(encryptionAlgorithm));
            }
 
            Version = 0;  // It makes little sense to ask for a version before you've decoded, but since the .NET Framework returns 0 in that case, we will too.
            ContentInfo = contentInfo;
            ContentEncryptionAlgorithm = encryptionAlgorithm;
            Certificates = new X509Certificate2Collection();
            UnprotectedAttributes = new CryptographicAttributeObjectCollection();
            _decryptorPal = null;
            _lastCall = LastCall.Ctor;
        }
 
        //
        // Latched properties.
        //     - For senders, the caller sets their values and they act as implicit inputs to the Encrypt() method.
        //     - For recipients, the Decode() and Decrypt() methods reset their values and the caller can inspect them if desired.
        //
 
        public int Version { get; private set; }
        public ContentInfo ContentInfo { get; private set; }
        public AlgorithmIdentifier ContentEncryptionAlgorithm { get; private set; }
        public X509Certificate2Collection Certificates { get; private set; }
        public CryptographicAttributeObjectCollection UnprotectedAttributes { get; private set; }
 
        //
        // Recipients invoke this property to retrieve the recipient information after a Decode() operation.
        // Senders should not invoke this property.
        //
        public RecipientInfoCollection RecipientInfos
        {
            get
            {
                switch (_lastCall)
                {
                    case LastCall.Ctor:
                        return new RecipientInfoCollection();
 
                    case LastCall.Encrypt:
                        throw PkcsPal.Instance.CreateRecipientInfosAfterEncryptException();
 
                    case LastCall.Decode:
                    case LastCall.Decrypt:
                        Debug.Assert(_decryptorPal != null);
                        return _decryptorPal.RecipientInfos;
 
                    default:
                        Debug.Fail($"Unexpected _lastCall value: {_lastCall}");
                        throw new InvalidOperationException();
                }
            }
        }
 
        //
        // Encrypt() overloads. Senders invoke this to encrypt and encode a CMS. Afterward, invoke the Encode() method to retrieve the actual encoding.
        //
        public void Encrypt(CmsRecipient recipient)
        {
            if (recipient is null)
            {
                throw new ArgumentNullException(nameof(recipient));
            }
 
            Encrypt(new CmsRecipientCollection(recipient));
        }
 
        public void Encrypt(CmsRecipientCollection recipients)
        {
            if (recipients is null)
            {
                throw new ArgumentNullException(nameof(recipients));
            }
 
            // .NET Framework compat note: Unlike the desktop, we don't provide a free UI to select the recipient. The app must give it to us programmatically.
            if (recipients.Count == 0)
                throw new PlatformNotSupportedException(SR.Cryptography_Cms_NoRecipients);
 
            if (_decryptorPal != null)
            {
                _decryptorPal.Dispose();
                _decryptorPal = null;
            }
            _encodedMessage = PkcsPal.Instance.Encrypt(recipients, ContentInfo, ContentEncryptionAlgorithm, Certificates, UnprotectedAttributes);
            _lastCall = LastCall.Encrypt;
        }
 
        //
        // Senders invoke Encode() after a successful Encrypt() to retrieve the RFC-compliant on-the-wire representation.
        //
        public byte[] Encode()
        {
            if (_encodedMessage == null)
                throw new InvalidOperationException(SR.Cryptography_Cms_MessageNotEncrypted);
 
            return _encodedMessage.CloneByteArray();
        }
 
        //
        // Recipients invoke Decode() to turn the on-the-wire representation into a usable EnvelopedCms instance. Next step is to call Decrypt().
        //
        public void Decode(byte[] encodedMessage)
        {
            if (encodedMessage is null)
            {
                throw new ArgumentNullException(nameof(encodedMessage));
            }
 
            Decode(new ReadOnlySpan<byte>(encodedMessage));
        }
 
        /// <summary>
        ///   Decodes the provided data as a CMS/PKCS#7 EnvelopedData message.
        /// </summary>
        /// <param name="encodedMessage">
        ///   The data to decode.
        /// </param>
        /// <exception cref="CryptographicException">
        ///   The <paramref name="encodedMessage"/> parameter was not successfully decoded.
        /// </exception>
        public void Decode(ReadOnlySpan<byte> encodedMessage)
        {
            if (_decryptorPal != null)
            {
                _decryptorPal.Dispose();
                _decryptorPal = null;
            }
 
            int version;
            ContentInfo contentInfo;
            AlgorithmIdentifier contentEncryptionAlgorithm;
            X509Certificate2Collection originatorCerts;
            CryptographicAttributeObjectCollection unprotectedAttributes;
            _decryptorPal = PkcsPal.Instance.Decode(encodedMessage, out version, out contentInfo, out contentEncryptionAlgorithm, out originatorCerts, out unprotectedAttributes);
            Version = version;
            ContentInfo = contentInfo;
            ContentEncryptionAlgorithm = contentEncryptionAlgorithm;
            Certificates = originatorCerts;
            UnprotectedAttributes = unprotectedAttributes;
 
            // .NET Framework compat: Encode() after a Decode() returns you the same thing that ContentInfo.Content does.
            _encodedMessage = contentInfo.Content.CloneByteArray();
 
            _lastCall = LastCall.Decode;
        }
 
        //
        // Decrypt() overloads: Recipients invoke Decrypt() after a successful Decode(). Afterwards, invoke ContentInfo to get the decrypted content.
        //
        public void Decrypt()
        {
            DecryptContent(RecipientInfos, null);
        }
 
        public void Decrypt(RecipientInfo recipientInfo)
        {
            if (recipientInfo is null)
            {
                throw new ArgumentNullException(nameof(recipientInfo));
            }
 
            DecryptContent(new RecipientInfoCollection(recipientInfo), null);
        }
 
        public void Decrypt(RecipientInfo recipientInfo, X509Certificate2Collection extraStore)
        {
            if (recipientInfo is null)
            {
                throw new ArgumentNullException(nameof(recipientInfo));
            }
            if (extraStore is null)
            {
                throw new ArgumentNullException(nameof(extraStore));
            }
 
            DecryptContent(new RecipientInfoCollection(recipientInfo), extraStore);
        }
 
        public void Decrypt(X509Certificate2Collection extraStore)
        {
            if (extraStore is null)
            {
                throw new ArgumentNullException(nameof(extraStore));
            }
 
            DecryptContent(RecipientInfos, extraStore);
        }
 
        public void Decrypt(RecipientInfo recipientInfo, AsymmetricAlgorithm? privateKey)
        {
            if (recipientInfo is null)
            {
                throw new ArgumentNullException(nameof(recipientInfo));
            }
 
            CheckStateForDecryption();
 
            X509Certificate2Collection extraStore = new X509Certificate2Collection();
            ContentInfo? contentInfo = _decryptorPal!.TryDecrypt(
                recipientInfo,
                null,
                privateKey,
                Certificates,
                extraStore,
                out Exception? exception);
 
            if (exception != null)
                throw exception;
 
            SetContentInfo(contentInfo!);
        }
 
        private void DecryptContent(RecipientInfoCollection recipientInfos, X509Certificate2Collection? extraStore)
        {
            CheckStateForDecryption();
            extraStore ??= new X509Certificate2Collection();
 
            X509Certificate2Collection certs = new X509Certificate2Collection();
            PkcsPal.Instance.AddCertsFromStoreForDecryption(certs);
            certs.AddRange(extraStore);
 
            X509Certificate2Collection originatorCerts = Certificates;
 
            ContentInfo? newContentInfo = null;
            Exception? exception = PkcsPal.Instance.CreateRecipientsNotFoundException();
            foreach (RecipientInfo recipientInfo in recipientInfos)
            {
                X509Certificate2? cert = certs.TryFindMatchingCertificate(recipientInfo.RecipientIdentifier);
                if (cert == null)
                {
                    exception = PkcsPal.Instance.CreateRecipientsNotFoundException();
                    continue;
                }
 
                newContentInfo = _decryptorPal!.TryDecrypt(
                    recipientInfo,
                    cert,
                    null,
                    originatorCerts,
                    extraStore,
                    out exception);
 
                if (exception != null)
                    continue;
 
                break;
            }
 
            if (exception != null)
                throw exception;
 
            SetContentInfo(newContentInfo!);
        }
 
        private void CheckStateForDecryption()
        {
            switch (_lastCall)
            {
                case LastCall.Ctor:
                    throw new InvalidOperationException(SR.Cryptography_Cms_MessageNotEncrypted);
 
                case LastCall.Encrypt:
                    throw PkcsPal.Instance.CreateDecryptAfterEncryptException();
 
                case LastCall.Decrypt:
                    throw PkcsPal.Instance.CreateDecryptTwiceException();
 
                case LastCall.Decode:
                    break; // This is the expected state.
 
                default:
                    Debug.Fail($"Unexpected _lastCall value: {_lastCall}");
                    throw new InvalidOperationException();
            }
        }
 
        private void SetContentInfo(ContentInfo contentInfo)
        {
            ContentInfo = contentInfo;
 
            // .NET Framework compat: Encode() after a Decrypt() returns you the same thing that ContentInfo.Content does.
            _encodedMessage = contentInfo.Content.CloneByteArray();
 
            _lastCall = LastCall.Decrypt;
        }
 
        //
        // Instance fields
        //
 
        private DecryptorPal? _decryptorPal;
        private byte[]? _encodedMessage;
        private LastCall _lastCall;
 
        private enum LastCall
        {
            Ctor = 1,
            Encrypt = 2,
            Decode = 3,
            Decrypt = 4,
        }
    }
}