fpr-ff1 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. fpr_ff1-0.1.0/.codegraph/.gitignore +5 -0
  2. fpr_ff1-0.1.0/.gitattributes +21 -0
  3. fpr_ff1-0.1.0/.github/workflows/ci.yml +108 -0
  4. fpr_ff1-0.1.0/.github/workflows/publish.yml +41 -0
  5. fpr_ff1-0.1.0/.gitignore +37 -0
  6. fpr_ff1-0.1.0/.gitleaks.toml +4 -0
  7. fpr_ff1-0.1.0/.python-version +1 -0
  8. fpr_ff1-0.1.0/AGENTS.md +140 -0
  9. fpr_ff1-0.1.0/CHANGELOG.md +9 -0
  10. fpr_ff1-0.1.0/LICENSE +21 -0
  11. fpr_ff1-0.1.0/PKG-INFO +348 -0
  12. fpr_ff1-0.1.0/README.md +322 -0
  13. fpr_ff1-0.1.0/SECURITY.md +86 -0
  14. fpr_ff1-0.1.0/docs/AGENTS.md +25 -0
  15. fpr_ff1-0.1.0/docs/architecture.md +59 -0
  16. fpr_ff1-0.1.0/docs/backlog.md +62 -0
  17. fpr_ff1-0.1.0/docs/configuration.md +28 -0
  18. fpr_ff1-0.1.0/docs/developer-guide.md +107 -0
  19. fpr_ff1-0.1.0/docs/directory-structure.md +45 -0
  20. fpr_ff1-0.1.0/justfile +69 -0
  21. fpr_ff1-0.1.0/pyproject.toml +120 -0
  22. fpr_ff1-0.1.0/src/fpr_ff1/__init__.py +23 -0
  23. fpr_ff1-0.1.0/src/fpr_ff1/_exceptions.py +34 -0
  24. fpr_ff1-0.1.0/src/fpr_ff1/_ff1.py +549 -0
  25. fpr_ff1-0.1.0/src/fpr_ff1/py.typed +0 -0
  26. fpr_ff1-0.1.0/tests/__init__.py +0 -0
  27. fpr_ff1-0.1.0/tests/_oracle/__init__.py +95 -0
  28. fpr_ff1-0.1.0/tests/_oracle/_m2crypto_shim.py +85 -0
  29. fpr_ff1-0.1.0/tests/conftest.py +18 -0
  30. fpr_ff1-0.1.0/tests/test_contract.py +205 -0
  31. fpr_ff1-0.1.0/tests/test_differential.py +231 -0
  32. fpr_ff1-0.1.0/tests/test_exact_arithmetic.py +140 -0
  33. fpr_ff1-0.1.0/tests/test_intermediates.py +59 -0
  34. fpr_ff1-0.1.0/tests/test_interoperability.py +120 -0
  35. fpr_ff1-0.1.0/tests/test_nist_vectors.py +54 -0
  36. fpr_ff1-0.1.0/tests/test_properties.py +147 -0
  37. fpr_ff1-0.1.0/tests/test_smoke.py +70 -0
  38. fpr_ff1-0.1.0/tests/test_validation.py +497 -0
  39. fpr_ff1-0.1.0/tests/vectors/nist_ff1_intermediates.json +7797 -0
  40. fpr_ff1-0.1.0/tests/vectors/nist_ff1_samples.json +87 -0
  41. fpr_ff1-0.1.0/uv.lock +581 -0
@@ -0,0 +1,5 @@
1
+ # CodeGraph data files — local to each machine, not for committing.
2
+ # Ignore everything in .codegraph/ except this file itself, so transient
3
+ # files (the database, daemon.pid, sockets, logs) never show up in git.
4
+ *
5
+ !.gitignore
@@ -0,0 +1,21 @@
1
+ # Normalise line endings to LF in the repository and on checkout, on every
2
+ # platform.
3
+ #
4
+ # Without this, Windows checkouts get CRLF (git's core.autocrlf defaults to
5
+ # true on GitHub's Windows runners) while `[tool.ruff.format] line-ending`
6
+ # is "lf", so `ruff format --check` reports every file as needing reformatting
7
+ # and the Windows CI leg fails before it runs a single test.
8
+ * text=auto eol=lf
9
+
10
+ # Test fixtures are compared byte-for-byte against transcribed NIST values;
11
+ # never let any tool rewrite them.
12
+ tests/vectors/*.json text eol=lf
13
+
14
+ # Lockfiles and generated artefacts.
15
+ uv.lock text eol=lf
16
+
17
+ # Anything genuinely binary, so git does not attempt conversion.
18
+ *.png binary
19
+ *.jpg binary
20
+ *.gz binary
21
+ *.whl binary
@@ -0,0 +1,108 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+ # Allows publish.yml to reuse this workflow as a release gate.
9
+ workflow_call:
10
+
11
+ concurrency:
12
+ group: ci-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ env:
16
+ # Turn a missing differential oracle into a hard failure. Those tests are
17
+ # the only coverage for radices without published NIST vectors, so a silent
18
+ # skip would look exactly like a pass.
19
+ FPR_FF1_REQUIRE_ORACLE: "1"
20
+
21
+ jobs:
22
+ quality:
23
+ name: ${{ matrix.os }} / py${{ matrix.python-version }}
24
+ runs-on: ${{ matrix.os }}
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ os: [ubuntu-latest, macos-latest, windows-latest]
29
+ python-version: ["3.12", "3.13", "3.14"]
30
+
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - name: Install uv
35
+ uses: astral-sh/setup-uv@v5
36
+ with:
37
+ enable-cache: true
38
+ cache-dependency-glob: uv.lock
39
+
40
+ - name: Set up Python ${{ matrix.python-version }}
41
+ run: uv python install ${{ matrix.python-version }}
42
+
43
+ - name: Sync dependencies
44
+ run: uv sync --all-extras --dev --python ${{ matrix.python-version }}
45
+
46
+ - name: Format check
47
+ run: uv run ruff format --check .
48
+
49
+ - name: Lint
50
+ run: uv run ruff check .
51
+
52
+ - name: Type check
53
+ run: uv run pyright
54
+
55
+ # FPR_FF1_REQUIRE_ORACLE (set at workflow level) makes a missing oracle
56
+ # raise at collection time, so this run cannot pass with the
57
+ # differential and interoperability suites silently skipped.
58
+ - name: Test with 100% coverage gate
59
+ run: uv run pytest
60
+
61
+ build:
62
+ name: Build distributions
63
+ runs-on: ubuntu-latest
64
+ needs: quality
65
+ steps:
66
+ - uses: actions/checkout@v4
67
+
68
+ - name: Install uv
69
+ uses: astral-sh/setup-uv@v5
70
+ with:
71
+ enable-cache: true
72
+
73
+ - name: Build
74
+ run: uv build
75
+
76
+ - name: Check metadata
77
+ run: uvx twine check dist/*
78
+
79
+ - uses: actions/upload-artifact@v4
80
+ with:
81
+ name: distributions
82
+ path: dist/
83
+
84
+ secrets:
85
+ name: Secret scan
86
+ runs-on: ubuntu-latest
87
+ env:
88
+ # Pinned so a scanner update cannot turn a release red without warning.
89
+ GITLEAKS_VERSION: "8.30.1"
90
+ steps:
91
+ - uses: actions/checkout@v4
92
+ with:
93
+ fetch-depth: 0
94
+
95
+ # The gitleaks CLI is used rather than gitleaks/gitleaks-action, which
96
+ # rejects `release` events outright ("ERROR: The [release] event is not
97
+ # yet supported"). publish.yml reuses this workflow as its release gate,
98
+ # so the action failed there and blocked publishing. The CLI has no
99
+ # event restrictions and runs the same command as `just secrets`.
100
+ - name: Install gitleaks
101
+ run: |
102
+ curl -sSfL \
103
+ "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
104
+ | sudo tar -xz -C /usr/local/bin gitleaks
105
+ gitleaks version
106
+
107
+ - name: Scan for secrets
108
+ run: gitleaks dir . --redact --exit-code 1
@@ -0,0 +1,41 @@
1
+ name: Publish
2
+
3
+ # Publishes to PyPI via Trusted Publishing (OIDC). No API tokens are stored
4
+ # in this repository; configure the publisher at
5
+ # https://pypi.org/manage/project/fpr-ff1/settings/publishing/
6
+
7
+ on:
8
+ release:
9
+ types: [published]
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ quality:
17
+ # Never publish something that has not passed the full matrix.
18
+ uses: ./.github/workflows/ci.yml
19
+
20
+ publish:
21
+ needs: quality
22
+ runs-on: ubuntu-latest
23
+ environment:
24
+ name: pypi
25
+ url: https://pypi.org/p/fpr-ff1
26
+ permissions:
27
+ id-token: write # required for Trusted Publishing
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Install uv
32
+ uses: astral-sh/setup-uv@v5
33
+
34
+ - name: Build
35
+ run: uv build
36
+
37
+ - name: Check metadata
38
+ run: uvx twine check dist/*
39
+
40
+ - name: Publish to PyPI
41
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,37 @@
1
+ # Secrets
2
+ .env
3
+ .env.*
4
+ !.env.sample
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+ .python-version.local
11
+ .venv/
12
+ venv/
13
+ ENV/
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+ .mypy_cache/
17
+ .pyright/
18
+ # Hypothesis property-testing scratch database (per-machine, never commit).
19
+ .hypothesis/
20
+ .coverage
21
+ coverage.xml
22
+ htmlcov/
23
+ dist/
24
+ build/
25
+ *.egg-info/
26
+
27
+ # Editors and OS
28
+ .DS_Store
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+
34
+ # Logs and local scratch
35
+ *.log
36
+ .tmp/
37
+ tmp/
@@ -0,0 +1,4 @@
1
+ [allowlist]
2
+ paths = [
3
+ '''tests/vectors/''',
4
+ ]
@@ -0,0 +1 @@
1
+ 3.12.13
@@ -0,0 +1,140 @@
1
+ # AGENTS.md
2
+
3
+ Agent contract for this repository. Read fully before any code change.
4
+
5
+ ## Project
6
+
7
+ Open-source Python implementation of **FF1**, the format-preserving encryption mode from NIST SP 800-38G. Published as a standalone PyPI library (`fpr-ff1`; claiming the name and configuring Trusted Publishing is a pre-1.0 backlog item) — no network, accounts, or key-management features. It replaces `ubiq_security_fpe`, deprecated in favour of a SaaS client and no longer maintained.
8
+
9
+ The design goal: a reviewer can compare the source against SP 800-38G line by line and find no gaps.
10
+
11
+ ### Scope
12
+
13
+ - **In scope:** FF1 encrypt/decrypt, parameter validation, numeral and string interfaces, alphabet handling, tests, docs.
14
+ - **Out of scope, permanently:** FF3/FF3-1, identifier generation, persistence, checksums, key generation/storage/derivation, application-specific defaults or alphabets.
15
+ - A private downstream project consumes this library. **Nothing about that consumer may leak into this package.** Reject radix-32 defaults, fixed 13-numeral lengths, or any caller-specific convenience; a feature that only makes sense for one caller belongs in the caller.
16
+
17
+ ### FF1 only
18
+
19
+ FF3 and FF3-1 are **not** in scope and must never be added. The February 2025 second public draft of SP 800-38G Rev. 1 removes FF3 entirely, following Beyne's tweak-schedule weakness affecting both FF3 and FF3-1. Being FF1-only is a deliberate feature; the README says so.
20
+
21
+ ## Standards baseline
22
+
23
+ SP 800-38G (2016, updated 2019) is the normative text, plus the Rev. 1 second public draft's tightened limits:
24
+
25
+ - Minimum domain: `radix ** minlen >= 1_000_000` — stricter than the 2016 text's `>= 100`; a deliberate fail-closed choice noted in the changelog.
26
+ - Maximum length: `maxlen < 2**32`, implemented as `2**32 - 1` (fail closed on the boundary).
27
+ - Key sizes: 128, 192, 256 bits only.
28
+ - Radix: `2 <= radix < 2**16`.
29
+ - Forward AES only; no inverse cipher function.
30
+ - Exactly 10 rounds.
31
+ - **No floating-point arithmetic anywhere in the FF1 core.**
32
+
33
+ Rev. 1 is still a draft. Track its status; if it is finalised with different limits, that is a breaking change requiring a major version. The README documents the draft baseline.
34
+
35
+ ## Public API
36
+
37
+ A single `FF1` class:
38
+
39
+ ```python
40
+ class FF1:
41
+ def __init__(
42
+ self,
43
+ key: bytes, # 16, 24 or 32 bytes
44
+ radix: int,
45
+ *,
46
+ alphabet: str | None = None, # enables the str interface
47
+ tweak: bytes = b"", # default tweak
48
+ min_tweak_len: int | None = None,
49
+ max_tweak_len: int | None = None,
50
+ ) -> None: ...
51
+
52
+ # numeral interface - the primitive
53
+ def encrypt_numerals(self, x: Sequence[int], tweak: bytes | None = None) -> list[int]: ...
54
+ def decrypt_numerals(self, x: Sequence[int], tweak: bytes | None = None) -> list[int]: ...
55
+
56
+ # string interface - requires alphabet
57
+ def encrypt(self, s: str, tweak: bytes | None = None) -> str: ...
58
+ def decrypt(self, s: str, tweak: bytes | None = None) -> str: ...
59
+
60
+ @property
61
+ def min_length(self) -> int: ...
62
+ @property
63
+ def max_length(self) -> int: ...
64
+ ```
65
+
66
+ Rules:
67
+
68
+ - `encrypt_numerals` / `decrypt_numerals` are the primitive; `encrypt` / `decrypt` are thin string wrappers. Do not duplicate logic between them.
69
+ - String methods without an `alphabet` raise, with a message pointing at the numeral interface.
70
+ - Validate `alphabet` length and uniqueness at construction.
71
+ - No global or module-level state, no implicit default key, no environment-variable configuration.
72
+ - Every rejection raises a typed exception rooted at `FF1Error`; never silently truncate, pad, coerce, or clamp.
73
+
74
+ ## Implementation gotchas
75
+
76
+ These are the failure modes that produce **plausible but wrong output** — every one still runs without raising.
77
+
78
+ - `b` is derived from `v`, not `u`. They differ when `n` is odd.
79
+ - Bit length is `(radix ** v - 1).bit_length()`. Never `math.log2`, `math.log`, `math.ceil`, `**0.5`, or float literals — this is the Bouncy Castle bug class. Leave a comment saying so, or it will be "simplified" later.
80
+ - Padding is `(-t - b - 1) % 16`. Python's modulo already returns the non-negative result; do not rewrite it defensively.
81
+ - Encrypt and decrypt differ in exactly three places: `Q` built from `B` vs `A`; round order `0..9` vs `9..0`; final assignment `A, B = B, C` vs `B, A = A, C`. The parity rule `m = u if i % 2 == 0 else v` is **identical in both** — do not mirror it.
82
+ - `S` is truncated to `d` bytes, not `d` bits.
83
+ - The PRF is CBC-MAC with a zero IV over 16-byte-aligned input.
84
+ - Cipher context caching: cache **one ECB encryptor** and call `update()` repeatedly (roughly 1.6x). Never call `finalize()` on it. Never cache a CBC encryptor (it carries chaining state).
85
+ - Cite spec steps in internal docstrings, e.g. "SP 800-38G Algorithm 7, step 6.iii".
86
+
87
+ ## Local development
88
+
89
+ - Python `>=3.12,<3.15` (CI matrix: 3.12, 3.13, 3.14 across Linux, macOS, Windows). Local dev on 3.12. Tools: `uv`, `just`. `src/` layout; ships `py.typed`.
90
+ - `just setup` — create `.venv` and install deps.
91
+ - `just quality` — format check, lint, typecheck, tests with coverage.
92
+ - `just build` — quality gate plus `uv build`.
93
+ - `just secrets` — gitleaks scan (must be installed locally).
94
+
95
+ `pyproject.toml` enforces a **100% line and branch coverage floor** on the FF1 module (`fail_under = 100`, branch coverage on). Every raise path must be exercised by a test; delete unreachable branches rather than leaving dead code.
96
+
97
+ Documentation is maintained under `docs/`. Update the matching doc file in the same change that makes it true; see `docs/AGENTS.md`.
98
+
99
+ ## Tests
100
+
101
+ Conformance is the product. A change that makes tests pass by weakening them is a defect.
102
+
103
+ 1. **NIST sample vectors:** all 9 published samples, encrypt and decrypt (AES-128/192/256, radix 10 with/without tweak, radix 36).
104
+ 2. **Per-round intermediates — the real conformance test:** the NIST document publishes `P`, `Q`, `R`, `S`, `y`, `m`, `c`, `C`, plus derived `u`, `v`, `b`, `d` for every round. Transcribe these and assert round-by-round via a **test-only trace hook** (never exported from the public API). Two compensating bugs can pass an output test; they cannot pass an intermediate test.
105
+ 3. **Parameter validation:** exercise every rejection path — key lengths 0/15/17/23/25/31/33 bytes; radix 0, 1, `2**16`, negative; lengths below `min_length` (and at `min_length` exactly, which must succeed) and above `max_length`; numerals negative or `>= radix`; tweak bounds; alphabet length mismatch, duplicate characters, non-str; string characters absent from the alphabet; string methods without an alphabet. Verify `min_length` across radices: 2 → 20, 10 → 6, 16 → 5, 32 → 4, 36 → 4, 256 → 3.
106
+ 4. **Exact-arithmetic regression:** radices where `radix ** v` sits exactly on a power-of-two boundary (2, 4, 8, 16, 32, 64, 256) — precisely where float evaluation flips to the wrong integer. Also static-scan (AST or token) the FF1 module for float ops (`math.log`, `math.log2`, `math.ceil`, `math.pow`, `/`, float literals) and fail CI on violation.
107
+ 5. **Property-based (Hypothesis):** round-trip; length and alphabet preservation; tweak sensitivity; key sensitivity (a one-bit key change produces unrelated output); determinism.
108
+ 6. **Bijectivity:** for exhaustively enumerable domains (e.g. radix 2 length 20, radix 10 length 6), confirm the image is the full domain — no gaps, no repeats.
109
+ 7. **Differential testing:** radix 10 and 36 have NIST vectors; **every other radix has none.** Compare against independent oracles (`ubiq-security-fpe`, Rust `fpe` crate), dev dependencies only, pinned. Never commit self-generated outputs as "vectors" — that tests nothing and locks in bugs permanently. Mark oracle tests optional-but-run-in-CI so a missing oracle does not block local development.
110
+ 8. **Edge cases:** empty tweak vs absent tweak (must be equivalent); very long tweaks; minimum and maximum practical lengths; odd and even `n` (the `u != v` path); all-zero and all-max numerals; radix 2 and radix `2**16 - 1`.
111
+ 9. **Interoperability:** a documented test matching `ubiq_security_fpe` output for the same inputs, so migrating users can verify ciphertext portability. A correctness obligation to downstream users, not a nicety.
112
+
113
+ Vector files live in `tests/vectors/` as JSON, never inline literals. Never regenerate NIST fixtures from this implementation.
114
+
115
+ ## Never do
116
+
117
+ - Never add FF3 or FF3-1.
118
+ - Never add key generation, storage, derivation, or management helpers.
119
+ - Never claim FIPS validation; passing published vectors is conformance evidence only. The README states this explicitly.
120
+ - Never claim key zeroization; Python bytes are immutable and the GC copies them. Document as a known limitation.
121
+ - Never regenerate NIST fixtures from this implementation.
122
+ - Never add a runtime dependency beyond `cryptography` without explicit approval.
123
+ - Never add application-specific defaults, convenience wrappers, or alphabets shaped to one caller.
124
+ - Never let a test assert current behaviour where it should assert specified behaviour.
125
+
126
+ ## Conventions
127
+
128
+ - Full type annotations; ship `py.typed`. Runtime dependency: `cryptography` only.
129
+ - Licence: MIT (decided).
130
+ - Semantic versioning; any change to accepted inputs or produced outputs is a major version.
131
+ - Migration from `ubiq_security_fpe` is **guide only** (decided 2026-08-21); no compatibility shim ships.
132
+ - CI matrix across all supported Python versions on Linux, macOS and Windows. Publish via PyPI Trusted Publishing; do not commit tokens.
133
+ - README must cover: what FF1 is, why FF3 is excluded, the Rev. 1 constraints applied, the FIPS disclaimer, and a migration section for `ubiq_security_fpe` users. Include `SECURITY.md` with a disclosure contact.
134
+ - Prefer clarity over cleverness. This module is read far more often than written, and a subtle bug is invisible without the vectors.
135
+
136
+ ## Open decisions
137
+
138
+ Ask before deciding:
139
+
140
+ 1. Optional accelerated backend (backlogged for post-1.0): whether to build it at all; if built, opt-in only — the pure-Python implementation stays the reference and the default.
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html); per the project
7
+ contract, **any change to accepted inputs or produced outputs is a major version**.
8
+
9
+ ## [Unreleased]
fpr_ff1-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joel Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.