transform

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package transform provides redaction, masking, and tokenization transformations that implement the contracts defined by the root incognigo package.

Redaction either removes a complete match or substitutes fixed replacement bytes. Masking retains explicitly configured prefix and suffix bytes and reveals the original byte length.

HMACTokenizer produces stable, irreversible HMAC-SHA-256 tokens and exposes equality within one key and PII type. ReversibleTokenizer produces randomized AES-256-GCM tokens and supports explicit active and decryption-only keys for rotation. Neither mode performs hidden storage or network access.

No transformation in this package is described as anonymization.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidMaskOptions identifies options rejected by NewMasker.
	ErrInvalidMaskOptions = errors.New("transform: invalid mask options")

	// ErrMaskInputTooShort identifies a value that cannot retain the requested
	// prefix and suffix while masking at least one byte.
	ErrMaskInputTooShort = errors.New("transform: value too short to mask")
)
View Source
var (
	// ErrInvalidTokenOptions identifies options rejected by a tokenizer
	// constructor, including missing, malformed, or duplicate keys.
	ErrInvalidTokenOptions = errors.New("transform: invalid token options")

	// ErrInvalidToken identifies a token whose format, algorithm, encoding, or
	// authentication is invalid for the requested operation.
	ErrInvalidToken = errors.New("transform: invalid token")

	// ErrTokenKeyUnavailable identifies a well-formed reversible token whose
	// key ID is not present in the configured decryption key ring.
	ErrTokenKeyUnavailable = errors.New("transform: token key unavailable")

	// ErrTokenValueTooLarge identifies input or decoded plaintext above the
	// token transformer's configured byte limit.
	ErrTokenValueTooLarge = errors.New("transform: token value too large")

	// ErrInvalidTokenPIIType identifies an invalid PII type passed directly to
	// a token transformer or detokenizer.
	ErrInvalidTokenPIIType = errors.New("transform: invalid token PII type")
)

Functions

This section is empty.

Types

type HMACTokenizer

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

HMACTokenizer replaces a value with a keyed, deterministic HMAC-SHA-256 token.

Tokens have the form:

icg.v1.hmac-sha256.<key-id>.<base64url-hmac>

The full 256-bit HMAC is encoded without padding. The complete header, PII type, input length, and exact input bytes are domain-separated inside the MAC. The PII type and input are not present in plaintext in the token.

HMAC tokens are irreversible without exhaustive guessing, but possession of the key permits offline guesses. Stable tokens reveal equality and frequency for identical PII type and value bytes under one key ID and key. Rotating either deliberately changes that equality domain and all newly produced tokens. A full-width HMAC makes accidental collision computationally negligible but cannot make collisions mathematically impossible.

A HMACTokenizer is immutable and safe for concurrent use. It retains a private copy of its key for its lifetime; Go does not guarantee memory zeroization. Tokenization is not idempotent and is not anonymization.

func NewHMACTokenizer

func NewHMACTokenizer(options HMACTokenizerOptions) (*HMACTokenizer, error)

NewHMACTokenizer validates options and returns an immutable tokenizer.

The constructor performs no network, storage, or secret-manager access. On failure it returns ErrInvalidTokenOptions without including an ID or key.

Example
package main

import (
	"context"
	"fmt"

	"github.com/Chadi00/incognigo/transform"
)

func main() {
	// Load 32 unpredictable bytes from a secret manager in production. This
	// sequence is fixed only to make the example output reproducible.
	key := make([]byte, 32)
	for index := range key {
		key[index] = byte(index)
	}
	tokenizer, err := transform.NewHMACTokenizer(
		transform.HMACTokenizerOptions{
			Key: transform.TokenKey{
				ID:  "2026-01",
				Key: key,
			},
		},
	)
	if err != nil {
		panic(err)
	}

	token, err := tokenizer.Transform(
		context.Background(),
		"email_address",
		[]byte("person@example.test"),
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(token))
}
Output:
icg.v1.hmac-sha256.2026-01.3rDSpGYbv2Mt9LkfD0SiBefyvnDQN47gLdInd8STLyk

func (*HMACTokenizer) Transform

func (tokenizer *HMACTokenizer) Transform(
	ctx context.Context,
	piiType incognigo.PIIType,
	value []byte,
) ([]byte, error)

Transform implements incognigo.Transformer.

Transform never mutates, retains, aliases, or returns value. A canceled context returns its fixed context error and no token.

type HMACTokenizerOptions

type HMACTokenizerOptions struct {
	// Key is the active HMAC-SHA-256 key.
	Key TokenKey

	// MaxValueBytes bounds one direct Transform call. Zero selects 8 MiB;
	// negative values are invalid. Engine calls remain subject to the
	// engine's stricter effective limits as well.
	MaxValueBytes int
}

HMACTokenizerOptions configures deterministic, irreversible tokenization.

type MaskOptions

type MaskOptions struct {
	// PrefixBytes is the number of leading bytes retained.
	PrefixBytes int

	// SuffixBytes is the number of trailing bytes retained.
	SuffixBytes int

	// MaskByte replaces each non-retained source byte. Any byte, including
	// zero and malformed UTF-8, is valid.
	MaskByte byte

	// ShortInput determines behavior when the requested retained regions
	// would leave no source byte to mask.
	ShortInput ShortInputBehavior
}

MaskOptions configures a Masker. PrefixBytes and SuffixBytes count bytes, not Unicode code points. They must be non-negative. ShortInput must be set explicitly to a valid ShortInputBehavior.

type Masker

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

Masker preserves configured leading and trailing bytes and replaces every byte between them with one configured byte.

Masking is deterministic and irreversible without an external copy of the input. It always reveals the input byte length, the configured prefix and suffix, and which byte positions were hidden. It never expands output: successful output has exactly the same byte length as input.

Masking is idempotent when applied directly. End-to-end engine idempotence also depends on whether configured detectors match the masked output.

A Masker must be constructed with NewMasker. Its fields are immutable after construction, and one Masker is safe for concurrent use.

func NewMasker

func NewMasker(options MaskOptions) (*Masker, error)

NewMasker validates options and returns an immutable Masker.

On failure NewMasker returns a nil Masker and ErrInvalidMaskOptions. The error contains no option values.

Example
package main

import (
	"context"
	"fmt"

	"github.com/Chadi00/incognigo/transform"
)

func main() {
	masker, err := transform.NewMasker(transform.MaskOptions{
		PrefixBytes: 2,
		SuffixBytes: 4,
		MaskByte:    '*',
		ShortInput:  transform.ShortInputMaskAll,
	})
	if err != nil {
		panic(err)
	}
	protected, err := masker.Transform(
		context.Background(),
		"example.payment_card",
		[]byte("1234567812345678"),
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(protected))
}
Output:
12**********5678

func (*Masker) Transform

func (masker *Masker) Transform(
	_ context.Context,
	_ incognigo.PIIType,
	value []byte,
) ([]byte, error)

Transform implements incognigo.Transformer.

Transform treats value as bytes and never mutates, retains, aliases, or returns it. If a short value is configured to fail, Transform returns a nil result with ErrMaskInputTooShort rather than returning the original value.

type Redactor

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

Redactor removes a matched value or replaces it with fixed bytes.

Construct a Redactor with Remove or Replace. Its output is deterministic, irreversible without an external copy of the input, and does not retain information from the matched value. A replacement can still reveal that a value was present and, when replacements differ by policy, its PII type.

Redaction is idempotent when applied directly: redacting the replacement produces the same replacement. End-to-end engine idempotence also depends on whether configured detectors match the replacement.

The zero value removes matched values and is equivalent to Remove.

func Remove

func Remove() Redactor

Remove returns a Redactor that removes the entire matched value.

Removal produces an empty replacement, never returns the matched value, and performs no allocation in Transform.

Example
package main

import (
	"context"
	"fmt"

	"github.com/Chadi00/incognigo/transform"
)

func main() {
	redactor := transform.Remove()
	protected, err := redactor.Transform(
		context.Background(),
		"example.email_address",
		[]byte("person@example.test"),
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%q\n", protected)
}
Output:
""

func Replace

func Replace(replacement []byte) Redactor

Replace returns a Redactor that substitutes replacement for every matched value.

Replace copies replacement before returning. The caller may mutate or reuse replacement without changing the Redactor. Replacement length is fixed by configuration, so output may shrink, remain the same size, or expand.

Example
package main

import (
	"context"
	"fmt"

	"github.com/Chadi00/incognigo/transform"
)

func main() {
	redactor := transform.Replace([]byte("[REDACTED]"))
	protected, err := redactor.Transform(
		context.Background(),
		"example.email_address",
		[]byte("person@example.test"),
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(protected))
}
Output:
[REDACTED]

func (Redactor) Transform

func (redactor Redactor) Transform(
	context.Context,
	incognigo.PIIType,
	[]byte,
) ([]byte, error)

Transform implements incognigo.Transformer.

The PII type and matched value do not affect the result. Every non-empty result is newly allocated and owned by the caller.

type ReversibleTokenizer

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

ReversibleTokenizer replaces values with randomized, authenticated AES-256-GCM tokens and can recover their original bytes.

Tokens have the form:

icg.v1.aes-256-gcm.<key-id>.<base64url-nonce-ciphertext-tag>

Tokens bind the complete header and expected PII type as authenticated associated data. Substituting a version, algorithm, key ID, or PII type therefore fails authentication. The 96-bit random nonce and 128-bit tag are generated and encoded by Go's GCM implementation.

Randomized tokens do not intentionally reveal equality, although their encoded length reveals the exact input byte length once the public format overhead is removed. A key must not encrypt more than 2^32 values across all tokenizer instances, as required by crypto/cipher's random-nonce GCM contract. Callers own that aggregate accounting and key rotation boundary. Key compromise makes every token using that key reversible.

A ReversibleTokenizer is immutable and safe for concurrent use. It retains expanded key schedules for its lifetime; Go does not guarantee memory zeroization. Tokenization is not idempotent and is not anonymization.

func NewReversibleTokenizer

func NewReversibleTokenizer(
	options ReversibleTokenizerOptions,
) (*ReversibleTokenizer, error)

NewReversibleTokenizer validates and compiles an AES-256-GCM key ring.

The constructor performs no network, storage, or secret-manager access. The active key encrypts new values; all configured keys can decrypt matching old tokens. On failure it returns ErrInvalidTokenOptions without including an ID or key.

Example
package main

import (
	"bytes"
	"context"
	"fmt"
	"strings"

	"github.com/Chadi00/incognigo/transform"
)

func main() {
	// Load 32 unpredictable bytes from a secret manager in production. This
	// sequence is fixed only for the executable example.
	key := make([]byte, 32)
	for index := range key {
		key[index] = byte(index)
	}
	tokenizer, err := transform.NewReversibleTokenizer(
		transform.ReversibleTokenizerOptions{
			ActiveKey: transform.TokenKey{
				ID:  "2026-01",
				Key: key,
			},
		},
	)
	if err != nil {
		panic(err)
	}

	first, err := tokenizer.Transform(
		context.Background(),
		"email_address",
		[]byte("person@example.test"),
	)
	if err != nil {
		panic(err)
	}
	second, err := tokenizer.Transform(
		context.Background(),
		"email_address",
		[]byte("person@example.test"),
	)
	if err != nil {
		panic(err)
	}

	fmt.Println(strings.HasPrefix(
		string(first),
		"icg.v1.aes-256-gcm.2026-01.",
	))
	fmt.Println(!bytes.Equal(first, second))
}
Output:
true
true

func (*ReversibleTokenizer) Detokenize

func (tokenizer *ReversibleTokenizer) Detokenize(
	ctx context.Context,
	piiType incognigo.PIIType,
	token []byte,
) ([]byte, error)

Detokenize authenticates token for piiType and returns newly owned original bytes.

Tokens from the active key and configured decryption-only keys are accepted. Missing keys return ErrTokenKeyUnavailable. Malformed, tampered, wrongly typed, or unauthenticated tokens return ErrInvalidToken with no partial plaintext. No error contains token or recovered data.

Example
package main

import (
	"context"
	"fmt"

	"github.com/Chadi00/incognigo/transform"
)

func main() {
	// Load 32 unpredictable bytes from a secret manager in production. This
	// sequence is fixed only for the executable example.
	key := make([]byte, 32)
	for index := range key {
		key[index] = byte(index)
	}
	tokenizer, err := transform.NewReversibleTokenizer(
		transform.ReversibleTokenizerOptions{
			ActiveKey: transform.TokenKey{
				ID:  "2026-01",
				Key: key,
			},
		},
	)
	if err != nil {
		panic(err)
	}

	token, err := tokenizer.Transform(
		context.Background(),
		"email_address",
		[]byte("person@example.test"),
	)
	if err != nil {
		panic(err)
	}
	original, err := tokenizer.Detokenize(
		context.Background(),
		"email_address",
		token,
	)
	if err != nil {
		panic(err)
	}
	fmt.Println(string(original))
}
Output:
person@example.test

func (*ReversibleTokenizer) Transform

func (tokenizer *ReversibleTokenizer) Transform(
	ctx context.Context,
	piiType incognigo.PIIType,
	value []byte,
) ([]byte, error)

Transform implements incognigo.Transformer.

Transform never mutates, retains, aliases, or returns value. Each successful call uses a fresh random nonce and normally returns a different token even for equal input. A canceled context returns its fixed context error and no token.

type ReversibleTokenizerOptions

type ReversibleTokenizerOptions struct {
	// ActiveKey encrypts all new tokens and is also available for
	// detokenization.
	ActiveKey TokenKey

	// DecryptionKeys contains inactive keys retained only to detokenize old
	// tokens after rotation. IDs must be unique and must not repeat ActiveKey.
	// The constructor copies the slice metadata and compiles every key.
	DecryptionKeys []TokenKey

	// MaxValueBytes bounds plaintext accepted by Transform or returned by
	// Detokenize. Zero selects 8 MiB; negative values and values above the
	// AES-GCM or encoded-size limit are invalid. Engine calls remain subject to
	// the engine's stricter effective limits as well.
	MaxValueBytes int
}

ReversibleTokenizerOptions configures randomized, reversible tokenization and its explicit key ring.

type ShortInputBehavior

type ShortInputBehavior uint8

ShortInputBehavior controls masking when a value is too short to retain the requested prefix and suffix while replacing at least one byte.

const (
	// ShortInputMaskAll replaces every byte of a short value. This is the
	// privacy-preserving behavior for values of unexpected length.
	ShortInputMaskAll ShortInputBehavior = iota + 1

	// ShortInputError returns ErrMaskInputTooShort and no output.
	ShortInputError
)

func (ShortInputBehavior) Valid

func (behavior ShortInputBehavior) Valid() bool

Valid reports whether behavior is recognized.

type TokenKey

type TokenKey struct {
	// ID is embedded in produced tokens and therefore is not secret.
	ID TokenKeyID

	// Key is secret AES-256 or HMAC-SHA-256 key material, depending on the
	// tokenizer. It must contain exactly 32 bytes.
	Key []byte
}

TokenKey associates a public rotation identifier with 256-bit key material. Key must contain exactly 32 unpredictable bytes loaded from a caller-owned secret-management boundary; it is not a password.

Tokenizer constructors copy or compile Key before returning. Later caller mutation cannot change a constructed tokenizer.

type TokenKeyID

type TokenKeyID string

TokenKeyID is public, non-sensitive metadata identifying token key material. It is embedded in tokens so callers can rotate keys explicitly.

func (TokenKeyID) Valid

func (id TokenKeyID) Valid() bool

Valid reports whether id is safe to embed as one token-format component.

IDs contain at most 64 lowercase ASCII bytes. They start and end with an ASCII letter or digit and may contain single '-' or '_' separators between alphanumeric runs. A period is excluded because it delimits token components.

Jump to

Keyboard shortcuts

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