Displaying posts tagged with

“PHP”

Behavior の適用範囲を広げる $mapMethods

(CakePHP 1.2.5)
ビヘイビアに定義されたメソッドは、モデル自身のメソッドであるかのように呼び出すことができます。

class FooBehavior extends ModelBehavior {
  function foo($model){ /* ... */ }
}
 
class Bar extends AppModel {
  var $actsAs = array('Foo');
}
 
// in Controllers
$this->Bar->foo();

しかしこの方法では Dynamic Finder のように動的に名前が変化するメソッドは実装できません。

ModelBehavior クラスの $mapMethods プロパティを使用すると、この制約を乗り越えることができます。$mapMethods は配列で、次のような構造をしています:

array( '/メソッド名に対する正規表現/' => '実際に呼び出されるメソッド'  )

例えば '/^list/' => '_genericList' という設定を行った場合、listから始まる全てのメソッド呼び出しが、ビヘイビアの _genericList メソッドの呼び出しに変換されます。このとき _genericList メソッドの引数には元のメソッド名と引数が渡されます。ちょうど __call によるメソッドのオーバーロードと同じです。

この機能を活用して、あるカラムの値を配列として取得するための listColumn メソッドを作ってみます(Column はカラム名)。このメソッドは ListingBehavior に実装することにしましょう。

class ListingBehavior extends ModelBehavior {
  var $mapMethods = array('/^list[a-zA-z0-9]+$/' => '_listColumn');
 
  function _listColumn($model, $methodName, $query=array()) {
    $column = substr($methodName, 4);
    $query = am($query, array('fields' => array($model->alias.".".$column)));
    $arr = $model->find('all', $query);
    if(is_array($arr)) {
      return Set::extract("/{$model->alias}/{$column}", $arr);
    }
    return $arr;
  }
}

モデルにこのビヘイビアを組み込むと、任意のカラムに対して listXXX メソッドを呼び出すことができるようになります。

class User extends AppModel {
  var $actsAs = array('Listing');
}
 
// in Controllers
$this->User->listId();
$this->User->listUsername(array('conditions' => 'active = 1'));
$this->User->listGroup_id();

メソッド名は小文字に変換されて渡されるので(これが仕様なのかバグなのか微妙なところです)、残念ながら Inflector::underscore を使うことはできません。

paginateで複雑な検索を行う

joinやサブクエリ、集合演算などを含む複雑な検索を行う場合、

  1. まず検索条件にマッチする全行のidを求め、
  2. Model.id IN (1で求めた全id) という条件で一覧表示に使うデータを取得する

という手順を踏んだ方が合理的なことがある。

この場合、1の段階で「検索条件にマッチする全行数」が判明するため、Paginationのために改めてCOUNTクエリを発行するのは完全に無駄である。CakePHP 1.2のPagination機能でこの無駄を回避するには、コントローラの $paginate プロパティに適当なキー(下例では numberOfRows)で件数を保存しておき、

$allIds = $this->Foo->find('all', array('fields' => 'DISTINCT Foo.id',
                                        'conditions' => $complexCond));
$this->paginate['numberOfRows'] = $this->Foo->getNumRows();

対象となるモデルの paginateCount メソッドでその数値をそのまま返すようにする。

class Foo extends Model
{
  function paginateCount($conditions=null, $recursive=0, $extra=array()) {
    if(isset($extra['numberOfRows'])) {
      return $extra['numberOfRows'];
    }
    $parameters = compact('conditions');
    if ($recursive != $this->recursive) {
      $parameters['recursive'] = $recursive;
    }
    return $this->find('count', array_merge($parameters, $extra));
  }
}

$paginate の中で認識されないキーは、Model::paginate や Model::paginateCount の最後の $extra 引数として渡ってくる。

メソッドチェーンでHTMLを組み立てるView Helper

メソッドチェーンを用いて複雑なHTMLを組み立てることができるMarkup Helperを公開しました。(実用上)PHP 5.2以上が必須です。

cakephp-markup-helper – GitHub

例1:単純なdiv,pタグを出力

echo $markup->div('section')->p->text('これは<テスト>です')->end->end;
/*
出力されるHTML:
<div class="section">
 
これは&lt;テスト&gt;です
</div>
 
*/

例2:HtmlHelperと併用し、「ユーザ一覧」テーブルを構築

echo $markup->div('list')
->table
->thead
->html($html->tableHeaders(a('id', 'ユーザ名', '操作')))
->end
->tbody->nl;
 
foreach ($users as $user) {
  echo $markup->tr
    ->td->text($user['User']['id'])->end
    ->td->text($user['User']['username'])->end
    ->td('actions')
    ->html($html->link('編集',array('action'=>'edit', $user['User']['id'])))
    ->end
    ->endtr->nl;
}
 
echo $markup->endAllTags;

使い方は上の例からほとんど想像がつくだろうと思います。特徴は次の通りです。

  • ほとんど全てのメソッドがインスタンス自身を返すので、延々メソッドチェーンを繋げていくことでどんな複雑なHTMLも構築することができます。
  • 内部で「どのタグがまだ閉じられていないか」という情報を管理しているため、タグ名を明示しなくてもタグを閉じることが出来ます。逆に敢えてタグ名を明示して、構造が正しいことを確認することもできます。
  • 生成されたHTMLは内部のバッファに蓄えられ、__toString で文字列として返却されます。PHP 5.2以降ではオブジェクトが文字列として使用される際に自動的に __toString が呼ばれるため、違和感なく echo(またはその他の関数)で出力することができます。

省略記法の種類

できる限り短く、書きやすくするため、__get と __call を活用した省略記法が多数用意されています。

  省略記法 実際のメソッド呼び出し
プロパティ $markup->{method_name}; $markup->{method_name}();
メソッド $markup->{tag_name}(arg1, arg2, …); $markup->startTag({tag_name}, arg1, arg2, …);
$markup->end{tag_name}(); $markup->endTag({tag_name});
$markup->nl(); $markup->newline();
$markup->end(); $markup->endTag();

これらの省略記法を用いることで、例えば次のような記述が

$markup->div('css-class')->p->end->enddiv->nl;

次のようなメソッド呼び出しに変換されます。

$markup->startTag('div', 'css-class')
  ->startTag('p')->endTag()->endTag('div')->newline();

主要なメソッド

細かい動作についてはユニットテストも参考にしてください。

startTag($tag, [$attrs, $content $escapeContent]) $tagの開始タグを生成してバッファに追加します。$contentが与えられた場合は終了タグも追加します。

$tag string 必須 タグ名
$attrs string or array null HTML属性。文字列が与えられた場合はclass属性の値と見なされる
$content string null 要素の内容
$escapeContent boolean true trueの場合は$contentをHTMLエスケープする
endTag([$tag]) 終了タグを生成してバッファに追加します。$tagが与えられなかった場合は最も内側のタグを、$tagが与えられた場合は最も内側の$tagタグとその内側の全てのタグを閉じます。$tagが与えられ、かつ閉じるべき$tagが存在しない場合はE_USER_WARNINGとなります。

$tag string null タグ名
endAllTags() 全てのタグの終了タグを生成してバッファに追加します。
text([arg1, arg2, ...]) 任意の数の文字列をHTMLエスケープしてバッファに追加します。
html([arg1, arg2, ...]) 任意の数の文字列をそのままバッファに追加します。
newline() 改行(0x0A)をバッファに追加します。
renderElement([arg1, arg2, ...]) エレメントをレンダリングしてバッファに追加します。下記の説明を参照してください。

エレメントのレンダリング

エレメントをレンダリングするための専用メソッドとして、renderElement が用意されています。

echo $markup->div('search-result')->renderElement('search/result')->end;

もちろん html メソッドを使って、View の elementメソッドの戻り値をそのまま埋め込むこともできます。

echo $markup->div('search-result')->html($this->element('search/result'))->end;

ただしこの方法では MarkupHelper が内部で保持している文脈情報がそのままエレメント内に引き継がれます。エレメント内で endAllTags メソッドなどを呼んだ場合、意図しないタグまで閉じられてしまうことがあります。

MarkupHelper には文脈情報を隔離するために pushNewContext/popContext メソッドが用意されています。renderElement メソッドでは自動的にそれらのメソッドを呼び出し、エレメントのレンダリングが独立した文脈の中で実行されるようになっています。

echo $markup->div;   //
<div>
 
$markup->pushContext();
echo $markup->p->strong->text('foo')->endAllTags; //
 
<strong>foo</strong>
 
$markup->popContext();
 
echo $markup->end;  //</div>

他のヘルパーの中から使う

この pushNewContext/popContext メソッドを活用することで、他のヘルパーの中でも安全に MarkupHelper を使用できます。「生成したHTMLをバッファ変数に溜めておいて、最後にreturnする」という典型的なヘルパーの処理は、全て MarkupHelper が肩代わりしてくれます。

<?php
class YourHelper extends AppHepler
{
  var $helpers = array('markup');
  /**
   * 定義リスト
<dl>..</dl>
 
 を作るヘルパーメソッド
   */
  function defList($data)
  {
    $this->Markup->pushNewContext->dl->nl;
    foreach($data as $term => $desc) {
      $this->Markup
        ->dt->text($term)->end
        ->dd->html($desc)->end->nl;
    }
    return $this->Markup->endAllTgs->popContext;
  }
}
 
/* 使用例 */
echo $your->defList(aa('用語1', '<strong>説明1</strong>', '用語2', '説明2'));
 
/* 出力(改行もこの通りになります)
<dl>
<dt>用語1</dt>
<dd><strong>説明1</strong></dd>
<dt>用語2</dt>
<dd>説明2</dd>
</dl>
 
*/

AuthComponentを使いやすくするビヘイビア

CakePHP 1.2 の AuthComponent はとても便利なのですが、POSTされたパスワードの値を自動的に暗号化(hash)してしまうため、ユーザの create/update を行う際のバリデーションが困難になります。そこで AuthComponent を補助する AuthModel ビヘイビアを作ってみました。

auth_model.php – gist:188066

このビヘイビアを使うことで

  • 標準のバリデーション機能を使ってパスワード入力値を検証できるようになります
  • 「再入力」による確認を行うための sameValue バリデータが使えるようになります
  • データ更新時に「パスワード欄が空ならパスワードを変更しない」という動作を簡単に実現できます

以下、password カラムを備えた users テーブル(すなわち User モデル)を例に使い方を説明していきます。

まず User モデルに AuthModel ビヘイビアを登録してください。いくつか使用可能なオプションがありますが最後に説明します。

class User extends AppModel {
  var $actsAs = array('AuthModel');
}

次にユーザ登録用のビューを作ります。AuthComponent による自動変換処理を回避するため、パスワード入力用のフィールドには本来のカラム名とは異なる名前を使用してください。AuthModel ビヘイビアはデフォルトで password_input という名前を使用するので、特にこだわりがなければこの名前を使うことをおすすめします。またパスワード再入力確認用のフィールドも用意しておきます。こちらは名前は何でも構いません。下の例では password_confirmation にしています。

<?php
echo $form->create('User');
echo $form->input('username', array('label' => 'ユーザ名'));
echo $form->input('password_input',
                  array('type' => 'password',
                        'label' => 'パスワード',
                        'value' => ""));
echo $form->input('password_confirmation',
                  array('type' => 'password',
                        'label' => 'パスワード(再入力)',
                        'value' => ""));
echo $form->end('作成');
?>

更新用のビューもほぼ同じ構成です。パスワードを空にした場合は変更されない、という説明書きを加えておきます。

<?php
echo $form->create('User');
echo $form->hidden('id');
echo $form->input('username', array('label' => 'ユーザ名'));
echo $form->input('password_input',
                  array('type' => 'password',
                        'label' => 'パスワード',
                        'value' => ""));
echo $form->input('password_confirmation',
                  array('type' => 'password',
                        'label' => 'パスワード(再入力)',
                        'value' => ""));
echo $form->end('更新');
?>
※パスワードを空欄にした場合は更新されません

そしてこの password_input フィールドに対するバリデーションを User モデルに設定していきます。以下の例では3つの設定を行っています。

  • 入力は必須(requiredルール)
  • 文字数は6文字以上(lengthルール)
  • 「再入力」フィールドと値が一致(confirmルール)

最後のルールは AuthModel ビヘイビアが提供する sameValue バリデータを使います。このバリデータに対する唯一のオプションは再入力用フィールドの名前です。

class User extends AppModel {
  var $actsAs = array('AuthModel');
 
  var $validate = array('password_input' =>
    array('required' =>
          array('rule' => '/.+/',
                'required' => true,
                'allowEmpty' => false,
                'last' => true,
                'on' => 'create',
                'message' => 'パスワードを入力してください'),
          'length' =>
          array('rule' => array('minLength', 6),
                'allowEmpty' => true,
                'last' => true,
                'message' => 'パスワードは6文字以上で入力してください'),
          'confirm' =>
           array('rule' => array('sameValue', 'password_confirmation'),
                 'allowEmpty' => true,
                 'message' => 'パスワードと再入力の値が一致しません')));
}

それぞれのルールにおける required, allowEmpty, on, last オプションの設定が少し複雑ですが、よく分からなければCookbookを参照してください。

最後に UsersController です。beforeFilter 中で setAuthComponent メソッドを呼び出し、AuthComponent を登録しておきます(実は AuthComponent の中でデフォルトのhashアルゴリズムを使用している限り、この操作は必要ありません。しかし将来的に互換性の問題が発生する可能性もあるので、Userモデルのcreate/updateを行うコントローラでは必ず実行しておいてください)。

ユーザの追加/更新を行うadd/editアクションの中では特別な操作は必要ありません。

class UsersController extends AppController {
  function beforeFilter() {
    parent::beforeFilter();
    $this->User->setAuthComponent($this->Auth);
  }
 
  function add() {
    if (!empty($this->data)) {
      if ($this->User->save($this->data)) {
        $this->Session->setFlash('ユーザを作成しました。');
        $this->redirect('index');
      } else {
        $this->Session->setFlash('エラーを修正してください。');
      }
    }
  }
 
  function edit($id = null) {
    if(empty($this->data)) {
      if (!$id || !($row = $this->User->findById($id))) {
        $this->redirect('index');
      }
      $this->data = $row;
    } else {
      if($this->User->save($this->data)) {
        $this->Session->setFlash('ユーザ情報を更新しました。');
        $this->redirect('index');
      } else {
        $this->Session->setFlash('エラーを修正してください。');
      }
    }
  }
}

注意

このビヘイビアを使用すると直接パスワード用カラムに値を設定して保存することができなくなります(セキュリティ上の配慮)。この制約を回避するには save メソッドの validate オプションを無効にして実行してください。

  //ユーザ名 = パスワード = admin のユーザを追加
  function install() {
    $initialUser = array('User' =>
                         array('username' => 'admin',
                               'password' => $this->Auth->password('admin')));
    $validate = false;
 
    if($this->User->find('count') == 0) {
      $this->User->save($initialUser, $validate);
    }
    $this->redirect('/');
  }

応用

データ更新時の「パスワード入力欄が空ならパスワードを変更しない」動作が不要な場合、required ルールの on => create オプションを取り除いてください。さらに、滅多にないことだとは思いますが、空のパスワードを許可する場合は下記の allow_blank オプションを有効にしてください。

オプション

キー デフォルト値 説明
column string password パスワードを保存するカラムの名前
input string password_input パスワード入力用のフィールド名
allow_blank bool false 空のパスワード使用可否
hash_function callback array(Security,hash) パスワードのハッシュを求める際に使用するコールバック

全てのオプションを設定した場合の例:

class User extends AppModel {
  var $actsAs = array('AuthModel' =>
                      array('column' => 'secret',
                            'input'  => 'secret_input',
                            'allow_blank' => true,
                            'hash_function' => 'md5'));
}

参考にしたサイト

mod_rewriteとPHP_SELFの関係

mod_rewrite でURLを書き換える場合の PHP_SELF の値は、RewriteRule を .htaccess に書くか httpd.conf に書くかによって違ってくる。http://example.com/foo/bar を http://example.com/index.php に書き換える場合について例示すると以下のようになる。

.htaccessに書く場合

RewriteRule ^foo/bar$ index.php

index.php でアクセスできるPHP_SELFの値は /index.php になる。

httpd.confに書く場合

RewriteRule ^/foo/bar$ /index.php

index.php でアクセスできるPHP_SELFの値は /foo/bar になる。

フレームワークの中には .htaccess で書き換えることを前提としてパス処理を行っているものがあり(CakePHP 1.2など)、httpd.confで書き換えをする場合には何らかの回避策をとる必要がある。

(1) PTオプションを使用する

RewriteRule に passthrough|PT オプションを指定する。

RewriteRule ^/foo/bar$ /index.php [PT]

このオプションを指定した場合、書き換え後のURLに対してAliasやRedirectの適用が行われるようになることに注意(元々そういった用途に使うためのオプション)。

(2) フレームワークレベルで設定する

CakePHP 1.2の場合はindex.php内Dispacherクラスのコンストラクタ第2引数で、PHP_SELFに替わる基準パスを指定できる。

$Dispatcher = new Dispatcher(null, ''); //空文字列でルートを意味する

……

技術的に言うとこれはURL書き換え時に request_rec 構造体の uri メンバを書き換えるか否かの違い。.htaccess でURLを書き換えた場合は書き換え後のURLに対して internal-redirect が発生するらしく、結果として handler の中から書き換え後の uri が見えるようだ(RewriteLogから推測)。

PHP で Non-blocking I/O

stream_set_blocking関数を使う。

C/C++セキュアプログラミングクックブック〈VOLUME2〉』に載っている「レシピII-4.3 Unix標準の乱数インフラストラクチャを使用する」をPHPで実装すれば、次のようになると思われる。
(続きを読む…)

暗号乱数インフラの初期化処理

暗号ライブラリ内部でどんな処理が行われているのか、Ruby と PHP について調べてみた。調査に使用したバージョンは ruby-1.9.1-p129, php-5.2.9, openssl-0.9.8k

ruby-openssl ライブラリの場合

乱数用のインターフェイスは OpenSSL::Random モジュール。例えば

salt = OpenSSL::Random.random_bytes(8)

とすると8バイトのランダムなバイト列が取得できるのだが、こんな風に初期化処理なしで、いきなり乱数の取得メソッドを使っても大丈夫なのだろうか?

  1. Ruby のソースを見ると random_bytes の定義は ossl_rand.c にあり、実体は OpenSSL の RAND_bytes() に対するアダプタ
  2. OpenSSL のソースを見ると RAND_bytes の定義は rand_lib.c にあり、実体は(非FIPSモードでは)md_rand.c の中の ssleay_rand_bytes()
  3. ssleay_rand_bytes() の中で初期化済みか否か(initialized)を調べて、初期化済みでなかったら RAND_poll() を呼び出す
  4. RAND_poll の定義はOS/環境ごとに分かれているが、可能な限りのソースからエントロピーを収集するようになっている
    • UNIX: rand_unix.c 通常はまず乱数デバイスからエントロピーの読み出しを試みる。試行対象となるデバイスのパスは e_os.h で DEVRANDOM として定義されており、デフォルトでは /dev/urandom が最優先となる(この際、Linux では poll, その他では select を使って非同期で読み出される)。それでも十分なエントロピーが集まらなかった場合、エントロピー収集デーモン(EGD)のソケットファイルから読み出しを試みる(こちらも e_os.h に試行対象となるパスが定義されている)。さらに pid と uid と現在時刻もエントロピーとして追加する。
    • Windows: rand_win.c の場合は CryptAPI の CryptGenRandom() 関数から取得した疑似乱数をエントロピーとして追加する。さらにメモリ使用量やプロセスIDといったデータもエントロピーとして追加する。

ということで、ほとんどのケースではいきなり random_bytes メソッドなどを使っても大丈夫そうである。

ちなみに OpenSSL::Random モジュールには十分なエントロピーで初期化されたかどうかを調べる status? メソッドもある(現在はUndocumentedだが)。こちらもソースをたどって行けば初期化コードに繋がっているので、システムに十分なエントロピーが存在する環境ならば、常に true を返すはずである。

OpenSSL::Random.status?  #=> true

php-mcrypt ライブラリの場合

直接的な乱数インターフェイスは存在しないが、初期化ベクトルの生成用として mcrypt_create_iv() 関数が用意されている。 この関数の定義は ext/mcrypt/mcrypt.c にあり、中身は指定されたソース(/dev/random または /dev/urandom)からそのままデータを読み出しているだけだった。

fd = open(source == RANDOM ? "/dev/random" : "/dev/urandom", O_RDONLY);
if (fd < 0) {
	efree(iv);
	php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot open source device");
	RETURN_FALSE;
}
while (read_bytes < size) {
	n = read(fd, iv + read_bytes, size - read_bytes);
	if (n < 0) {
		break;
	}
	read_bytes += n;
}
n = read_bytes;
close(fd);

結構ナイーブな実装だな、というのが率直な感想。てっきり mcrypt に対応するインターフェイスがあって、処理を委譲しているのかと思っていたが…(気になって libmcrypt-2.5.8 のソースもざっとチェックしてみたが、やはり乱数の処理は入っていないようだ)。

Non-blocking I/O を使うなどの工夫は無いため、/dev/random では読み出しでブロックする可能性がある。また MCRYPT_RAND は単に rand 関数を必要な回数呼び出すだけなので、暗号用途では使うべきではない(つまりWindows環境では実質使用できない、ということになる)。

OpenSSL::Cipher::Cipher のパディング処理

OpenSSL::Cipher::Cipher#padding= でパディング処理の有無を制御できる。では実際にどのようなパディング処理が行われているのだろう?

暗号化時にはパディングを有効にして、復号時に無効にすれば確認できる。

# Ruby 1.8.7 or 1.9
require 'openssl'
 
def enc_dec_base(operation)
  password = 'fixed password'
 
  lambda do | cipher, data, padding |
    c = OpenSSL::Cipher::Cipher.new(cipher)
    c.send(operation)
    c.padding = padding
    c.pkcs5_keyivgen(password)
    c.update(data) + c.final
  end
end
encrypt = enc_dec_base(:encrypt)
decrypt = enc_dec_base(:decrypt)
 
cipher = 'aes-256-cbc'
msg = 'ABCDEFGHIJKLMNOP' # 16bytes = AES block size
 
(1..msg.size).each do |len|
  plain = msg[0, len]
 
  encrypted = encrypt[cipher, plain, 1]
  decrypted = decrypt[cipher, encrypted, 0] # disable padding
 
  puts "original message: #{plain}"
  p plain.bytes.to_a
  p decrypted.bytes.to_a
  puts
end

結果、PKCS#5 の PBES1, PBES2 と同じく RFC 1423 で定義されたパディング処理を行っていることが分かった。このパディングアルゴリズムは一般には PKCS#5 Padding などと呼ばれているらしい。

PHP の mcrypt ライブラリと相互運用する場合は、PHP 側で適切に処理する必要がある。PHP Manual の User Contributed Notes に実装例を見つけた:duerra_NOT_THIS_ at pushitlive dot net

一応実行結果も載せておく。見事に階段状になっていてちょっと面白い。
(続きを読む…)

PHP で共通鍵暗号(mcrypt)を使用する

最初に決めておくこと

以下の情報は暗号化においても復号においても必要になる。全てが一致しなければ復号はできない。

  • 鍵(以下 KEY)
  • 暗号アルゴリズム(以下 CIPHER) … (1)
  • ブロック暗号のモード(以下 MODE)… (2)

暗号化

KEY, CIPHER, MODE を用いてメッセージ $msg を暗号化する関数 encrypt は次のようになる(わざと冗長な書き方をしている)。

function encrypt($msg)
{
  //初期化ベクトルを生成
  $ivSize = mcrypt_get_iv_size(CIPHER, MODE);
  $iv = mcrypt_create_iv($ivSize, MCRYPT_DEV_URANDOM);
  $dummyIV = str_repeat("x", $ivSize);
 
  //メッセージの暗号化 ... (3)
  $cryptMsg = mcrypt_encrypt(CIPHER, KEY, base64_encode($msg), MODE, $iv);
 
  //初期化ベクトルの暗号化 ... (4)
  $cryptIV = mcrypt_encrypt(CIPHER, KEY, base64_encode($iv), MODE, $dummyIV);
 
  return array($cryptMsg, $cryptIV);
}

戻り値は次の2要素からなる配列である。

  • 暗号化されたメッセージ
  • 暗号化された初期化ベクトル

この2つが揃っていないと復号できないので、必ずペアで保存・転送すること。

復号

暗号化されたメッセージ $cryptMsg と暗号化された初期化ベクトル $cryptIV を KEY, CIPHER, MODE で復号する関数 decrypt は次のようになる。

function decrypt($cryptMsg, $cryptIV)
{
  //ダミーの初期化ベクトルを生成
  $ivSize  = mcrypt_get_iv_size(CIPHER, MODE);
  $dummyIV = str_repeat("x", $ivSize);
 
  //初期化ベクトルの復号
  $iv = _decryptSupport($cryptIV, $dummyIV);
 
  //メッセージの復号
  $msg = _decryptSupport($cryptMsg, $iv);
 
  return $msg;
}
 
function _decryptSupport($cryptMsg, $iv)
{
  //復号してNULLバイトを取り除いてbase64デコード ... (5)
  return base64_decode(rtrim(
    mcrypt_decrypt(CIPHER, KEY, $cryptMsg, MODE, $iv), "\0"));
}

細かい説明

(1) 暗号アルゴリズム
使用できる暗号アルゴリズムは libmcrypt のバージョンに依存するので、関数 mcrypt_list_algorithms で一覧表示して確認しておくこと。PHP Manual に定数として載っているもの(MCRYPT_RIJNDAEL_128 など)はあまり当てにならないので、文字列で指定した方が良い。どのアルゴリズムを使うべきか分からなければ「AES」を使えば良い(…と『暗号技術入門』に書いてあった)。AES を使用したい場合は rijndael-128 を指定する。rijndael-192 や rijndael-256 はブロック長が異なるため、「AES」の規格には合致しないので注意。
(2) ブロック暗号のモード
これも PHP 定数で指定するよりは mcrypt_list_modes で一覧表示して文字列として指定した方が良い。どのモードを使うべきか分からなければ「CBCモード」か「CTRモード」を使えば良い(…とこれまた『暗号技術入門』に書いてあった)。すなわち cbc か ctr を指定する。
(3) 暗号化の際の base64 エンコード
暗号化の際に NULL でパディングされる場合があるので、事前に base64 エンコードしておく。
(4) 初期化ベクトルの暗号化
初期化ベクトルの暗号化については先日のエントリを参照。
(5) 復号の際の NULL バイト除去
暗号化の際にパディングされた NULL は復号後もそのまま残されるので、base64 デコードする前に rtrim で NULL を取り除いていておく。もっとも、少なくとも PHP 5.2.9 の base64_decode 関数は NULL を文字列終端と見なす(いわゆる”バイナリセーフでは無い”関数)ので、今のところは取り除かなくても問題ない。

参考

PHP: Mcrypt – Manual
各ページの User Contributed Notes にも有用な情報がある
新版暗号技術入門 秘密の国のアリス (結城浩)
暗号についての知識は、ほぼ全てこの書籍から得た

CentOSで最新のPHP 5.2を使用する

サードパーティのリポジトリを利用することで、2009年3月20日現在最新のphp-5.2.9をyumでインストールすることができる。リポジトリ管理者の素晴らしい仕事には頭が下がるばかりだ…。

注意:この方法でインストールされる php-mysql は MySQL 5.1.30 の libmysqlclient とリンクされる。

English : Repository Configuration – Les RPM de Remiに従ってEPELとLes RPM de Remiのリポジトリを追加。CentOS 5の場合は

wget http://download.fedora.redhat.com/pub/epel/5/i386/epel-release-5-2.noarch.rpm
wget http://rpms.famillecollet.com/el5.i386/remi-release-5-6.el5.remi.noarch.rpm
rpm -Uvh remi-release-5*.rpm epel-release-5*.rpm

php-mysql をインストールする場合は、予め remi-test リポジトリから mysqlclient16 パッケージをインストールしておく。

sudo yum --enablerepo=remi-test install mysqlclient16

自分が必要とするパッケージをインストールする(パッケージによってはEPEL内のパッケージに依存することがある)。

sudo yum --enablerepo=epel,remi install php php-cli php-mysql php-gd

インストール後はリポジトリ管理者のブログLes RPM de Remiを常にチェックしておくことをおすすめする。PHPの新しいバージョンが出た場合、今までは数日中には対応されてきた。しかしアップデートに伴って、時々パッケージの構成が変わったりライブラリのバージョンが変わったりして、すんなりアップデートできなくなることもあった。そのあたりは自己責任で。