@@ -26,9 +26,10 @@ All checks run sequentially (`.NOTPARALLEL`). CI runs `make check-fast`,
2626## Compiler flags
2727
2828Always 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
4344Internally: ` 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
0 commit comments