BetterIAM
Secrets and keys

Data protection

Replace card numbers, SSNs, emails, phone numbers and free text with tokens, and read the values back only per profile and stated purpose.

new@better-iam/server@better-iam/clientdata-protection.mdtokenization.tsprotection.ts

Once a card number or a national identifier lands in one table, it spreads: into logs, analytics exports, backups, support tools and every service that reads the table. Each copy widens what an attacker can steal and what an audit must cover, and erasing one person's data means finding every copy.

Tokenization keeps sensitive values out of your application's databases, logs and analytics. Card numbers, national identifiers, email addresses, phone numbers and free-text fields are replaced by tokens, which are safe to store, index and pass between services. The values themselves are encrypted under a tenant KMS key. Turning a token back into its value (detokenizing) is a separate permission, decided per profile and per stated purpose, and every call is audited. The model follows Skyflow, VGS and the de-identification side of Google DLP.

await iam.api.protection.createProfile(admin, {
  tenantId,
  name: 'cards',
  dataType: 'card',
  deterministic: true,
});

const { tokens } = await iam.api.protection.tokenize(checkout, {
  tenantId,
  profile: 'cards',
  values: ['4242 4242 4242 4242'],
});
// tokens[0] looks like '7304918265534242': same length, same last four digits, never a valid card number.

const { values } = await iam.api.protection.detokenize(paymentService, {
  tenantId,
  profile: 'cards',
  tokens,
  purpose: 'payment-processing',
});

Over HTTP the same calls are POST {basePath}/protection/{method}, and @better-iam/client exposes them as client.protection.*.

Profiles

A profile says how one kind of value is handled. Its name is how refer to it: iam/protection/{name}. A tenant keeps at most 100 profiles. createProfile (iam:protection:manage) takes:

Prop

Type

The data type, format and key never change after the profile is created, because issued tokens depend on them. updateProfile changes only the description, the mask and the retention (retentionDays: null keeps tokens until they are deleted). getProfile and listProfiles report how many tokens each profile holds (tokens).

Format-preserving ssn and phone profiles are always deterministic

Their tokens keep the last four digits, which leaves few random ones: 10,000 tokens per last four digits of a social security number, 1,000 for a seven-digit phone number. Issuing a new token on every call would use them up, so these profiles default to deterministic: true and refuse false. Use format: 'random' for non-deterministic tokens of these types.

Values and tokens by data type

dataTypeAccepted valuesFormat-preserving token
card12 to 19 digits (spaces and dashes ignored), Luhn-validSame length and last four digits, and never Luhn-valid
ssn9 digits, stored as NNN-NN-NNNN9XX-, group digits outside the ITIN ranges, and the last four: never a valid SSN or ITIN
emailAn email address, stored in Unicode NFC and lowercaseA random local part at the same domain
phone7 to 15 digits with an optional +, compared as written (give them in E.164)Same length, leading + and last four digits
generic1 to 4096 characters of well-formed text, stored in NFC; tab and line feed allowedEach letter of any script replaced by an ASCII letter (capitals by capitals), each digit by a digit, everything else kept

A token is never the value itself. A format-preserving generic value needs at least 12 letters or digits, so its token has enough random positions; use a random profile for shorter text.

Email tokens look deliverable

A format-preserving email token is an address at the real domain. Never send mail to tokens.

Values are normalized before they are tokenized. 4242 4242 4242 4242 and 4242424242424242 are the same card, and composed and decomposed spellings of José are the same text, so a deterministic profile gives them the same token, erasure by value finds both, and detokenizing returns the normalized form. Phone numbers are not reformatted: +14155550199 and 14155550199 are different values.

Control characters (DEL and the C1 range included), unpaired surrogates and bidirectional formatting characters are refused: they would store or display as something other than what was sent.

Masks

dataTypeMasksDefault
cardlast4, first6last4, fulllast4
ssnlast4, fulllast4
emailemail (j***@example.com), fullemail
phonelast4, fulllast4
genericfull, last4full

A mask never shows more than half of a value's letters and digits: a seven-digit phone number shows its last three. The one exception is first6last4 on a card number of 15 digits or more, which PCI DSS allows; on a shorter card number it shows the last four only. The email mask shows the first character of the local part only when it has four or more. Letters and digits of every script are hidden.

Tokenizing and reading back

  • tokenize takes up to 100 values of one profile and returns their tokens in order. A deterministic profile returns the token it already issued for a value; otherwise every call issues new tokens. All the new values of one call share one data key, which is wrapped under the profile's KMS key. The response does not say which values were already stored (that would tell the caller who else is); the audit event records how many were new.
  • detokenize takes up to 100 tokens of one profile and a purpose, and returns the values in order, with null for tokens the profile does not hold. The purpose is a short lowercase name (up to 64 lowercase letters, digits and hyphens, starting with a letter) such as payment-processing, fraud-review or tax-filing.
  • mask returns up to 100 masked values (************4242, j***@example.com), with the profile's mask or a style its data type allows. It is a separate permission, so support staff can recognize a card without ever seeing it.
  • deleteTokens erases tokens and their values for good, named by up to 100 tokens or values, and returns { deleted }. Erasing by value finds every token issued for it through a keyed fingerprint, even in profiles that are not deterministic, which is what a data-subject erasure request needs. It needs the profile's KMS key enabled; erasing by token does not.
const { values: masked } = await iam.api.protection.mask(supportAgent, {
  tenantId,
  profile: 'cards',
  tokens,
});
// ['************4242']

await iam.api.protection.deleteTokens(privacyHandler, {
  tenantId,
  profile: 'cards',
  values: ['4242 4242 4242 4242'],
});

Permissions

ActionResourceCalls
iam:protection:manageiam/protection/{name}createProfile, updateProfile, deleteProfile
iam:protection:readiam/protection/{name}listProfiles (per profile), getProfile
iam:protection:tokenizeiam/protection/{name}tokenize
iam:protection:detokenizeiam/protection/{name}detokenize
iam:protection:maskiam/protection/{name}mask
iam:protection:deleteiam/protection/{name}deleteTokens, and setting or shortening retentionDays in updateProfile (with manage)

can use resource.profile, resource.dataType, resource.format and resource.deterministic. detokenize adds resource.purpose, and mask adds resource.style: the style actually used, the profile's default included.

Detokenize cards only to process payments
{
  "effect": "allow",
  "actions": ["iam:protection:detokenize"],
  "resources": ["iam/protection/cards"],
  "conditions": { "StringEquals": { "resource.purpose": "payment-processing" } }
}

Tokenizing and detokenizing are separate so that the services that collect data (checkout forms, import jobs) can be allowed to tokenize without ever being able to read anything back. Callers from another tenant (other than a root administrator) and sessions that view as someone else () are refused.

Tokenizing into a deterministic profile is a lookup

Whoever may tokenize can submit guesses and compare the tokens with ones they hold. A format-preserving SSN token shows the last four digits, which leaves 100,000 candidates: one thousand calls of 100 values. Grant iam:protection:tokenize on deterministic profiles as narrowly as detokenize (conditions can name resource.deterministic), and watch the iam:protection:tokenize audit events for volume.

Setting or shortening a profile's retention in updateProfile deletes tokens at the next sweep, so it needs iam:protection:delete as well as iam:protection:manage, and a , like deleting the profile.

Customer keys

A profile bound to an existing key (keyId) leaves the key's owner in charge, as the secrets vault does. Every call needs the caller's own KMS permission on the key, by policy, besides the protection one: iam:kms:generate-data-key to tokenize, iam:kms:decrypt to detokenize or mask. The key must be an enabled aes-256-gcm key, and whoever creates the profile must hold both permissions on it. Revoking those permissions, or disabling the key, stops the profile.

A key the profile created is managed by it (managedBy: 'protection'): the KMS API refuses to use it directly (KEY_MANAGED), and deleting the profile schedules the key's deletion after 7 days. A customer key stays when the profile is deleted. A key another profile or module manages cannot be bound.

How values are protected

  • Each call that stores new values generates one data key, wraps it under the profile's AES KMS key (bound to the profile), and encrypts each value with AES-256-GCM. The associated data names the tenant, the profile and the token, so a stored ciphertext cannot be moved to another token. A stored value that fails authentication (altered in storage) is reported as KEY_MATERIAL_UNAVAILABLE.
  • Every token stores an HMAC fingerprint of its value, in every profile: deterministic profiles find existing tokens by it, and erasure by value finds every token of a value. The fingerprint key is a data key wrapped under the profile's KMS key, so it stops with the key, and without it fingerprints cannot be tested against guesses. Its material lives in KMS, which iam.rotateSecrets() re-seals, so tokens stay the same across secret rotations.
  • Disabling the KMS key makes every value of the profile unreadable (KEY_STATE_INVALID) and, because every tokenize call computes fingerprints, stops tokenizing too until the key is enabled again. Deleting the key destroys the values for good.
  • Tokens are unique per profile. SQL adapters index fingerprints (migration 0006_protection_indexes), so deterministic lookups and erasure by value do not scan the tenant's tokens.

The audit trail records the profile, the number of values or tokens, how many were found or new, and the purpose. It never records values or tokens. Refusals record the profile and the purpose too; a call refused after it was authorized is recorded as a deny with metadata.reason key-unavailable (the key is disabled or pending deletion) or refused. Uses of the KMS key are audited on the key with via: 'protection'. afterOperation hooks never see values or tokens.

Retention

Run iam.protection.sweep() daily next to the other scheduler jobs. It deletes tokens older than their profile's retentionDays, 500 per transaction so the write lock is never held for a whole profile, audits each profile it deleted tokens from as protection:retention-sweep (actor deployment-operator, with the number deleted), and returns { deleted, profiles }. Pass { tenantId } to limit it to one tenant.

setInterval(() => void iam.protection.sweep(), 24 * 60 * 60 * 1000);

deleteProfile (iam:protection:manage and a recent sign-in) refuses a profile that still holds tokens. Delete its tokens first, or let retention empty it.

Errors

CodeStatusWhen
INVALID_INPUT400A value that is not valid for the data type, a mask its type does not allow, a bad purpose
KEY_STATE_INVALID409The profile's KMS key is disabled or pending deletion
KEY_MANAGED409Binding a key another profile or module manages
KEY_MATERIAL_UNAVAILABLE500A stored value failed authentication (altered in storage)
RESOURCE_IN_USE409Deleting a profile that still holds tokens
CONFLICT409A profile name in use, or no free format-preserving token for a value
LIMIT_EXCEEDED409The tenant already has 100 profiles
NOT_FOUND404No profile of that name in this tenant
ACCESS_DENIED403The profile, the purpose, the mask style or the customer key is not allowed

Next steps

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page