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 License: MIT

A small, correct Python implementation of FF1, the format-preserving encryption mode from NIST SP 800-38G.

This package is intentionally just the algorithm: no accounts, no network, no key management, and no FF3/FF3-1 modes.

Install

pip install fpr-ff1

Quick start

from fpr_ff1 import FF1

# The all-zero key here is for the example only. Never use it (or any other
# published key) for real data: load key material from your secret store.
key = load_key_from_your_secret_store()  # 16, 24, or 32 bytes

ff1 = FF1(
    key=key,
    radix=10,
    alphabet="0123456789",
    # A tweak separates ciphertexts across contexts: two records with the
    # same plaintext encrypt to the same ciphertext under the same tweak,
    # so derive the tweak from stable record context (an account ID, a
    # table name) rather than leaving it empty.
    tweak=b"customer-pans",
)

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

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.

Features

  • Pure Python with a single runtime dependency: cryptography.
  • An optional compiled backend (backend="rust") for high-throughput callers, with the pure-Python implementation retained as the reference and the default.
  • Conformance-tested against the NIST SP 800-38G sample vectors.
  • No floating-point arithmetic in the FF1 core.
  • Tightened domain limits from the SP 800-38G Rev. 1 second public draft:
    • radix range 2 <= radix < 2**16 (a deliberate supported subset of the spec's inclusive [2..2**16] — see Domain limits)
    • minimum domain radix ** minlen >= 1_000_000
    • maximum length 2 ** 32 - 1 (SP 800-38G specifies maxlen < 2 ** 32)
    • AES keys of 128, 192, or 256 bits only
  • Strongly typed public API with typed exceptions rooted at FF1Error.

Why you can trust this implementation

Format-preserving encryption is unusually easy to get almost right. A subtly wrong FF1 still round-trips perfectly — decrypt(encrypt(x)) == x — while producing ciphertext no conformant implementation can read. By the time anyone notices, the data is written. So conformance here is not a checkbox; it is the entire product, and it is evidenced rather than asserted.

The full suite — NIST sample vectors, per-round intermediate-value conformance for every round of every sample, differential tests against an independent implementation, exhaustive bijectivity sweeps, and a malformed-input sweep — runs in CI with 100% line and branch coverage enforced. The build fails below it.

That coverage figure measures the Python package. The compiled backend's Rust core is not line-coverage measured: it is held to the same bar by running the entire conformance suite, bit-exact, against it as well, plus its own cargo test unit tests.

This is strong conformance evidence, not proof. It has not received an independent cryptographic audit or NIST validation; see What this is not below.

Conformance is verified at the round level, not just the output level

All nine published NIST sample vectors pass in both directions. That alone is a weak statement: nine input/output pairs can be satisfied by two bugs that cancel out.

So this package also asserts the per-round intermediate values the NIST sample document publishes — P, Q, R, S, y, m, c and C, plus the derived u, v, b and d — for every round of every sample, 90 rounds in total. Compensating bugs survive an output test. They do not survive this one.

The vectors are transcribed from the NIST document and stored as data files. They are never regenerated from this implementation, which would make them a record of whatever the code does rather than of what the standard requires.

Radices without published vectors are verified against an independent implementation

NIST publishes vectors for radix 10 and 36 only. Every other radix has none, so agreement with an independent implementation is the only correctness evidence available — expected values authored from this code would test nothing and lock in any bug permanently.

fpr-ff1 is therefore differential-tested against ubiq_security_fpe across radices 2, 10, 16, 32, 36, 62, 256 and 65535, including every length where the algorithm's internal block structure changes. The oracle is itself validated against all nine NIST vectors before a single comparison is trusted. Oracle-derived known-answer vectors are also frozen into the repository, so this evidence survives even if the (deprecated, unmaintained) oracle package one day stops installing.

Bijectivity is tested exhaustively for these two domains

For two domains small enough to enumerate completely — radix 2 at length 20 (1,048,576 values) and radix 10 at length 6 (1,000,000 values) — every point is encrypted and the image checked to be the full domain, with no gaps and no collisions. That is the strongest correctness statement available for a permutation, and it is run in CI rather than kept as a manual check.

The known failure modes are tested for by name

Published FF1 bugs cluster in a few places. Each 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

Migration is safe by construction

Output is byte-identical to ubiq_security_fpe, verified in both directions — old ciphertext decrypts with this library, and new ciphertext decrypts with the old one. Existing encrypted data stays readable, and a rollback strands nothing. See Migrating.

What this is not

Passing the published sample vectors is conformance evidence, not FIPS validation. This package is not FIPS 140 validated and makes no such claim. It also does not attempt key zeroization, and offers no constant-time guarantee — see SECURITY.md for the full statement of limitations.

Why FF1 only — and why FF3 is excluded

SP 800-38G originally specified two modes, FF1 and FF3. FF3 was revised to FF3-1 after an attack on the original construction, but Beyne subsequently demonstrated a weakness in the tweak schedule that affects FF3 and FF3-1 alike — the repair did not address the underlying problem.

The February 2025 second public draft of SP 800-38G Rev. 1 removes FF3 entirely, leaving FF1 as the only approved format-preserving mode.

fpr-ff1 will therefore never implement FF3 or FF3-1. This is a deliberate feature, not an omission: there is no configuration flag, no opt-in, and no plan to add one. If you need FF3 you need a different library, and you should first satisfy yourself that you actually need a mode NIST has withdrawn.

Domain limits are stricter than the 2016 text

This package implements SP 800-38G (2016, updated 2019) as the normative algorithm, but enforces the tightened constraints from the Rev. 1 second public draft:

Constraint This package SP 800-38G (2016)
Minimum domain radix ** minlen >= 1_000_000 radix ** minlen >= 100
Maximum length 2 ** 32 - 1 2 ** 32 - 1
Key sizes 128, 192, 256 bits same
Radix 2 <= radix < 2**16 — a deliberate supported subset of the spec's inclusive [2..2**16] 2 <= radix <= 2**16
Rounds exactly 10 same

The radix bound is an implementation limit, not a spec deviation: NIST permits an implementation to support a subset of radices, and this package supports 2..65535. Radix 65536 is excluded deliberately (its numerals do not fit in uint16-sized values, and no practical alphabet reaches it); if that ever changes, widening the accepted domain without changing existing behaviour will be a minor version, not a major one.

The minimum-domain rule is the one that will bite. A domain of only 100 values is trivially enumerable, so this package fails closed and rejects it. Concretely, min_length is 6 for radix 10 and 4 for radix 36 — inputs shorter than that raise LengthError, even though some older libraries (including ubiq_security_fpe) accept them.

Rev. 1 is still a draft. If it is finalised with different limits, that will be a breaking change and a major version.

Scope

  • In scope: FF1 encryption and decryption, numeral and string interfaces, alphabet handling, parameter validation, tests, documentation.
  • Out of scope, permanently: FF3/FF3-1, identifier generation, persistence, checksums, key generation/storage/derivation, application-specific defaults or alphabets.

Supported Python versions

3.12, 3.13 and 3.14 are the versions exercised in CI on Linux, macOS and Windows (stated in the trove classifiers). requires-python is >=3.12 with no upper bound: a capped requires-python becomes a hard resolution failure on future interpreters — a claim that they don't work, which cannot be known in advance — so the floor rises as the CI matrix grows rather than the ceiling punishing users of new Pythons.

Roadmap

Version Focus
1.0 Pure Python. Conformance, a stable API, and a single runtime dependency (cryptography). No compiled extension, no optional backends — one code path, and it is the one the vectors test.
1.1 Pure-Python performance. Subquadratic base conversion and an O(n) power-of-two fast path; ciphertext bit-identical to 1.0.0. Still one code path, still one dependency.
2.0 Optional accelerated backend. An opt-in faster path for high-throughput callers, with the pure-Python implementation retained as the reference and the default. Shipped as 2.0.0rc1.

The 2.0 backend is opt-in and additive: the pure-Python path is unchanged and remains the default, so existing callers are unaffected. The accelerated path is only worth having once the reference implementation is settled and there is a conformance suite strong enough to prove the two agree bit for bit — which is the point of the differential and interoperability tests, and which the 2.0 suite runs against both backends. The compiled backend removes the per-call overhead that dominates short inputs and shares 1.1's subquadratic conversion for long ones; see Backends for the measured numbers.

Nothing in the roadmap changes the scope boundary above. FF3 and FF3-1 remain permanently out of scope, and no release will add key management.

Performance

Measured on one core, CPython 3.12.13, macOS (Apple Silicon) — reproduce on your own hardware with just bench (benchmarks/timing.py is the harness):

Input Throughput Per numeral
6 numerals, radix 10 ~34,000 ops/s 29.1 µs/op
Instance construction ~770,000 /s 1.3 µs
n = 100, radix 10 — 1.1 µs
n = 1,000, radix 10 — 1.0 µs
n = 5,000, radix 10 — 1.1 µs
n = 20,000, radix 10 — 1.4 µs

The per-numeral cost is now flat across input lengths. Before 1.1.0 the internal conversion between a numeral sequence and a big integer was a digit-at-a-time loop, which is quadratic in the number of numerals — that was an implementation choice, not an algorithmic invariant, and 1.1.0 replaced it with subquadratic divide-and-conquer conversion plus an O(n) fast path for power-of-two radices (about 19× faster at n=20,000 radix 10, 25× at radix 256), with ciphertext bit-identical to 1.0.0 for every valid input. If you are sizing a nightly job over millions of rows, measure with just bench against production-representative hardware.

Backends

FF1 accepts a keyword-only backend parameter: "python" (the default, and the reference implementation) or "rust" (the opt-in compiled backend). Both produce bit-identical ciphertext and raise identical exceptions — validation runs in Python for both, so the typed errors and messages are the same. The compiled backend ships as fpr_ff1._rs inside the platform wheels; the pure-Python wheel and the sdist omit it, and requesting backend="rust" there raises a clear BackendError rather than an opaque ImportError.

Where the compiled backend is available. Native wheels are published for these platforms, each built once for the stable ABI (abi3) and so installable on CPython 3.12, 3.13 and 3.14:

Platform Wheel tag Requires
Linux x86_64 manylinux_2_34_x86_64 glibc 2.34 or newer
Linux aarch64 manylinux_2_34_aarch64 glibc 2.34 or newer
macOS x86_64 (Intel) macosx_10_12_x86_64 macOS 10.12 or newer
macOS arm64 (Apple silicon) macosx_11_0_arm64 macOS 11 or newer
Windows x64 win_amd64 —

Every one of these wheels is installed and tested on its own platform, on all three Python versions, before release. Everywhere else, including Linux with a glibc older than 2.34, musl distributions such as Alpine, other architectures, and free-threaded CPython builds, pip installs the pure-Python wheel instead. The default backend then works unchanged, and only backend="rust" raises BackendError. No native support is implied beyond the table.

Measured on one core, CPython 3.12.13, Linux x86_64 (AMD Ryzen AI Max+ PRO 395), extension built in release mode with rustc 1.98.1 — reproduce with just bench:

Input backend="python" backend="rust" Speedup
6 numerals, radix 10 29.5 µs/op 4.1 µs/op ~7.3×
n = 100, radix 10 110.6 µs/op 38.8 µs/op ~2.9×
n = 1,000, radix 10 966.0 µs/op 399.5 µs/op ~2.4×
n = 5,000, radix 10 5.3 ms/op 2.2 ms/op ~2.5×
n = 20,000, radix 10 27.9 ms/op 9.9 ms/op ~2.8×
n = 100, radix 256 145.6 µs/op 50.2 µs/op ~2.9×
n = 1,000, radix 256 2.2 ms/op 0.2 ms/op ~11×
n = 5,000, radix 256 11.2 ms/op 1.0 ms/op ~11×
n = 20,000, radix 256 45.7 ms/op 4.1 ms/op ~11×

On every shape measured here, the compiled backend is faster. At short inputs it eliminates the per-call cipher-context construction that dominates the pure-Python path (about 55% of an n=6 call). At long inputs both cores use the same subquadratic numeral conversion — divide and conquer, plus an O(n) byte-packing path for power-of-two radices, which is why radix 256 gains most — so the compiled core keeps its lead instead of being overtaken, as it was in 2.0.0rc1.

These are one machine's numbers, not a guarantee: the ratio depends on the interpreter, the CPU and the shape of your data, so measure your own inputs with just bench before choosing.

The two backends are complementary, not a replacement: the pure-Python path remains the reference and the default, needs no compiled extension, and produces bit-identical ciphertext.

API

FF1(key, radix, *, alphabet=None, tweak=b"", min_tweak_len=None, max_tweak_len=None, backend="python")

Parameter Description
key 16, 24, or 32 bytes.
radix Integer base of the numeral system.
alphabet Optional string of exactly radix unique characters; enables encrypt/decrypt.
tweak Default tweak used when not supplied per call. At most 2**32 - 1 bytes, the limit of FF1's four-byte tweak-length field.
min_tweak_len / max_tweak_len Optional per-instance tweak length bounds, each at most 2**32 - 1; a larger bound raises TweakLengthError rather than being clamped.
backend "python" (default, the reference) or "rust" (the opt-in compiled backend). See Backends.

The package exports fpr_ff1.__version__ — the version of the installed distribution. Callers recording which build produced a dataset should capture it alongside their data.

Instances are picklable and deep-copyable (the cipher objects are rebuilt on the far side), so an FF1 can be passed to multiprocessing workers or broadcast by PySpark. Note that pickling an instance serialises the key — see SECURITY.md.

Numeral interface

The primitive interface works on integers in [0, radix).

ciphertext = ff1.encrypt_numerals([1, 2, 3, 4, 5, 6])
plaintext = ff1.decrypt_numerals(ciphertext)

Accepted numeral types. Anything losslessly integral — int, IntEnum, and integers from other numeric libraries such as NumPy, which are normalised to Python int so fixed-width values cannot overflow in the internal big-integer arithmetic.

float, Decimal, Fraction and str are rejected with ValueRangeError, even when they compare equal to a valid numeral: 1.0 < 10 is True, so comparison alone is not a type check.

bool is rejected deliberately. True would otherwise encrypt silently as 1, and a list of booleans arriving here is a caller mistake, not an intent to encrypt ones and zeros.

The input must be a Sequence — something with a known length. A generator raises TypeError (not FF1Error), because that is misuse of the API rather than bad data; wrap it in list(...). The Sequence contract is enforced: mappings and sets are rejected, and a Sequence whose __len__ disagrees with the values it yields raises LengthError rather than encrypting a domain smaller than the enforced minimum.

String interface

When alphabet is provided, the string interface maps characters to numerals and back.

ff1.encrypt("123456")
ff1.decrypt("654321")

Alphabet uniqueness is by Unicode code point. FF1 operates on code points, so normalisation is the caller's responsibility. Precomposed é (U+00E9) and decomposed é (U+0065 U+0301) are visually identical but count as two distinct symbols, and an alphabet containing both is accepted. If your alphabet comes from user input or an external source, normalise it first:

import unicodedata

alphabet = unicodedata.normalize("NFC", alphabet)

Exceptions

Every rejection raises a typed exception derived from FF1Error. Nothing is silently truncated, padded, coerced or clamped.

Exception Raised when
KeyLengthError key is not 16, 24 or 32 bytes
RadixError radix outside 2 <= radix < 2**16
LengthError input length outside [min_length, max_length]
ValueRangeError a numeral outside [0, radix), or a character absent from the alphabet
TweakLengthError tweak outside the configured bounds
AlphabetError alphabet length mismatched to radix, or containing duplicates
BackendError backend is not a known name, or is "rust" and the compiled extension is not installed

AlphabetError signals malformed configuration (caught at construction); ValueRangeError signals malformed data (caught per call). They are deliberately distinct so callers can handle a programming error differently from a bad input record.

Thread safety

FF1 instances are thread-safe. No mutable state is shared between calls — every cipher context is created locally to the call that uses it — so separate calls on one instance may run concurrently and produce exactly the single-threaded results. There is no module-level or global state either, so any number of instances may be used concurrently. A web service may freely share one FF1 across request threads.

Thread-safe is not the same as parallel. The pure-Python backend holds the GIL throughout, so concurrent calls interleave rather than overlap. The compiled backend releases the GIL for the duration of the FF1 computation, so concurrent calls on one instance genuinely run in parallel — measured 2.9× on four threads (n = 5,000, radix 10) against 0.96× for the pure-Python control. Releasing the GIL also means a long call no longer stalls unrelated threads in the process. Reproduce both rows with just bench.

Migrating from ubiq_security_fpe

fpr-ff1 exists to replace ubiq_security_fpe, which was deprecated in favour of a SaaS client and is no longer maintained. The two produce identical ciphertext for identical inputs, so existing encrypted data stays readable — no re-encryption, no migration window, no rollback risk.

That claim is enforced by tests/test_interoperability.py, which checks both directions (old ciphertext decrypts with the new library and vice versa) across all three key sizes, tweaked and untweaked. Migration safety is treated as a correctness obligation, not a promise.

No compatibility shim ships, deliberately. A Context(...) / .Encrypt() drop-in would mean maintaining a permanent second API, in a naming style this project does not use, mirroring a library that is itself deprecated. The migration below is three mechanical edits per call site, and the part that would actually be hard — identical ciphertext — is already done.

API mapping

# before
from ubiq_security_fpe import ff1

ctx = ff1.Context(key, tweak, twk_min_len, twk_max_len, radix, alphabet)
ciphertext = ctx.Encrypt(plaintext, None)
plaintext = ctx.Decrypt(ciphertext, None)

# after
from fpr_ff1 import FF1

ctx = FF1(
    key, radix, alphabet=alphabet, tweak=tweak, min_tweak_len=twk_min_len, max_tweak_len=twk_max_len
)
ciphertext = ctx.encrypt(plaintext)
plaintext = ctx.decrypt(ciphertext)
ubiq_security_fpe fpr-ff1
ff1.Context(key, twk, twk_min_len, twk_max_len, radix, alpha) FF1(key, radix, alphabet=..., tweak=..., min_tweak_len=..., max_tweak_len=...)
ctx.Encrypt(pt, twk) ctx.encrypt(pt, twk)
ctx.Decrypt(ct, twk) ctx.decrypt(ct, twk)
— ctx.encrypt_numerals(...) / ctx.decrypt_numerals(...) (no alphabet needed)
RuntimeError for every rejection typed exceptions under FF1Error

Behaviour changes to check before you switch

  1. Shorter inputs are rejected. fpr-ff1 enforces the Rev. 1 draft's radix ** minlen >= 1_000_000; ubiq_security_fpe enforced the same rule, so ciphertext produced by the legacy library decrypts unchanged. But if your data contains values that only ever passed under the 2016 text's weaker >= 100 bound — through another library or a manual path — those inputs now raise LengthError. Before switching, check your shortest values against ctx.min_length for your radix (6 for radix 10, 4 for radix 36, 3 for radix 256).
  2. Errors are typed. Rejections raise KeyLengthError, RadixError, LengthError, ValueRangeError, TweakLengthError or AlphabetError — all subclasses of FF1Error — rather than bare RuntimeError. Catch FF1Error if you want the old catch-all behaviour.
  3. No M2Crypto dependency. fpr-ff1 depends only on cryptography.
  4. Alphabet is validated at construction. A wrong-length alphabet or one with duplicate characters raises AlphabetError immediately rather than misbehaving later.

FIPS disclaimer

Passing the published NIST sample vectors is evidence of conformance. It is not FIPS validation. This package makes no claims of FIPS 140 conformance.

Key material

fpr-ff1 does not attempt to zeroize key material. Python bytes are immutable and the interpreter may copy them during garbage collection.

Development

Requires Python 3.12, uv, and just.

just setup    # create venv and install deps
just quality  # format check, lint, typecheck, tests
just build    # quality gate + uv build
just secrets  # gitleaks scan (must be installed locally)

Documentation

  • docs/architecture.md — design and module overview
  • docs/developer-guide.md — setup, commands, testing, and CI
  • docs/directory-structure.md — repository layout
  • docs/configuration.md — FF1 constructor parameters and runtime constraints
  • docs/backlog.md — active and completed work
  • CHANGELOG.md — release history, including behaviour changes that affect accepted inputs
  • SECURITY.md — disclosure process and known limitations
  • CONTRIBUTING.md — how to contribute, including the vector-provenance rules
  • CODE_OF_CONDUCT.md — community standards

License

MIT

Release files for fpr-ff1 2.0.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.0.0rc2
File Size Uploaded
fpr_ff1-2.0.0rc2.tar.gz 907.5 kB Details

Built distributions (wheels)

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

Download URL fpr_ff1-2.0.0rc2.tar.gz
Size 907.5 kB
Tags Source
SHA-256 checksum
How to use checksums
ab2dd080eb46e7c0376080865a44a1ee8bffee87262a04d7f7e5db6ed6f60a59
BLAKE2b-256 checksum
How to use checksums
cbe763681fc9c9e398c2f3c646499f0d8c0adb0a0ab26219ae03d69c7ca20068
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 18, 2026.

Transparency log

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

Download URL fpr_ff1-2.0.0rc2-py3-none-any.whl
Size 26.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d4a24026f54af15572cf80db9ade1a3f284949a7083802533634da4cbc78e684
BLAKE2b-256 checksum
How to use checksums
015e4472487fce8331d6798903840651d1e514b59468964b22944cb612c8e372
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 18, 2026.

Transparency log

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

Download URL fpr_ff1-2.0.0rc2-cp312-abi3-win_amd64.whl
Size 199.6 kB
Tags CPython 3.12 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
3cbd7e6b412dc26a165239494c215924ec87b681066dbd94eb31cb4e03ac9af5
BLAKE2b-256 checksum
How to use checksums
051e044a2c46aa871ed2ef6ee40b1619a7b4bbaba923cf03e9a96fbe3162ff53
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 18, 2026.

Transparency log

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

Download URL fpr_ff1-2.0.0rc2-cp312-abi3-manylinux_2_34_x86_64.whl
Size 341.7 kB
Tags CPython 3.12 Linux glibc 2.34+ x86-64 abi3
SHA-256 checksum
How to use checksums
41452725897c0bbc76133de7aa15e7e5cadc69c2d5290ca0ff25f8216f976044
BLAKE2b-256 checksum
How to use checksums
276f0acc64abe74649d6e4149882780276f030c331157820fa9f4a48b71ae8a6
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 18, 2026.

Transparency log

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

Download URL fpr_ff1-2.0.0rc2-cp312-abi3-manylinux_2_34_aarch64.whl
Size 330.3 kB
Tags CPython 3.12 Linux glibc 2.34+ ARM64 abi3
SHA-256 checksum
How to use checksums
537be8d829d542f46b4e23666cd1db82b833e6876eced4c05050077ea8e3b875
BLAKE2b-256 checksum
How to use checksums
bd64d1ece34241e0c2b5c295bd4a0aa48e0002ae937fe100c09802a9030a2311
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 18, 2026.

Transparency log

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

Download URL fpr_ff1-2.0.0rc2-cp312-abi3-macosx_11_0_arm64.whl
Size 294.8 kB
Tags CPython 3.12 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
7dfefb3c29998aa8a87637dc3b3099050e8bc6db3ccbeb09d5e510f52e48ad35
BLAKE2b-256 checksum
How to use checksums
9838cb54642eafd83122a2dfd021ddf4533651c61485fb5d8ea8c3c6d78f9cda
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 18, 2026.

Transparency log

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

Download URL fpr_ff1-2.0.0rc2-cp312-abi3-macosx_10_12_x86_64.whl
Size 300.0 kB
Tags CPython 3.12 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
7ad48c3be68254e171bc9b9943f840d04021c188aa8088017e89c163f96f87ab
BLAKE2b-256 checksum
How to use checksums
25763ce9c4cd3da27b7167293107ecff1f7187a8275391e2ed0b5231cd01dbdb
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 18, 2026.

Transparency log

Release history Release notifications | RSS feed

2.0.0

7 release files

This release

2.0.0rc2 This release

7 release files

1.1.0

2 release files

1.0.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

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