securecookie

package
v0.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2026 License: MIT Imports: 18 Imported by: 0

README

securecookie

Authenticated cookie values. Two modes:

  • SecureCookie -- AES-GCM authenticated encryption (payload secret + tamper-proof).
  • SignedCookie -- HMAC-SHA256 authenticated signing (payload readable + tamper-proof).

Use SecureCookie for sensitive data (PII, tokens, credentials). Use SignedCookie for already-opaque payloads (JWTs, opaque IDs) where confidentiality is unnecessary; you save the AES key schedule and avoid double-encrypting opaque data.

Features

  • AES-GCM authenticated encryption (AES-128, AES-192, AES-256) -- SecureCookie
  • HMAC-SHA256 authenticated signing -- SignedCookie
  • Optional additional authenticated data (AAD) binding (user ID, cookie name, tenant, etc.)
  • Embedded timestamp with configurable MaxAge, MinAge, and future-timestamp rejection
  • Key rotation via multi-codec encode/decode (both modes)
  • Pluggable serialization (JSON by default)
  • Entropy-adaptive compression (deflate for compressible data, skipped for high-entropy payloads)
  • Cryptographically random key generation
  • Builder-style configuration

Installation

go get github.com/vitalvas/kasper/securecookie

Usage

Basic Encode/Decode (encrypted)
key, _ := securecookie.GenerateKey(32) // 16 for AES-128, 24 for AES-192, 32 for AES-256
sc, _ := securecookie.New(key)

encoded, _ := sc.Encode(map[string]string{"user": "alice"})

var dst map[string]string
_ = sc.Decode(encoded, &dst)
Signed-only Encode/Decode (HMAC, no encryption)

Use SignedCookie when the payload is already opaque (JWTs, server-issued IDs) or non-sensitive. The cookie is tamper-proof but readable.

key, _ := securecookie.GenerateSignedKey(32) // 32 bytes recommended
sc, _ := securecookie.NewSigned(key)

encoded, _ := sc.Encode("eyJhbGciOiJ...") // a JWT
// encoded contains the JWT visible in base64; HMAC-SHA256 prevents tampering.

var dst string
_ = sc.Decode(encoded, &dst)

SignedCookie shares the same fluent API as SecureCookie (MaxAge, MinAge, MaxLength, SetSerializer, AdditionalData) and satisfies the same Codec interface, so it composes with EncodeMulti / DecodeMulti and graceful key rotation via SignedCodecsFromKeys.

Configuration
sc, _ := securecookie.New(key)
sc.MaxAge(3600).     // cookie expires after 1 hour
   MinAge(10).       // reject cookies younger than 10 seconds
   MaxLength(8192)   // max encoded length in bytes

Set to 0 to disable MaxAge, MinAge, or MaxLength checks.

Key Rotation

CodecsFromKeys creates a Codec slice from multiple AES keys. EncodeMulti always uses the first (newest) key. DecodeMulti tries each key in order until one succeeds.

codecs, _ := securecookie.CodecsFromKeys(currentKey, previousKey)

encoded, _ := securecookie.EncodeMulti(value, codecs...)

var dst MySession
_ = securecookie.DecodeMulti(encoded, &dst, codecs...)

Rotation strategy:

  1. Generate a new key and add it at the front of the list
  2. Keep old keys in the list until all cookies issued with them have expired (MaxAge)
  3. Remove expired keys from the list

Keys can have different sizes (e.g., rotating from AES-128 to AES-256):

codecs, _ := securecookie.CodecsFromKeys(newKey256, oldKey128)

Use SignedCodecsFromKeys for the same pattern with signing-only codecs:

codecs, _ := securecookie.SignedCodecsFromKeys(currentKey, previousKey)
Custom Serializer
sc, _ := securecookie.New(key)
sc.SetSerializer(mySerializer{})

Any type implementing the Serializer interface works:

type Serializer interface {
    Serialize(src any) ([]byte, error)
    Deserialize(src []byte, dst any) error
}
Additional Authenticated Data (AAD)

AAD is bound into the GCM authentication tag. By default no AAD is used. Use AdditionalData to bind cookies to context such as a user ID, cookie name, or tenant:

// Bind to user ID
sc.AdditionalData([]byte("user-123"))

// Bind to cookie name
sc.AdditionalData([]byte("session"))

// Clear AAD (default)
sc.AdditionalData(nil)
func setHandler(w http.ResponseWriter, r *http.Request) {
    encoded, _ := sc.Encode(map[string]string{"theme": "dark"})
    http.SetCookie(w, &http.Cookie{
        Name:     "prefs",
        Value:    encoded,
        Path:     "/",
        MaxAge:   3600,
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
    })
}

func getHandler(w http.ResponseWriter, r *http.Request) {
    cookie, _ := r.Cookie("prefs")
    var prefs map[string]string
    _ = sc.Decode(cookie.Value, &prefs)
}

Compression

Payloads are automatically compressed with deflate before encryption when beneficial. A fast Shannon entropy check skips compression entirely for high-entropy data (random tokens, encrypted blobs) to avoid wasted CPU.

Payload JSON Encoded Ratio
2 strings 31 B 91 B 294%
5 strings 102 B 178 B 175%
OIDC session (JWT tokens) 696 B 180 B 26%
50 repeated fields 2,001 B 280 B 14%
Keycloak heavy (10 roles, 5 clients) 5,220 B 1,538 B 29%

Small payloads (< 32 B) and high-entropy data (> 6.5 bits/byte) are stored raw with zero compression overhead. All scenarios above fit within the 4KB browser cookie limit.

Security

Property SecureCookie (AES-GCM) SignedCookie (HMAC-SHA256)
Tamper detection Yes Yes
Confidentiality (payload secret) Yes No -- payload is readable
Constant-time signature compare Yes Yes
AAD binding Yes Yes

Common to both:

  • Future timestamps beyond 5 minutes of clock skew are rejected
  • Decompressed payloads are limited to 512 KB to prevent zip-bomb attacks
  • Generate keys with GenerateKey(32) / GenerateSignedKey(32) and store them securely
  • Always transmit cookies over HTTPS with HttpOnly and Secure flags

Pick SecureCookie whenever the cookie body could include anything sensitive. Reach for SignedCookie only for opaque or non-sensitive payloads.

Documentation

Overview

Package securecookie provides authenticated cookie values in two modes:

  • SecureCookie -- AES-GCM authenticated encryption: payload is secret and tamper-proof. Use for sensitive data (PII, credentials, tokens). Three key sizes: 16 (AES-128), 24 (AES-192), or 32 (AES-256) bytes.

  • SignedCookie -- HMAC-SHA256 authenticated signing: payload is readable but tamper-proof. Use when the payload is already opaque (JWTs, server-issued opaque IDs) or non-sensitive. Saves the AES key schedule and avoids double-encrypting opaque data. Any non-empty key is accepted; 32 bytes is recommended.

Basic Usage

key, _ := securecookie.GenerateKey(32)
sc, _ := securecookie.New(key)

encoded, _ := sc.Encode(map[string]string{"user": "alice"})

var dst map[string]string
_ = sc.Decode(encoded, &dst)

Configuration

Builder-style methods configure cookie validation:

sc, _ := securecookie.New(key)
sc.MaxAge(3600).     // reject cookies older than 1 hour
   MinAge(10).       // reject cookies younger than 10 seconds
   MaxLength(8192)   // max encoded value length in bytes

Signed-Only Mode

NewSigned creates a SignedCookie for HMAC-SHA256 signing without encryption. Same fluent API and same Codec interface as SecureCookie:

key, _ := securecookie.GenerateSignedKey(32)
sc, _ := securecookie.NewSigned(key)
encoded, _ := sc.Encode("eyJhbGciOiJ...") // a JWT
var dst string
_ = sc.Decode(encoded, &dst)

Key Rotation

CodecsFromKeys (encrypted) and SignedCodecsFromKeys (signed) create a Codec slice from multiple keys. EncodeMulti always encodes with the first (newest) key. DecodeMulti tries each key in order until one succeeds, enabling seamless rotation without invalidating existing cookies:

codecs, _ := securecookie.CodecsFromKeys(currentKey, previousKey)
encoded, _ := securecookie.EncodeMulti(value, codecs...)
_ = securecookie.DecodeMulti(encoded, &dst, codecs...)

During rotation, add the new key at the front of the list and keep old keys until all cookies issued with them have expired (MaxAge).

Additional Authenticated Data (AAD)

By default no AAD is used. Use SecureCookie.AdditionalData to bind cookies to context such as a user ID, cookie name, or tenant:

sc.AdditionalData([]byte("user-123"))  // bind to user
sc.AdditionalData([]byte("session"))   // bind to cookie name
sc.AdditionalData(nil)                 // clear AAD (default)

Compression

Payloads are automatically compressed with deflate before encryption when beneficial. A Shannon entropy check skips compression for high-entropy data (> 6.5 bits/byte) and small payloads (< 32 bytes) to avoid wasted CPU. This is transparent and requires no configuration.

Timestamp Validation

Each encoded value embeds a Unix timestamp. On decode, the timestamp is checked against configurable MaxAge and MinAge bounds to enforce cookie freshness. Cookies with future timestamps beyond 5 minutes of clock skew are always rejected.

Serialization

Values are serialized to JSON by default. A custom Serializer can be provided via SecureCookie.SetSerializer:

sc.SetSerializer(mySerializer{})

Security Considerations

  • Always use cryptographically random keys (see GenerateKey).
  • Prefer 32-byte keys (AES-256) for maximum security margin.
  • Always transmit cookies over HTTPS.
  • Bind cookies with HttpOnly, Secure, and SameSite attributes at the HTTP layer; this package handles only the value encoding.
  • Decompressed payloads are limited to 512 KB to prevent zip-bomb attacks.

References:

  • NIST SP 800-38D: Recommendation for Block Cipher Modes of Operation: GCM
  • RFC 6265: HTTP State Management Mechanism

Index

Constants

View Source
const (
	// DefaultMaxAge is the default maximum cookie age: 30 days.
	DefaultMaxAge = 30 * 24 * 60 * 60

	// DefaultMaxLength is the default maximum encoded cookie length in bytes.
	DefaultMaxLength = 4096
)

Variables

View Source
var (
	// ErrInvalidKey is returned when the encryption key is not 16, 24, or 32 bytes.
	ErrInvalidKey = errors.New("securecookie: invalid key (must be 16, 24, or 32 bytes)")

	// ErrEncodeFailed is returned when encoding a cookie value fails.
	ErrEncodeFailed = errors.New("securecookie: encode failed")

	// ErrDecodeFailed is returned when decoding a cookie value fails.
	ErrDecodeFailed = errors.New("securecookie: decode failed")

	// ErrValueTooLong is returned when the encoded value exceeds MaxLength.
	ErrValueTooLong = errors.New("securecookie: encoded value too long")

	// ErrTimestampExpired is returned when the cookie timestamp exceeds MaxAge.
	ErrTimestampExpired = errors.New("securecookie: cookie expired")

	// ErrTimestampTooNew is returned when the cookie timestamp is newer than MinAge allows.
	ErrTimestampTooNew = errors.New("securecookie: cookie too new")

	// ErrTimestampFuture is returned when the cookie timestamp is in the future.
	ErrTimestampFuture = errors.New("securecookie: cookie timestamp is in the future")

	// ErrNoCodecs is returned when an empty codec list is passed to EncodeMulti or DecodeMulti.
	ErrNoCodecs = errors.New("securecookie: no codecs provided")
)

Encoding and decoding errors.

Functions

func DecodeMulti

func DecodeMulti(value string, dst any, codecs ...Codec) error

DecodeMulti tries each codec in order and returns the first successful decode. Returns the last error if all codecs fail.

For SecureCookie codecs (produced by CodecsFromKeys), dst is only written on a fully successful decode because GCM decryption and timestamp validation occur before deserialization. Custom Codec implementations should follow the same pattern: validate before writing to dst.

func EncodeMulti

func EncodeMulti(value any, codecs ...Codec) (string, error)

EncodeMulti encodes a value using the first codec in the list.

func GenerateKey

func GenerateKey(size int) ([]byte, error)

GenerateKey returns a cryptographically random key suitable for use with New. The size must be 16 (AES-128), 24 (AES-192), or 32 (AES-256).

func GenerateSignedKey added in v0.13.0

func GenerateSignedKey(size int) ([]byte, error)

GenerateSignedKey returns a cryptographically random key of the given size suitable for use with NewSigned. 32 bytes is recommended.

Types

type Codec

type Codec interface {
	Encode(value any) (string, error)
	Decode(value string, dst any) error
}

Codec encodes and decodes cookie values.

func CodecsFromKeys

func CodecsFromKeys(keys ...[]byte) ([]Codec, error)

CodecsFromKeys creates a Codec slice from one or more AES keys. Each key must be 16, 24, or 32 bytes. The first key is used for encoding; all keys are tried for decoding.

func SignedCodecsFromKeys added in v0.13.0

func SignedCodecsFromKeys(keys ...[]byte) ([]Codec, error)

SignedCodecsFromKeys returns a slice of Codec backed by SignedCookie instances, one per key, for graceful key rotation. The first key is used for new cookies; all keys are tried for decoding.

type JSONSerializer

type JSONSerializer struct{}

JSONSerializer uses JSON encoding. This is the default serializer.

Note: encoding/json replaces invalid UTF-8 in string fields with the Unicode replacement character (U+FFFD). Use []byte fields for binary data that must round-trip exactly.

func (JSONSerializer) Deserialize

func (JSONSerializer) Deserialize(src []byte, dst any) error

Deserialize unmarshals JSON bytes into dst.

func (JSONSerializer) Serialize

func (JSONSerializer) Serialize(src any) ([]byte, error)

Serialize marshals src to JSON.

type SecureCookie

type SecureCookie struct {
	// contains filtered or unexported fields
}

SecureCookie encodes and decodes authenticated, encrypted cookie values using AES-GCM (AES-128, AES-192, or AES-256 depending on key size).

func New

func New(key []byte) (*SecureCookie, error)

New creates a SecureCookie with the given AES key. The key must be 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256).

func (*SecureCookie) AdditionalData

func (s *SecureCookie) AdditionalData(data []byte) *SecureCookie

AdditionalData sets custom additional authenticated data (AAD) bound into the GCM authentication tag. Use this to bind cookies to context such as a user ID, session ID, or cookie name.

By default no AAD is used. Pass nil to clear.

func (*SecureCookie) Decode

func (s *SecureCookie) Decode(value string, dst any) error

Decode base64-decodes, decrypts, validates the timestamp, and deserializes a cookie value into dst.

func (*SecureCookie) Encode

func (s *SecureCookie) Encode(value any) (string, error)

Encode serializes, encrypts, and base64-encodes a value.

func (*SecureCookie) MaxAge

func (s *SecureCookie) MaxAge(seconds int) *SecureCookie

MaxAge sets the maximum age in seconds. Cookies older than this are rejected during decode. Set to 0 to disable age checking. Negative values are treated as 0.

func (*SecureCookie) MaxLength

func (s *SecureCookie) MaxLength(length int) *SecureCookie

MaxLength sets the maximum encoded cookie value length in bytes. Set to 0 to disable length checking. Negative values are treated as 0.

func (*SecureCookie) MinAge

func (s *SecureCookie) MinAge(seconds int) *SecureCookie

MinAge sets the minimum age in seconds. Cookies newer than this are rejected during decode. Default: 0 (no minimum). Negative values are treated as 0.

func (*SecureCookie) SetSerializer

func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie

SetSerializer sets the serializer used for encoding and decoding values. A nil serializer is ignored. Default: JSONSerializer.

type Serializer

type Serializer interface {
	Serialize(src any) ([]byte, error)
	Deserialize(src []byte, dst any) error
}

Serializer defines how values are converted to and from bytes for storage in a cookie.

type SignedCookie added in v0.13.0

type SignedCookie struct {
	// contains filtered or unexported fields
}

SignedCookie encodes and decodes authenticated (HMAC-SHA256) but unencrypted cookie values. The payload is integrity-protected against tampering but readable to anyone who can read the cookie.

Use SignedCookie when the cookie payload is already opaque or non-sensitive: JWTs, OAuth state, opaque server-issued IDs, or anti-CSRF tokens. Prefer SecureCookie for any data that must remain secret from the client.

SignedCookie avoids the AES key schedule on every request and uses a 32-byte HMAC tag instead of a 12-byte nonce + 16-byte auth tag, which can give smaller cookies for some payloads after compression.

func NewSigned added in v0.13.0

func NewSigned(key []byte) (*SignedCookie, error)

NewSigned creates a SignedCookie with the given HMAC-SHA256 key. Any non-empty key is accepted; 32 bytes is recommended (RFC 2104 §3). Use GenerateSignedKey to produce a fresh key.

func (*SignedCookie) AdditionalData added in v0.13.0

func (s *SignedCookie) AdditionalData(data []byte) *SignedCookie

AdditionalData sets custom additional authenticated data bound into the HMAC. Use this to namespace cookies that share a key (e.g., separate "session" from "csrf" tokens) and to bind cookies to context such as a user ID. Cookies signed with one AAD will not verify under another.

By default no AAD is used. Pass nil to clear.

func (*SignedCookie) Decode added in v0.13.0

func (s *SignedCookie) Decode(value string, dst any) error

Decode base64-decodes, verifies the HMAC, validates the timestamp, and deserializes a cookie value into dst.

func (*SignedCookie) Encode added in v0.13.0

func (s *SignedCookie) Encode(value any) (string, error)

Encode serializes, signs, and base64-encodes a value. The wire format is base64url(timestamp || payload || hmac-sha256(timestamp || payload || aad)).

func (*SignedCookie) MaxAge added in v0.13.0

func (s *SignedCookie) MaxAge(seconds int) *SignedCookie

MaxAge sets the maximum age in seconds. Cookies older than this are rejected during decode. Set to 0 to disable age checking. Negative values are treated as 0.

func (*SignedCookie) MaxLength added in v0.13.0

func (s *SignedCookie) MaxLength(length int) *SignedCookie

MaxLength sets the maximum encoded cookie value length in bytes. Set to 0 to disable length checking. Negative values are treated as 0.

func (*SignedCookie) MinAge added in v0.13.0

func (s *SignedCookie) MinAge(seconds int) *SignedCookie

MinAge sets the minimum age in seconds. Cookies newer than this are rejected during decode. Default: 0 (no minimum). Negative values are treated as 0.

func (*SignedCookie) SetSerializer added in v0.13.0

func (s *SignedCookie) SetSerializer(sz Serializer) *SignedCookie

SetSerializer sets the serializer used for encoding and decoding values. A nil serializer is ignored. Default: JSONSerializer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL