Skip to content

Add rate limiting to password attempts in unlock flow#13

Merged
kwsantiago merged 3 commits intomainfrom
rate-limiting
Dec 29, 2025
Merged

Add rate limiting to password attempts in unlock flow#13
kwsantiago merged 3 commits intomainfrom
rate-limiting

Conversation

@wksantiago
Copy link
Contributor

@wksantiago wksantiago commented Dec 26, 2025

Implements rate limiting for password attempts with exponential backoff after 5 failed attempts.

Summary by CodeRabbit

  • New Features

    • Per-path persistent rate limiting for unlock attempts with exponential backoff; when active, operations return "Rate limited: try again in X seconds".
    • Unlock flows now check, record, and reset rate-limit state so successful unlocks clear prior failures.
  • Chores

    • Added a dependency to support persistent file-backed locking used by the rate limiter.
  • Tests

    • Added tests covering rate-limit triggers, backoff progression, reset on success, combined unlock scenarios, and path isolation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

Walkthrough

Adds a persistent per-path on-disk rate limiter, integrates rate checks into HiddenStorage and Storage unlock flows, introduces KeepError::RateLimited(u64), refactors hidden unlock helpers to record outcomes, adds pub(crate) mod rate_limit, and includes tests exercising rate-limiting behavior.

Changes

Cohort / File(s) Summary
Dependency
keep-core/Cargo.toml
Added fs2 = "0.4" dependency for file locking.
Error enum
keep-core/src/error.rs
Added RateLimited(u64) variant to KeepError with message "Rate limited: try again in {0} seconds".
Rate limiting core
keep-core/src/rate_limit.rs
New module implementing persistent per-path on-disk rate limiter with checksums, file locking, exponential backoff capped at MAX_DELAY_SECS, and public helpers check_rate_limit(path) -> Result<(), Duration>, record_failure(path), record_success(path) plus comprehensive tests.
Crate module
keep-core/src/lib.rs
Added crate-private module: pub(crate) mod rate_limit;.
Storage integration
keep-core/src/storage.rs
unlock() now calls rate_limit::check_rate_limit and returns KeepError::RateLimited when active; on decryption/auth failures calls record_failure; on successful unlock calls record_success; tests updated/added.
Hidden volume integration
keep-core/src/hidden/volume.rs
Introduced private try_unlock_outer / try_unlock_hidden helpers; public unlock_outer/unlock_hidden now wrap those with rate-limit checks and outcome recording; unlock() performs combined checks and records accordingly; tests added for outer/hidden/combined rate-limiting.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Storage
    participant RateLimiter as "RateLimiter\n(on-disk)"
    participant Crypto

    Client->>Storage: unlock(path, password)
    Storage->>RateLimiter: check_rate_limit(path)
    alt rate limited
        RateLimiter-->>Storage: Err(remaining)
        Storage-->>Client: Err(KeepError::RateLimited(remaining_secs))
    else allowed
        RateLimiter-->>Storage: Ok(())
        Storage->>Crypto: decrypt_master_key(password)
        alt decrypt fails (auth)
            Crypto-->>Storage: Err(decrypt_err)
            Storage->>RateLimiter: record_failure(path)
            Storage-->>Client: Err(decrypt_err)
        else decrypt succeeds
            Crypto-->>Storage: Ok(master_key)
            Storage->>Crypto: decrypt_data_key(master_key)
            Crypto-->>Storage: Ok(data)
            Storage->>RateLimiter: record_success(path)
            Storage-->>Client: Ok(data)
        end
    end
Loading
sequenceDiagram
    participant Client
    participant HiddenStorage
    participant RateLimiter as "RateLimiter\n(on-disk)"
    participant Crypto

    Client->>HiddenStorage: unlock(password)
    HiddenStorage->>RateLimiter: check_rate_limit(outer_path)
    alt outer rate limited
        RateLimiter-->>HiddenStorage: Err(remaining)
        HiddenStorage-->>Client: Err(KeepError::RateLimited(remaining_secs))
    else outer allowed
        RateLimiter-->>HiddenStorage: Ok(())
        HiddenStorage->>HiddenStorage: try_unlock_outer(password)
        HiddenStorage->>Crypto: decrypt_outer_volume(...)
        alt outer auth fails
            Crypto-->>HiddenStorage: Err(err_outer)
        else outer succeeds
            Crypto-->>HiddenStorage: Ok(outer)
        end
        HiddenStorage->>RateLimiter: check_rate_limit(hidden_path)
        HiddenStorage->>HiddenStorage: try_unlock_hidden(password)
        HiddenStorage->>Crypto: decrypt_hidden_volume(...)
        alt hidden auth fails
            Crypto-->>HiddenStorage: Err(err_hidden)
        else hidden succeeds
            Crypto-->>HiddenStorage: Ok(hidden)
        end
        HiddenStorage->>RateLimiter: record_success(chosen_path) or record_failure(path) based on auth errors
        HiddenStorage-->>Client: Ok(VolumeType) or Err(...)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • feat: Hidden Volume #1: Modifies the hidden-volume unlock API and overlaps with the refactor of HiddenStorage::unlock/helpers present in this change.

Poem

🐰 I count failed hops and bide my pace,
The burrow marks each faltering trace,
Backoff grows till one true try clears,
A single right hop calms my fears,
Hooray — the secret burrow keeps its grace.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.71% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding rate limiting to password attempts in the unlock flow, which matches the actual implementation across all modified files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rate-limiting

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04531fc and 851d34d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • keep-core/Cargo.toml
  • keep-core/src/hidden/volume.rs
  • keep-core/src/lib.rs
  • keep-core/src/rate_limit.rs
  • keep-core/src/storage.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • keep-core/src/storage.rs
  • keep-core/Cargo.toml
🧰 Additional context used
🧬 Code graph analysis (1)
keep-core/src/hidden/volume.rs (2)
keep-core/src/rate_limit.rs (3)
  • check_rate_limit (111-130)
  • record_success (158-161)
  • record_failure (132-156)
keep-core/src/storage.rs (3)
  • unlock (191-236)
  • path (333-335)
  • create (122-165)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (10)
keep-core/src/lib.rs (1)

15-15: LGTM! Clean module declaration.

The crate-private rate_limit module is appropriately scoped for internal use by storage and hidden volume unlock flows.

keep-core/src/rate_limit.rs (4)

12-45: LGTM! Well-designed helper functions.

The constants are reasonable, path resolution logic correctly handles both file and directory storage, and the checksum provides adequate corruption detection for this use case.


47-100: LGTM! Solid exponential backoff implementation.

The record serialization is compact with integrity checking, and the exponential backoff calculation correctly prevents overflow while enforcing reasonable delay caps.


132-161: Verify intentional graceful degradation approach.

The record_failure and record_success functions silently swallow errors (file open failures at lines 141-143, lock failures at lines 145-147, and file removal errors at line 160). This appears to be an intentional design choice for graceful degradation—ensuring rate-limiting failures don't prevent unlock attempts.

However, this means rate limiting could silently fail to function (e.g., on permission issues or disk full). Consider whether observability (logging) would be valuable here to detect when rate limiting is not working.

Can you confirm this graceful degradation is intentional? If so, consider adding tracing for operational visibility when rate limiting operations fail.


163-289: LGTM! Comprehensive test coverage.

The tests thoroughly validate all critical behaviors: delay timing, exponential backoff, caps, persistence, path resolution, and corruption detection. Well done!

keep-core/src/hidden/volume.rs (5)

239-268: LGTM! Clean separation of unlock logic.

The private try_unlock_outer helper appropriately isolates the core unlock logic from rate-limiting concerns, enabling the public wrapper to control when failures are recorded.


270-292: LGTM! Correctly implements selective rate limiting.

The implementation properly addresses the previous review concern by only recording rate-limit failures for authentication-related errors (InvalidPassword and DecryptionFailed), while allowing system-level errors to propagate without penalty.


294-376: LGTM! Consistent rate limiting pattern.

Both try_unlock_hidden and unlock_hidden follow the same well-designed pattern as their outer counterparts, correctly distinguishing authentication failures from system errors.


378-415: LGTM! Sophisticated plausible deniability + rate limiting.

The combined unlock method elegantly handles the challenging requirement of maintaining constant-time behavior for plausible deniability while correctly rate-limiting only authentication failures. The logic properly:

  • Attempts both unlocks regardless of individual outcomes
  • Records success when either succeeds
  • Only records failures when both fail with authentication errors
  • Propagates system errors immediately without rate-limiting penalty

911-964: LGTM! Thorough rate limiting test coverage.

The three tests comprehensively validate rate limiting behavior across all unlock methods (unlock_outer, unlock_hidden, and unlock), confirming that rate limiting activates after the expected threshold (5 failed attempts) and returns the appropriate RateLimited error.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wksantiago wksantiago self-assigned this Dec 26, 2025
@wksantiago wksantiago linked an issue Dec 26, 2025 that may be closed by this pull request
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
keep-core/src/rate_limit.rs (1)

94-101: Consider logging mutex lock failures.

If the mutex lock fails (lines 96-98), failures are silently ignored, which could theoretically allow unlimited attempts if an attacker could poison the mutex. While mutex poisoning is rare and indicates a serious bug elsewhere, consider logging this condition for observability.

Optional: Add logging on lock failure
 pub fn record_failure(path: &Path) {
     let normalized = normalize_path(path);
     let Ok(mut limiter) = RATE_LIMITER.lock() else {
+        tracing::error!("Rate limiter mutex poisoned, cannot record failure");
         return;
     };
     let entry = limiter.get_or_create(&normalized);
     entry.record_failure();
 }
keep-core/Cargo.toml (1)

28-28: Update once_cell to the latest stable version.

The current version 1.19 is outdated. Version 1.21.3 is the latest stable release and should be used instead.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52c6235 and ad68880.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • keep-core/Cargo.toml
  • keep-core/src/error.rs
  • keep-core/src/hidden/volume.rs
  • keep-core/src/lib.rs
  • keep-core/src/rate_limit.rs
  • keep-core/src/storage.rs
🧰 Additional context used
🧬 Code graph analysis (4)
keep-core/src/error.rs (2)
keep-cli/src/server.rs (1)
  • error (427-433)
keep-cli/src/output.rs (1)
  • error (24-28)
keep-core/src/rate_limit.rs (2)
keep-core/src/hidden/volume.rs (1)
  • path (618-620)
keep-core/src/storage.rs (1)
  • path (331-333)
keep-core/src/storage.rs (2)
keep-core/src/rate_limit.rs (4)
  • check_rate_limit (80-92)
  • record_failure (51-54)
  • record_failure (94-101)
  • record_success (103-111)
keep-core/src/crypto.rs (2)
  • decrypt (96-100)
  • decrypt (203-213)
keep-core/src/hidden/volume.rs (3)
keep-core/src/rate_limit.rs (4)
  • check_rate_limit (80-92)
  • record_success (103-111)
  • record_failure (51-54)
  • record_failure (94-101)
keep-core/src/lib.rs (3)
  • unlock (58-63)
  • create (38-45)
  • open (48-55)
keep-core/src/storage.rs (4)
  • unlock (191-234)
  • path (331-333)
  • create (122-165)
  • open (167-189)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (21)
keep-core/src/lib.rs (1)

15-15: LGTM! Module exposure is correct.

The rate_limit module is properly exposed as a public module, making it accessible to the rest of the crate and external consumers.

keep-core/src/error.rs (1)

10-12: LGTM! Error variant is well-designed.

The RateLimited(u64) error variant properly encodes the wait time in seconds and provides a clear user-facing message.

keep-core/src/storage.rs (5)

13-13: LGTM! Import is necessary and properly placed.


196-198: LGTM! Rate limit check is correctly placed.

The rate limit check is properly positioned after the already-unlocked check and correctly returns the remaining delay with a minimum of 1 second.


215-221: LGTM! Failure recording on decryption error is correct.

The code properly records rate limit failures when password decryption fails, ensuring the rate limiter tracks failed unlock attempts.


230-230: LGTM! Success recording resets rate limiting correctly.

The success recording is properly placed after a complete successful unlock, ensuring the rate limit counter is reset.


483-525: LGTM! Tests comprehensively validate rate limiting behavior.

The tests properly verify:

  1. Rate limiting triggers after MAX_ATTEMPTS (5) failed attempts
  2. Successful unlock resets the rate limit counter
  3. Proper cleanup with record_success to avoid test interference
keep-core/src/rate_limit.rs (7)

10-14: Rate limiting constants are well-balanced.

The configuration (5 attempts before rate limiting, 1-second base delay, 5-minute cap) strikes a good balance between security and usability. The in-memory rate limiter will reset on process restart, which is acceptable for a desktop application but means an attacker could bypass limits by restarting the process.


16-18: Path normalization approach is pragmatic.

The fallback to to_path_buf() when canonicalization fails is reasonable. While different representations of the same path could theoretically be rate-limited separately, this is unlikely in practice for this use case.


20-60: LGTM! RateLimitEntry implementation is robust.

The exponential backoff logic is correctly implemented with proper overflow/underflow protection using saturating_add and saturating_sub. The delay progression (1s, 2s, 4s, 8s, ..., capped at 300s) provides strong protection against brute force while allowing legitimate users to retry after waiting.


62-78: LGTM! RateLimiter struct is clean and idiomatic.

The HashMap-based storage with entry().or_insert_with() pattern is the standard approach for this use case.


80-92: LGTM! Rate limit check handles edge cases defensively.

The function returns the maximum delay if the mutex lock fails (poisoned), which is appropriate since mutex poisoning indicates a serious bug. The API design using Result<(), Duration> clearly separates the success and rate-limited cases.


103-111: LGTM! Success recording correctly uses get_mut.

Using get_mut instead of get_or_create is correct here - if no entry exists, there's nothing to reset. Same logging consideration as record_failure applies if the mutex is poisoned.


113-224: Excellent test coverage!

The tests comprehensively validate all aspects of the rate limiter:

  • No delay before MAX_ATTEMPTS threshold
  • Exponential backoff progression
  • Maximum delay cap
  • Success resets
  • Delay expiration
  • Path independence

The use of non-existent paths in unique_path is fine since normalize_path handles this with its fallback logic.

keep-core/src/hidden/volume.rs (7)

43-43: LGTM! Import is necessary and correctly placed.


239-268: LGTM! Core unlock logic properly extracted.

Extracting the core unlock logic into try_unlock_outer as a private method is good separation of concerns, allowing the public method to handle rate limiting consistently.


270-289: LGTM! Rate limiting wrapper is correctly implemented.

The wrapper properly:

  1. Checks if already unlocked before rate limiting (efficiency)
  2. Enforces rate limits before attempting unlock
  3. Records success/failure based on outcome
  4. Propagates errors appropriately

291-349: LGTM! Hidden volume unlock logic properly extracted.

Consistent with the outer volume approach, the core hidden unlock logic is cleanly separated from rate limiting concerns.


351-370: LGTM! Consistent rate limiting pattern.

The unlock_hidden wrapper follows the same pattern as unlock_outer, maintaining consistency across the codebase.


372-394: LGTM! Smart unlock correctly implements rate limiting and timing attack mitigation.

The implementation correctly:

  1. Checks rate limit once upfront (not per volume type)
  2. Attempts both unlock methods to prevent timing side-channels
  3. Records success if either succeeds, failure only if both fail
  4. Returns appropriate volume type on success

This approach prevents an attacker from determining whether a password was tried against the outer or hidden volume based on timing or rate limit behavior.


890-949: LGTM! Comprehensive rate limiting test coverage.

The tests validate rate limiting across all three unlock methods:

  1. unlock_outer - outer volume specific
  2. unlock_hidden - hidden volume specific
  3. unlock - smart unlock attempting both

Each test properly verifies that the first 5 attempts receive password errors while the 6th triggers rate limiting, with proper cleanup to avoid test interference.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
keep-core/src/hidden/volume.rs (1)

890-949: Tests verify basic rate limiting but have manual cleanup.

The three tests correctly verify that rate limiting triggers after 5 failed attempts. However, the manual rate_limit::record_success(&path) calls at lines 908, 928, and 948 create potential test interdependencies if tests fail before cleanup executes.

Consider these test improvements:

  • Add a test helper or fixture to manage rate limiter state reset
  • Add test coverage for successful unlock resetting the failure counter
  • Verify that the rate limit delay increases with repeated failures (exponential backoff per PR description)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad68880 and e4fc695.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • keep-core/Cargo.toml
  • keep-core/src/error.rs
  • keep-core/src/hidden/volume.rs
  • keep-core/src/lib.rs
  • keep-core/src/rate_limit.rs
  • keep-core/src/storage.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • keep-core/src/lib.rs
  • keep-core/src/rate_limit.rs
  • keep-core/src/storage.rs
  • keep-core/Cargo.toml
🧰 Additional context used
🧬 Code graph analysis (2)
keep-core/src/error.rs (2)
keep-cli/src/server.rs (1)
  • error (427-433)
keep-cli/src/output.rs (1)
  • error (24-28)
keep-core/src/hidden/volume.rs (2)
keep-core/src/rate_limit.rs (4)
  • check_rate_limit (80-92)
  • record_success (103-111)
  • record_failure (51-54)
  • record_failure (94-101)
keep-core/src/storage.rs (4)
  • unlock (191-234)
  • path (331-333)
  • create (122-165)
  • open (167-189)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
keep-core/src/error.rs (1)

10-11: LGTM! Clean error variant addition.

The new RateLimited(u64) error variant is well-implemented with a clear message format. The u64 parameter appropriately holds the retry delay in seconds.

keep-core/src/hidden/volume.rs (3)

43-43: LGTM! Rate limit module imported.

The import is correctly added to enable rate limiting functionality.


239-268: Good refactoring to separate concerns.

The core unlock logic is now isolated in this private method, making it easier to add rate limiting in the public wrapper. The implementation correctly handles key derivation, decryption, and database opening.


291-349: Well-structured core unlock logic for hidden volume.

The private method correctly isolates the hidden volume unlock implementation, including proper error handling that returns InvalidPassword for authentication failures (lines 322, 329). The separation from rate limiting logic is clean.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
keep-core/src/hidden/volume.rs (1)

911-930: Consider more specific error assertions.

The test correctly verifies rate limiting behavior, but line 922's assertion !matches!(result, Err(KeepError::RateLimited(_))) only checks that the error is NOT rate-limited, rather than explicitly verifying it IS an authentication failure.

🔎 More specific assertion
         for _ in 0..5 {
             let mut storage = HiddenStorage::open(&path).unwrap();
             let result = storage.unlock_outer("wrong");
-            assert!(result.is_err());
-            assert!(!matches!(result, Err(KeepError::RateLimited(_))));
+            assert!(matches!(
+                result,
+                Err(KeepError::InvalidPassword | KeepError::DecryptionFailed)
+            ));
         }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4fc695 and 8e1859f.

📒 Files selected for processing (1)
  • keep-core/src/hidden/volume.rs
🧰 Additional context used
🧬 Code graph analysis (1)
keep-core/src/hidden/volume.rs (3)
keep-core/src/rate_limit.rs (4)
  • check_rate_limit (80-92)
  • record_success (103-111)
  • record_failure (51-54)
  • record_failure (94-101)
keep-core/src/lib.rs (3)
  • unlock (58-63)
  • create (38-45)
  • open (48-55)
keep-core/src/storage.rs (4)
  • unlock (191-234)
  • path (331-333)
  • create (122-165)
  • open (167-189)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (8)
keep-core/src/hidden/volume.rs (8)

43-43: LGTM: Rate limit module imported.

The import is necessary for the rate limiting integration throughout the unlock flows.


239-268: LGTM: Clean extraction of core unlock logic.

The separation of try_unlock_outer (core logic) from unlock_outer (rate-limiting wrapper) is well-structured and maintains the original unlock functionality.


270-292: Excellent: Past review feedback addressed correctly.

The implementation now only records rate-limit failures for authentication-related errors (lines 286-287), preventing users from being penalized for system-level issues like IO failures or database corruption. This directly addresses the concern raised in previous reviews.


294-352: LGTM: Hidden volume unlock logic properly extracted.

The private helper correctly returns InvalidPassword for authentication failures (lines 325, 332), which will be properly caught by the rate-limiting logic in the public wrapper.


354-376: Excellent: Consistent implementation with past feedback addressed.

Like unlock_outer, this method correctly limits rate-limit failure recording to authentication errors only (lines 370-371), resolving the concern from previous reviews.


378-415: Excellent: Sophisticated error handling that fully addresses past feedback.

The combined unlock implementation correctly:

  • Checks rate limits before expensive KDF operations (line 379)
  • Preserves constant-time execution for timing attack prevention (lines 383-384)
  • Returns non-authentication errors immediately without rate-limiting (lines 403-408)
  • Only records rate-limit failures when both unlock attempts fail with authentication errors (lines 410-412)

This comprehensively addresses the concern from previous reviews about incorrectly penalizing users for system-level failures.


932-950: LGTM: Rate limiting test with explicit error check.

This test correctly verifies rate limiting for the hidden volume unlock flow. Line 942's explicit check for InvalidPassword is good practice.


952-970: LGTM: Comprehensive test for combined unlock rate limiting.

The test correctly verifies that rate limiting applies to the combined unlock flow, with proper explicit error type assertions and cleanup.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
keep-core/src/lib.rs (1)

15-15: Module visibility is inconsistent with its contents.

The module is declared as pub but all functions inside (check_rate_limit, record_failure, record_success) are pub(crate). External crate users can see keep_core::rate_limit exists but cannot access anything within it. This is likely unintentional.

🔎 Suggested fix: Make module crate-private
-pub mod rate_limit;
+pub(crate) mod rate_limit;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e1859f and 04531fc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • keep-core/Cargo.toml
  • keep-core/src/error.rs
  • keep-core/src/hidden/volume.rs
  • keep-core/src/lib.rs
  • keep-core/src/rate_limit.rs
  • keep-core/src/storage.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • keep-core/src/storage.rs
🧰 Additional context used
🧬 Code graph analysis (2)
keep-core/src/error.rs (2)
keep-cli/src/server.rs (1)
  • error (427-433)
keep-cli/src/output.rs (1)
  • error (24-28)
keep-core/src/hidden/volume.rs (3)
keep-core/src/rate_limit.rs (4)
  • check_rate_limit (80-92)
  • record_success (103-111)
  • record_failure (51-54)
  • record_failure (94-101)
keep-core/src/lib.rs (3)
  • unlock (58-63)
  • create (38-45)
  • open (48-55)
keep-core/src/storage.rs (4)
  • unlock (191-234)
  • path (331-333)
  • create (122-165)
  • open (167-189)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (15)
keep-core/Cargo.toml (1)

28-28: **Dependency addition looks good.**The latest version of once_cell is 1.21.3, while this PR uses 1.19. Consider updating to a more recent version for any bug fixes. Note that most of this crate's functionality is available in std starting with Rust 1.70 (std::sync::OnceLock), and std::sync::LazyLock is stable as of Rust 1.80. Since the crate's MSRV is 1.81, you could potentially use std::sync::LazyLock directly without the external dependency. However, using once_cell is also perfectly acceptable.

keep-core/src/rate_limit.rs (7)

1-14: Well-structured module setup with appropriate safety constraints.

The #![forbid(unsafe_code)] directive is excellent for security-critical rate limiting code. The constants provide sensible defaults: 5 failed attempts before lockout, 1-second base delay with exponential backoff, capped at 5 minutes.


16-18: Path normalization approach is sound.

The fallback to the original path when canonicalization fails handles edge cases gracefully (e.g., paths that don't exist yet). Since normalize_path is called consistently within all public API functions, path representation remains consistent across calls.


20-60: Solid rate limit entry implementation with proper overflow protection.

The exponential backoff calculation is correct: after 5 failed attempts, delays grow as 1s, 2s, 4s, 8s... capped at 300s. The saturating_add on line 52 prevents integer overflow, and the shift clamping (excess.min(8)) prevents shift overflow.


62-78: Clean rate limiter structure.

The implementation is straightforward. One consideration: entries are never evicted, so memory will grow with each unique path. For a key management application where the number of vaults is typically small, this is acceptable. If this were a high-throughput service with many paths, you'd want TTL-based eviction.


80-92: Fail-closed behavior on mutex poisoning is the right security choice.

If the mutex is poisoned (another thread panicked while holding the lock), returning MAX_DELAY_SECS ensures the system fails safely rather than allowing unlimited attempts.


94-111: Recording functions handle edge cases correctly.

Good design choice: record_success (line 108) only resets existing entries rather than creating new ones, avoiding unnecessary memory allocation for paths that were never rate-limited.


113-224: Comprehensive test coverage.

The tests effectively validate all rate limiting behaviors including the exponential backoff progression, delay caps, success resets, time expiration, and path isolation. The unique_path helper ensures test isolation when tests run in parallel.

keep-core/src/error.rs (1)

10-11: Well-designed error variant for rate limiting.

The RateLimited(u64) variant correctly carries the wait duration in seconds, and the error message is clear and actionable for users. The placement after InvalidPassword is logical since both relate to authentication flow control.

keep-core/src/hidden/volume.rs (6)

43-43: Import addition looks good.


239-268: Clean refactor separating core unlock logic from rate limiting concerns.

The private try_unlock_outer method encapsulates the actual decryption logic, allowing the public unlock_outer to handle rate limiting as a cross-cutting concern. This separation improves maintainability.


270-292: Correctly implements selective rate limiting for authentication errors only.

The implementation now properly distinguishes between authentication failures (lines 286-288) and other errors (IO, database). Only InvalidPassword and DecryptionFailed increment the rate limit counter, addressing the previous review feedback.


294-376: Consistent rate limiting pattern applied to hidden volume unlock.

The try_unlock_hidden/unlock_hidden split mirrors the outer volume pattern, maintaining consistency. Authentication error filtering (lines 370-372) correctly prevents rate limiting on system errors.


378-415: Comprehensive error handling for combined unlock preserves plausible deniability.

The implementation correctly handles all error combinations:

  • Either path succeeding → record success
  • Non-auth error on either path → return that error without rate-limit penalty
  • Both paths failing with auth errors → record single failure

The is_auth_error helper (lines 397-399) cleanly encapsulates the authentication error check. This addresses the previous review feedback about incorrectly penalizing system-level failures.


910-970: Good test coverage for rate limiting across all unlock paths.

The tests effectively validate:

  1. First 5 failed attempts return authentication errors (not RateLimited)
  2. 6th attempt is blocked with RateLimited error
  3. All three unlock methods (outer, hidden, combined) are covered

The rate_limit::record_success(&path) calls at the end of each test ensure proper cleanup since the rate limiter is a global static.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add rate limiting to password attempts in unlock flow

2 participants