KeygraphHQ/shannon · error · Error

Invalid base32 character: ${char}

Error message

Invalid base32 character: ${char}

What it means

Thrown by base32Decode in generate-totp when a post-cleaning character is somehow not in the base32 alphabet. Because cleanInput already strips non-[A-Z2-7] chars via regex, this branch is normally unreachable through the CLI; it exists as a defensive guard for direct callers and future alphabet changes. Surfaced to CLI users as a JSON error with retryable:false and exit code 1.

Source

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

// === 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;
    }
  }

  return Buffer.from(output);
}

// === TOTP Generation (RFC 6238) ===

function generateHOTP(secret: string, counter: number, digits: number = 6): string {
  const key = base32Decode(secret);

View on GitHub (pinned to 1ae0a142f8)

Solutions

  1. If scripting, run /^[A-Z2-7]+$/i on the raw secret first.
  2. If forking, keep the alphabet and the cleaning regex in sync.
  3. Use the CLI as-is rather than calling base32Decode directly.

Example fix

// before - direct call with bad input
base32Decode("JBSW Y3DP!@#")  // '!' survives an inconsistent cleaner
// after
base32Decode("JBSWY3DPEHPK3PXP")
Defensive patterns

Strategy: validation

Validate before calling

const BASE32_RE = /^[A-Z2-7]+$/i;
function isStrictBase32(s: string): boolean {
  return BASE32_RE.test(s);
}

Try / catch

try {
  const code = generateTOTP(secret);
} catch (e) {
  if (e instanceof Error && /Invalid base32 character/.test(e.message)) {
    // re-clean input and retry, or surface to caller
  } else throw e;
}

Prevention

When it happens

Trigger: Effectively unreachable via the generate-totp CLI: cleanInput only contains A-Z and 2-7 chars by construction, all of which are in alphabet. Could fire only if base32Decode is called directly with a pre-cleaned string containing other characters, or if the alphabet constant is changed to drop a character.

Common situations: Calling base32Decode in tests with already-cleaned input that violates the alphabet; modifying the alphabet or the cleaning regex inconsistently; future regressions in the cleaning step.

Related errors


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