sipeed/picoclaw · error

credential: passphrase must not be empty

Error message

credential: passphrase must not be empty

What it means

Returned by Encrypt when the passphrase argument is empty. Encryption derives its AES key from passphrase + SSH key, so an empty passphrase is refused outright rather than producing values encrypted under a weak/empty secret. Callers typically source the passphrase from PICOCLAW_KEY_PASSPHRASE; note this guard is on Encrypt's own argument, distinct from the ErrPassphraseRequired sentinel used on the decrypt path.

Source

Thrown at pkg/credential/credential.go:206

		return "", fmt.Errorf("credential: enc:// gcm init: %w", err)
	}

	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return "", fmt.Errorf("%w: %w", ErrDecryptionFailed, err)
	}
	return string(plaintext), nil
}

// Encrypt encrypts plaintext and returns an enc:// credential string.
//
// passphrase is required (PICOCLAW_KEY_PASSPHRASE value).
// sshKeyPath is the SSH private key file to use; pass "" to auto-detect via
// PICOCLAW_SSH_KEY_PATH env var or ~/.ssh/picoclaw_ed25519.key.
// An SSH private key must be resolvable or Encrypt returns an error.
func Encrypt(passphrase, sshKeyPath, plaintext string) (string, error) {
	if passphrase == "" {
		return "", fmt.Errorf("credential: passphrase must not be empty")
	}
	sshKeyPath = pickSSHKeyPath(sshKeyPath)

	salt := make([]byte, saltLen)
	if _, err := io.ReadFull(rand.Reader, salt); err != nil {
		return "", fmt.Errorf("credential: failed to generate salt: %w", err)
	}

	key, err := deriveKey(passphrase, sshKeyPath, salt)
	if err != nil {
		return "", err
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return "", fmt.Errorf("credential: cipher init: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Export a real passphrase before encrypting: `export PICOCLAW_KEY_PASSPHRASE=$(...secure source...)`
  2. If a wrapper invokes Encrypt, fail fast with a clear message when the env var is missing rather than passing "" (see validationCode)
  3. For CI, inject the passphrase via the secrets mechanism instead of assuming the environment carries it

Example fix

# before
export PICOCLAW_KEY_PASSPHRASE=   # empty
picoclaw encrypt ...

# after
export PICOCLAW_KEY_PASSPHRASE='correct horse battery staple'
picoclaw encrypt ...
Defensive patterns

Strategy: validation

Validate before calling

pass := os.Getenv("PICOCLAW_KEY_PASSPHRASE")
if pass == "" {
	return errors.New("PICOCLAW_KEY_PASSPHRASE is not set; refusing to encrypt with empty passphrase")
}
encVal, err := credential.Encrypt(pass, "", plaintext)

Try / catch

if _, err := credential.Encrypt(pass, keyPath, plaintext); err != nil {
	if strings.Contains(err.Error(), "passphrase must not be empty") {
		// fetch the passphrase from the proper secret source and retry once
	}
	return err
}

Prevention

When it happens

Trigger: Calling credential.Encrypt("", ...) — concretely, an encrypt CLI/wrapper that reads PICOCLAW_KEY_PASSPHRASE when the env var is unset or empty and passes it through without checking.

Common situations: Running the encrypt command in a fresh shell/CI job where PICOCLAW_KEY_PASSPHRASE was never exported; `.env` file not loaded; empty-string export (`export PICOCLAW_KEY_PASSPHRASE=`) from a templated script; secrets manager lookup returning empty silently.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/e686301ffbc2c39f. Report an issue: GitHub.