JuliusBrussee/caveman · error

envelope: ciphertext too short

Error message

envelope: ciphertext too short

What it means

The ciphertext blob passed to envelope decryption is shorter than the GCM nonce size (12 bytes for AES-GCM), so there is not even a nonce present. The library rejects it before attempting authenticated decryption. This almost always means the input is truncated, empty, corrupted, or not actually an envelope ciphertext.

Solutions

  1. Check the length of the ciphertext before calling Open — it must be at least 12 bytes (nonce) plus 16 bytes (tag) plus plaintext
  2. Re-fetch or restore the original encrypted blob; the stored copy is corrupted or truncated
  3. Verify the write path that persisted the ciphertext completed without truncation (column size, streaming cutoffs)
  4. Confirm you are passing the ciphertext, not plaintext or an encoded ID, into Open/OpenForScope

Example fix

// before
plaintext, err := envelope.Open(ctx, keyRef, blobFromCache, nil)
// after
if len(blob) < 12+16 {
    return fmt.Errorf("stored blob truncated: %d bytes", len(blob))
}
plaintext, err := envelope.Open(ctx, keyRef, blob, nil)
Defensive patterns

Strategy: validation

Validate before calling

if len(ciphertext) < 12+16 {
    return fmt.Errorf("ciphertext too short (%d bytes): blob is truncated or not envelope data", len(ciphertext))
}

Type guard

func isPlausibleCiphertext(b []byte) bool { return len(b) >= 12+16 }

Try / catch

pt, err := envelope.Open(ctx, keyRef, ct, aad)
if err != nil {
    if strings.Contains(err.Error(), "ciphertext too short") {
        // treat as corrupted/unrecoverable record; re-seal from source or surface 404
    }
    return err
}

Prevention

When it happens

Trigger: Calling envelope.Open or envelope.OpenForScope with a ciphertext byte slice of length less than the GCM nonce size (12) — e.g. an empty slice, a truncated stored blob, or plaintext handed in by mistake.

Common situations: A database row or object-store blob was truncated by a bad write; a column sized too small silently cut the value; decoding a base64 blob produced empty/partial bytes; passing a hash or ID string instead of the ciphertext.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/e26a4922140e22ac. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/envelope/envelope.go:158

	wrapped, err := base64.StdEncoding.DecodeString(meta.WrappedDataKey)
	if err != nil {
		return nil, fmt.Errorf("envelope: decode wrapped key: %w", err)
	}
	dataKey, err := secretbox.DecryptPayloadKey(wrapped)
	if err != nil {
		return nil, fmt.Errorf("envelope: unwrap data key: %w", err)
	}
	block, err := aes.NewCipher(dataKey)
	if err != nil {
		return nil, fmt.Errorf("envelope: aes: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("envelope: gcm: %w", err)
	}
	ns := gcm.NonceSize()
	if len(ciphertext) < ns {
		return nil, fmt.Errorf("envelope: ciphertext too short")
	}
	nonce, ct := ciphertext[:ns], ciphertext[ns:]
	plaintext, err := gcm.Open(nil, nonce, ct, aad)
	if err != nil {
		return nil, fmt.Errorf("envelope: open: %w", err)
	}
	return plaintext, nil
}

func scopeAAD(scope Scope) ([]byte, string, error) {
	scope.OrganizationID = strings.TrimSpace(scope.OrganizationID)
	scope.ProjectID = strings.TrimSpace(scope.ProjectID)
	scope.Kind = strings.TrimSpace(scope.Kind)
	if scope.OrganizationID == "" {
		return nil, "", fmt.Errorf("envelope: organization scope is required")
	}
	if scope.Kind == "" {
		return nil, "", fmt.Errorf("envelope: object kind is required")

View on GitHub (pinned to 3ee70a1026)