LoginSignup
1
1

More than 3 years have passed since last update.

ワンタイムパスワードをサーバー側とクライアント側で作るサンプル

Last updated at Posted at 2020-09-25

ワンタイムパスワードのロジックを作ってみた

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分後のパスワード)

image.png

心残り部分

  • 久しぶりにJS書いたら何実行してもPromiseばかり帰ってくる。試行錯誤で, async, await を書きまくったけど、あんまり自身がない。
  • RFCにもちゃんとワンタイムパスワードのロジックはあるらしいが読まずに「きっとこういうことだろう」で済ませてる
  • 端末側はJSで書いちゃったけど、肝心の「秘密鍵を渡す方法」がこの記事では触れられてない。QRコードで渡すのが良さそうだけど、QRコードを読んで端末側のローカストレージに秘密鍵を保存するというプログラムの方がよほど上記のソースよりも長くなりそう。そういうことまで考えると、 Google Authenticator を使い、 Google Authenticator のサーバー側のライブラリを使っちゃうので解決か。
  • sha256でダイジェストを作ったけど、10進数にして最後の6桁を取り出してる時点で、衝突確率は増える。
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1