JavaScriptだけでシークレットキーからTOTPの6桁認証コードを生成する方法

TOTPの6桁認証コードとは

TOTPの6桁認証コードとは、「時間ベースのワンタイムパスワード(Time-based One-Time Password)」のことで、30秒ごとに変化する6桁の数字のパスコードです。

TOTPの6桁認証コードの例

主に2要素認証(2FA)で使用され、Google AuthenticatorやAuthyなどのアプリで生成されます。

TOTPの6桁認証コードは以下のようなシークレットキーを、もとに生成されます。

5D75PX73V5SMK47PYNG7HLJTHIXL5GJNKXSHZJUK4MU7VTUPFXYZ

Google AuthenticatorやAuthyなどのアプリに登録する場合は、通常はQRコードを読み込んで登録しますが、QRコードにはこのようなシークレットキーのデータが含まれています。

JavaScriptでシークレットキーからTOTPの6桁認証コードを生成

TOTPの6桁認証コードはシークレットキーがあればJavaScriptで処理して生成できます。

やり方はまず、Base32エンコードされたシークレットキーをデコードし、Uint8Arrayとして返すための関数を作成します。

TOTPで使用されるシークレットキーは、通常Base32でエンコードされているため、これをバイト列に変換します。

JavaScript
async function base32Decode(base32) {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
  const cleaned = base32.replace(/=+$/, '').toUpperCase()
  const bits = cleaned.split('').map(c => {
    const val = alphabet.indexOf(c)
    if (val === -1) throw new Error('Invalid base32 char')
    return val.toString(2).padStart(5, '0')
  }).join('')
  const bytes = []
  for (let i = 0; i + 8 <= bits.length; i += 8) {
    bytes.push(parseInt(bits.slice(i, i + 8), 2))
  }
  return new Uint8Array(bytes)
}

次にTOTPの6桁コードを生成する関数を作成します。

処理手順

  1. Base32の秘密鍵をデコード
  2. 現在時刻を30秒単位で分割(Unix time / 30)
  3. HMAC-SHA1で署名
  4. RFC 6238の規定に従って6桁コードを抽出
  5. 100万で割って6桁にする
JavaScript
async function generateTOTP(secretBase32) {
  const secret = await base32Decode(secretBase32)
  const timeStep = Math.floor(Date.now() / 1000 / 30)
  const buffer = new ArrayBuffer(8)
  const view = new DataView(buffer)
  view.setUint32(4, timeStep)
  const key = await crypto.subtle.importKey(
    'raw',
    secret,
    { name: 'HMAC', hash: 'SHA-1' },
    false,
    ['sign']
  )
  const hmac = await crypto.subtle.sign('HMAC', key, buffer)
  const hash = new Uint8Array(hmac)
  const offset = hash[hash.length - 1] & 0x0f
  const binary =
    ((hash[offset] & 0x7f) << 24) |
    ((hash[offset + 1] & 0xff) << 16) |
    ((hash[offset + 2] & 0xff) << 8) |
    (hash[offset + 3] & 0xff)
  return (binary % 1000000).toString().padStart(6, '0')
}

試しにブラウザのConsoleで以下のコードを実行すると、シークレットキーから6桁の認証コードが生成されることが確認できます。

JavaScript
async function base32Decode(base32) {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
  const cleaned = base32.replace(/=+$/, '').toUpperCase()
  const bits = cleaned.split('').map(c => {
    const val = alphabet.indexOf(c)
    if (val === -1) throw new Error('Invalid base32 char')
    return val.toString(2).padStart(5, '0')
  }).join('')
  const bytes = []
  for (let i = 0; i + 8 <= bits.length; i += 8) {
    bytes.push(parseInt(bits.slice(i, i + 8), 2))
  }
  return new Uint8Array(bytes)
}

async function generateTOTP(secretBase32) {
  const secret = await base32Decode(secretBase32)
  const timeStep = Math.floor(Date.now() / 1000 / 30)
  const buffer = new ArrayBuffer(8)
  const view = new DataView(buffer)
  view.setUint32(4, timeStep)
  const key = await crypto.subtle.importKey(
    'raw',
    secret,
    { name: 'HMAC', hash: 'SHA-1' },
    false,
    ['sign']
  )
  const hmac = await crypto.subtle.sign('HMAC', key, buffer)
  const hash = new Uint8Array(hmac)
  const offset = hash[hash.length - 1] & 0x0f
  const binary =
    ((hash[offset] & 0x7f) << 24) |
    ((hash[offset + 1] & 0xff) << 16) |
    ((hash[offset + 2] & 0xff) << 8) |
    (hash[offset + 3] & 0xff)
  return (binary % 1000000).toString().padStart(6, '0')
}

const secretKey = prompt('シークレットキーを入力', '5D75PX73V5SMK47PYNG7HLJTHIXL5GJNKXSHZJUK4MU7VTUPFXYZ')
generateTOTP(secretKey).then((code) => {
  console.log('認証コード:', code)
})
ブラウザのConsoleでシークレットキーから6桁の認証コードを生成

シークレットキーはあるが、Google Authenticatorなどをパソコンにインストールできない状況でも、この方法であればアプリなしで6桁の認証コードを生成できます。

TOTPの6桁認証コードを生成するWebページを作成

前述のコードの処理をsetIntervalで30秒ごとに実行するようにすれば、シークレットキーからTOTPの6桁認証コードを生成するWebページを作成できます。

実際に作成したサンプルは以下のリンクから確認できます。

シークレットキーから6桁の認証コードを生成