Created
January 3, 2023 03:36
-
-
Save zachlankton/bf0ec610d044e793a1a9cccef6170e97 to your computer and use it in GitHub Desktop.
A simple TOTP File / Mini Library
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
const Convert: any = {}; | |
//Converts a base32 string into a hex string. The padding is optional | |
Convert.base32toHex = function (data: string) { | |
//Basic argument validation | |
if (typeof data !== typeof '') { | |
throw new Error('Argument to base32toHex() is not a string'); | |
} | |
if (data.length === 0) { | |
throw new Error('Argument to base32toHex() is empty'); | |
} | |
if (!data.match(/^[A-Z2-7]+=*$/i)) { | |
throw new Error('Argument to base32toHex() contains invalid characters'); | |
} | |
//Return value | |
let ret = ''; | |
//Maps base 32 characters to their value (the value is the array index) | |
const map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.split(''); | |
//Split data into groups of 8 | |
const segments = (data.toUpperCase() + '========').match( | |
/.{1,8}/g, | |
) as string[]; | |
//Adding the "=" in the line above creates an unnecessary entry | |
segments.pop(); | |
//Calculate padding length | |
const strip = segments[segments.length - 1].match(/=*$/); | |
if (strip === null) throw new Error('Strips is null!'); | |
//Too many '=' at the end. Usually a padding error due to an incomplete base32 string | |
if (strip[0].length > 6) { | |
throw new Error('Invalid base32 data (too much padding)'); | |
} | |
//Process base32 in sections of 8 characters | |
for (let i = 0; i < segments.length; i++) { | |
//Start with empty buffer each time | |
let buffer = 0; | |
const chars = segments[i].split(''); | |
//Process characters individually | |
for (let j = 0; j < chars.length; j++) { | |
//This is the same as a left shift by 32 characters but without the 32 bit JS int limitation | |
buffer *= map.length; | |
//Map character to real value | |
let index = map.indexOf(chars[j]); | |
//Fix padding by ignoring it for now | |
if (chars[j] === '=') { | |
index = 0; | |
} | |
//Add real value | |
buffer += index; | |
} | |
//Pad hex string to 10 characters (5 bytes) | |
const hex = ('0000000000' + buffer.toString(16)).substr(-10); | |
ret += hex; | |
} | |
//Remove bytes according to the padding | |
switch (strip[0].length) { | |
case 6: | |
return ret.substr(0, ret.length - 8); | |
case 4: | |
return ret.substr(0, ret.length - 6); | |
case 3: | |
return ret.substr(0, ret.length - 4); | |
case 1: | |
return ret.substr(0, ret.length - 2); | |
default: | |
return ret; | |
} | |
}; | |
//Converts a hex string into an array with numerical values | |
Convert.hexToArray = function (hex: { match: (arg0: RegExp) => any[] }) { | |
return hex.match(/[\dA-Fa-f]{2}/g).map(function (v: string) { | |
return parseInt(v, 16); | |
}); | |
}; | |
//Converts an array with bytes into a hex string | |
Convert.arrayToHex = function ( | |
array: string | any[] | ArrayBuffer | ArrayLike<number>, | |
) { | |
let hex = ''; | |
if (array instanceof ArrayBuffer) { | |
return Convert.arrayToHex(new Uint8Array(array)); | |
} | |
for (let i = 0; i < array.length; i++) { | |
hex += ('0' + array[i].toString(16)).substr(-2); | |
} | |
return hex; | |
}; | |
//Converts an unsigned 32 bit integer into a hexadecimal string. Padding is added as needed | |
Convert.int32toHex = function (i: number) { | |
return ('00000000' + Math.floor(Math.abs(i)).toString(16)).substr(-8); | |
}; | |
//TOTP implementation | |
const TOTP = { | |
//Calculates the TOTP counter for a given point in time | |
//time(number): Time value (in seconds) to use. Usually the current time (Date.now()/1000) | |
//interval(number): Interval in seconds at which the key changes (usually 30). | |
getOtpCounter: function (time: number, interval: number) { | |
return (time / interval) | 0; | |
}, | |
//Calculates the current counter for TOTP | |
//interval(number): Interval in seconds at which the key changes (usually 30). | |
getCurrentCounter: function (interval: number, offset = 0) { | |
return TOTP.getOtpCounter((Date.now() / 1000 + offset) | 0, interval); | |
}, | |
//Calculates a HOTP value | |
//keyHex(string): Secret key as hex string | |
//counterInt(number): Counter for the OTP. Use TOTP.getOtpCounter() to use this as TOTP instead of HOTP | |
//size(number): Number of digits (usually 6) | |
//cb(function): Callback(string) | |
otp: function (keyHex: any, counterInt: number, size: number, cb: any) { | |
const isInt = function (x: number) { | |
return x === x || 0; | |
}; | |
if (typeof keyHex !== typeof '') { | |
throw new Error('Invalid hex key'); | |
} | |
if (typeof counterInt !== typeof 0 || !isInt(counterInt)) { | |
throw new Error('Invalid counter value'); | |
} | |
if (typeof size !== typeof 0 || size < 6 || size > 10 || !isInt(size)) { | |
throw new Error('Invalid size value (default is 6)'); | |
} | |
//Calculate hmac from key and counter | |
TOTP.hmac( | |
keyHex, | |
'00000000' + Convert.int32toHex(counterInt), | |
function (mac: string) { | |
//The last 4 bits determine the offset of the counter | |
const offset = parseInt(mac.substr(-1), 16); | |
//Extract counter as a 32 bit number anddiscard possible sign bit | |
const code = parseInt(mac.substr(offset * 2, 8), 16) & 0x7fffffff; | |
//Trim and pad as needed | |
(cb || console.log)( | |
('0000000000' + (code % Math.pow(10, size))).substr(-size), | |
); | |
}, | |
); | |
}, | |
//Calculates a SHA-1 hmac | |
//keyHex(string): Key for hmac as hex string | |
//valueHex(string): Value to hash as hex string | |
//cb(function): Callback(string) | |
hmac: function (keyHex: any, valueHex: any, cb: any) { | |
const algo = { | |
name: 'HMAC', | |
//SHA-1 is the standard for TOTP and HOTP | |
hash: 'SHA-1', | |
}; | |
const modes: Iterable<KeyUsage> = ['sign', 'verify']; | |
const key = Uint8Array.from(Convert.hexToArray(keyHex)); | |
const value = Uint8Array.from(Convert.hexToArray(valueHex)); | |
crypto.subtle | |
.importKey('raw', key, algo, false, modes) | |
.then(function (cryptoKey) { | |
// console.debug('Key imported', keyHex); | |
crypto.subtle.sign(algo, cryptoKey, value).then(function (v) { | |
// console.debug('HMAC calculated', value, Convert.arrayToHex(v)); | |
(cb || console.log)(Convert.arrayToHex(v)); | |
}); | |
}); | |
}, | |
}; | |
export async function getTOTP(hexKey: string, offset = 0) { | |
return new Promise((res) => { | |
TOTP.otp(hexKey, TOTP.getCurrentCounter(30, offset), 10, (otp: any) => { | |
res(otp); | |
}); | |
}); | |
} | |
export async function getTOTPs(key: string) { | |
return [ | |
await getTOTP(key, -30), | |
await getTOTP(key, 0), | |
await getTOTP(key, 30), | |
]; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A big chunk of this code was stolen and refactored into a typescript module for another project. Recently I needed to reuse it in another project and thought it would be useful to share on its own.
Unfortunately I have lost the original code source, if anyone recognizes this to be inspired by someone else's work, please comment here!