kemal-identity-argon2 v0.1.0
kemal_identity_argon2
Argon2id password hashing for kemal_identity.
A separate shard because it needs a C binding to libargon2. docs/00-scope.md in the main project calls that "the only real reason to split a shard", and this is the one case that qualifies — everything else ships in kemal_identity itself.
Why Argon2id
bcrypt is CPU-hard and nothing else, so an attacker with GPUs or FPGAs gets a large constant factor over a defender running ordinary servers. Argon2id is memory-hard: raising m_cost raises the attacker's silicon budget rather than only their clock. RFC 9106 recommends Argon2id unless you have a specific reason to differ.
bcrypt remains a perfectly reasonable default, which is why it is the one kemal_identity ships. This is the upgrade, not a correction.
Install
libargon2 first:
apt-get install libargon2-dev # Debian, Ubuntu
pacman -S argon2 # Arch
brew install argon2 # macOS
Then:
dependencies:
kemal_identity_argon2:
github: urunsiyabend/kemal-identity-argon2
version: ~> 0.1.0
Use
require "kemal_identity_argon2"
KemalIdentity.configure(
accounts: accounts,
sessions: sessions,
hasher: KemalIdentity::Passwords::HashingExecutor.new(
KemalIdentity::Argon2::Hasher.new, size: 2
),
)
That is the whole integration. It is a KemalIdentity::Passwords::Hasher and runs the same shared contract spec as BcryptHasher — pulled from the dependency rather than copied, so it cannot drift.
Keep it behind a HashingExecutor
A C call blocks the calling fiber for its whole duration and never yields, exactly as bcrypt does — more so here, since the work is deliberately memory-bound too. Everything kemal_identity says about keeping hashing off the request fiber applies unchanged.
Memory is the new thing to size. Each concurrent hash holds m_cost kibibytes for its duration, so an executor of two at the default 19 MiB reserves about 38 MiB. Raising either multiplies that.
Cost
Median of five verifications, Crystal 1.21, 20 CPUs. Numbers from a development machine — recalibrate on your deployment target.
m_cost |
t_cost |
p |
|
|---|---|---|---|
| 19,456 KiB | 2 | 1 | 15.4 ms |
| 19,456 KiB | 3 | 1 | 21.0 ms |
| 46,080 KiB | 1 | 1 | 24.8 ms |
| 65,536 KiB | 3 | 1 | 90.0 ms |
The defaults are OWASP's first recommended Argon2id configuration: m=19456, t=2, p=1. Pick the highest cost that keeps p95 login latency inside your budget, on your hardware.
KemalIdentity::Argon2::Hasher.new(memory_cost: 46_080, time_cost: 1, parallelism: 1)
Parameters libargon2 would reject are refused at construction, so a bad configuration is a startup failure rather than an error at the first login.
Migrating from bcrypt costs nothing
needs_rehash? reports true for any digest it cannot parse, and a bcrypt digest is one of those. So existing users log in against their old digest and each successful login rewrites it with Argon2id. Nobody is forced through a password reset.
The one thing to keep: BcryptHasher has to stay reachable to verify the old digests during the migration. Track how many remain so you know when it can go.
Password length
Argon2 accepts up to 2³²−1 bytes and never truncates, so unlike bcrypt there is no length at which two different passwords open the same account. max_secret_bytesize is nonetheless 1024 — a policy cap, not an algorithm limit, because accepting an unbounded secret on an unauthenticated endpoint means copying and hashing whatever a client sent. It is far past any passphrase a person will type.
License
MIT.
kemal-identity-argon2
- 1
- 0
- 0
- 0
- 2
- about 4 hours ago
- August 26, 2026
MIT License
Wed, 26 Aug 2026 09:31:34 GMT