Skip to content

Latest commit

 

History

History
449 lines (377 loc) · 26.3 KB

File metadata and controls

449 lines (377 loc) · 26.3 KB

Changelog

1.1.1 - 2026-08-26

Correctness and honesty fixes from two external reviews of 1.1.0. Three of these were silent-failure bugs: the run completed, the status bar said Done, and the target got nothing or got the wrong thing.

CapsLock silently inverted the case of everything typed. With CapsLock on, rootpw arrived as ROOTPW, every keystroke accepted and the counter still reporting success - the worst shape of bug this tool can have, on the one kind of text it exists to carry. The layout lookup that picks each key's shift state answers for a keyboard with CapsLock off, and nothing corrected for it.

The fix does not assume CapsLock means "shift for letters", which is false on Turkish and on any layout using SGCAPS. It asks the layout what the key would actually produce right now and flips the shift flag only when flipping is what recovers the character. Verified across en-US, de-DE, fr-FR, ru-RU and tr-TR by querying every mappable character: 355 characters came out miscased before the fix and 8 after, with zero regressions and byte-identical decisions when CapsLock is off. The 8 are dead keys, which were already wrong for an unrelated reason. A console that forwards raw scancodes applies the guest's own CapsLock, which nothing on this side can see; README and DEPLOY now say so.

Writing that fix introduced a second bug worth recording, because the test caught it and inspection would not have: the ToUnicodeEx declaration inherited DllImport's ANSI default, so the runtime handed the API a one-byte-per-char buffer while it wrote UTF-16 into it. The overrun corrupted the heap and killed the test host several calls later with STATUS_HEAP_CORRUPTION. Same trap the VkKeyScanEx comment already warned about, one function away.

"N keystrokes were blocked" could never fire for the case it named. The message pointed at elevation, and elevation is precisely what it cannot detect: when UIPI discards injected input, SendInput returns the full event count and sets no error. The operator saw Done - 42 characters typed over an empty window and went looking at the key delay and the focus, never at privilege.

Elevation is now checked before typing by comparing integrity levels, and the run is refused with the target window's process named. The same pre-flight refuses when the workstation is locked or a UAC prompt holds the secure desktop, the other two ways keystrokes vanish without a trace. The blocked counter stays, because it does catch real SendInput refusals, but its message no longer promises to detect elevation.

The status bar now names the window being typed into. There was no feedback at all about where the keystrokes were going: a mistimed Alt+Tab or a chat window stealing focus sent a root password somewhere that may well log it, and the countdown was a blind safeguard. It also refuses outright when RSPaster itself is the focused window, which previously typed into its own controls and could press its own Cancel button with a space.

A partial send could leave Shift, Ctrl or AltGr held down. SendInput can insert some events and stop, and the chord's key-up never went out - so the system went on believing a modifier was pressed. In front of a root shell a latched Ctrl turns the next keystrokes into signals, at the moment the operator is already puzzling over missing characters. Modifiers are now released on any short send and on every exit from the typing loop, cancellation included.

OCR results no longer land in clipboard history or cloud clipboard. The feature is sold on "nothing leaves the machine", and that was true of the recognition and stopped being true one clipboard write later: on a default Windows install the text was captured into Win+V history and, with sync on, uploaded to the Microsoft account. Clipboard writes now carry the documented opt-out formats password managers use. The clipboard is not cleared afterwards, because pasting the text is the point of the feature; the docs now say plainly that the clipboard is a shared surface.

Restart as Admin is hidden when running from source. Elevating that path ran PowerShell with the execution policy bypassed against a script that compiles and executes every .cs file beside it, unsigned and unchecked - so anything able to write that folder gained administrator rights the moment someone clicked the link, behind a UAC prompt that said "Windows PowerShell". Build the exe first.

A pasted value could silently disable the safety countdown. The delay fields filtered typed characters to digits but not Ctrl+V. A pasted value too large for int failed to parse and the field fell back to its minimum - zero for the start delay, meaning no countdown at all on the one field whose job is to give the operator time to focus the right window. Paste is now filtered to digits.

Also fixed: a character with no scancode in the target layout is sent as a unicode event rather than as a zero scancode a KVM console has nothing to forward; non-BMP characters are sent as a single call so the surrogate pair composes; OCR recognition has a timeout and handles cancellation instead of hanging on "Reading..."; the WinRT stream and writer holding the captured PNG are disposed; the full-screen bitmap is released even if the overlay throws; a second OCR run can no longer start while the first is still recognizing and race it for the clipboard; the OCR result window disposes its fonts; the region selector's hint is centered on the monitor holding the pointer rather than across the bezel; settings values can no longer forge a following line; and an unhandled background exception now exits without letting Windows Error Reporting write a heap dump, which is the most likely path from "secret in memory" to "secret in a file".

Docs: DEPLOY gains a section for whoever has to approve this tool, since the capability list reads like an infostealer's and every entry on it is load-bearing for a visible feature. It is also now precise about memory - no secret is written to disk, which is not the same as "cannot end up on disk" - and about elevating from a user-writable folder. Make-Dist.ps1 writes and prints SHA256 hashes, so "verify the hash, then unblock" replaces "unblock".

Two review findings were checked and not acted on. A zero scancode from MapVirtualKeyEx was reported as a live risk; surveying every mappable character across four layouts found none, so it is guarded but treated as theoretical. And ObjectDisposedException was reported as escaping the cross-thread marshaling guards; it derives from InvalidOperationException, so the existing catch already covered it - the compiler rejects the second clause as unreachable.

1.1.0 - 2026-08-25

The other half of the emergency, and the workday features. OCR reads console output back out through the on-device engine Windows ships, with a result window for fixing the zeros it is known to misread. Phrases keep a few masked credentials at hand for repeated typing, in memory only. Hide text masks the main box for screenshares. Plus the fixes below from the first round of real use.

Clicking a phrase row's Type button made the cursor jump into the next phrase box, as if Tab had been pressed. Measured one link at a time: the click focuses the button, arming the run disables the panel, and WinForms responds to disabling the focused control by moving focus to the next control in tab order - the following phrase field. Parking focus elsewhere from the Click handler then lost a second race, against the button's own post-click processing. The fix removes the first link instead: phrase Type buttons no longer take focus at all, toolbar-style, so focus stays wherever the user had it. The main action button additionally parks focus deliberately when a run arms.

The phrase rows' Paste buttons are gone. Two buttons labeled Paste and Type left users unsure which one actually sends keystrokes, and pasting into a visible masked field is something everyone already knows how to do with Ctrl+V. The remaining button is worded Type after delay, identical to the main action it mirrors. The first-line-only paste guard did not leave with the button: it moved into the field itself, so a password manager's trailing newline still cannot become a stray Enter, and paste is still refused while a run is in flight.

The Grab button now reads OCR, and shows its result. OCR is the term the audience knows. The grabbed text still lands on the clipboard, and now also opens in a result window with an editable box: a misread character - a zero especially - is visible immediately, fixable on the spot, and re-copied with one click instead of pasting somewhere else to inspect it. The hotkey and the rest of the flow are unchanged.

Added Grab: read text back out of a console. Typing in was half the emergency; the consoles that refuse paste also refuse copy, so getting a NIC name or an error line back out meant retyping it from a phone photo. Ctrl+Alt+G (or the Grab button) freezes the screen, the user drags a rectangle, and the recognized text lands on the clipboard with line structure preserved.

Recognition is the on-device engine Windows ships (Windows.Media.Ocr - the same platform Snipping Tool's copy-text uses), so nothing is installed and nothing leaves the machine. Reaching it inside the zero-dependency rule took two findings worth recording:

  • The WinRT metadata compiles from %WINDIR%\System32\WinMetadata plus two GAC facades, all OS-shipped. The SDK interop assembly that would make the code pleasant is built against the SDK's unified Windows.winmd, which a stock machine does not have - so it is deliberately not referenced. Async operations are polled by hand; the stream is built from pure WinRT types.
  • Add-Type -ReferencedAssemblies pre-loads what it names and cannot load a .winmd (0x80131047). Run-From-Source passes the references through -CompilerParameters instead, which hands them straight to the same in-box csc the build script uses.

Upscaling before recognition uses nearest-neighbor, and that is measured, not assumed: the same 12pt console text read with 1 character error unscaled or 2x-nearest, and 24 errors after 2x bicubic - smoothing blurs exactly the thin strokes that distinguish console glyphs. Also measured, and documented rather than fixed because no preprocessing helped: stylized zeros (Consolas' slashed, Cascadia's dotted) misread as e/8/O/@ at small sizes. The README tells users to eyeball grabbed digits; the screen freeze, at least, means the text cannot scroll away mid-aim.

The screen is frozen before aiming and the window hides itself first, so the always-on-top RSPaster cannot sit inside its own capture covering the console. On images without an OCR language pack (some LTSC/Server) the button disables itself with the reason in its tooltip; typing is untouched.

Added the Phrases panel. A button in the top bar opens a row of masked one-line slots - label, hidden field, Paste, Type - with Add and Remove for one to six rows. It serves credentials that rotate daily: fetch once, then type them all day from the tray without going back to the vault each time.

The design constraints, because they are easy to unpick by accident:

  • Contents are held in memory only and are never written to disk. Labels and the row count are settings; the secrets are not. The tray-resident process is what makes memory-only sufficient for a workday.
  • Paste takes only the first line of the clipboard. Password managers append a trailing newline, and typed into a console that becomes an Enter fired in the middle of a login prompt.
  • A phrase run reports no character counts anywhere - not in progress, not on completion - because the status bar is visible on the same shared screen the masking exists for, and a length is a real hint.
  • Clear after typing does not apply to phrase runs; repeated typing is their entire point.
  • These fields mask natively via UseSystemPasswordChar, which works on a single-line TextBox, so hand-editing is allowed here - none of the shadow copy machinery the multiline Hide text needed.

1.0.0 - 2026-08-20

First public release. Types multi-line text into whatever window has focus, through SendInput, for the consoles that will not take a clipboard paste.

Everything below is the development history. It is kept in full rather than collapsed into a release note, because each entry records why a piece of the code is shaped the way it is, and several of them are Windows behavior that will bite the next person who touches this file.

Added Hide text. The tool this one grew out of pasted single-line passwords, and a password on screen during a screenshare is exactly the case this could not previously serve. One checkbox now shows the contents as dots.

PasswordChar and UseSystemPasswordChar are both ignored on a multiline TextBox, so masking could not be handed to the control: the real text is held beside it and the box shows a stand-in, with line breaks preserved so the shape of a script survives while its content does not. The keystroke engine reads the real text, and Clear after typing wipes that copy rather than just the mask.

Pasting works while hidden and replaces the whole box, which is what makes the feature usable: tick Hide text, paste from a password manager, send it, and the secret is never rendered. Typing by hand is blocked while hidden, because editing a mask means mapping every caret move and selection back onto the hidden string, and getting that subtly wrong on a password is worse than not offering it.

A 1px line along the top and left of every owner-drawn control. Each paint method set SmoothingMode.AntiAlias before filling its background, and GDI+ anti-aliases the edges of a filled rectangle: the first row and column came out at roughly half coverage, letting whatever was underneath show through. The background fill is now done with smoothing off, and anti-aliasing is switched on afterwards for the rounded shapes that actually need it.

Measured rather than guessed: filling the checkbox background with pure lime produced #008000 on the top row and #004000 in the corner - exactly 50% and 25% coverage - which is what named anti-aliasing as the cause rather than a stale repaint.

It also explains why the artifact behaved so strangely. It only appeared on the first paint of a control, because blending the correct color 50% over itself gives the correct color, so any later repaint erased it permanently: hovering made it vanish and nothing would bring it back. And it was far more obvious on the Paper and Warm palettes, where the half-covered row contrasts hardest against a light panel.

Affected InputHost, ThemedButton, ThemedCheck, SpinBox and ThemedScrollBar.

Tooltips left stray dark edges behind and flickered at random. The ToolTip was a local variable. It is a Component, not a child control, so nothing else held a reference: it became garbage as soon as the method returned, and whenever the finalizer happened to run it destroyed the native tooltip window out from under a tip that might be on screen. That is where the randomness came from. It is now a field, disposed with the form.

The tooltip was also drawn by the system as a pale box, which on a dark palette is both jarring and the source of the leftover edges: it is a separate top level window that covers whatever is beneath it, including neighboring controls, so any imperfect repaint on dismissal shows as a thin light line under a control that has no tooltip of its own. It is now owner-drawn in theme colors.

Added tools/Dpi-Report.cmd. A 100% display cannot be used to check a 150% layout, because text metrics come from the real DPI. This compiles a probe against the app's own sources, runs it on the scaled display, and prints the measured geometry with a pass or fail on each label-to-field gap. It reports whether SetProcessDPIAware actually succeeded, so a run that only describes an unscaled layout says so instead of looking like a pass.

Note: Run-From-Source.cmd cannot be DPI-aware. SetProcessDPIAware has to be called before the process creates its first window, and the PowerShell host has already done that. The script path therefore lays out at 1x and lets Windows stretch the result: correct proportions, but soft at 125% and above. The exe is the one to use on a scaled display, and the README now says so.

Field labels overlapped their spin boxes at 150% scaling. An AutoSize label reports the stock 100x23 default until it is parented, and the layout runs before that, so "Start delay (s)" was placed against a width of 100 when it actually needed 79. The 21px of slack that leaves is invisible at 100% and turns into an overlap at 150%, because 100 is a fixed pixel value that does not scale while the text does. Labels are now measured and sized explicitly, the same way ThemedCheck already sized itself. The same stale default was also making the labels sit a few pixels high, since Height read 23 instead of 15.

Worth recording how this got through: it was checked at 1x through 2x before release and passed, because that check scaled Dpi.Factor without scaling the font. A 100% display cannot simulate 150% faithfully - text metrics come from the real DPI, so scaling the layout alone grows the gaps but not the words, and scaling a probe font instead grows the words but not the layout. The check now asserts something scale-independent: each control sits at the previous one's Right plus a fixed gap, so that distance must come out exactly right at every factor. With the defect put back it reports 29px where 8 was expected, and the error stays a constant 21px at every scale, which is the signature of a constant that is not scaling.

The "seconds" label was never given theme colors, so it inherited full brightness text while the two field labels beside it render dimmed.

Non-ASCII characters were typed as a literal ?. VkKeyScanEx was declared without CharSet, and DllImport defaults to ANSI, so the CLR squeezed each char through the system code page on the way to VkKeyScanExA. Anything the page could not represent arrived as ?, which resolved to a real key, so the engine typed a question mark instead of falling through to the unicode path. Measured on a Windows-1252 machine, the old declaration mapped Greek, CJK, arrows and checkmarks to the ? key; the new one reports them unmapped and the unicode fallback takes over.

AltGr characters were silently dropped by the consoles this tool exists for. Anything needing more than plain Shift fell back to a unicode event, and on European layouts that is @ { } [ ] \ | ~ and the euro sign - the characters a shell command is made of. Hypervisor and IPMI KVM consoles emulate a hardware keyboard and ignore KEYEVENTF_UNICODE, so those characters reached the guest as nothing at all. Ctrl+Alt pairs are now sent as a real AltGr chord: left Control plus the extended right Alt, exactly the scancode sequence a physical AltGr press emits. Verified end to end against a German layout.

The hotkey could fire while its own modifiers were still held. With a start delay of 0, Ctrl+Alt+V began injecting before the user's fingers were off the keys, so the first characters arrived at the target as Ctrl+Alt chords: menu shortcuts, interrupted commands. Typing now waits for Shift, Ctrl, Alt, Win and V to be physically up, with a three second cap so a stuck key cannot hang a run.

Ctrl+V could edit the text while it was being typed. The paste handler assigns SelectedText, which ignores ReadOnly, so the box that is deliberately locked during a run accepted pastes anyway.

Choosing a theme reverted every unsaved setting. The theme menu called _settings.Save(), writing the object as it was at startup, so any delay or checkbox changed since launch was rolled back. It now goes through SaveSettings(), which reads the controls first.

Hiding to the tray recreated the window handle. ShowInTaskbar was toggled on hide and restore, and changing it forces WinForms to destroy and rebuild the HWND, which drops and re-registers the global hotkey and reapplies the dark title bar for nothing. Hide() already clears the taskbar button. Confirmed the handle now survives a hide.

Blurry on scaled displays. The process declared no DPI awareness, so Windows bitmap-stretched the whole window at 125% and above. It now calls SetProcessDPIAware before the first window and scales every hand-placed dimension through one factor - awareness without the scaling would have traded blurry for tiny.

Controls in the bottom panel were positioned at fixed pixel offsets and would collide if a label were reworded or the font metrics differed. Each is now placed relative to the one before it. Checked for collisions and overflow at 1x through 2x.

The disabled seconds field showed a system-gray hole on dark themes. A disabled WinForms TextBox ignores BackColor. The child now stays enabled but read-only, with the border, arrows and text painted dimmed, matching the half-opacity treatment the suite gives disabled buttons.

Status text was truncated when running elevated, because the layout reserved room for the "Restart as Admin" link even when it was hidden. Note for anyone making the same change: the fix cannot test _lnkAdmin.Visible, because Control.Visible reports effective visibility and answers false for every control while the form is still being built - which lands the status label on top of the link instead. The intended state is kept in a field.

Renamed RSPaster.cmd to Run-From-Source.cmd, and the .ps1 with it. RSPaster.cmd sitting beside RSPaster.exe read as two variants of the same thing, when one is the app and the other compiles the app from source on every launch; Build-RSPaster.cmd then looked like a sibling of RSPaster.cmd while doing something else entirely. The three files now say what they do: RSPaster.exe runs it, Run-From-Source.cmd runs it without the exe, Build-RSPaster.cmd builds the exe.

The README opens with a "Which file do I run?" table and the measured cost of each path, since the difference is not obvious from the outside: the exe shows its window in 0.7 s against 2.8 s and holds 38 MB against 95 MB, because the source path pays for a compile and a PowerShell host on every launch. It also appears in Task Manager as powershell rather than RSPaster. The source path is kept because it ships no binary, which is what a recipient wary of an unsigned exe, or a machine that blocks one, actually needs.

Added tools/Make-Dist.ps1, which packages dist/RSPaster.zip. Handing the tool to someone meant copying the whole working folder, .git and all. The zip carries the exe, the five sources, both launchers, the build script, README and LICENSE: enough to run it, and enough to audit or rebuild it. Around 70 KB.

It rebuilds the exe before packaging rather than zipping whatever binary is lying around, because a distribution whose exe does not match the sources beside it is worse than one with no exe. A missing file aborts the run and names itself, and the abort happens before the previous zip is touched, so a failed run cannot leave a half-built artifact where a good one was.

Added a delay between lines. Multi-line command lists sent to a slow or busy machine lost every line after the first: the next line was typed into a shell still working on the previous command, so it went nowhere. A checkbox and a seconds field now hold after each Enter before the next line is typed. The wait is only taken between lines, never after the last one, where it would just postpone the finish.

The wait is slept in 100 ms slices rather than one long Thread.Sleep, because a 15 second sleep leaves Esc, Cancel and the hotkey looking dead for the whole of it. Cancelling a 30 second wait returns immediately. The status bar and the tray tooltip count the remaining seconds down and name the line about to be typed, since the window is often hidden while this runs.

First working version. A GUI that types multi-line text into the focused window through SendInput, for consoles that ignore the clipboard: hypervisor VM consoles, VNC, IPMI/BMC KVM, and UAC prompts on the normal desktop. Runs either from source through PowerShell or as an exe built by the C# compiler already present in Windows, so there is nothing to install on a jump box.

LF-only text pasted as a single unreadable line. A Windows EDIT control breaks lines on CRLF only, and text copied from a shell, an SSH session or a web page is usually LF-only. The whole script arrived as one line, which defeats the point of reviewing a command before sending it. WM_PASTE is now intercepted and the clipboard text normalized to CRLF before insertion; the keystroke engine normalizes back to \n, so only the display changed, never what is sent.

Scrollbars and title bar stayed system-colored on every dark theme. Windows will not recolor an EDIT control's non-client scrollbars: SetWindowTheme with DarkMode_Explorer is ignored for them even after the process opts into dark mode via the undocumented uxtheme ordinals, so a bright white strip sat down the side of the text box on 20-odd palettes. The stock bars are now switched off and a themed one is painted in their place, which also lets it match palettes an OS scrollbar could never reach, like Phosphor and Amber. Word wrap is on, which removes the need for a horizontal bar at all. The title bar is handled separately through DwmSetWindowAttribute, which does work.

Checkbox labels rendered clipped. They were measured before being added to their parent, so the measurement used Control.DefaultFont while the paint used the wider form font. The font is now set before measuring.

Spin boxes and the text box opened system-colored. Both apply their palette on a theme-changed event, which does not fire for the theme already in force at startup. They now apply it once at construction too.

Notes for later

  • NumericUpDown and CheckBox are replaced by owner-drawn equivalents. WinForms renders their spin buttons and check glyph in system colors regardless of what is set on the control, which reads wrong on most palettes.
  • The tray icon is drawn at runtime from --se-logo-a / --se-logo-b rather than shipped as an .ico, so it follows the theme and the repo carries no binary asset. Bitmap.GetHicon handles are destroyed explicitly on each switch, or the process bleeds a GDI handle per theme change.
  • Palettes are ported from launchcanvas/public/themes.js in the same overrides-on-defaults shape, so the two files can be diffed when the suite palette moves.