C++とOpenSSLライブラリを利用してデータの暗号化・復号化をAES-CBCで行う
C++でまともに暗号化・復号化するサンプルをやっている例を見ないので書く。 実際にはsaltがどうだとかパディングがどうだとかストレッチングがどうだとか暗号化する前にデータを圧縮する話だとかは出さない。 運用上は重要だけど、そういうのは後付けできる。 問題はOpenSSLをC++でまともにあつかう例がない方なので、ここではこのはなしをざっくり切ってどのように記述するのかを解説していく。
方針
OpenSSLライブラリを利用する小さなAES-CBCモードに限定したopensslコマンドを実装する。
小さいといっても興が乗って300行程になってしまったが、問題はないだろう。
実際に重要なところは合わせても100行も行かない。
また、ここではC++17前提で実装を進める。
それ以前のC++を利用する方は適宜読み替えて欲しい。
ただ、OpenSSLのライブラリはCで書かれているので、中核となる部分はC++11までならコンパイルできるはず(試していない)。
ただ、それもcstdint
ライブラリ内にあるuint8_t
を用いるためなので、おそらくunsigned char
に変更すれば(char
が8 bitな環境であれば)C++98でもコンパイルできる...かもしれない。
興味があれば試して欲しい。
準備
さて、まずはいくつかデータ型を定義しておく。 まずはbyte型を定義する
#include <cstdint> using byte = uint8_t;
C++17ではstd::byteという型があるが、これよりは符号なし8 bit整数を利用することを明確にしたいのでuint8_tの方を採用する。
さらに、この型を用いてバイトストリーム用の型を定義する。
#include <vector> #include <cstdint> using byte_stream = std::vector<byte>;
8 bitな動的配列は全てこのbyte_stream
から作成する。
また、AES-CBCにおける鍵を表す型を定義する
#include <type_traits> #include <array> #include <cstddef> template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > using key = std::array<byte, key_size / 8>;
template
やstd::enable_if_t
がはいってきて何やらややこしい。
しかし、やりたいことは単純で、鍵長key_size
が128 bitか192 bitか256 bitなものしか鍵の型を生成できないように制限しているだけだ。
また、key_size / 8
をしているのはkey_size
の単位がbitでarrayが格納する要素の型は8 bit長なため、要素数はkey_size
bitを要素の型の長さ8 bitで割ったものとなるためだ。
最後にAES-CBCにはIVが必要となるので、これを表現する型を導入する。
#include <array> const auto BLOCK_SIZE = 16; using iv = std::array<byte, BLOCK_SIZE>;
BLOCK_SIZE == 16
なのはAESではブロック長が128 bit固定であり、IVもそのブロック長にする必要があるので、byte == uint8_t
からその要素数が128 / 8 == 16
となるため。
コマンドのインターフェース
今回用意するコマンドのインターフェースは次のようにしようと思う。
- 標準入力からデータを受け取って、受け取ったデータを暗号化した上で標準出力へ出力するサブコマンド
enc
を持つ - 標準入力からデータを受け取って、受け取ったデータを復号化した上で標準出力へ出力するサブコマンド
dec
を持つ
どちらも標準入力から受け取るので、main関数はこんな感じでいいだろう。
#include <iostream> int main(int argc, char* argv[]) { if (argc < 1) { std::cerr << "no subcommand" << std::endl; } auto subcommand = std::string(argv[1]); if (subcommand == "enc") { } else if (subcommand = "dec") { } }
ただ、これだけだとターミナルに移ったときに使い方がわからないので、help
サブコマンドとついでで-h
オプションをコマンドに追加しておこう。
#include <iostream> #include <unistd.h> #include <string> int main(int argc, char* argv[]) { int opt; // -hオプションを追加するためにgetopt関数を使った while ((opt = getopt(argc, argv, "h")) != -1) { switch (opt) { case 'h': usage(program_path); return 0; default: return 1; } } if (argc < 1) { std::cerr << "no subcommand" << std::endl; } auto subcommand = std::string(argv[1]); if (subcommand == "help") { usage(argv[0]); return 0; } if (subcommand == "enc") { } else if (subcommand = "dec") { } }
usage
という関数はプログラムのパスargv[0]
を受け取ってその使い方を表示してくれる。
#include <filesystem> #include <iostream> auto usage(const std::filesystem::path& program_path) -> void { const auto program_name = program_path.filename().generic_string(); std::cout << "OpenSSLライブラリの使用方法のサンプル" << std::endl << "Usage:" << std::endl << " " << program_name << " enc" << std::endl << " " << program_name << " dec" << std::endl << " " << program_name << " help" << std::endl << " " << program_name << " -h" << std::endl << "Arguments:" << std::endl << " enc" << std::endl << " 標準入力から受け取ったデータを暗号化し、" << std::endl << " 標準出力へ出力する" << std::endl << " dec" << std::endl << " 標準入力から受け取ったデータを復号化し、" << std::endl << " 標準出力へ出力する" << std::endl << " help" << std::endl << " ヘルプを表示する" << std::endl << "Options:" << std::endl << " -h" << std::endl << " ヘルプを表示する" << std::endl; }
AESの暗号化と復号化のインターフェースはいずれも鍵とIV、そしてデータを必要とするので、次のような形で良いだろう。
#include <type_traits> #include <cstddef> template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto encrypt( const key<key_size>& key, const byte_stream& data, const iv& iv ) -> byte_stream; template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto decrypt( const key<key_size>& key, const byte_stream& data, const iv& iv ) -> byte_stream;
いずれも鍵、IV、データを受け取ってデータを暗号化・復号化した結果であるbyte_stream
型なデータを返す。
インターフェースはどちらも同じなので、データを暗号化・復号化した結果を標準出力に返すために次のような関数を用意する
#include <type_traits> #include <functional> #include <stdexcept> #include <algorithm> #include <cstddef> template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto print_data_while_encoding( const byte_stream& original_key, const byte_stream& data, const byte_stream& original_iv, const std::function<byte_stream(const key<key_size>&, const byte_stream&, const iv&)>& encoder ) -> void { iv iv; key<key_size> key; if (iv.size() != original_iv.size()) { throw std::invalid_argument("iv length must be 128 bit "); } if (original_key.size() != key.size()) { throw std::invalid_argument("invalid key size"); } std::copy(original_iv.begin(), original_iv.end(), iv.begin()); std::copy(original_key.begin(), original_key.end(), key.begin()); auto encoded_data = encoder(key, data, iv); for (const auto byte_value : encoded_data) { std::cout << byte_value; } }
これは鍵、IV、データだけでなく、そららのデータを受け取って暗号化・復号化を行う関数encoder
を受け取っている。
途中、byte_stream
型で受け取った鍵とIVのデータ型を変換しているが、それ以外はencoder
に通し、その結果を表示する部分と引数のチェックを行っている部分とわかれている。
OpenSSLによるAES-CBC暗号化・復号化
本題のOpenSSLによる暗号化・復号化に入っていく。
暗号化
最初に暗号化を行うencrypt
関数の実装を行う。
まずは、次のように書く。
#include <openssl/evp.h> #include <type_traits> #include <cstddef> template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto encrypt( const key<key_size>& key, const byte_stream& data, const iv& iv ) -> byte_stream { EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); }
このctx
を暗号化が終わるまで一貫して用いる。
このctx
に対して暗号化モードや必要な設定・鍵・IVなどを保存した上でデータを暗号化・復号化するという仕組みになっている。
初期化コードを書いたらctx
に暗号化スイートを設定しておくようにコーディングする。
#include <openssl/evp.h> #include <type_traits> #include <cstddef> template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto encrypt( const key<key_size>& key, const byte_stream& data, const iv& iv ) -> byte_stream { EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); const EVP_CIPHER* cipher_suite; switch (key_size) { case 128: cipher_suite = EVP_aes_128_cbc(); break; case 192: cipher_suite = EVP_aes_192_cbc(); break; case 256: cipher_suite = EVP_aes_256_cbc(); break; } }
EVP_aes_(key-size)_cbc
という関数がAES-CBC暗号化スイートの設定を作成し、返してくれる。
スイートの設定を作成したら、その設定と鍵、IVをctx
に設定する。
#include <openssl/evp.h> #include <type_traits> #include <cstddef> template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto encrypt( const key<key_size>& key, const byte_stream& data, const iv& iv ) -> byte_stream { EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); const EVP_CIPHER* cipher_suite; switch (key_size) { case 128: cipher_suite = EVP_aes_128_cbc(); break; case 192: cipher_suite = EVP_aes_192_cbc(); break; case 256: cipher_suite = EVP_aes_256_cbc(); break; } EVP_EncryptInit(ctx, cipher_suite, key.data(), iv.data()); }
以上でAES-CBCによる暗号化を行う準備はととのった。 これから実際に暗号化処理を行う部分を実装するが、実は次のようなコードを追加するだけで大部分の実装が終わる。
#include <openssl/evp.h> #include <type_traits> #include <cstddef> template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto encrypt( const key<key_size>& key, const byte_stream& data, const iv& iv ) -> byte_stream { // 初期化処理は省略 auto encrypted_data = byte_stream( raw_data.size() + (BLOCK_SIZE - raw_data.size() % BLOCK_SIZE) ); int len = 0; EVP_EncryptUpdate(ctx, encrypted_data.data(), &len, data.data(), raw_data.size() }
encrypted_data
のデータサイズの調整をおこなっている式についてはすぐ後で触れるので、ここでは忘れて欲しい。
最後に、データの後処理を行う。
#include <openssl/evp.h> #include <type_traits> #include <cstddef> template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto encrypt( const key<key_size>& key, const byte_stream& data, const iv& iv ) -> byte_stream { // 初期化処理と暗号化処理は省略 EVP_EncryptFinal(ctx, encrypted_data.data() + len, &len) EVP_CIPHER_CTX_free(ctx); return encrypted_data; }
最後にこのような処理をしなければならないのはAESではデータサイズがブロック長単位になっている必要があり、その部分の暗号化処理を行う必要があるからだ。 AESではブロック長は128 bitであることを思いだそう。 AESでは暗号化・復号化処理は1ブロック毎に行われる。 つまり、AESで処理されるデータ長は128 bit単位になっていなければならない。 しかし、データ長のbit数がAESで処理する128 bit長で割り切れない、すなわちデータ長が128 bit単位になっていないとき、最後1つ手前の128 bit長のブロックを処理したあとにはみ出した分のデータを処理するときにそのはみ出したデータの長さはちょうど128 bitでない場合がある。 そもそもほとんどのデータはちょうどそのデータ長が128 bitで割り切れるということは稀だろう。 そこで、パディングというある一定のルールに乗っ取ったデータをデータの末尾に追加するという処理を施してデータ長を128 bit長に帳尻合わせする必要がでてくる。 しかし、ここではOpenSSLがどのパディングを使用するかやそのパディングの詳細については述べない。 各自興味があれば調べて欲しい。
実はデータ長がピッタリ128 bitのながさであってももう1ブロック分のパディング処理が行われる。 これには理由があって、パディングがあるデータとないデータの区別をするのを頑張るよりは、128 bit長ちょうどのデータに対しても1ブロック分のパディング処理を施しておくことで処理を共通して行える上にどこからどこまでがパディングでデータであるかの区別を見分けやすくできるというメリットがある。
とにかく、最後のはみだしたデータはパディングという処理を施した上でまだ暗号化されていないはみだしたデータを暗号化する必要がある。
そこで、len
に暗号化できたデータ長を保存しておき、encrypted_data
の先頭アドレスencrypted_data.data()
にlen
分足した、パディングが施されたはみだしているデータに対して暗号化するという形をとる。
先程のencrypted_data
のサイズを調整しているのはこの処理のためである。
元々はみだすデータサイズはデータサイズ % ブロック長(128 bit)
でわかるので、パディングされるデータはブロック長(128 bit) - データサイズ % ブロック長(128 bit)
で求められるので、パディング分だけ保存する配列を伸ばしておく必要があったのだ。
最後に、ctr
は動的にメモリー確保されてているのでEVP_CIPHER_CTX_free
でメモリーを開放する。
このとき、設定された鍵やIV以外のOpenSSL側で用意するEVP_aes_key-size_cbc
のような動的に確保されているメモリーが開放される。
まとめると、encrypt
関数は次のような形になる。
template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto encrypt( const key<key_size>& key, const byte_stream& raw_data, const iv& iv ) -> byte_stream { auto ctx = EVP_CIPHER_CTX_new(); if (ctx == nullptr) { throw "cannot initialize OpenSSL cipher context"; } const EVP_CIPHER* cipher_suite; switch (key_size) { case 128: cipher_suite = EVP_aes_128_cbc(); break; case 192: cipher_suite = EVP_aes_192_cbc(); break; case 256: cipher_suite = EVP_aes_256_cbc(); break; } if (cipher_suite == nullptr) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot get cipher suite"; } if (EVP_EncryptInit(ctx, cipher_suite, key.data(), iv.data()) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot construct OpenSSL cipher context"; } auto encrypted_data = byte_stream( raw_data.size() + (BLOCK_SIZE - raw_data.size() % BLOCK_SIZE) ); int len = 0; if (EVP_EncryptUpdate(ctx, encrypted_data.data(), &len, raw_data.data(), raw_data.size()) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot encrypt data"; } if (EVP_EncryptFinal(ctx, encrypted_data.data() + len, &len) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot padding"; } EVP_CIPHER_CTX_free(ctx); return encrypted_data; }
解説では省いていたエラー処理をここでは入れてある。
復号化
実は暗号化処理と復号化処理はほとんど同じような操作でできるので、解説することは少ない。 なので、いきなりコードを載せてしまう。
template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto decrypt( const key<key_size>& key, const byte_stream& encrypted_data, const iv& iv ) -> byte_stream { EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); if (ctx == nullptr) { throw "cannot initialize OpenSSL cipher context"; } const EVP_CIPHER* cipher_suite; switch (key_size) { case 128: cipher_suite = EVP_aes_128_cbc(); break; case 192: cipher_suite = EVP_aes_192_cbc(); break; case 256: cipher_suite = EVP_aes_256_cbc(); break; } if (cipher_suite == nullptr) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot get cipher suite"; } if (EVP_DecryptInit(ctx, cipher_suite, key.data(), iv.data()) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot construct OpenSSL cipher context"; } auto plain_data = byte_stream(encrypted_data.size()); int plain_data_len = 0; if (EVP_DecryptUpdate(ctx, plain_data.data(), &plain_data_len, encrypted_data.data(), encrypted_data.size()) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot encrypt data"; } int remain_plain_data_len; if (EVP_DecryptFinal(ctx, plain_data.data() + plain_data_len, &remain_plain_data_len) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot padding"; } plain_data.resize(plain_data_len + remain_plain_data_len); EVP_CIPHER_CTX_free(ctx); return plain_data; }
ほとんどEVP_EncryptXXX
がEVP_DecryptXXX
になっただけである。
ただ、最後にデータからパディングをはがす処理が異なっているだけである。
ここではそのパディングをはがす処理について解説する。
まず、plain_data_len
には暗号化時にははみだしていなかったデータの復号化後の長さが入っている。
また、remain_plain_data_len
にはパデイングデータをのぞいた暗号化時にはみだしていたデータ分の長さがはいる。
つまり、暗号化前のデータ長はplain_data_len + remain_plain_data_len
である。
パディング処理によってデータの末尾には余計なデータがくっついているので、byte_stream
がvetctor
であることを利用してresize
メソッドによってデータ長を暗号化前のデータ長に合わせてやる。
これによって真の復号化データを得ることができる。
コマンドの拡充
暗号化と復号化はできるようになったが、コマンドは最低限のインターフェースを供えただけで標準入力からデータを読み込んだり、出力したりIVや鍵を設定したりなどの部分が抜け落ちている。 したがってここではこれらを実装していくことにしよう。
まず、IVや鍵を設定しよう。
ただ、これは確認のためであれば全部を0埋めしたデータにしておけばよいだろうし、0埋めだけだと不安なので、デフォルトは0埋めにしておいて、あとでオプションから設定できれば良いだろう。
そこで-i
オプションでIVを受け取り、-k
で鍵を受け取るようにしよう。
ただし、これらはuint8_tな配列でなければならないので、これらオプションに指定する引数はhex string(16進数な数字の羅列)で受け取るようにする。
#include <iostream> #include <cstddef> #include <cstdint> #include <type_traits> #include <unistd.h> #include <string> #include <stdexcept> #include <algorithm> auto convert_hex_string(const std::string& hex_string) -> byte_stream { if (hex_string.size() % 2 != 0) { throw std::invalid_argument("invalid hex string"); } byte_stream byte_array(hex_string.size() / 2); for (std::size_t i = 0; i < hex_string.size(); i += 2) { auto&& byte_value = static_cast<uint8_t>( std::stoi(hex_string.substr(i, 2), nullptr, 16) ); byte_array[i / 2] = byte_value; } return byte_array; } int main(int argc, char* argv[]) { const auto program_path = argv[0]; byte_stream iv(BLOCK_SIZE, 0); byte_stream key(128 / 8, 0); int opt; while ((opt = getopt(argc, argv, "hi:k:V")) != -1) { switch (opt) { case 'h': usage(program_path); return 0; case 'i': try { iv = convert_hex_string(optarg); } catch (const std::invalid_argument& error) { std::cerr << "invalid iv format" << std::endl; return 1; } catch (const std::out_of_range& error) { std::cerr << error.what() << std::endl; return 1; } if (iv.size() != BLOCK_SIZE) { std::cerr << "iv must be 128 bit" << std::endl; } break; case 'k': try { key = convert_hex_string(optarg); } catch (const std::invalid_argument& error) { std::cout << "invalid key format" << std::endl; return 1; } catch (const std::out_of_range& error) { std::cerr << error.what() << std::endl; return 1; } if (!(key.size() == 128 / 8 || key.size() == 192 / 8 || key.size() == 256 / 8)) { std::cerr << "key size must be 128, 192 or 256 bit" << std::endl; } break; default: return 1; } } if (argc - optind < 1) { std::cerr << "no subcommand" << std::endl; return 1; } const auto subcommand = std::string(argv[optind]); if (subcommand == "help") { usage(program_path); return 0; } if (subcommand == "enc") { } else if (subcommand == "dec") { } else { std::cerr << "unknown command: " << subcommand << std::endl; return 1; } }
convert_hex_string
が受け取ったhex stringを符号無し8 bit整数配列に直す関数である。
この関数は2文字ずつ読んでいき、その2文字を8 bit整数を表す16進数文字列だと見なして符号無し8 bit整数に変換し、それをvectorにつめていくというものになっている。
また、-i
と-v
はそれぞれhex stringを引数として受け取り、convert_hex_string
にかけて符号無し8 bit整数配列に変換したうえで、各鍵長、IV長に当っているかをみている。
-i
と-v
オプションを追加したので、usage
関数をアップデートする。
auto usage(const std::filesystem::path& program_path) -> void { const auto program_name = program_path.filename().generic_string(); std::cout << "OpenSSLライブラリの使用方法のサンプル" << std::endl << "Usage:" << std::endl << " " << program_name << " enc" << std::endl << " " << program_name << " dec" << std::endl << " " << program_name << " help" << std::endl << " " << program_name << " -h" << std::endl << "Arguments:" << std::endl << " enc" << std::endl << " 標準入力から受け取ったデータを暗号化し、" << std::endl << " 標準出力へ出力する" << std::endl << " dec" << std::endl << " 標準入力から受け取ったデータを復号化し、" << std::endl << " 標準出力へ出力する" << std::endl << " help" << std::endl << " ヘルプを表示する" << std::endl << "Options:" << std::endl << " -h" << std::endl << " ヘルプを表示する" << std::endl << " -i <iv>" << std::endl << " <iv>で指定された値でIVを作成する。" << std::endl << " <iv>はHex String形式(例: AB01DF93BC)で" << std::endl << " 長さが128bitになるようにする必要がある" << std::endl << " -k <key>" << std::endl << " <key>で指定された値で鍵を作成する。" << std::endl << " <key>はhex String形式で長さが128 bit," << std::endl << " 192 bit, 256 bitのいずれかの" << std::endl << " 長さになる必要がある。" << std::endl; }
最後に、データを標準入力からうけとる部分と、暗号化・復号化したものを標準出力へ出力する部分を実装しよう。 データは符号無し8 bit配列に収めてやる。
#include <cstdio> #include <cstdint> int main(int argc, char* argv[]) { // オプション処理部分等は省略 byte_stream data; while (true) { auto byte_value = getc(stdin); if (byte_value == EOF) { break; } data.emplace_back(byte_value); } // データの暗号化・復号化部分は省略 }
データを暗号化・復号化した上で標準出力に出力するようにしてやる。
#include <iostream> #include <cstdint> int main(int argc, char* argv[]) { // オプション処理部分やデータの入力部分は省略 if (subcommand == "enc") { switch (key.size() * 8) { case 128: print_data_while_encoding<128>(key, data, iv, encrypt<128>); break; case 192: print_data_while_encoding<192>(key, data, iv, encrypt<192>); break; case 256: print_data_while_encoding<256>(key, data, iv, encrypt<256>); break; default: std::cout << "invalid key type" << std::endl; return 1; } } else if (subcommand == "dec") { switch (key.size() * 8) { case 128: print_data_while_encoding<128>(key, data, iv, decrypt<128>); break; case 192: print_data_while_encoding<192>(key, data, iv, decrypt<192>); break; case 256: print_data_while_encoding<256>(key, data, iv, decrypt<256>); break; default: std::cout << "invalid key type" << std::endl; return 1; } } else { std::cerr << "unknown command: " << subcommand << std::endl; return 1; } }
コードのまとめ
以上のコードをまとめると、最終的には次のようなコードができている
#include <openssl/evp.h> #include <iostream> #include <vector> #include <array> #include <cstdint> #include <cstddef> #include <type_traits> #include <unistd.h> #include <string> #include <filesystem> #include <stdexcept> #include <algorithm> #include <functional> #include <cstdio> const auto BLOCK_SIZE = 16; using byte = uint8_t; using byte_stream = std::vector<byte>; template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > using key = std::array<byte, key_size / 8>; using iv = std::array<byte, BLOCK_SIZE>; template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto encrypt( const key<key_size>& key, const byte_stream& raw_data, const iv& iv ) -> byte_stream { auto ctx = EVP_CIPHER_CTX_new(); if (ctx == nullptr) { throw "cannot initialize OpenSSL cipher context"; } const EVP_CIPHER* cipher_suite; switch (key_size) { case 128: cipher_suite = EVP_aes_128_cbc(); break; case 192: cipher_suite = EVP_aes_192_cbc(); break; case 256: cipher_suite = EVP_aes_256_cbc(); break; } if (cipher_suite == nullptr) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot get cipher suite"; } if (EVP_EncryptInit(ctx, cipher_suite, key.data(), iv.data()) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot construct OpenSSL cipher context"; } auto encrypted_data = byte_stream( raw_data.size() + (BLOCK_SIZE - raw_data.size() % BLOCK_SIZE) ); int len = 0; if (EVP_EncryptUpdate(ctx, encrypted_data.data(), &len, raw_data.data(), raw_data.size()) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot encrypt data"; } if (EVP_EncryptFinal(ctx, encrypted_data.data() + len, &len) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot padding"; } EVP_CIPHER_CTX_free(ctx); return encrypted_data; } template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto decrypt( const key<key_size>& key, const byte_stream& encrypted_data, const iv& iv ) -> byte_stream { EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new(); if (ctx == nullptr) { throw "cannot initialize OpenSSL cipher context"; } const EVP_CIPHER* cipher_suite; switch (key_size) { case 128: cipher_suite = EVP_aes_128_cbc(); break; case 192: cipher_suite = EVP_aes_192_cbc(); break; case 256: cipher_suite = EVP_aes_256_cbc(); break; } if (cipher_suite == nullptr) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot get cipher suite"; } if (EVP_DecryptInit(ctx, cipher_suite, key.data(), iv.data()) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot construct OpenSSL cipher context"; } auto plain_data = byte_stream(encrypted_data.size()); int plain_data_len = 0; if (EVP_DecryptUpdate(ctx, plain_data.data(), &plain_data_len, encrypted_data.data(), encrypted_data.size()) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot encrypt data"; } int remain_plain_data_len; if (EVP_DecryptFinal(ctx, plain_data.data() + plain_data_len, &remain_plain_data_len) == 0) { EVP_CIPHER_CTX_cleanup(ctx); throw "cannot padding"; } plain_data.resize(plain_data_len + remain_plain_data_len); EVP_CIPHER_CTX_free(ctx); return plain_data; } auto convert_hex_string(const std::string& hex_string) -> byte_stream { if (hex_string.size() % 2 != 0) { throw std::invalid_argument("invalid hex string"); } byte_stream byte_array(hex_string.size() / 2); for (std::size_t i = 0; i < hex_string.size(); i += 2) { auto&& byte_value = static_cast<uint8_t>( std::stoi(hex_string.substr(i, 2), nullptr, 16) ); byte_array[i / 2] = byte_value; } return byte_array; } auto usage(const std::filesystem::path& program_path) -> void { const auto program_name = program_path.filename().generic_string(); std::cout << "OpenSSLライブラリの使用方法のサンプル" << std::endl << "Usage:" << std::endl << " " << program_name << " enc" << std::endl << " " << program_name << " dec" << std::endl << " " << program_name << " help" << std::endl << " " << program_name << " -h" << std::endl << "Arguments:" << std::endl << " enc" << std::endl << " 標準入力から受け取ったデータを暗号化し、" << std::endl << " 標準出力へ出力する" << std::endl << " dec" << std::endl << " 標準入力から受け取ったデータを復号化し、" << std::endl << " 標準出力へ出力する" << std::endl << " help" << std::endl << " ヘルプを表示する" << std::endl << "Options:" << std::endl << " -h" << std::endl << " ヘルプを表示する" << std::endl << " -i <iv>" << std::endl << " <iv>で指定された値でIVを作成する。" << std::endl << " <iv>はHex String形式(例: AB01DF93BC)で" << std::endl << " 長さが128bitになるようにする必要がある" << std::endl << " -k <key>" << std::endl << " <key>で指定された値で鍵を作成する。" << std::endl << " <key>はhex String形式で長さが128 bit," << std::endl << " 192 bit, 256 bitのいずれかの" << std::endl << " 長さになる必要がある。" << std::endl; } template < size_t key_size, std::enable_if_t<key_size == 128 || key_size == 192 || key_size == 256, std::nullptr_t> = nullptr > auto print_data_while_encoding( const byte_stream& original_key, const byte_stream& data, const byte_stream& original_iv, const std::function<byte_stream(const key<key_size>&, const byte_stream&, const iv&)>& encoder ) -> void { iv iv; key<key_size> key; if (iv.size() != original_iv.size()) { throw std::invalid_argument("iv length must be 128 bit "); } if (original_key.size() != key.size()) { throw std::invalid_argument("invalid key size"); } std::copy(original_iv.begin(), original_iv.end(), iv.begin()); std::copy(original_key.begin(), original_key.end(), key.begin()); auto encoded_data = encoder(key, data, iv); for (const auto byte_value : encoded_data) { std::cout << byte_value; } } int main(int argc, char* argv[]) { const auto program_path = argv[0]; byte_stream iv(BLOCK_SIZE, 0); byte_stream key(128 / 8, 0); int opt; while ((opt = getopt(argc, argv, "hi:k:V")) != -1) { switch (opt) { case 'h': usage(program_path); return 0; case 'i': try { iv = convert_hex_string(optarg); } catch (const std::invalid_argument& error) { std::cerr << "invalid iv format" << std::endl; return 1; } catch (const std::out_of_range& error) { std::cerr << error.what() << std::endl; return 1; } if (iv.size() != BLOCK_SIZE) { std::cerr << "iv must be 128 bit" << std::endl; } break; case 'k': try { key = convert_hex_string(optarg); } catch (const std::invalid_argument& error) { std::cout << "invalid key format" << std::endl; return 1; } catch (const std::out_of_range& error) { std::cerr << error.what() << std::endl; return 1; } if (!(key.size() == 128 / 8 || key.size() == 192 / 8 || key.size() == 256 / 8)) { std::cerr << "key size must be 128, 192 or 256 bit" << std::endl; } break; default: return 1; } } if (argc - optind < 1) { std::cerr << "no subcommand" << std::endl; return 1; } const auto subcommand = std::string(argv[optind]); if (subcommand == "help") { usage(program_path); return 0; } byte_stream data; while (true) { auto byte_value = getc(stdin); if (byte_value == EOF) { break; } data.emplace_back(byte_value); } try { if (subcommand == "enc") { switch (key.size() * 8) { case 128: print_data_while_encoding<128>(key, data, iv, encrypt<128>); break; case 192: print_data_while_encoding<192>(key, data, iv, encrypt<192>); break; case 256: print_data_while_encoding<256>(key, data, iv, encrypt<256>); break; default: std::cout << "invalid key type" << std::endl; return 1; } } else if (subcommand == "dec") { switch (key.size() * 8) { case 128: print_data_while_encoding<128>(key, data, iv, decrypt<128>); break; case 192: print_data_while_encoding<192>(key, data, iv, decrypt<192>); break; case 256: print_data_while_encoding<256>(key, data, iv, decrypt<256>); break; default: std::cout << "invalid key type" << std::endl; return 1; } } else { std::cerr << "unknown command: " << subcommand << std::endl; return 1; } } catch (const char* error) { std::cerr << error << std::endl; return 1; } }
ビルド
最後にビルドするが、ファイル名をmain.cpp
として保存した上で次のようなコマンドでビルドできる。
$ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp
OpenSSL 1.1ではlibssl
とlibcrypto
をリンクしなければならないが、OpenSSL 1.0では次のコマンドのようにlibsslだけリンクするようにしてビルドすれば良い。
$ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lssl main.cpp
もしlibsslをリンクできなければfind / -name 'libssl.so*'
を、libcryptoをリンクできなければfind / -name 'libcrypto.so*'
を実行し、次のように実行することでリンクできる。
# libsslがリンクできなければ`-L libssl.so*のファイルがあるディレクトリへのパス`をコマンドに追加する(`*`はワイルドカード) $ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp -L libssl.so*のファイルがあるディレクトリのパス # libcryptoがリンクできなければ`-L libcrypto.so*のファイルがあるディレクトリへのパス`をコマンドに追加する $ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp -L libcrypto.so*のファイルがあるディレクトリのパス # どちらもなければ`-L libssl.so*のファイルがあるディレクトリへのパス -L libcrypto.so*のファイルがあるディレクトリのパス`をコマンドに追加する $ g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp -L libcrypto.so*のファイルがあるディレクトリのパス -L libssl.so*のファイルがあるディレクトリへのパス
こうしてできたバイナリを実行する際には先程追加したディレクトリへのパスをLD_LIBRARY_PATHに指定してやる必要がある(LD_LIBRARY_PATH
は場合によっては優先度の高いディレクトリに差し替え用のライブラリを差し込まれて攻撃に利用される可能性があるため、十分に注意してとりあつかう)。
# libsslがリンクできなければこのコマンド $ env LD_LIBRARY_PATH=libssl.so*のファイルのあるディレクトリへのパス:$LD_LIBRARY_PATH ビルドしたコマンド # libcryptoがリンクできなければこのコマンド $ env LD_LIBRARY_PATH=libcrypto.so*のファイルのあるディレクトリへのパス:$LD_LIBRARY_PATH ビルドしたコマンド # どちらもない場合 $ env LD_LIBRARY_PATH=libssl.so*のファイルのあるディレクトリへのパス:libcrypto.so*のファイルのあるディレクトリへのパス:$LD_LIBRARY_PATH ビルドしたコマンド
もし、openssl/evp.h
をインクルードできなければ、find / -name 'evp.h'
を実行し、出力されたパスを元に-I openssl/evp.hのあるディレクトリのパス
というオプションを追加してビルドする
g++ -std=c++17 -Wall -Wextra -pedantic -O2 -lcrypto -lssl main.cpp -I openssl/evp.hのあるディレクトリのパス
opensslコマンドによるテスト
最後に、実装した暗号化・復号化するコマンドの出力がopensslコマンドによって暗号化・復号化をできるかを試してみる。
まず暗号化。
$ bash $ iv="$(head -c 16 /dev/urandom | od -x -A n | tr -d ' \n')"; key="$(head -c 32 /dev/urandom | od -x -A n | tr -d ' \n')"; head -c 1000 /dev/urandom | base64 | tr -d '\n' | xargs echo | tee /dev/stderr | ./a.out -i "$iv" -k "$key" enc | openssl aes-256-cbc -nosalt -d -iv "$iv" -K "$key" | xargs echo 27mAkzccTx6UudmFio9DOhzKnvJYzvKfZ01JhARg6SdGo7XpvRyCoYMg9r9by3Oa/fdHo3ORYlZmVbjFPPndalwyDotG2G77OyaFxsdtfyAgI2lFo3DhpN9tqCWVhyd1pFngjs8r+5LoYzxyTKzEl8gVdyLD8T7KXx9SfcHK1xYiTHQ7/OFcwB5/5TtUBhImTFOge9DT8DOQUapWVkYf6f5ObveroBygCB1r5WinWo8zdeXJVzbO43hakhDmZoUcdrWecStgzodNVIHXMx1GbS7fJA9i+jnHSd2SmUxiWjrAm0STlvLsXNlVsXG1In7ovqZihlNntheSMPBtR+RSkfRjr0v7Mxtn5WIwcSPBv1jZyKZ9V81fYDA7CFKTF9eZo8dsk7iGZmzAQL4gIrn5mbwzdFbTsSbleeZIW6j/FVB+iGAHunTgpBQio5VvsX/+d+xir14/4BXeYLSmH3VWPxgHo+d/yxV8QY/1vKEcKs99OsI8BVaXMYe5Pc+CeL5rsUqY5yDI5jPw2ww5iaiM75a0/Li7V5cJTSx0fTMyUzT6aj8A3NykOGmC4HxN3o/gvD6FU/aOE1NqlQrYZTYSyOKyncZH0cP/rP1lFsv6/fBsvoXSB3grcppoGnWKMWRMnMN/JaUlF/rFsJE8/b+5fABqQsW58X7x5oYnRbluwtE8iPepSbCRww+zJWujSftYn2QjRnc4zOGHVdJPMaStSGWeiJOyfS5F9ezr6b6Vn2zLPoY3xAC9b23evnHbGSmSnKLfkDbhjF6jpg+c/T8khpFf4ayNYcn4qnt1hBgqlDCnB3/GcDEoXdc0A8xAMRXg7Mk8tLTxoasx7eUSB3T9Oi4YKdk7ZGhGbecPb5CjNcDjBchRFJG1tfnInR596Y8IOVmxTKKBS0lZ0ImINGN1oE3HEewSXK3+JFeGsWw/Pv9hSESvSm/7IiZKI1piNJlkgMESkYq/y6Wy+cE887qfLmIXmXh0pmbJLuHM/QgIy2DivIJhOOnSWNFNOokcYZo1KqZ7HxZ28A64hhbZB00YxIh66bK/C1LgglOPvtr8xItadST5ENLwofezMhMdDlntju9EgJWM/ZKAsVaNxQY8BCI0IoouxnOGelt6AmbKWM3HHNn4n7t+WZIQQyphHcjdp4V+ZLPg526ikDja+eu8bAFtb5TW3WGP8x6jsDDvlfm6tRBg39BJWnWJtbfUYyRN4/JDGHpjPuKb3P7E3JjBx8IXXmvIqBaiNBnbC5zxLtSg4uFIqGKOeCithuaXA7zlj1KEaY4h1Yru9JHD5e3kAnniQzExlopNg7cbEEZEvlf0NAsdKxCfHg== 27mAkzccTx6UudmFio9DOhzKnvJYzvKfZ01JhARg6SdGo7XpvRyCoYMg9r9by3Oa/fdHo3ORYlZmVbjFPPndalwyDotG2G77OyaFxsdtfyAgI2lFo3DhpN9tqCWVhyd1pFngjs8r+5LoYzxyTKzEl8gVdyLD8T7KXx9SfcHK1xYiTHQ7/OFcwB5/5TtUBhImTFOge9DT8DOQUapWVkYf6f5ObveroBygCB1r5WinWo8zdeXJVzbO43hakhDmZoUcdrWecStgzodNVIHXMx1GbS7fJA9i+jnHSd2SmUxiWjrAm0STlvLsXNlVsXG1In7ovqZihlNntheSMPBtR+RSkfRjr0v7Mxtn5WIwcSPBv1jZyKZ9V81fYDA7CFKTF9eZo8dsk7iGZmzAQL4gIrn5mbwzdFbTsSbleeZIW6j/FVB+iGAHunTgpBQio5VvsX/+d+xir14/4BXeYLSmH3VWPxgHo+d/yxV8QY/1vKEcKs99OsI8BVaXMYe5Pc+CeL5rsUqY5yDI5jPw2ww5iaiM75a0/Li7V5cJTSx0fTMyUzT6aj8A3NykOGmC4HxN3o/gvD6FU/aOE1NqlQrYZTYSyOKyncZH0cP/rP1lFsv6/fBsvoXSB3grcppoGnWKMWRMnMN/JaUlF/rFsJE8/b+5fABqQsW58X7x5oYnRbluwtE8iPepSbCRww+zJWujSftYn2QjRnc4zOGHVdJPMaStSGWeiJOyfS5F9ezr6b6Vn2zLPoY3xAC9b23evnHbGSmSnKLfkDbhjF6jpg+c/T8khpFf4ayNYcn4qnt1hBgqlDCnB3/GcDEoXdc0A8xAMRXg7Mk8tLTxoasx7eUSB3T9Oi4YKdk7ZGhGbecPb5CjNcDjBchRFJG1tfnInR596Y8IOVmxTKKBS0lZ0ImINGN1oE3HEewSXK3+JFeGsWw/Pv9hSESvSm/7IiZKI1piNJlkgMESkYq/y6Wy+cE887qfLmIXmXh0pmbJLuHM/QgIy2DivIJhOOnSWNFNOokcYZo1KqZ7HxZ28A64hhbZB00YxIh66bK/C1LgglOPvtr8xItadST5ENLwofezMhMdDlntju9EgJWM/ZKAsVaNxQY8BCI0IoouxnOGelt6AmbKWM3HHNn4n7t+WZIQQyphHcjdp4V+ZLPg526ikDja+eu8bAFtb5TW3WGP8x6jsDDvlfm6tRBg39BJWnWJtbfUYyRN4/JDGHpjPuKb3P7E3JjBx8IXXmvIqBaiNBnbC5zxLtSg4uFIqGKOeCithuaXA7zlj1KEaY4h1Yru9JHD5e3kAnniQzExlopNg7cbEEZEvlf0NAsdKxCfHg==
次に復号化。
$ bash $ iv="$(head -c 16 /dev/urandom | od -x -A n | tr -d ' \n')"; key="$(head -c 32 /dev/urandom | od -x -A n | tr -d ' \n')"; head -c 1000 /dev/urandom | base64 | tr -d '\n' | xargs echo | tee /dev/stderr | openssl aes-256-cbc -nosalt -e -iv "$iv" -K "$key" | ./a.out -i "$iv" -k "$key" dec | xargs echo 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
あっていそうだ。
スクリプトでやってることは簡単で