- Pascal 87%
- Shell 7.7%
- Makefile 5.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Free Pascal Argon2id CI / Build (push) Successful in 16s
Free Pascal Argon2id CI / Test: Tree Clean (push) Successful in 5s
Free Pascal Argon2id CI / Test: Units (push) Successful in 14s
Free Pascal Argon2id CI / Test: Reference (push) Successful in 17s
Free Pascal Argon2id CI / Test: Install (push) Successful in 14s
Reviewed-on: #1 |
||
| .forgejo/workflows | ||
| tests | ||
| .gitignore | ||
| .gitlab-ci.yml | ||
| Argon2Cli.pp | ||
| Argon2id.pp | ||
| Argon2idCore.pp | ||
| Argon2idPHC.pp | ||
| Argon2idRandom.pp | ||
| Blake2b.pp | ||
| CliDiffTest.sh | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
Argon2id for Free Pascal
A pure Free Pascal implementation of Argon2id v1.3 (RFC 9106),
with PHC string encoding, and a drop-in reimplementation of the reference
argon2 command line tool.
No C library, no bindings, no external dependencies — just fpc and make.
Output is byte-for-byte identical to the PHC reference implementation, so
hashes interoperate in both directions with libsodium, PHP password_hash,
Python passlib, and the argon2 CLI.
uses Argon2id;
Stored := TArgon2id.Hash('correct horse battery staple');
// $argon2id$v=19$m=65536,t=3,p=4$ltP1+9bd0a30GIgTC9VMEg$0IYTyTD3pldG86QZQsj6/gyai68G7uhuCrruqI/N3RU
if TArgon2id.Verify(Stored, Attempt) then
if TArgon2id.NeedsRehash(Stored) then
Stored := TArgon2id.Hash(Attempt); // costs have been raised since signup
Contents
| File | |
|---|---|
Argon2id.pp |
Facade: TArgon2id, type aliases, procedural wrappers |
Argon2idCore.pp |
The algorithm: TArgon2idHasher, TArgon2Base, TArgon2Context |
Argon2idPHC.pp |
TPHCCodec — PHC string encode/parse, base64 |
Argon2idRandom.pp |
IArgon2Random, TSystemRandom — OS entropy |
Blake2b.pp |
TBlake2b — BLAKE2b (RFC 7693), keyed and unkeyed |
Argon2Cli.pp |
TArgon2CliApp — the argon2-compatible command line tool |
tests/ |
Known-answer and differential test suites |
CliDiffTest.sh |
CLI differential test against the reference binary |
You only ever uses Argon2id. It re-exports everything from the other
units, so the split is an implementation detail.
Design boundary
The orchestration is object-oriented; the compression function is not. GB
runs 128 times per 1024-byte block — about 25 million times per default hash —
so nothing on that path carries a receiver, a vtable, or an allocation. The
rule, which is worth preserving: nothing invoked more than ~10,000 times per
hash gets a receiver. FillSegment, at t*4*p = 48 calls, is the first
routine that qualifies, and that is exactly where the object boundary sits.
Below it, TQWordBlock stays a plain array[0..127] of QWord and the memory
matrix stays one flat contiguous array.
Build
make # builds build/argon2
make check # run the test suites
make install # PREFIX=/usr/local, honours DESTDIR
make help # every target
To use the library, put Argon2id.pp and Blake2b.pp on your unit path.
There is nothing else to link.
Storing passwords
Argon2idHash is the whole flow: it draws a fresh salt from the OS CSPRNG,
hashes, and encodes. The returned string carries the algorithm, version, cost
parameters, salt and tag, so it is the only thing you store — verification
needs no extra columns.
With the defaults the string is 97 characters. It is not a fixed width: changing any cost parameter changes it, so size a column at 128 or more.
| Function | |
|---|---|
Argon2idHash(pw [, t, m, p, tau]) |
PHC string, fresh random salt |
Argon2idHash(pw, salt, t, m, p, tau) |
PHC string, caller's salt |
Argon2idVerify(encoded, pw) |
Boolean, constant-time, never raises |
Argon2idNeedsRehash(encoded [, t, m, p]) |
stored costs weaker than these? |
Argon2idDecodeParams(...) |
read costs/salt/tag without rehashing |
Argon2idVerify returns False rather than raising for anything malformed,
unsupported or hostile, so it is safe to call directly on untrusted database
contents. 11 of the tests cover exactly that (p=0, data=, non-canonical
base64, m=99999999999, trailing $, and so on).
Defaults
t=3, m=65536 KiB (64 MiB), p=4, 32-byte tag, 16-byte salt — RFC 9106
§4's second recommended option. About 0.4 s per hash on one modern
desktop core.
Tune with a measurement, not a guess: raise m until a hash takes as long as
your login path can afford. Memory is what costs an attacker; iterations are
the cheaper knob.
Raw tags, pepper and associated data
HashArgon2id returns the raw tag and takes a full TArgon2Context. Use it
when you need the optional secret (K, a server-side pepper held outside the
database) or associated data (X) — the PHC string format has nowhere to put
either, so the encoded API does not expose them.
Ctx := Argon2Context(Password, Salt, 3, 65536, 4, 32);
Ctx.Secret := ServerPepper;
Tag := HashArgon2id(Ctx);
HashArgon2id wraps TArgon2idHasher, which is deliberately not exported:
no external code can name the type, so none can construct one and leak 64 MiB
of password-derived material. It is reachable only through this function, which
wraps it in try/finally.
Build the context with Argon2Context rather than declaring one and filling
fields: it zeroes the record, so Secret and AssociatedData can never be
left holding stack garbage.
Invalid parameters raise EArgon2Parameter. p=0, t=0, tau<4 and
m < 8p are all rejected rather than silently adjusted, so you cannot
accidentally compute a hash no other implementation agrees with.
Injecting entropy
TArgon2id.Hash takes an optional IArgon2Random, which exists so a test can
supply deterministic salt bytes. Note what is deliberately missing: there is no
way to replace the process-wide default. TSystemRandom is the only
implementation the library ships, and injection is per-call — so no
configuration mistake can downgrade production salts to something predictable.
A deterministic source lives in tests/Argon2idTests.pp, not in the library.
Command line tool
build/argon2 takes the same arguments as the reference argon2: the password
on stdin, the salt as the first positional argument.
$ echo -n password | ./build/argon2 somesalt -id -t 3 -m 16 -p 4
Type: Argon2id
Iterations: 3
Memory: 65536 KiB
Parallelism: 4
Hash: 661fefbd6f29bcbc8f4646abc32a9d7a4645bb5c059537f8a5587f31adbecccd
Encoded: $argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$Zh/vvW8pvLyPRkarwyqdekZFu1wFlTf4pVh/Ma2+zM0
0.346 seconds
Verification ok
-e prints only the encoded hash, -r only the hex digest. Exit codes match
too: 1 for errors and for -h, 226 for no arguments.
Install it as argon2 to shadow the C version, or keep both:
make install PROG=pargon2
Deliberate differences from the reference
| Reference | Here | |
|---|---|---|
-i / -d |
Argon2i / Argon2d | rejected — this is an Argon2id-only library |
| default type | Argon2i | Argon2id, for the same reason |
-v 10 |
computes v0x10 | rejected; only v13 (0x13) is implemented |
| password length | capped at 127 bytes | no limit |
-t 1x |
strtoul → silently t=1 |
rejected: bad numeric input for -t |
The last one is a judgement call rather than an oversight. A typo such as
-t 1O silently weakening your time cost is worse than a loud error. Any
intentional invocation that works with the reference works here.
One reference quirk is reproduced faithfully: -r prints hex text, not raw
bytes, despite its own help text saying otherwise.
Testing
make check # self-contained: needs only fpc + make
make check-all # adds the differential suites (needs libargon2 + argon2)
| Suite | Cases | What it pins |
|---|---|---|
tests/Blake2bTests.pp |
26 | RFC 7693 vectors, keyed mode, buffering boundaries at 127/128/129/256 bytes, byte-at-a-time updates, and that TBlake2b.Reset is indistinguishable from a fresh instance |
tests/Argon2idTests.pp |
57 | RFC 9106 §5.3, multi-lane, H' tag lengths either side of 64, m not a multiple of 4p, parameter validation, PHC encoding, hostile input, and the TArgon2id / injected-RNG API |
tests/VerifyArgon2.pp |
38 | every hash compared against libargon2, fixed and randomised parameters, including secret and associated data |
CliDiffTest.sh |
65 | CLI vs the reference binary: stdout, stderr and exit status compared separately |
All four currently pass with zero failures (137 cases in total). make check deliberately installs
nothing beyond the compiler — if the library ever grows a hidden dependency,
that is where it shows up.
Warnings are errors (-Sew). The tree is warning-clean; keep it that way.
Security notes
- Single-threaded. Lanes are processed sequentially within each slice.
Output is identical to a threaded implementation, because the algorithm only
depends on the synchronisation points between slices — but
p > 1buys no speedup here. It still affects the hash, so it must be recorded, and it is. - No SIMD. Roughly 5–10× slower than libargon2 at the same parameters. Budget accordingly when choosing costs; the tool prints its own timing.
- Password-derived buffers (the memory matrix,
H0, intermediate blocks, computed tags) are wiped after use. This is best-effort: Pascal's managed strings mean a caller's ownPasswordcopy is not under our control. Argon2idVerifycompares tags in constant time. Length is compared first, which is not secret — it is published in the stored hash.- Salts come from
/dev/urandomon Unix andBCryptGenRandomon Windows. On any other target the unit fails to compile rather than silently falling back to something predictable.
Independent review is welcome and has not happened. The implementation matches the reference bit-for-bit on everything tested here, which is evidence about correctness, not about side channels.
Licence
Dual licensed, at your option:
- Apache License 2.0
- CC0 1.0 Universal
SPDX-License-Identifier: Apache-2.0 OR CC0-1.0
These are the same terms as the Argon2 reference implementation, so this code can be used anywhere that can. Both are GPL-compatible. See LICENSE for the full texts and a note on provenance.