Skip to content

Instantly share code, notes, and snippets.

@hdf
Last active March 3, 2025 09:10
Show Gist options
  • Save hdf/a15398e784a5d7db45d779cce740a5dc to your computer and use it in GitHub Desktop.
Save hdf/a15398e784a5d7db45d779cce740a5dc to your computer and use it in GitHub Desktop.
Input Secret, see TOTP code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/fontawesome.min.css">
<title>TOTP</title>
<style>
* {
padding: 0;
margin: 0;
font-family: Arial, sans-serif;
box-sizing: border-box;
}
body {
background-color: #e8f0f7;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.circular-container {
position: relative;
width: 55px;
height: 55px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #e8f0f7;
box-shadow: 6px 6px 10px -1px rgba(0,0,0,0.15),
-6px -6px 10px -1px rgba(255,255,255,0.7);
}
.inner-shadow {
position: absolute;
width: 42px;
height: 42px;
background: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.7), rgba(0, 0, 0, 0.1));
border-radius: 50%;
box-shadow: inset 6px 6px 10px -1px rgba(0,0,0,0.15),
inset -6px -6px 10px -1px rgba(255,255,255,0.7);
}
svg {
transform: rotate(-90deg);
}
.bar-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 22px;
font-weight: bold;
}
.totpArea {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.inputArea {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
margin: 10px;
}
.inputArea input {
width: 39ch;
}
.break {
flex-basis: 100%;
width: 0;
height: 0;
}
.inputArea #TOTP {
font-size: 1.2em;
font-weight: bold;
margin: 39px 9px 9px 0;
padding: 3px;
border: 1px solid #4390e1;
border-radius: 5px;
box-shadow: 6px 6px 10px -1px rgba(0,0,0,0.15),
-6px -6px 10px -1px rgba(255,255,255,0.7);
}
.clipbrd {
font-size: 1.7em;
margin: 27px 0 0 3px;
padding: 3px 4px 4px 3px;
border-radius: 5px;
border: 1px solid lightgray;
}
.clipbrd:hover {
background-color: #63b3f3;
cursor: pointer;
}
.clipbrd:active {
background-color: #53a3e3;
}
</style>
</head>
<body>
<div class="totpArea">
<div class="inputArea">
<label for="secret">Secret:&nbsp;</label>
<input id="secret" value="GIYEE6LUMVZUY33OM5ZWKY3SMV2EWZLZ">
<div class="break"></div>
<div id="TOTP"></div>
<i class="fa-regular fa-clipboard clipbrd" onClick="navigator.clipboard.writeText(document.getElementById('TOTP').innerHTML.replace(' ', ''));"></i>
</div>
<div class="break"></div>
<div class="circular-container">
<svg width="80" height="80" viewBox="0 0 100 100">
<!-- Background Circle -->
<circle cx="50" cy="50" r="42" stroke="#4285f4" stroke-width="12" fill="none" />
<!-- Progress Circle -->
<circle id="progress-bar" cx="50" cy="50" r="42"
stroke="#ddd" stroke-width="12" fill="none"
stroke-linecap="butt"/>
</svg>
<div class="inner-shadow"></div>
<div class="bar-text">30</div>
</div>
</div>
<script>
async function TOTP(secret) {
async function gen(key32) {
const keybytes = bigIntToByteArray(BigInt('0x' + base32tohex(key32)));
const timeFactor = int32ToByteArray(Math.floor((Date.now() / 1000) / 30));
// compute HMACSHA1(key, shift)
const key = await crypto.subtle.importKey("raw", keybytes,
{name: "HMAC", hash: { name: "SHA-1" }},
false, // no export
["sign"] // what this key can do
);
const signature = await crypto.subtle.sign("HMAC", key, timeFactor);
return truncate(new Uint8Array(signature));
}
/** This is supposed to convert 32-bit value to 8 byte array, padded left */
function int32ToByteArray(time) {
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setUint32(4, time, false);
return buf;
}
/** This is supposed to convert bitint to byte array in correct endianness */
function bigIntToByteArray(bigNumber) {
let bytes = [];
while (bigNumber > 0) {
bytes.push(Number(bigNumber % BigInt(256)));
bigNumber = bigNumber / BigInt(256);
};
let result = Uint8Array.from(bytes);
return result.reverse();
}
function truncate(hmac) {
const offset = hmac[hmac.length - 1] & 0xf;
const bin_code =
(hmac[offset + 0] & 0x7f) << 24 |
(hmac[offset + 1] & 0xff) << 16 |
(hmac[offset + 2] & 0xff) << 8 |
(hmac[offset + 3] & 0xff);
let code = (bin_code % Math.pow(10, 6)).toString().padStart(6, '0');
return code;
}
function base32tohex(base32) {
for (var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", bits = "", hex = "", i = 0; i < base32.length; i++) {
var val = base32chars.indexOf(base32.charAt(i).toUpperCase());
bits += val.toString(2).padStart(5, '0');
}
for (i = 0; i + 4 <= bits.length; i += 4) {
var chunk = bits.substr(i, 4);
hex += parseInt(chunk, 2).toString(16)
}
return hex;
}
return await gen(secret);
}
function remainingSeconds() {
return Math.floor(30 - ((((Date.now() / 1000) / 30) - Math.floor((Date.now() / 1000) / 30)) * 30));
}
let progressCircle = document.getElementById("progress-bar");
let barText = document.querySelector(".bar-text");
window.maxVal = parseInt(barText.innerHTML);
window.currVal = remainingSeconds();
window.firstRun = true;
let speed = 100; // Update frequency of animation.
let circumference = 2 * Math.PI * parseInt(progressCircle.getAttribute("r"));
progressCircle.style.strokeDasharray = circumference;
progressCircle.style.strokeDashoffset = circumference;
function updateProgress() {
progressCircle.style.transition = `stroke-dashoffset ${1}s linear`;
if (window.currVal <= 0) {
window.currVal = window.maxVal;
progressCircle.style.transition = `stroke-dashoffset ${0.06}s linear`;
} else if (window.firstRun) {
progressCircle.style.transition = `stroke-dashoffset ${0.01}s linear`;
window.firstRun = false;
}
let specialOffset = ((window.currVal === window.maxVal) ? 0 : 0.5);
progressCircle.style.strokeDashoffset = ((window.currVal - specialOffset) / window.maxVal) * circumference;
barText.innerHTML = window.currVal;
}
setInterval(updateProgress, speed);
function updateStuff() { // This is where you change the value.
window.currVal = remainingSeconds();
(async () => {
var val = await TOTP(document.getElementById('secret').value);
document.getElementById('TOTP').innerHTML = val.substr(0, 3) + " " + val.substr(3);
})();
setTimeout(updateStuff, 1000);
}
updateStuff();
</script>
</body>
</html>
@hdf
Copy link
Author

hdf commented Mar 2, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment