feat(fido2): bump demand to v2, mask PIN during typing#334
Conversation
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>
Summary of ChangesHello, 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 Highlights
Changelog
Activity
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| 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}")))?; |
There was a problem hiding this comment.
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.
| 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}")))?; |
| 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)))?; |
There was a problem hiding this comment.
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 SummaryThis PR bumps the Key observations:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
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
Last reviewed commit: a28dba9 |
PIN is a security credential — mask during typing, not just after submit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| 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}")))?; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| } else if atty::is(atty::Stream::Stderr) { | ||
| let input = demand::Input::new("FIDO2 PIN (leave empty if not required)") | ||
| .placeholder("") | ||
| .password(true) |
There was a problem hiding this comment.
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)
### 🚀 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)


Summary
demanddependency from v1 to v2.0.0.password(true)for both FIDO2 PIN input prompts (runtime and setup) so the PIN is masked with asterisks during typing, preventing shoulder-surfingTest plan
fnox setwith a FIDO2 provider — PIN should be masked during typing🤖 Generated with Claude Code