公開鍵暗号方式でファイルを暗号化してみる

# はじめに
OpenSSLの公開鍵暗号方式を利用し、公開鍵をサーバ側に保存しておいて暗号鍵はサーバに保管しないようにしておけば、安全にファイルを集められるのではないかと考えて試したメモ。
おそらく多くの人がいままでに同じ事を考えて試したと思うが、これからも同じ事を試す人がいると思うのでメモを残す。

# テスト用コード
考えたことが実際にできるか試すために書いたテストコード。
``rand.dat`` は ``dd if=/dev/urandom of=rand.dat bs=1M count=1`` のようにして作成したファイル。
``openssl_public_encrypt()`` は暗号化できる文字列長に制限があるため適当な長さのブロックに分けて暗号化している。

```PHP
<?php 
$config = [
        'digest_alg' => 'sha256',
        'private_key_bits' => 2048,
        'private_key_type' => OPENSSL_KEYTYPE_RSA,
    ];
 
$file = 'rand.dat';
 
$t0 = time();
 
$keypair = openssl_pkey_new($config);
if (is_resource($keypair)) {
    openssl_pkey_export($keypair, $privkey);
 
    $pubkey = (object) openssl_pkey_get_details($keypair);
    $pubkey = $pubkey->key;
 
    $data = file_get_contents($file);
    $t1 = microtime(true);
    echo strlen($data) . ' / ' . md5($data) . PHP_EOL;
    echo 'prepare: ' . ($t1 - $t0) . PHP_EOL;
 
    $t2s = microtime(true);
    $encrypted = ssl_pub_encrypt($data, $pubkey);
    $t2f = microtime(true);
    echo strlen($encrypted) . ' / ' . md5($encrypted) . PHP_EOL;
    echo 'encrypt: ' . ($t2f - $t2s) . PHP_EOL;
 
    $t3s = microtime(true);
    $decrypted = ssl_priv_decrypt($encrypted, $privkey);
    $t3f = microtime(true);
    echo strlen($decrypted) . ' / ' . md5($decrypted) . PHP_EOL;
    echo 'decrypt: ' . ($t3f - $t3s) . PHP_EOL;
}
 
function ssl_pub_encrypt($source, $pem) {
    $bits = ssl_getbits($pem);
    $encrypted = '';
    $cursor = 0;
    $blocksize = $bits / 8 - 42;
 
    while ($data = substr($source, $cursor, $blocksize)) {
        set_time_limit(10);
        error_clear_last()
        openssl_public_encrypt($data, $blockdata, $pem, OPENSSL_PKCS1_OAEP_PADDING);
        if (!empty(error_get_last())) {
           return false;
        }
        $encrypted .= $blockdata;
        $cursor += $blocksize;
    }
 
    return $encrypted;
}
 
function ssl_priv_decrypt($source, $pem) {
    $bits = ssl_getbits($pem);
    $decrypted = '';
    $cursor = 0;
    $blocksize = $bits / 8;
 
    while ($data = substr($source, $cursor, $blocksize)) {
        set_time_limit(10);
        error_clear_last()
        openssl_private_decrypt($data, $blockdata, $pem, OPENSSL_PKCS1_OAEP_PADDING);
        if (!empty(error_get_last())) {
           return false;
        }
        $decrypted .= $blockdata;
        $cursor += $blocksize;
    }
 
    return $decrypted;
}
 
function ssl_getbits($pem) {
    $key = openssl_pkey_get_public($pem);
    if (is_resource($key)) {
        $keyinfo = (object) openssl_pkey_get_details($key);
        return $keyinfo->bits;
    }
 
    $key = openssl_pkey_get_private($pem);
    if (is_resource($key)) {
        $keyinfo = (object) openssl_pkey_get_details($key);
        return $keyinfo->bits;
    }
 
    return false;
}
```

# テスト結果
いくつかの条件で暗号化・復号にかかった時間以下のようになった。
CPUは AMD FX(tm)-8300 Eight-Core Processor なのでさほど速くはないのだが、暗号化処理も遅く復号処理は許容できないほど遅くなることが分かった。

| bit数 | ファイルサイズ | 暗号化処理時間 [sec] | 復号処理時間 [sec] |
|:------|----------:|------------------:|------------------:|
| 1024bit | 1MB | 0.49 | 5.17 |
| 1024bit | 2MB | 0.96 | 10.32 |
| 2048bit | 1MB | 0.37 | 9.61 |
| 2048bit | 2MB | 0.76 | 19.24 |
| 4096bit | 1MB | 0.46 | 26.75 |
| 4096bit | 2MB | 1.05 | 53.51 |
 
# 共通鍵暗号方式を利用して暗号化する
HTTPS通信でも最初の鍵交換だけ公開鍵暗号方式を利用し、後はその鍵を使って共通鍵暗号方式により通信内容の暗号化を行っている。
同じようにするとどうなるか、比較用のコードを書いてテストしてみる。鍵自体の暗号化と復号の処理は前述のコードで別途行うとし、このテストコードと結果には含めていない。
結果として納得の行く速度が得られたため、公開鍵暗号方式のみでファイルを暗号化しようとするのは諦めることにする。

```php
<?php
$data = file_get_contents('rand.dat');
$method = 'AES-256-CTR';
$key  = 'chiperkey';
$options = 0;
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));
echo strlen($data) . ' / ' . md5($data) . PHP_EOL;
 
$t1s = microtime(true);
$encrypted = openssl_encrypt($data, $method, $key, $options, $iv);
$t1f = microtime(true);
echo strlen($encrypted) . ' / ' . md5($encrypted) . PHP_EOL;
echo 'encrypt: ' . ($t1f - $t1s) . PHP_EOL;
 
$t2s = microtime(true);
$decrypted = openssl_decrypt($encrypted, $method, $key, $options, $iv);
$t2f = microtime(true);
echo strlen($decrypted) . ' / ' . md5($decrypted) . PHP_EOL;
echo 'decrypt: ' . ($t2f - $t2s) . PHP_EOL
```

| ファイルサイズ | 暗号化処理時間 [sec] | 復号処理時間 [sec] |
|----------:|------------------:|------------------:|
| 1MB | 0.0054 | 0.013 |
| 2MB | 0.0063 | 0.015 |
| 100MB | 0.19 | 0.44 |
| 1000MB | 1.84 | 4.41 |