cadrumo.adapters.persistence.storage.master_key.login_throttle module

Per-bucket failed-login throttle sidecar.

A plaintext, best-effort rate-limit cache that records only the count of consecutive failed authentication attempts and the timestamp of the most recent one — never a secret, a passphrase, or any key material. It lives beside the wrapped bucket DEK in the separated keystore directory and is written through the same hardened atomic writer.

The throttle enforces this backoff: the caller evaluates the remaining wait BEFORE running any Argon2id derivation, so the KDF can never become a passphrase-testing timing oracle. The required wait after n consecutive failures is min(2 ** n, 60) seconds, measured from last_failure_at — so a caller that simply waited the backoff out is cleared to retry.

There is deliberately NO permanent lockout: a local-CLI self-DoS is worse than throttled retry (NIST SP 800-63B §5.2.2 requires throttling, not lockout). For the same reason the sidecar is a revocable cache, not an authoritative record — a missing, unreadable, or version-mismatched file is treated as “no active throttle” and rewritten on the next failure, never a hard refusal that could strand the legitimate operator. The counter resets to clear on a successful login and on logout.

Every read-modify-write of the sidecar is serialized under exclusive_file_lock(), because the counter is the whole substance of the control: overlapping attempts that each read n and each write n + 1 advance it once, so a burst of wrong passwords faces the backoff owed to a single failure. That tolerance is not one of the ones above – treating an unusable file as cleared protects the legitimate operator, while a lost increment only helps whoever is guessing.

See also

BucketSession

The unlock session whose authentication attempts this throttle guards.

LOGIN_THROTTLE_SCHEMA_VERSION

Version stamped on the persisted throttle state.

THROTTLE_BACKOFF_CAP_SECONDS

Maximum required wait between consecutive failed attempts.

class LoginThrottleState(**data)[source]

Bases: BaseModel

Strict, plaintext throttle state persisted per bucket.

Carries only a non-negative failure count and the timestamp of the most recent failure. It never holds a passphrase, key, or any other secret.

last_failure_at is a canonical UtcInstant because the whole backoff is a comparison against it. A bare datetime admitted two shapes the sidecar must never carry: a naive stamp, which made _evaluate_state() raise a raw TypeError out of the security gate rather than clearing, and an offset stamp, which silently shifted the deadline by that offset – a +01:00 value read an hour into the past and reported the operator clear to retry while the same instant in UTC was still throttled. Refusing both at the model boundary routes such a sidecar through the documented unreadable-means-cleared path instead.

Parameters:

data (Any)

schema_version: int
consecutive_failures: int
last_failure_at: UtcInstant | None
class ThrottleEvaluation(**data)[source]

Bases: BaseModel

Typed outcome of a throttle evaluation at a given instant.

Parameters:

data (Any)

throttled: bool
remaining_seconds: NonNegativeInt
consecutive_failures: NonNegativeInt
login_throttle_path(*, storage_root, bucket_id)[source]

Return the throttle sidecar path inside the bucket keystore directory.

Return type:

Path

Parameters:
  • storage_root (Path)

  • bucket_id (str)

evaluate_login_throttle(*, storage_root, bucket_id, now)[source]

Return the remaining backoff wait for a bucket at now.

Call this BEFORE any Argon2id derivation. remaining_seconds is zero (and throttled is False) whenever the operator is clear to retry.

Parameters:
  • storage_root (Path) – The Cadrumo storage root owning the bucket keystore.

  • bucket_id (str) – Identifier of the bucket whose throttle to evaluate.

  • now (datetime) – UTC timestamp at which the evaluation runs.

Return type:

ThrottleEvaluation

Returns:

A ThrottleEvaluation carrying throttled, remaining_seconds, and the current consecutive_failures.

record_login_failure(*, storage_root, bucket_id, now)[source]

Increment the consecutive-failure count and stamp now.

Parameters:
  • storage_root (Path) – The Cadrumo storage root owning the bucket keystore.

  • bucket_id (str) – Identifier of the bucket whose failure to record.

  • now (datetime) – UTC timestamp of the failed attempt.

Return type:

LoginThrottleState

Returns:

The persisted LoginThrottleState after the increment.

reset_login_throttle(*, storage_root, bucket_id)[source]

Clear the throttle by removing the sidecar (idempotent).

Called on a successful login and on logout. A missing sidecar is a clean no-op; a removal failure is logged rather than raised so it never breaks the success path — the stale window expires by clock regardless.

Return type:

None

Parameters:
  • storage_root (Path)

  • bucket_id (str)