zeroclaw-labs/zeroclaw · error
OTP secret is empty
Error message
OTP secret is empty
What it means
OtpConfig::from_config decodes the base32 secret via decode_base32_secret, which strips whitespace, '-', and trailing '=' padding, then rejects the result if nothing remains (otp.rs:225-235). The configured secret therefore consists only of padding/separators or is empty.
Source
Thrown at crates/zeroclaw-runtime/src/security/otp.rs:234
fn decode_base32_secret(raw: &str) -> Result<Vec<u8>> {
fn decode_char(ch: char) -> Option<u8> {
match ch {
'A'..='Z' => Some((ch as u8) - b'A'),
'2'..='7' => Some((ch as u8) - b'2' + 26),
_ => None,
}
}
let mut cleaned = raw
.chars()
.filter(|ch| !matches!(ch, ' ' | '\t' | '\n' | '\r' | '-'))
.collect::<String>()
.to_ascii_uppercase();
while cleaned.ends_with('=') {
cleaned.pop();
}
if cleaned.is_empty() {
anyhow::bail!("OTP secret is empty");
}
let mut output = Vec::new();
let mut buffer = 0u32;
let mut bits_left = 0u8;
for ch in cleaned.chars() {
let value = decode_char(ch)
.with_context(|| format!("OTP secret contains invalid base32 character '{ch}'"))?;
buffer = (buffer << 5) | u32::from(value);
bits_left += 5;
if bits_left >= 8 {
let byte = ((buffer >> (bits_left - 8)) & 0xff) as u8;
output.push(byte);
bits_left -= 8;
}
}View on GitHub (pinned to 88bb9c8533)
Solutions
- Set a real base32 secret (generate one with your authenticator or openssl rand -base32) in the OTP config
- Check how the secret is injected — unset env vars often render as empty strings
- Add a startup assertion that the secret is non-empty when OTP is enabled
Example fix
# before otp_secret = "" # after (32 chars = 160 bits, RFC 4226 minimum) otp_secret = "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP"
Defensive patterns
Strategy: validation
Validate before calling
fn otp_secret_ok(raw: &str) -> bool {
let cleaned: String = raw
.chars()
.filter(|c| !matches!(c, ' ' | '\t' | '\n' | '\r' | '-'))
.collect::<String>()
.trim_end_matches('=')
.to_ascii_uppercase();
!cleaned.is_empty() && cleaned.len() >= 32
} Try / catch
If from_config fails with 'OTP secret is empty', refuse startup with a clear config-error message naming the missing key — an empty secret must never fall back to a default.
Prevention
- Fail config load, not first use, when the OTP secret is empty
- Generate secrets with a tool (openssl rand -base32) and store them verbatim
- Never ship placeholder secrets in templates
When it happens
Trigger: OtpConfig secret is "", "====", or only spaces/tabs — typically an unset env var interpolated into config, or a placeholder value shipped in a config template.
Common situations: OTP enabled in an environment where the secret env var was never set; .env file not loaded in the deployment; secret field emptied during a config edit.
Related errors
- OTP secret did not decode to any bytes
- matrix: configure either `access_token` or `password`
- Nextcloud Talk: no bot secret configured (set bot_token or w
- model_provider `{family}` has multiple configured aliases; u
- security.otp.cache_valid_secs must be greater than or equal
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/6591461b07bb3c5c.
Report an issue: GitHub.