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:
| Type | Constant | Example Input | Example Output |
|---|---|---|---|
ssn | MaskSSN | 123-45-6789 | ***-**-6789 |
email | MaskEmail | alice@example.com | a***@example.com |
phone | MaskPhone | (555) 123-4567 | (***) ***-4567 |
card | MaskCard | 4111111111111111 | ************1111 |
ip | MaskIP | 192.168.1.100 | 192.168.xxx.xxx |
uuid | MaskUUID | 550e8400-e29b-41d4-a716-446655440000 | 550e8400-****-****-****-************ |
iban | MaskIBAN | GB82WEST12345698765432 | GB82**************5432 |
name | MaskName | John Smith | J*** 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
| Aspect | Masking | Redaction |
|---|---|---|
| Preserves format | Yes | No |
| Shows partial data | Yes | No |
| Human readable | Yes | Yes |
| Useful for debugging | More | Less |
| Tag | send.mask | send.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:
| Type | Validation | Error When |
|---|---|---|
ssn | Exactly 9 digits | Not 9 digits after extraction |
email | Contains @ with local part | No @ or @ at position 0 |
phone | At least 7 digits | Fewer than 7 digits |
card | 13-19 digits | Outside valid card range |
ip | Valid IPv4 or IPv6 | Invalid format |
uuid | 8-4-4-4-12 segment lengths | Wrong segment count or lengths |
iban | 15-34 chars, starts with 2 letters | Invalid length or prefix |
name | Non-empty after trim | Empty 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.