Skip to content

feat(fido2): bump demand to v2, mask PIN during typing#334

Merged
jdx merged 2 commits intomainfrom
feat/fido2-mask-on-submit
Mar 8, 2026
Merged

feat(fido2): bump demand to v2, mask PIN during typing#334
jdx merged 2 commits intomainfrom
feat/fido2-mask-on-submit

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 8, 2026

Summary

  • Bump demand dependency from v1 to v2.0.0
  • Use .password(true) for both FIDO2 PIN input prompts (runtime and setup) so the PIN is masked with asterisks during typing, preventing shoulder-surfing

Test plan

  • All cargo tests pass
  • Manual: run fnox set with a FIDO2 provider — PIN should be masked during typing

🤖 Generated with Claude Code

Bump demand to v2.0.0 and use mask_on_submit(true) for both FIDO2 PIN
prompts so the PIN is replaced with asterisks after submission.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request improves the security of FIDO2 PIN handling by introducing a feature that masks the PIN input after it has been submitted. This change is facilitated by an upgrade of the demand crate to its version 2, which provides the necessary functionality to obscure sensitive input post-entry.

Highlights

  • Dependency Update: The demand dependency has been updated from version 1.x to 2.0.0, enabling new input masking capabilities.
  • FIDO2 PIN Masking: FIDO2 PIN input prompts, both for runtime authentication and setup, now mask the entered PIN with asterisks immediately after submission for enhanced security.
Changelog
  • Cargo.lock
    • Updated demand package version from 1.8.2 to 2.0.0.
    • Updated the checksum for the demand package.
  • Cargo.toml
    • Updated the demand dependency specification from 1 to 2.
  • src/providers/fido2.rs
    • Added mask_on_submit(true) to the demand::Input for the FIDO2 PIN prompt during runtime.
    • Added mask_on_submit(true) to the demand::Input for the FIDO2 PIN prompt during setup.
Activity
  • The author confirmed that all 107 cargo tests passed.
  • A manual test plan was outlined to verify the PIN masking behavior with a FIDO2 provider.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request upgrades the demand dependency to v2 and uses the new mask_on_submit feature to improve security when prompting for a FIDO2 PIN. However, a critical security vulnerability exists where the FIDO2 PIN can be stored in the configuration file, undermining hardware key security. Additionally, the PIN remains visible in cleartext while the user is typing it, posing a shoulder surfing risk. It is recommended to remove the PIN storage capability entirely and implement character masking during the input process.

Comment thread src/providers/fido2.rs
Comment on lines 77 to 81
let input = demand::Input::new("FIDO2 PIN (leave empty if not required)")
.placeholder("")
.mask_on_submit(true)
.run()
.map_err(|e| FnoxError::Provider(format!("Failed to read PIN: {e}")))?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The FIDO2 PIN is displayed in cleartext while the user is typing it, exposing it to shoulder surfing attacks. While mask_on_submit is a good enhancement, the overall logic has a critical security flaw: the PIN can be stored in the configuration (self.pin), which is a significant risk. Hardware key PINs should never be stored on disk. It is recommended to always prompt for the PIN, remove storage capability, and use character masking during input. For in-memory protection, consider secrecy::SecretString.

Suggested change
let input = demand::Input::new("FIDO2 PIN (leave empty if not required)")
.placeholder("")
.mask_on_submit(true)
.run()
.map_err(|e| FnoxError::Provider(format!("Failed to read PIN: {e}")))?;
let input = demand::Input::new("FIDO2 PIN (leave empty if not required)")
.placeholder("")
.mask('*')
.mask_on_submit(true)
.run()
.map_err(|e| FnoxError::Provider(format!("Failed to read PIN: {e}")))?;

Comment thread src/providers/fido2.rs
Comment on lines 177 to 181
let pin_input = demand::Input::new("FIDO2 PIN (leave empty if not set)")
.placeholder("")
.mask_on_submit(true)
.run()
.map_err(|e| FnoxError::Config(format!("Failed to read PIN: {}", e)))?;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The setup PIN prompt displays the PIN in cleartext during entry, posing a shoulder surfing risk. Additionally, this setup function returns the PIN (pin_to_store) to be stored in the configuration, which is a critical security vulnerability. FIDO2 PINs should not be stored. It is recommended to mask input characters while typing, modify the function to not return the PIN, and remove PIN storage capability. For in-memory protection, consider secrecy::SecretString.

        let pin_input = demand::Input::new("FIDO2 PIN (leave empty if not set)")
            .placeholder("")
            .mask('*')
            .mask_on_submit(true)
            .run()
            .map_err(|e| FnoxError::Config(format!("Failed to read PIN: {}", e)))?;

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 8, 2026

Greptile Summary

This PR bumps the demand dependency from v1 to v2.0.0 and adds .password(true) to the two interactive FIDO2 PIN prompts in src/providers/fido2.rs, bringing them in line with how every other secret-input prompt in the codebase already works (e.g. set.rs line 87). The net effect is that the FIDO2 PIN is now hidden as the user types, rather than echoed in plaintext.

Key observations:

  • The functional change is a clear security improvement and is consistent with the existing demand usage patterns elsewhere in the project.
  • The PR title ("mask PIN on submit") and description ("Add mask_on_submit(true)... PIN is replaced with asterisks after the user submits") do not match the actual implementation — .password(true) hides input during typing, while mask_on_submit(true) would show it while typing and replace it only after submission. The test plan also describes mask_on_submit semantics. The description/test plan should be corrected, or the implementation should be updated to use mask_on_submit(true) if that was the intended UX.
  • The demand v1→v2 major-version bump appears safe for this codebase: all other call sites (demand::Input, demand::Confirm, demand::Select) use API that exists in both versions, and the 107-test suite passes.

Confidence Score: 4/5

  • Safe to merge — the change is a security improvement with no cryptographic or storage impact, but the PR description and test plan describe a different behaviour than what was actually implemented.
  • The code change itself is small, correct, and consistent with existing patterns in the codebase. The only issue is a discrepancy between the PR description/test plan (which claim mask_on_submit / show-then-mask-after-submit semantics) and the actual implementation (.password(true) / hide-during-typing). This is documentation/intent ambiguity rather than a functional defect, and the implemented behaviour is arguably more secure, so confidence is high but not perfect.
  • No files require special attention; the discrepancy between the described and actual PIN-masking behaviour in src/providers/fido2.rs should be clarified.

Important Files Changed

Filename Overview
src/providers/fido2.rs Adds .password(true) to both FIDO2 PIN prompts (runtime at line 79, setup at line 179), hiding input during typing. Functionally a security improvement, but the change does not match the PR description's claim of adding mask_on_submit(true).
Cargo.toml Bumps demand from "1" to "2". All other demand call sites in the repo (set.rs, init.rs, auth_prompt.rs, yubikey.rs) appear to use the same API surface that exists in both v1 and v2.
Cargo.lock Lock file updated to pin demand at v2.0.0 with a new checksum; no other dependency changes.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant P as fido2.rs (PIN prompt)
    participant D as demand::Input (.password(true))
    participant F as FIDO2 Device

    U->>P: Invokes FIDO2 operation
    P->>D: Input::new("FIDO2 PIN…").password(true).run()
    Note over D: Characters hidden during typing (no echo)
    U->>D: Types PIN
    D-->>P: Returns PIN string (or empty)
    alt PIN entered
        P->>F: pass PIN to ctap_hid_fido2
    else Empty input
        P->>F: no PIN
    end
    F-->>P: hmac-secret
    P-->>U: Secret used for encrypt/decrypt
Loading

Fix All in Claude Code

Last reviewed commit: a28dba9

Comment thread src/providers/fido2.rs
PIN is a security credential — mask during typing, not just after submit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/providers/fido2.rs
Comment on lines 77 to 81
let input = demand::Input::new("FIDO2 PIN (leave empty if not required)")
.placeholder("")
.password(true)
.run()
.map_err(|e| FnoxError::Provider(format!("Failed to read PIN: {e}")))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR description / implementation mismatch

The PR title says "mask PIN on submit" and the description claims mask_on_submit(true) was added, but the actual change adds .password(true) — which hides characters during typing (no echo), not merely on submit. The test plan ("PIN should show during typing but mask after submit") also describes mask_on_submit semantics, not password-field semantics.

To confirm: src/commands/set.rs line 87 already used .password(true) with demand v1 for "hidden input", so this method hides keystrokes as they are entered. That's the more secure choice and consistent with how the rest of the codebase handles secrets — but it does not match what the PR description promises.

If the intent was truly a "show while typing, mask on submit" experience, mask_on_submit(true) (if available in demand v2) should be used instead, or the PR description and test plan should be updated to reflect that .password(true) (fully hidden input) is the actual and intended behaviour.

Fix in Claude Code

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread src/providers/fido2.rs
} else if atty::is(atty::Stream::Stderr) {
let input = demand::Input::new("FIDO2 PIN (leave empty if not required)")
.placeholder("")
.password(true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wrong method: password() masks during typing, not on submit

Medium Severity

The PR title and description state the intent is to use mask_on_submit(true) so the PIN is visible during typing but masked with asterisks after the user submits. However, the code uses .password(true) instead, which masks input during typing (standard password field behavior). This contradicts the stated UX goal of "PIN should show during typing but mask after submit." The user won't be able to visually verify their PIN while entering it.

Additional Locations (1)

Fix in Cursor Fix in Web

@jdx jdx changed the title feat(fido2): mask PIN on submit using demand v2 feat(fido2): bump demand to v2, mask PIN during typing Mar 8, 2026
@jdx jdx enabled auto-merge (squash) March 8, 2026 21:20
@jdx jdx merged commit dd0e17a into main Mar 8, 2026
17 checks passed
@jdx jdx deleted the feat/fido2-mask-on-submit branch March 8, 2026 21:37
jdx pushed a commit that referenced this pull request Mar 9, 2026
### 🚀 Features

- **(cloudflare)** add Cloudflare API token lease backend by
[@jdx](https://github.com/jdx) in
[#335](#335)
- **(fido2)** bump demand to v2, mask PIN during typing by
[@jdx](https://github.com/jdx) in
[#334](#334)
- **(init)** add -f as short alias for --force by
[@jdx](https://github.com/jdx) in
[#329](#329)
- **(lease)** add --all flag, default to creating all leases by
[@jdx](https://github.com/jdx) in
[#337](#337)
- **(lease)** add GitHub App installation token lease backend by
[@jdx](https://github.com/jdx) in
[#342](#342)

### 🐛 Bug Fixes

- **(config)** fix directory locations to follow XDG spec by
[@jdx](https://github.com/jdx) in
[#336](#336)
- **(exec)** use unix exec and exit silently on subprocess failure by
[@jdx](https://github.com/jdx) in
[#339](#339)
- **(fido2)** remove duplicate touch prompt by
[@jdx](https://github.com/jdx) in
[#332](#332)
- **(set)** write to lowest-priority existing config file by
[@jdx](https://github.com/jdx) in
[#331](#331)
- **(tui)** skip providers requiring interactive auth by
[@jdx](https://github.com/jdx) in
[#333](#333)

### 🛡️ Security

- **(ci)** retry lint step to handle transient pkl fetch failures by
[@jdx](https://github.com/jdx) in
[#341](#341)
- **(mcp)** add MCP server for secret-gated AI agent access by
[@jdx](https://github.com/jdx) in
[#343](#343)
- add guide for fnox sync by [@jdx](https://github.com/jdx) in
[#328](#328)

### 🔍 Other Changes

- share Rust cache across CI jobs by [@jdx](https://github.com/jdx) in
[#340](#340)
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