'Store a known AES key with CngKey by name for later use

I want to store a known AES key (retrieved offline) by entering it into the application once, saving it to Cng for storage, then reference it only by name on subsequent use.

I want to save the key in the Key Storage Provider so my application won't load it into memory.

I can create (generate) a AES key (that I can retreive and create an instance of AesCng with) like this:

CngProvider keyStorageProvider = CngProvider.MicrosoftSoftwareKeyStorageProvider;
CngKeyCreationParameters keyCreationParameters = new CngKeyCreationParameters()
{
    ExportPolicy = CngExportPolicies.AllowPlaintextExport,
    KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey
};
var name = "mykey";
var algo = new CngAlgorithm("AES");
var created = CngKey.Create(algo, name, keyCreationParameters);

But how can I add my already known AES symmetric key and just reference it by name the next time I run my application to run encryption/decryption using Cng?

Using CngKey.Import won't let me specify a name and I think I've tried all overloads but all yield some kind of error.

UPDATE:

This is a complete working example when creating a key.

    // Calling code
    byte[] key = //<from external input>;
    byte[] data = new byte[] { 0xA, 0xB, 0xC, 0xD };
    crypto.StoreKey("appkey", key);
    var encryptedData = crypto.EncryptWithStoredKey("appkey", data);

    // Implementation
    public void StoreKey(string name, byte[] key)
    {

        CngKeyCreationParameters keyCreationParameters = new CngKeyCreationParameters()
        {
            KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey
        };

        var algo = new CngAlgorithm("AES");

        // Question: How can I import the byte[] key with name "appkey" instead of generating a new key here?
        CngKey.Create(algo, name, keyCreationParameters);
    }

    public byte[] EncryptWithStoredKey(string name, byte[] data)
    {
        using (var cng = new AesCng(name))
        using (var encryptor = cng.CreateEncryptor())
        using (var memoryStream = new MemoryStream())
        {
            using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
            {
                cryptoStream.Write(data, 0, data.Length);
                cryptoStream.FlushFinalBlock();
                return memoryStream.ToArray();
            }

        }
    }


Solution 1:[1]

It's... not nice. But it's technically doable. Reusing some of the import definitions from X509Certificate2.Import with NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG:

private static unsafe CngKey ImportPersistedAesKey(string keyName, byte[] key)
{
    switch (key.Length * 8)
    {
        case 128:
        case 192:
        case 256:
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(key));
    }

    byte[] blob = new byte[s_cipherKeyBlobPrefix.Length + key.Length];
    Buffer.BlockCopy(s_cipherKeyBlobPrefix, 0, blob, 0, s_cipherKeyBlobPrefix.Length);
    blob[12] = (byte)(12 /* sizeof(BCRYPT_KEY_DATA_BLOB_HEADER) */ + key.Length);
    blob[32] = (byte)key.Length;
    Buffer.BlockCopy(key, 0, blob, s_cipherKeyBlobPrefix.Length, key.Length);

    fixed (char* keyNamePtr = keyName)
    fixed (byte* blobPtr = blob)
    {
        NativeMethods.NCrypt.NCryptBuffer nameBuf;
        nameBuf.BufferType = NativeMethods.NCrypt.BufferType.PkcsName;
        nameBuf.cbBuffer = (keyName.Length + 1) * 2;
        nameBuf.pvBuffer = (IntPtr)keyNamePtr;

        NativeMethods.NCrypt.NCryptBufferDesc bufferDesc;
        bufferDesc.ulVersion = 0;
        bufferDesc.cBuffers = 1;
        bufferDesc.pBuffers = (IntPtr)(&nameBuf);

        int ret = NativeMethods.NCrypt.NCryptOpenStorageProvider(
            out SafeNCryptProviderHandle hProv,
            CngProvider.MicrosoftSoftwareKeyStorageProvider.Provider,
            0);

        using (hProv)
        {
            if (ret != 0)
            {
                throw new Win32Exception(ret);
            }

            ret = NativeMethods.NCrypt.NCryptImportKey(
                hProv,
                IntPtr.Zero,
                "CipherKeyBlob",
                ref bufferDesc,
                out SafeNCryptKeyHandle hKey,
                (IntPtr)blobPtr,
                blob.Length,
                NativeMethods.NCrypt.NCryptImportFlags.NCRYPT_OVERWRITE_KEY_FLAG);

            using (hKey)
            {
                if (ret != 0)
                {
                    throw new Win32Exception(ret);
                }

                return CngKey.Open(hKey, CngKeyHandleOpenOptions.None);
            }
        }
    }
}

private static byte[] s_cipherKeyBlobPrefix = {
    // NCRYPT_KEY_BLOB_HEADER.cbSize (16)
    0x10, 0x00, 0x00, 0x00,
    // NCRYPT_KEY_BLOB_HEADER.dwMagic (NCRYPT_CIPHER_KEY_BLOB_MAGIC (0x52485043))
    0x43, 0x50, 0x48, 0x52,
    // NCRYPT_KEY_BLOB_HEADER.cbAlgName (8)
    0x08, 0x00, 0x00, 0x00,
    // NCRYPT_KEY_BLOB_HEADER.cbKeyData (to be determined)
    0x00, 0x00, 0x00, 0x00,
    // UTF16-LE "AES\0"
    0x41, 0x00, 0x45, 0x00, 0x53, 0x00, 0x00, 0x00,
    // BCRYPT_KEY_DATA_BLOB_HEADER.dwMagic (BCRYPT_KEY_DATA_BLOB_MAGIC (0x4D42444B))
    0x4B, 0x44, 0x42, 0x4D,
    // BCRYPT_KEY_DATA_BLOB_HEADER.dwVersion (1)
    0x01, 0x00, 0x00, 0x00,
    // BCRYPT_KEY_DATA_BLOB_HEADER.cbKeyData (to be determined)
    0x00, 0x00, 0x00, 0x00
};

internal static class NativeMethods
{
    internal static class NCrypt
    {
        [StructLayout(LayoutKind.Sequential)]
        internal struct NCryptBufferDesc
        {
            public int ulVersion;
            public int cBuffers;
            public IntPtr pBuffers;
        }

        [StructLayout(LayoutKind.Sequential)]
        internal struct NCryptBuffer
        {
            public int cbBuffer;
            public BufferType BufferType;
            public IntPtr pvBuffer;
        }

        internal enum BufferType
        {
            PkcsName = 45,
        }

        [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
        internal static extern int NCryptOpenStorageProvider(
            out SafeNCryptProviderHandle phProvider,
            string pszProviderName,
            int dwFlags);

        internal enum NCryptImportFlags
        {
            None = 0,
            NCRYPT_MACHINE_KEY_FLAG = 0x00000020,
            NCRYPT_OVERWRITE_KEY_FLAG = 0x00000080,
            NCRYPT_DO_NOT_FINALIZE_FLAG = 0x00000400,
        }

        [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
        internal static extern int NCryptImportKey(
            SafeNCryptProviderHandle hProvider,
            IntPtr hImportKey,
            string pszBlobType,
            ref NCryptBufferDesc pParameterList,
            out SafeNCryptKeyHandle phKey,
            IntPtr pbData,
            int cbData,
            NCryptImportFlags dwFlags);

        [DllImport("ncrypt.dll", CharSet = CharSet.Unicode)]
        internal static extern int NCryptFinalizeKey(SafeNCryptKeyHandle hKey, int dwFlags);
    }
}

The expected flow is that you should be able to create a key and then use NCryptSetKeyProperty to set the NCRYPT_CIPHER_KEY_BLOB property for the NCRYPT_CIPHER_KEY_BLOB payload (from the .NET perspective that'd be you call CngKey.Create with creation parameters and specify it as a creation property). But it looks like CNG didn't wire that up for persisted symmetric keys, so you have to use the complicated form of NCryptImportKey directly.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 bartonjs