Bivio::Type::PasswordHashArgon2ID
# Copyright (c) 2026 bivio Software, Inc. All rights reserved.
package Bivio::Type::PasswordHashArgon2ID;
use strict;
use Bivio::Base 'Type.PasswordHashBase';
use Crypt::Argon2 ();
# OWASP recommendation for argon2id params
my($_MEMORY_COST) = '19456k';
my($_TIME_COST) = 2;
my($_PARALLELISM) = 1;
my($_TAG_SIZE) = 32;
my($_SALT_BYTES) = 16;
my($_R) = b_use('Biz.Random');
my($_D) = b_use('Bivio.Die');
sub ID {
return 'argon2id';
}
sub REGEX {
return qr{^\$argon2id\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+/]+\$[A-Za-z0-9+/]+$};
}
sub SALT_LENGTH {
return $_SALT_BYTES;
}
sub as_literal {
# argon2 PHC format is self-contained (algorithm, version, params, salt,
# tag) so we store and return it verbatim rather than re-wrapping in the
# base class's $id$salt$hash format.
return shift->get_hash;
}
sub compare {
my($self, $clear_text) = @_;
# argon2_verify does constant-time tag comparison internally, so Biz.SecureCompare is not
# needed. Catch errors so a malformed stored hash falls through as not matching rather than
# crashing.
my($ok);
$_D->catch_quietly(sub {
$ok = Crypt::Argon2::argon2_verify(
$self->get_hash, defined($clear_text) ? $clear_text : '');
});
return $ok ? 0 : 1;
}
sub internal_format_literal {
b_die('unused method');
}
sub internal_random_salt {
return $_R->bytes($_SALT_BYTES);
}
sub internal_to_literal {
my($proto, $clear_text, $salt) = @_;
return Crypt::Argon2::argon2id_pass(
defined($clear_text) ? $clear_text : '',
$salt,
$_TIME_COST,
$_MEMORY_COST,
$_PARALLELISM,
$_TAG_SIZE,
);
}
sub internal_to_parts {
my($proto, $value) = @_;
# Full PHC string lives in `hash`; `salt` exposes the base64 salt field
# for inspection only (compare() uses the full string via argon2_verify).
return {
id => $proto->ID,
salt => (split(/\$/, $value))[4],
hash => $value,
};
}
1;