|||||||||||||||||||||

なんぶ電子

- 更新: 

PHPでのハッシュ処理

phpでハッシュ生成

前回はふたつのフォルダの中身が一致するかをpythonでハッシュを使って検証しましたが、今回はPHPでハッシュを扱ってみたいと思います。

PHPではWebページで用いられていることが多く、パスワードを安全に管理したり、アップロードされた複数の画像が同一かどうか判断したりするときなどに、ハッシュが用いられます。

hash

まずよく使うのは hash関数だと思います。第1引数にアルゴリズム、第2引数にハッシュ化したいデータを渡します。

実際にハッシュを作成してみたいと思います。

ターミナルから「 php -a 」と -aオプションをつけて実行することで対話的にPHPを起動させることが可能です。

コマンドを入力するとプロンプトが php に代わりそこからはPHPのコマンドが利用できます。

「 exit 」と打ち込む事でphpの対話モードから抜け元のプロンプトへ戻ることができます。

PS C:\> php -a
Interactive shell

php > echo hash('md5','test');
098f6bcd4621d373cade4e832627b4f6

php > var_dump(hash_algos());
array(60) {
  [0]=>
  string(3) "md2"
  [1]=>
  string(3) "md4"
  [2]=>
  string(3) "md5"
  [3]=>
  string(4) "sha1"
  [4]=>
  string(6) "sha224"
  [5]=>
  string(6) "sha256"
  [6]=>
  string(6) "sha384"
  [7]=>
  string(10) "sha512/224"
  [8]=>
  string(10) "sha512/256"
...

第1引数に指定できるアルゴリズムの文字列は md5 や sha256 をはじめ様々あります。利用できるリストは hash_algos関数を実行することで配列として得られます。

省略可能なオプションとして、第3引数にバイナリか16進文字列かを選ぶboolean値を設定可能です。これを true にするとバイナリ値が戻ります。

この関数で得られるハッシュはもともとバイナリ値ですが、16進数として表記するのが一般的です。16進数化する際は元のバイナリ4ビットに対して16進数の文字列をひとつ割り当てることになります。ASCII文字列は1文字8ビットで保持されますので、16進数としてハッシュを保持すると通常の倍の容量が必要となります。

hash_file

hash_fileは、ファイルの中身をハッシュ化するための関数で、第2引数以外は、hash関数と同じ構造です。第2引数にはファイルへのパスを渡します。

これは基本的に、hash関数にファイルの中身を渡した値と同じになります。

php > echo hash_file('sha256','d:/data.txt');
7895d162e2dc8b62d2a5271cfb2c12ad7cd32e2ba54a920601a8880aa184ff76

php > echo hash('sha256',file_get_contents('d:/data.txt'));
7895d162e2dc8b62d2a5271cfb2c12ad7cd32e2ba54a920601a8880aa184ff76

php > echo hash_file('haval160,4','d:/data.txt');
2c986b7f75056b27f002c089dfc3c90156e6b0de

php > echo hash('haval160,4',file_get_contents('d:/data.txt'));
2c986b7f75056b27f002c089dfc3c90156e6b0de

hash_init

さきほど、hash_fileで行うhash化がファイルの中身を読みだしたデータのハッシュと一致するという例示としてfile_get_contens関数を用いる方法を紹介しましたが、file_get_contens関数ではサイズが大きなファイルだとメモリを圧迫することになります。hash_file関数を用いれば(おそらく)データを分割して処理してくれるのでそのようなことにはなりませんが、そのような分割処理を明示的に行うには hash_init関数をはじめとする関数群を使います。

hash_initはhash関数と同様のアルゴリズムを指定する文字列を第1引数にうけハッシュコンテキストを返します。

hash_update関数に、コンテキストとハッシュ化したい文字列を断続的に渡します。

ハッシュ化したいすべての文字列を渡し終えたら、hash_final関数を呼ぶ事で目的のハッシュが得られます。

これらをつかって先ほどの hash_file('md5','d:/data.txt'); を置き換えると次のようになります。

$md5 = hash_init('md5');
$fp = fopen('d:/data.txt', "r");
while (($line = fgets($fp)) !== false) {
  hash_update($md5, $line);
}
fclose($fp);
echo hash_final($md5);

crypt

crypt関数は、これまでのhash関数とは少し違った形のハッシュを生成します。この関数から得られる値は16進数ではありません。また、ハッシュを生成した際のアルゴリズムや、ハッシュのセキュリティを向上させるソルトといったハッシュのメタデータも同時に保持します。

第1引数にハッシュ化した文字列、第2引数には文字列を渡します。

第2引数の文字列は、ソルトという説明になっていますが、実際にはハッシュアルゴリズムとソルト、ストレッチ等を設定するための文字列です。

ルールにそぐわない第2引数を渡すと関数は、空の文字列を返します。

アルゴリズムとその指定の仕方は次のようになります。

  • 標準DES

    ソルト部にアルファベット文字列(./0-9A-Za-z)を2字渡すと、ハッシュ化アルゴリズムは標準DESとなります。

    空文字や1文字の場合はエラーとなり、ハッシュは空文字となります。

    3文字以上の値を渡すと先頭2字だけがソルトとなります。

    ソルトはハッシュの前に付与されて返されます。

    php > echo crypt('TEST', 'sa');
    saF0oZegeYoHs
    
  • 拡張DES

    アンダーバーから始まる9字のアルファベット文字列(./0-9A-Za-z)だった場合は拡張DESと判断されます。アンダーバーの後の4文字は反復回数(ストレッチ)、その後の4文字がソルトと判断されます。

    php > echo crypt('test', '_12345678');
    _123456783GFbe41Cen.
    
  • MD5

    $1$で始まり$で終わる12字の場合、MD5ハッシュとなります。全体で12字をオーバーした場合は、最後の一文字を$に変えた12字でハッシュが生成されるようです。

    php > echo crypt('TEST', '$1$12345678$');
    $1$12345678$6ixjOrpUkAZKrah.arl01/
    
  • BLOWFISH

    $2a$、$2x$、$2y$の4文字で始まるソルトは、Blowfishハッシュの使用を意味します。5文字目と6文字めで数値でコスト表します。コストの後さらに$を付与し残りはアルファベット文字列(./0-9A-Za-z)のソルトを指定します。

    指定の文字以外のソルトを使うか、文字数が21字以下だとハッシュは空になります。23文字以上の場合はその文字は切り捨てられます。

    ソルトのパラメータ内に基本的に、$記号は区切り文字として利用されていて、公式ページにも、ソルトの最後に$を付与しているようです。ただ、最後の$は付与しなくても問題なさそうですし、逆に付与してあるとソルトが20字や21字でもハッシュが戻ってきました。これはおそらく意図しない挙動だと想像します。

    php > echo crypt('test', '$2a$04$1234567890123456789012');
    $2a$04$123456789012345678901upHHr.tvk59gEcVReva1VSf3jvDcJ8we
    

    コストには4(04)から31までの間で設定します。指定した乗数分だけ2を乗算した数だけ反復(ストレッチ)します。

    最初の文字は後方互換の意図がなければ$2y$を使うことが推奨されています。

  • SHA256

    $5$からはじまる場合はSHA256ハッシュとなります。その後の16字がソルトとなり、長い分は切り捨てられます。

    ソルトの前にrounds=反復回数$として、ストレッチの回数を指定できます。省略した場合は5000で、1000~999,999,999までの値で指定可能です。範囲外の値を指定するとエラーとなりハッシュは空文字となります。(公式ページには範囲の近い値に寄せられるという事でしたが、筆者の環境ではエラーとなりました。)

  • SHA512

    $6$からはじまる場合はSHA512ハッシュとなります。他はSHA256と同じです。

password_hash

pawwword_hashは先のcryptのラッパークラスです。アルゴリズムを指定することで適切なソルトを指定したハッシュを返してくれます。

第1引数には、ハッシュ化したい文字列、第2引数にアルゴリズム、第3引数にはアルゴリズム別に利用可能な値が変化するオプションを指定します。

第2引数で、指定できるアルゴリズムは次の通りです。

  • PASSWORD_BCRYPT

    BLOWFISHを用います。デフォルトでは 2y、コスト10、ランダムなソルト値を使ってハッシュが生成されます。

  • PASSWORD_ARGON2I または PASSWORD_ARGON2ID

    Argon2iハッシュアルゴリズムを用います。PHPのビルド時のオプションによっては利用できない場合があります。Algon2アルゴリズムではハッシュ計算の時間や利用メモリの量が指定できます。

    php > echo password_hash('test',PASSWORD_ARGON2I);
    $argon2i$v=19$m=65536,t=4,p=1$SFVWNk92Rm1wV1drb3RwYg$Bv3RLEDu5Jobnh5rubNz4ey8CeIz01jXgR/GWvhd7Ho
    
  • PASSWORD_DEFAULT

    PHPが定めるデフォルトのハッシュアルゴリズムです。現在はBCRYPTとなっています。

オプションの渡す際は連想配列を使います。たとえば、BCRYPTに任意のコスト(cost)を渡す場合は次のようになります。

php > password_hash('test',PASSWORD_BCRYPT,array('cost'=>12));
$2y$12$k4GTEExjXj1FmW/mt2WmLOlEmm9IKUp6WplpQoe7RSgcStxlJdx8u

password_verify

crypt関数や、password_hash関数は主に、パスワードをハッシュ化して保存し照合するのに用います。

その照合に使う関数がpassword_verifyです。

生のパスワード値(入力値)を第1引数に、crypt関数やpassword_hash関数が生成したメタ情報を含むハッシュを第2引数として受け取りそれらが一致するかどうかをboolean値で返します。

この関数はタイミング攻撃に対して安全(=入力値が変わっても実行時間が変化しない)ようになっています。

password_verifyを使わない場合で、タイミング攻撃に耐性を持ちながら文字列の一致をチェックしたい際は hash_equals関数があります。関数名にhashと入っていますが、ハッシュに限ったことでなく第1引数と第2引数のふたつの文字列が一致するかを同じ処理時間で返します。

使用例を示しますが、このコードは使い方のサンプルの為そのままの形でパスワードがあります。実際にはこのような書き方はしません。

sample1.php

<?php

// ユーザーが入力したパスワード
$userInput="password";

// 実際はここでハッシュを作るのではなくハッシュ化されたデータを取得します
$HASH = password_hash('password',PASSWORD_BCRYPT);

if (password_verify($userInput,$HASH)) {
  echo "ログインしました";
} else {
  die('ログイン失敗');
}

hash_equalsを例示的に使うコードは次のようになります。

sample2.php

<?php

// ユーザーが入力したパスワード
$userInput="password";

// 実際はここでハッシュを作るのではなくハッシュ化されたデータを取得します
$HASH = password_hash('password',PASSWORD_BCRYPT);

// オリジナルのハッシュからソルト(とアルゴリズム、ストレッチ)を取得します。
$salt = substr($HASH,0,29);

// ユーザー入力をハッシュ化して、hash_equalsで照合します
if (hash_equals(crypt($userInput,$salt),$HASH)) {
  echo "ログインしました";
} else {
  die('ログイン失敗');
}

最後にタイミング攻撃について少し書いて終わりにします。

仮に、プレーンテキストで===を使ってパスワード照合をした場合、

if ("abcdefg"==="xxxxxxx") という条件文は、文字を先頭から比較していった時、1文字目で不一致が判明し結果が戻されます。

これが、if ("abcdefg"==="axxxxx") という条件文に変わった場合、今度は2文字目で不一致の結果が戻されます。

人間の感覚では判断できないわずかな違いですが、先頭の1文字が一致している方がレスポンスが遅くなる為攻撃者にヒントを与えてしまいます。この手法によってパスワードを突破しようとするのがタイミング攻撃です。

この問題が起きないのが、タイミング攻撃に対して安全という意味です。もし手書きのコードで実装するなら、たとえばfor文で最初から最後まで一文字ずつチェックした上で結果を返すといった方法や、相違があった場合はランダムな時間のウェイトをかけたりする方法があります。ちなみに、ランダムなウェイトをかける場合は、攻撃者の試行回数が増えるとレスポンス時間の平均値がパスワード一致の場合とパスワード不一致の場合で差を検出できてしまうので完全な対応ではありません。

筆者紹介


自分の写真
がーふぁ、とか、ふぃんてっく、とか世の中すっかりハイテクになってしまいました。プログラムのコーディングに触れることもある筆者ですが、自分の作業は硯と筆で文字をかいているみたいな古臭いものだと思っています。 今やこんな風にブログを書くことすらAIにとって代わられそうなほど技術は進んでいます。 生活やビジネスでPCを活用しようとするとき、そんな第一線の技術と比べてしまうとやる気が失せてしまいがちですが、おいしいお惣菜をネットで注文できる時代でも、手作りの味はすたれていません。 提示されたもの(アプリ)に自分を合わせるのでなく、自分の活動にあったアプリを作る。それがPC活用の基本なんじゃなかと思います。 そんな意見に同調していただける方向けにLinuxのDebianOSをはじめとした基本無料のアプリの使い方を紹介できたらなと考えています。

広告