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

更新まで: 30
<div id="secret">
  <input type="text" id="secretKey" value="5D75PX73V5SMK47PYNG7HLJTHIXL5GJNKXSHZJUK4MU7VTUPFXYZ" />
</div>
<div id="code"></div>
<div id="timer">更新まで: <span id="countdown">30</span> 秒</div>
<button id="copy">コピー</button>
body {
  font-family: sans-serif;
  text-align: center;
  margin-top: 50px;
}
#secret {
  margin-block: 1rem;
}
#secretKey {
  width: 90%;
  max-width: 540px;;
  font-size: 1rem;
}
#code {
  font-size: 3rem;
  color: #00c;
  margin-bottom: 1rem;
}
#timer {
  font-size: 1.5rem;
  color: #555;
  margin-bottom: 1rem;
}
#copy {
  padding: 0.5rem 1rem;
  font-size: 1rem;
  background: #00c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
#copy:active {
  background: #990000;
}
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 = 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 codeElem = document.getElementById('code')
const countdownElem = document.getElementById('countdown')
const copyBtn = document.getElementById('copy')
let lastTimeStep = null
let currentCode = '------'

async function tick() {
  const secretKey = document.getElementById('secretKey').value
  if (!secretKey) {
    codeElem.textContent = '------'
    countdownElem.textContent = '--'
    return
  }
  const now = Date.now()
  const seconds = Math.floor(now / 1000)
  const step = Math.floor(seconds / 30)
  const remain = 30 - (seconds % 30)
  countdownElem.textContent = remain

  if (step !== lastTimeStep) {
    lastTimeStep = step
    currentCode = await generateTOTP(secretKey)
    codeElem.textContent = currentCode
  }
}

copyBtn.addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText(currentCode)
    copyBtn.textContent = 'コピーしました!'
    setTimeout(() => {
      copyBtn.textContent = 'コピー'
    }, 2000)
  } catch (err) {
    alert('コピーに失敗しました')
  }
})

tick()
setInterval(tick, 1000)

元記事を表示する