{"record":{"id":"1e1a048e1e01a08c","repo":"sipeed/picoclaw","slug":"credential-enc-payload-too-short","errorCode":null,"errorMessage":"credential: enc:// payload too short","messagePattern":"credential: enc:// payload too short","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/credential/credential.go","lineNumber":171,"sourceCode":"\treturn raw, nil\n}\n\n// resolveEncrypted decrypts an enc:// credential using PassphraseProvider.\nfunc resolveEncrypted(raw string) (string, error) {\n\tpassphrase := PassphraseProvider()\n\tif passphrase == \"\" {\n\t\treturn \"\", ErrPassphraseRequired\n\t}\n\n\tsshKeyPath := pickSSHKeyPath(\"\") // override=\"\": consult env then auto-detect\n\n\tb64 := strings.TrimPrefix(raw, EncScheme)\n\tblob, err := base64.StdEncoding.DecodeString(b64)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"credential: enc:// invalid base64: %w\", err)\n\t}\n\tif len(blob) < saltLen+nonceLen+1 {\n\t\treturn \"\", fmt.Errorf(\"credential: enc:// payload too short\")\n\t}\n\n\tsalt := blob[:saltLen]\n\tnonce := blob[saltLen : saltLen+nonceLen]\n\tciphertext := blob[saltLen+nonceLen:]\n\n\tkey, err := deriveKey(passphrase, sshKeyPath, salt)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"credential: enc:// cipher init: %w\", err)\n\t}\n\tgcm, err := cipher.NewGCM(block)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"credential: enc:// gcm init: %w\", err)\n\t}","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/credential/credential.go#L153-L189","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Regenerate the value with the provided Encrypt/encrypt CLI — it always produces the full salt|nonce|ciphertext blob","Never hand-base64 a plaintext and prefix enc://; the scheme is AES-GCM with a fixed header layout, not an encoding","If a template truncated the value, store enc:// credentials in YAML block scalars or quoted strings that don't elide"],"exampleFix":"# before (hand-encoded plaintext, too short)\napi_key: enc://c2stMTIz\n\n# after (produced by credential.Encrypt)\napi_key: enc://<salt|nonce|ciphertext std-base64 from Encrypt>","handlingStrategy":"validation","validationCode":"// Structural pre-check: decoded payload must be at least salt(16)+nonce(12)+1.\nfunc encPayloadLongEnough(raw string) error {\n\tif !strings.HasPrefix(raw, \"enc://\") { return nil }\n\tblob, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(raw, \"enc://\"))\n\tif err != nil { return err }\n\tif len(blob) < 29 { // saltLen(16)+nonceLen(12)+1\n\t\treturn fmt.Errorf(\"enc:// payload too short (%d bytes)\", len(blob))\n\t}\n\treturn nil\n}","typeGuard":"func isWellFormedEncCredential(raw string) bool {\n\tif !strings.HasPrefix(raw, \"enc://\") { return true }\n\tblob, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(raw, \"enc://\"))\n\treturn err == nil && len(blob) >= 29\n}","tryCatchPattern":"if _, err := resolver.Resolve(raw); err != nil {\n\tif strings.Contains(err.Error(), \"payload too short\") {\n\t\t// hand-crafted value: replace with output of Encrypt, do not attempt repair\n\t}\n\treturn err\n}","preventionTips":["Never hand-base64 plaintext into enc://; the format is salt|nonce|AES-GCM ciphertext","Generate fixtures with Encrypt in tests, not with short dummy strings","Reject enc:// values below ~40 base64 chars in config linting"],"tags":["go","credentials","encryption","validation","format"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}