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