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

  1. If tests override rand.Reader, restore the original (t.Cleanup) so production paths see the real CSPRNG
  2. In containers/VMs, verify `ls -l /dev/urandom` and `head -c 16 /dev/urandom | xxd` works as the service user
  3. Retry the encrypt operation once — a transient read failure is the only recoverable flavor; persistent failure means the environment must be fixed
  4. 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

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


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