A lightweight Elixir library for creating and verifying ALTCHA challenges.
- Elixir 1.17+ (OTP 26+)
defp deps do
[
{:altcha, "~> 2.0"}
]
endInstall ALTCHA
mix ALTCHA.install # --version 3.2.1
# ... or: npm install altcha --prefix assets-
Add
altcha.installtoassets.setupinmix.exs(if using Mix installer):"assets.setup": ["altcha.install", ... ]
-
Register JavaScript hook (optional if using liveview) in your
app.js:import altcha; let Hooks = {}; Hooks.AltchaHook = { mounted() { const widget = this.el.querySelector("altcha-widget"); const input = this.el.querySelector("#altcha-token-input"); if (widget && input) { widget.addEventListener("statechange", (event) => { const { state, payload } = event.detail || {}; console.log(`[Altcha] State: ${state}, Payload:`, payload); if (state === "verified" && payload) { input.value = payload; } else if (state === "error") { input.value = ""; } }); } } }; const liveSocket = new LiveSocket('/live', Socket, { hooks: Hooks, });
-
add to endpoint.ex
plug :serve_altcha_challenge
plug Web.Router
def serve_altcha_challenge(%Plug.Conn{method: "GET", path_info: ["challenge"]} = conn, _opts) do
hmac_secret = System.get_env("ALTCHA_HMAC_SECRET", "change-me-in-production")
hmac_key_secret = System.get_env("ALTCHA_HMAC_KEY_SECRET", "change-me-in-production-2")
counter = Enum.random(5_000..10_000)
challenge =
Altcha.V2.create_challenge(%Altcha.V2.CreateChallengeOptions{
algorithm: "PBKDF2/SHA-256",
cost: 5_000,
counter: counter,
expires_at: DateTime.to_unix(DateTime.utc_now(), :second) + 600,
hmac_signature_secret: hmac_secret,
hmac_key_signature_secret: hmac_key_secret
})
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(challenge))
|> halt()
end
# Fallthrough clause so other routes like /register pass through safely to the router
def serve_altcha_challenge(conn, _opts), do: connor use the example router
plug Server.Router
plug Web.RouterThe 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.
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):
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.
The client submits a Base64-encoded JSON payload containing the challenge and solution:
# 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{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 timestampverify_server_signature/2 accepts a %Altcha.V2.ServerSignaturePayload{} struct, a plain map, a raw JSON string, or a Base64-encoded JSON string.
Altcha.V2.verify_fields_hash(form_data, ["email", "message"], fields_hash, "SHA-256")| 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 |
For algorithms not built-in (e.g. Scrypt, Argon2id), provide a derive_key_fn:
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"
})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)MIT