The ALTCHA Java Library is a lightweight library designed for creating and verifying ALTCHA challenges.
- Java 17+
examples/server/
Minimal ALTCHA v2 example server. Run:gradle :examples:server:runexamples/argon2/
Example using Argon2id algorithm. Run:gradle :examples:argon2:run
Maven Central: org.altcha/altcha
Maven:
<dependency>
<groupId>org.altcha</groupId>
<artifactId>altcha</artifactId>
<version>2.0.1</version>
</dependency>Gradle:
implementation 'org.altcha:altcha:2.0.1'
org.json must be present at runtime (it is a provided dependency):
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>| Version | Package | Algorithm |
|---|---|---|
| v1 (legacy) | org.altcha.altcha.v1 |
SHA-1 / SHA-256 / SHA-512 |
| v2 | org.altcha.altcha.v2 |
PBKDF2 / SHA-iterative (pluggable KDF) |
Both versions live side-by-side. Use org.altcha.altcha.v2 for new integrations.
v2 uses a configurable key-derivation function (KDF). The server creates a signed challenge; the client brute-forces a counter until the derived key starts with the required prefix.
import org.altcha.altcha.v2.Altcha;
var options = new Altcha.CreateChallengeOptions()
.algorithm("PBKDF2/SHA-256")
.cost(5_000) // PBKDF2 iterations
.hmacSignatureSecret("your-secret-key")
.expiresInSeconds(600); // 10 minutes
Altcha.Challenge challenge = Altcha.createChallenge(options);
// Serialize challenge to JSON and send to clientSupported algorithms (built-in):
| String | KDF |
|---|---|
"PBKDF2/SHA-256" |
PBKDF2-HMAC-SHA-256 |
"PBKDF2/SHA-384" |
PBKDF2-HMAC-SHA-384 |
"PBKDF2/SHA-512" |
PBKDF2-HMAC-SHA-512 |
"SHA-256" |
Iterative SHA-256 |
"SHA-384" |
Iterative SHA-384 |
"SHA-512" |
Iterative SHA-512 |
External KDFs (Argon2id, Scrypt) can be plugged in via the KeyDerivationFunction interface.
var kdf = Altcha.kdf(challenge.parameters().algorithm());
var solution = Altcha.solveChallenge(challenge, kdf);
// Encode {challenge, solution} as JSON, base64 it, and submit// From a base64-encoded payload submitted by the client:
Altcha.VerifySolutionResult result = Altcha.verifySolution(
base64Payload,
"your-secret-key",
Altcha.kdf("PBKDF2/SHA-256"));
if (result.verified()) {
// accept
} else if (result.expired()) {
// challenge expired
} else if (Boolean.TRUE.equals(result.invalidSignature())) {
// challenge was tampered with
}Or from already-parsed objects:
Altcha.VerifySolutionResult result = Altcha.verifySolution(
challenge, solution, "your-secret-key", kdf);var options = new Altcha.CreateChallengeOptions()
.algorithm("PBKDF2/SHA-256")
.cost(5_000)
.hmacSignatureSecret("secret")
.expiresInSeconds(300)
.data(Map.of("userId", "42", "action", "login"));In deterministic mode the server pre-computes the expected key prefix from a known counter. This allows fast verification without re-running the KDF.
var options = new Altcha.CreateChallengeOptions()
.algorithm("SHA-256")
.cost(5_000)
.counter(123) // random counter
.hmacSignatureSecret("secret")
.hmacKeySignatureSecret("key-secret"); // signs the derived key
Altcha.Challenge challenge = Altcha.createChallenge(options);
// Verify using key signature (fast — no KDF re-invocation)
Altcha.VerifySolutionResult result = Altcha.verifySolution(
challenge, solution, "secret", "key-secret", null, null);Altcha.KeyDerivationFunction argon2id = (params, salt, password) -> {
byte[] dk = /* your Argon2id library */ computeArgon2id(
password, salt,
params.cost(), // time cost
params.memoryCost(), // memory in KiB
params.parallelism(),
params.keyLength());
return new Altcha.DeriveKeyResult(dk);
};
var options = new Altcha.CreateChallengeOptions()
.algorithm("ARGON2ID")
.cost(3)
.memoryCost(65536)
.parallelism(1)
.deriveKey(argon2id)
.hmacSignatureSecret("secret");boolean ok = Altcha.verifyFieldsHash(formData, fields, fieldsHash, "SHA-256");Altcha.ServerSignatureVerification result =
Altcha.verifyServerSignature(base64Payload, "secret");
if (result.verified()) {
double score = result.verificationData().score();
}| Method | Returns | Description |
|---|---|---|
createChallenge(CreateChallengeOptions) |
Challenge |
Creates a new signed v2 challenge |
signChallenge(String, ChallengeParameters, byte[], String, String) |
Challenge |
Signs challenge parameters with HMAC |
solveChallenge(Challenge, KeyDerivationFunction) |
Solution |
Brute-forces a solution (counter start=0, step=1) |
solveChallenge(Challenge, KeyDerivationFunction, int, int) |
Solution |
Brute-forces a solution with custom start/step |
verifySolution(String, String, KeyDerivationFunction) |
VerifySolutionResult |
Verifies a base64 JSON payload from the client |
verifySolution(Challenge, Solution, String, KeyDerivationFunction) |
VerifySolutionResult |
Verifies typed challenge + solution objects |
verifySolution(Challenge, Solution, String, String, KeyDerivationFunction) |
VerifySolutionResult |
Verifies with optional key-signature secret (fast path) |
parsePayload(String) |
Payload |
Decodes a base64 JSON payload into typed objects |
isServerSignaturePayload(String) |
boolean |
Returns true if the payload is from the Sentinel service |
verifyFieldsHash(Map<String,String>, String[], String, String) |
boolean |
Verifies a Sentinel fields hash |
verifyServerSignature(ServerSignaturePayload, String) |
ServerSignatureVerification |
Verifies a typed Sentinel server-signature payload |
verifyServerSignature(String, String) |
ServerSignatureVerification |
Verifies a base64-encoded Sentinel payload |
kdf(String) |
KeyDerivationFunction |
Returns the built-in KDF for the given algorithm string |
pbkdf2() |
KeyDerivationFunction |
PBKDF2-based KDF factory |
sha() |
KeyDerivationFunction |
SHA-iterative KDF factory |
randomBytes(int) |
byte[] |
Generates cryptographically random bytes |
bytesToHex(byte[]) |
String |
Encodes a byte array as a lowercase hex string |
| Type | Kind | Description |
|---|---|---|
CreateChallengeOptions |
mutable builder | Options for createChallenge — algorithm, cost, secrets, expiry, data, KDF override |
ChallengeParameters |
record | Parameters embedded in a challenge (algorithm, nonce, salt, cost, keyPrefix, …) |
Challenge |
record | Challenge object sent to the client: parameters + HMAC signature |
Solution |
record | Solution found by the client: counter, derivedKey, time |
Payload |
record | Full client submission: challenge + solution |
VerifySolutionResult |
record | Verification outcome: verified, expired, invalidSignature, invalidSolution, time |
ServerSignaturePayload |
record | Raw Sentinel attestation payload |
ServerSignatureVerification |
record | Sentinel verification result: verified, verificationData |
ServerSignatureVerificationData |
record | Parsed Sentinel data: score, classification, email, expire, fields, … |
KeyDerivationFunction |
functional interface | Pluggable KDF: deriveKey(ChallengeParameters, byte[] salt, byte[] password) |
DeriveKeyResult |
record | Wraps the derivedKey byte array returned by a KDF |
PasswordBuffer |
class | Combines nonce + counter into a reusable byte array for KDF iterations |
v1 uses simple hashcash-style proof-of-work. It is preserved for backward compatibility.
import org.altcha.altcha.v1.Altcha;
// Create challenge
var options = new Altcha.ChallengeOptions()
.hmacKey("secret")
.maxNumber(1_000_000L)
.expiresInSeconds(600);
Altcha.Challenge challenge = Altcha.createChallenge(options);
// Verify solution (base64 payload from client)
boolean valid = Altcha.verifySolution(base64Payload, "secret", true);| Method | Description |
|---|---|
createChallenge(ChallengeOptions) |
Creates a new challenge |
verifySolution(Payload, String, boolean) |
Verifies a typed payload |
verifySolution(String, String, boolean) |
Verifies a base64 JSON payload |
solveChallenge(String, String, Algorithm, long, long) |
Brute-forces a solution (client utility) |
extractParams(String) |
Parses params embedded in a salt |
verifyFieldsHash(Map, String[], String, Algorithm) |
Verifies a Sentinel fields hash |
verifyServerSignature(ServerSignaturePayload, String) |
Verifies a Sentinel server signature |
verifyServerSignature(String, String) |
Verifies from a base64 payload |
v2 always uses SecureRandom for the nonce and salt, as these values must be unpredictable.
v1 uses a non-secure random number generator by default to avoid blocking on low-entropy systems. To opt in to a cryptographically secure RNG:
new Altcha.ChallengeOptions().secureRandomNumber(true)On low-entropy systems (e.g. containers at startup), SecureRandom may block. If that happens, add this JVM option:
-Djava.security.egd=file:/dev/./urandom
This applies to both v1 (when secureRandomNumber is enabled) and v2.
MIT