PHP 5.3.0 ではマジックメソッド __callStatic が追加された。グローバルに使用されるユーティリティクラスで、動的にメソッド名を生成したいときに便利。
class FooUtil {
static protected $_inc = array('test1' => 1,
'test2' => 2);
public static function __callStatic($method, $args) {
return isset(static::$_inc[$method])
? static::$_inc[$method] + $args[0] : $args[0];
}
}
echo FooUtil::test1(100); // => 101
echo FooUtil::test2(100); // => 102
echo FooUtil::testxxx(100); // => 100
同等の機能を PHP 5.2 より前の環境で再現したい場合、シングルトンパターンと __call を組み合わせるのが最も普通のやり方だと思う。
class FooUtil2 {
protected $_inc = array('test1' => 1,
'test2' => 2);
static private $__singleton = null;
public static function getInstance() {
if(self::$__singleton === null) {
self::$__singleton = new self;
}
return self::$__singleton;
}
public function __call($method, $args) {
return isset($this->_inc[$method])
? $this->_inc[$method] + $args[0] : $args[0];
}
}
echo FooUtil2::getInstance()->test1(100); // => 101
echo FooUtil2::getInstance()->test2(100); // => 102
echo FooUtil2::getInstance()->testxxx(100); // => 100
この方法の欠点は「呼び出し時のコードが長い」ことである。そこで個人的にはクラス名と同じ名前のグローバル関数を使う方法を提案したい。
function FooUtil3() {
static $singleton = null;
if($singleton === null) {
$singleton = new FooUtil3();
}
return $singleton;
}
class FooUtil3 {
protected $_inc = array('test1' => 1,
'test2' => 2);
public function __call($method, $args) {
return isset($this->_inc[$method])
? $this->_inc[$method] + $args[0] : $args[0];
}
}
echo FooUtil3()->test1(100); // => 101
echo FooUtil3()->test2(100); // => 102
echo FooUtil3()->testxxx(100); // => 100
この方法ならば __callStatic を使うのと比べて2文字長いだけだ。しかも他2つの方法よりもカプセル化の面では優れている。関数とクラスは「名前が同じ」というごく緩やかなつながりを持っているだけなので、静的な意味でも動的な意味でも、あとから自由に実装を切り替えられる。staticメソッドでなければならない、シングルトンでなければならない、といった制約はない。
これはグローバル変数や単純なレジストリ(CakePHPにおけるConfigureのような)を使うのとも違う。関数なので、どんな処理でも差し挟むことができる。関数の方がパラメータを持ってもいい。
FooUtil4($context)->test($parameters);
グローバル関数というと問題になるのは名前の衝突だが、関数名とクラス名では名前の傾向が異なるので、クラス名が衝突しないのであれば同じ名前の関数も衝突しない可能性が高いと考えられる。
あまり公のプロジェクトで採用する気にはならないが、内々に使うにはそれなりに便利なテクニックだと思う。
せっかく私のpatchが採用されたので、ブログにも書いておきます。
CakePHP 1.3からはプラグイン内の DataSource が使えるようになりますが、1.3RC1からは DataSource の driver もプラグインから読み込めるようになりました。
たとえばGithubのCakePHP datasources pluginでは、Cakeコアに含まれない様々なデータベースに対するDboドライバが提供されていますが、RC1ではこれを次のようにして読み込むことができます。
class DATABASE_CONFIG {
var $sqlite3 = array('driver' => 'Datasources.DboSqlite3',
...);
}
また driver を使用する DataSource をプラグインとして提供することも可能です。拙作のKeyValueSourceは次のように使用できます。
class DATABASE_CONFIG {
var $memcache = array('datasource' => 'KeyValueStore.KeyValueSource',
'driver' => 'KeyValueStore.KeyValueMemcache',
...);
}
プラグインが読み込む driver を app/models/datasources 内に作ることもできます。たとえば memcache ドライバを拡張した my_memcache ドライバを作った場合、次のように読み込みます。
// app/models/datasources/key_value/key_value_my_memcache.php
class DATABASE_CONFIG {
var $memcache = array('datasource' => 'KeyValueStore.KeyValueSource',
'driver' => 'my_memcache',
...);
}
(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 を使うことはできません。
joinやサブクエリ、集合演算などを含む複雑な検索を行う場合、
- まず検索条件にマッチする全行のidを求め、
- 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を組み立てることができるMarkup Helperを公開しました。(実用上)PHP 5.2以上が必須です。
cakephp-markup-helper – GitHub
例1:単純なdiv,pタグを出力
echo $markup->div('section')->p->text('これは<テスト>です')->end->end;
/*
出力されるHTML:
<div class="section">
これは<テスト>です
</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となります。
|
| endAllTags() |
全てのタグの終了タグを生成してバッファに追加します。 |
| text([arg1, arg2, ...]) |
任意の数の文字列をHTMLエスケープしてバッファに追加します。 |
| html([arg1, arg2, ...]) |
任意の数の文字列をそのままバッファに追加します。 |
| newline() |
改行(0×0A)をバッファに追加します。 |
| 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>
*/
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 で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から推測)。
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バイトのランダムなバイト列が取得できるのだが、こんな風に初期化処理なしで、いきなり乱数の取得メソッドを使っても大丈夫なのだろうか?
- Ruby のソースを見ると random_bytes の定義は ossl_rand.c にあり、実体は OpenSSL の RAND_bytes() に対するアダプタ
- OpenSSL のソースを見ると RAND_bytes の定義は rand_lib.c にあり、実体は(非FIPSモードでは)md_rand.c の中の ssleay_rand_bytes()
- ssleay_rand_bytes() の中で初期化済みか否か(initialized)を調べて、初期化済みでなかったら RAND_poll() を呼び出す
- 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#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。
一応実行結果も載せておく。見事に階段状になっていてちょっと面白い。
(続きを読む…)