Skip to main content

fpr-ff1

CI PyPI Python crates.io docs.rs License: Python MIT, Rust MIT OR Apache-2.0

FF1 format-preserving encryption (NIST SP 800-38G) for Python and Rust, conformance-tested round by round against the published NIST vectors.

  • Python (PyPI): pure Python with one dependency, cryptography, plus an optional compiled backend that is faster and bit-identical.
  • Rust (crates.io): the same FF1 core as a native crate with a small, stable API, validating inputs exactly as the Python package does.

Both are built from this repository, share one conformance suite, and are released together under one version number.

What FF1 is for

Format-preserving encryption turns a value into ciphertext of the same shape: a 16-digit card number encrypts to 16 digits, and a code over A-Z0-9 stays over A-Z0-9. Encrypted values still fit the columns, fixed-width files, validators and APIs built for the plaintext, so you can protect:

  • card and account numbers in legacy schemas;
  • national and customer identifiers passed between services;
  • production data copied into test and analytics environments, still valid for the code that reads it.

FF1 is reversible encryption, not hashing or vault tokenisation: anyone holding the key can decrypt. It provides confidentiality only, so read the security notes.

Install

pip install fpr-ff1       # Python 3.12 or newer
cargo add fpr-ff1         # Rust 1.89 or newer

Quick start

import secrets

from fpr_ff1 import FF1

key = secrets.token_bytes(32)  # for real data, load the key from your secret store

ff1 = FF1(
    key=key,
    radix=10,
    alphabet="0123456789",
    # Derive the tweak from stable record context (an account ID, a table name):
    # equal plaintexts under the same key and tweak give equal ciphertexts.
    tweak=b"customer-pans",
)

encrypted = ff1.encrypt("123456")
assert ff1.decrypt(encrypted) == "123456"

In Rust, with NIST's published sample 2 values, so the output can be checked:

use fpr_ff1::FF1;

fn main() -> Result<(), fpr_ff1::Error> {
    // NIST SP 800-38G sample 2: AES-128, radix 10, a ten-byte tweak. The key is a
    // published test key; load real keys from your secret store.
    let key: [u8; 16] = [
        0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
        0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C,
    ];
    let ff1 = FF1::builder(&key, 10)
        .alphabet("0123456789")
        .tweak(b"9876543210")
        .build()?;

    let ciphertext = ff1.encrypt("0123456789", None)?;
    assert_eq!(ciphertext, "6124200773");
    assert_eq!(ff1.decrypt(&ciphertext, None)?, "0123456789");
    Ok(())
}

Check it against NIST

The same NIST sample 2 check in Python. Its key is a published test key, for checking only:

nist = FF1(
    key=bytes.fromhex("2B7E151628AED2A6ABF7158809CF4F3C"),
    radix=10,
    alphabet="0123456789",
    tweak=b"9876543210",
)
assert nist.encrypt("0123456789") == "6124200773"

Security notes — read before use

FF1 is a deterministic permutation for a fixed key and tweak. That has operational consequences:

  1. Equal plaintexts produce equal ciphertexts under the same key and tweak. NIST recommends varying the tweak with each encryption instance where feasible — derive it from stable record context so identical plaintexts in different records encrypt differently.
  2. FF1 provides confidentiality only — no integrity or authentication. Modified ciphertext decrypts to another plausible in-domain value; there is no error, and the tampering is invisible. Applications needing tamper detection must authenticate the ciphertext and its context separately, where their format allows.
  3. A wrong key or tweak does not raise. Decryption with the wrong key or tweak yields plausible-looking plaintext, not an exception. There is no way to detect key mismatch from the output alone.
  4. The one-million-value domain is a standards floor, not a guarantee. radix ** minlen >= 1_000_000 rules out trivially enumerable domains, but a determined attacker with oracle access can still search a million-value space. Small-domain FPE needs rate limiting or access control around the encryption interface.
  5. Validation exceptions never echo your data. Rejected values are located by index, not repeated in the message, so a malformed record does not leak plaintext into your logs.

Why you can trust this implementation

A subtly wrong FF1 still round-trips perfectly while producing ciphertext no conformant implementation can read, and by the time anyone notices, the data is written. So conformance is the product here, and it is evidenced rather than asserted:

  • All nine NIST sample vectors in both directions, and every per-round intermediate value NIST publishes, for every round of every sample. Compensating bugs can pass an output test; they cannot pass this one.
  • Differential tests against an independent implementation across radices 2, 10, 16, 32, 36, 62, 256 and 65535, at every length where FF1's block structure changes. NIST publishes vectors for radix 10 and 36 only. The oracle is checked against NIST first, and its outputs are frozen into the repository.
  • Exhaustive bijectivity over two whole domains: radix 2 at length 20 and radix 10 at length 6.
  • Vectors transcribed from the NIST document, never regenerated from this code.
  • 100% line and branch coverage of the Python package, enforced in CI. The Rust core passes the whole Python conformance suite bit for bit, and its own tests of the same fixtures.

Each known way FF1 implementations go wrong has a dedicated test:

Known failure mode How it is prevented
Floating-point ceil(v · log₂(radix)) — the Bouncy Castle bug class Exact integer arithmetic; an AST scan fails the build if math.log, ceil, / or any float literal appears in the core
b derived from u instead of v Asserted against the traced value, not a round-trip — which passes either way
Wrong S expansion when d > 16 Differential cases at each block-count transition; no NIST sample reaches this branch
Mirrored parity rule in decrypt Encrypt and decrypt share one code path for it
Silent coercion of bad input Every rejection raises a typed exception; a 50-case sweep asserts nothing escapes as a bare AttributeError or KeyError

This is strong conformance evidence, not proof: see what this is not.

Why FF1 only — and why FF3 is excluded

SP 800-38G originally specified FF1 and FF3. FF3 was revised to FF3-1 after an attack, but Beyne then showed a weakness in the tweak schedule that affects FF3 and FF3-1 alike, and the February 2025 second public draft of SP 800-38G Rev. 1 removes FF3 entirely. FF1 is the only format-preserving mode left standing, so this project will never implement FF3 or FF3-1: no flag, no opt-in, no plan to add one.

Limits

SP 800-38G, with the tighter limits of its Rev. 1 second public draft. Inputs below the minimum domain fail closed with LengthError, even where older libraries accept them.

Constraint Value
Minimum domain radix ** minlen >= 1_000_000: 6 numerals at radix 10, 4 at radix 36
Maximum length 2 ** 32 - 1 numerals; tweaks up to 2 ** 32 - 1 bytes
Key sizes AES-128, AES-192, AES-256
Radix 2 <= radix < 2**16, a deliberate subset of the specification's range

Why each limit is where it is: docs/configuration.md.

Backends and platforms

In Python, FF1(..., backend="rust") opts in to the compiled backend. backend="python" is the default and the reference implementation. Both produce bit-identical ciphertext and raise identical exceptions, because validation runs in Python for both.

Native wheels carry the compiled backend for Linux x86_64 and aarch64 (glibc 2.34 or newer), macOS on Intel and Apple silicon, and Windows x64, on CPython 3.12 to 3.14 (wheel list). Elsewhere, including musl, older glibc and free-threaded builds, pip installs the pure-Python wheel, and backend="rust" raises BackendError rather than falling back silently.

Performance

just bench on 2026-09-27: one core, Linux x86_64 (AMD Ryzen AI Max+ PRO 395), CPython 3.12.13, extension built in release mode with rustc 1.98.1. Microseconds per encryption:

Input backend="python" backend="rust" Speedup
6 numerals, radix 10 28.2 4.6 6.20×
n = 100, radix 10 108.7 39.0 2.79×
n = 1,000, radix 10 937.8 397.5 2.36×
n = 5,000, radix 10 5,212.3 2,117.5 2.46×
n = 20,000, radix 10 26,770.3 9,774.2 2.74×
n = 100, radix 256 141.0 51.3 2.75×
n = 1,000, radix 256 2,542.9 211.4 12.03×
n = 5,000, radix 256 13,105.2 1,046.2 12.53×
n = 20,000, radix 256 52,585.5 4,167.8 12.62×

The compiled backend releases the GIL: in the same run, four threads sharing one instance did the same work 2.17× faster than one thread (n = 5,000, radix 10), against 0.86× for pure Python. These are one machine's numbers; measure your own data with just bench.

Python API at a glance

assert ff1.decrypt_numerals(ff1.encrypt_numerals([1, 2, 3, 4, 5, 6])) == [1, 2, 3, 4, 5, 6]
assert ff1.decrypt(ff1.encrypt("123456", b"per-call"), b"per-call") == "123456"  # per-call tweak
assert ff1.min_length == 6

Numerals need no alphabet. Every rejection raises a typed exception derived from FF1Error that never echoes your data. Instances are thread-safe and picklable (pickling serialises the key). Full reference: docs/python-api.md. Rust API: docs.rs.

Migrating from ubiq_security_fpe

fpr-ff1 replaces the deprecated, unmaintained ubiq_security_fpe. The two produce identical ciphertext in both directions, enforced by tests/test_interoperability.py, so existing data stays readable without re-encryption. One trap: the legacy twk_max_len=0 meant "no maximum", which here is max_tweak_len=None; a literal 0 means empty tweaks only. The migration guide has the API mapping and every behaviour change.

What this is not

  • Not FIPS validated. Passing the published NIST sample vectors is conformance evidence, not FIPS 140 validation, and no such claim is made.
  • Not independently audited, and not constant-time; see SECURITY.md.
  • No key zeroization. Python bytes are immutable and the interpreter may copy them; the Rust crate holds keys in ordinary heap memory and does not wipe them.
  • Not a key-management or tokenisation toolkit, permanently. No key generation, storage or derivation, identifier generation, persistence, checksums or application-specific alphabets.

Documentation

Development

Requires Python 3.12, uv and just; the compiled backend and the crate also need Rust.

just setup    # create venv and install deps
just quality  # format check, lint, typecheck, tests

License

The Python package is MIT. The Rust crate is MIT OR Apache-2.0, at your option; its sources are also compiled into the Python platform wheels, where the MIT option applies.

Metadata

Release files for fpr-ff1 2.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fpr-ff1 2.1.0
File Size Uploaded Provenance
fpr_ff1-2.1.0.tar.gz 935.5 kB With provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for fpr-ff1 2.1.0
File
fpr_ff1-2.1.0-py3-none-any.whl Python 3 none any Details
fpr_ff1-2.1.0-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
fpr_ff1-2.1.0-cp312-abi3-manylinux_2_34_x86_64.whl CPython 3.12 abi3 Linux glibc 2.34+ x86-64 Details
fpr_ff1-2.1.0-cp312-abi3-manylinux_2_34_aarch64.whl CPython 3.12 abi3 Linux glibc 2.34+ ARM64 Details
fpr_ff1-2.1.0-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
fpr_ff1-2.1.0-cp312-abi3-macosx_10_12_x86_64.whl CPython 3.12 abi3 macOS 10.12+ x86-64 Details

Total release size: 2.4 MB

Release files / fpr_ff1-2.1.0.tar.gz

Download URL fpr_ff1-2.1.0.tar.gz
Size 935.5 kB
Tags Source
SHA-256 checksum
How to use checksums
3419da24425fb6b411fd7fcc4da9f611884dac07a7b38339dd5c94212c4d3ca5
BLAKE2b-256 checksum
How to use checksums
46131ed0acfcbf45896dfd2dc4837bb67cf6ae6c1fccb49abad87e6876d62ee6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / fpr_ff1-2.1.0-py3-none-any.whl

Download URL fpr_ff1-2.1.0-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ed9dbfcad32da929be6a3fc183ca10cdeeb7274073fa231dbf13066345b2ab60
BLAKE2b-256 checksum
How to use checksums
d2b07bb8622d086ccafe6741d0458fad033177a704186ab4504ad03bd30de69a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / fpr_ff1-2.1.0-cp312-abi3-win_amd64.whl

Download URL fpr_ff1-2.1.0-cp312-abi3-win_amd64.whl
Size 200.0 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
1b8cc219ec02d9c3e036ea4813a8d50bfa51f25a50cb1d3186ed6d7db9efa1d6
BLAKE2b-256 checksum
How to use checksums
6225228e4ee3c0ee90a7a94264e2172b29072bad912c6e6b95f512e094d77ff6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / fpr_ff1-2.1.0-cp312-abi3-manylinux_2_34_x86_64.whl

Download URL fpr_ff1-2.1.0-cp312-abi3-manylinux_2_34_x86_64.whl
Size 342.3 kB
Tags CPython 3.12 Linux glibc 2.34+ x86-64 abi3
SHA-256 checksum
How to use checksums
ed3133cbd7ac8fc6cf793d34e7214521128568109b04afb55b092d9a5601eee7
BLAKE2b-256 checksum
How to use checksums
4e46b536160039854320923a74a70953be40161a665235f430d196948e399bc3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / fpr_ff1-2.1.0-cp312-abi3-manylinux_2_34_aarch64.whl

Download URL fpr_ff1-2.1.0-cp312-abi3-manylinux_2_34_aarch64.whl
Size 326.9 kB
Tags CPython 3.12 Linux glibc 2.34+ ARM64 abi3
SHA-256 checksum
How to use checksums
a71a1b9918e1762a7d3e5e85cdd5801f6c3a9409f845829bb3f98c71403a7efe
BLAKE2b-256 checksum
How to use checksums
942e87742eccd3633b57db5a004b4f091d43c2456fc0a830e91cfd32c6c5526e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / fpr_ff1-2.1.0-cp312-abi3-macosx_11_0_arm64.whl

Download URL fpr_ff1-2.1.0-cp312-abi3-macosx_11_0_arm64.whl
Size 282.2 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
e9eca1cb1bbd02b1dcef7811f60d46e1555899b89a55d01f9405cd89d355699b
BLAKE2b-256 checksum
How to use checksums
790fcbcabb0039dd9889f47027862451c5ec44e0d2c8627736df63d4eb4f09df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Release files / fpr_ff1-2.1.0-cp312-abi3-macosx_10_12_x86_64.whl

Download URL fpr_ff1-2.1.0-cp312-abi3-macosx_10_12_x86_64.whl
Size 300.5 kB
Tags CPython 3.12 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
bdd135f5187ce690157e7e21096c222825e673898a1218d84ad0781a4761a0cf
BLAKE2b-256 checksum
How to use checksums
aff6d5520a5a1f840d9b8c85cf8f398e875421f6ea2a3dc1d317babd85ae05f3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

Security for fpr-ff1 2.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 7 files in this release have been uploaded with provenance metadata.

Select from 7 files

Attestation for fpr_ff1-2.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 28, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page