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.1

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.1
File Size Uploaded Provenance
fpr_ff1-2.1.1.tar.gz 941.1 kB With provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for fpr-ff1 2.1.1
File
fpr_ff1-2.1.1-py3-none-any.whl Python 3 none any Details
fpr_ff1-2.1.1-cp312-abi3-win_amd64.whl CPython 3.12 abi3 Windows x86-64 Details
fpr_ff1-2.1.1-cp312-abi3-manylinux_2_34_x86_64.whl CPython 3.12 abi3 Linux glibc 2.34+ x86-64 Details
fpr_ff1-2.1.1-cp312-abi3-manylinux_2_34_aarch64.whl CPython 3.12 abi3 Linux glibc 2.34+ ARM64 Details
fpr_ff1-2.1.1-cp312-abi3-macosx_11_0_arm64.whl CPython 3.12 abi3 macOS 11.0+ ARM64 Details
fpr_ff1-2.1.1-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.1.tar.gz

Download URL fpr_ff1-2.1.1.tar.gz
Size 941.1 kB
Tags Source
SHA-256 checksum
How to use checksums
8f2df086131ef7f44dec05ec308333f801985b563d077e989f420529850833fb
BLAKE2b-256 checksum
How to use checksums
6a7acf0250a321e8f2921dc655d6d8a786d9dee78fca48bae4d2ff864d97dc83
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 Oct 11, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.1-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f4d8a005a740fb959744a7ba4e3f09580c0be472aa1c168ef4dcdd0596f630e4
BLAKE2b-256 checksum
How to use checksums
5148ef3f77cc45827a5ccc82a22d9f325b56e4420ac991061bbcdc8a1c4513ee
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 Oct 11, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.1-cp312-abi3-win_amd64.whl
Size 200.9 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
50e5edb494b06d89430d624b868344bf67efcad2d616ebdcc99b889c226bce3b
BLAKE2b-256 checksum
How to use checksums
b3d57e94e0e154dfbb94e059d359d4392ec8b360f05eb741ddf3a2040ec69f45
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 Oct 11, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.1-cp312-abi3-manylinux_2_34_x86_64.whl
Size 342.0 kB
Tags CPython 3.12 Linux glibc 2.34+ x86-64 abi3
SHA-256 checksum
How to use checksums
aa059fffe5c91028abdbc512bb0cbaf262188f2e2138827cbc7f29ecff7acea3
BLAKE2b-256 checksum
How to use checksums
771c78c64745b6028f488efba77c4e10445796956e74ef089ea5c765af7d442e
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 Oct 11, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.1-cp312-abi3-manylinux_2_34_aarch64.whl
Size 328.5 kB
Tags CPython 3.12 Linux glibc 2.34+ ARM64 abi3
SHA-256 checksum
How to use checksums
20644bef792e4d6030241d10aba75309261e48b8394b92d03cd3853f5479eede
BLAKE2b-256 checksum
How to use checksums
2f8e41779f349d484b5ff6b5280e70681cc0751d7ad330e09a4f9478602d085f
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 Oct 11, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.1-cp312-abi3-macosx_11_0_arm64.whl
Size 284.8 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
924c02fd1def66f46ae3caea52f976aec86d21e96b1c9e44d710153fb9732ae2
BLAKE2b-256 checksum
How to use checksums
2096a3a0866b38d15ee38fdb43a681a84c40b03574f7e4b0675930ae219dc11a
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 Oct 11, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.1-cp312-abi3-macosx_10_12_x86_64.whl
Size 303.0 kB
Tags CPython 3.12 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
1d8831f26d6e584e7a1dee0ffabe3bb340a08bc420bc2386eea014db17f834cd
BLAKE2b-256 checksum
How to use checksums
2a07d85afac50a3134131bbd83a29407045851e4b81b72b4b2a4297ea279d51f
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 Oct 11, 2026.

Transparency log

Security for fpr-ff1 2.1.1

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.1.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 Oct 11, 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