ワンタイムパスワードのロジックを作ってみた
2020/09/25 現在では何かと銀行がクラックされまくっているが、いずれもワンタイムパスワードのような認証が入ってない。もちろんワンタイムパスワードも「そのワンタイムの間に」盗まれればアウトなんだけど、常時通信内容を奪うのは盗む方も大変だ。
ただ、ワンタイムパスワードの仕組みが、Google Authenticator などの外部の仕組みに依存するのは、別の意味でリスク(急に仕様が変わるなど)。
なので、自前でどこまでできるかを検証。
どのようにして動く?
以下の要素を組み合わせて、ダイジェスト(sha256)を生成し、ダイジェストから数値6桁を取り出してます。
- ユーザ毎に違う秘密鍵(クライアントとサーバーで同じ秘密鍵を持ちます)
- タイムスタンプ(今回のサンプルでは60秒で割った整数部を使います)
- ソルト(サーバー側で保持する長い文字列)
サンプルソース
端末側
onetime.js
async function digestMessage(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hash = crypto.subtle.digest('SHA-256', data);
return hash;
}
function buf2hex(buffer) { // buffer is an ArrayBuffer
return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
}
async function onetimePass(secret_key){
const ts_min = parseInt( Date.now() / 60000 ).toString();
const buffer = await digestMessage(secret_key + '_and_sault' + ts_min);
const hexString = '0x' + await buf2hex(buffer) ;
const digitString = BigInt(hexString).toString(10);
return digitString.slice(-6);
}
onetime.html
<html>
<script type="text/javascript" src="./onetime.js"></script>
<body>
<script>
(async () => {
document.write(await onetimePass('secret_key'));
})();
</script>
</body>
</html>
サーバー側
onetime.rb
require 'digest/sha1'
def onetime_pass(secret_key, next_min = false)
# タイムスタンプを秒から分に丸める
ts_min = Time.now.to_i / 60
ts_min += 1 if next_min # 次の分までカバーしたい場合(チェックを受ける側)
text = "#{secret_key}_and_sault"
Digest::SHA256.hexdigest("#{text}#{ts_min}").to_i(16).to_s[-6, 6]
end
# ちょうど一分をまたがることを想定して、一分後のパスワードも取得できるようにしてる
puts onetime_pass('secret_key')
puts onetime_pass('secret_key', true)
実行結果
フロント側とサーバー側で同じ6桁ができてることがわかる。
(サーバー側のニ行目は、1分後のパスワード)
心残り部分
- 久しぶりにJS書いたら何実行してもPromiseばかり帰ってくる。試行錯誤で, async, await を書きまくったけど、あんまり自身がない。
- RFCにもちゃんとワンタイムパスワードのロジックはあるらしいが読まずに「きっとこういうことだろう」で済ませてる
- 端末側はJSで書いちゃったけど、肝心の「秘密鍵を渡す方法」がこの記事では触れられてない。QRコードで渡すのが良さそうだけど、QRコードを読んで端末側のローカストレージに秘密鍵を保存するというプログラムの方がよほど上記のソースよりも長くなりそう。そういうことまで考えると、 Google Authenticator を使い、 Google Authenticator のサーバー側のライブラリを使っちゃうので解決か。
- sha256でダイジェストを作ったけど、10進数にして最後の6桁を取り出してる時点で、衝突確率は増える。