Skip to content

Instantly share code, notes, and snippets.

@pgaskin
Created May 6, 2022 07:16
Show Gist options
  • Select an option

  • Save pgaskin/f2b8967010816fe5bcb079d913836269 to your computer and use it in GitHub Desktop.

Select an option

Save pgaskin/f2b8967010816fe5bcb079d913836269 to your computer and use it in GitHub Desktop.
TOTP/HOTP implementation for modern browsers (RFC 4226/6238)
/** Compute a HOTP/TOTP for a base32 secret. */
export async function computeOTP(secret, digits = 6, counter = Math.floor(Date.now() / (30 * 1000))) {
// parse the secret as base32, stripping whitespace and padding
const parsedSecret = Uint8Array.from(Array
.from(secret.replace(/[\s=]/g, "").toUpperCase())
.reduce((state, c) => {
const n = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c)
if (n < 0) throw new Error("Invalid base32")
state.as = (state.as << 5) | n
state.in += 5
while (state.in > 7) {
state.buf.push(state.as >> (state.in -= 8))
state.as &= ~(0xff << state.in)
}
return state
}, {buf: [], in: 0, as: 0}).buf
).buffer
// get last 8 bytes (left padded with zeroes)
const moving = Uint8Array.from(new Array(8)
.fill(counter.toString(2).slice(-8*8).padStart(8*8, "0"))
.map((s, i) => parseInt(s.slice(i*8, i*8+8), 2))
).buffer
// sign it with the secret
const key = await crypto.subtle.importKey(
"raw", parsedSecret,
{ name: "HMAC", hash: "SHA-1" },
false, ["sign"],
)
const hmac = new Uint8Array(await crypto.subtle.sign("HMAC", key, moving))
// HOTP dynamic truncation
const arr = hmac.subarray(hmac[hmac.length - 1] & 0xF)
const truncated = (
((arr[0] & 0x7F) << 24) |
((arr[1] & 0xFF) << 16) |
((arr[2] & 0xFF) << 8) |
((arr[3] & 0xFF))
)
// HOTP code generation
return (truncated % 10**digits).toString().slice(-digits).padStart(digits, "0")
}
export async function computeOTP(d,b=6,e=Math.floor(Date.now()/3e4)){let f=Uint8Array.from(Array.from(d.replace(/[\s=]/g,"").toUpperCase()).reduce((a,c)=>{let b="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".indexOf(c);if(b<0)throw new Error("Invalid base32");for(a.as=a.as<<5|b,a.in+=5;a.in>7;)a.buf.push(a.as>>(a.in-=8)),a.as&=~(255<<a.in);return a},{buf:[],in:0,as:0}).buf).buffer,g=Uint8Array.from(new Array(8).fill(e.toString(2).slice(-64).padStart(64,"0")).map((b,a)=>parseInt(b.slice(8*a,8*a+8),2))).buffer,h=await crypto.subtle.importKey("raw",f,{name:"HMAC",hash:"SHA-1"},!1,["sign"]),c=new Uint8Array(await crypto.subtle.sign("HMAC",h,g)),a=c.subarray(15&c[c.length-1]),i=(127&a[0])<<24|(255&a[1])<<16|(255&a[2])<<8|255&a[3];return(i%10**b).toString().slice(-b).padStart(b,"0")}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment