はじめに
Windowsで文字列を暗号化したい状況があった。
過去に書いたコードを思い出しながらググりながらコードを書いたが、どうにもやりにくかった。
Microsoftのサイトも昔とは印象が違って読みにくくなった気がするし、かつて参考にしたサイトがググっても出てこなかった。
ということで、ちょっとメモを残しておいたほうが良いだろうと考えた。
大雑把に言うと
暗号化は自分で処理するわけじゃない。OSに含まれる機能を使うのが便利だ。
WindowsにはCryptography API Next Generationがある。
https://docs.microsoft.com/en-us/windows/desktop/seccng/cng-portal
事前にIV(Initialization Vector)を作っておき、アルゴリズムプロバイダを開いて、暗号化キーを作成する。
あとはそれを使って暗号化または復号すればよい。
処理が終わったら後始末する。
大雑把なコードの形はこうなる。
IVを作る
アルゴリズムプロバイダを開く
if (開けた?)
{
暗号化キーを作る
if (作れた?)
{
暗号化とか復号とか
暗号化キーを破棄する
}
アルゴリズムプロバイダを閉じる
}
ソースコード
C++で書くと、基本的には次のようになる。
言語は案件によって違うだろうが、OS(ここではWindows)のAPIを利用できれば基本的には何でもいい。
#define UNICODE
#define WINVER 0x0601
#include <iostream>
#include <random>
#include <string>
#include <vector>
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
int main(int argc, char **argv)
{
// この部分は暗号化とは関係ない。コンソールの扱いをUTF-8に変更してる。
SetConsoleOutputCP(CP_UTF8);
setvbuf(stdout, nullptr, _IOFBF, 4096);
// 事前にBCryptGenerateSymmetricKeyのpbSecretに渡すデータを定めておく。
// ここでは単純に作成するために1を16バイト並べたデータを使う。
// この部分は、このまま実運用に持ち込んではいけない。
std::vector<uint8_t> secret(16, 1);
// 事前にinitialization vectorを作っておく。
// 両者を同一にしておかないと暗号化したデータを復号できない。
std::random_device rd;
std::uniform_int_distribution<int> uid(0, 0xff);
std::vector<uint8_t> iv0, iv1;
for (auto i = 0; i < 16; i++)
{
auto temp = uid(rd);
iv0.push_back(temp);
iv1.push_back(temp);
}
// アルゴリズムプロバイダを開く。
// BCryptOpenAlgorithmProviderの戻りを確認するのが王道かもしれないが、
// ここではアルゴリズムのハンドルが得られた否かで成否を判定する。
BCRYPT_ALG_HANDLE alg = NULL;
BCryptOpenAlgorithmProvider(&alg, L"AES", NULL, 0);
if (alg != NULL)
{
// 暗号化キーのサイズを確認して領域を確保する
ULONG key_length = 0;
ULONG temp;
BCryptGetProperty(alg, L"ObjectLength", reinterpret_cast<PUCHAR>(&key_length), sizeof(key_length), &temp, 0);
std::vector<uint8_t> key_object(key_length);
// 暗号化キーを作成する
// BCryptGenerateSymmetricKeyの戻りを確認するのが王道かもしれないが、
// ここでは暗号化キーのハンドルが得られた否かで成否を判定する。
BCRYPT_KEY_HANDLE key = NULL;
BCryptGenerateSymmetricKey(alg, &key, &key_object[0], key_length, &secret[0], 16, 0);
if (key != NULL)
{
// これを暗号化のテストに使う。
char *plain_text = u8"あいうえお かきくけこ";
std::cout << u8"元の文字列: " << plain_text << std::endl;
// サイズを確認して領域を確保してから暗号化してみる。
ULONG encrypted_size;
BCryptEncrypt(key, reinterpret_cast<uint8_t *>(plain_text), strlen(plain_text) + 1, NULL, &iv0[0], 16, NULL, 0, &encrypted_size, 0x00000001);
std::vector<uint8_t> encrypted(encrypted_size);
BCryptEncrypt(key, reinterpret_cast<uint8_t *>(plain_text), strlen(plain_text) + 1, NULL, &iv0[0], 16, &encrypted[0], encrypted_size, &encrypted_size, 0x00000001);
// 暗号化後のデータを表示する。
std::cout << u8"暗号化後:";
for (auto i : encrypted)
printf(u8" %02x", i);
std::cout << std::endl;
// サイズを確認して領域を確保してから復号してみる。
ULONG decrypted_size;
BCryptDecrypt(key, &encrypted[0], encrypted_size, NULL, &iv1[0], 16, NULL, 0, &decrypted_size, 0x00000001);
std::vector<char> decrypted(decrypted_size);
BCryptDecrypt(key, &encrypted[0], encrypted_size, NULL, &iv1[0], 16, reinterpret_cast<uint8_t *>(&decrypted[0]), decrypted_size, &decrypted_size, 0x00000001);
// 複合語の文字列を表示する。
std::cout << u8"復号後: " << &decrypted[0] << std::endl;
// 使い終わった暗号化キーを破棄する。
BCryptDestroyKey(key);
}
// 使い終わったアルゴリズムプロバイダを閉じる。
BCryptCloseAlgorithmProvider(alg, 0);
}
return 0;
}
ビルドのコマンドラインは、こんな感じになる。
cl.exe /nologo /utf-8 /std:c++17 /EHsc crypt.cpp
実行すると、例えば、次のように表示される。
元の文字列: あいうえお かきくけこ
暗号化後: b0 21 35 8a 45 23 5d 05 77 18 83 ea 10 b5 66 bc 77 22 42 1d c5 ae 2e 0c 84 fa 2e 5c 36 00 2a 8e 1d b3 cb 8e 67 fd c8 d7 fb 9e ea df 08 28 dd 8c
復号後: あいうえお かきくけこ
その他
-
std::vector<uint8_t> secret(16, 1);
の部分は実運用に持ち込んではいけない。かつてのFFFTPの問題は、これを固定化したから発生したんだと記憶してる。ユーザーに入力させる形にしておくのが良いだろう。 - 今回は何も指定せずデフォルトのままだが、いろいろと設定したくなるだろう。
- 他にも、選択肢がある部分を固定的に書いてる部分がある。例えばブロックサイズの16バイトは、BCryptGetProperty()で確認するのが王道だと思う。
- もっとAPIの戻りを確認してエラーチェックを強化してもいいと思う。
- クラス化したほうが使いやすい。アルゴリズムプロバイダと暗号化キーの初期化部分をコンストラクタに、後始末をデストラクタに、暗号化と復号はメンバー関数ってことで。
- いろいろとあるけど、実運用に持ち込む際にリクエストに応じてコーディングすれば良いと思う。