Microsoft-Rewards-Script icon indicating copy to clipboard operation
Microsoft-Rewards-Script copied to clipboard

Support for TOTP-based authentication

Open ppc32el opened this issue 3 months ago • 2 comments

Can you add support for a totp property for the account data in accounts.json, so that we may put in TOTP secret and the script will be able to generate TOTP codes accordingly

ppc32el avatar Sep 16 '25 12:09 ppc32el

import crypto from "crypto";

/**
 * Decode Base32 (RFC 4648) to a Buffer.
 * Accepts lowercase/uppercase, optional padding.
 */
function base32Decode(input: string): Buffer {
  const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  const clean = input.toUpperCase().replace(/=+$/g, "").replace(/[^A-Z2-7]/g, "");
  let bits = 0;
  let value = 0;
  const bytes: number[] = [];

  for (const char of clean) {
    value = (value << 5) | alphabet.indexOf(char);
    bits += 5;
    if (bits >= 8) {
      bits -= 8;
      bytes.push((value >>> bits) & 0xff);
    }
  }
  return Buffer.from(bytes);
}

/**
 * Generate an HMAC using Node's crypto and return Buffer.
 */
function hmac(algorithm: string, key: Buffer, data: Buffer): Buffer {
  return crypto.createHmac(algorithm, key).update(data).digest();
}

/**
 * Generate TOTP per RFC 6238.
 * @param secretBase32 - shared secret in Base32
 * @param time - Unix time in seconds (defaults to now)
 * @param options - { digits, step, algorithm }
 * @returns numeric TOTP as string (zero-padded)
 */
export function generateTOTP(
  secretBase32: string,
  time: number = Math.floor(Date.now() / 1000),
  options?: { digits?: number; step?: number; algorithm?: "SHA1" | "SHA256" | "SHA512" }
): string {
  const digits = options?.digits ?? 6;
  const step = options?.step ?? 30;
  const alg = (options?.algorithm ?? "SHA1").toUpperCase();

  const key = base32Decode(secretBase32);
  const counter = Math.floor(time / step);

  // 8-byte big-endian counter
  const counterBuffer = Buffer.alloc(8);
  counterBuffer.writeBigUInt64BE(BigInt(counter), 0);

  let hmacAlg: string;
  if (alg === "SHA1") hmacAlg = "sha1";
  else if (alg === "SHA256") hmacAlg = "sha256";
  else if (alg === "SHA512") hmacAlg = "sha512";
  else throw new Error("Unsupported algorithm. Use SHA1, SHA256 or SHA512.");

  const hash = hmac(hmacAlg, key, counterBuffer);

  // Dynamic truncation
  const offset = hash[hash.length - 1] & 0x0f;
  const code =
    ((hash[offset] & 0x7f) << 24) |
    ((hash[offset + 1] & 0xff) << 16) |
    ((hash[offset + 2] & 0xff) << 8) |
    (hash[offset + 3] & 0xff);

  const otp = (code % 10 ** digits).toString().padStart(digits, "0");
  return otp;
}

Functional snippet, one may integrate this and he wont have to update it for a long time.

ppc32el avatar Sep 16 '25 13:09 ppc32el

Add https://github.com/TheNetsky/Microsoft-Rewards-Script/pull/361 & https://github.com/LightZirconite/Microsoft-Rewards-Script-Private/tree/V2

LightZirconite avatar Sep 20 '25 21:09 LightZirconite