EternalWindows
CSP / 共通鍵暗号

セッション鍵による暗号化では、暗号化と複合化に同じ鍵を使うことから 共通鍵暗号と呼ばれることがあります。 暗号化を行うには、CryptEncryptを呼び出すことになります。

BOOL WINAPI CryptEncrypt(
  HCRYPTKEY hKey, 
  HCRYPTHASH hHash, 
  BOOL Final, 
  DWORD dwFlags, 
  BYTE *pbData, 
  DWORD *pdwDataLen, 
  DWORD dwBufLen 
);

hKeyは、暗号化に使用する鍵のハンドルを指定します。 hHashは、ハッシュオブジェクトのハンドルを指定します。 このハッシュオブジェクトが維持しているハッシュ値は、 暗号化されたデータにさらに署名を付け加える目的で利用されます。 暗号化と同時に署名を行はない場合は、0で問題ありません。 Finalは、このCryptEncryptの呼び出しで暗号化が完了する場合はTRUEを指定し、 そうでない場合はFALSEを指定します。 dwFlagsは、CSP特有のフラグを指定しますが、基本的には0で問題ありません。 pbDataは、暗号化されるデータを格納しているバッファを指定します。 関数が成功した場合、このバッファのデータが暗号化されます。 pdwDataLenは、データの長さを格納した変数のアドレスを指定します。 関数から制御が返ると、暗号化されたデータのサイズが格納されます。 dwBufLenは、pbDataのバッファのサイズを指定します。 暗号化されたデータのサイズがバッファのサイズを超えるような場合は データを格納するわけにはいきませんから、このような引数が必要になります。

複合化を行うには、CryptDecryptを呼び出します。

BOOL WINAPI CryptDecrypt(
  HCRYPTKEY hKey, 
  HCRYPTHASH hHash, 
  BOOL Final, 
  DWORD dwFlags, 
  BYTE *pbData, 
  DWORD *pdwDataLen 
);

hKeyは、複合化に使用する鍵のハンドルを指定します。 hHashは、ハッシュオブジェクトのハンドルを指定します。 CryptEncryptで署名を行った場合は、 ハッシュオブジェクトに署名されたハッシュ値が格納されます。 Finalは、このCryptDecryptの呼び出しで復号化が完了する場合はTRUEを指定し、 そうでない場合はFALSEを指定します pbDataは、暗号化されたデータを格納しているバッファを指定します。 関数が成功した場合、このバッファのデータが複合化されます。 pdwDataLenは、暗号化されたデータの長さを格納した変数のアドレスを指定します。 関数から制御が返ると、復号化されたデータのサイズが格納されます。

Finalという引数は、データが分割して処理される場面で活用されます。 たとえば、既存ファイルのデータを全て暗号化したいような場合、 まずそのファイルのデータを全て読み取る必要がありますが、 バッファのサイズなどの関係上、データを一定サイズで 繰り返し読み取らなければならないようことがあります。 このようなとき、その一定サイズのデータ毎に暗号化を行い、FinalにはFALSEを指定します。 そして、最後のデータを読み取った場合は、 そのデータが最後に暗号化されるデータということで、FinalにTRUEを指定します。 TRUEの際に実行される特別な処理は暗号方式によって異なりますが、 たとえば、ブロック暗号ならデータのサイズをブロック単位にするべく、 データにいくつかのパディングを付加することがあります。

今回のプログラムは、暗号化と複合化の処理が実装されています。 暗号化の処理では、カレントディレクトリにファイルを作成して、 そこに暗号化したデータを書き込みます。 複合化の処理では、カレントディレクトリに存在するファイルを読み取って、 その暗号化されているデータを複合化します。

#include <windows.h>

BOOL CreateSessionKey(HCRYPTPROV hProv, LPBYTE lpData, DWORD dwDataSize, HCRYPTKEY *phKey);
BOOL EncryptData(HCRYPTKEY hKey, LPTSTR lpszFileName, LPBYTE lpData, DWORD dwDataSize, LPBYTE lpBuffer, DWORD dwBufferSize);
BOOL DecryptData(HCRYPTKEY hKey, LPTSTR lpszFileName, LPBYTE lpBuffer, DWORD dwBufferSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szFileName[] = TEXT("sample.txt");
	TCHAR      szPassword[] = TEXT("password");
	TCHAR      szData[] = TEXT("sample-data");
	TCHAR      szBuf[256];
	HCRYPTPROV hProv;
	HCRYPTKEY  hKey;
	BOOL       bEncrypt = TRUE;
	
	if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) {
		MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	if (!CreateSessionKey(hProv, (LPBYTE)szPassword, sizeof(szPassword), &hKey)) {
		MessageBox(NULL, TEXT("セッション鍵の作成に失敗しました。"), NULL, MB_ICONWARNING);
		CryptReleaseContext(hProv, 0);
		return 0;
	}

	if (bEncrypt) {
		if (EncryptData(hKey, szFileName, (LPBYTE)szData, sizeof(szData), (LPBYTE)szBuf, sizeof(szBuf))) 
			MessageBox(NULL, TEXT("暗号化しました。"), TEXT("OK"), MB_OK);
	}
	else {
		if (DecryptData(hKey, szFileName, (LPBYTE)szBuf, sizeof(szBuf)))
			MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
	}

	CryptDestroyKey(hKey);
	CryptReleaseContext(hProv, 0);

	return 0;
}

BOOL CreateSessionKey(HCRYPTPROV hProv, LPBYTE lpData, DWORD dwDataSize, HCRYPTKEY *phKey)
{
	BOOL       bResult;
	HCRYPTHASH hHash;

	if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash))
		return FALSE;
	
	CryptHashData(hHash, lpData, dwDataSize, 0);
	
	bResult = CryptDeriveKey(hProv, CALG_RC4, hHash, 0, phKey);
	
	CryptDestroyHash(hHash);

	return bResult;
}

BOOL EncryptData(HCRYPTKEY hKey, LPTSTR lpszFileName, LPBYTE lpData, DWORD dwDataSize, LPBYTE lpBuffer, DWORD dwBufferSize)
{
	HANDLE hFile;
	DWORD  dwWriteByte;
	BOOL   bResult;

	if (dwDataSize > dwBufferSize)
		return FALSE;
	
	CopyMemory(lpBuffer, lpData, dwDataSize);
	bResult = CryptEncrypt(hKey, 0, TRUE, 0, lpBuffer, &dwDataSize, dwBufferSize);
	if (!bResult)
		return FALSE;
	
	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpBuffer, dwDataSize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	return TRUE;
}

BOOL DecryptData(HCRYPTKEY hKey, LPTSTR lpszFileName, LPBYTE lpBuffer, DWORD dwBufferSize)
{
	HANDLE hFile;
	DWORD  dwReadByte;
	DWORD  dwDataSize;
	BOOL   bResult;

	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;

	dwDataSize = GetFileSize(hFile, NULL);
	if (dwDataSize > dwBufferSize) {
		CloseHandle(hFile);
		return FALSE;
	}

	ReadFile(hFile, lpBuffer, dwDataSize, &dwReadByte, NULL);
	CloseHandle(hFile);

	bResult = CryptDecrypt(hKey, 0, TRUE, 0, lpBuffer, &dwDataSize);

	return bResult;
}

bEncryptという変数がTRUEであるときに呼ばれる自作関数のEncryptDataは、 内部でCryptEncryptを呼び出す設計になっています。 一方、bEncryptがFALSEのときに呼ばれる自作関数のDecryptDataは、 内部でCryptDecryptを呼び出す設計になっています。 実行例としては、最初にbEncryptをTRUEとして実行し、 その後にbEncryptをFALSEとして実行します。 両者の関数に指定されるセッション鍵は、どちらも"password"という同じパスワードで 作成されているため、常に同じセッション鍵が得られることになり、 暗号化したデータを復号化できることになります。 EncryptDataの内部は、次のようになっています。

BOOL EncryptData(HCRYPTKEY hKey, LPTSTR lpszFileName, LPBYTE lpData, DWORD dwDataSize, LPBYTE lpBuffer, DWORD dwBufferSize)
{
	HANDLE hFile;
	DWORD  dwWriteByte;
	BOOL   bResult;

	if (dwDataSize > dwBufferSize)
		return FALSE;
	
	CopyMemory(lpBuffer, lpData, dwDataSize);
	bResult = CryptEncrypt(hKey, 0, TRUE, 0, lpBuffer, &dwDataSize, dwBufferSize);
	if (!bResult)
		return FALSE;
	
	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpBuffer, dwDataSize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	return TRUE;
}

暗号化したいデータをバッファにコピーしている点が重要です。 CryptEncryptで暗号化されたデータは、元のデータのサイズを超える場合があるため、 lpDataをCryptEncryptに指定するわけにはいきません。 また、元のデータがバッファのサイズを超える場合はコピーできませんから、 その場合は関数が失敗することになります。 CryptEncryptの第2引数はハッシュオブジェクトのハンドルですが、 今回は署名を行わないので0で問題ありません。 第4引数はCSP特有のフラグですが、これも利用しない場合は0を指定することができます。 EncryptDataでは暗号化を1回のCryptEncryptで終わらすことになっているので、 第3引数はTRUEとなります。 関数が成功した場合、lpBufferは暗号化されたデータで上書きされ、 dwDataSizeは暗号化されたデータの長さになります。 このデータとサイズを、それぞれWriteFileに指定することになります。 次に、DecryptDataの内部を確認します。

BOOL DecryptData(HCRYPTKEY hKey, LPTSTR lpszFileName, LPBYTE lpBuffer, DWORD dwBufferSize)
{
	HANDLE hFile;
	DWORD  dwReadByte;
	DWORD  dwDataSize;
	BOOL   bResult;

	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;

	dwDataSize = GetFileSize(hFile, NULL);
	if (dwDataSize > dwBufferSize) {
		CloseHandle(hFile);
		return FALSE;
	}

	ReadFile(hFile, lpBuffer, dwDataSize, &dwReadByte, NULL);
	CloseHandle(hFile);

	bResult = CryptDecrypt(hKey, 0, TRUE, 0, lpBuffer, &dwDataSize);

	return bResult;
}

EncryptDataでは、ファイルに暗号化データ単一を保存したため、 ファイルサイズを暗号化データのサイズと考えることができます。 このサイズがバッファより大きい場合は、複合化したデータをバッファに格納できないため、 関数が失敗することになります。 ReadFileで読み取られた暗号化データはlpBuuferに格納され、 CryptDecryptが成功した場合に複合化されたデータで上書きされます。

CreateSessionKeyで呼び出しているCryptDeriveKeyには、 暗号化アルゴリズムとしてCALG_RC4を指定しているため、 今回の暗号方式はストリーム暗号です。 ストリーム暗号では、暗号化後のデータのサイズと暗号化前のデータのサイズは 変化することがありませんから、CryptEncryptの第6引数に指定した データのサイズは変更されることはありません。 また、ストリーム暗号は1バイト単位の暗号方式であるため、 たとえば作成されたファイルをバイナリエディタで開いて1バイトを変更した場合、 複合化の際に影響を受けているのは変更した1バイトだけであることが分かります。

ソルト値について

以前、鍵のサイズはCSPによって決定されると説明したことがありましたが、 正確には鍵のサイズはCreateSessionKeyに指定するハッシュ値のサイズによって決定されることになっています。 たとえば、ハッシュ値のサイズが16バイト(MD5ハッシュ)であるならば、 作成される鍵のサイズは128ビットになります。 CSPによって決定された鍵のサイズが40ビットであった場合、 それは本来の長さ128ビットの40ビットを占めることになり、 残り88ビットは使用しないということで、基本的に0が指定されます。 しかし、鍵のサイズが長ければそれだけ暗号化も強力になるわけですから、 その残りのビット数に何らかの値を設定したいものです。 実は、ここで設定するランダムな値がソルト値と言われており、 CryptDeriveKeyに第4引数にCRYPT_CREATE_SALTを指定することで設定できます。

CryptDeriveKey(hProv, CALG_RC4, hHash, CRYPT_CREATE_SALT, phKey);

ソルト値が残存ビットに対しての値でしかないことに注意してください。 たとえば、hHashのサイズが128ビットであったとして、 hProvはMS_ENHANCED_PROVのハンドルとします。 このCryptDeriveKeyの第4引数では上位16ビットに鍵のサイズを指定していないため、 決定されるサイズはMS_ENHANCED_PROVのCALG_RC4のデフォルト値によって決まることになります。 そして、この値は128ビットであり、これはハッシュ値のサイズと同一であるため、 残存ビットは存在せず、ソルト値を設定することはできません。 CryptGetKeyParamにKP_SALTを指定すると、関数が成功してソルト値を取得できたかと思えますが、 実際にその値を確認してみると、0しか格納されていないことが分かります。

セッション鍵は暗号化と複合化に同一の鍵を使うという関係上、 通信する相手の数だけ異なるセッション鍵を用意しなければならないことがあります。 これは、全ての相手に同一のセッション鍵を公開してしまっては、 当事者間以外の者がデータを複合化することができてしまうためです。 相手の数だけ鍵を用意するにあたってもっとも単純な方法は、 その数だけ新しい鍵を作成することですが、 既存の鍵のソルト値を変更することによって、 新しい鍵を作成したかのように振舞うこともできます。

BYTE            salt[11];
CRYPT_DATA_BLOB blob;

CryptDeriveKey(hProv, CALG_RC4, hHash, 0x00280000 | CRYPT_EXPORTABLE, phKey);

CryptGenRandom(hProv, sizeof(salt), salt);

blob.cbData = sizeof(salt);
blob.pbData = salt;
CryptSetKeyParam(hKey, KP_SALT_EX, &blob, 0);

CryptDeriveKeyでは、鍵を公開することを考えてCRYPT_EXPORTABLEを指定し、 さらに鍵のサイズを40ビットに設定しています。 残り88ビットにはソルト値を格納するということで、 要素数が11のバイトデータを用意し、 ここにCryptGenRandomでランダムな値を設定します。 そして、CRYPT_DATA_BLOB構造体にデータとサイズを指定して、 KP_SALT_EXを指定したCryptSetKeyParamを呼び出せば、 鍵にソルト値が設定されることになります。 後はこの鍵を相手側に公開し、新しい相手が接続してきた場合は、 またソルト値を作成して鍵に設定することになるでしょう。 ソルト値を設定する前の状態の鍵を保存しておきたい場合は、 CryptDuplicateKeyを呼び出すことができます。



戻る