# ALTCHA Java Library
The ALTCHA Java Library is a lightweight library designed for creating and verifying [ALTCHA](https://altcha.org) challenges.
## Compatibility
- Java 17+
## Examples
- [`examples/server/`](/examples/server/)
Minimal ALTCHA v2 example server. Run: `gradle :examples:server:run`
- [`examples/argon2/`](/examples/argon2/)
Example using Argon2id algorithm. Run: `gradle :examples:argon2:run`
## Installation
Maven Central: [org.altcha/altcha](https://central.sonatype.com/artifact/org.altcha/altcha)
Maven:
```xml
org.altcha
altcha
2.0.3
```
Gradle:
```
implementation 'org.altcha:altcha:2.0.3'
```
`org.json` must be present at runtime (it is a `provided` dependency):
```xml
org.json
json
20240303
```
## Protocol versions
| 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 Usage
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.
### Create a challenge (server)
```java
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 client
```
**Supported 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.
### Solve a challenge (client-side utility)
```java
var kdf = Altcha.kdf(challenge.parameters().algorithm());
var solution = Altcha.solveChallenge(challenge, kdf);
// Encode {challenge, solution} as JSON, base64 it, and submit
```
### Verify a solution (server)
```java
// 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:
```java
Altcha.VerifySolutionResult result = Altcha.verifySolution(
challenge, solution, "your-secret-key", kdf);
```
### Custom metadata / expiry
```java
var options = new Altcha.CreateChallengeOptions()
.algorithm("PBKDF2/SHA-256")
.cost(5_000)
.hmacSignatureSecret("secret")
.expiresInSeconds(300)
.data(Map.of("userId", "42", "action", "login"));
```
### Deterministic mode (key signature)
In deterministic mode the server pre-computes the expected key prefix from a known counter. This allows fast verification without re-running the KDF.
```java
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);
```
### Pluggable KDF (e.g. Argon2id)
```java
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");
```
### Fields hash (ALTCHA Sentinel)
```java
boolean ok = Altcha.verifyFieldsHash(formData, fields, fieldsHash, "SHA-256");
```
### Server signature (ALTCHA Sentinel)
```java
Altcha.ServerSignatureVerification result =
Altcha.verifyServerSignature(base64Payload, "secret");
if (result.verified()) {
double score = result.verificationData().score();
}
```
### v2 API reference
#### Static methods
| 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)` | `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 |
#### Data types
| 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 Usage (legacy)
v1 uses simple hashcash-style proof-of-work. It is preserved for backward compatibility.
```java
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);
```
### v1 API reference
| 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 |
---
## Random Number Generator
**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:
```java
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.
## License
MIT