tag:crieit.net,2005:https://crieit.net/tags/TOTP/feed
「TOTP」の記事 - Crieit
Crieitでタグ「TOTP」に投稿された最近の記事
2024-02-09T09:29:30+09:00
https://crieit.net/tags/TOTP/feed
tag:crieit.net,2005:PublicArticle/18735
2024-02-09T02:11:50+09:00
2024-02-09T09:29:30+09:00
https://crieit.net/posts/JavaScript-TOTP
JavaScript で TOTP を計算する
<p>TOTP: Time-Based One-Time Password Algorithm とは、二要素認証でよく見る <a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=ja&gl=US">Google Authenticator</a> 等で QRコード を読み込ませたりして、一定時間毎に替わる 6桁 くらいの数字を入力するアレだ。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://tex2e.github.io/rfc-translater/html/rfc6238.html">RFC 6238</a> に詳しい仕様が書かれている。<br />
コイツを TOTP をブラウザ上で生成したい。</p>
<p>幸い近年の Web API には、バイナリ操作やクリプトまわりの機能が出揃っているので、わりかし簡単に実装できそうだ。<br />
…とまぁ、そんな愚生が思いつくような話など既に、偉大なる先人たちによる多くのサンプルが残されている。</p>
<p>せっかく自分で実装するなら、最新の ES 仕様や Web API を活用して簡潔に、それでいて汎用的な仕様で書いてみよう。</p>
<h2 id="実装"><a href="#%E5%AE%9F%E8%A3%85">実装</a></h2>
<pre><code class="javascript">// @ts-check
const base32Alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const base32AlphabetValuesMap = new Map([
...Array.from(base32Alphabet, (key, index) => /** @type {[string, number]} */([key, index])),
]);
/** @type { (strBase32: string) => Uint8Array } */
function decodeBase32(strBase32) {
const strTrimmed = strBase32.replace(/=*$/, "");
const result = new Uint8Array(strTrimmed.length * 5 >>> 3);
let dataBuffer = 0;
let dataBufferBitLength = 0;
let byteOffset = 0;
for (const encoding of strTrimmed) {
const value = base32AlphabetValuesMap.get(encoding);
if (typeof value === "undefined") throw new Error("Invalid base32 string");
dataBuffer <<= 5;
dataBuffer |= value;
dataBufferBitLength += 5;
if (dataBufferBitLength >= 8) {
dataBufferBitLength -= 8;
result[byteOffset++] = dataBuffer >>> dataBufferBitLength;
}
}
if (dataBufferBitLength >= 5) throw new Error("Invalid base32 string");
if ((dataBuffer << (4 - dataBufferBitLength) & 0xf) !== 0) throw new Error("Invalid base32 string");
return result;
}
const hashAlgorithmNameMap = new Map([
["SHA1", "SHA-1"],
["SHA256", "SHA-256"],
["SHA512", "SHA-512"],
]);
/** @type { (secretUint8Array: Uint8Array, unixTimeMilliseconds: number, digits?: 6|7|8, period?: number, algorithm?: "SHA1"|"SHA256"|"SHA512") => Promise<string> } */
async function generateTOTP(secretUint8Array, unixTimeMilliseconds, digits = 6, period = 30, algorithm = "SHA1") {
// unixTimeMilliseconds to step binary
const stepsBuffer = new ArrayBuffer(8);
const dv = new DataView(stepsBuffer);
dv.setBigUint64(0, BigInt(Math.floor(unixTimeMilliseconds / 1000 / period)));
// calc Hash
const hashAlgorithmName = hashAlgorithmNameMap.get(algorithm);
if (!hashAlgorithmName) throw new Error(`invalid algorithm: ${algorithm}`);
const cryptKey = await crypto.subtle.importKey("raw", secretUint8Array, { name: "HMAC", hash: hashAlgorithmName }, false, ["sign"]);
const hash = new Uint8Array(await crypto.subtle.sign("HMAC", cryptKey, stepsBuffer));
// Truncate
const offset4Bit = hash.at(-1) & 0xf;
const binary = (
(hash[offset4Bit] & 0x7f) << 24 |
hash[offset4Bit + 1] << 16 |
hash[offset4Bit + 2] << 8 |
hash[offset4Bit + 3]
);
// stringify
const otp = binary % Math.pow(10, digits);
return otp.toString().padStart(digits, "0");
}
</code></pre>
<p>実行例</p>
<pre><code class="javascript">// 実行例
await generateTOTP(decodeBase32('XXXXXXXXXXXXXXXX'), Date.now(), 8, 30, "SHA1");
// 実行例2 => "94287082"
await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8)
// 実行例2.2 => "94287082"
await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA1")
// 実行例3 => "32247374"
await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA256")
// 実行例4 => "69342147"
await generateTOTP(decodeBase32('GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'), Date.parse("1970-01-01T00:00:59Z"), 8, 30, "SHA512")
</code></pre>
<p>ハッシュアルゴリズムや、表示桁数、表示期間と言ったパラメータを変化させて、TOTP を出力できる。</p>
<h2 id="応用例"><a href="#%E5%BF%9C%E7%94%A8%E4%BE%8B">応用例</a></h2>
<p>例えば、前回の QRコード リーダーと組み合わせれば、 TOTP の管理を Web アプリ側で完結させるような実装もできる。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/?p=2028">D&DしたQRコード画像をブラウザ上でデコードする | Aqua Ware つぶやきブログ</a></p>
<p>ちゃんと多要素認証の一要素として所有者認証を満たせるよう、実装にはかなり気をつける必要はあるだろうが。</p>
<h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2>
<ul>
<li><a target="_blank" rel="nofollow noopener" href="https://tex2e.github.io/rfc-translater/html/rfc6238.html">RFC 6238</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key Uri Format · google/google-authenticator Wiki</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kerupani129/items/4f3b44b2e00d32731ca4">[JavaScript] Unicode 文字列やバイナリデータを Base32 エンコードおよびデコードする #JavaScript - Qiita</a></li>
<li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kerupani129/items/4780fb1eea160c7a00bd">JavaScript で HOTP および TOTP を計算する #JavaScript - Qiita</a></li>
</ul>
advanceboy