ory/hydra · error
malformed ciphertext: too short
Error message
malformed ciphertext: too short
What it means
Decrypt base64-url-decodes the ciphertext and requires at least chacha20poly1305.NonceSizeX bytes so a nonce can be split off. A decoded message shorter than the nonce size cannot possibly be valid XChaCha20-Poly1305 output, so Decrypt returns 'malformed ciphertext: too short'.
Source
Thrown at aead/xchacha20.go:63
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(plaintext)+aead.Overhead())
_, err = cryptorand.Read(nonce)
if err != nil {
return "", errors.WithStack(err)
}
ciphertext := aead.Seal(nonce, nonce, plaintext, additionalData)
return base64.URLEncoding.EncodeToString(ciphertext), nil
}
func (x *XChaCha20Poly1305) Decrypt(ctx context.Context, ciphertext string, aad []byte) (plaintext []byte, err error) {
msg, err := base64.URLEncoding.DecodeString(ciphertext)
if err != nil {
return nil, errors.WithStack(err)
}
if len(msg) < chacha20poly1305.NonceSizeX {
return nil, errors.WithStack(fmt.Errorf("malformed ciphertext: too short"))
}
nonce, ciphered := msg[:chacha20poly1305.NonceSizeX], msg[chacha20poly1305.NonceSizeX:]
keys, err := allKeys(ctx, x.d)
if err != nil {
return nil, errors.WithStack(err)
}
var aead cipher.AEAD
for _, key := range keys {
aead, err = chacha20poly1305.NewX(key)
if err != nil {
continue
}
plaintext, err = aead.Open(nil, nonce, ciphered, aad)
if err == nil {
return plaintext, nil
}View on GitHub (pinned to 4174065ffb)
Solutions
- Ensure the value passed to Decrypt is the exact string returned by Encrypt (base64url, untouched).
- Check the storage column is large enough and the value was not truncated (log length before decrypt).
- Re-encrypt the data: decrypt with the original mechanism/key, then Encrypt again with the current key set.
- Guard call sites: only attempt Decrypt on values produced by Encrypt, return a fallback for legacy formats.
Example fix
// before
token := strings.Split(raw, ":")[1] // custom mangling
val, err := x.Decrypt(ctx, token)
// after
val, err := x.Decrypt(ctx, raw) // pass stored ciphertext as-is
if err != nil { return nil, fmt.Errorf("unreadable token: %w", err) } Defensive patterns
Strategy: validation
Validate before calling
// Cheap pre-check before calling Decrypt:
if raw == "" || base64.RawURLEncoding.DecodedLen(len(raw)) < 24 { // XChaCha nonce size
return fmt.Errorf("not a valid encrypted token")
} Type guard
func looksLikeCiphertext(s string) bool {
if s == "" { return false }
msg, err := base64.URLEncoding.DecodeString(s)
return err == nil && len(msg) >= chacha20poly1305.NonceSizeX
} Try / catch
val, err := x.Decrypt(ctx, ct)
if err != nil {
if strings.Contains(err.Error(), "malformed ciphertext") {
return nil, ErrCorruptToken // treat as absent/invalid, do not retry
}
return nil, err
} Prevention
- Only pass values returned by Encrypt to Decrypt.
- Use sufficiently wide DB columns and verify no truncation on write.
- Keep old keys in rotated_keys so data encrypted before rotation stays readable.
- Add a migration path/re-encryption step when changing storage format or algorithm.
When it happens
Trigger: Calling Decrypt with a string that is not output of Encrypt: truncated database value, empty string, hand-crafted or corrupted token, wrong encoding (raw/hex instead of base64url) yielding a too-short decode.
Common situations: Data written by an older/other algorithm in the same column; manual DB edits or migrations truncating TEXT columns; passing a plain secret string instead of an encrypted token; different keys causing misinterpretation (though that usually fails auth, not length).
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- key must be exactly %d bytes long, got %d bytes
- at least one encryption key must be defined but none were
- plaintext too large
- unknown algorithm %s for encryption key
- key must be exactly 32 long bytes, got %d bytes
AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03).
Data as JSON: /api/errors/d546f6a2ada822a3.
Report an issue: GitHub.