sipeed/picoclaw · error

credential: enc:// payload too short

Error message

credential: enc:// payload too short

What it means

Returned by resolveEncrypted when the decoded blob is shorter than saltLen+nonceLen+1 = 16+12+1 = 29 bytes. The payload layout is salt(16) | nonce(12) | ciphertext(>=1), so anything under 29 bytes cannot even be structurally split, let alone decrypted. This fires after base64 decoding succeeded, i.e. the value is well-formed base64 but far too short.

Source

Thrown at pkg/credential/credential.go:171

	return raw, nil
}

// resolveEncrypted decrypts an enc:// credential using PassphraseProvider.
func resolveEncrypted(raw string) (string, error) {
	passphrase := PassphraseProvider()
	if passphrase == "" {
		return "", ErrPassphraseRequired
	}

	sshKeyPath := pickSSHKeyPath("") // override="": consult env then auto-detect

	b64 := strings.TrimPrefix(raw, EncScheme)
	blob, err := base64.StdEncoding.DecodeString(b64)
	if err != nil {
		return "", fmt.Errorf("credential: enc:// invalid base64: %w", err)
	}
	if len(blob) < saltLen+nonceLen+1 {
		return "", fmt.Errorf("credential: enc:// payload too short")
	}

	salt := blob[:saltLen]
	nonce := blob[saltLen : saltLen+nonceLen]
	ciphertext := blob[saltLen+nonceLen:]

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

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Regenerate the value with the provided Encrypt/encrypt CLI — it always produces the full salt|nonce|ciphertext blob
  2. Never hand-base64 a plaintext and prefix enc://; the scheme is AES-GCM with a fixed header layout, not an encoding
  3. If a template truncated the value, store enc:// credentials in YAML block scalars or quoted strings that don't elide

Example fix

# before (hand-encoded plaintext, too short)
api_key: enc://c2stMTIz

# after (produced by credential.Encrypt)
api_key: enc://<salt|nonce|ciphertext std-base64 from Encrypt>
Defensive patterns

Strategy: validation

Validate before calling

// Structural pre-check: decoded payload must be at least salt(16)+nonce(12)+1.
func encPayloadLongEnough(raw string) error {
	if !strings.HasPrefix(raw, "enc://") { return nil }
	blob, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(raw, "enc://"))
	if err != nil { return err }
	if len(blob) < 29 { // saltLen(16)+nonceLen(12)+1
		return fmt.Errorf("enc:// payload too short (%d bytes)", len(blob))
	}
	return nil
}

Type guard

func isWellFormedEncCredential(raw string) bool {
	if !strings.HasPrefix(raw, "enc://") { return true }
	blob, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(raw, "enc://"))
	return err == nil && len(blob) >= 29
}

Try / catch

if _, err := resolver.Resolve(raw); err != nil {
	if strings.Contains(err.Error(), "payload too short") {
		// hand-crafted value: replace with output of Encrypt, do not attempt repair
	}
	return err
}

Prevention

When it happens

Trigger: An enc:// value whose base64 decodes to fewer than 29 bytes — e.g. someone base64-encoded a plaintext key directly (16-byte AES key -> 16 bytes), a truncated blob that still happens to be valid base64, or a placeholder/test string like enc://QUJD.

Common situations: Users hand-crafting enc:// values by base64-encoding the raw secret instead of running the encryptor; test fixtures with dummy enc:// strings; values mangled by config templating that drops long strings.

Related errors


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