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
BucketSessionThe 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:
BaseModelStrict, 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_atis a canonicalUtcInstantbecause the whole backoff is a comparison against it. A baredatetimeadmitted two shapes the sidecar must never carry: a naive stamp, which made_evaluate_state()raise a rawTypeErrorout of the security gate rather than clearing, and an offset stamp, which silently shifted the deadline by that offset – a+01:00value 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:
BaseModelTyped 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.
- 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_secondsis zero (andthrottledisFalse) whenever the operator is clear to retry.- Parameters:
- Return type:
- Returns:
A
ThrottleEvaluationcarryingthrottled,remaining_seconds, and the currentconsecutive_failures.
- record_login_failure(*, storage_root, bucket_id, now)[source]¶
Increment the consecutive-failure count and stamp
now.- Parameters:
- Return type:
- Returns:
The persisted
LoginThrottleStateafter 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.