Browse Source

totp imple

isundil 1 month ago
parent
commit
b9cf6bd3fa
2 changed files with 51 additions and 11 deletions
  1. 1 0
      package.json
  2. 50 11
      src/totpChecker.ts

+ 1 - 0
package.json

@@ -19,6 +19,7 @@
     "typescript": "^5.9.3"
   },
   "dependencies": {
+    "base32-decode": "^1.0.0",
     "ldapts": "^8.0.9"
   }
 }

+ 50 - 11
src/totpChecker.ts

@@ -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