fix(cli): mask private key and mnemonic input on wallet import (WLT-2075) - #112
fix(cli): mask private key and mnemonic input on wallet import (WLT-2075)#112zerts wants to merge 1 commit into
Conversation
…075) `wallet import` masked the passphrase but echoed the secret it actually imports, so an EVM/Solana private key or mnemonic was printed in cleartext for anyone looking at the screen (or a recording, or the scrollback). Masking the three key prompts alone was not enough: readSecret's raw-mode branch treated every stdin `data` event as one keystroke, which holds for a typed passphrase but not for pasted key material — a paste arrives as one multi-character chunk, so it echoed a single `*` and folded the paste's trailing newline into the secret instead of submitting it. The masked branch now walks each chunk character by character via applyMaskedChunk: one `*` per character, backspace/Ctrl-C/Ctrl-D handled mid-chunk, bracketed-paste and arrow-key escape sequences swallowed, and anything after the terminator dropped so it cannot leak into the next prompt. Confirmation prompts (`Type DELETE`, `Type YES`) stay unmasked — `mask` is still opt-in per call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Loop review — iteration 1/5Verdict:
SummaryReviewed PR #112 at head Findings[minor/code] SS3 arrows and chunk-split CSI sequences leak into the secret buffer
Why this doesn't block: the CLI never enables application-cursor-keys mode, so terminals send the CSI form ( Verification log
Posted by ai-runner. |
Summary
zerion wallet importmasked the passphrase but echoed the secret it actually imports — the EVM/Solana private key or mnemonic was printed in cleartext on screen (shoulder-surfing, shared screens, recordings, scrollback).Masking the three prompts alone wasn't enough.
readSecret's raw-mode branch treated each stdindataevent as one keystroke — true for a hand-typed passphrase, false for key material, which is pasted. A paste arrives as a single multi-character chunk, so the old reader echoed one*for the whole key and folded the paste's trailing newline into the secret instead of submitting it (the import would then fail with an invalid key).applyMaskedChunk(current, chunk)— new exported state machine that walks a raw-mode chunk character by character: one*per character, backspace / Ctrl-C (exit 130) / Ctrl-D handled mid-chunk, bracketed-paste (ESC[200~/ESC[201~) and arrow-key CSI sequences swallowed rather than masked as input, other control characters ignored, and anything after the terminator dropped so it can't leak into the next prompt.wallet importnow passes{ mask: true }for--evm-key,--sol-keyand--mnemonic. A mnemonic is included deliberately — it's strictly more sensitive than a single key.Type DELETE to confirm,Type YES to confirm) stay unmasked;maskremains opt-in per call site.Decisions and rejected alternatives are captured in the PRD comment on the Linear issue.
Closes WLT-2075
Test plan
npm test— 404 pass / 0 fail, including 13 newapplyMaskedChunkcases (typed char, pasted key, paste with trailing\n, CRLF, post-terminator drop, Ctrl-D, Ctrl-C, backspace, backspace on empty buffer, bracketed paste, arrow keys, other control chars, spaces in a mnemonic).script -q /dev/null): a pasted 66-char key echoes exactly 66 asterisks and resolves to the full key; a pasted 12-word mnemonic resolves intact; typed input with backspace erases correctly (0xab+ BS +c→0xac).zerion wallet import --name tmp --evm-keyin a real terminal — the key line should show only asterisks.🤖 Generated with Claude Code