フロントエンド開発者のためのハッシュ関数入門
1. はじめに
「ハッシュ関数」と聞くとなんだか難しい分野と考えがちですが、私たちの身近にハッシュ関数は多く潜んでいます。ハッシュ関数を用いることで私たちは安全にYoutubeで動画の試聴を行えたり、MetaMaskで安全にETHの取引を行うことができます。今回はそんな「ハッシュ関数」をカジュアルに、(なるべく)フロントエンド目線で皆さんにお話しできればと思います。
1-1.ハッシュ関数とは何か、その重要性について
https://medium-company.com/ハッシュ関数/
ハッシュ関数とは、任意の長さのデータ(メッセージ)をあらかじめ決められた固定長のハッシュ値(デジタル指紋)に変換する関数のことを指します。同じデータをハッシュ関数に入力すると、必ず同じハッシュ値が出力されます。しかし、ほんのわずかな違い(1ビットの違いでさえ)があると、全く異なるハッシュ値が生成されます。
ハッシュ関数の重要な特性はその一方向性です。つまり、ハッシュ値から元のメッセージを復元することは理論的には不可能です。また、理想的なハッシュ関数は「衝突耐性」を持っています。これは、2つの異なるメッセージが同じハッシュ値になること(衝突)を避ける能力を指します。
主なハッシュ関数には以下の特徴があると言えます。
- 元のデータと計算の結果得られたハッシュ値の間に規則性がない
- 入力値が少しでも違うと全く別のハッシュ値になる
- ハッシュ値から元のデータを効率良く求めることができない(復号できない)
- 同じハッシュ値となる元データが容易に見つけられない
つまりハッシュ関数を用いることでデータの完全性、認証、セキュリティ、パフォーマンスの最適化といった、特にセキュリティの観点からは必要不可欠なツールと言えます。
実際に元データとそのハッシュ値をみてみましょう。
以下のケースでは「aaaAAA111」という文字列をSHA256でハッシュした場合の結果です。
aaaAAA111 // 元データ
7c61de005bc5a585a488d5076109cf2623c16580359e7334fe374514b3e3f5d6 // ハッシュ値
またハッシュ関数は入力値が少しでも違うと全く別のハッシュ値になる性質を持っているのでsufixに「@」をつけた結果全く別のハッシュ値になっていることが確認できますね。
aaaAAA111@ // 元データ
8b2d88ddfe82ad3bf99369e1905a343d03fd776684d57145e1fed707ed56cc80 // 元データ
実際にハッシュ関数を用いたアプリケーションの例をご紹介します。
ケース1-YouTubeを見よう-
たとえばあなたがYouTubeでお気に入りの動画を視聴しようとします。その時あなたはまずYouTubeを利用するためにログインを行います。あなたは自身の保有するメールアドレスとパスワードを使用してログインを試みるとします(Googleアカウントでログインするだろ普通...といったツッコミはスルーします)。この時あなたはメールアドレスとパスワードの入力フォームにそれぞれ求められるテキストデータを入力し、ログインボタンを押します。程なくしてあなたはYouTubeのトップページを目にするでしょう。
この過程でハッシュ化は行われています。以下に手順を記載します。
- メールアドレスとパスワードの入力フォームに入力
- ログインボタンを押す
- クライアントを介してサーバーにメールアドレスとパスワードが送られる
- サーバーは送られてきたメールアドレスで特定のユーザーをDBから見つけ出す
- 該当ユーザーデータの持つパスワードのハッシュ値を取得
- 送られてきたパスワードをハッシュしてハッシュ値を生成しておく
- ユーザーデータの持つハッシュ値と送られてきたパスワードの2つのハッシュ値を比較・評価する
- 2つのハッシュ値が同じであればログイン成功
- YouTubeのトップページに遷移する
これによりサービス側はパスワードの直データを保持せず、ハッシュ関数を用いることでユーザーは安全にサービスを利用することができます。これによりパスワード漏洩を防ぐことができます。
他にも似たようなケースで
- アカウント仮登録機能
- ログイン不要のプライベート機能の利用
など様々なケースでハッシュ関数を利用することができます。
ケース2-NFTを保有しよう-
実はハッシュ関数をふんだんに取り入れているのは仮想通貨をはじめとしたブロックチェーン技術なのです。例えばあなたがMetaMaskを利用して新たにウォレット(アカウント)を作成して、ETHのを購入し、そのETHを使用してお気に入りのNFTを購入するとします。この間に多くのハッシュ関数を使用して安全な取引を成立させているのです。
- MetaMaskを利用してウォレットを作成する
- ウォレットを作成する際に秘密鍵と公開鍵のペアが生成されます
- その公開鍵からハッシュ関数を用いて一意なETHウォレットのアドレスが生成されます(このアドレスが自身のウォレットを特定することができる)
- ETHを購入する
- ETHを購入擦る取引を行うとその取引情報(送金者、受取人、金額など)が取引のレコードとして生成されます。(このレコードはハッシュ関数により一意のハッシュ値を持つようになり取引を一意的に扱います)
- NFTを購入します
- 5と同じ要領で取引情報がハッシュ関数を用いて一意の取引として記録されます
- 購入後のNFTが誰のものであるかの所有権を確認します
- NFTのハッシュ値と取引のハッシュ値を用いて対象のNFTが現在誰に所有権があるのかを追跡して証明することができます。
各取引はブロックチェーン(台帳)に記録され続けます。ここでも、直前の取引のハッシュ値を含んで最新の取引情報は記録されます。そのため台帳の改竄が大変難しく現実的には不可能とされています。仮想通貨といった諸に機密情報が常に飛び交う世界ではハッシュ関数は至る所で使われています。それはまるで、現実世界の借用書にサインすることと同等、ないしそれよりも信頼性が高いと言えます。
2. ハッシュ関数の基本
前述でハッシュ関数の用途や重要性についてある程度ご理解いいただけたと思いますので、改めてハッシュ関数の基本的な概念と特性についてお話しします。
一定の出力長さ
ハッシュ関数は、どんな長さの入力データでも、常に一定の長さの出力(ハッシュ値)を生成します。たとえば、SHA-256は名前の通り256ビットのハッシュ値を出力します。
一方向性
ハッシュ関数は「一方向」の関数です。つまり、ハッシュ値から元のメッセージを逆算することは非常に困難(実質的に不可能)です。この性質は、パスワードの安全な保存やデータの改ざん検出などに役立ちます。もし仮に元メッセージを求めようとすると大抵総当たり攻撃が一般的な手段として挙げられますが、これは効率とは程遠い手法になります。
また一般的な暗号技術では「暗号」と「復号(暗号を正常に読み解く)」という手法を用いて安全な情報処理を行いますが、一方向性にはこの概念はありません。
衝突耐性
良いハッシュ関数は、「衝突耐性」を持つべきです。これは、異なる2つのメッセージが同じハッシュ値を生成すること(衝突)が非常に困難であるという性質を指します。もし衝突が容易に発生するようなハッシュ関数を使用すると、攻撃者が異なるデータで同じハッシュ値を作り出すことが可能になり、セキュリティ上の問題を引き起こす可能性があります。つまりこれは、異なる文字列のパスワードを入力したにも関わらずハッシュ値が同じになってしまい不正ログインができてしまうリスクを下げることができます。
感度
ハッシュ関数は「微小な入力の変化が大きな出力の変化を引き起こす」という特性を持ちます。これは、入力データがわずかに変化しても(たとえ1ビットだけでも)、出力されるハッシュ値は大きく異なるべきであるという性質です。前述でも例をお見せしましたが、これによりデータの改ざんが容易に検出できます。
また、ハッシュ関数には種類が様々ありますが中でも一般的なのはSHA256(一部の数学界隈では「シャニゴロ」と呼称するらしい)があります。
3. SHA-256について
現在SHA-256が一般的に使用されているハッシュ関数で大抵最も推奨されているハッシュ関数です。そんなSHA-256についてその概要と特性をまとめてみました。
3-1. SHA-256の概要とその特性
SHA-256(Secure Hash Algorithm 256-bit)は、アメリカ国立標準技術研究所(NIST)が公開したセキュアハッシュアルゴリズム(SHA)ファミリーの一部であり、主にデジタルセキュリティ分野で使用されています。SHA-256はその名の通り、256ビット長のハッシュ値を出力します。
固定長出力
SHA-256は、どのような長さの入力データでも常に256ビットのハッシュ値を生成します。
一方向性
SHA-256はハッシュ値から元のデータを復元するのは不可能とされています。これは主にパスワードの保管などに利用されます。
衝突耐性
SHA-256は非常に強力な衝突耐性を持っています。つまり、異なる入力から同じハッシュ値が生成される可能性(衝突)は非常に低いです。
感度
SHA-256は入力の微小な変化に対しても、出力(ハッシュ値)が大きく変化します。これによりデータの改ざんがすぐに検出できます。
3-2. SHA-256の具体的な動作プロセス
SHA-256は、メッセージを固定長の256ビットのハッシュ値に変換するためのプロセスを実行します。以下にその大まかなプロセスを説明します。
メッセージのパディング
入力されたメッセージは512ビット単位になるようにパディング(詰め物)が行われます。パディングにはメッセージの元の長さを示すビットが含まれます。
メッセージの分割
パディング後のメッセージは512ビットのブロックに分割されます。
ハッシュ値の初期化
初期ハッシュ値は事前に定義された8つの32ビットワードに設定されます。
メインのハッシュループ
各512ビットのメッセージブロックはメインのハッシュループ(64ラウンドからなる)に入力されます。このループでは、各ラウンドで特定の数学的操作(ビットの回転、ビットの論理演算など)が行われ、新しいハッシュ値が計算されます。
最終的なハッシュ値の生成
全てのメッセージブロックが処理された後、最後のハッシュ値が生成されます。このハッシュ値は8つの32ビットワードを連結したものとなり、合計256ビットになります。
また以下のページではSHA256のハッシュプロセスを可視化したシミュレーターで試すことができます。
3-3. その他ハッシュ関数
MD5 (Message Digest Algorithm 5)
128ビットのハッシュ値を生成します。しかし、現在では脆弱性が明らかになっているため、セキュリティに敏感なアプリケーションでは使用すべきではありません。
SHA-1 (Secure Hash Algorithm 1)
160ビットのハッシュ値を生成します。しかし、このアルゴリズムも現在では衝突脆弱性が確認されており、新たなシステムでは避けるべきです。
SHA-2 (Secure Hash Algorithm 2)
これは6つのハッシュ関数(SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256)を含んでいます。256ビット以上のハッシュ値を生成します。大抵のソフトウェアで高速な構成になっています。
SHA-3 (Secure Hash Algorithm 3)
SHA-3ファミリーも複数のハッシュ関数(SHA3-224, SHA3-256, SHA3-384, SHA3-512など)を含んでいます。ハードウェアでの処理では高速でよりセキュアな構成になっています。
Blake2
SHA-3のコンテストに参加したが選ばれなかったアルゴリズムで、SHA-3と同等のセキュリティを提供しながらより高速です。
Tiger
主に64ビットプラットフォームに最適化された暗号学的ハッシュ関数です。
Whirlpool
512ビットのハッシュ値を生成するハッシュ関数で、ISO/IEC 10118-3:2004で標準化されています。
RIPEMD (RACE Integrity Primitives Evaluation Message Digest)
RIPEMD-160など、複数のバージョンがあります。
CityHash, MurmurHash, FNV (Fowler–Noll–Vo) hash
非暗号学的ハッシュ関数の例です。主にハッシュテーブルなどのデータ構造で使用されます。
ちなみに2017年にGoogleがSHA-1の検証中に実際に衝突したことを記録した記事があります。
4. JavaScriptでのSHA-256の使用
サーバーサイドのイメージが強いハッシュ関数ですが、実はフロントエンドでも実際に利用することができます。またそれらを用いて特定の課題を解決する糸口にもなり得ます。ここではJavaScriptでSHA256を実装する方法をサンプルコードを交えてお話しします。
JavaScriptで使えるSHA-256ライブラリの紹介
JavaScriptでSHA-256を実装するためには、いくつかのライブラリがありますが、ここでは2つ、crypto-jsとWeb APIのSubtleCrypto.digestを紹介します。
crypto-js
crypto-jsはJavaScriptで広く使われている暗号化ライブラリの一つで、ハッシュ関数を含む多くの暗号化関数を提供しています。SHA-256のハッシュを計算するためのサンプルコードは以下の通りです。
const CryptoJS = require("crypto-js");
let hash = CryptoJS.SHA256("Message");
console.log(hash.toString(CryptoJS.enc.Hex));
このコードは、"Message"という文字列をSHA-256でハッシュ化し、その結果を16進数の文字列として出力します。
SubtleCrypto.digest
WebブラウザのJavaScript環境では、Web Cryptography APIの一部として提供されるSubtleCrypto.digestメソッドを使用してSHA-256のハッシュを計算することも可能です。これは暗号化の標準化を目指すWeb APIで、以下のように使用します。
async function digestMessage(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hash = await window.crypto.subtle.digest('SHA-256', data);
return hash;
}
digestMessage('Message').then(hash => {
console.log(hash);
});
このコードも、"Message"という文字列をSHA-256でハッシュ化しますが、出力はArrayBuffer形式となります。このArrayBufferをさらに扱いやすい形(例えば16進数文字列)に変換するためには追加の処理が必要です。
またCryptoは、ウェブページが暗号に関連したサービスにアクセスできるようにします。たとえば前述のハッシュ関数を提供したり、乱数を生成したり、UUIDといったランダムな文字列を生成するのに役立ちます。
5. フロントエンドでのハッシュ関数の適用
ここでは実際にフロントエンドでハッシュ関数を使った実装例をご紹介します。フロントエンドでもハッシュ関数を用いることでよりセキュアなサービス提供が可能になります。
5-1. データの整合性チェックの実装
ハッシュ関数はパスワードリセットの流れにおいて、ユーザーのデータ整合性の確認に役立てることができます。
仮にユーザーがパスワードを忘れ、パスワードリセットを要求したとしましょう。システムはユーザーに一時的なパスワードを生成し、そのハッシュ値をデータベースに保存しておきます。そして、メールにて一時的なパスワードとパスワードリセット用のURL(ハッシュ値をパラメータに含む)をユーザーに送信します。
つまり以下のリンクを対象のユーザー宛のメールに載せます。
https://example.com/reset=password?tempPass={仮のパスワード}?hash={仮のパスワードのハッシュ値}
このURLにアクセスしたユーザーは、新しいパスワードを入力し、送信します。送信された新しいパスワードはサーバでハッシュ化され、先ほどメールで送られてきたハッシュ値と比較されます。ハッシュ値が一致する場合、ユーザーのパスワードを新しいものに更新します。
JavaScriptにてフロントエンド側の実装例を以下に示します:
// 仮パスワードとハッシュ値を取得
const params = new URLSearchParams(window.location.search);
const temporaryPassword = params.get('tempPass');
const hashedPassword = params.get('hash');
// ハッシュ関数(SHA-256)を用いて仮パスワードをハッシュ化
async function getHash(tempPass) {
const msgUint8 = new TextEncoder().encode(tempPass);
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashedPass = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashedPass;
}
getHash(temporaryPassword).then((hashedTempPass) => {
// 仮パスワードのハッシュ値とURLから取得したハッシュ値を比較
if(hashedTempPass === hashedPassword) {
// 新しいパスワードの入力と更新処理を実行
let newPassword = prompt("新しいパスワードを入力してください:");
updatePassword(newPassword); // updatePasswordはサーバーと通信してパスワードを更新する関数とする
} else {
alert("URLが無効です。");
}
});
なお、実際の実装においては、セキュリティの観点から、このような処理は主にサーバーサイドで行うことが一般的です。フロントエンドで直接パスワードやそのハッシュを扱うことは、セキュリティリスクを高める可能性があるためです。
5-2. ハッシュ関数を用いたトークンの生成と検証
OAuth 2.0及びOIDCの認証プロセスの一つであるPKCEではフロントでハッシュ値を渡すとよりセキュアな設計になるとして、機能を提供しています。
PKCEとは?
PKCE(Proof Key for Code Exchange)は、認証プロトコルであるOAuth 2.0が公共のクライアント(例えばモバイルアプリやSPA(Single Page Application)などのフロントエンドのみで動作するアプリケーション)を安全にサポートするための拡張です。
PKCEは、最初に一時的な秘密(コードベリファイア)を生成し、そのハッシュ(コードチャレンジ)を認証サーバーに送信します。認証が成功した後、サーバーは認証コードをクライアントに送信します。その後、クライアントはこの認証コードと元の秘密(コードベリファイア)を用いてアクセストークンを要求します。認証サーバーはコードベリファイアから自身でもう一度ハッシュを計算し、以前に保存したコードチャレンジと比較します。一致した場合に限りアクセストークンをクライアントに発行します。
PKCEにより、フロントエンドのみのクライアントでもアクセストークンを安全に取得することが可能になります。なぜなら、認証コードが攻撃者によって傍受されても、アクセストークンの取得には元の秘密(コードベリファイア)が必要であり、これはクライアントのみが知っているためです。
サンプルコード
// 1. code_verifierを生成
let array = new Uint32Array(56);
window.crypto.getRandomValues(array);
const code_verifier = Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
// 2. SHA256でハッシュ化してAuthrizaitionエンドポイントを叩く
const encoder = new TextEncoder();
const encode_data = encoder.encode(code_verifier);
window.crypto.subtle.digest('SHA-256', encode_data).then(hash => {
let base64 = btoa(String.fromCharCode(...new Uint8Array(hash)));
const code_challenge = base64.replace('+', '-').replace('/', '_').replace(/=+$/, '');
sessionStorage.setItem('code_verifier', code_verifier);
const authorize_endpoint = `${domain}/authorize?redirect_uri=${redrectUri}&response_type=${responseType}&scope=${scope}&state=${state}&response_mode=${responseMode}&account_type=${accountType}&password_reset=${passwordResetURL}&code_challenge=${code_challenge}&code_challenge_method=S256`;
});
// ...中略
// 3. Tokenエンドポイントを叩く
const fetchTokenByCode = async (reqBody) => {
const headers = new Headers()
const requestURL = `${token_endpoint}`
const reqeust = new Request(requestURL, {
method: 'POST',
headers: headers,
mode: 'cors',
cache: 'default',
body: reqBody
})
// ...中略
// 4. code_verifierを都ークンエンドポイントのリクエストに含める
let reqBody = JSON.stringify({
'code': code,
redirectUrl: redrectUri,
code_verifier: sessionStorage.getItem('code_verifier'),
})
細かい解説は省きますが注目していただきたいのがcode_verifier
とcode_challenge
とcode_challenge_methods
の部分です。
code_verifier: クライアント側で生成したランダムな文字列です。
code_challenge: code_verifierをSHA256でハッシュした文字列です。
code_challenge_methods: code_challengeの検証方法を指定します。plain
とS256
が選べます
authorize_endpoint
のパラメータにcode_challenge
とcode_challenge_methods
があります。サンプルコードではcode_challengeにハッシュ値を入れていて、code_challenge_methodsをS256に指定してしています。これにより認証サーバーにハッシュ値が保存されます。
その後code_verifierをtoken_endpointに含めてリクエストします。するとサーバーではcode_verifierと認証サーバーで保持しているcode_challengeと比較しハッシュ値が一致したら成功とみなしトークンをレスポンスします。
6. まとめ
いかがでしたでしょうか?ハッシュ関数についてある程度ご理解はいただけたかと思います。ハッシュ化は主にバックエンドで行われるべきプロセスではありますが、要件次第ではフロントエンドでも対応できるとよりセキュアな構成にすることができます。ハッシュ関数をうまく使いより良いサービス開発に従事してまいりましょう!
しかし、SHA-2やSHA-3などセキュアでハイクオリティなハッシュファミリーが存在しますが、量子コンピュータが一般的に普及されるとSHA-2やSHA-3も脆弱性が出てくるリスクがあります。そのためサービス開発者は常にセキュリティについて知識をアプデートしていく必要があります。
また、今回はハッシュ関数を中心にお話ししましたが、より広義な「暗号技術」について調べてみるとより良い知見が得られることを期待できます。
おまけ(本編)
ハッシュ関数が256ビットの出力を持つということは、その出力として取りうる値の総数が2の256乗であるということを意味します。つまり、SHA-256が生成するハッシュ値の一意性(衝突耐性)は、この非常に大きな数によって保証されていると言えます。
そこで2の256乗ってどれだけ大きい値なのかをイメージできる検証動画があったので最後にこちらをみてお開きにしましょう。
7. 参考文献
Discussion