2段階認証アプリが読み取る otpauth:// のQRコードを作ります(TOTP / HOTP 対応・ブラウザ内で完結)
2段階認証アプリが読み取るQRコードの中身は、otpauth://totp/{発行者}:{アカウント}?secret={Base32}&issuer={発行者} という1本のURLです。この書式は Google Authenticator の Key URI Format として公開されており、多くの認証アプリが同じ形を受け付けます。totp の部分が種類で、時刻ベースなら totp、カウンタベースなら hotp になります。
共有シークレットは Base64 ではなく Base32(A〜Z と 2〜7 の32文字)で書きます。数字の 0 1 8 と紛らわしい文字を外した集合なので、0 や 1 が入っていたら入力ミスの可能性が高いです。このツールは入力を大文字化し、Base32 に無い文字を取り除いてから使います。パディングの = は付けても付けなくても構いません。長さは20バイト(Base32 で32文字)が一般的です。
既定は SHA1・6桁・30秒です。既定値はURIに書かなくても同じ意味になるため、このツールは既定と違うときだけパラメータを足します。注意点として、SHA256 や SHA512、8桁、30秒以外の period に対応していない認証アプリがあります。サーバ側の実装が対応していても、利用者のアプリが無視して SHA1・6桁・30秒で計算すると、コードが永久に合いません。変更する場合は、配布前に実際のアプリで1回合わせてみてください。
TOTP は現在時刻を period(既定30秒)で割った値を使うので、一定時間ごとにコードが変わります。HOTP はカウンタを使い、1回使うごとにカウンタが1つ進みます。時計合わせが不要な代わりに、サーバ側とアプリ側でカウンタがずれると合わなくなります。一般的なWebサービスの2段階認証はほぼ TOTP です。
いいえ。入力からQRコードの生成・ダウンロードまで、すべての処理はお使いのブラウザ内(JavaScript)で完結します。シークレットがサーバーへ送信されることはありません。ただし共有シークレットはパスワードと同じ重さを持つ情報なので、生成したQR画像の取り扱いにはご注意ください。
ブラウザの暗号用乱数(crypto.getRandomValues)で20バイトを生成し、Base32 の32文字に変換しています。日時や連番から作ってはいません。ただしこのツールで作った値をそのまま本番に使う場合は、サーバ側にも同じ値を安全な経路で登録する必要があります。
多い原因は3つです。1つ目はシークレットの取り違えで、Base32 として不正な文字が混ざっていないか確認してください。2つ目は端末の時計のずれで、TOTP は時刻を使うため数十秒ずれると合いません。3つ目は algorithm・digits・period を既定から変えた場合で、認証アプリ側が対応していないと既定値で計算されます。