Skip to content

Conversation

@atrtde
Copy link
Owner

@atrtde atrtde commented Feb 9, 2026

Summary by CodeRabbit

  • New Features

    • Password caching with configurable TTL options (15 minutes to 7 days)
    • Per-profile and global cache settings with encryption support
  • Bug Fixes

    • Package registry and backup outputs no longer display ANSI escape codes
    • Cache invalidation on password errors during restore and validation operations
  • Chores

    • Added .secrets/ directory to .gitignore for local configuration management

@atrtde atrtde self-assigned this Feb 9, 2026
Copilot AI review requested due to automatic review settings February 9, 2026 21:54
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A new security subsystem introduces per-profile and global password caching with age-based encryption, replacing direct password prompts across backup, restore, and validate tasks. ANSI escape code stripping is added for clean output, and .gitignore is extended to exclude the .secrets/ directory.

Changes

Cohort / File(s) Summary
Security Subsystem Core
src/main.rs, src/security.rs, src/utils/paths.rs
Introduces SecurityConfig with per-profile password caching backed by age encryption, TTL management, and persistent identity. Adds constants and helpers (SECRETS_DIR, SECURITY_CONFIG_FILE, get_security_config_path, get_secrets_dir) for configuration and secrets storage paths.
Password Cache Integration
src/tasks/backup.rs, src/tasks/restore.rs, src/tasks/validate.rs
Replaces direct prompt_password calls with get_or_prompt_password and adds password error detection to invalidate cache on scrypt/decrypt failures. Import updates reflect new security module utilities.
Output Cleanup
src/utils/system.rs
Adds strip_ansi_codes function using regex to remove ANSI escape sequences from command output.
Repository Configuration
src/tasks/sync.rs
Extends .gitignore handling to explicitly exclude .secrets/ directory in both creation and update flows.
Documentation
CHANGELOG.md
Documents v2.3.0 fix: Package Registry Output now strips ANSI escape codes.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Task as Task (backup/restore/validate)
    participant Cache as Password Cache
    participant Encryption as age Encryption
    participant Identity as Identity Manager
    participant Prompt as User Prompt

    User->>Task: Initiate encrypted operation
    Task->>Cache: Check cached password
    alt Cache hit & not expired
        Cache->>Task: Return cached password
        Task->>User: Proceed with operation
    else Cache miss or expired
        Task->>Prompt: Request password from user
        Prompt->>User: Display prompt
        User->>Prompt: Enter password
        Prompt->>Task: Return password input
        Task->>Identity: Get or create cache identity
        Identity->>Encryption: Encrypt password with identity
        Encryption->>Cache: Store encrypted password + TTL
        Task->>User: Proceed with operation
    end
    
    alt Operation failure (password-related)
        Task->>Cache: Invalidate cached password
        Cache-->>Task: Cache cleared
        Task->>User: Log error & warn user
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🔐 A rabbit hops through secrets deep,
With passwords locked and caches to keep,
Encrypted keys in .secrets store,
No ANSI codes to clutter the floor,
Security strengthened, profiles all blessed! 🐰

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'feat: secure password cache' directly reflects the main change in the changeset, which introduces a comprehensive password caching system with encryption and TTL management.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-secure-password-cache

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.

Copy link
Contributor

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/tasks/restore.rs (1)

316-342: ⚠️ Potential issue | 🟠 Major

After detecting a bad password, the loop continues decrypting remaining files with the same stale password.

When is_password_error is true on Line 323, the cache is invalidated — good. But the loop keeps iterating over the remaining entries using the same password value, which will almost certainly fail for every subsequent file with the same error, producing noisy warnings and redundant cache-invalidation calls.

Consider breaking out of the loop (or at least short-circuiting the remaining encrypted entries) after the first password-related failure, since retrying with the same password is pointless.

Proposed fix
                     Err(e) => {
                         let err_msg = e.to_string();
                         if is_password_error(&err_msg) {
                             invalidate_password_cache(profile);
+                            log_warning(&format!(
+                                "Failed to restore encrypted {}: {}",
+                                entry.name, e
+                            ));
+                            log_warning("Password error detected — skipping remaining encrypted restores");
+                            skipped_count += 1;
+                            // Count remaining entries as skipped
+                            // (they would fail with the same password)
+                            break;
                         }
                         log_warning(&format!(
                             "Failed to restore encrypted {}: {}",
                             entry.name, e
                         ));
                         skipped_count += 1;
                     }

Note: you'll need to account for the remaining unprocessed entries in skipped_count after the break.

🤖 Fix all issues with AI agents
In `@CHANGELOG.md`:
- Around line 5-8: The changelog entry for v2.3.0 is missing the primary feature
from this PR: the secure password caching subsystem; update CHANGELOG.md under
v2.3.0 to add an "Added" section describing the new secure password cache
(mentioning the implemented types/APIs such as PasswordCache or
SecurePasswordStore and the public methods like cachePassword and
getCachedPassword or equivalent) and a brief note about security/expiry behavior
so users know it's available and how it behaves.

In `@src/security.rs`:
- Around line 291-303: The current code writes the encrypted bytes to cache_path
with default permissions (fs::write) then tightens them, creating a TOCTOU
window; change it to write to a temporary file in the same directory (e.g.,
cache_path.with_extension("tmp") or a generated unique name), write the
encrypted bytes using the existing encryptor.wrap_output/write_all/finish flow
into that temp file, set Unix permissions
(std::fs::Permissions::from_mode(0o600)) on the temp file, flush/sync the temp
file if needed, then atomically rename (std::fs::rename) the temp file to
cache_path so the final cache_path is created with correct permissions and no
window exists between write and permission change (refer to variables/functions:
encrypted, writer, encryptor.wrap_output, cache_path, fs::set_permissions).
- Around line 174-186: The function get_password_cache_path currently only
replaces '/' in profile.name which is insufficient; create and call a sanitizer
(e.g., sanitize_profile_name) used inside get_password_cache_path to convert
profile.name to a safe basename: decode Option with unwrap_or("common"), trim
whitespace, reject or replace null bytes and path separators (both '/' and
'\\'), collapse/normalize sequences of "..", and then apply a whitelist (e.g.,
allow only ASCII letters, digits, '-', '_', and '.') falling back to a safe
default like "common" if the result is empty or too long; reference
get_password_cache_path, ActiveProfile.name, and PASSWORD_CACHE_FILE_PREFIX when
updating code.
- Around line 308-330: The private key file is created with default umask then
permissions are tightened after writing, creating a TOCTOU window; change
get_or_create_cache_identity to create the key file atomically with restrictive
permissions: on Unix use std::fs::OpenOptions +
std::os::unix::fs::OpenOptionsExt and call
.create_new(true).write(true).mode(0o600).open(&key_path) to obtain a File,
write identity.to_string() via file.write_all(...)? and call file.sync_all()
(and drop the file) instead of fs::write, falling back to the existing fs::write
+ set_permissions behavior on non-Unix; reference symbols:
get_or_create_cache_identity, PASSWORD_CACHE_KEY_FILE, key_path.
🧹 Nitpick comments (6)
src/security.rs (3)

79-83: PasswordCacheRecord stores the password as a plain String in the serialization struct.

While the record is encrypted before hitting disk, the plaintext password field in PasswordCacheRecord (and the serialized JSON Vec<u8>) will remain in memory until dropped, with no zeroing guarantee. Consider using secrecy::Zeroize on the decrypted buffer in load_cached_password (line 222) and on serialized in store_cached_password (line 289) after use.

This is a defense-in-depth concern rather than a critical vulnerability.


25-30: ProfileSecurityConfig struct only has one optional field.

Not an issue now, but the struct seems like a thin wrapper. If more per-profile security settings are planned, this is fine as-is. Otherwise, consider a note/comment about intended extensibility.


332-344: Test coverage is minimal — only as_seconds() is tested.

The core logic (load/store/invalidate cache, identity management, TTL expiry) has no test coverage. Consider adding tests for at least SecurityConfig::default() serialization round-trip and TTL expiry logic.

Would you like me to open an issue to track adding integration tests for the security subsystem?

src/utils/system.rs (1)

26-30: Regex is recompiled on every call — consider using a static LazyLock.

Regex::new parses and compiles the pattern each time strip_ansi_codes is invoked. If this is called frequently (e.g., for each package manager output), that's wasteful. std::sync::LazyLock is stable as of Rust 1.80.0 and provides an efficient solution.

♻️ Proposed fix using std::sync::LazyLock
+use std::sync::LazyLock;
+
+static ANSI_RE: LazyLock<Regex> = LazyLock::new(|| {
+    Regex::new(r"\x1B\[[0-?]*[ -/]*[`@-`~]").unwrap()
+});
+
 /// Strips ANSI escape codes from a string.
 pub fn strip_ansi_codes(input: &str) -> String {
-    // Matches CSI (Control Sequence Introducer) ANSI escape codes.
-    let re = Regex::new(r"\x1B\[[0-?]*[ -/]*[`@-`~]").unwrap();
-    re.replace_all(input, "").to_string()
+    ANSI_RE.replace_all(input, "").to_string()
 }
src/tasks/backup.rs (1)

174-175: Good addition, but strip_ansi_codes recompiles a Regex on every call.

The underlying strip_ansi_codes in src/utils/system.rs calls Regex::new(…).unwrap() each invocation. In a loop over many package managers this is wasteful. Consider using lazy_static! or std::sync::LazyLock (stable since Rust 1.80) to compile the regex once.

Since the fix lives in src/utils/system.rs (not this file), flagging here for awareness.

src/tasks/restore.rs (1)

347-350: is_password_error heuristic is reasonable but brittle.

String-matching on error messages couples this to the age crate's internal wording. If the crate changes its error text, this silently stops working. Consider adding a comment noting the dependency on age error strings, and/or adding a unit test that verifies the expected error substring from the current age version.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new security layer for password handling (encrypted, TTL-based password caching) and integrates it into the backup/restore/validate workflows, while also cleaning package registry output by stripping ANSI codes.

Changes:

  • Added security module that caches the encryption password in ~/.mntn/.secrets/ (encrypted with age) with configurable TTL and invalidation support.
  • Updated backup/restore/validate tasks to use cached password prompting and to invalidate the cache on suspected password/decrypt failures.
  • Added ANSI escape stripping for package-manager command output and ensured .secrets/ is excluded from git sync via .gitignore management.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/utils/system.rs Adds strip_ansi_codes helper + unit test.
src/utils/paths.rs Adds .secrets + security.json paths/helpers + tests.
src/security.rs New encrypted password cache + security config logic + minimal tests.
src/tasks/backup.rs Switches to cached password prompting; strips ANSI from package outputs.
src/tasks/restore.rs Switches to cached password prompting; invalidates cache on password-like errors.
src/tasks/validate.rs Switches to cached password prompting; invalidates cache on decrypt/password-like errors.
src/tasks/sync.rs Adds .secrets/ to default .gitignore and appends it if missing.
src/main.rs Registers the new security module.
CHANGELOG.md Adds a v2.3.0 entry (currently only documents ANSI stripping).
Cargo.lock Dependency lockfile updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@atrtde atrtde changed the base branch from main to staging February 9, 2026 22:23
@atrtde atrtde merged commit fa30d0e into staging Feb 9, 2026
8 checks passed
@atrtde atrtde deleted the feat-secure-password-cache branch February 9, 2026 23:08
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.

1 participant