What is format-preserving encryption?
In cryptography, you have plain text and cipher text. An encryption algorithm transforms the plain text into the cipher text. The cipher text won't look anything like the plain text — in characters or length. There are many different encryption algorithms serving many different purposes, and the cipher text for each one will be different.

Take the case of a credit card number, a common piece of sensitive information that's often encrypted. A credit card number is 16 digits long. Encrypting it with the industry-standard AES-128-CBC algorithm produces a cipher text much longer than 16 digits — usually around 64 base64-encoded characters. If you're storing the credit card number in a database column configured for length 16, the cipher text won't fit.
Format-preserving encryption (FPE) is a method of encryption that causes the cipher text to retain the same format as the plain text. Encrypting a 16-digit credit card number with FPE produces another 16-digit cipher text — same length, same character set (digits), looks nothing like the original. Typically only numeric, alphabetic, or alphanumeric characters can be used with FPE.
The cipher text can be decrypted back into the original plain text if you have the encryption key. FPE is reversible — that's what distinguishes it from masking, hashing, or random replacement.
Why use format-preserving encryption
FPE solves three problems that plain encryption doesn't:
- Schema compatibility. Existing database columns, file formats, and downstream systems often have rigid expectations about field length and character set. Storing a 64-character base64 blob in a column declared
CHAR(16)won't work. FPE produces output that fits. - Validation passes. Downstream code that validates input format (Luhn checksum on credit cards, regex on SSN format) keeps passing on the FPE-encrypted value. Important when you have legacy systems you can't refactor but still need to encrypt the value flowing through them.
- Reversibility. Unlike masking or redaction strategies, FPE is fully reversible. The system holding the encryption key can decrypt back to the original; every other system sees only cipher text. Useful when one workflow needs the real value (a payment-processing system) while every other consumer (analytics, ML, logs) doesn't.
The trade-off: FPE doesn't hide the fact that some value of that shape exists at this position. An attacker who knows the column contains credit card numbers still knows the cipher text refers to a credit card number, just not which one. For most threat models that's fine; for the rare case where the existence of any value is itself sensitive, FPE is the wrong tool.
Read more about the formal definition and supported algorithms on the Wikipedia article on format-preserving encryption.
FPE in Philter and Phileas
Philter and the underlying Phileas library both support FPE as a filter strategy for several PII types: bank numbers, Bitcoin addresses, credit cards, driver's license numbers, IBAN codes, passport numbers, SSNs/TINs, package tracking numbers, and VINs. The strategy is named FPE_ENCRYPT_REPLACE; specify it in the policy and Philter encrypts the detected PII using format-preserving encryption.
Because FPE preserves shape, the redacted document remains structurally valid — downstream systems that expect credit cards in a specific field don't break, but the original card numbers are no longer recoverable without the key.
If you're not concerned about reversibility, the RANDOM_REPLACE strategy substitutes randomly-generated values in the same format. Useful for ML training data or demo datasets where preserving shape matters but the original values don't.
Enabling FPE for credit card numbers
Here is a complete Philter / Phileas policy that uses FPE on credit card numbers:
{
"name": "credit-cards-fpe",
"identifiers": {
"creditCard": {
"creditCardFilterStrategies": [
{
"strategy": "FPE_ENCRYPT_REPLACE",
"key": "your-32-byte-hex-encryption-key-here",
"tweak": "your-tweak-value-here"
}
]
}
}
}Two values you need to provide:
key— the encryption key. Hex-encoded. The same key produces the same cipher text for the same input, so use a stable key for consistent encryption-decryption. Generate viaopenssl rand -hex 32or your KMS of choice; rotate per your normal key-management policy.tweak— a non-secret value that varies per use case (different tweak for credit cards vs. SSNs, for example). Functions like a domain separator, ensuring the same plaintext encrypted with the same key in two different contexts produces different cipher texts.
Apply the policy in Phileas (Java, in-process):
Policy policy = Policy.fromFile("credit-cards-fpe.json");
PhileasFilterService phileas = new PhileasFilterService(policy);
String input = "Card 4111-1111-1111-1111 expires 06/27.";
String redacted = phileas
.filter("credit-cards-fpe", "default", "doc-001", input)
.getFilteredText();
// Card 7382-9410-5562-1838 expires 06/27.
// (Same shape, different digits; recoverable with the key + tweak.)Or call the same policy via Philter's HTTP API:
$ curl "http://localhost:8080/api/filter?p=credit-cards-fpe" \
--data "Card 4111-1111-1111-1111 expires 06/27." \
-H "Content-type: text/plain"
Card 7382-9410-5562-1838 expires 06/27.To decrypt later, hold the same key and tweak; reverse the operation via the Phileas API or any FF1-compatible decryption library.
Learn more about FPE and the other available filter strategies in the Philter User's Guide — per-entity strategies give you fine-grained control over how every detected entity type is handled.
Related posts: