Windowsで暗号プログラムを書いてみる(AES編) | reverse-eg-mal-memoのブログ

reverse-eg-mal-memoのブログ

サイバーセキュリティに関して、あれこれとメモするという、チラシの裏的存在。
medium(英語):https://sachiel-archangel.medium.com/

コロナウィルスで世間がなにかと騒がしいですが、相変わらず淡々とサイバーセキュリティな私です。

コロナウィルス対策?1月末頃の段階で、政治家は与党は観光客ガー野党は花見の話しかしていなかったので、この時既に「対策する気ないみたいだから、コロナは罹っても耐える。」で腹くくって、筋肉体操で体を鍛えています。筋肉は裏切らないwww

とまあ、時事と政治の皮肉とマッチョの話で入るという・・・、本当に技術のブログかこれ?

 

WindowsのCyptAPIを使った暗号プログラムで、Hash計算に続いて第二弾です。今回はAES暗号のコードです(AESの基本については前回記事を参考)。

フォレンジック屋なのに、何故か暗号化のプログラムのネタ。まあ、Autopsyで色々試してて、上手くいってなくてネタがないだけなんですけどねw

あと、自作ツールを気分で使ってみようと思ったほか、CTRモードでちょっと検証コード書きたいとか・・・。まあ、ベースを作っておくと何かと便利なんです。

 

 

AES暗号化・復号化のCryptAPIのざっくりな流れ

大雑把なフローは以下の通り。

 

前提:

  • "WinCrypt.h"をincludeすること。

 

概略フロー:

  • Cryptコンテキストの取得(CryptAcquireContext)
  • 鍵のインポート(CryptImportKey)
  • 暗号化・復号化に必要なパラメータ設定(CryptSetKeyParam)
  • 暗号化・復号化の実行(CryptEncrypt、CryptDecrypt)
  • キーオブジェクトの開放(CryptDestroyKey)
  • Cryptコンテキストの開放(CryptReleaseContext)

 

ポイント:

  • 利用するCryptAPIの種類と順番、概ねのパラメータ。
  • インポートされる鍵の管理、鍵交換方法は別途考慮しておく必要がある。
  • 暗号化モード、初期化ベクトルをCryptSetKeyParamで設定する。

 

 

CryptAPIを使ったAES計算サンプルソース

これも、VisualStudio2017で動作することが確認されたソースです。

もし流用するのであれば、パラメータ等については、用途に合わせて変えてください。

MicrosoftのMSDNを参考にするとよいでしょう。

特に、今回は暗号化モードをCBCモードに固定したものにしています。利用したいモードが異なる場合は適宜修正してください。

また、例によってタブが上手く入らなくてスペースでインデントしているので、適宜直してください。

 

関数の仕様:

static int Encrypt(BYTE *binKey, int iKeySize, BYTE *binInData, int *pInDataSize, BYTE *binOutData, int *pOutSize);

第1引数で渡した鍵で第3引数のデータを暗号化し、第5引数の領域に格納し返す。

第5引数の領域にNULLを指定した場合、暗号化結果を受け取るサイズを返す。

 

引数:

binKey:暗号化に用いる鍵データのポインタ

iKeySize:ハッシュ化したいデータのサイズ

binInData:暗号化されるデータのポインタ

pInDataSize:暗号化されるデータのサイズ

binOutData:暗号化されたデータの受取りバッファ

pOutSize:暗号化されたデータの受取りサイズ

 

返り値:

0:正常終了

負値:異常終了(詳細はヘッダのdefine参照)

 

制限:

大きなサイズのデータには未対応。

大きなデータを暗号化する場合は、CryptEncrypt関数の第3引数(Final)をFalseにしたうえで、ループする処理にする必要がある。

 

 

static int Decrypt(BYTE *binKey, int iKeySize, BYTE *binInOutData, int *binInOutData);

第1引数で渡した鍵で第3引数のデータを暗号化し、第3引数の領域に格納し返す。

 

引数:

binKey:暗号化に用いる鍵データのポインタ

iKeySize:ハッシュ化したいデータのサイズ

binInOutData:復号化されるデータのポインタ。復号結果がそのまま格納される。

binInOutData:復号化されるデータのサイズ。復号されたサイズがそのまま格納される。

 

返り値:

0:正常終了

負値:異常終了(詳細はヘッダのdefine参照)

 

制限:

大きなサイズのデータには未対応。

大きなデータを暗号化する場合は、CryptDecrypt関数の第3引数(Final)をFalseにしたうえで、ループする処理にする必要がある。

 

CryptAESCBC.h

#pragma once

#define CRYPTAES_SUCCESS 0
#define CRYPTAES_ER_FATAL -1
#define CRYPTAES_ER_PARAM -2
#define CRYPTAES_ER_ACQUIRECONTEXT -3
#define CRYPTAES_ER_IMPORTKEY -4
#define CRYPTAES_ER_SETKEYPARAM -5
#define CRYPTAES_ER_ENCRYPT -6
#define CRYPTAES_ER_DECRYPT -7

class CryptAESCBC
{
public:
    CryptAESCBC();
    ~CryptAESCBC();

    static int Encrypt(BYTE *binKey, int iKeySize, BYTE *binInData, int *pInDataSize, BYTE *binOutData, int *pOutSize);
    static int Decrypt(BYTE *binKey, int iKeySize, BYTE *binInOutData, int *pInOutSize);
};

 

CryptAESCBC.cpp

#include "stdafx.h"
#include "CryptAESCBC.h"
#include <WinCrypt.h>


CryptAESCBC::CryptAESCBC()
{
}


CryptAESCBC::~CryptAESCBC()
{
}

int CryptAESCBC::Encrypt(BYTE *binKey, int iKeySize, BYTE *binInData, int *pInDataSize, BYTE *binOutData, int *pOutSize)
{
    int iRetCode = CRYPTAES_ER_FATAL;
    HCRYPTPROV hProv = NULL;        // コンテキストハンドル
    HCRYPTKEY hKey = NULL;            // 鍵をインポートしたときのハンドル
    BYTE *objKeyBlob = NULL;
    HANDLE hHeap = NULL;
    DWORD dwPadding_mode = 0;
    DWORD byteCryptMode = 0;
    DWORD dwEncryptSize = 0;
    int iBufSize = 0;

    // 引数チェック
    if (binKey == NULL || iKeySize == 0 || binInData == NULL || pInDataSize == 0 || pOutSize == NULL)
    {
        return CRYPTAES_ER_PARAM;
    }

    // 受け取りバッファチェック
    if(binOutData != NULL && *pInDataSize > *pOutSize)
    {
        return CRYPTAES_ER_PARAM;
    }

    try {
        // コンテキストの取得
        if (CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_AES,
                                         CRYPT_VERIFYCONTEXT) == false)
        {
            if (GetLastError() == NTE_BAD_KEYSET)
            {
                if (CryptAcquireContext(&hProv, NULL, NULL,
                                       PROV_RSA_AES, CRYPT_NEWKEYSET) == false)
                {
                    throw CRYPTAES_ER_ACQUIRECONTEXT;
                }
            }
            else
            {
                throw CRYPTAES_ER_ACQUIRECONTEXT;
            }
        }

        // 鍵情報をオブジェクトへ格納
        hHeap = GetProcessHeap();
        if (hHeap == NULL)
        {
            throw CRYPTAES_ER_FATAL;
        }

        objKeyBlob = (BYTE *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, iKeySize + 12);
        if (objKeyBlob == NULL)
        {
            throw CRYPTAES_ER_FATAL;
        }


        PUBLICKEYSTRUC *pPublicKeyStruc = (PUBLICKEYSTRUC *)objKeyBlob;

        pPublicKeyStruc->bType = PLAINTEXTKEYBLOB;
        pPublicKeyStruc->bVersion = 2;
        pPublicKeyStruc->aiKeyAlg = CALG_AES_128;
        *((int *)(objKeyBlob + sizeof(PUBLICKEYSTRUC))) = iKeySize;
        memcpy(objKeyBlob + sizeof(PUBLICKEYSTRUC) + 4, binKey, iKeySize);

        // 鍵のインポート
        if (CryptImportKey(hProv, (const BYTE *)objKeyBlob, iKeySize + 12, 0, 0, &hKey) == false)
        {
            throw CRYPTAES_ER_IMPORTKEY;
        }

        HeapFree(hHeap, NULL, objKeyBlob);

        // パディングを設定する。
        dwPadding_mode = PKCS5_PADDING;

        if (!CryptSetKeyParam(
            hKey,                        // 鍵のハンドル
            KP_PADDING,                    // パディング
            (BYTE*)&dwPadding_mode,        // PKCS#5のパディングモード
            0))
        {
            throw CRYPTAES_ER_SETKEYPARAM;
        }


        // 初期化ベクトルを設定する
        //(ただし、このサンプルではオール0。必要に応じて設定すること。)
        char byteIV[16];

        memset(byteIV, 0x00, sizeof(byteIV));

        if (CryptSetKeyParam(hKey, KP_IV, (BYTE *)byteIV, 0) == false)
        {
            throw CRYPTAES_ER_SETKEYPARAM;
        }

        // 暗号化モードをCBCにする。
        byteCryptMode = CRYPT_MODE_CBC;
        if (CryptSetKeyParam(hKey, KP_MODE, (BYTE *)&byteCryptMode, 0) == false)
        {
            throw CRYPTAES_ER_SETKEYPARAM;
        }

        dwEncryptSize = *pInDataSize;


        // 暗号化するデータをコピー
        if (*pOutSize >= (int)dwEncryptSize)
        {
            memset(binOutData, 0x00, *pOutSize);
            memcpy(binOutData, binInData, dwEncryptSize);
        }

        // 暗号化
        // *binOutDataがNULLの場合、dwEncryptSizeに必要なサイズが格納されるのみ。
        if (CryptEncrypt(hKey, NULL, TRUE, NULL, (BYTE *)binOutData, &dwEncryptSize, *pOutSize) == false)
        {
            throw CRYPTAES_ER_ENCRYPT;
        }

        // 暗号化結果のサイズを格納
        // *binOutDataがNULLの場合、dwEncryptSizeに必要なサイズが格納される。
        *pOutSize = dwEncryptSize;

        // リターン値の設定
        iRetCode = CRYPTAES_SUCCESS;
    }
    catch (int iError)
    {
        iRetCode = iError;
    }

    // 後処理
    if (hKey)
    {
        CryptDestroyKey(hKey);
        hKey = NULL;
    }

    if (hProv)
    {
        CryptReleaseContext(hProv, 0);
        hProv = NULL;
    }

    return iRetCode;
}

int CryptAESCBC::Decrypt(BYTE *binKey, int iKeySize, BYTE *binInOutData, int *pInOutSize)
{
    int iRetCode = CRYPTAES_ER_FATAL;
    HCRYPTPROV hProv = NULL;        // コンテキストハンドル
    HCRYPTKEY hKey = NULL;            // 鍵をインポートしたときのハンドル
    BYTE *objKeyBlob = NULL;
    HANDLE hHeap = NULL;
    DWORD dwPadding_mode = 0;
    DWORD byteCryptMode = 0;
    char byteIV[16];
    int iBufSize = 0;
    DWORD dwDecryptSize = 0;

    // 引数チェック
    if (binKey == NULL || iKeySize == 0 || binInOutData == NULL || pInOutSize == NULL)
    {
        return CRYPTAES_ER_PARAM;
    }

    try {
        // コンテキストの取得
        if (CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_AES,
                                        CRYPT_VERIFYCONTEXT) == false)
        {
            if (GetLastError() == NTE_BAD_KEYSET)
            {
                if (CryptAcquireContext(&hProv, NULL, NULL,
                                      PROV_RSA_AES, CRYPT_NEWKEYSET) == false)
                {
                    throw CRYPTAES_ER_ACQUIRECONTEXT;
                }
            }
            else
            {
                throw CRYPTAES_ER_ACQUIRECONTEXT;
            }
        }

        // 鍵情報をオブジェクトへ格納
        hHeap = GetProcessHeap();
        if (hHeap == NULL)
        {
            throw CRYPTAES_ER_FATAL;
        }

        objKeyBlob = (BYTE *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, iKeySize + 12);
        if (objKeyBlob == NULL)
        {
            throw CRYPTAES_ER_FATAL;
        }


        PUBLICKEYSTRUC *pPublicKeyStruc = (PUBLICKEYSTRUC *)objKeyBlob;

        pPublicKeyStruc->bType = PLAINTEXTKEYBLOB;
        pPublicKeyStruc->bVersion = 2;
        pPublicKeyStruc->aiKeyAlg = CALG_AES_128;
        *((int *)(objKeyBlob + sizeof(PUBLICKEYSTRUC))) = iKeySize;
        memcpy(objKeyBlob + sizeof(PUBLICKEYSTRUC) + 4, binKey, iKeySize);

        // 鍵のインポート
        if (CryptImportKey(hProv, (const BYTE *)objKeyBlob, iKeySize + 12, 0, 0, &hKey) == false)
        {
            throw CRYPTAES_ER_IMPORTKEY;
        }

        HeapFree(hHeap, NULL, objKeyBlob);

        // パディングを設定する。
        dwPadding_mode = PKCS5_PADDING;

        if (!CryptSetKeyParam(
            hKey,                        // 鍵のハンドル
            KP_PADDING,                    // パディング
            (BYTE*)&dwPadding_mode,        // PKCS#5のパディングモード
            0))
        {
            throw CRYPTAES_ER_IMPORTKEY;
        }

        // 初期化ベクトルを設定する。
        //(ただし、このサンプルではオール0。必要に応じて設定すること。)
        memset(byteIV, 0x00, sizeof(byteIV));

        if (CryptSetKeyParam(hKey, KP_IV, (BYTE *)byteIV, 0) == false)
        {
            throw CRYPTAES_ER_IMPORTKEY;
        }

        // 暗号化モードをCBCにする。
        byteCryptMode = CRYPT_MODE_CBC;
        if (CryptSetKeyParam(hKey, KP_MODE, (BYTE *)&byteCryptMode, 0) == false)
        {
            throw CRYPTAES_ER_IMPORTKEY;
        }

        // 暗号化されたデータのサイズを設定
        dwDecryptSize = *pInOutSize;

        // 復号化
        if (CryptDecrypt(hKey, NULL, TRUE, NULL, (BYTE *)binInOutData, &dwDecryptSize) == false)
        {
            throw CRYPTAES_ER_DECRYPT;
        }

        // 復号結果サイズを引数に反映
        *pInOutSize = dwDecryptSize;

        // リターン値の設定
        iRetCode = CRYPTAES_SUCCESS;
    }
    catch (int iError)
    {
        iRetCode = iError;
    }

    // 後処理
    if (hKey)
    {
        CryptDestroyKey(hKey);
        hKey = NULL;
    }

    if (hProv)
    {
        CryptReleaseContext(hProv, 0);
        hProv = NULL;
    }

    return iRetCode;
}

 

単発で使えるようにということで、関数一つでコンテキスト取得、パラメータ設定、暗号化・復号化、開放までやっています。

鍵を変えずに暗号化・復号化を複数回する場合は hKey 、鍵を変えるものの暗号化・復号化を複数回する場合は hProv が使いまわせるので、実装の都合により適当に改造してください。インスタンス化するなら、コンストラクタで取得、デストラクタで開放っていう仕様にするのもありかなと。

今回はメンバ変数を持たないようにしたので、わざわざインスタンス化しなくてもいいように、 static 関数にしてそのまま呼べるようにしています。

 

CBCモードにしているため、パディング初期化ベクトルが必要になります。

パディングは、指定しないと、暗号化・復号化時にエラーになります。

初期化ベクトルは、未指定の場合はオール0と同等になるようです。

 

概ねの処理の流れの参考になれば幸いです。

呼び出し側のメイン処理のサンプルは、そのうち追加するかもしれません。