mask

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 12 Imported by: 0

README

mask-go

GitHub Release Go Reference GitHub Actions Workflow Status GitHub License

A Go library for redacting API keys, access tokens and other credentials from text, with zero dependencies.

Installation

go get github.com/koki-develop/mask-go

Usage

m := mask.New(mask.WithPatterns(mask.AllBuiltinPatterns()...))

fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
// GITHUB_TOKEN=****************************************

Patterns

A Masker scans only with the patterns it is given. AllBuiltinPatterns() returns every built-in pattern, and grows as patterns are added.

Each vendor also has an accessor of its own, for a caller who wants some of them and not all:

m := mask.New(mask.WithPatterns(slices.Concat(
	mask.AWSPatterns(),
	mask.GitHubPatterns(),
)...))
Accessor Locates
AgePatterns() []Pattern age secret keys (X25519 and MLKEM768-X25519 hybrid identities)
AirtablePatterns() []Pattern Airtable personal access tokens
AnthropicPatterns() []Pattern Anthropic API keys, Anthropic Admin API keys, Anthropic OAuth tokens, Anthropic session keys
AWSPatterns() []Pattern AWS access key IDs, AWS secret access keys
BuildkitePatterns() []Pattern Buildkite API access tokens, agent session tokens, agent job tokens, unclustered agent tokens, agent (cluster) tokens, registry tokens, Package Registries temporary tokens, portal tokens, portal secrets, job acquisition tokens, token exchange tokens
CircleCIPatterns() []Pattern CircleCI personal API tokens, project API tokens
CloudflarePatterns() []Pattern Cloudflare API tokens, Cloudflare API keys
CratesIOPatterns() []Pattern crates.io API tokens, Trusted Publishing access tokens
DatabricksPatterns() []Pattern Databricks personal access tokens, Databricks OAuth client secrets
DigitalOceanPatterns() []Pattern DigitalOcean personal access tokens, OAuth access tokens, OAuth refresh tokens
DockerPatterns() []Pattern Docker personal access tokens
DopplerPatterns() []Pattern Doppler CLI tokens, personal tokens, service tokens, service account tokens, service account identity tokens, SCIM tokens, audit tokens
DynatracePatterns() []Pattern Dynatrace tokens of every type written in the published format — access tokens, personal access tokens, account API tokens, OAuth2 refresh tokens and platform tokens among them
FlyIOPatterns() []Pattern Fly.io access tokens (personal access tokens, deploy tokens, org tokens, SSH tokens, machine-exec tokens), and the v1 permission and discharge tokens Fly.io still accepts
GitHubPatterns() []Pattern GitHub personal access tokens (classic and fine-grained), GitHub OAuth app access tokens, GitHub App user access tokens, GitHub App installation access tokens, GitHub App refresh tokens
GitLabPatterns() []Pattern GitLab personal access tokens, project access tokens, group access tokens, impersonation tokens, OAuth application secrets, deploy tokens, runner authentication tokens, CI/CD job tokens, pipeline trigger tokens, feed tokens, incoming mail tokens, GitLab agent for Kubernetes tokens, SCIM OAuth tokens, feature flags client tokens
GooglePatterns() []Pattern Google API keys
GrafanaPatterns() []Pattern Grafana service account tokens
GroqPatterns() []Pattern Groq API keys
HashiCorpPatterns() []Pattern HashiCorp Vault service tokens, batch tokens, recovery tokens, HCP Terraform API tokens
HerokuPatterns() []Pattern Heroku API tokens
HuggingFacePatterns() []Pattern Hugging Face user access tokens
JWT() Pattern JSON Web Tokens, signed and encrypted
LinearPatterns() []Pattern Linear personal API keys
NewRelicPatterns() []Pattern New Relic user keys, including the admin keys New Relic migrated into user keys
NotionPatterns() []Pattern Notion internal integration tokens, Notion OAuth access tokens, Notion personal access tokens
NPMPatterns() []Pattern npm granular access tokens, npm classic tokens (read-only, automation, publish)
OnePasswordPatterns() []Pattern 1Password service account tokens
OpenAIPatterns() []Pattern OpenAI project API keys, service account keys, Admin API keys, user API keys
OpenRouterPatterns() []Pattern OpenRouter API keys
PaddlePatterns() []Pattern Paddle API keys (live and sandbox)
PlanetScalePatterns() []Pattern PlanetScale service tokens, OAuth access tokens, OAuth refresh tokens
PostHogPatterns() []Pattern PostHog personal API keys
PostmanPatterns() []Pattern Postman API keys
PrivateKey() Pattern PKCS#8 private keys, encrypted PKCS#8 private keys, PKCS#1 RSA private keys, EC private keys, DSA private keys, OpenSSH private keys, PGP private key blocks
PulumiPatterns() []Pattern Pulumi personal access tokens, organization access tokens, team access tokens
PyPIPatterns() []Pattern PyPI API tokens, TestPyPI API tokens, Trusted Publisher tokens
ReplicatePatterns() []Pattern Replicate API tokens
ResendPatterns() []Pattern Resend API keys
RubyGemsPatterns() []Pattern RubyGems.org API keys
SendGridPatterns() []Pattern Twilio SendGrid API keys
SentryPatterns() []Pattern Sentry user auth tokens, organization auth tokens, user application tokens, internal integration tokens
ShopifyPatterns() []Pattern Shopify access tokens (public app, custom app, private app and delegate), Shopify app secret keys
SlackPatterns() []Pattern Slack bot tokens, user tokens, app-level tokens, workflow tokens, refresh tokens, rotatable bot and user access tokens
SonarQubePatterns() []Pattern SonarQube user tokens, global analysis tokens, project analysis tokens, project badge tokens
SourcegraphPatterns() []Pattern Sourcegraph access tokens
StripePatterns() []Pattern Stripe publishable API keys, restricted API keys, secret API keys, organization API keys, webhook signing secrets
SupabasePatterns() []Pattern Supabase personal access tokens, Supabase OAuth access tokens, Supabase publishable API keys, Supabase secret API keys
XAIPatterns() []Pattern xAI API keys, xAI management API keys

MustRegexp builds a pattern from a regular expression, and Regexp the same for one that arrives at run time:

p := mask.MustRegexp("internal-token", `INT-[0-9a-f]{32}`)
// or p, err := mask.Regexp("internal-token", expr)

m := mask.New(mask.WithPatterns(p))

fmt.Println(m.Mask("token: INT-0123456789abcdef0123456789abcdef"))
// token: ************************************

Every match is located, including one that begins inside another: forty characters of hexadecimal written against forty more are redacted whole.

NewPattern builds one from a function. Here, a value known only at run time:

secret := "s3cr3t-value"

p := mask.NewPattern("shared-secret", func(src string) ([]mask.Span, int) {
	var spans []mask.Span
	for i := 0; ; {
		j := strings.Index(src[i:], secret)
		if j < 0 {
			break
		}
		spans = append(spans, mask.Span{Start: i + j, End: i + j + len(secret)})
		i += j + 1
	}
	return spans, max(0, len(src)-len(secret)+1)
})

m := mask.New(mask.WithPatterns(p))

fmt.Println(m.Mask("password=s3cr3t-value"))
// password=************

The second result says how far along src the answer can no longer change if more text follows. Mask ignores it and Streaming is what it is for; returning 0 is always correct.

The Pattern interface can also be implemented directly.

Streaming

A value written across two writes is in neither of them, so masking each piece as it arrives redacts nothing. NewWriter and NewReader hold text back until the patterns agree nothing more of the stream can change what they found:

w := mask.NewWriter(os.Stderr, m)
defer w.Close()

log.SetOutput(w)

Only a tail that could still be the beginning of a value is held, so an ordinary line goes straight through. Close releases whatever is left.

NewReader masks in the other direction:

body, err := io.ReadAll(mask.NewReader(resp.Body, m))

Redactors

A redactor decides what a located value is redacted to. Fill repeats one rune for every rune of the original, and is the default as Fill('*'):

m := mask.New(
	mask.WithPatterns(mask.AllBuiltinPatterns()...),
	mask.WithRedactor(mask.Fill('#')),
)

fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
// GITHUB_TOKEN=########################################

Fixed replaces the value with a constant, so its length does not survive:

m := mask.New(
	mask.WithPatterns(mask.AllBuiltinPatterns()...),
	mask.WithRedactor(mask.Fixed("[REDACTED]")),
)

fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
// GITHUB_TOKEN=[REDACTED]

NewRedactor builds one from a function, which can vary by the pattern that located the value:

m := mask.New(
	mask.WithPatterns(mask.AllBuiltinPatterns()...),
	mask.WithRedactor(mask.NewRedactor(func(m mask.Match) string {
		return "[" + strings.ToUpper(m.Pattern.Name()) + "]"
	})),
)

fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
// GITHUB_TOKEN=[GITHUB-TOKEN]

License

MIT

Documentation

Overview

Package mask redacts sensitive values such as API keys and access tokens from text.

A Masker scans its input with the patterns it was given and redacts every value it locates:

m := mask.New(mask.WithPatterns(mask.AllBuiltinPatterns()...))
fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))

Output:

GITHUB_TOKEN=****************************************

A Masker scans only with the patterns given to it; nothing is enabled implicitly. AllBuiltinPatterns returns the built-in ones, and a custom pattern comes from NewPattern, Regexp, MustRegexp or any implementation of the Pattern interface.

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.AllBuiltinPatterns()...))

	fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
}
Output:
GITHUB_TOKEN=****************************************

Index

Examples

Constants

View Source
const LookBehind = 64

LookBehind is how far in front of a value a Pattern may read: the bytes from LookBehind before the start of a span it reports up to that start, and no further back.

A Masker scanning a whole string hands every pattern the whole of it and the limit costs nothing. It is what NewReader and NewWriter rest on: a stream is masked by scanning a window that moves along it, and text the window has already carried past is what those readers keep rather than release. The limit is what tells them how much to keep.

Stated as a demand on a Find: hand it the text from an offset k on rather than the whole, where Find has settled at least k + LookBehind of the whole, and from k + LookBehind on it must report what it reports when handed everything. What that rules out is a Find whose answer at one place depends on the whole of the text in front of it. A scan walking the text from the start and stepping over each value it found would be such a Find: where the window begins would decide where the values fall, and a value would move under the window as the window moved.

A built-in pattern reads no further in front of a value than what decides whether a value stands there at all. Where that is the one character a prefix may not stand behind there is no count to state; a scan reading further than that character states in its own file how far, and holds it to this limit. A pattern built by Regexp reads one rune, which is what \b, \B and ^ are decided by. The limit is far above either so that a Pattern written by hand has room to read a keyword or an assignment in front of what it locates. A Find that cannot be held to it, where a value is decided by more text in front of it than this, must settle nothing: what settles nothing is never handed a window.

Variables

View Source
var ErrClosed = errors.New("mask: writer is closed")

ErrClosed is returned by a Writer written to after it has been closed.

Functions

This section is empty.

Types

type Masker

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

Masker redacts sensitive values in text.

A Masker is fixed once created and is safe for concurrent use by multiple goroutines.

func New

func New(opts ...Option) *Masker

New returns a Masker that scans with the patterns given to WithPatterns and redacts what it locates with the redactor given to WithRedactor, which defaults to Fill('*').

A Masker with no patterns redacts nothing.

func (*Masker) Mask

func (m *Masker) Mask(src string) string

Mask returns src with every located value redacted.

Values that overlap are redacted together as one, so that no part of a located value survives. The combined text is attributed to the pattern that located the value starting earliest; among those, the longest; among those, the one added first by WithPatterns.

Masking is not idempotent, and what Mask is for is the text a program is about to write rather than text it has already masked. A redaction is itself text, and it does not read as the value it replaced: Fill('*') leaves an asterisk where a letter stood, and a prefix that letter closed is then open, so an AWS access key ID written against a Slack prefix is redacted on the first pass and takes a Slack token with it on the second. Fixed("") takes the value out altogether, splicing the text either side of it into text that was never written. Either way masking again may redact more than masking once did, and neither is a defect in a scan.

type Match

type Match struct {
	// Pattern is the pattern that located the value.
	Pattern Pattern

	// Value is the text about to be redacted. When a pattern redacts only
	// part of what it matched, Value holds that part rather than the whole
	// match.
	//
	// It reaches the other way too. Values that overlap are redacted
	// together as one, and the combined text goes to the redactor under the
	// single pattern Masker.Mask attributes it to, so Value can hold more
	// than that pattern located and most of it can be what another pattern
	// found. Masker.Mask states how the attribution is decided.
	Value string
}

Match is a sensitive value located by a Pattern.

type Option

type Option func(*options)

Option configures a Masker.

func WithPatterns

func WithPatterns(patterns ...Pattern) Option

WithPatterns adds patterns for a Masker to scan with. Repeated options accumulate in the order given:

m := mask.New(
	mask.WithPatterns(mask.GitHubToken(), mask.JWT()),
	mask.WithPatterns(mask.MustRegexp("internal-token", `INT-[0-9a-f]{32}`)),
)
Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	// Repeated options accumulate, so the built-in patterns and one of your
	// own can be given separately.
	m := mask.New(
		mask.WithPatterns(mask.AllBuiltinPatterns()...),
		mask.WithPatterns(mask.MustRegexp("internal-token", `INT-[0-9a-f]{32}`)),
	)

	fmt.Println(m.Mask("github=ghp_0123456789abcdefghijklmnopqrstuvwxyz internal=INT-0123456789abcdef0123456789abcdef"))
}
Output:
github=**************************************** internal=************************************

func WithRedactor

func WithRedactor(r Redactor) Option

WithRedactor sets what located values are redacted to, replacing Fill('*').

type Pattern

type Pattern interface {
	// Name identifies the pattern. It should be stable, lowercase and
	// hyphenated, such as "github-token".
	Name() string

	// Find returns the byte ranges to redact in src, and the offset from
	// which src is not yet settled.
	//
	// The spans may be unordered and may overlap; a Masker sorts them and
	// resolves the overlaps. Spans reaching outside src, and spans whose
	// Start is not less than their End, are ignored.
	//
	// Both ends must fall on a rune boundary. A span cutting a multi-byte
	// rune in half is neither ignored nor repaired: the bytes either side of
	// it are written back as they were found, so what is left of that rune
	// stands beside the redaction and the output is not valid UTF-8. The
	// built-in patterns and Regexp cannot report such a span — every
	// built-in decides its ends on an ASCII alphabet, and Go's regexp
	// matches runes — so this is a demand on a Find written by hand.
	//
	// retain answers what src alone cannot: whether src is all there is. For
	// every text beginning with src, the values in it that begin before
	// retain are exactly the spans reported here that begin before retain,
	// with the same Start and the same End. Nothing is promised about what
	// begins at retain or after it: a value there may grow, may appear where
	// nothing was reported, and may turn out to be no value at all.
	//
	// Both directions of that are load-bearing. A value the shorter text
	// misses is one a stream writes out before it is found; one it reports
	// that the longer text does not is a redaction a stream cannot take
	// back.
	//
	// Zero promises nothing and is always true. It is what a Find written
	// without a stream in mind returns, and what a Find returns when the
	// whole of src is still open — a value running to the end of it that
	// more text would carry further. Reporting len(src) says the opposite:
	// src stands complete, and nothing appended to it changes any of this.
	//
	// Mask reads the whole of its input at once and ignores retain. NewReader
	// and NewWriter are what it is for: they hold back the text from retain
	// on until more of the stream settles it.
	Find(src string) (spans []Span, retain int)
}

Pattern locates sensitive values in text.

Implementations must be safe for concurrent use by multiple goroutines.

func AWSAccessKeyID

func AWSAccessKeyID() Pattern

AWSAccessKeyID locates AWS access key IDs: twenty characters opening with AKIA, which AWS gives the long-term key of an IAM user or of the account root user, or ASIA, which it gives the temporary credentials AWS STS issues. Those are the two prefixes AWS documents for an access key ID, and STS tells the two apart by them.

A key is located wherever it is written, with no word boundary either side, and exactly twenty characters of it are. So an unbroken run of twenty uppercase letters and digits opening with one of the prefixes is redacted whether or not AWS issued it, which ASIA, being a word, makes reachable: ASIAPACIFICSOUTHEAST is redacted, and ASIANELEPHANTCONSERVATION loses its first twenty characters. A space, a hyphen or a lowercase letter ends the run, so text as it is ordinarily written is not affected.

Its name is "aws-access-key-id".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.AWSAccessKeyID()))

	fmt.Println(m.Mask("AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF"))
}
Output:
AWS_ACCESS_KEY_ID=********************

func AWSPatterns

func AWSPatterns() []Pattern

AWSPatterns returns every built-in pattern that locates a credential AWS issues.

The returned slice is freshly allocated and may be modified by the caller.

func AWSSecretAccessKey

func AWSSecretAccessKey() Pattern

AWSSecretAccessKey locates AWS secret access keys: the forty characters the secret half of an access key is written in, standing behind the name it is assigned to. The name is what says a key is there — SECRET_ACCESS_KEY, the aws_secret_access_key of a shared credentials file, the SecretAccessKey of the JSON an assume-role call prints, and the rest of the ways those three words are written — and only the forty characters are redacted, so the name and the assignment stay in the text to be read.

A key is located wherever those three words stand in front of one: the aws in front of them is not read, so a name is enough on its own, and a service naming its own credential the same way has that credential located too.

Its name is "aws-secret-access-key".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	// The value carries nothing to be recognised by — forty characters of
	// base64 are as much a git object as a key — so this pattern reads the name
	// the value is assigned to, and redacts the value alone. The name is what
	// stays behind to say which credential leaked.
	m := mask.New(mask.WithPatterns(mask.AWSSecretAccessKey()))

	fmt.Println(m.Mask("AWS_SECRET_ACCESS_KEY=0123456789abcdef0123456789abcdef01234567"))
}
Output:
AWS_SECRET_ACCESS_KEY=****************************************

func AgePatterns added in v0.1.0

func AgePatterns() []Pattern

AgePatterns returns every built-in pattern that locates a credential age generates.

The returned slice is freshly allocated and may be modified by the caller.

func AgeSecretKey added in v0.1.0

func AgeSecretKey() Pattern

AgeSecretKey locates age secret keys: the identities age-keygen writes, each of which decrypts every file encrypted to the recipient beside it. Two kinds carry the name and both are located — the X25519 identity, written AGE-SECRET-KEY-1..., and the MLKEM768-X25519 hybrid one age-keygen -pq writes, AGE-SECRET-KEY-PQ-1....

Either kind is one of those prefixes and exactly fifty-eight characters of the Bech32 alphabet behind it, in the uppercase age writes and reads them in. A key is located wherever it stands, with no word boundary either side.

The recipient age prints above a key in an identity file, age1..., is the public half of the same key pair and is left in the text.

Its name is "age-secret-key".

func AirtablePatterns added in v0.1.0

func AirtablePatterns() []Pattern

AirtablePatterns returns every built-in pattern that locates a credential Airtable issues.

The returned slice is freshly allocated and may be modified by the caller.

func AirtablePersonalAccessToken added in v0.1.0

func AirtablePersonalAccessToken() Pattern

AirtablePersonalAccessToken locates Airtable personal access tokens: the prefix pat, the fourteen characters that finish the token's identifier, a dot, and the sixty-four lowercase hexadecimal characters of the secret behind it — eighty-two characters altogether. One string serves every scope a token is created with and every base it is granted, so nothing in a token says what it may reach.

A token is located wherever it is written, with no word boundary either side, and exactly eighty-two characters of it are. So text of that shape is redacted whether or not Airtable issued it. A space, an identifier of the wrong length, a hyphen where the dot belongs or an uppercase letter in the secret ends the reading, so text as it is ordinarily written is not affected. A longer run is a token with something written after it, and the token alone is redacted.

Its name is "airtable-personal-access-token".

func AllBuiltinPatterns

func AllBuiltinPatterns() []Pattern

AllBuiltinPatterns returns every built-in pattern:

m := mask.New(mask.WithPatterns(mask.AllBuiltinPatterns()...))

The set grows as patterns are added to this package. The returned slice is freshly allocated and may be modified by the caller.

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	// The built-in patterns are given to a Masker together, and each of them
	// scans for what it knows.
	m := mask.New(mask.WithPatterns(mask.AllBuiltinPatterns()...))

	fmt.Println(m.Mask("token=ghp_0123456789abcdefghijklmnopqrstuvwxyz jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMifQ.0123456789abcdef"))
}
Output:
token=**************************************** jwt=************************************************************************

func AnthropicAPIKey

func AnthropicAPIKey() Pattern

AnthropicAPIKey locates Anthropic API keys: the keys the Claude Console issues (sk-ant-api03-) and the keys of the Admin API (sk-ant-admin01-). Both are written the same way — the prefix sk-ant-, the name of a kind, a hyphen, and a long run of random characters — and it is that shape, rather than the two names, that this pattern is anchored on. The OAuth tokens and the session keys Anthropic writes the same way are therefore located as well.

A key is located wherever it is written, with no word boundary either side, and is redacted from its sk-ant- to the end of the run it stands in. So a key written against a word character keeps its span, and a character of the key's own alphabet written straight after a key is redacted with it.

Its name is "anthropic-api-key".

func AnthropicPatterns

func AnthropicPatterns() []Pattern

AnthropicPatterns returns every built-in pattern that locates a credential Anthropic issues.

The returned slice is freshly allocated and may be modified by the caller.

func BuildkitePatterns added in v0.2.0

func BuildkitePatterns() []Pattern

BuildkitePatterns returns every built-in pattern that locates a credential Buildkite issues.

The returned slice is freshly allocated and may be modified by the caller.

func BuildkiteToken added in v0.2.0

func BuildkiteToken() Pattern

BuildkiteToken locates the tokens Buildkite issues with a prefix of its own: API access tokens (bkua_), agent session tokens (bkaa_), agent job tokens (bkaj_), unclustered agent tokens (bkar_), agent tokens (bkct_), registry and Package Registries temporary tokens (bkpt_), portal tokens (bkpat_), portal secrets (bkps_), job acquisition tokens (bkjat_) and the tokens an exchanged assertion is minted into (bktx_).

Buildkite documents the prefixes and no length, so this pattern keys on the prefix: a token is redacted from it to the end of the run of base64url characters behind it, whatever that run comes to, once it is long enough to be a body at all.

A token is located wherever it is written, with no word boundary either side. So a token written against a word character keeps its span, and a character of the token's own alphabet written straight after a token is redacted with it.

Its name is "buildkite-token".

func CircleCIAPIToken added in v0.1.0

func CircleCIAPIToken() Pattern

CircleCIAPIToken locates CircleCI API tokens: the prefix CCIPAT_ a personal API token is written with or the CCIPRJ_ a project API token is written with, then twenty-two letters and digits, an underscore, and forty hexadecimal characters — seventy characters altogether.

A token is located wherever it is written, with no word boundary either side, and exactly seventy characters of it are. So text of that shape is redacted whether or not CircleCI issued it. A space, a hyphen, a run of other than twenty-two letters and digits in front of the second underscore, or a character outside hexadecimal behind it, all end the reading, so text as it is ordinarily written is not affected. A longer run of hexadecimal is a token with something written after it, and the token alone is redacted.

Its name is "circleci-api-token".

func CircleCIPatterns added in v0.1.0

func CircleCIPatterns() []Pattern

CircleCIPatterns returns every built-in pattern that locates a credential CircleCI issues.

The returned slice is freshly allocated and may be modified by the caller.

func CloudflareAPIKey

func CloudflareAPIKey() Pattern

CloudflareAPIKey locates Cloudflare API keys: the prefix cfk_, the forty characters of the secret behind it and the eight hexadecimal digits of the checksum behind that — fifty-two characters. One key carries everything the user who holds it can do, over every account and zone they can reach, and nothing in it says otherwise.

A key is located wherever it is written, with no word boundary either side, and exactly fifty-two characters of it are. So text of that shape is redacted whether or not Cloudflare issued it. A space, a hyphen, an underscore behind the prefix, a letter past f in the last eight characters or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "cloudflare-api-key".

func CloudflareAPIToken

func CloudflareAPIToken() Pattern

CloudflareAPIToken locates Cloudflare API tokens: the prefix cfut_ of a token a user owns or cfat_ of one an account owns, the forty characters of the secret behind it and the eight hexadecimal digits of the checksum behind that — fifty-three characters either way. One string serves every permission a token can be scoped to and every account and zone it can be scoped over, so nothing in a token says what it is allowed to do.

A token is located wherever it is written, with no word boundary either side, and exactly fifty-three characters of it are. So text of that shape is redacted whether or not Cloudflare issued it. A space, a hyphen, an underscore behind the prefix, a letter past f in the last eight characters or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "cloudflare-api-token".

func CloudflarePatterns

func CloudflarePatterns() []Pattern

CloudflarePatterns returns every built-in pattern that locates a credential Cloudflare issues.

The returned slice is freshly allocated and may be modified by the caller.

func CratesIOPatterns added in v0.1.0

func CratesIOPatterns() []Pattern

CratesIOPatterns returns every built-in pattern that locates a credential crates.io issues.

The returned slice is freshly allocated and may be modified by the caller.

func CratesIOToken added in v0.1.0

func CratesIOToken() Pattern

CratesIOToken locates the tokens crates.io issues: the API tokens a user creates on the settings page and cargo authenticates with, which are the prefix cio and thirty-two alphanumeric characters, and the short-lived access tokens Trusted Publishing mints in a CI job, which are the prefix cio_tp_ and thirty-two more. Thirty-five characters and thirty-nine.

A token is located wherever it is written, with no word boundary either side, and exactly thirty-five characters of an API token or thirty-nine of a Trusted Publishing one are. So text of that shape is redacted whether or not crates.io issued it. A character that is neither a letter nor a digit ends the reading, so text as it is ordinarily written is not affected.

Its name is "crates-io-token".

func DatabricksOAuthClientSecret added in v0.1.0

func DatabricksOAuthClientSecret() Pattern

DatabricksOAuthClientSecret locates Databricks OAuth client secrets: the prefix dose and the thirty-two lowercase hexadecimal characters behind it, thirty-six characters altogether. One string serves the secret a service principal is issued for the machine-to-machine flow and the secret a registered OAuth application is given for the user-to-machine one, so nothing in a secret says which of the two issued it.

A secret is located wherever it is written, with no word boundary either side, and exactly thirty-six characters of it are. So text of that shape is redacted whether or not Databricks issued it. An uppercase letter, a character outside hexadecimal or a run of fewer than thirty-two characters ends the reading, so text as it is ordinarily written is not affected. A longer run is a secret with something written after it, and the secret alone is redacted.

Its name is "databricks-oauth-client-secret".

func DatabricksPatterns added in v0.1.0

func DatabricksPatterns() []Pattern

DatabricksPatterns returns every built-in pattern that locates a credential Databricks issues.

The returned slice is freshly allocated and may be modified by the caller.

func DatabricksPersonalAccessToken added in v0.1.0

func DatabricksPersonalAccessToken() Pattern

DatabricksPersonalAccessToken locates Databricks personal access tokens: the prefix dapi and the thirty-two lowercase hexadecimal characters behind it — thirty-six characters altogether — followed where one is written by a hyphen and one digit, which brings such a token to thirty-eight. One string serves the tokens a workspace user creates and the tokens a service principal is issued, so nothing in a token says which of the two it authenticates as.

A token is located wherever it is written, with no word boundary either side, and exactly thirty-six characters of it are, or thirty-eight where the hyphen and the digit stand. So text of that shape is redacted whether or not Databricks issued it. An uppercase letter, a character outside hexadecimal or a run of fewer than thirty-two characters ends the reading, so text as it is ordinarily written is not affected. A longer run is a token with something written after it, and the token alone is redacted.

Its name is "databricks-personal-access-token".

func DigitalOceanPatterns added in v0.1.0

func DigitalOceanPatterns() []Pattern

DigitalOceanPatterns returns every built-in pattern that locates a credential DigitalOcean issues.

The returned slice is freshly allocated and may be modified by the caller.

func DigitalOceanToken added in v0.1.0

func DigitalOceanToken() Pattern

DigitalOceanToken locates the tokens DigitalOcean issues in the format it prefixes: the personal access tokens generated in the control panel (dop_v1_), the access tokens an application receives from the OAuth flow (doo_v1_) and the refresh tokens handed out beside them (dor_v1_), each with sixty-four lowercase hexadecimal characters behind it — seventy-one characters altogether.

A token is located wherever it is written, with no word boundary either side, and exactly seventy-one characters of it are. So text of that shape is redacted whether or not DigitalOcean issued it. A space, an uppercase letter, a letter past f or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "digitalocean-token".

func DockerPatterns added in v0.1.0

func DockerPatterns() []Pattern

DockerPatterns returns every built-in pattern that locates a credential Docker issues.

The returned slice is freshly allocated and may be modified by the caller.

func DockerPersonalAccessToken added in v0.1.0

func DockerPersonalAccessToken() Pattern

DockerPersonalAccessToken locates Docker personal access tokens: the prefix dckr_pat_ and twenty-seven base64url characters behind it — thirty-six characters altogether.

A token is located wherever it is written, with no word boundary either side, and exactly thirty-six characters of it are. So text of that shape is redacted whether or not Docker issued it. A space, a dot, a character outside the alphabet or an uppercase prefix ends the reading, so text as it is ordinarily written is not affected. A longer run of the alphabet is a token with something written after it, and the token alone is redacted.

Its name is "docker-personal-access-token".

func DopplerAuthToken added in v0.1.0

func DopplerAuthToken() Pattern

DopplerAuthToken locates the auth tokens Doppler issues: the CLI token, the personal token, the service token, the service account token, the service account identity token, the SCIM token and the audit token, each written as dp, the two to five characters naming its kind and forty to forty-four letters and digits, with a full stop between each part. A service token may carry the name of the environment it was cut for between its prefix and its body, which no other kind does.

A token is located wherever it is written, with no word boundary either side, and no more than forty-four characters of a body are. So text of that shape is redacted whether or not Doppler issued it. A space, a full stop, a hyphen or a body shorter than forty characters ends the reading, so text as it is ordinarily written is not affected.

Its name is "doppler-auth-token".

func DopplerPatterns added in v0.1.0

func DopplerPatterns() []Pattern

DopplerPatterns returns every built-in pattern that locates a credential Doppler issues.

The returned slice is freshly allocated and may be modified by the caller.

func DynatracePatterns added in v0.2.0

func DynatracePatterns() []Pattern

DynatracePatterns returns every built-in pattern that locates a credential Dynatrace issues.

The returned slice is freshly allocated and may be modified by the caller.

func DynatraceToken added in v0.2.0

func DynatraceToken() Pattern

DynatraceToken locates the tokens Dynatrace issues in the format it publishes: the three characters dt0, one letter and two digits naming the token type, then a full stop, the twenty-four characters of the public identifier, a second full stop and the sixty-four characters of the secret — ninety-six characters altogether.

A token is located wherever it is written, with no word boundary either side, and exactly ninety-six characters of it are. So text of that shape is redacted whether or not Dynatrace issued it. A space, a lowercase letter in either portion, a full stop out of place or a portion of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "dynatrace-token".

func FlyIOAccessToken added in v0.2.0

func FlyIOAccessToken() Pattern

FlyIOAccessToken locates the access tokens Fly.io issues: one of the labels fm2_, fm1r_ and fm1a_, and behind it a body of sixty-four characters or more written in standard base64 and closing with the padding base64 calls for, redacted to the end of the run it stands in. The padding counts toward the sixty-four. Every token Fly.io mints today carries fm2_, whatever it was scoped to — a personal access token, a deploy token cut for one app, an org token, an SSH token or a machine-exec token — and a token authenticates the API within the scope it was created with.

A token is located wherever it is written, with no word boundary either side, and with or without the FlyV1 scheme it is sent under. So text of that shape is redacted whether or not Fly.io issued it. A space, an underscore, a character outside the alphabet or a body of fewer than sixty-four characters ends the reading, so text as it is ordinarily written is not affected. Where the run carries on past the sixty-fourth character, it is redacted to its end.

Its name is "fly-io-access-token".

func FlyIOPatterns added in v0.2.0

func FlyIOPatterns() []Pattern

FlyIOPatterns returns every built-in pattern that locates a credential Fly.io issues.

The returned slice is freshly allocated and may be modified by the caller.

func GitHubPatterns

func GitHubPatterns() []Pattern

GitHubPatterns returns every built-in pattern that locates a credential GitHub issues.

The returned slice is freshly allocated and may be modified by the caller.

func GitHubToken

func GitHubToken() Pattern

GitHubToken locates GitHub credentials that carry a token prefix: personal access tokens (ghp_, github_pat_), OAuth app access tokens (gho_), GitHub App user and installation access tokens (ghu_, ghs_) and GitHub App refresh tokens (ghr_).

GitHub documents the prefixes but no token length, and changed installation tokens in 2026 from 40 characters to a longer format holding a JWT. This pattern therefore keys on the prefix rather than on an exact length. That longer format is read for installation tokens, which carry it, and for user access tokens, whose format GitHub has said is to change without saying what to; the kinds that announcement leaves out are read for the classic form alone.

Its name is "github-token".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.GitHubToken()))

	fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
}
Output:
GITHUB_TOKEN=****************************************

func GitLabPatterns

func GitLabPatterns() []Pattern

GitLabPatterns returns every built-in pattern that locates a credential GitLab issues.

The returned slice is freshly allocated and may be modified by the caller.

func GitLabToken

func GitLabToken() Pattern

GitLabToken locates GitLab credentials that carry a token prefix: personal, project, group and impersonation access tokens (glpat-), OAuth application secrets (gloas-), deploy tokens (gldt-), runner authentication tokens (glrt- and glrtr-), CI/CD job tokens (glcbt-), pipeline trigger tokens (glptt-), feed tokens (glft-), incoming mail tokens (glimt-), agent tokens for Kubernetes (glagent-), SCIM OAuth tokens (glsoat-) and feature flags client tokens (glffct-).

Both shapes a body is written in are read. The classic one is a count of base64url characters that kind of token has carried since GitLab gave it a prefix; the count differs by kind, and a pipeline trigger token is written to either of two. The routable one is what GitLab.com is moving to for Cells: a longer payload carrying the routing information, closed by a dot, the length of that payload and a checksum.

Its name is "gitlab-token".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.GitLabToken()))

	fmt.Println(m.Mask("GITLAB_TOKEN=glpat-0123456789abcdefghij"))
}
Output:
GITLAB_TOKEN=**************************

func GoogleAPIKey

func GoogleAPIKey() Pattern

GoogleAPIKey locates Google API keys: the prefix AIza and thirty-five characters behind it. One string serves every Google API that takes a key rather than a credentialled principal — Maps, YouTube Data, Firebase, the Cloud APIs reaching no private user data, and the Gemini API among them — so a key says which project it bills and not which API it was made for.

A key is located wherever it is written, with no word boundary either side, and exactly thirty-nine characters of it are. So an unbroken run of thirty-nine base64url characters opening with AIza is redacted whether or not Google issued it. A space, a dot or a slash ends the run and no word is spelled AIza, so text as it is ordinarily written is not affected.

Its name is "google-api-key".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.GoogleAPIKey()))

	fmt.Println(m.Mask("GOOGLE_API_KEY=AIza0123456789abcdefghijklmnopqrstuvwxy"))
}
Output:
GOOGLE_API_KEY=***************************************

func GooglePatterns

func GooglePatterns() []Pattern

GooglePatterns returns every built-in pattern that locates a credential Google issues.

The returned slice is freshly allocated and may be modified by the caller.

func GrafanaPatterns

func GrafanaPatterns() []Pattern

GrafanaPatterns returns every built-in pattern that locates a credential Grafana issues.

The returned slice is freshly allocated and may be modified by the caller.

func GrafanaServiceAccountToken

func GrafanaServiceAccountToken() Pattern

GrafanaServiceAccountToken locates Grafana service account tokens: the prefix glsa_, the thirty-two characters of the secret, an underscore, and the eight hexadecimal digits of the checksum behind it — forty-six characters altogether. One string serves every service account Grafana issues a token for, whatever role it carries and whichever stack it belongs to, so nothing in a token says what it is allowed to do.

A token is located wherever it is written, with no word boundary either side, and exactly forty-six characters of it are. So text of that shape is redacted whether or not Grafana issued it. A space, a hyphen where the underscore belongs, or a secret of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "grafana-service-account-token".

func GroqAPIKey added in v0.2.0

func GroqAPIKey() Pattern

GroqAPIKey locates Groq API keys: the prefix gsk_ and the fifty or more letters and digits behind it, redacted to the end of the run they stand in. Every key any published ruleset carries is fifty-two characters behind the prefix, fifty-six altogether. One string serves every model Groq serves and every endpoint it is served at, so nothing in a key says what it may be spent on.

A key is located wherever it is written, with no word boundary either side. So text of that shape is redacted whether or not Groq issued it. A space, a hyphen, an underscore or a run of fewer than fifty letters and digits ends the reading, so text as it is ordinarily written is not affected. Where the run carries on past the fiftieth character, it is redacted to its end.

Its name is "groq-api-key".

func GroqPatterns added in v0.2.0

func GroqPatterns() []Pattern

GroqPatterns returns every built-in pattern that locates a credential Groq issues.

The returned slice is freshly allocated and may be modified by the caller.

func HCPTerraformAPIToken added in v0.2.0

func HCPTerraformAPIToken() Pattern

HCPTerraformAPIToken locates the API tokens HCP Terraform issues: fourteen letters and digits, the nine characters .atlasv1. and sixty-seven letters and digits behind them — ninety characters altogether. One format carries every kind of token the service hands out — the user token a person authenticates with, the team token a pipeline runs plans and applies under, the organization token that manages teams and workspaces, and the token an agent pool authenticates with — so nothing in a token says which of those it is.

A token is located wherever it is written, with no word boundary either side, and exactly ninety characters of it are. So text of that shape is redacted whether or not HashiCorp issued it. A space, a hyphen, an underscore, a full stop out of place or a portion of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "hcp-terraform-api-token".

func HashiCorpPatterns

func HashiCorpPatterns() []Pattern

HashiCorpPatterns returns every built-in pattern that locates a credential HashiCorp issues.

The returned slice is freshly allocated and may be modified by the caller.

func HashiCorpVaultToken

func HashiCorpVaultToken() Pattern

HashiCorpVaultToken locates HashiCorp Vault tokens: the prefix hvs., hvb. or hvr. and the characters behind it. The three name the three kinds of token Vault issues — a service token, a batch token and a recovery token — and nothing else in the string says them apart, nor what any of them is allowed to do, which is carried by the policies attached to the token inside Vault.

A token is located wherever it is written, with no word boundary either side, and is redacted from its prefix to the end of the run it stands in. So a token written against a word character keeps its span, and a character of the token's own alphabet written straight after a token is redacted with it.

What Vault states of the format is a prefix and "24 or more" characters behind it, and this pattern reads exactly that, so text of that shape is redacted whether or not Vault issued it. The shape is reachable by writing as well as by encoding, which is worth knowing before this pattern is switched on over source code: a name written in dot-separated segments is located wherever the segment behind an hvs, hvb or hvr runs to twenty-four characters, so both hvs.example-host-name-of-that-length and a method call on a receiver named hvs are redacted from the h to the end of that segment. What holds ordinary text back is the twenty-four unbroken characters and nothing else.

Its name is "hashicorp-vault-token".

func HerokuAPIToken added in v0.1.0

func HerokuAPIToken() Pattern

HerokuAPIToken locates Heroku API tokens: the prefix HRKU- and either the sixty base64url characters Heroku writes one with now or the UUID it wrote one with before — sixty-five characters or forty-one.

A token is located wherever it is written, with no word boundary either side, and exactly as many characters of it are as the reading it matched comes to. So text of that shape is redacted whether or not Heroku issued it. A space, a dot, an equals sign or a run of fewer than sixty characters ends the longer reading, and anything but a UUID behind the prefix ends the shorter one, so text as it is ordinarily written is not affected. A longer run is a token with something written after it, and the token alone is redacted.

Its name is "heroku-api-token".

func HerokuPatterns added in v0.1.0

func HerokuPatterns() []Pattern

HerokuPatterns returns every built-in pattern that locates a credential Heroku issues.

The returned slice is freshly allocated and may be modified by the caller.

func HuggingFacePatterns added in v0.1.0

func HuggingFacePatterns() []Pattern

HuggingFacePatterns returns every built-in pattern that locates a credential Hugging Face issues.

The returned slice is freshly allocated and may be modified by the caller.

func HuggingFaceUserAccessToken added in v0.1.0

func HuggingFaceUserAccessToken() Pattern

HuggingFaceUserAccessToken locates Hugging Face user access tokens: the prefix hf_ and the thirty-four letters and digits behind it — thirty-seven characters altogether. One string serves every role a token is issued under, so nothing in a token says whether it may read a private repository, write to one, or reach only the resources a fine-grained token was scoped to.

A token is located wherever it is written, with no word boundary either side, and exactly thirty-seven characters of it are. So text of that shape is redacted whether or not Hugging Face issued it. A space, a hyphen, an underscore or a run of fewer than thirty-four letters and digits ends the reading, so text as it is ordinarily written is not affected. A longer run is a token with something written after it, and the token alone is redacted.

Its name is "huggingface-user-access-token".

func JWT

func JWT() Pattern

JWT locates JSON Web Tokens in compact serialization: a base64url encoded header, followed by the two segments of a signed token or the four of an encrypted one, separated by dots.

A header is read for the marks RFC 7515 and RFC 7516 require of one, namely a JSON object naming an algorithm in alg, and enc where the token is encrypted. Text that carries none of them is left alone.

The header may be written with a space between the brace and the first member name, which JSON allows though the compact JSON an encoder emits carries none. One written with a tab, a carriage return or a newline there is not located.

Its name is "jwt".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.JWT()))

	fmt.Println(m.Mask("Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMifQ.0123456789abcdef"))
}
Output:
Authorization: Bearer ************************************************************************

func LinearAPIKey

func LinearAPIKey() Pattern

LinearAPIKey locates Linear personal API keys: the prefix lin_api_ and the characters behind it. One shape serves every key Linear issues — a key carries the permissions of whoever created it and may be narrowed to reading alone or to a single team, so nothing in the string says what it is allowed to do.

A key is located wherever it is written, with no word boundary either side, and is redacted from its lin_api_ to the end of the run it stands in. So a key written against a word character keeps its span, and a character of the key's own alphabet written straight after a key is redacted with it.

Its name is "linear-api-key".

func LinearPatterns

func LinearPatterns() []Pattern

LinearPatterns returns every built-in pattern that locates a credential Linear issues.

The returned slice is freshly allocated and may be modified by the caller.

func MustRegexp

func MustRegexp(name, expr string) Pattern

MustRegexp returns what Regexp returns for name and expr, and panics where Regexp reports an error:

var internal = mask.MustRegexp("internal-token", `INT-[0-9a-f]{32}`)

It is what an expression written into the source is built with, where an invalid one is a bug in the program rather than something a caller could act on. Regexp is what an expression that arrives at run time — out of a configuration file, or off a flag — is built with.

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(
		mask.MustRegexp("internal-token", `INT-[0-9a-f]{32}`),
	))

	fmt.Println(m.Mask("token: INT-0123456789abcdef0123456789abcdef"))
}
Output:
token: ************************************
Example (MaskGroup)
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(
		mask.MustRegexp("user-id", `user_id=(?P<mask>\d+)`),
	))

	fmt.Println(m.Mask("user_id=12345 name=alice"))
}
Output:
user_id=***** name=alice

func NPMAccessToken

func NPMAccessToken() Pattern

NPMAccessToken locates npm access tokens: the prefix npm_ and thirty-six characters behind it. One shape serves every token the registry issues in this format — the granular access tokens npmjs.com creates today, and the classic tokens, read-only, automation and publish alike, created in it until npm disabled them — so a token says which registry it authenticates against and not what it is allowed to do there.

A token is located wherever it is written, with no word boundary either side, and is redacted from its npm_ to the end of the run it stands in. So a token written against a word character keeps its span, and a character of the token's own alphabet written straight after a token is redacted with it.

Its name is "npm-access-token".

func NPMPatterns

func NPMPatterns() []Pattern

NPMPatterns returns every built-in pattern that locates a credential npm issues.

The returned slice is freshly allocated and may be modified by the caller.

func NewPattern

func NewPattern(name string, find func(src string) (spans []Span, retain int)) Pattern

NewPattern returns a Pattern that reports name as its name and locates values with find:

mask.NewPattern("high-entropy", func(src string) ([]mask.Span, int) {
	// ...
	return spans, len(src)
})

find must be safe for concurrent use by multiple goroutines, and owes what Pattern.Find says about both of its results.

Example
package main

import (
	"fmt"
	"strings"

	"github.com/koki-develop/mask-go"
)

func main() {
	secret := "s3cr3t-value"

	p := mask.NewPattern("shared-secret", func(src string) ([]mask.Span, int) {
		var spans []mask.Span
		for i := 0; ; {
			j := strings.Index(src[i:], secret)
			if j < 0 {
				break
			}
			spans = append(spans, mask.Span{Start: i + j, End: i + j + len(secret)})
			// One byte past where this one began, not past where it ended: a
			// value written inside another is a value, and a scan resuming
			// past the match would step over it. LookBehind asks for the same
			// thing from the other side — where a scan resumes must not depend
			// on how much of the text in front of it it was shown.
			i += j + 1
		}
		// The value is one fixed width, so text further than a width from
		// the end of src holds nothing more of it to come.
		return spans, max(0, len(src)-len(secret)+1)
	})

	m := mask.New(mask.WithPatterns(p))

	fmt.Println(m.Mask("password=s3cr3t-value"))
}
Output:
password=************

func NewRelicPatterns added in v0.2.0

func NewRelicPatterns() []Pattern

NewRelicPatterns returns every built-in pattern that locates a credential New Relic issues.

The returned slice is freshly allocated and may be modified by the caller.

func NewRelicUserKey added in v0.2.0

func NewRelicUserKey() Pattern

NewRelicUserKey locates New Relic user keys: the prefix NRAK- and the twenty-seven or more uppercase letters and digits behind it, or the prefix NRAA- and the twenty-seven or more hexadecimal characters behind it, redacted to the end of the run they stand in. Every key anybody has published is twenty-seven characters behind the prefix, thirty-two altogether. A key queries NerdGraph and the REST API as the user it was issued to, across every account that user can see, so what one reaches is whatever its user was granted rather than one account's data.

A key is located wherever it is written, with no word boundary either side. So text of that shape is redacted whether or not New Relic issued it. A space, an underscore, a second hyphen or a run of fewer than twenty-seven characters of the kind's own alphabet ends the reading, so text as it is ordinarily written is not affected. Where the run carries on past the twenty-seventh character, it is redacted to its end.

Its name is "newrelic-user-key".

func NotionAPIToken

func NotionAPIToken() Pattern

NotionAPIToken locates Notion API tokens: the prefix ntn_ and the forty-six characters behind it, or the prefix secret_ and the forty-three behind that — fifty characters either way. One string serves every kind of token the Public API takes, the static token of an internal connection, the OAuth access token of a public one and a personal access token alike, so nothing in a token says what it authenticates as.

A token is located wherever it is written, with no word boundary either side, and exactly fifty characters of it are. So text of that shape is redacted whether or not Notion issued it. A space, a hyphen, a further underscore or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "notion-api-token".

func NotionPatterns

func NotionPatterns() []Pattern

NotionPatterns returns every built-in pattern that locates a credential Notion issues.

The returned slice is freshly allocated and may be modified by the caller.

func OnePasswordPatterns added in v0.1.0

func OnePasswordPatterns() []Pattern

OnePasswordPatterns returns every built-in pattern that locates a credential 1Password issues.

The returned slice is freshly allocated and may be modified by the caller.

func OnePasswordServiceAccountToken added in v0.1.0

func OnePasswordServiceAccountToken() Pattern

OnePasswordServiceAccountToken locates 1Password service account tokens: the prefix ops_, and behind it the account's credentials serialized into a JSON object and written out in base64url. One string serves every service account, whichever vaults it was granted and whatever it may do with them, so nothing in a token says what it reaches.

A token is located wherever it is written, with no word boundary either side, and is redacted from its ops_ to the end of the run it stands in. So a token written against a word character keeps its span, and a character of the token's own alphabet written straight after a token is redacted with it.

Its name is "1password-service-account-token".

func OpenAIAPIKey

func OpenAIAPIKey() Pattern

OpenAIAPIKey locates OpenAI API keys: the project keys the platform issues today (sk-proj-), the keys of a service account (sk-svcacct-), the keys of the Admin API (sk-admin-) and the user keys issued before projects existed (sk-, and the sk-None- written beside them). Every one of them is written the same way — the prefix sk-, a run of random characters, the marker T3BlbkFJ, and more random characters — and it is that marker, rather than the prefix a kind is named for, that this pattern is anchored on.

A key is located wherever it is written, with no word boundary either side, and is redacted from its sk- to the end of the run it stands in. So a key written against a word character keeps its span, and a character of the key's own alphabet written straight after a key is redacted with it.

Its name is "openai-api-key".

func OpenAIPatterns

func OpenAIPatterns() []Pattern

OpenAIPatterns returns every built-in pattern that locates a credential OpenAI issues.

The returned slice is freshly allocated and may be modified by the caller.

func OpenRouterAPIKey

func OpenRouterAPIKey() Pattern

OpenRouterAPIKey locates OpenRouter API keys: the prefix sk-or-v1- and the sixty-four hexadecimal digits behind it — seventy-three characters altogether. One string serves every model OpenRouter routes to and every provider behind them, so nothing in a key says what it may be spent on.

A key is located wherever it is written, with no word boundary either side, and exactly seventy-three characters of it are. So text of that shape is redacted whether or not OpenRouter issued it. A space, a letter past f, or a run of fewer than sixty-four hexadecimal digits ends the reading, so text as it is ordinarily written is not affected. A longer run is a key with something written after it, and the key alone is redacted.

Its name is "openrouter-api-key".

func OpenRouterPatterns

func OpenRouterPatterns() []Pattern

OpenRouterPatterns returns every built-in pattern that locates a credential OpenRouter issues.

The returned slice is freshly allocated and may be modified by the caller.

func PaddleAPIKey added in v0.2.0

func PaddleAPIKey() Pattern

PaddleAPIKey locates the API keys Paddle issues: the four characters pdl_, the environment written as live or sdbx, the eight characters _apikey_, then twenty-six lowercase letters and digits, an underscore, twenty-two letters and digits, a second underscore and three more — sixty-nine characters altogether.

A key is located wherever it is written, with no word boundary either side, and exactly sixty-nine characters of it are. So text of that shape is redacted whether or not Paddle issued it. A space, an underscore out of place, an uppercase letter in the first segment or a segment of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "paddle-api-key".

func PaddlePatterns added in v0.2.0

func PaddlePatterns() []Pattern

PaddlePatterns returns every built-in pattern that locates a credential Paddle issues.

The returned slice is freshly allocated and may be modified by the caller.

func PlanetScalePatterns added in v0.1.0

func PlanetScalePatterns() []Pattern

PlanetScalePatterns returns every built-in pattern that locates a credential PlanetScale issues.

The returned slice is freshly allocated and may be modified by the caller.

func PlanetScaleToken added in v0.1.0

func PlanetScaleToken() Pattern

PlanetScaleToken locates the tokens PlanetScale issues in the format it prefixes: the service token an organization is given (pscale_tkn_), the access token an application receives from the OAuth flow (pscale_oauth_) and the refresh token handed out beside it (pscale_oauth_refresh_), each with forty-three base64url characters behind it — fifty-four, fifty-six and sixty-four characters altogether.

A token is located wherever it is written, with no word boundary either side, and exactly as many characters of it are as its own prefix and the count come to. So text of that shape is redacted whether or not PlanetScale issued it. A space, a dot, an equals sign or a run of fewer than forty-three characters ends the reading, so text as it is ordinarily written is not affected. A longer run is a token with something written after it, and the token alone is redacted.

Its name is "planetscale-token".

func PostHogPatterns added in v0.2.0

func PostHogPatterns() []Pattern

PostHogPatterns returns every built-in pattern that locates a credential PostHog issues.

The returned slice is freshly allocated and may be modified by the caller.

func PostHogPersonalAPIKey added in v0.2.0

func PostHogPersonalAPIKey() Pattern

PostHogPersonalAPIKey locates PostHog personal API keys: the prefix phx_ and the forty-one or more letters and digits behind it, redacted to the end of the run they stand in. One key reads and writes the projects and organizations its owner can reach, within the scopes and the teams it was cut for, so it is worth as much as the access of the person who made it.

A key is located wherever it is written, with no word boundary either side. So text of that shape is redacted whether or not PostHog issued it. A space, a hyphen, an underscore or a run of fewer than forty-one letters and digits ends the reading, so text as it is ordinarily written is not affected. Where the run carries on past the forty-first character, it is redacted to its end.

Its name is "posthog-personal-api-key".

func PostmanAPIKey added in v0.1.0

func PostmanAPIKey() Pattern

PostmanAPIKey locates Postman API keys: the prefix PMAK-, twenty-four characters, a hyphen and thirty-four more — sixty-four characters altogether. One key carries the whole of the account that created it, over every workspace, collection and environment that account can reach, and nothing in it says otherwise.

A key is located wherever it is written, with no word boundary either side, and exactly sixty-four characters of it are. So text of that shape is redacted whether or not Postman issued it. A space, an underscore, a dot, a segment of the wrong length or a hyphen standing anywhere but the twenty-fifth character behind the prefix ends the reading, so text as it is ordinarily written is not affected.

Its name is "postman-api-key".

func PostmanPatterns added in v0.1.0

func PostmanPatterns() []Pattern

PostmanPatterns returns every built-in pattern that locates a credential Postman issues.

The returned slice is freshly allocated and may be modified by the caller.

func PrivateKey

func PrivateKey() Pattern

PrivateKey locates private keys written in the armor RFC 7468 lays out: a line opening with five dashes, BEGIN and a label, the base64 of the key behind it, and a closing line naming the same label. Every label whose last words are PRIVATE KEY is read, so the PKCS#8 key of RFC 7468, the encrypted PKCS#8 key beside it, the PKCS#1 and EC and DSA keys OpenSSL writes, the OPENSSH key ssh-keygen writes and the PGP PRIVATE KEY BLOCK of RFC 9580 are all located, as is a label no such document has been written for yet.

The whole block is redacted, the two boundary lines included, not the base64 alone. A block cut short — a key a log truncated before its closing line — is located as far as its last whole line of base64, so a truncation landing inside a line leaves that line, and one landing inside the first leaves the key.

A block written into JSON or into an environment assignment, where the line breaks stand as the two characters \ and n rather than as line breaks, is located as one written across lines is, and so is one indented under a name in YAML. Text escaped twice over is not: a block whose line breaks are written \\n is located nowhere.

Its name is "private-key".

func PulumiAccessToken added in v0.1.0

func PulumiAccessToken() Pattern

PulumiAccessToken locates Pulumi access tokens: the prefix pul- and the forty lowercase hexadecimal characters behind it — forty-four characters altogether. One string serves the token a user creates for themselves, the token an organization is issued and the token a team is issued, so nothing in a token says which of the three it authenticates as.

A token is located wherever it is written, with no word boundary either side, and exactly forty-four characters of it are. So text of that shape is redacted whether or not Pulumi issued it. A space, an uppercase letter, a letter past f or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "pulumi-access-token".

func PulumiPatterns added in v0.1.0

func PulumiPatterns() []Pattern

PulumiPatterns returns every built-in pattern that locates a credential Pulumi issues.

The returned slice is freshly allocated and may be modified by the caller.

func PyPIAPIToken

func PyPIAPIToken() Pattern

PyPIAPIToken locates PyPI API tokens: the upload tokens pypi.org issues, the ones test.pypi.org issues beside them, and the short-lived ones minted for a Trusted Publisher. Every one of them is written the same way — the prefix pypi-, and behind it a macaroon serialized into base64url — and it is that serialization, rather than the index which issued it, that this pattern is anchored on.

A token is located wherever it is written, with no word boundary either side, and is redacted from its pypi- to the end of the run it stands in. So a token written against a word character keeps its span, and a character of the token's own alphabet written straight after a token is redacted with it.

Its name is "pypi-api-token".

func PyPIPatterns

func PyPIPatterns() []Pattern

PyPIPatterns returns every built-in pattern that locates a credential PyPI issues.

The returned slice is freshly allocated and may be modified by the caller.

func Regexp added in v0.2.0

func Regexp(name, expr string) (Pattern, error)

Regexp returns a Pattern backed by expr, and an error where expr is not one regexp.Compile accepts. The syntax is Go's, and the error is the one regexp.Compile reports, handed back as it stands.

The whole match is redacted, unless expr contains a capture group named "mask", in which case only that group is:

// "Authorization: Bearer abc123" -> "Authorization: Bearer ******"
mask.Regexp("bearer-token", `Bearer (?P<mask>[\w.~+/-]+=*)`)

Go admits the name more than once, which is what a marker written in variants asks for — one branch of an alternation apiece — and every group named "mask" that took part in the match is redacted. A match where none of them did is redacted nowhere.

Where the expression has a ceiling on its width, every match it admits is located, including the ones that begin inside another: forty characters of hexadecimal written against forty more are redacted whole, where Go's FindAll resumes past each match it takes and would leave the second forty. What that costs is a second run of the expression at each position inside a match where one could open — nothing where matches are rare, and what a caller pays for an expression matching densely and never far.

What such a pattern settles, in the sense Pattern.Find gives the word, is worked out from expr two ways. A match can be no wider than the expression can match, so everything more than that many bytes in front of the end of the text is settled; and a match opens with whatever literal the expression opens with, so the text in front of the first place that literal could stand holds no match and is settled whatever the width.

An expression that can match text of any width — one written with * or + or an open repetition — has only the second of those, and one naming no literal to open with has neither and settles nothing at all. A Reader or a Writer holds what no pattern has settled, and gives up holding at WithMaxRetained by redacting what it holds, so such an expression turns everything from a match's opening onwards into a redaction once the limit is reached — whether or not a match was ever written there. Write a counted repetition for a pattern a stream is to mask with: INT-[0-9a-f]{32} rather than INT-[0-9a-f]+.

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	// An expression that arrives at run time — off a flag, or out of a
	// configuration file — is where the error is worth having.
	p, err := mask.Regexp("internal-token", `INT-[0-9a-f]{32}`)
	if err != nil {
		fmt.Println(err)
		return
	}
	m := mask.New(mask.WithPatterns(p))

	fmt.Println(m.Mask("token: INT-0123456789abcdef0123456789abcdef"))
}
Output:
token: ************************************

func ReplicateAPIToken added in v0.2.0

func ReplicateAPIToken() Pattern

ReplicateAPIToken locates Replicate API tokens: the prefix r8_ and the thirty-seven letters and digits behind it — forty characters altogether. One string authenticates every request to Replicate's HTTP API, so nothing in a token says what it may be spent on or which of an owner's tokens it is.

A token is located wherever it is written, with no word boundary either side, and exactly forty characters of it are. So text of that shape is redacted whether or not Replicate issued it. A space, a hyphen, an underscore or a run of fewer than thirty-seven letters and digits ends the reading, so text as it is ordinarily written is not affected. A longer run is a token with something written after it, and the token alone is redacted.

Its name is "replicate-api-token".

func ReplicatePatterns added in v0.2.0

func ReplicatePatterns() []Pattern

ReplicatePatterns returns every built-in pattern that locates a credential Replicate issues.

The returned slice is freshly allocated and may be modified by the caller.

func ResendAPIKey added in v0.2.0

func ResendAPIKey() Pattern

ResendAPIKey locates Resend API keys: the prefix re_, eight letters and digits, an underscore and the twenty-four letters and digits that follow — thirty-six characters altogether. One string authenticates every request to the Resend API and is the password its SMTP endpoint takes as well, so whoever holds one can send mail from the account's verified domains.

A key is located wherever it is written, with no word boundary either side, and exactly thirty-six characters of it are. So text of that shape is redacted whether or not Resend issued it. A space, a hyphen, a segment of the wrong length or an underscore anywhere but the two places one stands ends the reading, so text as it is ordinarily written is not affected. A longer run is a key with something written after it, and the key alone is redacted.

Its name is "resend-api-key".

func ResendPatterns added in v0.2.0

func ResendPatterns() []Pattern

ResendPatterns returns every built-in pattern that locates a credential Resend issues.

The returned slice is freshly allocated and may be modified by the caller.

func RubyGemsAPIKey

func RubyGemsAPIKey() Pattern

RubyGemsAPIKey locates RubyGems.org API keys: the prefix rubygems_ and the forty-eight lowercase hexadecimal characters behind it — fifty-seven characters altogether. One string serves every key RubyGems.org issues, whatever scopes it carries and whichever gem it is scoped to, so nothing in a key says what it is allowed to do.

A key is located wherever it is written, with no word boundary either side, and exactly fifty-seven characters of it are. So text of that shape is redacted whether or not RubyGems.org issued it. A space, an uppercase letter, a letter past f or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "rubygems-api-key".

func RubyGemsPatterns

func RubyGemsPatterns() []Pattern

RubyGemsPatterns returns every built-in pattern that locates a credential RubyGems issues.

The returned slice is freshly allocated and may be modified by the caller.

func SendGridAPIKey

func SendGridAPIKey() Pattern

SendGridAPIKey locates Twilio SendGrid API keys: the prefix SG., the twenty-two characters that identify the key, a dot, and the forty-three characters of the secret behind it. One string serves every access level SendGrid issues — full access, custom access and billing access — so nothing in a key says what it is allowed to do.

A key is located wherever it is written, with no word boundary either side, and exactly sixty-nine characters of it are. So text of that shape is redacted whether or not SendGrid issued it. A space, a hyphen where the dot belongs, or a segment of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "sendgrid-api-key".

func SendGridPatterns

func SendGridPatterns() []Pattern

SendGridPatterns returns every built-in pattern that locates a credential SendGrid issues.

The returned slice is freshly allocated and may be modified by the caller.

func SentryAuthToken

func SentryAuthToken() Pattern

SentryAuthToken locates the Sentry auth tokens that carry a token type prefix: user auth tokens (sntryu_), user application tokens (sntrya_), internal integration tokens (sntryi_) and organization auth tokens (sntrys_).

Two shapes a body is written in are read. Three of the four kinds carry thirty-two random bytes written as hexadecimal, which is sixty-four characters. The organization token carries a shape of its own: the base64 of a JSON payload naming the organization and the region it is served from, an underscore, and the base64 of thirty-two random bytes behind it.

A token is located wherever it is written, with no word boundary either side, and exactly as many characters of it are as the kind is written to. So text of that shape is redacted whether or not Sentry issued it. A space, a character outside the alphabet, or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "sentry-auth-token".

func SentryPatterns

func SentryPatterns() []Pattern

SentryPatterns returns every built-in pattern that locates a credential Sentry issues.

The returned slice is freshly allocated and may be modified by the caller.

func ShopifyAccessToken added in v0.1.0

func ShopifyAccessToken() Pattern

ShopifyAccessToken locates the access tokens Shopify issues in the format it prefixes: the tokens a public app receives for a shop (shpat_), the tokens a custom app is given (shpca_) and the tokens a private app was issued and a delegate token carries today (shppa_), each with thirty-two hexadecimal characters behind it — thirty-eight characters altogether.

A token is located wherever it is written, with no word boundary either side, and exactly thirty-eight characters of it are. So text of that shape is redacted whether or not Shopify issued it. A space, a letter past f or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "shopify-access-token".

func ShopifyAppSecretKey added in v0.1.0

func ShopifyAppSecretKey() Pattern

ShopifyAppSecretKey locates the secret half of a Shopify app's client credentials: the prefix shpss_ and the thirty-two hexadecimal characters behind it — thirty-eight characters altogether.

A key is located wherever it is written, with no word boundary either side, and exactly thirty-eight characters of it are. So text of that shape is redacted whether or not Shopify issued it. A space, a letter past f or a body of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "shopify-app-secret-key".

func ShopifyPatterns added in v0.1.0

func ShopifyPatterns() []Pattern

ShopifyPatterns returns every built-in pattern that locates a credential Shopify issues.

The returned slice is freshly allocated and may be modified by the caller.

func SlackPatterns

func SlackPatterns() []Pattern

SlackPatterns returns every built-in pattern that locates a credential Slack issues.

The returned slice is freshly allocated and may be modified by the caller.

func SlackToken

func SlackToken() Pattern

SlackToken locates Slack credentials that carry a token prefix: bot tokens (xoxb-), user tokens (xoxp-), app-level tokens (xapp-), workflow tokens (xwfp-), and the pair token rotation issues — refresh tokens (xoxe-) and the rotatable access tokens a xoxe. prefix puts in front of a bot or user token (xoxe.xoxb-, xoxe.xoxp-).

Slack documents the prefixes and nothing else: no length, no alphabet, no count of the parts a token is written in. So a body is read as the hyphen separated segments every Slack token anyone has published is written in, and a token is one where some segment other than the first is long enough to be the secret such a token ends with and carries a letter as every such secret does. Asking that the secret stand behind a part rather than against the prefix is what keeps a bare digest out. A prefix written against a letter or a digit opens nothing, which is what keeps an identifier ending in one of them from being read as a token.

Its name is "slack-token".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.SlackToken()))

	fmt.Println(m.Mask("SLACK_BOT_TOKEN=xoxb-0123456789ab-0123456789abc-0123456789abcdefghijklmn"))
}
Output:
SLACK_BOT_TOKEN=********************************************************

func SonarQubePatterns added in v0.2.0

func SonarQubePatterns() []Pattern

SonarQubePatterns returns every built-in pattern that locates a credential SonarQube issues.

The returned slice is freshly allocated and may be modified by the caller.

func SonarQubeToken added in v0.2.0

func SonarQubeToken() Pattern

SonarQubeToken locates SonarQube tokens: the prefix squ_ a user token is written with, the sqa_ of a global analysis token, the sqp_ of a project analysis token or the sqb_ of a project badge token, then forty hexadecimal characters — forty-four characters altogether.

A token is located wherever it is written, with no word boundary either side, and exactly forty-four characters of it are. So text of that shape is redacted whether or not SonarQube issued it. A space, a dot, an uppercase prefix or a character outside lowercase hexadecimal ends the reading, so text as it is ordinarily written is not affected. A longer run of hexadecimal is a token with something written after it, and the token alone is redacted.

Its name is "sonarqube-token".

func SourcegraphAccessToken added in v0.1.0

func SourcegraphAccessToken() Pattern

SourcegraphAccessToken locates the access tokens the Sourcegraph GraphQL API takes: the prefix sgp_, an instance identifier and the separator behind it where one is written, and the forty hexadecimal characters that are the token value. A token carrying no identifier is forty-four characters, one carrying the identifier a licensed instance writes is sixty-one, and one carrying the identifier a development instance writes is fifty.

A token is located wherever it is written, with no word boundary either side, and exactly forty-four, fifty or sixty-one characters of it are. So text of that shape is redacted whether or not Sourcegraph issued it. A letter past f, a body of the wrong length or an identifier of neither shape ends the reading, so text as it is ordinarily written is not affected.

Its name is "sourcegraph-access-token".

func SourcegraphPatterns added in v0.1.0

func SourcegraphPatterns() []Pattern

SourcegraphPatterns returns every built-in pattern that locates a credential Sourcegraph issues.

The returned slice is freshly allocated and may be modified by the caller.

func StripePatterns

func StripePatterns() []Pattern

StripePatterns returns every built-in pattern that locates a credential Stripe issues.

The returned slice is freshly allocated and may be modified by the caller.

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	// Every vendor has an accessor of its own, returning the built-in patterns
	// that read what that vendor issues. A vendor with more than one is given
	// whole by it.
	m := mask.New(mask.WithPatterns(mask.StripePatterns()...))

	fmt.Println(m.Mask("secret=sk_live_0123456789abcdef01234567 publishable=pk_live_0123456789abcdef01234567 webhook=whsec_0123456789abcdef0123456789abcdef"))
}
Output:
secret=******************************** publishable=******************************** webhook=**************************************

func StripePublishableKey

func StripePublishableKey() Pattern

StripePublishableKey locates the Stripe publishable API keys a page is initialized with (pk_live_, pk_test_).

A key is located wherever it is written, so long as no letter or digit stands in front of it, and is redacted from its prefix to the end of the run it stands in. So a key written after an underscore, a quote, an equals sign or a space keeps its span, and a letter or a digit written straight after a key is redacted with it.

Stripe says a publishable key is safe to expose: it is embedded in the page it initializes, so a reader who has one has taken nothing. It is a pattern of its own for that reason rather than in spite of it — a caller masking a frontend bundle, a browser console log or a bug report has no reason to redact a value that belongs there and every reason to keep it, since it says which account and which mode the report came from, while a caller masking a configuration dump they are about to share may want it gone. Reaching for StripeSecretKey (builtin_stripe_secret_key.go) alone is how the first caller says so, and reaching for both, or for StripePatterns, is how the second does.

Its name is "stripe-publishable-key".

func StripeSecretKey

func StripeSecretKey() Pattern

StripeSecretKey locates the Stripe API keys that must not be exposed: the restricted keys Stripe now asks a server to use (rk_live_, rk_test_), the unrestricted secret keys (sk_live_, sk_test_) and the organization keys reaching every account under an organization (sk_org_).

A key is located wherever it is written, so long as no letter or digit stands in front of it, and is redacted from its prefix to the end of the run it stands in. So a key written after an underscore, a quote, an equals sign or a space keeps its span, and a letter or a digit written straight after a key is redacted with it.

Its name is "stripe-secret-key".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	// Stripe marks the publishable key safe to expose and the restricted,
	// secret and organization keys not, so a pattern of its own reads each side
	// of that column. Reaching for this one alone redacts the keys that matter
	// and leaves the publishable key, which belongs in the page it initializes.
	m := mask.New(mask.WithPatterns(mask.StripeSecretKey()))

	fmt.Println(m.Mask("secret=sk_live_0123456789abcdef01234567 publishable=pk_live_0123456789abcdef01234567"))
}
Output:
secret=******************************** publishable=pk_live_0123456789abcdef01234567

func StripeWebhookSigningSecret

func StripeWebhookSigningSecret() Pattern

StripeWebhookSigningSecret locates the signing secrets Stripe issues for a webhook endpoint (whsec_): the key a handler computes the HMAC in the Stripe-Signature header with, and the one the Stripe CLI prints when it begins forwarding events to a local endpoint.

A secret is located wherever it is written, with no word boundary either side, and is redacted from its whsec_ to the end of the run it stands in. So a secret written against a word character keeps its span, and a character of the secret's own alphabet written straight after one is redacted with it.

Its name is "stripe-webhook-signing-secret".

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	// The signing secret is no row of Stripe's table of key types: it is
	// issued per endpoint rather than per account, and what it authenticates
	// is Stripe to the reader's server rather than the other way about.
	// Reaching for it alone redacts what verifies Stripe's own signature and
	// leaves the API keys, which the patterns beside it read.
	m := mask.New(mask.WithPatterns(mask.StripeWebhookSigningSecret()))

	fmt.Println(m.Mask("secret=whsec_0123456789abcdef0123456789abcdef key=sk_live_0123456789abcdef01234567"))
}
Output:
secret=************************************** key=sk_live_0123456789abcdef01234567

func SupabaseAccessToken

func SupabaseAccessToken() Pattern

SupabaseAccessToken locates the access tokens the Supabase Management API takes: the personal access token a user creates for themselves, which is the prefix sbp_ and the forty lowercase hexadecimal digits behind it, forty-four characters altogether, and the token an OAuth application is issued in a user's name, which writes oauth_ between the two and so is fifty. Nothing in either says what it is allowed to do — a personal access token carries the whole of the account that created it, and an OAuth issued one the scopes that account approved — so one string serves the Management API either way.

A token is located wherever it is written, with no word boundary either side, and exactly forty-four or fifty characters of it are. So text of that shape is redacted whether or not Supabase issued it. A letter past f, an uppercase one, or a run of the wrong length ends the reading, so text as it is ordinarily written is not affected.

Its name is "supabase-access-token".

func SupabasePatterns

func SupabasePatterns() []Pattern

SupabasePatterns returns every built-in pattern that locates a credential Supabase issues.

The returned slice is freshly allocated and may be modified by the caller.

func SupabasePublishableKey added in v0.1.0

func SupabasePublishableKey() Pattern

SupabasePublishableKey locates the publishable API keys a Supabase client is initialized with: the prefix sb_publishable_ and the thirty-one characters behind it, forty-six characters altogether.

A key is located wherever it is written, with no word boundary either side, and exactly forty-six characters of it are. So text of that shape is redacted whether or not Supabase issued it. A character outside the base64url alphabet, or anything but an underscore twenty-three characters behind the prefix, ends the reading, so text as it is ordinarily written is not affected.

Supabase says a publishable key is safe to expose: it is embedded in the client it initializes, it carries no privilege row level security has not already granted, and the documentation prints it into browser code. It is a pattern of its own for that reason rather than in spite of it — a caller masking a frontend bundle, a browser console log or a bug report has no reason to redact a value that belongs there and every reason to keep it, since it says which project the report came from, while a caller masking a configuration dump they are about to share may want it gone. Reaching for SupabaseSecretKey (builtin_supabase_secret_key.go) alone is how the first caller says so, and reaching for both, or for SupabasePatterns, is how the second does.

Its name is "supabase-publishable-key".

func SupabaseSecretKey added in v0.1.0

func SupabaseSecretKey() Pattern

SupabaseSecretKey locates the secret API keys a Supabase project is reached with from a server: the prefix sb_secret_ and the thirty-one characters behind it, forty-one characters altogether. A key bypasses row level security and stands for the whole of the project's data, which is why Supabase refuses one sent from a browser.

A key is located wherever it is written, with no word boundary either side, and exactly forty-one characters of it are. So text of that shape is redacted whether or not Supabase issued it. A character outside the base64url alphabet, or anything but an underscore twenty-three characters behind the prefix, ends the reading, so text as it is ordinarily written is not affected.

Its name is "supabase-secret-key".

func XAIAPIKey added in v0.2.0

func XAIAPIKey() Pattern

XAIAPIKey locates xAI API keys: the prefix xai- and the letters and digits behind it, of which there are at least eighty — eighty-four characters altogether where a key is the length xAI prints one at. The key an account manages its other keys with is written xai-token- and a body of the same shape, and is located too. One string authenticates every request a key of either kind is issued for, so nothing in one says what it may be spent on.

A key is located wherever it is written, with no word boundary either side, and is redacted from its prefix to the end of the run it stands in. So a letter or a digit written straight after a key is redacted with it, and text of that shape is redacted whether or not xAI issued it. A space, a hyphen, an underscore or a run of fewer than eighty letters and digits ends the reading, so text as it is ordinarily written is not affected.

Its name is "xai-api-key".

func XAIPatterns added in v0.2.0

func XAIPatterns() []Pattern

XAIPatterns returns every built-in pattern that locates a credential xAI issues.

The returned slice is freshly allocated and may be modified by the caller.

type Reader

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

Reader masks the text read from another reader.

A Reader holds text back — a value split across two reads is in neither of them — so what it returns lags what it has read. The end of the stream settles the last of it, and the text held back is returned before the error that ended the stream is.

A Reader is not safe for concurrent use.

func NewReader

func NewReader(src io.Reader, m *Masker, opts ...StreamOption) *Reader

NewReader returns a Reader that reads from src and masks what it reads with m:

body, err := io.ReadAll(mask.NewReader(resp.Body, m))
Example
package main

import (
	"fmt"
	"io"
	"strings"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.AllBuiltinPatterns()...))

	src := strings.NewReader("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz")
	masked, err := io.ReadAll(mask.NewReader(src, m))
	if err != nil {
		panic(err)
	}

	fmt.Println(string(masked))
}
Output:
GITHUB_TOKEN=****************************************

func (*Reader) Read

func (r *Reader) Read(p []byte) (int, error)

Read fills p with masked text.

Reading returns nothing until the stream settles enough text to fill something, so a read here can take several reads of the reader underneath. The error that ended that reader is held back until the text held with it has been returned, and is then reported as it was.

type Redactor

type Redactor interface {
	Redact(m Match) string
}

Redactor produces the text that replaces a located value.

Implementations must be safe for concurrent use by multiple goroutines.

func Fill

func Fill(r rune) Redactor

Fill redacts every value to r repeated once per rune of the original, so the length of the original survives. A Masker uses Fill('*') unless given another redactor.

Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(
		mask.WithPatterns(mask.AllBuiltinPatterns()...),
		mask.WithRedactor(mask.Fill('#')),
	)

	fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
}
Output:
GITHUB_TOKEN=########################################

func Fixed

func Fixed(s string) Redactor

Fixed redacts every value to s. Neither the content nor the length of the original survives:

mask.Fixed("[REDACTED]")
Example
package main

import (
	"fmt"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(
		mask.WithPatterns(mask.AllBuiltinPatterns()...),
		mask.WithRedactor(mask.Fixed("[REDACTED]")),
	)

	fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
}
Output:
GITHUB_TOKEN=[REDACTED]

func NewRedactor

func NewRedactor(redact func(m Match) string) Redactor

NewRedactor returns a Redactor that redacts values with redact:

mask.NewRedactor(func(m mask.Match) string {
	if m.Pattern.Name() == "jwt" {
		return "[JWT]"
	}
	return "[REDACTED]"
})

A redactor reading the name, as this one does, is reading the attribution of what it was handed rather than a promise about the whole of it: a JWT written against another credential is redacted together with it, and the one label then stands for both. Match.Value says where that comes from.

redact must be safe for concurrent use by multiple goroutines.

Example
package main

import (
	"fmt"
	"strings"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(
		mask.WithPatterns(mask.AllBuiltinPatterns()...),
		mask.WithRedactor(mask.NewRedactor(func(m mask.Match) string {
			return "[" + strings.ToUpper(m.Pattern.Name()) + "]"
		})),
	)

	fmt.Println(m.Mask("GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstuvwxyz"))
}
Output:
GITHUB_TOKEN=[GITHUB-TOKEN]

type Span

type Span struct {
	Start int
	End   int
}

Span is a half-open byte range [Start, End) within the scanned text. Offsets are zero-based, and Start must be less than End.

type StreamOption

type StreamOption func(*streamOptions)

StreamOption configures a Reader or a Writer.

func WithMaxRetained

func WithMaxRetained(n int) StreamOption

WithMaxRetained sets how much text a Reader or a Writer holds back before it gives up and redacts what it is holding, in bytes. Zero holds without limit.

A run of the characters a value is written in, arriving without end, is a value without end to every pattern that reads one, so somewhere the holding has to stop. It stops with a redaction rather than a release, because releasing held text is releasing the credential the pattern was still reading.

The redaction covers everything held at that moment and is attributed to the pattern that was holding it, so a redactor reading Match.Pattern sees which grammar ran long — and so does everything written after it, to the end of the stream. Giving up takes the opening of the value out of the window along with the rest, and a pattern shown the middle of a value without its opening reports nothing and settles everything; a stream going back to passing text through would write out the rest of the very value it gave up holding, and nothing but the pattern could have said the value had ended.

So the limit is a last resort rather than a knob to tune down. The default is generous enough that no credential written in one piece comes near it, and zero holds without limit for a caller who would rather spend the memory. Any n below zero is read as that same zero rather than as a limit no text can come under, which is what strings.SplitN reads a negative count as and what a caller computing the limit from a budget gets when the budget runs out.

What is redacted after that is redacted a write at a time, since a stream cannot hold the rest of itself back to redact it as one. Fill writes a rune for a rune either way — a rune the writes are split inside is held until the rest of it arrives — and a redactor writing a fixed string writes it once a write rather than once.

Example
package main

import (
	"io"
	"os"
	"strings"

	"github.com/koki-develop/mask-go"
)

func main() {
	// A run of the characters a key is written in, arriving without end, is a
	// key without end to the pattern reading it. The limit is where holding
	// stops, and what it stops with is a redaction of everything held.
	m := mask.New(
		mask.WithPatterns(mask.StripeSecretKey()),
		mask.WithRedactor(mask.Fixed("[REDACTED]")),
	)

	w := mask.NewWriter(os.Stdout, m, mask.WithMaxRetained(32))
	if _, err := io.WriteString(w, "sk_live_"+strings.Repeat("0123456789abcdef", 8)); err != nil {
		panic(err)
	}
	if err := w.Close(); err != nil {
		panic(err)
	}
}
Output:
[REDACTED]

type Writer

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

Writer masks the text written to it and writes the result on to another writer.

A Writer holds text back — a value split across two writes is in neither of them — so what reaches the writer underneath lags what was written here, and Close is what settles the end of the stream and lets the last of it go. A Writer that is never closed leaves whatever it was holding unwritten.

A Writer is not safe for concurrent use.

func NewWriter

func NewWriter(dst io.Writer, m *Masker, opts ...StreamOption) *Writer

NewWriter returns a Writer that masks what is written to it with m and writes the result to dst:

w := mask.NewWriter(os.Stderr, m)
defer w.Close()
log.SetOutput(w)

Close writes out what the Writer is still holding. It does not close dst.

Example
package main

import (
	"io"
	"os"

	"github.com/koki-develop/mask-go"
)

func main() {
	m := mask.New(mask.WithPatterns(mask.AllBuiltinPatterns()...))

	w := mask.NewWriter(os.Stdout, m)

	// A value split across two writes is in neither of them, so the first
	// half is held back until the second arrives.
	for _, piece := range []string{"GITHUB_TOKEN=ghp_0123456789abcdefghijklmnopqrstu", "vwxyz\n"} {
		if _, err := io.WriteString(w, piece); err != nil {
			panic(err)
		}
	}

	// Close writes out whatever the Writer is still holding back. A Writer
	// that is never closed leaves it unwritten.
	if err := w.Close(); err != nil {
		panic(err)
	}
}
Output:
GITHUB_TOKEN=****************************************

func (*Writer) Close

func (w *Writer) Close() error

Close writes out the text the Writer is holding back and reports what writing it cost. The writer underneath is left open, as a wrapper leaves what it wraps.

Closing twice reports what the first close did rather than doing it again.

func (*Writer) Write

func (w *Writer) Write(p []byte) (int, error)

Write masks p and writes on whatever that settles.

The count returned is len(p) whenever there is no error: p is taken in whole, and how much of it reaches dst is a question about the stream rather than about this write.

Source Files

Jump to

Keyboard shortcuts

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