2

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();
            }

        }
    }
Jonas Stensved
  • 14,378
  • 5
  • 51
  • 80
  • By putting it into a dictionary? I'm not sure what you're asking here. – Robert Harvey Feb 21 '22 at 13:48
  • Perhaps you must use CngKey.Open before? – J.Salas Feb 21 '22 at 13:56
  • The keystore is managed outside of the application, instead of MicrosoftSoftwareKeyStorageProvider a TPM or other hardware device can be used. This makes the key store secure rater than saving it. Generating a key and storing it works. But I'm asking now I can use an existing key rather than generating? – Jonas Stensved Feb 21 '22 at 21:12

1 Answers1

3

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.

bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • Thank you very much for providing a working example! I agree it's not so nice, with the NCrypt way, I was hoping for a "clean" Cng way but it's at least doable which is very nice. – Jonas Stensved Mar 29 '22 at 19:33