zoobzio December 26, 2025 Edit this page

Masking

Masking partially obscures sensitive data while preserving format and some identifying information. Applied on the send boundary for outgoing data.

Built-in Mask Types

Eight content-aware maskers are built in:

TypeConstantExample InputExample Output
ssnMaskSSN123-45-6789***-**-6789
emailMaskEmailalice@example.coma***@example.com
phoneMaskPhone(555) 123-4567(***) ***-4567
cardMaskCard4111111111111111************1111
ipMaskIP192.168.1.100192.168.xxx.xxx
uuidMaskUUID550e8400-e29b-41d4-a716-446655440000550e8400-****-****-****-************
ibanMaskIBANGB82WEST12345698765432GB82**************5432
nameMaskNameJohn SmithJ*** S****

Usage

Maskers are built in and require no registration:

type Customer struct {
    Email string `json:"email" send.mask:"email"`
    SSN   string `json:"ssn" send.mask:"ssn"`
    Phone string `json:"phone" send.mask:"phone"`
    Card  string `json:"card" send.mask:"card"`
}

func (c Customer) Clone() Customer { return c }

proc, _ := cereal.NewProcessor[Customer](json.New())

customer := &Customer{
    Email: "alice@example.com",
    SSN:   "123-45-6789",
    Phone: "(555) 123-4567",
    Card:  "4111111111111111",
}

// Masking only applies on Send
data, _ := proc.Send(ctx, customer)
// {"email":"a***@example.com","ssn":"***-**-6789","phone":"(***) ***-4567","card":"************1111"}

// Store doesn't mask
stored, _ := proc.Store(ctx, customer)
// {"email":"alice@example.com","ssn":"123-45-6789",...} (original values)

Redaction

For complete replacement instead of partial masking, use send.redact:

type Note struct {
    Content string `json:"content" send.redact:"[REDACTED]"`
    Secret  string `json:"secret" send.redact:"***"`
    Token   string `json:"token" send.redact:""`  // Empty string
}

The entire value is replaced with the tag value.

Masking vs Redaction

AspectMaskingRedaction
Preserves formatYesNo
Shows partial dataYesNo
Human readableYesYes
Useful for debuggingMoreLess
Tagsend.masksend.redact

Choose masking when format or partial identification helps. Choose redaction when no information should leak.

Boundary Specificity

Masking and redaction only apply on the send boundary:

type User struct {
    Email string `send.mask:"email"`  // Only masked when sending
}

// Receive: email unchanged
user, _ := proc.Receive(ctx, data)
// user.Email = "alice@example.com"

// Store: email unchanged
stored, _ := proc.Store(ctx, user)
// stored contains "alice@example.com"

// Load: email unchanged
loaded, _ := proc.Load(ctx, stored)
// loaded.Email = "alice@example.com"

// Send: email masked
response, _ := proc.Send(ctx, loaded)
// response contains "a***@example.com"

This lets you store full values but sanitize output.

Validation Rules

Maskers validate input format and return errors for invalid data:

TypeValidationError When
ssnExactly 9 digitsNot 9 digits after extraction
emailContains @ with local partNo @ or @ at position 0
phoneAt least 7 digitsFewer than 7 digits
card13-19 digitsOutside valid card range
ipValid IPv4 or IPv6Invalid format
uuid8-4-4-4-12 segment lengthsWrong segment count or lengths
iban15-34 chars, starts with 2 lettersInvalid length or prefix
nameNon-empty after trimEmpty or whitespace only
user := &Customer{Email: "not-an-email"}
_, err := proc.Send(ctx, user)
// err: "mask field Email: mask failed: invalid email format, missing or misplaced @"

if errors.Is(err, cereal.ErrMask) {
    // Handle validation failure
}

Not Reversible

Masked and redacted values cannot be restored:

user := &User{Email: "alice@example.com"}
data, _ := proc.Send(ctx, user)

// If you unmarshal this data, you get the masked value
var result User
json.Unmarshal(data, &result)
// result.Email == "a***@example.com" (masked, not original)

For reversible transformations, use store.encrypt + load.decrypt.

Custom Maskers

Implement the Masker interface:

type Masker interface {
    Mask(value string) (string, error)
}

Example custom masker:

type lastFourMasker struct{}

func (m *lastFourMasker) Mask(value string) (string, error) {
    if len(value) < 4 {
        return "", fmt.Errorf("%w: value too short", cereal.ErrMask)
    }
    return strings.Repeat("*", len(value)-4) + value[len(value)-4:], nil
}

proc.SetMasker(cereal.MaskCard, &lastFourMasker{})

IPv6 Support

The IP masker handles both IPv4 and IPv6:

// IPv4
send.mask:"ip" on "192.168.1.100"  -> "192.168.xxx.xxx"

// IPv6
send.mask:"ip" on "2001:db8::1"    -> "2001:db8:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx"

Combining Masks with Encryption

Common pattern: encrypt for storage, mask for external output:

type User struct {
    // Encrypted in database, masked in API responses
    Email string `store.encrypt:"aes" load.decrypt:"aes" send.mask:"email"`

    // Encrypted in database, redacted in API responses
    SSN string `store.encrypt:"aes" load.decrypt:"aes" send.redact:"[HIDDEN]"`
}

This provides defense in depth: data is protected at rest and sanitized in transit.