Last active
March 3, 2025 09:10
-
-
Save hdf/a15398e784a5d7db45d779cce740a5dc to your computer and use it in GitHub Desktop.
Input Secret, see TOTP code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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: </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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Can be viewed here:
https://gistpreview.github.io/?a15398e784a5d7db45d779cce740a5dc/totp.html