シークレットキーから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)
元記事を表示する