Free Pascal implementation of argon2id password hash
  • Pascal 87%
  • Shell 7.7%
  • Makefile 5.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
R. Eric Wheeler ee3c394799
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
Merge pull request 'Convert to classes via claud' (#1) from feature/move-to-classes into trunk
Reviewed-on: #1
2026-09-06 16:16:51 -07:00
.forgejo/workflows Fix forgejo workflow 2026-09-05 23:51:43 -07:00
tests Convert to classes via claud 2026-09-06 16:14:02 -07:00
.gitignore Update forgejo workflows 2026-09-05 23:46:44 -07:00
.gitlab-ci.yml Convert to classes via claud 2026-09-06 16:14:02 -07:00
Argon2Cli.pp Convert to classes via claud 2026-09-06 16:14:02 -07:00
Argon2id.pp Convert to classes via claud 2026-09-06 16:14:02 -07:00
Argon2idCore.pp Convert to classes via claud 2026-09-06 16:14:02 -07:00
Argon2idPHC.pp Convert to classes via claud 2026-09-06 16:14:02 -07:00
Argon2idRandom.pp Convert to classes via claud 2026-09-06 16:14:02 -07:00
Blake2b.pp Convert to classes via claud 2026-09-06 16:14:02 -07:00
CliDiffTest.sh Add CliDiffTest.sh 2026-09-05 23:24:16 -07:00
LICENSE initial commit 2026-09-05 23:21:20 -07:00
Makefile Convert to classes via claud 2026-09-06 16:14:02 -07:00
README.md Convert to classes via claud 2026-09-06 16:14:02 -07:00

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 > 1 buys no speedup here. It still affects the hash, so it must be recorded, and it is.
  • No SIMD. Roughly 510× 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 own Password copy is not under our control.
  • Argon2idVerify compares tags in constant time. Length is compared first, which is not secret — it is published in the stored hash.
  • Salts come from /dev/urandom on Unix and BCryptGenRandom on 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.