あるシステムで暗号化したものを他のシステムで復号する、 というパターンはありがちなのですが…

昔やったことがあるから大丈夫と高をくくっていたら、 とても面倒なことになりました。

今回は暗号化をPHPで行い、復号をrubyで行うパターンでした。

暗号アルゴリズムはBlowfishですが、 これもいくつかやり方があり、正直言って正確なところは掴みきれていない印象です。

暗号・復号の前提

Blowfishで暗号化する際、使用するのは以下のものです。

  • 暗号化する対象のテキスト
  • パスフレーズ
  • 初期化ベクトル

暗号化の対象については、これがないと始まりませんので割愛して、 パスフレーズはいわゆるパスワード(キーとも表現されていますが)です。

初期化ベクトルは、 Wikipediaの説明 がありますが、 同じテキストを同じパスフレーズで暗号化したものでも、 毎回違った結果になるようにするためのもののようです。

そして、Blowfishにはモードがいくつかあり、 大抵のサンプルにあるのはECBかCBCです。

ECBでは初期化ベクトルは不要ですが、 CBCでは必須になっています。

これは暗号強度に関わってくるものなので、 ここではCBCを使用することにしました。

PHPでのBlowfish暗号化

PHPで暗号化する場合、拡張モジュールMCryptを使用することが多いと思います。 Blowfishの場合は、PEARライブラリCrypt_Blowfishを用いることもできます(かなり古いですが)。

以下、コマンドラインで実行するサンプルです。

encrypt.php

#!/usr/bin/env php
<?php
define('PASSPHRASE', 'passphrase-test');
{
    $opt = getopt("t:");

    if (!isset($opt['t'])) {
        exit(1);
    }

    $text = $opt['t'];
    // PKCS#5 Padding
    $pkcs5_pad = function($text, $blocksize) {
        $pad = $blocksize - (strlen($text) % $blocksize);
        return $text . str_repeat(chr($pad), $pad);
    };

    $mc   = mcrypt_module_open(MCRYPT_BLOWFISH, '', MCRYPT_MODE_CBC, '');
    $is   = mcrypt_enc_get_iv_size($mc);
    $iv   = substr(sha1(uniqid()), 0, $is);
    $ks   = mcrypt_enc_get_key_size($mc);
    $key  = $pkcs5_pad(PASSPHRASE, $ks);
    $bs   = mcrypt_enc_get_block_size($mc);
    $text = $pkcs5_pad($text, $bs);
    mcrypt_generic_init($mc, $key, $iv);
    $result = array(
        'encrypted' => bin2hex(mcrypt_generic($mc, $text)),
        'iv' => $iv
    );
    printf("encrypted: %s\niv: %s\n", $result['encrypted'], $result['iv']);
}

$pkcs5_pad という関数がありますが、これが今回の肝かもしれません。

かなり簡単に言うと、ブロック暗号では対象を規定のサイズで分割して暗号化するのですが、

そうすると最後の部分が規定のサイズに満たない場合があります。

その時に規定のサイズになるように、何らかのデータで埋める必要があり、その方法がいくつか存在します。

PHPのmcrypt関数は、この作業を自動的に行うようになっていますが、

いくつかある方法を選択することはできず、ZeroBytePaddingという方式で行ってしまいます。

この状態では、復号側で問題になることがあるので、予め手動で行うことで、自動的にZeroBytePaddingを行うのを回避できます。

今回のものはPKCS#5 Paddingというもので、より一般的に用いられているもののようです。

これについては、暗号側と復号側で方法が一致していないと機能しませんので、 より一般的な方を使用することにしました。

この仕組み自体はmcrypt関数などには存在しないようで、 関数を定義しました(あるかもしれませんが…コードはPHPドキュメントのコード例を流用しています)。

また、初期化ベクトルについては、最大長の文字列をuniqid()で適当に作っています。

PEARのCrypt_Blowfishを使った方法も書いておきます。

encrypt_pear.php

#!/usr/bin/env php
<?php
require_once 'vendor/autoload.php';
require_once 'Crypt/Blowfish.php';
define('PASSPHRASE', 'passphrase-test');
{
    $opt = getopt("t:");

    if (!isset($opt['t'])) {
        exit(1);
    }

    $text = $opt['t'];
    // PKCS#5 Padding
    $pkcs5_pad = function($text, $blocksize) {
        $pad = $blocksize - (strlen($text) % $blocksize);
        return $text . str_repeat(chr($pad), $pad);
    };
    $bw   = Crypt_Blowfish::factory('cbc', null, null, CRYPT_BLOWFISH_PHP);
    $is   = $bw->getIVSize();
    $iv   = substr(sha1(uniqid()), 0, $is);
    $ks   = $bw->getMaxKeySize();
    $key  = $pkcs5_pad(PASSPHRASE, $ks);
    $bs   = $bw->getBlockSize();
    $text = $pkcs5_pad($text, $bs);
    $bw->setKey($key, $iv);
    $encrypted = bin2hex($bw->encrypt($text));
    printf("encrypt: %s\niv: %s\n", $encrypted, $iv);
}

PEARはcomposerを使用してインストールしています。

Crypt_Blowfishは1.1.0RC2です。

composer.json

{
    "repositories": [
        {
            "type": "pear",
            "url": "http://pear.php.net"
        }
    ],
    "require": {
        "pear-pear.php.net/PEAR": "*@stable",
        "pear-pear.php.net/Log": "*@stable",
        "pear-pear.php.net/Crypt_Blowfish": "1.1.0RC2"
    }
}

rubyでのBlowfish復号

rubyの方もlibmcryptを使用できるgemがありますが、 今回はgemをインストールせずにできるかどうかを試してみました。

rubyは2.2.3です。

decrypt.rb

#!/usr/bin/env ruby
# coding: utf-8
require 'openssl'
require 'optparse'

PASSPHRASE = 'passphrase-test'
KEY_LENGTH = 56

encrypted = nil
iv = nil

OptionParser.new do |opt|
  opt.on('-e xxx') {|var| encrypted = var }
  opt.on('-i xxx') {|var| iv = var }
  opt.parse! ARGV
end

if nil == encrypted || nil == iv
  puts "invalid argument."
  exit(1)
end

cipher = OpenSSL::Cipher::BF.new(:CBC)
cipher.decrypt
cipher.key_len = KEY_LENGTH

passphrase = PASSPHRASE

# PKCS#5 Padding
if passphrase.length < KEY_LENGTH
  pad = KEY_LENGTH - (passphrase.length % KEY_LENGTH)
  passphrase += pad.chr * pad
end

cipher.key = passphrase
cipher.iv  = iv

result = cipher.update([encrypted].pack("H*")) + cipher.final
puts result

まず、KEY_LENGTH = 56 の部分ですが、 opensslを使用した際、デフォルト値が16になっており、 これをPHP側で確認した値と比較した結果、 この設定が必要でした。

PKCS#5 PaddingについてはPHPと同じです。

OpenSSL::Cipherについては、公式のマニュアルにあまり記載がないので、 もしかしたらこのあたりをやってくれるメソッドがあるかもしれません。

暗号化側から受取るデータは、暗号化テキストと初期化ベクトルです。

また、PHPのところでは特に書いていませんが、 この例では暗号文を16進数に変換して扱っていますので、復号側では元に戻しています。


以上、利用頻度が結構微妙な暗号化・復号ですが、 見てみると上記言語ではあまりコード量は多くありません。

その割には仕組みが複雑なせいでハマりやすい分野だと思います。

気をつけましょう。



blog comments powered by Disqus