I made a fix for the shared clipboard issue

I wrote a python script that allows for automatic clipboard sharing using a shared file.The same script is to be run on both host and guest. The script will connect to the shared file (clipboard file) which is set to a config file in ~/.config/cbdaemon/config.txt and also use/create a hidden “trigger file” in the same directory as the clipboard file (used to signal changes in the clipboard file)To rewrite the config file, use -set {clipboard file path} along with the script.If @Patrick could take a look at it that’d be great, since it’s better for anonymity to have the script in all releases.

https://whopaste.com/?da773daee556b181#8VQq6eWi4hF1HKxCWciTXf62HATVfjbKn2Mb8YaWLQcx

password: XJv\a%z#t#+7gEhaSd=

Dependencies:
xclip (if on X11)
wl-clipboard (If on Wayland)
pyperclip

1 Like

Thanks for the idea and the work done
Let’s wait until @Patrick checks the script and tells us all what he thinks about it .

It’s clear how to use the script if there are 1 Gateway & 1 Workstation
How to use it if I have at least 6 Gateways & 6 Workstations at the same time ?

1 Like

it should still work (although I haven’t tested it out), since you are only copy/pasting once at a time. copying edits the clipboardfile & triggerfile which then gets pushed to every host listening to changes in the triggerfile, so if you copy on one host (i’m using the word host here for both host and guests in vm context), every host that is running the script and connected to the same clipboardfile, the clipboard on each host should just change to what you just copied.

Thank you for sharing this! Patrick asked me to take a look at the script, these are the notes I have on it. The SHA256 hash of the file I downloaded and read through is fb9530b6158a113ff997f75fa1baaac27cda78f7021cad0b424c303110db150c, in case that’s useful to anyone else who downloads it.

For the code itself:

  • unicode-show (our tool for detecting potentially malicious Unicode) shows that the script has a lot of trailing spaces in various places. These can cause some programming languages to misbehave. You might find it useful to automatically strip these using your text editor of choice; many editors have an option for this, Vim can be programmed to do it.
  • At the moment, under X11, pyperclip is being used to handle copy and paste, while under Wayland, you’re using wl-clipboard directly. However, the version of pyperclip in Debian 13 supports wl-clipboard already, so you might consider using pyperclip for both since you have it available.
  • The set_config() function will write a config file with no terminating newline. Under Linux, text files have to have a newline at the end of every line, including the last line, otherwise they technically aren’t truly text files. Many tools will work with these “almost text files” without problems, but some will break quite badly, and this config file might be parsed by tools other than this script. Rather than using file.write(arg), you might use print(arg, file=file). That will give you a terminating newline.
  • Some notes on load_clipboardfile():
    • If the program has to abort due to an error, it should exit with a non-zero exit code so that the caller knows something went wrong. The sys.exit() calls in load_clipboardfile() should probably be sys.exit(1). (You can also use different codes for different errors if you want.)
    • printf(f"Clipboard file {CLIPBOARDFILE} loaded.") This is slightly confusing; the config file has been loaded, and you’ve retrieved the path of the clipboard file from it. Perhaps Clipboard file path '{CLIPBOARDFILE}' loaded from config file '{CONFIGFILE}'. would be better? (The function might need to be renamed too.)
    • When you’re exposing the value of a variable to a user, like CLIPBOARDFILE, it’s a good idea to enclose it in quotes (generally single quotes so you don’t have to escape double quotes) so that users can tell where the variable starts and ends. Otherwise variables with spaces in them can cause confusing output.
    • Printing the exception value is good, but printing the full exception backtrace can be even more useful for debugging. You can use traceback.print_exc() to do that.
    • Generally error messages should be printed to sys.stderr, and non-error messages to sys.stdout.
    • It doesn’t look like you ensure that the directory the clipboard file is in exists. If someone sets their clipboard file to /this/doesnt/exist, things will crash due to /this/doesnt not existing. Maybe try creating the directory with Path.mkdir(), using the parents=True, exist_ok=True arguments? If the creation fails, the script should error out.
      • You also want to make sure that the script has permission to write to this directory. os.access() is good for testing that.
  • In load_trigger_status(), all of the sys.exit() calls here should probably also be sys.exit(1) calls.
  • poll_local() is where the script will crash if the directory CLIPBOARDFILE is in either doesn’t exist or can’t be written to. Even after adding the check for the clipboard file to load_clipboardfile(), you’ll still probably want to use a try/except block here and terminate the script cleanly if the file open or write fails.
  • set_trigger() should also be prepared for the possibility that it won’t be able to write to TRIGGERFILE.
  • poll_external() should be prepared for the possibility that it won’t be able to read CLIPBOARDFILE (the user may have deleted it).
  • All of the file I/O here is being done without specifying a text encoding. It’s generally best to explicitly say encoding="utf-8" when opening a text file for reading or writing. That way if the file contains something that isn’t valid UTF-8 text, or if you unintentionally try to write something that isn’t valid UTF-8 text, it will cause an exception to be raised.
    • Unless you replace the subprocess.run calls for wl-copy/paste with pyperclip, you should also specify the text encoding in the subprocess.run calls for similar reasons.
  • if os.path.getsize(CONFIGFILE) == 0: could raise an exception and crash the script even though you ensure CONFIGFILE exists in the if os.path.exists(CONFIGFILE) == False: block above. This is a time-of-check-to-time-of-use (TOCTOU) bug, which sometimes can have security implications. This should also be wrapped in a try/except block. (Pretty much anything related to I/O should be wrapped in a try/except block, or at least have some safeguards in one way or another.)

Some notes on the design of the script:

  • It might also be worth considering making it so that the config file specifies a clipboard directory, rather than a clipboard file. Then the user can configure a directory that will be used for nothing except clipboard exchange, and you can calculate both CLIPBOARDFILE and TRIGGERFILE as being clipboard_dir/clipboard-file-name-here and clipboard_dir/.cb-daemon-trigger, respectively. That would be faster and probably come with less risk of overwriting someone’s data on accident, though the chances of that are already pretty low as it is.
  • It’s generally best to avoid polling when possible (though starting with polling is fine for getting things working intiially). pyinotify and asyncio can be used to detect when the signal file changes without the need for polling.
  • Python by itself can be a bit brittle, especially because of dynamic typing. In Whonix, we often use a combination of Pylint, mypy, and black to check for common errors and format the code automatically. You might find those useful.
  • There’s a decent chance that at some point in the future, there will be a reason to add more configuration options to this. Rather than having a simple one-line config file with a pointer to a config file, you might want to use a TOML configuration file that can be extended in the future. The helper-scripts package in Whonix provides a strict_config_parser library that can be used to make reliable TOML config parsing fairly easy (this library hasn’t made it into Whonix 18’s stable repository yet, but it’s there in the developer repository and will end up in the stable repo eventually).

UX things I noticed when running the script:

  • If the user misunderstands the “type the clipboard file filepath to save to the configfile” prompt, and types a directory like /mnt/shared rather than /mnt/shared/clipfile, the bad value /mnt/shared will end up saved to the config file, the script will crash because it can’t write to /mnt/.cb-daemon-trigger (since it thinks that /mnt/shared is the file path and /mnt is the clipboard directory), and then it will crash again every time it is run. One has to manually fix the config file to work around this.
  • The script doesn’t check to see if wl-copy and wl-paste exist, it just tries to use them and errors out if it fails. It would be helpful if a prompt like Please install 'tool-name' to use cbdaemon appeared if a required tool is missing.
  • If the tool is started while nothing is present on the clipboard, the error Error pasting text from clipboard: Command '['wl-paste']' returned non-zero exit status 1. multiple times a second until something is copied to the clipboard.

I was able to get the tool to run, and can confirm that it does provide me working clipboard sharing! So while there are some rough edges that could be smoothed out, this definitely works.

Long-term, this probably can be integrated into Whonix. It would probably go in the package usability-misc, with a corresponding user systemd unit to start it on login. It would likely not be enabled automatically because of security considerations (users need to make sure their shared folder is either encrypted or in RAM, or might not want clipboard data touching the filesystem at all), but there would be a button somewhere to make turning it on easy.

Thank you for the hard work you did on this and for sharing it with us!

2 Likes

Thanks for going through all that and the feedback you’ve provided. I will go through it ASAP to see if I can implement the fixes you mentioned, I’m kind of busy right now but it will be soon.

I glanced at one of your points that you mentioned about wl-clipboard being integrated in pyperclip already, and that is true, but I tried that and it got me errors because pyperclip doesn’t use wl-clipboard correctly (it doesn’t use the primary clipboard), so I decided to use wl-clipboard directly. Isn’t it better practice to not use a library like pyperclip anyway and directly use xclip and wl-clipboard, who seem to be more “official”? If so, I’d rather change it to directly use xclip as well and abandon pyperclip altogether.

2 Likes

Using wrappers around existing programs that provide a more Pythonic interface is generally fine (as long as the project is reputable, has a package present in the Debian archives already, and doesn’t take an overly large amount of disk space), but in this instance if pyperclip is being somewhat buggy, I would agree that just using xclip directly would be better. That gives you more control, and gets rid of a dependency.

1 Like