CreaCaptcha

Engine
in package

Wraps the bundled ALTCHA library for the WordPress plugin.

Table of Contents

Constants

CLEANUP_HOOK  = 'creationell_captcha_cleanup_replay_markers'
Cron hook that sweeps expired replay markers out of the options table.
REPLAY_PREFIX  = 'creationell_captcha_used_'
Option-name prefix of the single-use replay markers claimed by verify().
UA_GATE_PARAM  = 'uagate'
Key inside `challenge.parameters.data` that marks a challenge as "issued for the under-attack gate only".
CLEANUP_INTERVAL  = 900
Seconds between two replay-marker sweeps while markers still exist.
CODE_PASS_PREFIX  = 'creationell_captcha_ccpass_'
Transient-name prefix of the "image-code stage passed" markers that the `/code-verify` handler writes (see includes/code-challenge.php).
PRESETS  = ['pbkdf2' => ['low' => ['cost' => 3000, 'min' => 5, 'max' => 30], 'medium' => ['cost' => 6000, 'min' => 10, 'max' => 60], 'high' => ['cost' => 15000, 'min' => 20, 'max' => 120]], 'argon2id' => ['low' => ['cost' => 1, 'min' => 5, 'max' => 40], 'medium' => ['cost' => 2, 'min' => 10, 'max' => 80], 'high' => ['cost' => 3, 'min' => 20, 'max' => 160]]]
Difficulty presets per algorithm: PoW cost + counter range [min, max].

Methods

cleanup_replay_markers()  : void
Deletes replay markers whose lifetime has run out and re-arms itself while markers are still around. Registered on self::CLEANUP_HOOK at the bottom of this file.
code_pass_key()  : string
Transient key of the "image-code stage passed" marker for a challenge.
create_challenge()  : array<string, mixed>
Builds a fresh challenge as a JSON-serialisable array.
verify()  : bool
Verifies a base64-encoded ALTCHA payload and enforces single use.
verify_structural()  : bool
Verifies a base64 ALTCHA payload structurally — same signature requirement and same derive-key caps as `verify()`, but without the single-use replay claim and without the code-stage rule.
algorithm()  : DeriveKeyInterface
The derive-key algorithm instance for the configured algorithm.
algorithm_for_name()  : DeriveKeyInterface
Maps a challenge algorithm name to a derive-key instance.
algorithm_key()  : string
Effective algorithm key — falls back to pbkdf2 when argon2id is selected but ext-sodium is unavailable.
altcha()  : Altcha
Builds the underlying ALTCHA object with both HMAC secrets.
claim_replay_marker()  : bool
Claims the single-use replay marker for a challenge signature. Returns true for exactly one caller, false for every other.
difficulty_key()  : string
Normalises the configured difficulty.
params_within_caps()  : bool
Server-side ceiling for the derive-key work an INCOMING payload may ask for (CM-6, Wurzel 3.1).
prepare_payload()  : array{params: array, signature: string, counter: int, derived_key: string}|null
Decodes a base64 ALTCHA payload and enforces everything that has to hold BEFORE the vendor library is allowed to derive a single key.
replay_marker_lifetime()  : int
How long the single-use marker of a just-verified payload has to stay around — namely for exactly as long as that payload can still be redeemed.
run_verify_solution()  : bool
Hands a prepared payload to the vendor library.
schedule_replay_cleanup()  : void
Makes sure the replay markers written above get swept again.

Constants

CLEANUP_HOOK

Cron hook that sweeps expired replay markers out of the options table.

public mixed CLEANUP_HOOK = 'creationell_captcha_cleanup_replay_markers'

REPLAY_PREFIX

Option-name prefix of the single-use replay markers claimed by verify().

public mixed REPLAY_PREFIX = 'creationell_captcha_used_'

Deliberately plain options, not transients — see claim_replay_marker() for why the transient pair could not be made atomic.

Public because two routines outside the engine have to find these rows as well (E4a): the deactivation sweep in includes/lifecycle.php and the unconditional removal in uninstall.php. Both used to carry their own copy of the literal; a rename here would have left their rows behind.

UA_GATE_PARAM

Key inside `challenge.parameters.data` that marks a challenge as "issued for the under-attack gate only".

public mixed UA_GATE_PARAM = 'uagate'

Written by the /challenge handler (includes/rest.php) whenever a valid ctx token suppressed the image-code stage, read by verify() below. Deliberately a shared constant and not the same string typed twice: a typo on either side would turn the rule into a silent no-op that nothing on the interstitial path would notice.

CLEANUP_INTERVAL

Seconds between two replay-marker sweeps while markers still exist.

private mixed CLEANUP_INTERVAL = 900

CODE_PASS_PREFIX

Transient-name prefix of the "image-code stage passed" markers that the `/code-verify` handler writes (see includes/code-challenge.php).

private mixed CODE_PASS_PREFIX = 'creationell_captcha_ccpass_'

PRESETS

Difficulty presets per algorithm: PoW cost + counter range [min, max].

private mixed PRESETS = ['pbkdf2' => ['low' => ['cost' => 3000, 'min' => 5, 'max' => 30], 'medium' => ['cost' => 6000, 'min' => 10, 'max' => 60], 'high' => ['cost' => 15000, 'min' => 20, 'max' => 120]], 'argon2id' => ['low' => ['cost' => 1, 'min' => 5, 'max' => 40], 'medium' => ['cost' => 2, 'min' => 10, 'max' => 80], 'high' => ['cost' => 3, 'min' => 20, 'max' => 160]]]

Erstkalibrierung (Spec §7) — bei Bedarf nach dem DDEV-Solve-Zeit-Test an dieser einen Stelle nachjustieren.

These values are also the reference point for the incoming-payload caps in params_within_caps(): the plugin never issues anything above them.

Methods

cleanup_replay_markers()

Deletes replay markers whose lifetime has run out and re-arms itself while markers are still around. Registered on self::CLEANUP_HOOK at the bottom of this file.

public static cleanup_replay_markers() : void

code_pass_key()

Transient key of the "image-code stage passed" marker for a challenge.

public static code_pass_key(string $signature) : string

The marker is keyed by the challenge SIGNATURE, and that signature covers parameters.data.ccode (the ALTCHA library signs the canonical JSON of the whole parameter set). Two consequences, and they are the reason this binding exists:

  • a code token cannot be re-paired with a different challenge (CM-8) — changing data.ccode invalidates the signature, and the signature is mandatory since prepare_payload();
  • a payload that never passed the image-code stage has no marker at all (CM-1) — omitting the JSON key code on /code-verify no longer produces anything the form path accepts.
Parameters
$signature : string

The challenge.signature of the payload.

Return values
string —

Transient name (never empty).

create_challenge()

Builds a fresh challenge as a JSON-serialisable array.

public create_challenge() : array<string, mixed>
Return values
array<string, mixed>

verify()

Verifies a base64-encoded ALTCHA payload and enforces single use.

public verify(string $payload_b64[, bool $under_attack_gate = false ]) : bool
Parameters
$payload_b64 : string

The base64 payload from the altcha form field.

$under_attack_gate : bool = false

Whether the CALLER is the under-attack interstitial gate. Only that one call site (UnderAttack::run()) passes true; it LIFTS the uagate rule rather than requiring the marker — the gate accepts an ordinary challenge just as well, and deliberately so (see below). Default false — every form path keeps rejecting a marked challenge.

Return values
bool

verify_structural()

Verifies a base64 ALTCHA payload structurally — same signature requirement and same derive-key caps as `verify()`, but without the single-use replay claim and without the code-stage rule.

public verify_structural(string $payload_b64) : bool

Used by the code-challenge verify endpoint (Modul 15), which must not consume the incoming payload: the user may retry after a wrong code, and the one and only single-use consumption happens in verify() when the form is finally submitted.

Parameters
$payload_b64 : string

The base64 payload from the widget.

Return values
bool

algorithm()

The derive-key algorithm instance for the configured algorithm.

private algorithm() : DeriveKeyInterface
Return values
DeriveKeyInterface

algorithm_for_name()

Maps a challenge algorithm name to a derive-key instance.

private algorithm_for_name(string $name) : DeriveKeyInterface
Parameters
$name : string
Return values
DeriveKeyInterface

algorithm_key()

Effective algorithm key — falls back to pbkdf2 when argon2id is selected but ext-sodium is unavailable.

private algorithm_key() : string
Return values
string

altcha()

Builds the underlying ALTCHA object with both HMAC secrets.

private altcha() : Altcha
Return values
Altcha

claim_replay_marker()

Claims the single-use replay marker for a challenge signature. Returns true for exactly one caller, false for every other.

private claim_replay_marker(string $signature, int $expiry) : bool
Parameters
$signature : string

The challenge.signature of the verified payload.

$expiry : int

Seconds the marker has to stay around.

Return values
bool

difficulty_key()

Normalises the configured difficulty.

private difficulty_key(array<string, mixed> $settings) : string
Parameters
$settings : array<string, mixed>

Plugin settings.

Return values
string

params_within_caps()

Server-side ceiling for the derive-key work an INCOMING payload may ask for (CM-6, Wurzel 3.1).

private params_within_caps(array<string, mixed> $params_raw, int $counter, string $context) : bool

Pbkdf2::deriveKey() clamps cost only downwards (max(1, $cost)) and Argon2id::deriveKey() does the same with memoryCost — an anonymous POST could therefore request cost: 200000000 or memoryCost: 2000000 and bind a PHP worker inside a native C call that max_execution_time cannot interrupt. The caps are derived from what the plugin itself ever issues (self::PRESETS) plus headroom, so no legitimate payload can hit them; every value is filterable for installs with custom presets.

Parameter extraction mirrors ChallengeParameters::fromArray() one-to-one (same is_int() checks, same defaults) — otherwise a value this method waves through could reach the library as something else.

Parameters
$params_raw : array<string, mixed>

Raw challenge.parameters from the payload.

$counter : int

Counter of the submitted solution.

$context : string

Short label for the debug log.

Return values
bool

prepare_payload()

Decodes a base64 ALTCHA payload and enforces everything that has to hold BEFORE the vendor library is allowed to derive a single key.

private prepare_payload(string $payload_b64, string $context) : array{params: array, signature: string, counter: int, derived_key: string}|null
Parameters
$payload_b64 : string

Base64 payload from the client.

$context : string

Short label for the debug log.

Return values
array{params: array, signature: string, counter: int, derived_key: string}|null —

Prepared payload parts, or null when the payload must be rejected.

replay_marker_lifetime()

How long the single-use marker of a just-verified payload has to stay around — namely for exactly as long as that payload can still be redeemed.

private replay_marker_lifetime(array<string, mixed> $params) : int

Until 1.1.0 this was the CURRENT challenge_expiry setting, which is not the same thing: the redeemable lifetime of a payload is fixed when the challenge is issued and travels with it as expiresAt. Lowering the setting — the settings range is 60–3600 s and the help text explicitly recommends short values — therefore handed every challenge issued before the change a marker that dies BEFORE the challenge does. In that gap the same payload verifies again, as often as the client likes, until expiresAt is finally reached. That is the CM-9 guarantee ("one PoW = one submission") leaking out through a side door.

Why reading expiresAt out of the payload is safe (and not a new attacker-controlled criterion):

  • the value is part of the SIGNED challenge parameters, and the caller reaches this method only after run_verify_solution() has checked that signature against this site's HMAC secret. A signature is mandatory since CM-2 (prepare_payload), and the empty-secret case fails closed there as well;
  • the extraction mirrors ChallengeParameters::fromArray() one-to-one (isset() + is_int(), see lib/altcha-org/altcha/src/ChallengeParameters.php), so this method reads exactly the value the signature was verified against. Anything the library would normalise differently — a string, a float, a missing key — changes the canonical JSON and thus kills the signature one step earlier;
  • inflating the number is therefore not "a longer marker", it is an unverifiable payload. And a longer marker would only ever REDUCE what an attacker can do.
Parameters
$params : array<string, mixed>

Raw, signature-checked challenge.parameters.

Return values
int —

Seconds the marker has to stay around (at least 60).

run_verify_solution()

Hands a prepared payload to the vendor library.

private run_verify_solution(array{params: array, signature: string, counter: int, derived_key: string} $prepared, string $context) : bool
Parameters
$prepared : array{params: array, signature: string, counter: int, derived_key: string}

Prepared payload parts.

$context : string

Short label for the debug log.

Return values
bool

schedule_replay_cleanup()

Makes sure the replay markers written above get swept again.

private schedule_replay_cleanup(int $expiry) : void

Unlike transients, plain options never expire on their own — so the sweep is their ONLY cleanup path and must not be optional.

Parameters
$expiry : int

Lifetime of the marker that was just claimed.


        
On this page

Search results