zeroclaw-labs/zeroclaw · critical

Key file must contain exactly 32 bytes (got {})

Error message

Key file must contain exactly 32 bytes (got {})

What it means

load_or_create_key validates that an existing master key file contains exactly 32 bytes, the ChaCha20 key size used by the secrets module. Wrong-length files are rejected rather than padded or truncated, to avoid silently deriving a weak or garbage key. Note the reported length tells you how big the file actually is.

Source

Thrown at crates/zeroclaw-config/src/secrets.rs:574

/// Opening with `open_no_follow` binds the "not a symlink" check to the same
/// object we read from — there is no check-then-follow window.
fn read_key_file_no_follow(key_path: &Path) -> std::io::Result<String> {
    use std::io::Read; // function-scoped — avoids redundant module import
    let mut file = open_no_follow(key_path)?;
    let mut buf = String::new();
    file.read_to_string(&mut buf)?;
    Ok(buf)
}

/// Load the key from `key_path`, creating it if absent.
///
/// Reads go through a no-follow / reparse-point-verified handle.  Creation
/// uses atomic no-replace publication (write-to-temp then `hard_link` on Unix
/// / `MoveFileExW` on Windows) so concurrent readers never observe empty or
/// partial key material.
fn load_or_create_key(key_path: &Path) -> Result<Vec<u8>> {
    let validate_key = |bytes: Vec<u8>| {
        anyhow::ensure!(
            bytes.len() == 32,
            "Key file must contain exactly 32 bytes (got {})",
            bytes.len()
        );
        Ok(bytes)
    };

    match read_key_file_no_follow(key_path) {
        Ok(hex) => validate_key(hex_decode(hex.trim()).context("Secret key file is corrupt")?),
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
            let key = generate_random_key();
            match write_key_file_atomic_publish(key_path, &key) {
                Ok(()) => Ok(key),
                Err(write_err) => {
                    // Only recover if another process won the race.
                    // All other failures must propagate so we don't
                    // silently accept a bad key.
                    if !is_already_exists_error(&write_err) {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. If no secrets were encrypted yet, remove the invalid key file and let the loader atomically create a correct 32-byte one
  2. Regenerate a proper key: head -c 32 /dev/urandom > keyfile (chmod 600)
  3. If secrets already exist, restore the original 32-byte key from backup instead of regenerating — a new key makes existing secrets undecryptable

Example fix

# before
printf 'my-password' > ~/.zeroclaw/key   # 11 bytes, rejected
# after
head -c 32 /dev/urandom > ~/.zeroclaw/key && chmod 600 ~/.zeroclaw/key
Defensive patterns

Strategy: validation

Validate before calling

let meta = std::fs::metadata(&key_path)?;
if meta.len() != 32 {
    anyhow::bail!(
        "key file {} is {} bytes; expected 32 — restore the original key or remove it to regenerate (existing secrets would be lost)",
        key_path.display(),
        meta.len()
    );
}
// safe to call Secrets::with_key(...) now

Try / catch

on "Key file must contain exactly 32 bytes" — stop startup; if no secrets exist yet, delete the file and retry once; otherwise restore the correct 32-byte key from backup

Prevention

When it happens

Trigger: Hand-creating a key file containing a passphrase, hex text, or base64 instead of 32 raw bytes; a key file with a trailing newline; truncation or corruption of the key file; pointing the key path at the wrong file; writing a 64-character hex string as text (64 bytes).

Common situations: Operators generating keys with echo secret > keyfile instead of raw random bytes; editors appending newlines; copying keys between machines in a lossy way; leftover files from experiments at the configured key path.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/c0404b531c38f550. Report an issue: GitHub.