Skip to content

Mask secret input in the CLI credentials prompt - #5889

Merged
MaceWindu merged 2 commits into
masterfrom
feature/cli-mask-secret-input
Sep 8, 2026
Merged

MaceWindu merged 2 commits into
masterfrom
feature/cli-mask-secret-input

Conversation

@MaceWindu

Copy link
Copy Markdown
Contributor

Problem

dotnet linq2db credentials set — the interactive step that stores the password query / execute / schema / mcp later resolve through --credentials — read the password with Console.ReadKey(true) and echoed nothing at all.

A paste was therefore invisible. There was no way to tell whether the clipboard had content, whether it landed once or twice, or whether anything had been entered before pressing Enter; the only feedback was Passwords do not match. after the confirm prompt, or a silently-stored empty password.

SystemCliEnvironment.TryReadSecret was the only interactive secret reader in the product (one Console.ReadKey in all of Source/), and TestCliEnvironment supplies its own implementation, so no test exercised the key loop.

Change

The key loop moves to SecretConsoleReader, which echoes * per accepted character:

  • Backspace removes one character and erases one mask character.
  • Escape / Ctrl+U clear the whole entry, then re-prompt on a fresh line (Password: *** (cleared)) rather than backspacing over the mask characters — a long entry wraps, and \b does not cross a line boundary on any common terminal, so an erase run would strand mask characters on the previous row. Clearing a bad long paste is exactly the case this key exists for.
  • Other control keys are ignored, as before.
  • Masking is suppressed when stderr is redirected, so no \b / * bytes reach a file. The prompt still goes there, as it did before.

Both spellings of the clear keys are matched: Windows and the Unix terminfo path populate different ConsoleKeyInfo fields. A plain u keypress is covered by a test so it cannot be swallowed as Ctrl+U.

Extracting the loop is what makes it testable — the reader takes a Func<ConsoleKeyInfo> and a TextWriter instead of talking to Console directly.

Trade-off

Masking with one character per keystroke reveals the password's length to anyone watching the screen. That is the standard behaviour of cmdkey, az and gh, and it is what makes a paste verifiable; no opt-out switch was added.

Tests

Tests/LinqToDB.CLI/SecretConsoleReaderTests.cs — 11 tests driving the reader with a scripted key queue over a StringWriter, asserting the exact echo (newline pinned to \n so expectations hold on every platform): per-character masking, a paste-shaped burst, backspace with and without content, both clear-key spellings, clear on an empty entry, control keys, a plain u, and the unmasked path.

Disabling the mask write turns 8 of the 11 red; the 3 survivors are precisely the ones asserting the absence of a mask.

Verification

  • Full CLI suite: 248/248 green (net10.0, Testing).
  • Release builds (analyzers on): CLI + test project green on net10.0 and net8.0, 0 warnings. The Release leg caught MA0192 on a bitwise modifier check, fixed to HasFlag.
  • Redirected-stdin guard unchanged: exit -3, Interactive secret input requires a console.
  • Manually confirmed in a real Windows console: both typing and pasting echo correctly.

Docs

Source/LinqToDB.CLI/readme.md and SKILL.md both asserted the prompt had no echo; corrected, along with the ICliEnvironment.TryReadSecret doc comment.

`credentials set` read the password with `Console.ReadKey(true)` and echoed
nothing, so a paste was invisible: there was no way to tell whether anything
had been entered before pressing Enter, or whether it had been entered twice.

The key loop moves to `SecretConsoleReader`, which echoes `*` per accepted
character. Backspace erases one; Escape and Ctrl+U clear the whole entry and
re-prompt on a fresh line rather than backspacing over the mask characters,
since a long entry wraps and `\b` does not cross a line boundary. Masking is
suppressed when stderr is redirected, so no mask bytes reach a file.

Extracting the loop also makes it testable: `SecretConsoleReaderTests` drives
it with a scripted key queue over a `StringWriter` and asserts the exact echo.
Disabling the mask write turns 8 of the 11 tests red.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MaceWindu MaceWindu added this to the 6.5.0 milestone Sep 8, 2026
@MaceWindu MaceWindu self-assigned this Sep 8, 2026
@MaceWindu
MaceWindu requested a lite review from Copilot September 8, 2026 11:38

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@MaceWindu
MaceWindu marked this pull request as ready for review September 8, 2026 11:43
@MaceWindu
MaceWindu merged commit 1c9c054 into master Sep 8, 2026
7 of 9 checks passed
@MaceWindu
MaceWindu deleted the feature/cli-mask-secret-input branch September 8, 2026 15:08
@MaceWindu

Copy link
Copy Markdown
Contributor Author

📝 Release-notes draft

🤖 Auto-generated user-facing summary for this PR. Toggle the boxes to control how it ships; the text is regenerated when new commits land (the maintainer confirms every change).

  • Omit from release notes (exclude from both the wiki notes and the GitHub release highlights)
  • Include in the GitHub release highlights (the brief release-page notes)

Full release notes (wiki)

credentials set now shows a * for each character typed at the password prompts, so a pasted or typed entry is visibly registered before you commit to it, and Esc or Ctrl+U clears the entry and re-prompts. Previously the prompts gave no feedback at all, so an empty entry could be stored without warning. Masking is skipped when the error stream is redirected, leaving redirected output unchanged.

GitHub release highlight (brief)

(none)


Generated from commit fb93282.

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.

3 participants