sipeed/picoclaw · error
credential: failed to generate salt: %w
Error message
credential: failed to generate salt: %w
What it means
Returned by Encrypt when io.ReadFull fails while filling the 16-byte salt from crypto/rand.Reader. The OS CSPRNG essentially never fails on Linux/macOS, so in practice this surfaces only under abnormal conditions: a custom/mocked rand.Reader in tests, a broken /dev/urandom in a constrained container, or fd exhaustion at OS level. It is wrapped with %w so the OS error is preserved.
Source
Thrown at pkg/credential/credential.go:212
}
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 {
return "", fmt.Errorf("credential: gcm init: %w", err)
}
nonce := make([]byte, nonceLen)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("credential: failed to generate nonce: %w", err)View on GitHub (pinned to 49183d7e8d)
Solutions
- If tests override rand.Reader, restore the original (t.Cleanup) so production paths see the real CSPRNG
- In containers/VMs, verify `ls -l /dev/urandom` and `head -c 16 /dev/urandom | xxd` works as the service user
- Retry the encrypt operation once — a transient read failure is the only recoverable flavor; persistent failure means the environment must be fixed
- If persistent, escalate to the platform level (container spec, kernel entropy settings) rather than working around in app code
Example fix
// before (test leaks a stubbed reader)
rand.Reader = stubReader
callEncrypt() // later production code fails here
// after
t.Cleanup(func() { rand.Reader = origReader }) Defensive patterns
Strategy: retry
Validate before calling
// Verify the CSPRNG is usable in this environment before encrypt workloads.
probe := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, probe); err != nil {
return fmt.Errorf("crypto/rand unavailable: %w", err)
} Try / catch
var enc string
err := retry(2, func() error { // transient OS entropy read failures only
var e error
enc, e = credential.Encrypt(pass, keyPath, plaintext)
return e
})
if err != nil && strings.Contains(err.Error(), "generate salt") {
// environment-level CSPRNG failure — stop and fix the platform, don't loop Prevention
- In tests, always restore crypto/rand.Reader via t.Cleanup after stubbing
- Container images: ensure /dev/urandom exists and is readable by the service user
- Include a tiny crypto/rand probe in startup health checks for hardened/sandboxed deployments
When it happens
Trigger: Code that swaps package-level rand.Reader with a failing stub (unit tests) and then calls Encrypt; exotic sandboxed environments where /dev/urandom is unavailable or reads error; heavy fd/entropy pressure during early boot on minimal VMs.
Common situations: Test suites overriding crypto/rand.Reader without restoring it, leaking into subsequent Encrypt calls; stripped-down containers (no /dev/urandom mount); extremely rare kernel-level issues. In normal deployments this error is a signal that the runtime environment, not the code, is broken.
Related errors
- credential: passphrase must not be empty
- credential: enc:// invalid base64: %w
- credential: enc:// payload too short
- credential: enc:// passphrase required
- start process hook %q: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/8e961192977eb844.
Report an issue: GitHub.