AES 256 암호화를 위한 WinCrypt 사용하기

MSDN에서 참고하자니… 너무나도 설명이 장황하고 API 사용에 핵심이되는걸 찾지못하여 example을 구글링하여 여차저차 짜집기하였다. 그중 가장 많은 도움이된 글은 이글이글.

만약 Crypt관련함수들 링크실패하면 헤더나 소스코드에 #pragma comment(lib, "advapi32")를 넣으면된다.

아래는 최소한의 헤더파일

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <wincrypt.h>

AES 암호화에서 안정성이 보장되는 AES 256을 기준으로 설명한다. 그 하위 AES 암호화는 KEY의 길이가 다르기때문에 필요하다면 32비트 KEY 대신 16비트 키를 사용하면… 될것이다. 나머지는 알고있기로는 동일.

필요한 변수는 아래와같다.

HCRYPTPROV hCryptProv;
HCRYPTHASH hHash;
HCRYPTKEY hKey;

첫번째로 Provider를 얻어야한다.

if (CryptAcquireContext(&hCryptProv, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, 0) == FALSE) {
    return FALSE;
}

핸들을 해제하는 코드는 아래와같다. 이 핸들은 지속적으로 사용하게된다. 완전하게 사용이 끝났으면 그때 Release 해주면된다.

CryptReleaseContext(hCryptProv, 0);

MS_ENH_RSA_AES_PROV, PROV_RSA_AES는 wincrypt.h 헤더에 정의되어있고 MS_ENH_RSA_AES_PROV 값은 아래와같이 정의되어있다.

#if (NTDDI_VERSION >= NTDDI_WINXP)
#define MS_ENH_RSA_AES_PROV_A   "Microsoft Enhanced RSA and AES Cryptographic Provider"
#define MS_ENH_RSA_AES_PROV_W   L"Microsoft Enhanced RSA and AES Cryptographic Provider"
#define MS_ENH_RSA_AES_PROV_XP_A "Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)"
#define MS_ENH_RSA_AES_PROV_XP_W L"Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)"
#ifdef UNICODE
#define MS_ENH_RSA_AES_PROV_XP  MS_ENH_RSA_AES_PROV_XP_W
#define MS_ENH_RSA_AES_PROV     MS_ENH_RSA_AES_PROV_W
#else
#define MS_ENH_RSA_AES_PROV_XP  MS_ENH_RSA_AES_PROV_XP_A
#define MS_ENH_RSA_AES_PROV     MS_ENH_RSA_AES_PROV_A
#endif
#endif //(NTDDI_VERSION >= NTDDI_WINXP)

즉 대강보면 알 수 있겠지만 AES 암호화는 XP이상부터 지원하며 XP일경우엔 Prototype로 명시되어있다. 아마… XP일경우 MS_ENH_RSA_AES_PROV_XP 값으로 초기화해야할것이고 XP도 지원하도록한다면 아래와같이 하면 될것이다. 또한 저 정의가 OS의 어디에 나열되어있는지는 위 codeproject글 Figure 4 이미지를 참고하면 알 수 있다.

if (CryptAcquireContext(&hCryptProv, NULL, MS_ENH_RSA_AES_PROV, PROV_RSA_AES, 0) == FALSE) {
    if (CryptAcquireContext(&hCryptProv, NULL, MS_ENH_RSA_AES_PROV_XP, PROV_RSA_AES, 0) == FALSE) {
        return FALSE;
    }
}

이후 HCRYPTKEY 핸들을 생성해야한다. AES 암호화를 사용하는 핸들을 생성하는 방법은 크게 두가지인것같다. 하나는 HCRYPTHASH 핸들을 KEY로 사용하여 생성, 또다른 하나는 이미 생성된 32 바이트 난수를 사용하여 생성하는 방법.

그리고 AES 핸들 생성에 사용할 목적은 아닌거같지만 CryptGenKey 함수가있다. 하지만 KEY를 설정하는 방법을 모르겠으므로 패스. (CryptSetKeyParam 함수로 KP_KEYVAL에 값을 넣어봤지만 실패, 구글링해도 안나온다. 왜 CALG_AES_256으로 핸들이 생성되는지는 의문.)

첫번째 방법으로 HCRYPTHASH 핸들을 사용한 HCRYPTKEY 생성.
(그냥 예제코드라서 실제 pValue 값의 글자길이인 15를 넘기지않고 문자열 끝인 0이 포함되어 16이 반환될 sizeof(pValue)를 넘겼다.)

BYTE pValue[] = "TEST HASH VALUE";

if (CryptCreateHash(hCryptProv, CALG_SHA_256, 0, 0, &hHash) == FALSE) {
    return FALSE;
}
if (CryptHashData(hHash, pValue, sizeof(pValue), 0) == FALSE) {
    return FALSE;
}
if (CryptDeriveKey(hCryptProv, CALG_AES_256, hHash, CRYPT_EXPORTABLE, &hKey) == FALSE) {
    return FALSE;
}

32 바이트 길이를 가지는 HASH 값을 생성하는 SHA 256 핸들을 생성, 이후 CryptHashData 함수를 호출하여 HCRYPTHASH 핸들의 HASH 값을 업데이트한다. 참고로 CryptDeriveKey에 전될 된 값인 CALG_AES_256은 XP에서 안된다. XP는 128까지만 되는것으로 알고있다. (XP는 SHA 128 HASH 값을 사용해야할거같은데… 어차피 16 바이트 KEY를 사용하는 AES 128에서 32 바이트 HASH를 사용하는건 별로 의미없다고 생각되므로 테스트는 패스.)

두번째 방법으로 이미 생성되었거나 직접 생성한 32 바이트 KEY를 사용하여 HCRYPTKEY 핸들을 생성. (32바이트 변수 pKey에는 이미 임의의 값이 생성되어있다고 가정.)

const DWORD AES_KEY_LENGTH = 32;
struct {
    BLOBHEADER hdr;
    DWORD cbKeySize;
    BYTE rgbKeyData[AES_KEY_LENGTH];
} keyBlob;

keyBlob.hdr.bType = PLAINTEXTKEYBLOB;
keyBlob.hdr.bVersion = CUR_BLOB_VERSION;
keyBlob.hdr.reserved = 0;
keyBlob.hdr.aiKeyAlg = CALG_AES_256;
keyBlob.cbKeySize = AES_KEY_LENGTH;
CopyMemory(keyBlob.rgbKeyData, pKey, AES_KEY_LENGTH);

if (CryptImportKey(hCryptProv, (BYTE*)&keyBlob, sizeof(keyBlob), 0, 0, &hKey) == FALSE) {
    return FALSE;
}

구글링하면 다들 저렇게 일회용 구조체 변수를 할당하여 사용한다. 구버전 컴파일러에서는 const 값으로 배열을 정의하지 못한다. 그럴땐 define으로 AES_KEY_LENGTH를 정의하면된다.

만약 HCRYPTHASH 핸들에서 생성한 HASH 값을 저장하여 나중에 다시 사용하려면 아래와같이 계산된 HASH 값을 얻을 수 있다.

BYTE pKey[32];
DWORD length = sizeof(pKey);
BOOL result = CryptGetHashParam(hHash, HP_HASHVAL, pKey, &length, 0);

이제 알고리즘을 사용하기 전 KP_MODE 값을 명시적으로 CRYPT_MODE_CBC로 설정한다. (MSDN에서는 기본으로 CBC를 사용한다고되어있지만 몇몇 셈플에서는 명시적으로 설정하도록 되어있었다.) 모드에 대한 설명은 위키문서인 이곳에서 확인가능.

DWORD dwMode = CRYPT_MODE_CBC;
if(CryptSetKeyParam(hKey, KP_MODE, (BYTE*)&dwMode, 0) == FALSE) {
    return FALSE;
}

마지막으로 16 바이트 IV 값 설정. 이것은 꼭 필요한것이 아니지만 조금이나마 더 강력한 암호화를하려면 쓰는것이 더 좋다.

BYTE pIV[16];
if (CryptGenRandom(hCryptProv, sizeof(pIV), pIV) == FALSE) {
    return FALSE;
}

if (CryptSetKeyParam(hKey, KP_IV, pIV, 0) == FALSE) {
    return FALSE;
}

이제 아래와같이 암호화/복호화 할 수 있다.

const char PLAINTEXT[] = "TEST MESSAGE";

BYTE pData[128];
DWORD dwStrLen = lstrlenA(PLAINTEXT) + 1;

DWORD dwPadding = 16 - (dwStrLen & 0xF);
DWORD dwDataLen = dwStrLen;
DWORD dwBufferLen = dwStrLen + dwPadding;
BOOL result;

CopyMemory(pData, PLAINTEXT, dwStrLen);

result = CryptEncrypt(hKey, 0, TRUE, 0, pData, &dwDataLen, dwBufferLen);
result = CryptDecrypt(hKey, 0, TRUE, 0, pData, &dwDataLen);

CryptEncrypt 함수가 성공적으로 수행되면 dwDataLen은 padding이 포함된 최종 암호화 된 데이터 길이가 할당된다.
CryptDecrypt 함수가 성공적으로 수행되면 dwDataLen은 padding이 제외된 최종 원본데이터의 길이가 할당된다.

쓸대없는 코드가 보일것이다. Padding과 strlen에 + 1 하는 것. 암호화는 바이너리 데이터가 대상이라 문자열이라고 끝에 \0 까지도 함께 암호화하지 않기때문에 일부러 + 1해주었다. 복호화하고나서 dwDataLen을 사용하여 문자열 끝에 0을 대입한다면 길이에 + 1할 필요없다.

padding 값은 16 바이트 블록단위로 암호화하고 남은 공간을 특정 값으로 넣어주는 것이다. 그래서 AES로 암호화하면 결과 데이터는 16의 배수의 길이를 가지게된다. 16 – (dwStrLen & 0xF)는 16 – (dwStrLen % 16)과 동일하다. 나머지 연산은 비트연산에 비하면 연산에 들어가는 비용이 크기때문이다. (하지만 요즘 프로세서에서는 의미없는 수치일듯하다.)

16의 배수이지만 strLen이 16이라면 padding값을 더한 bufferLen값은 16이 아니라 32가된다. 정확한 이유는 찾지못했지만 아무래도 16바이트가 암호화되면 padding이 된 값인지 아닌지 구분할 수 없어 padding 규칙으로 값이 채워진 여분의 16바이트를 더 쓰는것같다. padding에도 표준이있지만 WinCrypt에서는 PKCS5, RANDOM, ZERO padding만 지원하는거같다. 이곳 KP_PADDING 항목에서 확인 가능.

기본으로 PKCS5_PADDING 값을 사용하지만 PKCS7 padding을 사용하도록 초기화 된 외부 라이브러리에서 문제없이 복호화 되었다. PKCS5를 확장한 표준이 PKCS7이라고하던데 아마도 그러한 이유때문이 아닐까 싶다. 웃기게도 닷넷은 PKCS5가 없고 PKCS7이 있다. -_-;; (참고)

덧. 쓸대없는 부분이 한가지 더 있다. pData 변수가 넉넉하게 128 바이트라 dwDataLen을 dwStrLen + dwPadding 값으로 초기화 할필요없이 dwDataLen = sizeof(pData) 해도된다. 어차피 CryptEncrypt에서 TRUE를 return하면 dwDataLen 변수가 암호화 된 최종 사이즈로 변경되기때문. 하지만 스택의 공간을 넘어가는 크기의 데이터를 암호화한다면 heap 공간 메모리 할당하여 사용할텐데 필요한 바이트 수를 정확하게 계산하는 방법을 명시한것일 뿐이다. 사실 무조건 실제 데이터보다 16 바이트 더 많이 할당해서 encrypt 함수에 넘겨도된다. 선택은 자유.

덧. IV 값 외에 랜덤값을 사용하는 SALT 기법이있는데, IV는 이것과 다른거라고한다. SALT를 사용하면 더 높은 암호화 데이터의 보안을 유지할 수 있디고하지만 AES 암호화와는 다른 개념이므로 궁금하면 이곳 Naver D2 문서에서 확인가능. (실제 서비스에서 HASH를 생성할 때 사용할 SALT값이 공개되면 무용지물일텐데 어디에 어떻게 저장하여 HASH 될 Password 값에 사용해야할지 모범답안을 찾지 못하겠다.)

덧. 간혹 라이브러리 따라서는 padding 호환이 안되는경우가 있다고한다. 그럴땐 위 PKCS7 참고 페이지처럼 직접 padding값을 채워서 암호화할 수 있고 복호화 때에도 동일하게 마지막 padding 값을가지고 padding 된 데이터를 재외할 수 있다고한다.

덧. MS는 가끔 집요하게 추상화를 하려고하는거같다. 아무대로 새로운 알고리즘이 나와서 추가되어도 함수 원형을 유지하려는거같은데… 다른 오픈소스보다 사용하기 복잡하고 과연 그게 좋은건지 나쁜건지 모르겠다(… 옆동내 애플은 macOS/iOS 둘 다 초기화 필요없이 CC_SHA256, CCCrypt 함수 한방으로 해결할 수 있던데. 물론 상태를 가지는 변수를 생성하여 참조하는 함수도 있고) 라이센스 신경써야하고 빌드 환경설정이 필요한 외부 라이브러리 쓰기가 귀찮아서 WinCrypt를 사용하는 방법을 찾아서 사용해보았지만 덕분에 구글링을 열심히하게되었다.

One Comment

    WinCrypt의 핸들은 스레드 안정성이 보장안되는거같다 – 날조 블로그

    […] AES 256 암호화를 위한 WinCrypt 사용하기 […]

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

*
*

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.