Help us shape the future of Go! Take the 2026 Go Developer Survey

pii

package
v0.8.1 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package pii is the product-neutral half of personal-data masking: value detectors (email, phone, IBAN, card, national ids), mask modes, the multilingual name heuristics that propose mask rules, and the store of the clear values behind redacted references. Which value is personal (column provenance, JSON paths, headers) is the product's.

Index

Constants

View Source
const (
	// ModeRedact replaces the whole value: "<redacted>" (the default).
	ModeRedact = "redact"
	// ModePartial keeps the first character and the length: "j***(12)".
	ModePartial = "partial"
	// ModeEmail keeps the first character and the domain of an address:
	// "j***@example.com" (other values are masked as partial).
	ModeEmail = "email"
)

Mask modes.

View Source
const (
	MaxRefResults = 50
	MaxRefBytes   = 16 << 20
)

Bounds of the reference store: the oldest results are forgotten first.

View Source
const ClockSkew = time.Hour

ClockSkew is how far in the future an epoch's creation time may be before the store treats it as wrong (a clock that was ahead).

View Source
const CutKind = "cut"

CutKind is the kind of the match that covers the end of a text past DetectLimit (or past where a view reached its cap), masked whole.

View Source
const CutMark = "…"

CutMark ends a text value that was cut.

View Source
const DetectLimit = 64 << 10

DetectLimit is how much of a text value the detectors scan. A longer value is cut there, and ends with CutMark, before detection: nothing past the scanned part can reach the output. Renderers cut values much earlier, so the cut is normally invisible; it bounds the time a huge value can cost.

View Source
const EpochLen = 8

EpochLen is the length of an epoch id: 8 characters of Crockford base32, 40 random bits.

View Source
const KeyLen = 32

KeyLen is the length of an epoch key.

View Source
const PseudonymLen = 12

PseudonymLen is the length of a pseudonym ID: 12 characters of Crockford base32, 60 bits.

View Source
const Redacted = "<redacted>"

Redacted is a value masked in mode redact.

Variables

View Source
var (
	// ErrNoEpoch: the store holds no epoch and was not asked to create one.
	ErrNoEpoch = errors.New("pii: no pseudonym epoch key")
	// ErrClosed: Rotate or Prune on a closed store (it no longer holds the
	// directory's lock).
	ErrClosed = errors.New("pii: epoch store closed")
	// ErrUnknownEpoch: the epoch was never kept here, or was pruned.
	ErrUnknownEpoch = errors.New("pii: unknown or deleted pseudonym epoch")
)

Errors of the epoch store.

View Source
var ErrForgottenEpoch = errors.New("pii: pseudonym epoch key deleted")

ErrForgottenEpoch is the error of ID on an epoch whose key was deleted (EpochStore.Prune): the caller must redact the value, never show it.

View Source
var ErrInvalidKind = errors.New("pii: invalid pseudonym kind")

ErrInvalidKind is the error of ID on a kind that is not 1 to 32 of [a-z0-9_].

Functions

func CellText

func CellText(v any) string

CellText is the text of a value, as compared and substituted.

func CutAt

func CutAt(s string, n int) string

CutAt cuts s, which must be longer than n bytes, to at most n bytes followed by CutMark: at a rune boundary, then back over up to cutTail value-like characters (valueRune), so that a value the cut splits (a partial e-mail address or number, which no detector would recognise) is dropped rather than shown in part.

func Detect

func Detect(v any, ds []Detector) any

Detect runs the detectors over a text or integer value (a national number or a card can be stored as one), or an integral float (the same number in a double column), and returns the masked text, or v itself, with its type, when nothing was found. Other values pass.

func Detects

func Detects(s string, ds []Detector) bool

Detects reports whether a detector would mask something in s.

func LooksPersonal

func LooksPersonal(name string) bool

LooksPersonal reports a field name that looks like personal data.

func LooksPersonalColumn

func LooksPersonalColumn(name, typ string) bool

LooksPersonalColumn is LooksPersonal for a database column of type typ: a network address type is personal whatever its name, and a binary column whose name suggests a document is too.

func LooksQuasi

func LooksQuasi(name string) bool

LooksQuasi reports a field name that looks like a quasi-identifier (birth date, postal code, gender, coordinates, free-text notes).

func LooksSecret added in v0.8.0

func LooksSecret(name string) bool

LooksSecret reports a field, label, header or flag name that holds a credential: a password, a token, an API or private key, a cookie, an authorization. Its value is a secret whatever it looks like. Metadata about a credential ("token_type", "password_changed_at") does not count, nor do token counts ("max_tokens", "prompt_tokens"), nor the shell's working directory ("PWD").

func Mask

func Mask(mode, value string) string

Mask masks the text value in mode.

func MaskText

func MaskText(s string) string

MaskText is the partial mask format: first character + "***" + "(length in characters)". An invalid UTF-8 byte counts as one U+FFFD character.

func MaskValue

func MaskValue(v any, mode string) any

MaskValue masks one non-nil value in mode. Bytes become "<masked bytes:N>" in modes partial and email; an unknown mode masks as partial.

func Normalise added in v0.8.0

func Normalise(kind, value string) string

Normalise returns the form of value (one detected value: digits of two phone numbers would be joined) whose pseudonym is taken, so that one value written differently is one pseudonym: the detection view of the value, or of its decoding (percent, JSON escape, HTML reference) when only that is a whole value of kind; then, by kind:

  • email: lower case;
  • phone, phone_loose: digits only, with "+" for an international number ("00" becomes "+": E.164 when the country code is written);
  • iban: blanks removed, upper case;
  • card, be_niss, nl_bsn, us_ssn: digits only; fr_nir: digits and the Corsican letter, upper case;
  • ip: the canonical address (an IPv4-mapped IPv6 address is the IPv4 one; zones dropped);
  • person_name: lower case, blanks collapsed to one space;
  • other kinds: the detection view with surrounding blanks trimmed.

func RedactedRef

func RedactedRef(name string) string

RedactedRef is a redacted value that carries a reference to it, which the console alone resolves (see RefStore).

func Tokens

func Tokens(name string) []string

Tokens splits a name on non-alphanumerics and camelCase, lowered, with accents removed from common Latin letters ("prénom" → "prenom").

func ValidID added in v0.8.0

func ValidID(s string) bool

ValidID reports whether s has the form of a pseudonym ID.

func ValidKind added in v0.8.0

func ValidKind(kind string) bool

ValidKind reports whether kind may name a pseudonym's kind: 1 to 32 characters of [a-z0-9_].

func ValidMode

func ValidMode(m string) bool

ValidMode reports whether m is a mask mode.

Types

type Detector

type Detector interface {
	Name() string
	// Mask returns s with every value the detector recognises replaced by
	// its masked form (first character + "***" + "(length)").
	Mask(s string) string
}

Detector masks personal values found inside a text, in place.

func AllDetectors added in v0.3.0

func AllDetectors() []Detector

AllDetectors is every detector but the opt-in ones (phone_loose, ip, secret), in their run order: what masking falls back to when it was given none.

func Detectors

func Detectors(names []string) ([]Detector, error)

Detectors returns the named detectors, de-duplicated, in their fixed run order. Names: email, phone, iban, card, be_niss, fr_nir, nl_bsn, us_ssn, and the opt-in phone_loose, ip (public IPv4 and IPv6 addresses) and secret (credentials, tokens and private keys, masked as Redacted).

type EpochStore added in v0.8.0

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

EpochStore keeps pseudonym epochs in a directory of their own: one file per epoch ("<epoch>.key", 0600, written once, atomically), the directory 0700, both owned by the process's user. The current epoch is the newest; an epoch is replaced when a newer one is created, and its key is deleted by Prune once it has been replaced for long enough (references minted under it can be re-read until then). A corrupt or exposed key file refuses the store: nothing is repaired silently. Epochs are ordered by a sequence number, not by the clock. The store locks its directory (one process at a time; Close releases it; opening creates the .lock file even when create is false) and is safe for concurrent use within that process. On non-unix platforms there are no permission, owner, symbolic-link or lock checks: keep one process per store there.

func OpenEpochStore added in v0.8.0

func OpenEpochStore(dir string, create bool, now time.Time) (*EpochStore, error)

OpenEpochStore opens the epochs kept in dir. When dir holds none, it creates the directory (0700) and a first epoch if create is set, and fails with ErrNoEpoch otherwise.

func (*EpochStore) Close added in v0.8.0

func (s *EpochStore) Close() error

Close releases the directory's lock and forgets every epoch's key in memory (another process may now prune them): ID then fails with ErrForgottenEpoch, Rotate and Prune with ErrClosed.

func (*EpochStore) Current added in v0.8.0

func (s *EpochStore) Current() *Pseudonymizer

Current returns the current epoch.

func (*EpochStore) Epochs added in v0.8.0

func (s *EpochStore) Epochs() []string

Epochs returns the ids of the kept epochs, oldest first.

func (*EpochStore) Get added in v0.8.0

func (s *EpochStore) Get(epoch string) (*Pseudonymizer, error)

Get returns a kept epoch, current or replaced but not yet pruned.

func (*EpochStore) GoString added in v0.8.0

func (s *EpochStore) GoString() string

GoString shows the directory and the epochs, never a key.

func (*EpochStore) MarshalJSON added in v0.8.0

func (s *EpochStore) MarshalJSON() ([]byte, error)

MarshalJSON writes the epochs, never a key.

func (*EpochStore) Prune added in v0.8.0

func (s *EpochStore) Prune(now time.Time, keep time.Duration) ([]string, error)

Prune deletes the keys of the epochs replaced at least keep ago (an epoch is replaced when the next one is created) and returns their ids. The current epoch is never deleted.

func (*EpochStore) Rotate added in v0.8.0

func (s *EpochStore) Rotate(now time.Time) (*Pseudonymizer, error)

Rotate creates a new epoch, which becomes current. The previous one is kept until Prune deletes it.

func (*EpochStore) RotationDue added in v0.8.0

func (s *EpochStore) RotationDue(now time.Time, every time.Duration) bool

RotationDue reports whether the current epoch began at least every ago, or claims to begin more than ClockSkew in the future (a clock that was ahead must not keep an epoch alive).

func (*EpochStore) String added in v0.8.0

func (s *EpochStore) String() string

String shows the directory and the epochs, never a key.

type Match added in v0.8.0

type Match struct {
	Start, End int
	Kind       string
}

Match is a value a detector recognised: the byte range [Start, End) of the text it was found in, and the name of the detector (its kind: "email", "iban", ...).

func Find added in v0.8.0

func Find(s string, ds []Detector) []Match

Find returns the values that Detect(s, ds) masks, as byte ranges of s: sorted, non-overlapping, each with the kind of the detector that found it. The detectors run in the order given (Detectors returns their fixed run order), each on the text the previous ones masked, exactly as Detect runs them, so replacing every match with MaskText of its text gives Detect's output. Two exceptions, where Find masks more: a later detector whose match overlaps an earlier mask makes one match of both (Detect would mask the mask), and a Detector that is not one of this package's cannot say where it found a value, so anything it masks makes the whole text one match of its kind.

Past DetectLimit, the rest of s is one more match, of kind CutKind, starting earlier where a value runs across the limit (the detectors scan a little past it to find such values whole): the matches are always safe to apply to s itself.

func FindInViews added in v0.8.0

func FindInViews(s string, ds []Detector) []Match

FindInViews returns the values found in s and in each of its detection views (DetectionViews), as byte ranges of s: sorted, non-overlapping. A value found only in a view (behind an escape, a look-alike or a zero-width character) is the range of s it was written with, whole. Where matches of different kinds overlap, they make one match, of the kind of the earliest detector in ds (an encoded value next to a plain one may so take a neighbour's kind). Long texts are cut as by Find. Match boundaries are not moved to escape boundaries: a raw match may start inside an escape (the "n" of "\\n" before an address).

type Pseudonymizer added in v0.8.0

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

Pseudonymizer gives each personal value a stable, keyed pseudonym for the duration of one epoch: the same value, however it is spelt (Normalise), gets the same ID; without the key, an ID says nothing about the value beyond what guessing a value and asking for its pseudonym would. The key never leaves the value: there is no accessor, and the fmt and JSON forms show the epoch only. It is safe for concurrent use.

func NewEpoch added in v0.8.0

func NewEpoch(now time.Time) (*Pseudonymizer, error)

NewEpoch returns a Pseudonymizer for a new epoch, with a random id and key, kept in memory only (a session epoch). EpochStore keeps them on disk.

func NewPseudonymizer added in v0.8.0

func NewPseudonymizer(epoch string, created time.Time, key []byte) (*Pseudonymizer, error)

NewPseudonymizer returns the Pseudonymizer of an existing epoch: its id (EpochLen characters of Crockford base32) and its KeyLen-byte key, which it copies.

func (*Pseudonymizer) Created added in v0.8.0

func (p *Pseudonymizer) Created() time.Time

Created returns when the epoch began (to the second).

func (*Pseudonymizer) Epoch added in v0.8.0

func (p *Pseudonymizer) Epoch() string

Epoch returns the epoch id: random, it says nothing about the key.

func (*Pseudonymizer) Forgotten added in v0.8.0

func (p *Pseudonymizer) Forgotten() bool

Forgotten reports whether the epoch's key was deleted.

func (*Pseudonymizer) GoString added in v0.8.0

func (p *Pseudonymizer) GoString() string

GoString shows the epoch, never the key.

func (*Pseudonymizer) ID added in v0.8.0

func (p *Pseudonymizer) ID(kind, value string) (string, error)

ID returns the pseudonym of value as a value of kind (a detector name: "email", "phone", ...): the first PseudonymLen characters of the Crockford base32 of HMAC-SHA256(key, "lockkit-pseudonym-v1" || 0x00 || kind || 0x00 || Normalise(kind, value)); kind is 1 to 32 of [a-z0-9_] (ErrInvalidKind otherwise). Different kinds give different IDs for the same text. Once the epoch's key was deleted (EpochStore.Prune), ID fails with ErrForgottenEpoch and never computes an ID: the caller redacts the value.

func (*Pseudonymizer) MarshalJSON added in v0.8.0

func (p *Pseudonymizer) MarshalJSON() ([]byte, error)

MarshalJSON writes the epoch, never the key.

func (*Pseudonymizer) String added in v0.8.0

func (p *Pseudonymizer) String() string

String shows the epoch, never the key.

type RefStore

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

RefStore keeps the clear values behind the redacted references of the last results, so that an agent can name a value ('${rN.K}') without seeing it. It lives in the console's memory and dies with it. The zero value is ready; it is not safe for concurrent use.

A reference is "r<result>.<key>", where key is positive integers separated by dots chosen by the product: locksql uses "<row>.<column>", PutDistinct numbers the distinct values of a result. A key never holds the value.

func (*RefStore) Begin

func (r *RefStore) Begin() int

Begin opens the next result and returns its number.

func (*RefStore) Evict

func (r *RefStore) Evict()

Evict forgets the oldest result.

func (*RefStore) Get

func (r *RefStore) Get(ref string) (string, bool)

Get resolves a reference.

func (*RefStore) Put

func (r *RefStore) Put(n int, key, value string) string

Put stores value under key in result n and returns its reference, or "" when the key is malformed, the result unknown or the store cannot hold the value.

func (*RefStore) PutDistinct

func (r *RefStore) PutDistinct(n int, value string) string

PutDistinct stores value in result n under the next number, or returns the reference it already has in that result: the same value gets the same reference across the records of one result.

type View added in v0.8.0

type View struct {
	Text string
	// contains filtered or unexported fields
}

View is a text derived from a source text (normalised, decoded) with the map back to it: Source gives the range of the source each range of Text was written with. The zero map means Text is the source itself.

func DetectionView added in v0.8.0

func DetectionView(s string) View

DetectionView is s as the detectors should see it: the characters fold knows mapped to ASCII (the spaces digits are grouped with, full-width digits), NFKC-normalised (full-width letters, ligatures, other compatibility look-alikes), and without default-ignorable characters (Unicode Cf: zero-width spaces and joiners, bidi controls, the soft hyphen; variation selectors, the combining grapheme joiner, Hangul fillers). An invalid UTF-8 byte is kept as is. Texts longer than DetectLimit are cut as by Find.

func DetectionViews added in v0.8.0

func DetectionViews(s string) []View

DetectionViews returns the detection view of s, the views without its terminal sequences, then the detection view of each decoding of s that changes it: percent-decoded once and twice, JSON string escapes, HTML character references, all of them chained (until nothing changes, at most 16 rounds, cleaned up between rounds), \\xHH, \\e and \\033 escapes, and form encoding ("+" as a space) then percent. Texts are distinct. Texts longer than DetectLimit are cut as by Find.

func (View) Source added in v0.8.0

func (v View) Source(start, end int) (int, int)

Source returns the range of the source text that Text[start:end] was written with. It covers every source character dropped inside the range (a zero-width character between two digits) and every escape it decodes whole ("%40", "&#64;", "\u0040"). Out-of-range arguments are clamped.

Jump to

Keyboard shortcuts

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