EVP_*系APIでのBlowFish実装

パスワードを保存するアプリを作るため以前使ったことのあるBlowFishのmanを読み直していたら、BF_*系の関数を直接使うんじゃなくてもっと高レベルのEVP_EncryptInitとかを使いなよって書いてあったので忠告に従いこちらを使うべく調べてみました。BF_*系と違うのは暗号・復号に際して必要な情報を追跡するためのコンテキスト情報EVP_CIPHER_CTXを維持していく必要があるのが大きな違いになります。処理は

  1. 暗号コンテキストの初期化
  2. 暗号化方式の決定と初期化
  3. 暗号・復号処理
  4. 暗号コンテキストの解放

といったフローでそれほどステップは多くないカンジ。まず、暗号コンテキストの初期化は

EVP_CIPHER_CTX  ctx;
EVP_CIPHER_CTX_init(&ctx);

のようにEVP_CIPHER_CTX_init()を呼び出すだけです。次に、暗号化方式の決定と初期化ですが暗号化方式はすでに用意されているので使用したい暗号化方式から該当するオブジェクトを選択します。今回はBlowfishで暗号化するので

EVP_EncryptInit_ex(
   &ctx, EVP_bf_cbc(), NULL, (const unsigned char *)[key value], v);

ここで3番目の引数はアクセラレータの指定ですが通常は使わないのでNULL、4番目の引数は暗号化用の鍵となるバイト列を指定します。

暗号化を実行する前に出力用のバッファを確保する必要があります。ブロック暗号化の場合、出力はブロック単位なので入力サイズより長くなる場合がありますから注意が必要です。ブロック長はEVP_CIPHER_CTX_block_size()で取得することができます。

NSInteger ilen = [plainText lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
unsigned char* obuff = calloc(ilen + EVP_CIPHER_CTX_block_size(&ctx),
  sizeof(unsigned char));

出力バッファが用意できたらいよいよEVP_EncryptUpdate()を使って暗号化します。引数は、先に確保した出力用バッファのポインタとターゲットになる平文の格納された入力バッファ、暗号化するレングスを指定します。

EVP_EncryptUpdate(&ctx, obuff, (int*)&olen, ibuff, ilen);

注意が必要なのは、暗号化されるのはブロック長単位平文であるということです。例えば7バイトの平文では上記のコードを実行してもobuffにはなにも出力されませんし、9バイトの平文では8バイト分しか暗号化されず暗号コンテキスト内にバッファリングされます。これらブロック長からはみ出した分を取り出すのがEVP_EncryptFinal_ex()です。

NSInteger finalSuccess = 
  EVP_EncryptFinal_ex(&ctx, &obuff[olen], &templen);

これで、ようやくobuffにすべての暗号化データが格納されたことになります。結果はNSDataに保存しました。最後は暗号コンテキストのクリーンアップで終了。

NSData* encryptedData = 
  [NSData dataWithBytes:obuff length:olen + templen];
EVP_CIPHER_CTX_cleanup(&ctx);

次は逆の複合化。初期化は同じ。もちろん、keyもvも暗号化の際と同じものを使います。

  EVP_CIPHER_CTX_init(&ctx);
  EVP_DecryptInit_ex(&ctx, EVP_bf_cbc(), NULL, (const unsigned char *)[key value], v);

復号の場合も出力バッファの確保が必要になります。暗号化されているデータ長を超えることはない(はず?自信なし)ので出力用バッファは暗号化されたデータ長と同じだけアロケートします。(Webでいくつかのサンプルを探したのですが復号化の際のバッファにもブロック数文のマージンをとってアロケートしている実装を見かけますがそういうパタンはあるのでしょうか?)

NSInteger blocklen = EVP_CIPHER_CTX_block_size(&ctx);
unsigned char* obuff = calloc(ilen, sizeof(unsigned char));

実際の復号化はEVP_DecryptUpdate()を使います。

EVP_DecryptUpdate(&ctx, obuff, &olen, ibuff, ilen);

暗号化の時と同じようにブロック長からはみ出した分は復号化されませんのでやはり最後にEVP_DecryptFinal_ex()を呼び出してバッファをフラッシュします。

NSInteger finalSuccess = 
  EVP_DecryptFinal_ex(&ctx, &obuff[olen], &templen);

最終的にNSStringに戻して完了です。

NSString* decedString =  [[NSString alloc] initWithBytes:obuff
  length:(olen+templen) encoding:NSUTF8StringEncoding];
EVP_CIPHER_CTX_cleanup(&ctx);

ところでここまで鍵の長さについては何も書きませんでしたがBlowfishは32〜448までの可変長の鍵を使用することができます。デフォルトの鍵長はEVP_CIPHER_key_length()で確認したところMacOSX v10.6環境では128ビットでした。鍵の長さを変更しない場合16バイトのキーを用意する必要があります。(実はこれに気づかずしばらくハマりました。)さらにもっと強度を上げるため鍵の長さを変更する場合EVP_EncryptInit_ex/EVP_DecryptInit_exを二回呼び出す必要があります。

EVP_EncryptInit_ex(&ctx, EVP_bf_cbc(), NULL, NULL, v);
EVP_CIPHER_CTX_set_key_length(&ctx, 32);
EVP_EncryptInit_ex(&ctx, NULL, NULL, (const unsigned char *)[key value], NULL);

もし、なにもしないで128ビット以下のキーを使っていると復号時にエラーになりますし、128ビット以上のキーを指定しても128ビットキーを指定したものと同様に扱われるようです。僕は最初キーにMD5ハッシュ値(256ビット)を使っていたのですがキー長をセットしていなかったため切り捨てられて128ビットキーで暗号化されているような状態でした。**Init()という名前の関数を二度呼び出すのはあまり良くないように思いますがなにか実装上の理由があるのかしらん?注意が必要です。

サンプルコードはhttp://github.com/hippos/blowf/blob/master/BlowFishUtils.mをご参考ください。

この記事のトラックバックURL:

http://hippos-lab.com/blog/trackback/369

Comments