サーバ関連系でハマったお話です。

通信時にリクエストとアプリケーション用のキーからシグネチャを生成して認証を行うシステムの開発で、「PHPとnode.jsでマルチバイト文字が含まれる場合にSHA-256のハッシュ結果が異なる」ことがありました。
node.js側のシステムは稼働中であり、こちらを修正することが難しかったため、PHP側で対処することとなりました。

コード

前提として、文字コードはすべてutf8です。

// node.js
var crypto = require('crypto');

var ascii_string = '1';
console.log(hashHmac(ascii_string));
// 6ff6ceedb1c100266849d7e4e13449c98d1c98e569994f8ddbfe7b52e780dead

var utf8_string = 'あ';
console.log(hashHmac(utf8_string));
// 45709a1cb960f7f15115206d921776da69be6197f486df8b78a04e2790d79c9c

function hashHmac(data) {
    var hmac = crypto.createHmac('sha256', data);
    hmac = hmac.update(data);
    return hmac.digest('hex');
}
<?php
$ascii_string = '1';
echo hash_hmac('sha256', $ascii_string, $ascii_string, false);
// 6ff6ceedb1c100266849d7e4e13449c98d1c98e569994f8ddbfe7b52e780dead

$utf8_string = 'あ';
echo hash_hmac('sha256', $ascii_string, $ascii_string, false);
// 812a1c58990d93aaabb35a0d3cc9cc5b6c6cfcab965d2a5e36b253d80aeca33b

ASCII文字の場合は一致しますが、アルゴリズムが同じですので、言語に関わらず結果が等しくならなくてはなりません。
この違いはどこから来るのでしょうか

node.jsの暗黙的な変換

node.jsのドキュメントを見てみますが、こちらにはヒントになる情報はありませんでした。
https://nodejs.org/api/crypto.html#crypto_crypto_createhmac_algorithm_key

node.js側の実行ファイル文字コードををS-JISなどに変更すると、同じ「あ」でも結果が変わることに気づき「文字コードが何か変換されているのではないか」とあたりをつけます。

結論として、node.jsのcryptoでは文字を暗黙(?)にASCIIのBufferオブジェクトに変換していることがわかりました。
Bufferはnode.js内でバイナリデータを扱うためのクラスです。

上で示したコードは、次のコードと等価であることがわかりました。

// node.js
var crypto = require('crypto');

var utf8_string = 'あ';
console.log(hashHmac(utf8_string));
// 45709a1cb960f7f15115206d921776da69be6197f486df8b78a04e2790d79c9c

function hashHmac(data) {
    var buf  = new Buffer(data, 'ascii');
    var hmac = crypto.createHmac('sha256', buf);
    hmac = hmac.update(buf, 'binary');
    return hmac.digest('hex');
}

Bufferクラスの挙動について

以下参考にしたSourceForgeさんの記事に次の一文がありました。

非ASCII文字が含まれる文字列に対しasciiエンコーディングを指定された場合、各文字の下位7ビット部分のみが使われる。

同じ記事の抜粋ですが、utf8のつもりが、asciiとして扱われておりバイナリコードが書き換えらてていました。

> new Buffer('あ', 'utf8');
<Buffer e3 81 82>
> new Buffer('あ', 'ascii')
<Buffer 42>

「e3 81 82」=「42」???

「e3 81 82」を2進数変換して下位7ビットを抜き出しても、42にはならなずこの点だけが最後までわかりませんでした。
しかし「あ」をunicodeに変換するとu3042となりますが、この下位バイトが42と、一致するため解決に至りました。

最終的なPHPコード

<?php
$data = toAscii('あ');
echo hash_hmac('sha256', $data, $data, false);
// 45709a1cb960f7f15115206d921776da69be6197f486df8b78a04e2790d79c9c

function toAscii($string) {
    return preg_replace_callback("/((?:[^x09x0Ax0Dx20-x7E]{3})+)/", "callbackToAscii", $string);
}
function callbackToAscii($matches) {
    $char  = mb_convert_encoding($matches[1], "UTF-16", "UTF-8");
    $ascii = "";
    for ($i = 0, $l = strlen($char); $i < $l; $i += 2) {
      $ascii .= hex2bin( sprintf("%02x", ord($char[$i+1])) );
    }
    return $ascii;
}

まとめ

  • node.jsの文字列にはStringとBufferがある
  • node.jsでBufferにASCIIを指定すると、マルチバイトでも無理やりASCIIに変換してしまう
  • 型を意識する

参考サイト

スポンサーリンク
ad_336
ad_336
  • このエントリーをはてなブックマークに追加
  • Evernoteに保存Evernoteに保存