KeygraphHQ/shannon · error · Error

TOTP secret is empty after cleaning

Error message

TOTP secret is empty after cleaning

What it means

Thrown by base32Decode in generate-totp when the input, after uppercasing and stripping non-[A-Z2-7] characters, is empty - meaning the supplied --secret contained no valid base32 characters at all. In the CLI path this is effectively shadowed by the upstream base32-regex check (which emits 'Secret must be base32-encoded' first), so it mainly surfaces when base32Decode is called directly or in tests. The CLI wraps it as a JSON error with retryable:false and exit code 1.

Source

Thrown at apps/worker/src/scripts/generate-totp.ts:28

 * generate-totp CLI
 *
 * Generates a TOTP code for the target's MFA.
 * Based on RFC 6238 (TOTP) and RFC 4226 (HOTP).
 *
 * Usage:
 *   generate-totp --secret JBSWY3DPEHPK3PXP
 */

import { createHmac } from 'node:crypto';

// === Base32 Decoding ===

function base32Decode(encoded: string): Buffer {
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
  const cleanInput = encoded.toUpperCase().replace(/[^A-Z2-7]/g, '');

  if (cleanInput.length === 0) {
    throw new Error('TOTP secret is empty after cleaning');
  }

  const output: number[] = [];
  let bits = 0;
  let value = 0;

  for (const char of cleanInput) {
    const index = alphabet.indexOf(char);
    if (index === -1) {
      throw new Error(`Invalid base32 character: ${char}`);
    }

    value = (value << 5) | index;
    bits += 5;

    if (bits >= 8) {
      output.push((value >>> (bits - 8)) & 255);
      bits -= 8;

View on GitHub (pinned to 1ae0a142f8)

Solutions

  1. Supply a non-empty base32 secret (A-Z, 2-7) via --secret.
  2. If scripting around generate-totp, validate with /^[A-Z2-7]+$/i before invoking.
  3. Restore the upstream base32 regex check if you have forked the CLI.

Example fix

# before
generate-totp --secret "===="
# after
generate-totp --secret "JBSWY3DPEHPK3PXP"
Defensive patterns

Strategy: validation

Validate before calling

function hasBase32Content(s: string): boolean {
  return /[A-Z2-7]/i.test(s);
}

Try / catch

try {
  const code = generateTOTP(secret);
} catch (e) {
  if (e instanceof Error && /empty after cleaning/.test(e.message)) {
    // prompt for a valid --secret
  } else throw e;
}

Prevention

When it happens

Trigger: generate-totp --secret is passed a string that, after removing non-base32 chars, leaves zero characters - e.g. a string of only spaces, padding, or non-letter symbols. In normal CLI flow the regex gate at line 135-145 catches this first with a different message; this throw fires only if that gate is bypassed.

Common situations: Calling base32Decode directly in a test with '' or '===='; an upstream code change that removes the regex pre-check; passing a secret whose only characters are base32 padding ('=') and whitespace.

Related errors


AI-assisted analysis of KeygraphHQ/shannon@1ae0a142f8 (2026-08-12). Data as JSON: /api/errors/052920b6303d9374. Report an issue: GitHub.