Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

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.0rc2

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.0rc2
File Size Uploaded Provenance
fpr_ff1-2.1.0rc2.tar.gz 934.8 kB With provenance Details

Built distributions (wheels)

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

Download URL fpr_ff1-2.1.0rc2.tar.gz
Size 934.8 kB
Tags Source
SHA-256 checksum
How to use checksums
5019d491be767813ca7e891b0ba48df8bba2892ff2d005b4b44dc93a65395ca4
BLAKE2b-256 checksum
How to use checksums
c2a3873b1c95e5af07798a0efd8cecfcc85a7967d24826d128c136647f1b52dc
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 27, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.0rc2-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2cee8e01c8bf39c71fadd1b5cc58e91cfce9710d24c596ca680a8ce2c4fff2fa
BLAKE2b-256 checksum
How to use checksums
88c71ee4557aae783e0afb9e90e685ea7a72c251a9abc6a5362460c1430c6ecd
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 27, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.0rc2-cp312-abi3-win_amd64.whl
Size 200.1 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
c0b0d4460f6be854abef60c17aa323614f06d26c9be6527f93261adf56307b87
BLAKE2b-256 checksum
How to use checksums
eabae28680beff2d316aa89b73f67c76c1652414748edafc3ec0bab70455e80d
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 27, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.0rc2-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
282ce3ef344a2cdfdd11daf2e9da46d0e843f325c80b36e3077ae202d3d58c2b
BLAKE2b-256 checksum
How to use checksums
317cca408f0b9cd09b06172c6f8dd233afed58334d548a99e47e5c69ba600973
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 27, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.0rc2-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
69dfb4a036dffb469d11986ec20865de48b247b0a86d9141282b2cb9b34b357a
BLAKE2b-256 checksum
How to use checksums
b48d4d725a3930d37a8ae9901224f7b76d923c2e763bf60f2813d00cd06b778e
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 27, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.0rc2-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
8c651a86d979cffca5c502426e25e6f39490d8acfbd8d09487377bdbd60f5a0f
BLAKE2b-256 checksum
How to use checksums
436e017a0eed5bac49c1804a2a8640008e3447779b5f9be2d7561fbe9b53419e
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 27, 2026.

Transparency log

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

Download URL fpr_ff1-2.1.0rc2-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
79090040bbafb1d6d4467a6b5c6064b3f2850f75b142a21db575b44f8bb0d659
BLAKE2b-256 checksum
How to use checksums
d147761f8e17adad86579d3ce40a8a8aea933e9a29f17fa025c16efc90bbf56b
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 27, 2026.

Transparency log

Security for fpr-ff1 2.1.0rc2

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.0rc2.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 27, 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