kovidgoyal/kitty · error

invalid TOTP secret: %w

Error message

invalid TOTP secret: %w

What it means

generateTOTP decodes the configured secret as base32 (uppercase, no padding) before computing the HMAC-based one-time password. If decoding fails, the secret is not valid base32 and this error is returned. Called from RunSSHAskpass when a TOTP field is requested.

Source

Thrown at kittens/ssh/askpass.go:65

}

func isOTPPrompt(msg string) bool {
	q := strings.ToLower(msg)
	if strings.Contains(q, "passphrase") {
		return false
	}
	if strings.Contains(q, "verification code") || strings.Contains(q, "one-time password") || strings.Contains(q, "one time password") || strings.Contains(q, "authenticator code") || strings.Contains(q, "authentication code") || strings.Contains(q, "two-factor") || strings.Contains(q, "2fa") || strings.Contains(q, "otp") || strings.Contains(q, "passcode") {
		return true
	}
	return false
}

func generateTOTP(secret string, digits, period int64, t time.Time) (string, error) {
	s := strings.ToUpper(strings.TrimSpace(secret))
	s = strings.ReplaceAll(s, " ", "")
	key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(s)
	if err != nil {
		return "", fmt.Errorf("invalid TOTP secret: %w", err)
	}
	counter := uint64(t.Unix() / period)
	var buf [8]byte
	binary.BigEndian.PutUint64(buf[:], counter)
	mac := hmac.New(sha1.New, key)
	_, _ = mac.Write(buf[:])
	sum := mac.Sum(nil)
	off := sum[len(sum)-1] & 0x0f
	code := (uint32(sum[off])&0x7f)<<24 | (uint32(sum[off+1])&0xff)<<16 | (uint32(sum[off+2])&0xff)<<8 | (uint32(sum[off+3]) & 0xff)
	mod := uint32(1)
	for range digits {
		mod *= 10
	}
	val := code % mod
	fmtstr := fmt.Sprintf("%%0%dd", digits)
	return fmt.Sprintf(fmtstr, val), nil
}

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Provide the secret as standard base32 (A-Z, 2-7), no padding
  2. Strip whitespace and padding; the code already uppercases and removes spaces
  3. Re-copy the seed from the authenticator/QR provisioning URI
  4. Test decode with base32.StdEncoding.WithPadding(base32.NoPadding) locally

Example fix

// before
password: "hello world!"
// after
password: "JBSWY3DPEHPK3PXP"
Defensive patterns

Strategy: validation

Validate before calling

func validBase32(s string) bool {
    s = strings.ToUpper(strings.TrimSpace(strings.ReplaceAll(s, " ", "")))
    _, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(s)
    return err == nil
}

Type guard

func isTOTPSecret(s string) bool { return validBase32(s) }

Prevention

When it happens

Trigger: Configuring an ssh secret with a password field whose value contains characters outside the base32 alphabet (lowercase after normalization, digits like 0/1/8 in some alphabets, punctuation) or wrong padding.

Common situations: Pasting a TOTP seed that includes spaces handled incorrectly, hex-format seeds, or secrets copied with trailing characters/newlines.

Related errors


AI-assisted analysis of kovidgoyal/kitty@6d5d0c4406 (2026-08-27). Data as JSON: /api/errors/c4edaab98c1a6635. Report an issue: GitHub.