EternalWindows
CNG / 秘密協定

対象鍵を利用した暗号化をネットワーク上の相手と行う場合は、 対象鍵の交換という大きな課題を乗り越えなければなりません。 対象鍵では、暗号化と複合化に同じ鍵を使うことから、 この鍵を第三者に傍受されてしまっては、通信データの中身は容易に理解されてしまうからです。 このような問題を回避するためには、DHやECDHなどの鍵交換アルゴリズムを利用し、 通信の両者以外が同じ対象鍵を取得できないような仕組みが必要となります。 次に、通信の両者をそれぞれAとBに命名し、DHによる鍵交換の手順を簡単に示します。

1, Aは秘密鍵と公開鍵を作成する。
2, Bも秘密鍵と公開鍵を作成する。
3, AはBの公開鍵を取得し、Aの秘密鍵とBの公開鍵から対象鍵を作成するための値を算出する。
4, BはAの公開鍵を取得し、Bの秘密鍵とAの公開鍵から対象鍵を作成するための値を算出する。

この例でAが算出した値とBが算出した値はそれぞれ同一となります。 同じ値から作成される対象鍵は常に同一となるため、 AとBは互いに共通の対象鍵を取得することができます。 この手順を成功させるためには、俗にpと呼ばれる素数とqと呼ばれる原始根をAとBが 予め共有している必要がありますが、この部分はCNGによって隠蔽されているため、 深く意識する必要はありません。 また、ECDHは、鍵の算出における演算式に楕円曲線の離散対数問題を使用するだけで、 基本的な手順自体はDHと同様です。

CNGでは、先の例の秘密鍵と公開鍵から算出した値を秘密協定(SecretAgreement)と呼んでいます。 秘密協定を作成するには、BCryptSecretAgreementを呼び出します。

NTSTATUS WINAPI BCryptSecretAgreement(
  BCRYPT_KEY_HANDLE hPrivKey,
  BCRYPT_KEY_HANDLE hPubKey,
  BCRYPT_SECRET_HANDLE *phSecret,
  ULONG dwFlags
);

hPrivKeyは、秘密協定を作成するために使う秘密鍵のハンドルを指定します。 鍵ペアのハンドルを指定した場合、秘密鍵を指定したものと解釈されます。 hPubKeyは、秘密協定を作成するために使う公開鍵のハンドルを指定します。 この公開鍵は、通信相手から取得したものを指定するはずです。 phSecretは、秘密協定のハンドルを受け取る変数のアドレスを指定します。 dwFlagsは、現在は使用されないため0を指定します。

秘密協定を表すハンドルから対象鍵を作成するめには、 秘密協定から鍵マテリアルを派生させる必要があります。 鍵マテリアルというのは、簡単に述べれば秘密協定のハッシュ値です。 このハッシュ値から対象鍵を作成することになります。 秘密協定から鍵マテリアルを派生するには、BCryptDeriveKeyを呼び出します。

NTSTATUS WINAPI BCryptDeriveKey(
  BCRYPT_SECRET_HANDLE hSharedSecret,
  LPCWSTR pwszKDF,
  BCryptBufferDesc* pParameterList,
  PUCHAR pbDerivedKey,
  ULONG cbDerivedKey,
  ULONG* pcbResult,
  ULONG dwFlags
);

hSharedSecretは、秘密協定を表すハンドルを指定します。 pwszKDFは、使用するKDF(key derivation function)を表す文字列を指定します。 KDFは秘密協定から鍵マテリアルを作成するための機能であり、 BCRYPT_KDF_HASHを指定すると秘密協定をハッシュして鍵マテリアルを作成することになります。 pParameterListは、鍵マテリアルを作成するためのパラメータを指定します。 不要な場合は、NULLを指定して問題ありません。 pbDerivedKeyは、鍵マテリアルを受け取るバッファを指定します。 cbDerivedKeyは、pbDerivedKeyのサイズを指定します。 pcbResultは、pbDerivedKeyにコピーしたデータのサイズを受け取る変数のアドレスを指定します。 pbDerivedKeyにNULLを指定した場合は、必要なバッファのサイズが返ります。 dwFlagsは、基本的に0を指定します。

不要になった秘密協定のハンドルは、BCryptDestroySecretで開放します。

NTSTATUS WINAPI BCryptDestroySecret(
  BCRYPT_SECRET_HANDLE hSecret
);

hSecretは、秘密協定のハンドルを指定します。

今回のプログラムは、ECDH鍵交換アルゴリズムを使用する例を示しています。 実際に通信を行っているわけではありませんが、 同じ秘密協定が作成されていることを理解できる題材となります。

#include <windows.h>
#include <bcrypt.h>

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)Status) >= 0)
#endif

#pragma comment (lib, "bcrypt.lib")

BOOL CreateSymmetricKeyAndEncryptData(BCRYPT_KEY_HANDLE hPrivKey, BCRYPT_KEY_HANDLE hPubKey, BOOL bEncrypt);
BOOL GetPulicKeyFromKeyPair(BCRYPT_ALG_HANDLE hAlg, BCRYPT_KEY_HANDLE hKeyPair, BCRYPT_KEY_HANDLE *phPubKey);
BOOL EncryptData(BCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPBYTE lpData, DWORD dwDataSize);
BOOL DecryptData(BCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPBYTE *lplpData, LPDWORD lpdwDataSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	BCRYPT_ALG_HANDLE hAlgA;
	BCRYPT_ALG_HANDLE hAlgB;
	BCRYPT_KEY_HANDLE hKeyA;
	BCRYPT_KEY_HANDLE hKeyB;
	BCRYPT_KEY_HANDLE hPubKeyA;
	BCRYPT_KEY_HANDLE hPubKeyB;
	NTSTATUS          status;

	status = BCryptOpenAlgorithmProvider(&hAlgA, BCRYPT_ECDH_P256_ALGORITHM, NULL, 0);
	if (!NT_SUCCESS(status))
		return 0;
	status = BCryptGenerateKeyPair(hAlgA, &hKeyA, 256, 0);
	if (!NT_SUCCESS(status)) {
		BCryptCloseAlgorithmProvider(hAlgA, 0);
		return 0;
	}
	BCryptFinalizeKeyPair(hKeyA, 0);
	GetPulicKeyFromKeyPair(hAlgA, hKeyA, &hPubKeyA);
	
	status = BCryptOpenAlgorithmProvider(&hAlgB, BCRYPT_ECDH_P256_ALGORITHM, NULL, 0);
	if (!NT_SUCCESS(status)) {
		BCryptDestroyKey(hKeyA);
		BCryptCloseAlgorithmProvider(hAlgA, 0);
		return 0;
	}
	status = BCryptGenerateKeyPair(hAlgB, &hKeyB, 256, 0);
	if (!NT_SUCCESS(status)) {
		BCryptDestroyKey(hKeyA);
		BCryptCloseAlgorithmProvider(hAlgA, 0);
		BCryptCloseAlgorithmProvider(hAlgB, 0);
		return 0;
	}
	BCryptFinalizeKeyPair(hKeyB, 0);
	GetPulicKeyFromKeyPair(hAlgB, hKeyB, &hPubKeyB);

	if (CreateSymmetricKeyAndEncryptData(hKeyA, hPubKeyB, TRUE))
		CreateSymmetricKeyAndEncryptData(hKeyB, hPubKeyA, FALSE);

	BCryptDestroyKey(hPubKeyA);
	BCryptDestroyKey(hKeyA);
	BCryptCloseAlgorithmProvider(hAlgA, 0);
	BCryptDestroyKey(hPubKeyB);
	BCryptDestroyKey(hKeyB);
	BCryptCloseAlgorithmProvider(hAlgB, 0);

	return 0;
}

BOOL CreateSymmetricKeyAndEncryptData(BCRYPT_KEY_HANDLE hPrivKey, BCRYPT_KEY_HANDLE hPubKey, BOOL bEncrypt)
{
	BCRYPT_ALG_HANDLE    hAlg;
	BCRYPT_KEY_HANDLE    hKey;
	BCRYPT_SECRET_HANDLE hSecret;
	DWORD                dwKeyMaterialSize;
	DWORD                dwKeyObjectSize;
	DWORD                dwDataSize;
	DWORD                dwResult;
	LPBYTE               lpKeyMaterial;
	LPBYTE               lpKeyObject;
	LPBYTE               lpData;
	NTSTATUS             status;
	TCHAR                szFileName[] = TEXT("sample.dat");
	TCHAR                szData[] = TEXT("sample-data");
	
	status = BCryptSecretAgreement(hPrivKey, hPubKey, &hSecret, 0);
	if (!NT_SUCCESS(status))
		return FALSE;
	BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, NULL, NULL, 0, &dwKeyMaterialSize, 0);
	lpKeyMaterial = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeyMaterialSize);
	status = BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, NULL, lpKeyMaterial, dwKeyMaterialSize, &dwKeyMaterialSize, 0);
	if (!NT_SUCCESS(status)) {
		HeapFree(GetProcessHeap(), 0, lpKeyMaterial);
		BCryptDestroySecret(hSecret);
		return FALSE;
	}
	
	BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_RC4_ALGORITHM, NULL, 0);
	BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (LPBYTE)&dwKeyObjectSize, sizeof(DWORD), &dwResult, 0);
	lpKeyObject = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeyObjectSize);
	status = BCryptGenerateSymmetricKey(hAlg, &hKey, lpKeyObject, dwKeyObjectSize, lpKeyMaterial, dwKeyMaterialSize, 0);
	if (!NT_SUCCESS(status)) {
		HeapFree(GetProcessHeap(), 0, lpKeyMaterial);
		BCryptDestroySecret(hSecret);
		HeapFree(GetProcessHeap(), 0, lpKeyObject);
		BCryptCloseAlgorithmProvider(hAlg, 0);
		return FALSE;
	}

	if (bEncrypt) {
		EncryptData(hKey, szFileName, (LPBYTE)szData, sizeof(szData));
		MessageBox(NULL, TEXT("暗号化しました。"), TEXT("OK"), MB_OK);
	}
	else {
		DecryptData(hKey, szFileName, &lpData, &dwDataSize);
		MessageBox(NULL, (LPTSTR)lpData, TEXT("OK"), MB_OK);
		HeapFree(GetProcessHeap(), 0, lpData);
	}

	HeapFree(GetProcessHeap(), 0, lpKeyMaterial);
	BCryptDestroySecret(hSecret);
	HeapFree(GetProcessHeap(), 0, lpKeyObject);
	BCryptDestroyKey(hKey);
	BCryptCloseAlgorithmProvider(hAlg, 0);

	return TRUE;
}

BOOL GetPulicKeyFromKeyPair(BCRYPT_ALG_HANDLE hAlg, BCRYPT_KEY_HANDLE hKeyPair, BCRYPT_KEY_HANDLE *phPubKey)
{
	DWORD    dwDataSize;
	LPBYTE   lpData;
	NTSTATUS status;
	LPWSTR   lpszType = BCRYPT_ECCPUBLIC_BLOB;

	BCryptExportKey(hKeyPair, NULL, lpszType, NULL, 0, &dwDataSize, 0);
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	status = BCryptExportKey(hKeyPair, NULL, lpszType, lpData, dwDataSize, &dwDataSize, 0);
	if (!NT_SUCCESS(status)) {
		HeapFree(GetProcessHeap(), 0, lpData);
		return FALSE;
	}

	status = BCryptImportKeyPair(hAlg, NULL, lpszType, phPubKey, lpData, dwDataSize, 0);

	HeapFree(GetProcessHeap(), 0, lpData);

	return TRUE;
}

BOOL EncryptData(BCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPBYTE lpData, DWORD dwDataSize)
{
	HANDLE hFile;
	DWORD  dwWriteByte;
	DWORD  dwEncryptDataSize;
	LPBYTE lpEncryptData;
	
	BCryptEncrypt(hKey, lpData, dwDataSize, NULL, NULL, 0, NULL, 0, &dwEncryptDataSize, 0);
	lpEncryptData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwEncryptDataSize);
	BCryptEncrypt(hKey, lpData, dwDataSize, NULL, NULL, 0, lpEncryptData, dwEncryptDataSize, &dwEncryptDataSize, 0);

	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpEncryptData, dwEncryptDataSize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	HeapFree(GetProcessHeap(), 0, lpEncryptData);
	
	return TRUE;
}

BOOL DecryptData(BCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPBYTE *lplpData, LPDWORD lpdwDataSize)
{
	HANDLE hFile;
	DWORD  dwReadByte;
	DWORD  dwEncryptDataSize;
	DWORD  dwDataSize;
	LPBYTE lpEncryptData;
	LPBYTE lpData;

	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;
	
	dwEncryptDataSize = GetFileSize(hFile, NULL);
	lpEncryptData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwEncryptDataSize);
	ReadFile(hFile, lpEncryptData, dwEncryptDataSize, &dwReadByte, NULL);
	CloseHandle(hFile);
	
	BCryptDecrypt(hKey, lpEncryptData, dwEncryptDataSize, NULL, NULL, 0, NULL, 0, &dwDataSize, 0);
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	BCryptDecrypt(hKey, lpEncryptData, dwEncryptDataSize, NULL, NULL, 0, lpData, dwDataSize, &dwDataSize, 0);

	*lplpData = lpData;
	*lpdwDataSize = dwDataSize;

	HeapFree(GetProcessHeap(), 0, lpEncryptData);

	return TRUE;
}

秘密協定を作成するためには、まず秘密鍵と公開鍵を用意しなければなりません。 まず、Aの処理を確認します。

status = BCryptOpenAlgorithmProvider(&hAlgA, BCRYPT_ECDH_P256_ALGORITHM, NULL, 0);
if (!NT_SUCCESS(status))
	return 0;
status = BCryptGenerateKeyPair(hAlgA, &hKeyA, 256, 0);
if (!NT_SUCCESS(status)) {
	BCryptCloseAlgorithmProvider(hAlgA, 0);
	return 0;
}
BCryptFinalizeKeyPair(hKeyA, 0);
GetPulicKeyFromKeyPair(hAlgA, hKeyA, &hPubKeyA);

BCryptOpenAlgorithmProviderにECDHを示すアルゴリズムを指定し、 BCryptGenerateKeyPairで公開鍵/秘密鍵の鍵ペアを指定します。 BCRYPT_ECDH_P256_ALGORITHMを指定した場合の鍵のサイズは、256ビットとなります。 BCryptFinalizeKeyPairを呼び出しにより実際に鍵ペアが使用できるようになり、 GetPulicKeyFromKeyPairの呼び出しによって、鍵ペアの公開鍵の部分を取得しています。 この公開鍵は、Bに送るために必要となります。 続いて、Bの処理を確認します。

status = BCryptOpenAlgorithmProvider(&hAlgB, BCRYPT_ECDH_P256_ALGORITHM, NULL, 0);
if (!NT_SUCCESS(status)) {
	BCryptDestroyKey(hKeyA);
	BCryptCloseAlgorithmProvider(hAlgA, 0);
	return 0;
}
status = BCryptGenerateKeyPair(hAlgB, &hKeyB, 256, 0);
if (!NT_SUCCESS(status)) {
	BCryptDestroyKey(hKeyA);
	BCryptCloseAlgorithmProvider(hAlgA, 0);
	BCryptCloseAlgorithmProvider(hAlgB, 0);
	return 0;
}
BCryptFinalizeKeyPair(hKeyB, 0);
GetPulicKeyFromKeyPair(hAlgB, hKeyB, &hPubKeyB);

主な作成手順はAと同様ですが、1つ注意しておく点があります。 それは、BCryptOpenAlgorithmProviderに指定する第2引数を 必ずAで指定した値と同一にすることです。 これが一致していない場合は、BCryptSecretAgreementで失敗することになります。 GetPulicKeyFromKeyPairの処理は、次のようになっています。

LPWSTR lpszType = BCRYPT_ECCPUBLIC_BLOB;

BCryptExportKey(hKeyPair, NULL, lpszType, NULL, 0, &dwDataSize, 0);
lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
status = BCryptExportKey(hKeyPair, NULL, lpszType, lpData, dwDataSize, &dwDataSize, 0);
if (!NT_SUCCESS(status)) {
	HeapFree(GetProcessHeap(), 0, lpData);
	return FALSE;
}

status = BCryptImportKeyPair(hAlg, NULL, lpszType, phPubKey, lpData, dwDataSize, 0);

BCryptExportKeyにBCRYPT_ECCPUBLIC_BLOBを指定すれば、鍵ペアの公開鍵の部分をエクスポートすることができます。 このエクスポートされたデータから公開鍵のハンドルを取得するには、 データとBCRYPT_ECCPUBLIC_BLOBを指定してBCryptImportKeyPairを呼び出します。

AとB共に鍵ペアを作成したならば、AはBから公開鍵を取得し、 自身の秘密鍵とBの公開鍵から秘密協定を作成することになります。 また、BはAから公開鍵を取得し、自身の秘密鍵とAの公開鍵から秘密協定を作成することになります。 今回のプログラムはAとBが1つのコードで動作していため、 相手から公開鍵を受信するという処理は含まれていません。 このため、直ちに秘密協定の作成に入ることになります。

if (CreateSymmetricKeyAndEncryptData(hKeyA, hPubKeyB, TRUE))
	CreateSymmetricKeyAndEncryptData(hKeyB, hPubKeyA, FALSE);

CreateSymmetricKeyAndEncryptDataは、内部で秘密協定を作成し、対象鍵を作成します。 また、第3引数がTRUEの場合は対象鍵を使用して暗号化を行い、 FALSEの場合は複合化を行います。 1回目の呼び出しがAによるものであり、Aの秘密鍵とBの公開鍵が指定されています。 また、2回目の呼び出しがBによるものであり、Bの秘密鍵とAの公開鍵が指定されています。 Aが暗号化したものをBが複合化することになり、これが正しく複合化されていることから、 同じ対象鍵が作成されているといえます。 次に、CreateSymmetricKeyAndEncryptDataの処理を示します。

status = BCryptSecretAgreement(hPrivKey, hPubKey, &hSecret, 0);
if (!NT_SUCCESS(status))
	return FALSE;
BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, NULL, NULL, 0, &dwKeyMaterialSize, 0);
lpKeyMaterial = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeyMaterialSize);
status = BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, NULL, lpKeyMaterial, dwKeyMaterialSize, &dwKeyMaterialSize, 0);
if (!NT_SUCCESS(status)) {
	HeapFree(GetProcessHeap(), 0, lpKeyMaterial);
	BCryptDestroySecret(hSecret);
	return FALSE;
}

BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_RC4_ALGORITHM, NULL, 0);
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (LPBYTE)&dwKeyObjectSize, sizeof(DWORD), &dwResult, 0);
lpKeyObject = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeyObjectSize);
status = BCryptGenerateSymmetricKey(hAlg, &hKey, lpKeyObject, dwKeyObjectSize, lpKeyMaterial, dwKeyMaterialSize, 0);

BCryptSecretAgreementによって秘密鍵と公開鍵から秘密協定を作成し、 BCryptDeriveKeyで秘密協定から鍵マテリアルを派生させます。 そして、これをBCryptGenerateSymmetricKeyの第5引数に指定することで、 対象鍵が作成されることになります。

鍵マテリアルとハッシュアルゴリズム

BCryptDeriveKeyの第2引数にBCRYPT_KDF_HASHを指定した場合、 派生する鍵マテリアルは秘密協定のハッシュ値となります。 このハッシュ値は、デフォルトでSHA1アルゴリズムが使用されることになっていますが、 次のようにすればハッシュアルゴリズムを変更することができます。

BCryptBufferDesc desc;
BCryptBuffer     buffer;

desc.ulVersion = 0;
desc.cBuffers  = 1;
desc.pBuffers  = &buffer;

buffer.BufferType = KDF_HASH_ALGORITHM;
buffer.pvBuffer   = BCRYPT_MD5_ALGORITHM;
buffer.cbBuffer   = (lstrlen(BCRYPT_MD5_ALGORITHM) + 1) * sizeof(WCHAR);

BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, &desc, NULL, 0, &dwKeyMaterialSize, 0);
lpKeyMaterial = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeyMaterialSize);
status = BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, &desc, lpKeyMaterial, dwKeyMaterialSize, &dwKeyMaterialSize, 0);

BCryptDeriveKeyの第3引数には、鍵マテリアルを作成するためのデータを指定することができます。 個々のデータはBCryptBufferで表され、複数のデータを1つとして表すのがBCryptBufferDescとなります。 ハッシュアルゴリズムを指定するには、BCryptBuffer.BufferTypeはKDF_HASH_ALGORITHMを指定し、 pvBufferに使用したいハッシュアルゴリズムの名前、cbBufferにそのサイズを指定します。



戻る