File: System\Security\Cryptography\ProtectedData.cs
Web Access
Project: src\src\libraries\System.Security.Cryptography.ProtectedData\src\System.Security.Cryptography.ProtectedData.csproj (System.Security.Cryptography.ProtectedData)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Runtime.InteropServices;
using Internal.Cryptography;
using CryptProtectDataFlags = Interop.Crypt32.CryptProtectDataFlags;
using DATA_BLOB = Interop.Crypt32.DATA_BLOB;
 
namespace System.Security.Cryptography
{
    public static partial class ProtectedData
    {
        private static readonly byte[] s_nonEmpty = new byte[1];
 
        public static byte[] Protect(byte[] userData, byte[]? optionalEntropy, DataProtectionScope scope)
        {
            CheckPlatformSupport();
 
            if (userData is null)
                throw new ArgumentNullException(nameof(userData));
 
            return ProtectOrUnprotect(userData, optionalEntropy, scope, protect: true);
        }
 
        public static byte[] Unprotect(byte[] encryptedData, byte[]? optionalEntropy, DataProtectionScope scope)
        {
            CheckPlatformSupport();
 
            if (encryptedData is null)
                throw new ArgumentNullException(nameof(encryptedData));
 
            return ProtectOrUnprotect(encryptedData, optionalEntropy, scope, protect: false);
        }
 
        private static byte[] ProtectOrUnprotect(byte[] inputData, byte[]? optionalEntropy, DataProtectionScope scope, bool protect)
        {
            unsafe
            {
                // The Win32 API will reject pbData == nullptr, and the fixed statement
                // maps empty arrays to nullptr... so when the input is empty use the address of a
                // different array, but still assign cbData to 0.
                byte[] relevantData = inputData.Length == 0 ? s_nonEmpty : inputData;
 
                fixed (byte* pInputData = relevantData, pOptionalEntropy = optionalEntropy)
                {
                    DATA_BLOB userDataBlob = new DATA_BLOB((IntPtr)pInputData, (uint)(inputData.Length));
                    DATA_BLOB optionalEntropyBlob = default(DATA_BLOB);
                    if (optionalEntropy != null)
                    {
                        optionalEntropyBlob = new DATA_BLOB((IntPtr)pOptionalEntropy, (uint)(optionalEntropy.Length));
                    }
 
                    // For .NET Framework compat, we ignore unknown bits in the "scope" value rather than throwing.
                    CryptProtectDataFlags flags = CryptProtectDataFlags.CRYPTPROTECT_UI_FORBIDDEN;
                    if (scope == DataProtectionScope.LocalMachine)
                    {
                        flags |= CryptProtectDataFlags.CRYPTPROTECT_LOCAL_MACHINE;
                    }
 
                    DATA_BLOB outputBlob = default(DATA_BLOB);
                    try
                    {
                        bool success = protect ?
                            Interop.Crypt32.CryptProtectData(in userDataBlob, null, ref optionalEntropyBlob, IntPtr.Zero, IntPtr.Zero, flags, out outputBlob) :
                            Interop.Crypt32.CryptUnprotectData(in userDataBlob, IntPtr.Zero, ref optionalEntropyBlob, IntPtr.Zero, IntPtr.Zero, flags, out outputBlob);
                        if (!success)
                        {
#if NET
                            int lastWin32Error = Marshal.GetLastPInvokeError();
#else
                            int lastWin32Error = Marshal.GetLastWin32Error();
#endif
                            if (protect && ErrorMayBeCausedByUnloadedProfile(lastWin32Error))
                                throw new CryptographicException(SR.Cryptography_DpApi_ProfileMayNotBeLoaded);
                            else
                                throw lastWin32Error.ToCryptographicException();
                        }
 
                        // In some cases, the API would fail due to OOM but simply return a null pointer.
                        if (outputBlob.pbData == IntPtr.Zero)
                            throw new OutOfMemoryException();
 
                        int length = (int)(outputBlob.cbData);
                        byte[] outputBytes = new byte[length];
                        Marshal.Copy(outputBlob.pbData, outputBytes, 0, length);
                        return outputBytes;
                    }
                    finally
                    {
                        if (outputBlob.pbData != IntPtr.Zero)
                        {
                            int length = (int)(outputBlob.cbData);
                            byte* pOutputData = (byte*)(outputBlob.pbData);
                            for (int i = 0; i < length; i++)
                            {
                                pOutputData[i] = 0;
                            }
                            Marshal.FreeHGlobal(outputBlob.pbData);
                        }
                    }
                }
            }
        }
 
        // Determine if an error code may have been caused by trying to do a crypto operation while the
        // current user's profile is not yet loaded.
        private static bool ErrorMayBeCausedByUnloadedProfile(int errorCode)
        {
            // CAPI returns a file not found error if the user profile is not yet loaded
            return errorCode == HResults.E_FILENOTFOUND ||
                   errorCode == Interop.Errors.ERROR_FILE_NOT_FOUND;
        }
 
        private static void CheckPlatformSupport()
        {
            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                throw new PlatformNotSupportedException();
            }
        }
    }
}