Skip to content

Commit a11edfe

Browse files
g-andradeclaude
andcommitted
Polish docs and drop OTP 22 crypto scaffolding
- README: replace the removed Java-implementation attribution with a pointer to the sibling `ex_fpe` library; fix radix range to 2..65535. - AGENTS.md: rewrite the architecture/gotchas to match the `ex_fpe`-style algorithm (do_encdec/setup_encdec_vars/#aux record/prf/ciph), correct the -spec and dialyzer notes, and record the non_negative_rem/2 pitfall. - Module docs: bump the documented radix range to 2..65535. - rebar.config: drop the now-unused POST_OTP_22 platform_define and raise minimum_otp_vsn to 24.0 (crypto:crypto_one_time/5 is used unconditionally). - CHANGELOG: note the minimum OTP bump. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a7a79c4 commit a11edfe

5 files changed

Lines changed: 41 additions & 26 deletions

File tree

AGENTS.md

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ All checks run sequentially (`.NOTPARALLEL`). CI runs `make check-fast`,
2626
## Compiler flags
2727

2828
Always on: `warn_export_vars`, `warn_missing_spec`, `warn_unused_import`,
29-
`warnings_as_errors`. Every function (including internal helpers) carries a
30-
`-spec`. The `test` profile relaxes `warn_missing_spec` and
31-
`warnings_as_errors`.
29+
`warnings_as_errors`. `warn_missing_spec` only requires specs on **exported**
30+
functions, so the public API and the `validate_*` helpers are spec'd, but the
31+
internal round/PRF helpers (`do_encdec/7`, `do_encrypt_rounds/5`, etc.) are not.
32+
The `test` profile relaxes `warn_missing_spec` and `warnings_as_errors`.
3233

3334
## Architecture
3435

@@ -41,10 +42,27 @@ There is one module, `erlffx`, with a four-function public API:
4142
| `decrypt/2` | Reverse `encrypt/2` |
4243

4344
Internally: `config/3` merges params into a `raw_config()` map and validates it
44-
into the opaque `config()` (mandatory keys). `encrypt`/`decrypt` run the
45-
alternating Feistel network (`encrypt_loop`/`decrypt_loop`); each round's PRF
46-
`fk/4` builds the FFX `P`/`Q` blocks and runs AES-CBC-MAC (`aes_cbc_mac/2`
47-
`aes_cbc/3`). Integers are carried as `maginteger()` `{Value, Magnitude}` pairs.
45+
into the opaque `config()` (mandatory keys). Both `encrypt/2` and `decrypt/2`
46+
delegate to `do_encdec/7` (a `DoEncrypt` boolean picks the direction).
47+
`setup_encdec_vars/6` derives the per-call parameters into an `#aux{}` record
48+
(`u`/`v` halves, `small_b`, `d`, the `P` block, the `Q` zero-padding) and splits
49+
the input integer into its `{A, B}` halves via `split_num_string_int/3`. The
50+
Feistel network then runs as `do_encrypt_rounds/5` (rounds `0..NumberOfRounds-1`)
51+
or `do_decrypt_rounds/4` (rounds counting back down to `-1`). Each round builds
52+
the FFX `P || Q` blocks, computes `R = prf/2` (AES-CBC-MAC: `crypto_one_time/5`
53+
in CBC mode, keeping the last 16 bytes), expands it to `S` with
54+
`compute_s_blocks/3` (`R || CIPH(R⊕1) || …` using `ciph/2`, AES-ECB), and folds
55+
`Y = NUM(S)` into the running half with `non_negative_rem/2` (addition when
56+
encrypting, subtraction when decrypting) modulo `radix^m`. The result is
57+
reconstructed as `FinalB + FinalA * radix^v`. Integers are plain Erlang
58+
integers throughout — the earlier `maginteger()` `{Value, Magnitude}` pairs are
59+
gone.
60+
61+
> **`non_negative_rem/2` gotcha:** it must fold the result fully into `[0, B)`
62+
> via `((A rem B) + B) rem B`. A negative `A` that is an exact multiple of `B`
63+
> makes `A rem B` zero, so the naïve `(A rem B) + B` wrongly returns `B` — which
64+
> broke `decrypt/2` round-trips for the rare inputs where `B - Y` lands on an
65+
> exact multiple of `radix^m`.
4866
4967
## Conventions and gotchas
5068

@@ -53,10 +71,10 @@ alternating Feistel network (`encrypt_loop`/`decrypt_loop`); each round's PRF
5371
26 (its `-doc` triple-quoted-string handling is broken there); `rebar3_hank`
5472
is dropped on OTP 29 (katana_code bug). So `make check-fast` is a partial
5573
no-op on those OTP releases — develop/lint on OTP 27+ (28 is the reference).
56-
- **Crypto API split**: `aes_cbc/3` uses `crypto:crypto_one_time/5` on OTP ≥ 23
57-
and the removed `crypto:block_encrypt/4` on OTP 22, selected by the
58-
`POST_OTP_22` `platform_define`. `minimum_otp_vsn` is `22.0` but only **24+**
59-
is supported.
74+
- **Crypto**: `prf/2` and `ciph/2` call `crypto:crypto_one_time/5`
75+
unconditionally (no OTP 22 `crypto:block_encrypt/4` fallback anymore). The old
76+
`POST_OTP_22` `platform_define` is gone and `minimum_otp_vsn` is `24.0`, which
77+
matches what is actually supported.
6078
- **Docs are EEP-48**, not edoc text. Public docs are `-moduledoc`/`-doc`
6179
triple-quoted attributes guarded by `-ifdef(E48).` (`E48` is defined on OTP
6280
27+). The internal helpers are simply unexported, so ex_doc omits them — no
@@ -72,10 +90,11 @@ alternating Feistel network (`encrypt_loop`/`decrypt_loop`); each round's PRF
7290
`dont_repeat_yourself` and `max_line_length` because the known-answer test
7391
vectors are intentionally repetitive and carry long literals (256-bit keys,
7492
digit lists). `src/` uses the default ruleset with no exceptions.
75-
- **dialyzer** runs with `underspecs` on. Keep specs tight: the P value is
76-
`<<_:128>>`, magnitudes are `pos_integer()`, the opaque `config()` uses
77-
mandatory (`:=`) keys, and the AES key/CBC data flow is typed over binaries
78-
(`validate_aes_key/1` normalizes any `iodata()` key to a binary).
93+
- **dialyzer** runs with `underspecs` on. Keep specs tight: the opaque
94+
`config()` uses mandatory (`:=`) keys, the `#aux{}` record fields are typed
95+
(`u`/`v` as `pos_integer()`, `radix()`, binaries for `p`/`q_zeros`), and the
96+
AES key flow is typed over binaries (`validate_aes_key/1` normalizes any
97+
`iodata()` key to a binary).
7998

8099
## Tests
81100

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99
### Changed
1010

1111
- max radix from 255 to 65535
12+
- minimum OTP version to 24
1213
- implementation to be in the spirit of
1314
[`ex_fpe`](https://github.com/g-andrade/ex_fpe)
1415

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@
77
`erlffx` is an Erlang implementation of the mechanism described in the 2010
88
paper [The FFX Mode of Operation for Format-Preserving
99
Encryption](https://csrc.nist.gov/csrc/media/projects/block-cipher-techniques/documents/bcm/proposed-modes/ffx/ffx-spec.pdf)
10-
by Bellare, Rogaway and Spies. It is based on an existing [Java
11-
implementation](https://github.com/michaeltandy/java-ffx-format-preserving-encryption)
12-
by Michael Tandy.
10+
by Bellare, Rogaway and Spies. Its algorithm follows the sibling Elixir
11+
library [`ex_fpe`](https://github.com/g-andrade/ex_fpe).
1312

1413
* AES-128 / AES-192 / AES-256 keys are supported (CBC mode)
1514
* Any positive word length is supported
16-
* Any radix / alphabet size between 2 and 255 is acceptable (10 by default)
15+
* Any radix / alphabet size between 2 and 65535 is acceptable (10 by default)
1716
* Optional 'tweak' values may be defined
1817
* The number of rounds is configurable (10 by default)
1918

rebar.config

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
%% == Erlang Compiler == %%
44

5-
% but only 24+ is supported
6-
{minimum_otp_vsn, "22.0"}.
5+
{minimum_otp_vsn, "24.0"}.
76

87
{erl_opts, [
98
debug_info,
@@ -12,9 +11,6 @@
1211
warn_unused_import,
1312
warnings_as_errors,
1413
%
15-
% crypto API split: crypto:crypto_one_time/5 (23+) vs crypto:block_encrypt/4
16-
{platform_define, "^((2[3-9])|([3-9]))", 'POST_OTP_22'},
17-
%
1814
% EEP-48 doc attributes (OTP 27+)
1915
{platform_define, "^2[7-9]\\.", 'E48'},
2016
{platform_define, "^[3-9]", 'E48'}

src/erlffx.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ a chosen radix, so the ciphertext keeps the format of the plaintext. See the
1313

1414
- AES-128 / AES-192 / AES-256 keys are supported (CBC mode).
1515
- Any positive word length is supported.
16-
- Any radix / alphabet size between 2 and 255 is acceptable (10 by default).
16+
- Any radix / alphabet size between 2 and 65535 is acceptable (10 by default).
1717
- Optional 'tweak' values may be defined.
1818
- The number of Feistel rounds is configurable (10 by default).
1919
""".
@@ -120,7 +120,7 @@ Builds an opaque encryption `t:config/0`.
120120

121121
- `AesKey` must be a 16-, 24- or 32-byte (AES-128/192/256) key.
122122
- `ValueLength` is the word length, in digits of the chosen radix.
123-
- `OptionalParams` may set the `tweak`, the `radix` (2..255, default 10) and
123+
- `OptionalParams` may set the `tweak`, the `radix` (2..65535, default 10) and
124124
the `number_of_rounds` (default 10).
125125

126126
The same `t:config/0` must be used to `encrypt/2` and `decrypt/2` a value.

0 commit comments

Comments
 (0)