大事なソフトウェアデータの暗号/復号化、安全な暗号化ってなんなのか?利用する秘密鍵はどうすればいいのか?


はじめに

趣味でWebサーバーを立てました。立てたはいいんですが、例えばDBアクセスで使うDBのパスワードが設定ファイルに直書きだったりしています。
「DB内のハッシュ化は気にするのにここは平文か~」と暗号化することにしたのは別にいいのですが、「じゃあ安全な暗号化ってなんなのさ!」って考え出したらきりが無くなったのでここで吐き出しておきます。

何故人は暗号化するのか?理由とその際の留意点

端的に言うと、大事な情報を他人に知られたくないから。

Webショッピングで考える

例えばWebショッピングをするために外部Webサイトにクレジットカード情報を登録したいってなりました。この時登録するカード番号、ばれたくないですよね。
Webサイトに情報を登録するための流れはこんな感じでしょうかね。
1. インターネットを介してカード情報を外部Webサイトに送信
2. Webサイト側がカード会社に情報を確認
3. Webサイト側が何かしらの情報として保存

仮に3はDBサーバーに保存しているとしましょう。この時以下のように色々なところにカード情報が流れることになります。

  • 自分の端末 -- インターネット --> Webサイト
  • Webサイト <-- インターネット --> カード会社
  • Webサイト -- ローカルorインターネット --> DBサーバー(保存)

書き出すとそれなりに登場人物がいて、その間にやりとりが発生してるのが分かりますね。
「なんかめっちゃ私のカード情報がやり取りされてるじゃん!ばれたくないんだけど!?」って思いますよね。

Webショッピングでやり取りされる情報、どうやってばれないようにしてるの?

そんな時に登場するのが暗号化です。上記のやり取りも普通はこんな感じになるかと思います。

  • 自分の端末 -- HTTPS通信 --> Webサイト
  • Webサイト <-- HTTPS通信 --> カード会社
  • Webサイト -- 暗号化・ハッシュ化した上で何らかの通信 --> DBサーバー(保存)

インターネット上に流れるデータはHTTPSにより暗号化さればれないようになっています。
また、Webサイトに何か情報を保存しないといけない場合でも、普通は暗号化・ハッシュ化してばれないようにしています。
うちは平文で保存しているって?またまた御冗談を

暗号化とハッシュ化の違い

暗号化とハッシュ化の違いは、端的にいうと元に戻すこと(復号化)が出来るか出来ないか。
整数nに対する暗号化とハッシュ化の超シンプルな例を上げるとこんな感じ。

補足
暗号化 2n+1の値に変換する x/2-1で元に戻せる
ハッシュ化 n%10に変換する 1と11が同じ値になるので元に戻せない

情報がばれたくない!となった場合、元に戻す必要がないならハッシュ化が安全です。
(ハッシュ化もレインボーテーブルを使った元の値の類推方法がありますが割愛)
(ハッシュだからって簡単に同じ値を作り出せるなら意味ないので、ハッシュアルゴリズムを使いましょう。言語によってはBCryptってのが強いらしいです。)

じゃあ元に戻す必要がある場合はどうするのか?

ハッシュ化せずに暗号化する場合、大事なのは秘密鍵の扱い

暗号化をする際に出てくるキーワードとして秘密鍵があります。暗号化をするに辺り、なんの追加情報もなく暗号化してあると仕組みさえわかれば復号出来ちゃいます。
ここで登場するのが秘密鍵。先ほどの例でいうなら2n+1の値に変換する2n+x+1の値に変換。xは別パラメーターとして代入みたいにして、xの値を秘密鍵として挿入する感じ。

なので暗号化したデータとアルゴリズムがばれたとしても、秘密鍵さえなければ、暗号化データがばれても復号できない仕組みになっています。
裏を返せば、如何に暗号化で使う秘密鍵をばれないようにするのか?が肝になります。

秘密鍵の管理をどうすればいいのか?

じゃあこの大事な暗号化の為の秘密鍵、どうやって扱っているんでしょうか。まずは既存の仕組みから考えてみます。
こういった考え方で使えるものを取り入れていくのがいいのかな。

HTTPSでの暗号化

HTTPS上で暗号化通信を実現する際には、以下のように2種類の鍵を使っています。

  1. サーバーの秘密鍵で、実際の通信で利用する共通鍵を暗号化してクライアントと共有
  2. そこから先の通信に対する暗号化は共通鍵を利用する

もちろん共有鍵はその度に作成され、共有されます。
なので、実際の通信を復号するには、秘密鍵を手に入れてクライアントと共有した共通鍵を復号して共通鍵を利用して実際の通信を復号といったステップを踏まないといけません。
暗号アルゴリズムがDH、ECDHだったりすると、共有された共通鍵をそのまま使って暗号化せずに、クライアントとサーバーだけがその鍵を元に同じ鍵を生成出来るようになっています。よくできていますね。
(共通鍵って表現良くなかったな)

どちらも共通で言えることはこんなところですかね。
- 保存している秘密鍵でそのまま暗号化するわけではない
- 実際に使う鍵は通信毎にクライアント-サーバー間しか分からないような工夫がされている。

仮想通貨での秘密鍵管理

ウォレット

秘密鍵の管理でググると、かなり仮想通貨の話が引っかかりますね。仮想通貨ではウォレットと呼ばれるもので通貨を管理しており、ウォレットを使う為に秘密鍵がセットで語られているようです。
秘密鍵の管理こそセキュリティの要|投資家のための仮想通貨セキュリティ入門講座
そんな鍵管理で出てくるコールドウォレットとマルチシグという考え方があります。
コールドウォレット、マルチシグって何? 今さら聞けない仮想通貨の基礎知識

上記参考サイトより抜粋。分かりやすい。

参考に出来る考え方はこんなところですかね。
- 秘密鍵の分割、分散化
- 可能ならオフラインで使用

実際どうしよう? 考え出すと悩みは絶えず

はー、色々なやり方があるんだな~と思いつつ、じゃあ実際に扱う秘密鍵はどうしたらいいんだろう?

まずはOpenSSLコマンド+秘密鍵ファイル指定。
⇒秘密鍵ファイルの管理どうしましょう?例えばプログラム内で扱うとパスもわかりやすいな~
⇒じゃあマルチシグを参考にしてバイナリに秘密鍵の種を埋め込み、そこから実際に使う鍵を作ろう。これで秘密鍵自体がばれても即死しない。
⇒HTTPSみたいに毎回同じ鍵で暗号化しないようにしよう。OpenSSLライクにSaltをくっつけよう!
⇒う~ん、秘密鍵のファイルパス情報がソース見るとわかるって、結構危険かな?
⇒もうバイナリに埋め込むか!ビルド毎に秘密鍵生成してバイナリに埋め込めばGitHubにコードを上げても問題ない!
⇒…バイナリが流出したらとか考えちゃう?

みたいに錯乱中。セキュリティリスクって考えだしたらきりがないですね。。

そうはいいつつ作ってみた: OpenSSLのAPIを使った暗号化ライブラリとエディタ

暗号化用のライブラリAPIと、暗号化が出来るエディタとしてvimのラッパーを作ったのでさらしておきます。
https://github.com/developer-kikikaikai/EncryptedEditor

秘密鍵の扱い

内部でこんな感じに生成してみました。引数のseed、固定の秘密鍵、ビルド時に作った秘密鍵の3つを使って暗号化します。
なので暗号化毎にデータが変わるし、他環境でビルドしたバイナリを利用しても復号出来ないようにしています。
バイナリ流出しても即死しないようにするなら、ここでどこかのファイルから別途値を持ってきて咬み合わせるのがいいのかな。(飽きた)

encrypter_openssl_seed.cpp
//暗号化で実際に使う鍵を取得
const unsigned char * get_base_key(unsigned char *seed, int length) {
    unsigned char local_key_data[]="秘密鍵を埋め込み";
    //ビルド時に埋め込んだ秘密鍵を取得
    const unsigned char *local_seed = get_seed();

    //秘密鍵両者は同じ長さにした
    int len=strlen((const char *)local_seed), i=0, j=0;
    for(i=0;i<len; i++, j=(j+1)%length) {
        //鍵同士をxorでマージ。seedもくっつけて暗号毎に値が変わるようにする
        local_key_data[i] = ((local_key_data[i] ^ local_seed[i])) ^ seed[j];
    }

    if(local_private_key == NULL) {
        local_private_key = (unsigned char *)calloc(1, len * 2);
    }

    //ついでにbase64エンコード
    base64_encode(local_key_data, len, &local_private_key);
        return local_private_key;
}

暗号化

OpenSSLのライブラリであるlibcryptoを使った暗号化・復号化です。
EVP_CIPHER_CTX_newEVP_EncryptInit_exEVP_EncryptUpdateEVP_EncryptFinal_exEVP_CIPHER_CTX_freeと順に実行すればOK。
復号化はEVP_CIPHER_CTX_newEVP_DecryptInit_exEVP_DecryptUpdateEVP_DecryptFinal_exEVP_CIPHER_CTX_freeです。
参考:EVP Symmetric Encryption and Decryption

encrypter_openssl.cpp
int EncrypterOpenssl::encrypt(const unsigned char *src_buf, int src_len, unsigned char **result_buf) {
    /*encrypting as https://wiki.openssl.org/index.php/EVP_Symmetric_Encryption_and_Decryption*/

    /*fail safe*/
    if(src_len == 0 || src_buf == NULL) return -1;

    /*initialize EVP_CIPHER_CTX*/
    EVP_CIPHER_CTX * ctx = EVP_CIPHER_CTX_new();

    /*allocate, opensslは関係ないです*/
    EncrypterBuffer buffer;
    buffer.allocate(src_len + ENC_OPENSSL_SALT_LEN + 1);

    unsigned char salt[ENC_OPENSSL_SALT_LEN + 1];
    get_salt(salt);

    /*EVP_EncryptInit_ex時に暗号の種類とkey, ivを指定する。*/
    if(1 != EVP_EncryptInit_ex(ctx , EVP_aes_256_cbc(), NULL, _get_key(salt, ENC_OPENSSL_SALT_LEN), _get_iv(salt, ENC_OPENSSL_SALT_LEN))) return buffer._handle_err();

    unsigned char * buf = buffer.get();
    int result_len=0;
    int evp_len=0;
    if(1 != EVP_EncryptUpdate(ctx , buf, &evp_len, src_buf, src_len) ) return buffer._handle_err();
    result_len = evp_len;

    if(1 != EVP_EncryptFinal_ex(ctx , buf + evp_len, &evp_len)) return buffer._handle_err();
    result_len += evp_len;
    EVP_CIPHER_CTX_free(ctx);

    /*余分なバッファーの初期化, opensslは関係ないです*/
    buffer.padding(result_len);

    /*暗号化データにSaltをくっつけてる*/
    memmove(buf+ENC_OPENSSL_SALT_LEN, buf, result_len);
    memcpy(buf, salt, ENC_OPENSSL_SALT_LEN);

    /*事後処理。opensslは関係ないです*/
    *result_buf = buffer.pop();
    return result_len + ENC_OPENSSL_SALT_LEN;
}

その他参考

DHをどうやって復号化するのかの話:
DH鍵交換でもWiresharkでSSL/TLSを復号化したい

仮想通貨の情報管理の話:
コールドウォレット、マルチシグって何? 今さら聞けない仮想通貨の基礎知識
ウォレットの検索で使用:
【2018年最新】仮想通貨ウォレット5種類のメリット・デメリット徹底比較 | それぞれの特徴は?
あなたのビットコイン秘密鍵は安全ですか?最近遭ったビットコイン盗難!!