Documentation
¶
Overview ¶
Package ipanon implements IP address anonymization and pseudonymization techniques for privacy-preserving logging and analytics.
Anonymization is not one thing ¶
The techniques in this package sit on a spectrum, and the distinction is legally significant. Under the GDPR, data is only "anonymous" (and therefore out of scope, Recital 26) when re-identification is not reasonably possible by any means, for anyone, including the controller. Most of what the industry calls "IP anonymization" does not clear that bar:
- Dropper produces no data at all. Genuinely anonymous.
- Truncator zeroes host bits. A /24 collapses at most 256 addresses, so a truncated value combined with a timestamp and other request attributes can often still single out a person. Treat as pseudonymization.
- Hasher is keyed and one-way, but the IPv4 keyspace is only 2^32: anyone who obtains the key can rebuild the full lookup table offline in minutes. Pseudonymization, and only for as long as the key stays secret.
- Pseudonymizer is a Hasher whose key rotates on a schedule, so tokens cannot be linked across periods. Pseudonymization with a bounded blast radius.
- CryptoPAn is prefix-preserving and mathematically invertible by the key holder. It is pseudonymization by construction and never anonymization.
See docs/COMPLIANCE.md for the full treatment. Nothing in this package is legal advice.
Choosing a technique ¶
If you only need country- or region-level analytics, geolocate first and then drop the address entirely. If you need to recognize a repeat source over time, use Pseudonymizer with a rotation period matched to your retention window. If you need subnet structure preserved for abuse or netflow analysis, use CryptoPAn. Use Truncator when you need a value that is still routable -looking and coarsely geolocatable.
Concurrency ¶
Every anonymizer in this package is safe for concurrent use by multiple goroutines once constructed.
Index ¶
- Constants
- Variables
- func GenerateKey() ([]byte, error)
- func KeyFromEnv(name string) ([]byte, error)
- func LegacySaltedSHA256(salt, ip string) stringdeprecated
- func Normalize(addr netip.Addr) (netip.Addr, error)
- func ParseAddr(s string) (netip.Addr, error)
- type Action
- type AddrAnonymizer
- type Anonymizer
- type CryptoPAn
- func (c *CryptoPAn) AnonymizeAddr(addr netip.Addr) (netip.Addr, error)
- func (c *CryptoPAn) AnonymizePrefix(p netip.Prefix) (netip.Prefix, error)
- func (c *CryptoPAn) AnonymizeString(addr netip.Addr) (string, error)
- func (c *CryptoPAn) Kind() Kind
- func (c *CryptoPAn) WithScopeRules(r ScopeRules) *CryptoPAn
- type Dropper
- type Encoding
- type Hasher
- func (h *Hasher) AnonymizeString(addr netip.Addr) (string, error)
- func (h *Hasher) Kind() Kind
- func (h *Hasher) Token(addr netip.Addr) (string, KeyID, error)
- func (h *Hasher) TokenAt(addr netip.Addr, t time.Time) (string, KeyID, error)
- func (h *Hasher) WithEncoding(e Encoding) *Hasher
- func (h *Hasher) WithOutputBits(n int) (*Hasher, error)
- func (h *Hasher) WithScopeRules(r ScopeRules) *Hasher
- type KeyID
- type KeyProvider
- type Kind
- type Policy
- type Pseudonymizer
- func (p *Pseudonymizer) AnonymizeString(addr netip.Addr) (string, error)
- func (p *Pseudonymizer) Kind() Kind
- func (p *Pseudonymizer) Period() time.Duration
- func (p *Pseudonymizer) Token(addr netip.Addr) (string, KeyID, error)
- func (p *Pseudonymizer) TokenAt(addr netip.Addr, t time.Time) (string, KeyID, error)
- func (p *Pseudonymizer) WithEncoding(e Encoding) *Pseudonymizer
- func (p *Pseudonymizer) WithKeyLabel(label bool) *Pseudonymizer
- func (p *Pseudonymizer) WithOutputBits(n int) (*Pseudonymizer, error)
- func (p *Pseudonymizer) WithScopeRules(r ScopeRules) *Pseudonymizer
- func (p *Pseudonymizer) Zero()
- type Reversible
- type ReversibleCryptoPAn
- type RotatingKeys
- type Scope
- type ScopeRules
- type StaticKey
- type Truncator
- func (t *Truncator) AnonymizeAddr(addr netip.Addr) (netip.Addr, error)
- func (t *Truncator) AnonymizeString(addr netip.Addr) (string, error)
- func (t *Truncator) Bits() (v4, v6 int)
- func (t *Truncator) Kind() Kind
- func (t *Truncator) Prefix(addr netip.Addr) (netip.Prefix, error)
- func (t *Truncator) WithScopeRules(r ScopeRules) *Truncator
Constants ¶
const ( // RotateDaily gives per-day linkability: you can count distinct sources // within a day but cannot follow one across midnight. The usual choice for // analytics. RotateDaily = 24 * time.Hour // RotateWeekly suits abuse detection that needs a longer memory. RotateWeekly = 7 * 24 * time.Hour // RotateHourly is aggressive; useful for high-sensitivity logs where you // only need to correlate requests within a single session. RotateHourly = time.Hour )
Common rotation periods for NewRotatingKeys.
const CryptoPAnKeyLen = 32
CryptoPAnKeyLen is the key length required by NewCryptoPAn: 16 bytes of AES-128 key followed by 16 bytes from which the pad is derived.
const DefaultOutputBits = 128
DefaultOutputBits is the token width used unless overridden.
const KeyLen = 32
KeyLen is the length in bytes of the keys used by this package.
const MinOutputBits = 64
MinOutputBits is the smallest token width this package will produce.
Below 64 bits, collisions between distinct addresses stop being negligible: by the birthday bound, a 32-bit token collides after roughly 65,000 distinct addresses, which would silently merge unrelated users into one apparent identity and corrupt exactly the distinct-count metrics the token exists to support. Truncating a token does not add privacy in any case, because the keyspace being protected is the address space, not the digest.
Variables ¶
var ( // ErrInvalidAddr reports an address that is not valid, such as the zero // netip.Addr or a value that failed to parse. ErrInvalidAddr = errors.New("ipanon: invalid IP address") // ErrNotReversible reports an attempt to recover an original address from // a technique that is genuinely one-way. ErrNotReversible = errors.New("ipanon: technique is not reversible") // ErrInvalidKey reports a key that is missing, the wrong length, or // otherwise unusable. ErrInvalidKey = errors.New("ipanon: invalid key") // ErrInvalidConfig reports a configuration that cannot produce a working // anonymizer, such as a prefix length outside the valid range. ErrInvalidConfig = errors.New("ipanon: invalid configuration") )
Errors returned by this package. Callers should test with errors.Is.
Functions ¶
func GenerateKey ¶
GenerateKey returns a new random key of KeyLen bytes, suitable for any technique in this package.
Store the result in your secret manager. A key that is lost cannot be recovered, and every value derived from it becomes unlinkable to newly anonymized data; a key that leaks retroactively de-pseudonymizes every value ever derived from it.
func KeyFromEnv ¶
KeyFromEnv reads a hex-encoded key of at least KeyLen bytes from the named environment variable.
This is a convenience for containerized deployments. It is not an endorsement of environment variables as a secret store: they are visible in process listings, inherited by child processes, and frequently captured by crash reporters. A secret manager is better.
func LegacySaltedSHA256
deprecated
LegacySaltedSHA256 reproduces the sha256(salt + ip.String()) construction found throughout deployed code and older guidance.
It exists so that you can keep matching values you have already stored while you migrate, and for no other reason.
Deprecated: this construction hashes the textual form of the address, so "192.0.2.1" and "::ffff:192.0.2.1" produce different tokens for the same client, and it uses an ad hoc secret-prefix MAC rather than HMAC. Use NewHasher or NewPseudonymizer for anything new.
func Normalize ¶
Normalize returns addr in the canonical form used throughout this package.
It unmaps IPv4-in-IPv6 addresses, so ::ffff:192.0.2.1 is treated as the IPv4 address 192.0.2.1 rather than as a 128-bit address. This matters: without it, an IPv4 client arriving over a dual-stack listener would be truncated with IPv6 rules and produce a different value than the same client on an IPv4 listener.
It also strips any zone identifier, since a zone is a local interface name with no meaning outside the host that observed it.
Normalize reports ErrInvalidAddr for the zero Addr.
Types ¶
type Action ¶
type Action uint8
Action says what an anonymizer should do with an address of a given Scope.
const ( // ActionAnonymize applies the configured technique. The default. ActionAnonymize Action = iota // ActionPassThrough returns the address unchanged. Use it only for scopes // that cannot identify an external person, such as loopback. ActionPassThrough // ActionDrop discards the address entirely. ActionDrop )
The available actions. The zero value anonymizes, so a zero ScopeRules is the safe default: everything gets anonymized.
type AddrAnonymizer ¶
AddrAnonymizer is implemented by techniques whose output is itself an IP address, so the result can be stored in an inet column, geolocated, or fed into further processing.
The invalid zero netip.Addr is a valid result and means the address was deliberately discarded.
type Anonymizer ¶
Anonymizer maps an address to a stable, privacy-reduced string form.
Implementations must be safe for concurrent use. The empty string is a valid result and means the address was deliberately discarded.
type CryptoPAn ¶
type CryptoPAn struct {
// contains filtered or unexported fields
}
CryptoPAn implements the Crypto-PAn prefix-preserving address anonymization scheme of Xu, Fan, Ammar, and Moon.
What prefix-preserving means ¶
If two addresses share their first n bits, their anonymized forms also share their first n bits, and if they differ at bit n, so do the outputs. The hierarchy of the address space survives intact: a /24 stays a /24, subnets stay nested inside their parent networks, and two hosts in the same network are still visibly in the same network afterwards.
That is what makes it the right tool when the structure is the signal. Truncation destroys the host part, so you cannot tell two machines in a subnet apart; hashing destroys the structure, so you cannot tell that two hosts are related at all. Crypto-PAn keeps both, which is why it is the standard for sharing netflow and packet traces, and why it works for abuse analysis that reasons about "this whole /22 started scanning us".
It is reversible, therefore it is pseudonymization ¶
The mapping is a bijection, computable in both directions by anyone holding the key. Crypto-PAn output is personal data, full stop. Protect the key as carefully as you would protect the original addresses, and do not describe data anonymized this way as anonymous in a privacy notice.
Prefix preservation carries its own leak, independent of the key. The output preserves the frequency distribution of prefixes, so an adversary who knows roughly how your traffic is distributed across networks can match large blocks against public routing data without ever attacking the key. This is well documented in the literature on trace anonymization; it is a property of the scheme, not a flaw in this implementation. If you are publishing traces rather than protecting your own logs, read up before relying on it.
Cost ¶
One AES block encryption per address bit: 32 for IPv4, 128 for IPv6. That makes it roughly two orders of magnitude more expensive than truncation. Fine per HTTP request, worth measuring before putting it in a packet-rate path.
A CryptoPAn is safe for concurrent use.
func NewCryptoPAn ¶
NewCryptoPAn returns a CryptoPAn using key, which must be exactly CryptoPAnKeyLen bytes. The first half is the AES-128 key; the second half is encrypted under it to produce the pad, following the reference implementation, so that keys are interchangeable with other Crypto-PAn implementations.
The returned value does not implement Reversible. Use NewReversibleCryptoPAn when you deliberately want the ability to recover original addresses, so that the capability is visible at the call site and in code review rather than being available by accident.
func (*CryptoPAn) AnonymizeAddr ¶
AnonymizeAddr implements AddrAnonymizer.
The result is a syntactically valid address of the same family, and it is indistinguishable from a real one. It may well collide with space that genuinely belongs to somebody else, so never feed Crypto-PAn output to a geolocation lookup, a reputation service, a firewall rule, or an abuse report: you would be making claims about an unrelated network.
func (*CryptoPAn) AnonymizePrefix ¶
AnonymizePrefix anonymizes a network rather than a single address, keeping the prefix length. Because the scheme is prefix-preserving, this is the network that every address inside p maps into.
func (*CryptoPAn) AnonymizeString ¶
AnonymizeString implements Anonymizer.
func (*CryptoPAn) WithScopeRules ¶
func (c *CryptoPAn) WithScopeRules(r ScopeRules) *CryptoPAn
WithScopeRules returns a copy of c that applies the given per-scope handling.
type Dropper ¶
type Dropper struct {
// contains filtered or unexported fields
}
Dropper discards the address entirely. It is the only technique in this package that produces genuinely anonymous output, because it produces no output: there is nothing to correlate, nothing to brute-force, no key to leak, and nothing to hand over in response to a subject access request.
Dropping is not the same as not collecting. If the address reaches your process it has been processed, and if it reaches a proxy access log or a crash dump on the way, dropping it downstream does not help. Check the whole path.
The usual pattern is to derive what you actually need from the full address first and drop only the address:
country := geo.Country(addr) // your geolocation provider
asn := geo.ASN(addr)
log.Info("request", "country", country, "asn", asn) // no address at all
A Dropper exists as a type, rather than as the absence of one, so that dropping can be selected through the same Policy machinery as every other technique and switched on per environment without changing call sites.
func (*Dropper) AnonymizeAddr ¶
AnonymizeAddr implements AddrAnonymizer, returning the invalid zero netip.Addr for any address the scope rules do not exempt.
func (*Dropper) AnonymizeString ¶
AnonymizeString implements Anonymizer, returning the empty string for any address the scope rules do not exempt.
func (*Dropper) WithScopeRules ¶
func (d *Dropper) WithScopeRules(r ScopeRules) *Dropper
WithScopeRules returns a copy of d that applies the given per-scope handling. This is how you keep internal addresses in logs while discarding every external one:
dropper := ipanon.NewDropper().WithScopeRules(ipanon.PassThroughInternal())
type Encoding ¶
type Encoding uint8
Encoding selects the textual representation of a token.
const ( // EncodingHex is lowercase hexadecimal. The default: universally readable, // greppable, and safe in every log format and column type. EncodingHex Encoding = iota // EncodingBase32 is unpadded, lowercase RFC 4648 base32. Shorter than hex // and still case-insensitive, which suits systems that fold case. EncodingBase32 // EncodingBase64URL is unpadded RFC 4648 URL-safe base64. The most compact; // avoid it where the consumer folds case or splits on "-". EncodingBase64URL )
The available token encodings.
type Hasher ¶
type Hasher struct {
// contains filtered or unexported fields
}
Hasher replaces an address with a keyed one-way token.
Unlike truncation, hashing preserves distinctness: two different addresses almost certainly produce two different tokens, so distinct-source counts and per-source rate limits keep working while the address itself is not stored.
Why keyed, and why this is not anonymization ¶
The familiar formulation of this technique is sha256(salt + ip). That is weaker than it looks on two counts. First, a bare hash of an IPv4 address is not protection at all: the entire IPv4 space is 2^32 addresses, so anyone can enumerate it and build a complete reverse lookup table on a laptop. Second, prepending a secret to a SHA-256 input is a homemade MAC construction that invites length-extension mistakes when the scheme is later extended.
This implementation uses HMAC-SHA256, the standard keyed pseudorandom function, which addresses the second point. It cannot address the first: the security of the token rests entirely on the key staying secret. If the key leaks, every token ever produced under it can be reversed by brute force in minutes. That is why this is pseudonymization, not anonymization, and why you should prefer Pseudonymizer, whose rotating key bounds how much history a single leaked key exposes.
A Hasher is safe for concurrent use.
func NewHasher ¶
func NewHasher(keys KeyProvider) (*Hasher, error)
NewHasher returns a Hasher that derives tokens from the key supplied by keys, with DefaultOutputBits of output in EncodingHex.
Pass a StaticKey for tokens that stay linkable indefinitely. Pass RotatingKeys and you have built a Pseudonymizer by hand; use that type instead, since it also exposes the key identifier you need to interpret the result.
func (*Hasher) AnonymizeString ¶
AnonymizeString implements Anonymizer, returning the encoded token.
Addresses exempted by the scope rules are returned in their normal textual form, and dropped addresses yield the empty string. A token is never a valid IP address, so the two cases are always distinguishable downstream.
func (*Hasher) Token ¶
Token returns the encoded token along with the identifier of the key that produced it. Store the KeyID beside the token if you ever intend to rotate the key, so that you can tell which tokens are comparable.
func (*Hasher) TokenAt ¶
TokenAt is Hasher.Token for a specific point in time. It matters only when the KeyProvider rotates: use it to backfill historical records with the key that was in effect when each record was created, rather than with today's.
func (*Hasher) WithEncoding ¶
WithEncoding returns a copy of h that renders tokens using e.
func (*Hasher) WithOutputBits ¶
WithOutputBits returns a copy of h producing tokens of n bits. n must be a multiple of 8 between MinOutputBits and 256.
func (*Hasher) WithScopeRules ¶
func (h *Hasher) WithScopeRules(r ScopeRules) *Hasher
WithScopeRules returns a copy of h that applies the given per-scope handling.
type KeyID ¶
type KeyID string
KeyID is a short, non-secret label identifying which key produced a value. Store it alongside anonymized data so that you can tell which records share a key, and so that a key rotation is visible in the data rather than silently changing every token.
A KeyID is a fingerprint, not key material: it is derived from the key through a one-way function and reveals nothing about it.
type KeyProvider ¶
type KeyProvider interface {
// KeyAt returns the key active at t, along with its identifier. The
// returned slice must not be modified by the caller and must remain valid
// for the duration of the call that obtained it.
KeyAt(t time.Time) ([]byte, KeyID, error)
}
KeyProvider supplies the key in effect at a given time.
Implementations must be safe for concurrent use.
type Kind ¶
type Kind uint8
Kind identifies an anonymization technique.
The available techniques.
func ParseKind ¶
ParseKind returns the Kind named by s, which must be one of the values produced by Kind.String.
func (Kind) Anonymous ¶
Anonymous reports whether the technique yields data that is anonymous rather than merely pseudonymous, in the sense of GDPR Recital 26.
Only KindDrop qualifies. Every other technique in this package produces a value that is derived from, and to some degree linkable back to, the original address. See the package documentation.
type Policy ¶
type Policy struct {
// Technique selects the anonymizer. Required.
Technique Kind
// V4Bits and V6Bits are the prefix lengths kept by [KindTruncate].
// Zero means the defaults, 24 and 48.
V4Bits, V6Bits int
// Key is the secret for [KindHash], [KindPseudonym], and [KindCryptoPAn].
// It must be at least [KeyLen] bytes, and exactly [CryptoPAnKeyLen] for
// Crypto-PAn. See [GenerateKey] and [KeyFromEnv].
Key []byte
// RotationPeriod is how often [KindPseudonym] derives a new key. Zero
// means [RotateDaily].
RotationPeriod time.Duration
// OutputBits is the token width for [KindHash] and [KindPseudonym]. Zero
// means [DefaultOutputBits].
OutputBits int
// Encoding renders tokens for [KindHash] and [KindPseudonym].
Encoding Encoding
// Scopes overrides handling per address scope. The zero value anonymizes
// every scope. See [PassThroughInternal].
Scopes ScopeRules
// Reversible allows [KindCryptoPAn] to be built with the de-anonymization
// capability exposed. It has no effect on other techniques, none of which
// can be reversed at all.
//
// Setting this keeps the data squarely within the scope of the GDPR. Only
// set it for a process that genuinely needs to resolve addresses, and keep
// that process separate from the one writing logs.
Reversible bool
// Retention documents how long the anonymized output is kept. It is
// advisory: this package does not delete anything. It is here because the
// technique and the retention period are one decision, not two, and
// recording them together is the only way the pairing survives review.
Retention time.Duration
}
Policy is a declarative description of how a particular class of data should be anonymized, from which Policy.Build constructs the corresponding anonymizer.
It exists so that the choice of technique can live in configuration rather than at the call site. Two things follow from that. You can run a strict policy in production and a permissive one locally without conditionals in request handlers. And, more usefully, the policy value is a compact, reviewable statement of what you do with addresses, which is close to what a record of processing activities has to say anyway.
Fields not relevant to the chosen Kind are ignored.
func PolicyAnalytics ¶
func PolicyAnalytics() Policy
PolicyAnalytics is the recommended default for web and product analytics: truncate to /24 and /48, matching what Google Analytics does, with a 26-month retention to match the common analytics ceiling.
Geolocate before this runs and store the country as its own field; see Truncator.
func PolicyGoogleAnalytics ¶
func PolicyGoogleAnalytics() Policy
PolicyGoogleAnalytics is PolicyAnalytics without the retention hint, for when you only want the truncation behavior.
func PolicyRateLimit ¶
PolicyRateLimit suits identifying a repeat source without building a profile: pseudonyms on an hourly key, which is long enough for abuse control and short enough that yesterday's traffic cannot be linked to today's.
The key must be supplied by the caller.
func PolicySecurityAudit ¶
PolicySecurityAudit suits authentication events, administrative actions, and anything else you may need to reconstruct during an incident.
It uses reversible Crypto-PAn, so the audit trail keeps its network structure and an authorized investigator holding the key can resolve an address when there is cause. That is a deliberate trade: this data remains personal data, and the justification for retaining it is the legitimate interest in security rather than anything to do with anonymization. Restrict access to both the logs and the key, and keep this policy well away from your analytics pipeline.
func PolicyStrict ¶
func PolicyStrict() Policy
PolicyStrict drops addresses entirely. The only configuration here that produces genuinely anonymous data.
func (Policy) Build ¶
func (p Policy) Build() (Anonymizer, error)
Build constructs the anonymizer the policy describes.
The concrete type depends on Policy.Technique; type-assert to AddrAnonymizer if you need address-shaped output, or to Reversible to find out whether the result can be de-anonymized.
type Pseudonymizer ¶
type Pseudonymizer struct {
// contains filtered or unexported fields
}
Pseudonymizer replaces an address with a token that changes on a schedule.
Within one period the token for a given address is stable, so you can count distinct sources, spot a source hitting you repeatedly, and rate-limit per source. Across periods the token changes unpredictably, so the same visitor on Monday and Tuesday cannot be recognized as the same visitor. That is the point: it buys you the analytics that need short-term linkability without building a long-term profile.
Why this beats a lookup table ¶
The obvious implementation of rotating pseudonyms is a map from address to random identifier, cleared on a timer. That map is itself a re-identification table: it holds every raw address you have seen, it has to be secured and disclosed like any other store of personal data, it does not survive a restart, and it does not agree between replicas.
A Pseudonymizer stores nothing. The period's key is derived from a master key with HKDF, and the token is an HMAC under that key, so every replica computes the same token independently, a restart changes nothing, and there is no table to leak or to produce in response to a subject access request.
What it is not ¶
This is still pseudonymization. Anyone with the master key can recompute the token for a guessed address, and the IPv4 space is small enough to enumerate exhaustively. Rotation limits the exposure of a leaked key to the periods it covers; it does not eliminate it. Choose the period to match your retention: a key that rotates daily protects little if you keep 400 days of tokens and the master key is what leaks.
A Pseudonymizer is safe for concurrent use.
func NewPseudonymizer ¶
func NewPseudonymizer(master []byte, period time.Duration) (*Pseudonymizer, error)
NewPseudonymizer returns a Pseudonymizer deriving a fresh key every period from master, which must be at least KeyLen bytes; see GenerateKey.
RotateDaily is the usual period. Shorter periods give stronger privacy and less analytical reach; a period shorter than your typical session means you cannot even correlate one visit end to end.
func NewPseudonymizerWithKeys ¶
func NewPseudonymizerWithKeys(keys *RotatingKeys) (*Pseudonymizer, error)
NewPseudonymizerWithKeys returns a Pseudonymizer using an existing RotatingKeys, so that several anonymizers can share one derivation chain and one rotation schedule.
func (*Pseudonymizer) AnonymizeString ¶
func (p *Pseudonymizer) AnonymizeString(addr netip.Addr) (string, error)
AnonymizeString implements Anonymizer, returning the token for the current period.
func (*Pseudonymizer) Period ¶
func (p *Pseudonymizer) Period() time.Duration
Period returns the rotation period.
func (*Pseudonymizer) Token ¶
Token returns the token for the current period along with the identifier of the key that produced it.
func (*Pseudonymizer) TokenAt ¶
TokenAt returns the token for the period containing t.
Use it when anonymizing records after the fact: pass each record's own timestamp so that a log replayed on Friday still yields the tokens it would have had on Tuesday, and distinct-source counts per day stay correct.
func (*Pseudonymizer) WithEncoding ¶
func (p *Pseudonymizer) WithEncoding(e Encoding) *Pseudonymizer
WithEncoding returns a copy of p that renders tokens using e.
func (*Pseudonymizer) WithKeyLabel ¶
func (p *Pseudonymizer) WithKeyLabel(label bool) *Pseudonymizer
WithKeyLabel returns a copy of p that prefixes each token with the key identifier and a colon, for example "3f9a1c02.20291:a1b2...".
Turn this on when tokens from different periods end up in the same column. Without it, two tokens are simply unequal and you cannot tell whether that is because they are different sources or because a rotation happened in between, which quietly inflates distinct-source counts across a period boundary.
func (*Pseudonymizer) WithOutputBits ¶
func (p *Pseudonymizer) WithOutputBits(n int) (*Pseudonymizer, error)
WithOutputBits returns a copy of p producing tokens of n bits. n must be a multiple of 8 between MinOutputBits and 256.
func (*Pseudonymizer) WithScopeRules ¶
func (p *Pseudonymizer) WithScopeRules(r ScopeRules) *Pseudonymizer
WithScopeRules returns a copy of p that applies the given per-scope handling.
func (*Pseudonymizer) Zero ¶
func (p *Pseudonymizer) Zero()
Zero overwrites the master key and any derived period keys. The Pseudonymizer must not be used afterwards. Best-effort; see StaticKey.Zero.
type Reversible ¶
Reversible is implemented by techniques that are pseudonymization rather than anonymization: a key holder can recover the original address.
This interface exists so that reversibility is visible in the type system. Data produced by a Reversible technique remains personal data.
type ReversibleCryptoPAn ¶
type ReversibleCryptoPAn struct {
CryptoPAn
}
ReversibleCryptoPAn is a CryptoPAn that also exposes the inverse mapping.
Keep it out of the code path that writes logs. The usual arrangement is that the ingest path holds a plain CryptoPAn and cannot reverse anything, while a separate, access-controlled tool holds the key and constructs this type when an incident genuinely requires resolving an anonymized address.
func NewReversibleCryptoPAn ¶
func NewReversibleCryptoPAn(key []byte) (*ReversibleCryptoPAn, error)
NewReversibleCryptoPAn returns a ReversibleCryptoPAn for key.
Constructing this type is a deliberate decision to retain re-identification capability. Under the GDPR that capability is precisely what keeps the data in scope, so record why you need it.
func (*ReversibleCryptoPAn) Deanonymize ¶
Deanonymize implements Reversible, recovering the original address.
It has no way to detect an address that was never anonymized, or that was anonymized under a different key: it will return a plausible-looking wrong answer instead of an error. Track which key produced a dataset.
func (*ReversibleCryptoPAn) WithScopeRules ¶
func (c *ReversibleCryptoPAn) WithScopeRules(r ScopeRules) *ReversibleCryptoPAn
WithScopeRules returns a copy of c that applies the given per-scope handling.
Note that a scope configured to pass through or drop is not recoverable by ReversibleCryptoPAn.Deanonymize, since no anonymization took place.
type RotatingKeys ¶
type RotatingKeys struct {
// contains filtered or unexported fields
}
RotatingKeys is a KeyProvider that derives a fresh key for each period from a long-lived master key using HKDF-SHA256.
Rotation bounds the damage from a key compromise in time. An attacker who obtains the epoch key for one day can de-pseudonymize that day's data and nothing else; an attacker who obtains the master key can derive every epoch, so the master key is the thing to protect.
Because epoch keys are derived rather than stored, rotation needs no coordination: every process holding the same master key computes the same epoch key independently, and there is no mapping table to secure or to produce in response to a subject access request.
Epochs are aligned to the Unix epoch, not to the time the provider was created, so all processes roll over together.
func NewRotatingKeys ¶
func NewRotatingKeys(master []byte, period time.Duration) (*RotatingKeys, error)
NewRotatingKeys returns a RotatingKeys deriving from master with the given rotation period. The master key must be at least KeyLen bytes and is copied. Period must be positive; see RotateDaily and friends.
func (*RotatingKeys) Epoch ¶
func (r *RotatingKeys) Epoch(t time.Time) int64
Epoch returns the epoch number covering t. Values anonymized in the same epoch share a key and are mutually linkable; values in different epochs are not.
func (*RotatingKeys) KeyAt ¶
KeyAt implements KeyProvider, returning the key for the epoch containing t.
func (*RotatingKeys) Period ¶
func (r *RotatingKeys) Period() time.Duration
Period returns the rotation period.
func (*RotatingKeys) Zero ¶
func (r *RotatingKeys) Zero()
Zero overwrites the master key and every cached epoch key. The RotatingKeys must not be used afterwards. Best-effort; see StaticKey.Zero.
type Scope ¶
type Scope uint8
Scope classifies an address by the network it belongs to. Callers use it to apply different handling to addresses that are not personal data in the first place, such as loopback traffic from a health checker.
const ( // ScopeGlobal is a publicly routable address. This is the only scope that // reliably identifies an external party. ScopeGlobal Scope = iota // ScopeUnspecified is 0.0.0.0 or ::. ScopeUnspecified // ScopeLoopback is 127.0.0.0/8 or ::1. ScopeLoopback // ScopeLinkLocal is 169.254.0.0/16, fe80::/10, or a multicast link-local // address. ScopeLinkLocal // ScopePrivate is RFC 1918 space (10/8, 172.16/12, 192.168/16) or an IPv6 // unique local address (fc00::/7). ScopePrivate // ScopeCGNAT is RFC 6598 carrier-grade NAT space, 100.64.0.0/10. Addresses // here are shared by many subscribers of one ISP. ScopeCGNAT // ScopeMulticast is any non-link-local multicast address. ScopeMulticast )
Address scopes, in the order ClassifyScope tests them.
func ClassifyScope ¶
ClassifyScope returns the Scope of addr. The address should already be normalized; ClassifyScope normalizes defensively so that a mapped IPv4 address classifies as IPv4 space.
type ScopeRules ¶
type ScopeRules struct {
Unspecified Action
Loopback Action
LinkLocal Action
Private Action
CGNAT Action
Multicast Action
}
ScopeRules overrides the handling of addresses by scope. The zero value anonymizes every scope, which is always safe. Globally routable addresses are always anonymized and cannot be overridden.
A common configuration passes internal traffic through unchanged, since a loopback or RFC 1918 address identifies a machine in your own estate rather than a data subject:
rules := ipanon.ScopeRules{
Loopback: ipanon.ActionPassThrough,
Private: ipanon.ActionPassThrough,
}
Note that ScopeCGNAT is deliberately not grouped with Private. A carrier- grade NAT address belongs to an ISP subscriber and is personal data.
func PassThroughInternal ¶
func PassThroughInternal() ScopeRules
PassThroughInternal returns rules that leave loopback, link-local, unspecified, and RFC 1918 addresses untouched while anonymizing everything else, including carrier-grade NAT space.
type StaticKey ¶
type StaticKey struct {
// contains filtered or unexported fields
}
StaticKey is a KeyProvider that always returns the same key.
Use it when you want tokens to stay linkable indefinitely, for example to count lifetime distinct users. Be aware of the trade-off: with a static key there is no point at which old data stops being re-identifiable by whoever holds the key. Prefer RotatingKeys unless you specifically need indefinite linkage.
func NewStaticKey ¶
NewStaticKey returns a StaticKey wrapping key, which must be at least KeyLen bytes. The key is copied.
func (*StaticKey) KeyAt ¶
KeyAt implements KeyProvider. The time is ignored.
type Truncator ¶
type Truncator struct {
// contains filtered or unexported fields
}
Truncator zeroes the host bits of an address, keeping a fixed-length network prefix.
This is the most widely deployed technique and the one Google Analytics applies for its IP anonymization feature. It is cheap, needs no key, and preserves enough of the address that country- and often region-level geolocation still works, along with the owning ASN.
What it does not do is make the data anonymous. A /24 has 256 addresses, and many of them are unused; combined with a timestamp, a user agent, and a URL path, a truncated address frequently still singles out one household. Treat the output as pseudonymous personal data.
If you need geolocation, geolocate the full address first and store the resulting country or region as its own field. Truncating first and geolocating afterwards loses precision for no privacy gain, because you already handled the full address.
A Truncator is safe for concurrent use, and Truncator.AnonymizeAddr does not allocate.
func NewTruncator ¶
NewTruncator returns a Truncator keeping the leading v4Bits of an IPv4 address and the leading v6Bits of an IPv6 address. Both must be in range for their family: 0 to 32, and 0 to 128.
Fewer bits means more privacy and less analytical value. See TruncateGA and TruncateStrict for the two configurations worth defaulting to.
func TruncateGA ¶
func TruncateGA() *Truncator
TruncateGA returns the truncation Google Analytics performs when IP anonymization is enabled: /24 for IPv4 and /48 for IPv6.
This is the de facto industry baseline and what most data protection authorities have seen before. It is a reasonable default for web analytics.
func TruncateStrict ¶
func TruncateStrict() *Truncator
TruncateStrict returns an aggressive truncation, /16 for IPv4 and /32 for IPv6, which collapses each address into a large block.
Use it when you want little more than a rough sense of network origin. Be aware that a /16 can span multiple cities and occasionally multiple countries, so geolocation of the truncated value is unreliable; geolocate before truncating.
func (*Truncator) AnonymizeAddr ¶
AnonymizeAddr implements AddrAnonymizer, returning the base address of the network addr falls into.
func (*Truncator) AnonymizeString ¶
AnonymizeString implements Anonymizer. It returns the truncated address in its usual textual form, for example "192.168.42.0" or "2001:db8:1234::".
func (*Truncator) Prefix ¶
Prefix returns the network that addr is truncated to, which is useful when you want to store or aggregate by the prefix itself rather than by its base address.
func (*Truncator) WithScopeRules ¶
func (t *Truncator) WithScopeRules(r ScopeRules) *Truncator
WithScopeRules returns a copy of t that applies the given per-scope handling. See ScopeRules and PassThroughInternal.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
ipanon
command
Command ipanon anonymizes IP addresses in log files and streams.
|
Command ipanon anonymizes IP addresses in log files and streams. |
|
Package httpanon anonymizes client IP addresses in net/http servers.
|
Package httpanon anonymizes client IP addresses in net/http servers. |
|
Package kanon generalizes IP addresses until each released value covers at least k observed sources.
|
Package kanon generalizes IP addresses until each released value covers at least k observed sources. |
|
Package redact rewrites IP addresses found in arbitrary text.
|
Package redact rewrites IP addresses found in arbitrary text. |
|
Package sloganon anonymizes IP addresses in structured log records.
|
Package sloganon anonymizes IP addresses in structured log records. |