Argon2 Hash & Verify — Java source
Hash passwords with Argon2id — the winner of the Password Hashing Competition. Configure memory, iterations, and parallelism. WASM-powered, client-side.
This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Argon2 — Argon2id password hashing (parse/PHC logic in pure Java; the KDF
// itself via the reference argon2(1) CLI).
//
// Language: Java (17+, standard library only)
// Ported from src/lib/argon2.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// The TS build drives the reference C library compiled to WASM
// (argon2-browser). The JDK has no Argon2 in javax.crypto or anywhere else
// in the standard library, so this port drives the same reference C library
// through its CLI: `argon2` (https://github.com/P-H-C/phc-winner-argon2)
// must be on PATH for argon2Hash / argon2Verify. The PHC parser, validator,
// and Base64 codec are pure Java and dependency-free.
//
// PHC string format (what `encoded` holds - the string you store in a DB):
// $argon2id$v=19$m=65536,t=3,p=1$<b64 salt>$<b64 digest>
// Salt and digest are unpadded standard Base64.
//
// The CLI takes its salt as an argv string, and Java's ProcessBuilder
// re-encodes argv through the platform's native encoding (UTF-8 on most
// modern systems) - so raw binary salt bytes >= 0x80 do not arrive
// byte-for-byte. Random salts are therefore drawn as 128 bits rendered in
// 32 lowercase hex chars, which every encoding passes through unchanged, and
// argon2Hash reads the effective salt and digest straight back out of the
// PHC string the CLI prints: the returned hash / salt / encoded always
// describe one real, self-consistent hash. (Consequence: the PHC salt field
// of a freshly generated hash holds the 32-byte hex form rather than 16 raw
// bytes - a constraint the TS/WASM build does not have.)
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public final class Argon2 {
/** Defaults follow the OWASP-recommended Argon2id profile (64 MiB, 3 passes). */
public static final int DEFAULT_MEMORY = 65_536; // KiB
public static final int DEFAULT_ITERATIONS = 3;
public static final int DEFAULT_PARALLELISM = 1;
public static final int DEFAULT_HASH_LENGTH = 32; // bytes
/** Random salt size in bytes (128 bits - the PHC recommendation). */
public static final int SALT_BYTES = 16;
/** Hashing parameters; every field optional (null -> the OWASP default). */
public static final class Options {
/** Memory cost in KiB (default 65536 = 64 MiB). Must be >= 1024. */
public Integer memory;
/** Time cost - passes over memory (default 3). Must be >= 1. */
public Integer iterations;
/** Parallelism - lanes (default 1). Must be >= 1. */
public Integer parallelism;
/** Digest length in bytes (default 32). Must be 16..64. */
public Integer hashLength;
/** Explicit salt bytes; a random 16-byte salt is generated when null. */
public byte[] salt;
}
public record Argon2Result(
/** Raw digest, lowercase hex (hashLength bytes). */
String hash,
/** Self-contained PHC string - store this, verify against it. */
String encoded,
/** Salt used, lowercase hex (as embedded in the PHC string). */
String salt) {
}
/** Parameters extracted from a PHC string (parseArgon2's return type). */
public record Argon2Params(
String type, // "argon2d" | "argon2i" | "argon2id"
long version,
long memory,
long iterations,
long parallelism,
/** Salt, decoded from the embedded Base64 into lowercase hex. */
String salt,
/** Digest, decoded from the embedded Base64 into lowercase hex ("" if absent). */
String hash) {
}
private static final Pattern PHC_RE = Pattern.compile(
"\\$(argon2(?:d|i|id))\\$v=(\\d+)\\$m=(\\d+),t=(\\d+),p=(\\d+)\\$([A-Za-z0-9+/]+)(?:\\$([A-Za-z0-9+/]+))?");
private static final Pattern PHC_B64_RE = Pattern.compile("[A-Za-z0-9+/]+");
private static final SecureRandom RANDOM = new SecureRandom();
private Argon2() {
}
/** Lowercase hex of a byte array. */
static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
sb.append(Character.forDigit((b >> 4) & 0xf, 16));
sb.append(Character.forDigit(b & 0xf, 16));
}
return sb.toString();
}
private static byte[] hexToBytes(String hex) {
byte[] out = new byte[hex.length() / 2];
for (int i = 0; i < out.length; i++) {
out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
}
return out;
}
/**
* Unpadded standard Base64 (the PHC encoding) -> bytes. Throws on any
* non-alphabet character or an impossible length (1 mod 4).
*/
static byte[] phcBase64ToBytes(String b64) {
if (b64.isEmpty()) {
throw new IllegalArgumentException("Invalid Argon2 string: empty Base64 field");
}
if (!PHC_B64_RE.matcher(b64).matches()) {
throw new IllegalArgumentException("Invalid Argon2 string: non-Base64 characters");
}
if (b64.length() % 4 == 1) {
throw new IllegalArgumentException("Invalid Argon2 string: impossible Base64 length");
}
// Re-pad to a multiple of 4 and let java.util.Base64 do the 6-bit
// arithmetic; the length checks above already mirror the TS byte counting.
String padded = b64 + "=".repeat((4 - b64.length() % 4) % 4);
return java.util.Base64.getDecoder().decode(padded);
}
/**
* Parse a PHC-format Argon2 string (`$argon2id$v=19$m=65536,t=3,p=1$salt$hash`)
* into its typed parameters. Accepts argon2d / argon2i / argon2id. The digest
* segment is optional (some encoders omit it); salt and hash are returned as
* lowercase hex. Throws on any malformed input.
*/
public static Argon2Params parseArgon2(String encoded) {
var m = PHC_RE.matcher(encoded.trim());
if (!m.matches()) {
throw new IllegalArgumentException(
"Invalid Argon2 string: expected $argon2id$v=19$m=…,t=…,p=…$salt$hash");
}
return new Argon2Params(
m.group(1),
Long.parseLong(m.group(2)),
Long.parseLong(m.group(3)),
Long.parseLong(m.group(4)),
Long.parseLong(m.group(5)),
bytesToHex(phcBase64ToBytes(m.group(6))),
m.group(7) == null ? "" : bytesToHex(phcBase64ToBytes(m.group(7))));
}
/** Validate + normalise hashing parameters, throwing with a clear message. */
private static record Normalized(int memory, int iterations, int parallelism, int hashLength) {
}
private static Normalized normalizeOptions(Options options) {
int memory = options != null && options.memory != null ? options.memory : DEFAULT_MEMORY;
int iterations = options != null && options.iterations != null ? options.iterations : DEFAULT_ITERATIONS;
int parallelism = options != null && options.parallelism != null ? options.parallelism : DEFAULT_PARALLELISM;
int hashLength = options != null && options.hashLength != null ? options.hashLength : DEFAULT_HASH_LENGTH;
if (memory < 1024) throw new IllegalArgumentException("Memory must be at least 1024 KiB");
if (iterations < 1) throw new IllegalArgumentException("Iterations must be at least 1");
if (parallelism < 1) throw new IllegalArgumentException("Parallelism must be at least 1");
if (hashLength < 16 || hashLength > 64) {
throw new IllegalArgumentException("Hash length must be between 16 and 64 bytes");
}
return new Normalized(memory, iterations, parallelism, hashLength);
}
/** Run a command, feed it stdin, return stdout; throws with stderr on failure. */
private static String run(List<String> command, String stdin) throws IOException, InterruptedException {
Process p = new ProcessBuilder(command).start();
p.getOutputStream().write(stdin.getBytes(StandardCharsets.UTF_8));
p.getOutputStream().close();
String stdout = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String stderr = new String(p.getErrorStream().readAllBytes(), StandardCharsets.UTF_8);
if (p.waitFor() != 0) {
throw new IOException("argon2 failed: " + stderr.trim());
}
return stdout;
}
/**
* Hash a password with Argon2id (hybrid of Argon2i's side-channel resistance
* and Argon2d's GPU resistance - the Password Hashing Competition winner and
* the recommended mode for password storage). Returns the digest (hex), the
* salt used (hex, as read back from the PHC string), and the self-contained
* PHC string. A fresh random 16-byte salt is generated per call unless
* options.salt is given.
*/
public static Argon2Result argon2Hash(String password, Options options) throws IOException, InterruptedException {
Normalized opts = normalizeOptions(options);
byte[] salt = options != null && options.salt != null ? options.salt : randomSalt();
// The CLI's -m flag is log2(memory in KiB); memory is always a power of
// two in practice (the default 65536 = -m 16) and non-powers round to
// the nearest flag the CLI accepts, like every other CLI driver.
List<String> command = new ArrayList<>(List.of(
"argon2", new String(salt, StandardCharsets.ISO_8859_1),
"-id", "-v", "13", "-e",
"-m", Integer.toString((int) Math.round(Math.log(opts.memory()) / Math.log(2))),
"-t", Integer.toString(opts.iterations()),
"-p", Integer.toString(opts.parallelism()),
"-l", Integer.toString(opts.hashLength())));
String encoded = run(command, password).trim();
// Read the digest and the salt that was actually used back out of the
// PHC string (see the class comment on argv encoding).
Argon2Params produced = parseArgon2(encoded);
return new Argon2Result(produced.hash(), encoded, produced.salt());
}
/**
* A fresh random salt: SALT_BYTES CSPRNG bytes rendered as 32 lowercase hex
* chars (128 bits of entropy). Hex text is pure ASCII, so it survives the
* ProcessBuilder argv round-trip byte-for-byte under any native encoding -
* raw binary salt bytes >= 0x80 would not (see the class comment).
*/
static byte[] randomSalt() {
byte[] seed = new byte[SALT_BYTES];
RANDOM.nextBytes(seed);
return bytesToHex(seed).getBytes(java.nio.charset.StandardCharsets.US_ASCII);
}
/**
* Verify a password against a PHC-format encoded hash (as produced by
* argon2Hash). Returns true on match, false on mismatch; throws only on a
* malformed encoded string or a runtime error. Any Argon2 type (d/i/id) is
* accepted - the type is read from the string itself.
*
* The CLI has no verify mode, so this recomputes the hash with the
* embedded parameters + salt and compares digests (what the C library's
* argon2_verify does internally). The salt travels to the CLI over argv,
* so verification is exact for salts that are argv-transparent under the
* platform's native encoding - ASCII, including every hex-form salt this
* class generates; binary salt bytes >= 0x80 on a multi-byte encoding
* platform will mismatch (use the library directly, as the C port does,
* when that matters).
*/
public static boolean argon2Verify(String encoded, String password) throws IOException, InterruptedException {
Argon2Params params = parseArgon2(encoded); // validate format up front
Options options = new Options();
options.memory = (int) params.memory();
options.iterations = (int) params.iterations();
options.parallelism = (int) params.parallelism();
// The CLI emits a fixed-length digest; keep the embedded length when
// present so the comparison is byte-for-byte.
options.hashLength = params.hash().isEmpty() ? DEFAULT_HASH_LENGTH : params.hash().length() / 2;
options.salt = hexToBytes(params.salt());
// Recompute with the type embedded in the string (-d / -i / -id).
Normalized opts = normalizeOptions(options);
List<String> command = new ArrayList<>(List.of(
"argon2", new String(options.salt, StandardCharsets.ISO_8859_1),
"-" + params.type().substring("argon2".length()), "-v", "13", "-e",
"-m", Integer.toString((int) Math.round(Math.log(opts.memory()) / Math.log(2))),
"-t", Integer.toString(opts.iterations()),
"-p", Integer.toString(opts.parallelism()),
"-l", Integer.toString(opts.hashLength())));
String recomputed = run(command, password).trim();
return params.hash().isEmpty() ? recomputed.equals(encoded.trim()) : digestMatches(recomputed, params.hash());
}
/** True when the recomputed PHC string's digest segment equals `wantHex`. */
private static boolean digestMatches(String encoded, String wantHex) {
String[] parts = encoded.split("\\$");
return bytesToHex(phcBase64ToBytes(parts[parts.length - 1])).equals(wantHex);
}
}
Also available in 9 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →