opensslによる暗号化/復号化

自分用のメモ。

概要

  • OpenSSLを使ってファイルの暗号化と復号化を行う
  • C++にて実装
  • 自前のプログラムとopensslコマンドで双方でファイルの暗号化/復号化が出来るようにする

単純に自前のプログラム内で暗号化/復号化する方法はGoogleで検索すれば見つかったけど、opensslコマンドとの間でもやりとりしたかった。サンプルでは自前のプログラムで暗号化と復号化を一度にやっている。暗号化したファイルがopensslコマンドで復号化出来ることを確認。

ソースコード(OpensslSample.cpp。https://gist.github.com/asakawajunya/bb3098fc49b4507fbe31)

サンプルなので暗号化に使用するパスワードはソースにべた書き。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <openssl/evp.h>
#include <openssl/aes.h>
#include <openssl/rand.h>

#include <cstdlib>
#include <ctime>
#include <iostream>
#include <unistd.h>

unsigned char password[128] = "defaultpass";

void encrypt(const char* ifile, const char* ofile) {

    // salt key
    unsigned char salt_key[] = "xxxxxxxx";
    // salt string
    unsigned char salt[] = "Salted__xxxxxxxx";

    // saltをランダムに生成
    for (int i = 0; i < PKCS5_SALT_LEN; i++) {
        salt[i + PKCS5_SALT_LEN] = salt_key[i] =
                (int) ((unsigned char) std::rand());
    }

    struct stat ifile_stat;
    if (0 == stat(ifile, &ifile_stat)) {
        if (ifile_stat.st_size > 0) {
            unsigned char *indata = (unsigned char *) malloc(
                    ifile_stat.st_size);
            unsigned char *outdata = (unsigned char *) malloc(
                    ifile_stat.st_size + 32);
            unsigned char key[EVP_MAX_KEY_LENGTH], iv[EVP_MAX_IV_LENGTH];

            const EVP_CIPHER* ci = EVP_aes_256_cbc();
            EVP_BytesToKey(ci, EVP_md5(), salt_key, password,
                    strlen((const char *) password), 1, key, iv);

            FILE* i_file = fopen(ifile, "rb");
            fread(indata, sizeof(char), ifile_stat.st_size, i_file);

            int outdata1_len = 0;
            int outdata2_len = 0;

            EVP_CIPHER_CTX ctx;
            EVP_EncryptInit(&ctx, ci, key, iv);
            EVP_EncryptUpdate(&ctx, outdata, &outdata1_len, indata,
                    ifile_stat.st_size);
            EVP_EncryptFinal(&ctx, outdata + outdata1_len, &outdata2_len);
            EVP_CIPHER_CTX_cleanup(&ctx);

            FILE* o_file = fopen(ofile, "wb");

            // salt書き込み
            fwrite(salt, sizeof(char), strlen((const char *) salt), o_file);
            // データ書き込み
            fwrite(outdata, sizeof(char), outdata1_len + outdata2_len, o_file);

            fclose(i_file);
            fclose(o_file);

            free(indata);
            free(outdata);
            indata = NULL;
            outdata = NULL;
        }
    }
}

void decrypt(const char* ifile, const char* ofile) {

    struct stat ifile_stat;
    if (0 == stat(ifile, &ifile_stat)) {
        FILE* i_file = fopen(ifile, "rb");

        unsigned char salt_key[PKCS5_SALT_LEN];
        unsigned char salt[16];

        // salt読み込み
        fread(salt, sizeof(char), 16, i_file);
        for (int i = 0; i < PKCS5_SALT_LEN; i++) {
            salt_key[i] = salt[i + PKCS5_SALT_LEN];
        }

        int outdata1_len = 0;
        int outdata2_len = 0;

        unsigned char *indata = (unsigned char *) malloc(ifile_stat.st_size);
        unsigned char *outdata = (unsigned char *) malloc(ifile_stat.st_size);
        unsigned char key[EVP_MAX_KEY_LENGTH], iv[EVP_MAX_IV_LENGTH];

        // データ読み込み
        fread(indata, sizeof(char), ifile_stat.st_size, i_file);

        const EVP_CIPHER* ci = EVP_aes_256_cbc();
        EVP_BytesToKey(ci, EVP_md5(), salt_key, password,
                strlen((const char *) password), 1, key, iv);

        EVP_CIPHER_CTX ctx;
        EVP_DecryptInit(&ctx, ci, key, iv);
        EVP_DecryptUpdate(&ctx, outdata, &outdata1_len, indata,
                ifile_stat.st_size);
        EVP_DecryptFinal(&ctx, outdata + outdata1_len, &outdata2_len);
        EVP_CIPHER_CTX_cleanup(&ctx);

        FILE* o_file = fopen(ofile, "wb");
        // padding部分を削って書き込む。書き込みデータの一番最後のデータから削る分を特定する。
        fwrite(outdata, sizeof(char),
                outdata1_len - (int) outdata[ifile_stat.st_size - 16 - 1],
                o_file);

        fclose(i_file);
        fclose(o_file);

        free(indata);
        free(outdata);
        indata = NULL;
        outdata = NULL;
    }
}

int main(int argc, char *argv[]) {
    if (2 != argc) {
        fprintf(stderr, "Usage: %s original_file", argv[0]);
        return EXIT_FAILURE;
    }
    // 元ファイル
    std::string original_file = argv[1];
    // 暗号化ファイル
    std::string encrypt_file = std::string(original_file) + ".encrypt";
    // 復号化ファイル
    std::string decrypt_file = std::string(original_file) + ".decrypt";

    encrypt(original_file.c_str(), encrypt_file.c_str());
    decrypt(encrypt_file.c_str(), decrypt_file.c_str());

    return EXIT_SUCCESS;
}

ビルド方法

Mac OS 10.9.5だと結構warningが出るが、ひとまずそのまま。OpenSSLではなくCommonCryptoを使わないとダメな模様。Linuxなら平気かも(未確認)

$ c++ OpensslSample.cpp -o OpensslSample -lcrypto

確認

インラインで説明を追記。

# 暗号化対象のファイルを作成
/Users/junya/develop/OpensslSample% echo "HelloWorld" > test.txt

# md5確認
/Users/junya/develop/OpensslSample% md5 test.txt
MD5 (test.txt) = 6df4d50a41a5d20bc4faad8a6f09aa8f

# 暗号化および復号化
/Users/junya/develop/OpensslSample% ./OpensslSample test.txt

# OpensslSampleを実行すると、"元ファイル名.decrypt"と"元ファイル名.encrypt"というファイルを出力する
/Users/junya/develop/OpensslSample% ls -l
total 72
-rwxr-xr-x  1 junya  staff  15372  9 22 14:00 OpensslSample
-rw-r--r--  1 junya  staff   4193  9 22 13:58 OpensslSample.cpp
-rw-r--r--  1 junya  staff     11  9 22 14:00 test.txt
-rw-r--r--  1 junya  staff     11  9 22 14:01 test.txt.decrypt
-rw-r--r--  1 junya  staff     32  9 22 14:01 test.txt.encrypt

# md5確認。元ファイルと復号化したファイルのハッシュ値が一致することを確認
/Users/junya/develop/OpensslSample% md5 test.txt*
MD5 (test.txt) = 6df4d50a41a5d20bc4faad8a6f09aa8f
MD5 (test.txt.decrypt) = 6df4d50a41a5d20bc4faad8a6f09aa8f
MD5 (test.txt.encrypt) = df58fbb119ceea14dd9e109773fc73f9

# opensslコマンドを使って、暗号化ファイルを復号化する。パスワードにはサンプルソースに書かれている物を入力する
/Users/junya/develop/OpensslSample% openssl aes-256-cbc -d -in test.txt.encrypt
enter aes-256-cbc decryption password:
HelloWorld

その他

  • opensslコマンドでは、デフォルトではファイルの先頭部分に"Salted__xxxxxxxx"という16バイトのデータを書き込む("xxxxxxxx"部分はランダム。これによってファイルとパスワードが同一であっても、毎回異なる暗号化ファイルが出来上がる。nosaltオプションもあるが、今回は使わないようにした。
  • 暗号化すると、ファイルサイズは必ず16の倍数になる。足りない場合はpadding。自前のプログラムで復号化する場合、padding部分を取り除かないと元ファイルと一致しなくなる。書き込みデータの一番最後にはどれだけpaddingされたか書かれているので、その分だけファイルに出力しないようにしている。1バイトpaddingしている場合は"01"x1、2バイトpaddingされている場合は"02"x2、元ファイルが16の倍数であっても必ずpaddingされ、その場合は"10"x16が記述される。
  • 暗号化するのに必要なファイルサイズは元ファイル +32バイト("Salted__xxxxxxxx"とpadding分)あれば足りる。"Salted__xxxxxxxx"を除けば16バイト削れる、padding分も元ファイルサイズを16で割ったあまりを使えば正確になる。

2014年11月28日ソースコード修正

  • mallocに対応するfreeを追加
  • EVP_CIPHER_CTX_cleanupを追加