README.md

# ALTCHA Elixir Library

A lightweight Elixir library for creating and verifying [ALTCHA](https://altcha.org) challenges.

## Compatibility

- Elixir 1.17+ (OTP 26+)

## Examples

- [`examples/server/`](/examples/server/)
- [`examples/argon2.exs`](/examples/argon2.exs)

## Installation

```elixir
defp deps do
  [
    {:altcha, "~> 2.0"}
  ]
end
```

## Versions

The library supports two proof-of-work versions:

| Module | Algorithm | Use with |
|--------|-----------|----------|
| `Altcha.V1` | Hash-based (`SHA-256` etc.) | ALTCHA widget v1 |
| `Altcha.V2` | Key-derivation-based (`PBKDF2`, iterative `SHA`) | ALTCHA widget v2 |

The top-level `Altcha` module delegates to `Altcha.V1` for backward compatibility.

---

## V2 Usage

### Creating a challenge

```elixir
challenge = Altcha.V2.create_challenge(%Altcha.V2.CreateChallengeOptions{
  algorithm: "PBKDF2/SHA-256",
  cost: 10_000,
  hmac_signature_secret: "your-secret"
})

# Send as JSON to the client
Jason.encode!(challenge)
```

For deterministic mode (random counter, faster server-side verification):

```elixir
challenge = Altcha.V2.create_challenge(%Altcha.V2.CreateChallengeOptions{
  algorithm: "PBKDF2/SHA-256",
  cost: 5_000,
  counter: Enum.random(5_000..10_000),
  expires_at: DateTime.to_unix(DateTime.utc_now(), :second) + 600,
  hmac_signature_secret: "your-secret",
  hmac_key_signature_secret: "your-key-secret"
})
```

With `counter` and `hmac_key_signature_secret`, the server pre-computes the expected derived key and signs it. Verification then checks the HMAC of the submitted key instead of re-running the key derivation.

### Verifying a solution

The client submits a Base64-encoded JSON payload containing the challenge and solution:

```elixir
# Decode the client payload
payload = Altcha.V2.decode_payload(params["altcha"])

result = Altcha.V2.verify_solution(%Altcha.V2.VerifySolutionOptions{
  challenge: payload.challenge,
  solution: payload.solution,
  hmac_signature_secret: "your-secret",
  hmac_key_signature_secret: "your-key-secret"  # optional
})

result.verified       # true / false
result.expired        # true if the challenge has expired
result.invalid_signature  # true if the challenge was tampered with
result.invalid_solution   # true if the solution is incorrect
```

### Verifying a server signature

```elixir
{result, verification_data} = Altcha.V2.verify_server_signature(params["altcha"], "your-secret")

result.verified           # true / false
verification_data["email"]      # values parsed from verificationData
verification_data["verified"]   # boolean
verification_data["expire"]     # integer Unix timestamp
```

`verify_server_signature/2` accepts a `%Altcha.V2.ServerSignaturePayload{}` struct, a plain map, a raw JSON string, or a Base64-encoded JSON string.

### Verifying form fields hash

```elixir
Altcha.V2.verify_fields_hash(form_data, ["email", "message"], fields_hash, "SHA-256")
```

### Supported algorithms

| Algorithm string | Description |
|-----------------|-------------|
| `"SHA-256"` / `"SHA-384"` / `"SHA-512"` | Iterative SHA hashing (built-in) |
| `"PBKDF2/SHA-256"` / `"PBKDF2/SHA-384"` / `"PBKDF2/SHA-512"` | PBKDF2 (requires OTP 24+) |
| Custom | Pass a `derive_key_fn` option |

### Custom key derivation

For algorithms not built-in (e.g. Scrypt, Argon2id), provide a `derive_key_fn`:

```elixir
Altcha.V2.create_challenge(%Altcha.V2.CreateChallengeOptions{
  algorithm: "SCRYPT",
  cost: 16_384,
  memory_cost: 8,
  derive_key_fn: fn params, salt, password ->
    # return derived key as binary
  end,
  hmac_signature_secret: "your-secret"
})
```

---

## V1 Usage

```elixir
challenge = Altcha.V1.create_challenge(%Altcha.V1.ChallengeOptions{
  hmac_key: "your-secret",
  max_number: 100_000,
  expires: DateTime.to_unix(DateTime.utc_now(), :second) + 600
})

# Verify a solution submitted by the client
Altcha.V1.verify_solution(params["altcha"], "your-secret")

# Verify a server signature
{verified, verification_data} = Altcha.V1.verify_server_signature(params["altcha"], "your-secret")

# Verify form fields hash
Altcha.V1.verify_fields_hash(form_data, ["email", "message"], fields_hash, :sha256)
```

---

## License

MIT