Anonymizer Operators

Copy Markdown View Source

Obscura validates every configured operator before it modifies text. A bad configuration returns {:error, %Obscura.Anonymizer.Error{}}; it never returns a successful or partially anonymized result.

Errors expose only stable codes, operator and field atoms, safe reason atoms, and allowlisted metadata. They never contain source text, replacements, salts, callback return values, or exception messages.

Replace

%{type: :replace, value: "[EMAIL]"}

value is optional and defaults to [REDACTED]. When present, it must be a binary. The only accepted keys are type and value.

Redact

%{type: :redact}

Redact replaces the detected span with an empty binary. It accepts no options other than type.

Mask

%{type: :mask, char: "*", keep_last: 4}
  • char defaults to * and must contain exactly one valid Unicode grapheme.
  • keep_last defaults to 0 and must be a non-negative integer.
  • Masking operates on graphemes, while result offsets remain byte offsets.
  • When keep_last exceeds the source grapheme count, the source is unchanged.

Empty or multi-grapheme mask strings, negative counts, invalid types, and unknown options are rejected.

Hash

Hash replacements use this self-describing format:

$obscura$v1$hash$ALGORITHM$MODE$BASE64URL_SALT$HEX_DIGEST

Supported algorithms are :sha256 and :sha512.

Secure Mode

%{type: :hash, mode: :secure, algorithm: :sha256}

Secure mode is the default. It generates a fresh 16-byte salt with :crypto.strong_rand_bytes/1 for every replacement. The same source value therefore produces different replacements. Supplying a salt in secure mode is an error.

Deterministic Mode

%{
  type: :hash,
  mode: :deterministic,
  algorithm: :sha256,
  salt: "application-salt"
}

The salt must be a binary containing at least 16 bytes. Identical value, salt, and algorithm inputs produce identical replacements. This reveals equality and does not make low-entropy identifiers unguessable.

Obscura.Operator.Hash.verify/2 verifies a source value against either encoded mode without raising for malformed input.

Hash Migration

The pre-release hash contract previously defaulted to deterministic SHA-256, allowed an empty or short salt, and returned sha256:HEX. That format is no longer generated or accepted by verify/2.

To migrate intentional deterministic use, add mode: :deterministic, provide at least 16 salt bytes, and store the new versioned replacement. Otherwise omit the salt and use the secure default. Existing stored legacy hashes require an application-specific migration because Obscura cannot recover their source values.

Pseudonymize

%{type: :pseudonymize}

Pass the vault in the operator config or anonymizer options:

{:ok, vault} = Obscura.Vault.Memory.start_link()
text = "Email user@example.test"

Obscura.redact(text,
  entities: [:email],
  operators: %{email: %{type: :pseudonymize}},
  vault: vault
)

Pseudonymization is reversible vault-backed tokenization, not encryption. The vault and token formatting options are checked during operator preflight.

Custom Operators

Custom operators implement Obscura.Operator.Custom:

defmodule MyOperator do
  @behaviour Obscura.Operator.Custom

  @impl Obscura.Operator.Custom
  def apply(value, %{entity: entity}, options) do
    {:ok, "<#{options.prefix}:#{entity}:#{byte_size(value)}>", %{format: :custom}}
  end
end

Configure the module and a map of explicit options:

%{type: :custom, module: MyOperator, options: %{prefix: "private"}}

Valid callback results are {:ok, replacement} and {:ok, replacement, metadata}. The replacement must be a binary and metadata must be a map. Missing modules, missing behaviour declarations, invalid returns, callback errors, exceptions, throws, and exits become sanitized operator errors. The callback receives only the entity context in addition to the explicit source value and options.

Custom operator modules are trusted application code. They receive the raw matched value and can log, transmit, retain, or raise with it before Obscura sanitizes the callback boundary. Review callback implementations and avoid embedding source values in their own telemetry or process metadata.

Error Contract

Obscura.Anonymizer.Error has these public fields:

  • code: stable machine-readable category;
  • operator: operator atom when known;
  • field: invalid field atom when known;
  • reason: safe reason atom;
  • metadata: allowlisted non-sensitive details.

Applications should branch on code, operator, and field rather than the human-readable exception message. Unknown options and malformed operator collections are errors, including configurations for entities which do not appear in the current input.

Operator types, keys, defaults, preflight behavior, and stable error codes are part of the 0.1.x contract. Human-readable messages and additive metadata are not. See docs/public-api-stability.md.

Default Inspect implementations for anonymizer result and item structs hide redacted output, replacements, token-like values, and metadata. Their explicit public fields still contain the values required by the operation and must be handled as sensitive data.