Skip to content

Instantly share code, notes, and snippets.

@retsl
Last active January 4, 2025 14:43
Show Gist options
  • Save retsl/57f6c920474da953cfad4e21d16bb7a9 to your computer and use it in GitHub Desktop.
Save retsl/57f6c920474da953cfad4e21d16bb7a9 to your computer and use it in GitHub Desktop.
A proof of concept to authorise copy/paste events between a Mac host and VM
/*
OVERVIEW
--------
* Assumes that it is safe to allow untrusted hosts to connect via (properly configured) SSH
* The options for authorized_keys are taken from forgejo's authorized_keys[^authorized_keys],
so they should provide enough isolation
* Uses swift to be able to display the VM name and action (copy/paste) in the Touch ID prompt
* This does not seem to be possible with sudo, which afaict is the only way to launch a Touch ID
prompt from the shell
* With the text used for Touch ID prompts hardcoded in the authorized_keys commands, VMs shouldn't
be able to modify it
WARNING: Not made by a UNIX wizard. This is my first Swift program. It is not audited by anyone.
There might still be (super easy) ways to execute arbitrary commands/code on the host.
[^authorized_keys]: https://codeberg.org/forgejo/forgejo/src/commit/b54424316410803da5d67fe7315ee931b1c84035/models/asymkey/ssh_key_authorized_keys.go#L42
USAGE
-----
1. Compile this file: swiftc ClipGuard.swift -o ClipGuard
2. In the VM, create the ssh keys:
mkdir -p ~/.ssh && \
chmod 700 ~/.ssh && \
cd ~/.ssh && \
ssh-keygen -t ed25519 -N "" -f copy && \
ssh-keygen -t ed25519 -N "" -f paste && \
printf "\n\n\n=== copy.pub ===\n" && \
cat copy.pub && \
printf "=== paste.pub ===\n" && \
cat paste.pub && \
cd -
3. On the host, add
command="/usr/bin/env -i HOME=/nonexistent PATH=/usr/bin:/bin LANG=en_US.UTF-8 /PATH/TO/ClipGuard VM_NAME",from="VM_IP_ADDRESS",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict VM_SSH_KEY
to `~/.ssh/authorized_keys`, replace VM_SSH_KEY with the contents of copy.pub, and replace
VN_NAME and VM_IP_ADDRESS.
To also allow pasting from VM to host, add a second entry with `--paste` after VM_NAME in the command
and use paste.pub this time.
If the VM is cloned later on, the from= field likely keeps this script from working and hopefully reminds
you to generate new keys for the cloned VM.
3.1 On the host, ensure you have a strong password for your user account, then go to Settings, General, Sharing and enable Remote Login (you can keep full disk access disabled).
4. In the VM, you should now be able to run `ssh -T -i ~/.ssh/copy $USER@$IP` to copy the contents of the
host's clipboard to stdout and `echo "test" | ssh -T -i ~/.ssh/paste $USER@$IP` to copy the string "test" to
the host clipboard. To directly copy/paste to the VM clipboard, configure keyboard shortcuts in your
desktop environment or aliases in your terminal for the commands:
* On Wayland: `ssh -T -i ~/.ssh/copy $USER@$IP | wl-copy` and `wl-paste | ssh -T -i ~/.ssh/paste $USER@$IP`
* On X (untested): `ssh -T -i ~/.ssh/copy $USER@$IP | xsel — clipboard — input` and `xsel — clipboard — output | ssh -T -i ~/.ssh/paste $USER@$IP`
Example `authorized_keys` entries:
COPY (host → VM):
command="/usr/bin/env -i HOME=/nonexistent PATH=/usr/bin:/bin /path/to/ClipGuard 'Debian 12'",from="10.1.2.3",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 ...
PASTE (VM → host):
command="/usr/bin/env -i HOME=/nonexistent PATH=/usr/bin:/bin /path/to/ClipGuard 'Debian 12' --paste",from="10.1.2.3",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-ed25519 ...
*/
import Foundation
import Cocoa
import LocalAuthentication
import OSLog
let logger = Logger(subsystem: "re.lets.ClipGuard", category: "ClipboardAccess")
// Make sure we have at least the VM name
guard CommandLine.arguments.count >= 2 else {
logger.error("No VM name provided. Usage: ClipGuard <VM_NAME> [--paste]")
fputs("Usage: ClipGuard <VM_NAME> [--paste]\n", stderr)
exit(1)
}
let vmName = CommandLine.arguments[1]
// Check if the third argument is `--paste`
let isPasteOperation = (CommandLine.arguments.count >= 3 && CommandLine.arguments[2] == "--paste")
// Log the incoming request
if isPasteOperation {
logger.info("Clipboard PASTE request from VM: \(vmName, privacy: .public)")
} else {
logger.info("Clipboard COPY request from VM: \(vmName, privacy: .public)")
}
// Prepare LocalAuthentication context
let context = LAContext()
context.localizedReason = isPasteOperation
? "\n\nPASTE\n\n\(vmName)\n\ncontents into the host clipboard"
: "\n\nCOPY\n\nthe host clipboard to\n\n\(vmName)"
let policy = LAPolicy.deviceOwnerAuthentication
var authError: NSError?
// Check if we can evaluate policy
guard context.canEvaluatePolicy(policy, error: &authError) else {
let errorDesc = authError?.localizedDescription ?? "Unknown error"
logger.error("Cannot evaluate LocalAuthentication policy: \(errorDesc, privacy: .public)")
fputs("Unable to evaluate authentication policy: \(errorDesc)\n", stderr)
exit(1)
}
var authenticationDone = false
let runLoop = RunLoop.current
let runLoopMode = RunLoop.Mode.default
// Evaluate policy
context.evaluatePolicy(policy, localizedReason: context.localizedReason) { success, error in
defer { authenticationDone = true }
if success {
// Authentication succeeded
logger.info("Authentication succeeded for VM: \(vmName, privacy: .public)")
let pasteboard = NSPasteboard.general
if isPasteOperation {
// 1) Read from stdin
let inputData = FileHandle.standardInput.readDataToEndOfFile()
guard let clipboardString = String(data: inputData, encoding: .utf8),
!clipboardString.isEmpty
else {
logger.error("No valid data from stdin for VM: \(vmName, privacy: .public)")
fputs("No valid data provided on stdin.\n", stderr)
exit(1)
}
// 2) Write to host clipboard
pasteboard.declareTypes([.string], owner: nil)
pasteboard.setString(clipboardString, forType: .string)
logger.info("Successfully set the host clipboard with VM data for: \(vmName, privacy: .public)")
// (No output back to the VM needed, unless you want to confirm success)
exit(0)
} else {
// Copy from host → VM
// Attempt to retrieve string data from the host clipboard
if let clipboardString = pasteboard.string(forType: .string) {
logger.info("Successfully retrieved clipboard text for VM: \(vmName, privacy: .public)")
print(clipboardString)
exit(0)
} else {
logger.error("Clipboard has no valid string content for VM: \(vmName, privacy: .public)")
fputs("Clipboard does not contain a valid string.\n", stderr)
exit(1)
}
}
} else {
// Authentication failed or was canceled
let reason = error?.localizedDescription ?? "User canceled or Touch ID not available."
logger.error("Authentication failed for VM: \(vmName, privacy: .public) – \(reason, privacy: .public)")
fputs("Authentication failed: \(reason)\n", stderr)
exit(1)
}
}
// Keep the run loop running until authentication is done
while !authenticationDone && runLoop.run(mode: runLoopMode, before: Date(timeIntervalSinceNow: 0.2)) { }
// If somehow we exit the loop without completing authentication, exit non-zero
logger.error("Unexpected exit without completing authentication for VM: \(vmName, privacy: .public)")
exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment