|
|
@@ -1,10 +1,13 @@
|
|
|
|
|
|
+import base32Decode from 'base32-decode';
|
|
|
import crypto from 'crypto';
|
|
|
|
|
|
+export type TotpAlgorithm = "SHA-1" | "SHA-256" | "SHA-512";
|
|
|
+
|
|
|
export interface ToTpGeneratorOptions {
|
|
|
digits?: number;
|
|
|
period?: number;
|
|
|
- algorithm?: "SHA-1"|"SHA-256"|"SHA-512"
|
|
|
+ algorithm?: TotpAlgorithm;
|
|
|
label?: string;
|
|
|
secretLength?: number;
|
|
|
issuer: string;
|
|
|
@@ -18,29 +21,65 @@ export interface ToTpSecretAndUrl {
|
|
|
const RFC_4648 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
|
|
|
|
export class TotpChecker {
|
|
|
- public static async ValidateTotp(totpSecret: string|null, code: string|undefined): Promise<boolean> {
|
|
|
+ public static async ValidateTotp(totpSecret: string | null, code: string | undefined, period: number, digits?: number, algorithm?: TotpAlgorithm): Promise<boolean> {
|
|
|
if (!totpSecret && !code)
|
|
|
return true;
|
|
|
if ((!totpSecret && code) || (totpSecret && !code))
|
|
|
return false
|
|
|
- return true;
|
|
|
+ let currentPeriod = TotpChecker.getCurrentPeriod(period);
|
|
|
+ const input = Buffer.from(code!.replace(/[^0-9]/g, "").trim());
|
|
|
+ return TotpChecker.DoGenerateCode(totpSecret!, [ currentPeriod -1, currentPeriod, currentPeriod +1 ], digits || 6, algorithm || "SHA-1")
|
|
|
+ .find(x => {
|
|
|
+ try {
|
|
|
+ return crypto.timingSafeEqual(Buffer.from(x), input);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }) !== undefined;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static DoGenerateCode(totpSecret: string, period: number|number[], digits: number, algorithm: TotpAlgorithm): string[] {
|
|
|
+ const secretAsBase64: Buffer = Buffer.from(base32Decode(totpSecret, "RFC4648"));
|
|
|
+
|
|
|
+ return (Array.isArray(period) ? period : [ period ]).map(i => {
|
|
|
+ // Encode period as a Buffer
|
|
|
+ var periodAsBuffer = Buffer.alloc(8)
|
|
|
+ periodAsBuffer.write((i.toString(16)).padStart(16, '0'), 0, 'hex');
|
|
|
+ return periodAsBuffer;
|
|
|
+ }).map(period => {
|
|
|
+ // Encode period using algorithm and secret
|
|
|
+ return crypto.createHmac(algorithm, secretAsBase64)
|
|
|
+ .update(period)
|
|
|
+ .digest();
|
|
|
+ }).map(hash => {
|
|
|
+ // Truncate output hash
|
|
|
+ let offset = hash[hash.length - 1] & 0xF;
|
|
|
+ var truncatedHash = ((hash[offset + 0] & 0x7F) << 24 |
|
|
|
+ (hash[offset + 1] & 0xFF) << 16 |
|
|
|
+ (hash[offset + 2] & 0xFF) << 8 |
|
|
|
+ (hash[offset + 3] & 0xFF)) % (10 ** digits);
|
|
|
+ return (`${truncatedHash}`.padStart(digits, '0'));
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- public static EncodeBase32(input: Buffer) {
|
|
|
- let secret = [];
|
|
|
- for (let i of input)
|
|
|
- secret.push(RFC_4648[i % RFC_4648.length]);
|
|
|
- return secret.join("");
|
|
|
+ private static getCurrentPeriod(period?: number) {
|
|
|
+ return Math.floor(Date.now() / ((period || 30) * 1000));
|
|
|
}
|
|
|
|
|
|
- public static GenerateCode(optionsOrIssuer: ToTpGeneratorOptions|string): ToTpSecretAndUrl {
|
|
|
- let options: ToTpGeneratorOptions = typeof optionsOrIssuer === "string" ? {issuer: optionsOrIssuer} : optionsOrIssuer;
|
|
|
+ public static GenerateCode(totpSecret: string, period?: number, digits?: number, algorithm?: TotpAlgorithm): string {
|
|
|
+ return TotpChecker.DoGenerateCode(totpSecret, this.getCurrentPeriod(period), digits || 6, algorithm || "SHA-1").shift()!;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static GenerateUrl(optionsOrIssuer: ToTpGeneratorOptions | string): ToTpSecretAndUrl {
|
|
|
+ let options: ToTpGeneratorOptions = typeof optionsOrIssuer === "string" ? { issuer: optionsOrIssuer } : optionsOrIssuer;
|
|
|
options.digits = options.digits || 6;
|
|
|
options.period = options.period || 30;
|
|
|
options.algorithm = options.algorithm || "SHA-1";
|
|
|
options.label = encodeURIComponent(options.label || options.issuer);
|
|
|
options.secretLength = options.secretLength || 13;
|
|
|
- const secretStr = TotpChecker.EncodeBase32(crypto.randomBytes(options.secretLength));
|
|
|
+
|
|
|
+ const secretStr = Array.from(crypto.randomBytes(options.secretLength)).map(x => RFC_4648[x % RFC_4648.length]).join("");
|
|
|
return {
|
|
|
url: `otpauth://totp/${options.issuer}?issuer=${options.issuer}&secret=${secretStr}&digits=${options.digits}&period=${options.period}&algorithm=${options.algorithm}`,
|
|
|
secret: secretStr
|