Paste the unpastable: how to paste in RDP sessions that don't allow pasting

(, en)

NOTE: This solution works, but has its quirks. Enjoy with care!

Some companies, notably banks, put security on top priority. There is nothing wrong about it. I even support it. However, if taken too far, security becomes an unnecessary burden.

Setting the stage

You are connected via RDP to a VM, aka secure environment. Copy and paste is disabled. The secure environment has barely anything installed. Best editor is notepad. Due to a “migration to a new platform”™, you need to run some random commands for the services installed on that VM.

But I don’t want to transcribe a complete migration script from one editor into a different one by hand.

Automating it

Luckily there is hammerspoon. I use hammerspoon to automate tasks or key combinations on my MacBook. It turns out that it can also emulate key presses, leading to the following approach:

  1. Get the content of the clipboard
  2. Create a function to map the text to key presses
  3. Bind the function to a key combination

How does that look like?

The keymap:

-- https://github.com/Hammerspoon/hammerspoon/blob/master/extensions/keycodes/keycodes.lua#L67
keyMap = {
  ["0"] = { {}, "0" },
  [" "] = { {}, hs.keycodes.map.space },
  ["1"] = { {}, "1" },
  ["2"] = { {}, "2" },
  ["3"] = { {}, "3" },
  ["4"] = { {}, "4" },
  ["5"] = { {}, "5" },
  ["6"] = { {}, "6" },
  ["7"] = { {}, "7" },
  ["8"] = { {}, "8" },
  ["9"] = { {}, "9" },
  ["a"] = { {}, "a" },
  ["b"] = { {}, "b" },
  ["\\"] = { {}, "\\" },
  ["c"] = { {}, "c" },
  [","] = { {}, "," },
  ["d"] = { {}, "d" },
  ["e"] = { {}, "e" },
  ["="] = { {}, "=" },
  ["f"] = { {}, "f" },
  ["g"] = { {}, "g" },
  ["`"] = { {}, "`" },
  ["h"] = { {}, "h" },
  ["i"] = { {}, "i" },
  ["j"] = { {}, "j" },
  ["k"] = { {}, "k" },
  ["l"] = { {}, "l" },
  ["["] = { {}, "[" },
  ["m"] = { {}, "m" },
  ["-"] = { {}, "-" },
  ["n"] = { {}, "n" },
  ["o"] = { {}, "o" },
  ["p"] = { {}, "p" },
  ["."] = { {}, "." },
  ["q"] = { {}, "q" },
  ["'"] = { {}, "'" },
  ["r"] = { {}, "r" },
  ["]"] = { {}, "]" },
  ["s"] = { {}, "s" },
  [";"] = { {}, ";" },
  ["/"] = { {}, "/" },
  ["t"] = { {}, "t" },
  ["u"] = { {}, "u" },
  ["v"] = { {}, "v" },
  ["w"] = { {}, "w" },
  ["x"] = { {}, "x" },
  ["y"] = { {}, "y" },
  ["z"] = { {}, "z" },

  [")"] = { {"shift"}, "0" },
  ["!"] = { {"shift"}, "1" },
  ["@"] = { {"shift"}, "2" },
  ["#"] = { {"shift"}, "3" },
  ["$"] = { {"shift"}, "4" },
  ["%"] = { {"shift"}, "5" },
  ["^"] = { {"shift"}, "6" },
  ["&"] = { {"shift"}, "7" },
  ["*"] = { {"shift"}, "8" },
  ["("] = { {"shift"}, "9" },
  ["A"] = { {"shift"}, "a" },
  ["B"] = { {"shift"}, "b" },
  ["|"] = { {"shift"}, "\\" },
  ["C"] = { {"shift"}, "c" },
  ["<"] = { {"shift"}, "," },
  ["D"] = { {"shift"}, "d" },
  ["E"] = { {"shift"}, "e" },
  ["+"] = { {"shift"}, "=" },
  ["F"] = { {"shift"}, "f" },
  ["G"] = { {"shift"}, "g" },
  ["~"] = { {"shift"}, "`" },
  ["H"] = { {"shift"}, "h" },
  ["I"] = { {"shift"}, "i" },
  ["J"] = { {"shift"}, "j" },
  ["K"] = { {"shift"}, "k" },
  ["L"] = { {"shift"}, "l" },
  ["{"] = { {"shift"}, "[" },
  ["M"] = { {"shift"}, "m" },
  ["_"] = { {"shift"}, "-" },
  ["N"] = { {"shift"}, "n" },
  ["O"] = { {"shift"}, "o" },
  ["P"] = { {"shift"}, "p" },
  [">"] = { {"shift"}, "." },
  ["Q"] = { {"shift"}, "q" },
  ["\""] = { {"shift"}, "'" },
  ["R"] = { {"shift"}, "r" },
  ["}"] = { {"shift"}, "]" },
  ["S"] = { {"shift"}, "s" },
  [":"] = { {"shift"}, ";" },
  ["?"] = { {"shift"}, "/" },
  ["T"] = { {"shift"}, "t" },
  ["U"] = { {"shift"}, "u" },
  ["V"] = { {"shift"}, "v" },
  ["W"] = { {"shift"}, "w" },
  ["X"] = { {"shift"}, "x" },
  ["Y"] = { {"shift"}, "y" },
  ["Z"] = { {"shift"}, "z" },
  ["\n"] = {{}, hs.keycodes.map["return"] },
  ["\t"] = {{}, hs.keycodes.map["tab"] }
}

Emitting keys:

local function resetModifiers()
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.alt, false):post()
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.ctrl, false):post()
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.cmd, false):post()
end

local function emitKey(mod, c)
  -- I had many problems with modifiers, sometimes CTRL was activated,
  -- even though not pressed or emitted.
  resetModifiers()
  if #mod > 0 then
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.shift, true):post()
  else
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.shift, false):post()
  end
  hs.timer.usleep(20000)

  -- print(hs.inspect(hs.eventtap.checkKeyboardModifiers()))

  hs.eventtap.event.newKeyEvent(c, true):post()
  hs.timer.usleep(40000)
  hs.eventtap.event.newKeyEvent(c, false):post()

  if #mod > 0 then
    hs.timer.usleep(20000)
    hs.eventtap.event.newKeyEvent(hs.keycodes.map.shift, false):post()
  end
end

Here I was lazy: I don’t check any modifier keys, the presence of at least one modifer key activates shift. Room for improvement, I would say.

The sleep times were determined by experimentation: I was using RDP over a VPN connection, so lag was part of the game. With shorter intervals the key strokes were too fast and the content was not transmitted correctly.

Binding the function to a key combination:

hs.hotkey.bind({}, "f8", nil, function()
  hs.timer.doAfter(.6, function()
    -- print(hs.inspect(hs.eventtap.checkKeyboardModifiers()))
    local res = hs.pasteboard.getContents()
    for i = 1, #res do
      local c = res:sub(i,i)
      local u = keyMap[c]
      -- print(string.format("%s >%s<",c,u[2]))
      if u ~= nil then
        emitKey(u[1], u[2])
        hs.timer.usleep(40000)
      end
    end
  end)
end)

I chose F8, because I had problems with combinations based on modifier keys: they seemed to stay activated under certain (to me unknown) conditions (see also a comment on the issue), causing all kinds of window switching, closing and random behaviour.

This solution is not very fancy and for sure can be improved. But it works, serves its purpose and perhaps you can use it also in some way.