Skip to content

Instantly share code, notes, and snippets.

@rafaelsq
Last active June 10, 2024 07:20
Show Gist options
  • Save rafaelsq/e6a84d2f9b487917115362e39032d528 to your computer and use it in GitHub Desktop.
Save rafaelsq/e6a84d2f9b487917115362e39032d528 to your computer and use it in GitHub Desktop.
TOTP with Web Crypto API
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
<title>TOTP</title>
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">TOTP</h1>
</div>
<div class="container">
<div class="field has-addons">
<div class="control">
<input class="input" id="secret" type="text" value=""/>
</div>
<div class="control">
<button class="button is-primary" id=check>Gen&Copy</button>
</div>
</div>
<div class="field has-addons">
<div class="control"><label class=label>Code: </label><span id="code"></span></div>
</div>
</div>
</section>
<script>
const copy = text => {
const t = document.createElement('textarea')
t.value = text
document.body.appendChild(t)
t.select()
document.execCommand('copy')
t.remove()
}
const hexToBuf = hex => {
const bytes = []
for (let c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16))
return new Uint8Array(bytes)
}
const lPad = (str, len, pad) => {
if (len + 1 >= str.length) {
str = Array(len + 1 - str.length).join(pad) + str
}
return str
}
const b32ToHex = base32 => {
var base32chars, bits, chunk, hex, i, val
base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
bits = ''
hex = ''
i = 0
while (i < base32.length) {
val = base32chars.indexOf(base32.charAt(i).toUpperCase())
bits += lPad(val.toString(2), 5, '0')
i++
}
i = 0
while (i + 4 <= bits.length) {
chunk = bits.substr(i, 4)
hex = hex + parseInt(chunk, 2).toString(16)
i += 4
}
return hex
}
const decToHex = s => (s < 15.5 ? '0' : '') + Math.round(s).toString(16)
function getTimeCounter() {
const epoch = Math.round(new Date().getTime() / 1000.0)
return lPad(decToHex(Math.floor(epoch / 30)), 16, '0')
}
async function getOTP(secret) {
const keyData = hexToBuf(b32ToHex(secret))
const data = hexToBuf(getTimeCounter())
const key = await crypto.subtle.importKey('raw', keyData, {name: 'HMAC', hash: 'SHA-1'}, false, ['sign'])
const signature = await crypto.subtle.sign({name: 'HMAC', hash: 'SHA-1'}, key, data)
// Now that we have the hash, we need to perform the HOTP specific byte selection
// (called dynamic truncation in the RFC)
var signatureArray = new Uint8Array(signature)
var offset = signatureArray[signatureArray.length - 1] & 0xf
var binary =
((signatureArray[offset] & 0x7f) << 24) | ((signatureArray[offset + 1] & 0xff) << 16) | ((signatureArray[offset + 2] & 0xff) << 8) | (signatureArray[offset + 3] & 0xff)
return binary % 1000000
}
//
const secretEl = document.getElementById('secret')
const codeEl = document.getElementById('code')
const checkEl = document.getElementById('check')
const trigg = v => getOTP(v).then(code => {codeEl.innerText = code; copy(code)}).catch(alert)
secretEl.addEventListener('change', e => e.target.value && trigg(e.target.value))
checkEl.addEventListener('click', e => trigg(secretEl.value))
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment