Skip to content

Instantly share code, notes, and snippets.

@zachlankton
Created January 3, 2023 03:36
Show Gist options
  • Save zachlankton/bf0ec610d044e793a1a9cccef6170e97 to your computer and use it in GitHub Desktop.
Save zachlankton/bf0ec610d044e793a1a9cccef6170e97 to your computer and use it in GitHub Desktop.
A simple TOTP File / Mini Library
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),
];
}
@zachlankton
Copy link
Author

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!

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