Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Lokno
Last active January 9, 2023 07:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lokno/69027f2c5981aa53e5300568b68ed8c7 to your computer and use it in GitHub Desktop.
Save Lokno/69027f2c5981aa53e5300568b68ed8c7 to your computer and use it in GitHub Desktop.
Cross-platform python script that maintains an encrypted copy of your clipboard at a user-specified remote location
# Cross-platform python script that maintains an encrypted copy of your clipboard
# at a user-specified remote location. Complete with tkinter GUI set-up.
#
# Author: Lokno Decker
#
# Usage: webclipboard.py copy|paste [--reset] [--show-config]
# Options:
# copy : encrypts the contents of the current clipboard to file
# and uploads this file to a user-specified remote server.
# paste : downloads the clipboard file from a user-specified
# remote server, decrypts this file, and replaces the current
# clipboard with the file's unencrypted contents.
# --reset : Deletes your configuration setting and shows
# the set-up dialog box again.
# --show-config : Prints the configuration settings along
# with your secret key at the command line.
#
# On first usage on any machine, a tkinter dialog box will appear
# asking for a username, host, and remote directory to store the
# cryptography.fernet encrypted clipboard file. You need to have
# configured SSH public key authentication with your server.
#
# Configuring an SSH login without password: http://www.linuxproblem.org/art_9.html
#
# Example AutoHotKey shortcuts for Windows:
#
# ^+!c::
# Send, ^c
# Run, py "PATH\TO\SCRIPT\webclipboard.py" copy, , Min
# return
#
# ^+!v::
# RunWait, py "PATH\TO\SCRIPT\webclipboard.py" paste, , Min
# Send, ^v
# return
#
# Required Python Packages: appdirs, pyperclip, cryptography, argparse
# appdirs - library to determine the appropriate platform-specific
# location for user data for starting config.json
# pyperclip - cross-platform module for copy and paste clipboard functions
# cryptography - module for encryting/decrypting clipboard, as well as
# generating a secret key on initial usage
# argparse - parser for command-line options, arguments and sub-commands
import re
import sys
import os
import platform
import pkg_resources
app_name = "webclipboard"
app_author = "Lokno"
msgbox_imported = False
def show_error_window(msg):
global msgbox_imported
if not msgbox_imported:
try:
import tkinter.messagebox
msgbox_imported = True
except:
print('ERROR: Could not find tkinter to pop alert.')
print(f'ERROR: {msg:s}')
if msgbox_imported:
tkinter.messagebox.showerror('Web Clipboard Error', msg)
def check_dependencies(dependencies):
print(f'Python {platform.python_version():s}')
print('Checking script dependencies...')
not_found = set()
for d in dependencies:
try:
pkg_resources.get_distribution(d)
except pkg_resources.DistributionNotFound:
not_found.add(d)
count_missing = len(not_found)
if count_missing:
suffix = "" if count_missing < 2 else "s"
msg = f'ERROR: Missing {count_missing:d} module{suffix:s}: {", ".join(not_found):s}\n'
msg += f'Install and re-run {os.path.basename(sys.argv[0]):s}'
show_error_window(msg)
else:
print('All dependencies found!')
return count_missing == 0
if not check_dependencies(['appdirs','pyperclip','cryptography','argparse']):
sys.exit(-1)
import appdirs
from cryptography.fernet import Fernet,InvalidToken
import json
import pyperclip
import argparse
def check_ssh_publickey_auth(user,host):
cmd = "ssh -o PreferredAuthentications=publickey -o PasswordAuthentication=no {}@{} exit".format(user, host)
return os.system(cmd) == 0
def check_ssh_dir(user,host,directory):
cmd = 'ssh {}@{} "touch {}/clipboard.txt"'.format(user, host, directory)
return os.system(cmd) == 0
class DialogBox:
def __init__(self):
self.re_valid = re.compile(r'^[\w.-]+$')
self.re_key = re.compile(r'^[a-zA-Z0-9_-]+=$')
self.root = tk.Tk()
self.root.title("Web Clipboard Set-Up")
self.username_label = tk.Label(self.root, text="Username:")
self.username_entry = tk.Entry(self.root)
self.domain_label = tk.Label(self.root, text="Web domain:")
self.domain_entry = tk.Entry(self.root)
self.directory_label = tk.Label(self.root, text="Directory:")
self.directory_entry = tk.Entry(self.root)
self.secret_key_label = tk.Label(self.root, text="Secret key:")
self.secret_key_entry = tk.Entry(self.root)
self.generate_key_var = tk.IntVar()
self.generate_key_checkbox = tk.Checkbutton(self.root, text="Generate key", variable=self.generate_key_var)
self.generate_key_var.trace("w", lambda *args: self.generate_key())
self.submit_button = tk.Button(self.root, text="Submit", command=self.submit)
self.error_label = tk.Label(self.root, text="", fg="red")
self.username_label.grid(row=0, column=0)
self.username_entry.grid(row=0, column=1)
self.domain_label.grid(row=1, column=0)
self.domain_entry.grid(row=1, column=1)
self.directory_label.grid(row=2, column=0)
self.directory_entry.grid(row=2, column=1)
self.secret_key_label.grid(row=3, column=0)
self.secret_key_entry.grid(row=3, column=1)
self.generate_key_checkbox.grid(row=3, column=2)
self.submit_button.grid(row=4, column=1)
self.error_label.grid(row=5, column=1)
self.submitted = False
def submit(self):
self.user = self.username_entry.get()
self.host = self.domain_entry.get()
self.dir = self.directory_entry.get()
self.key = self.secret_key_entry.get()
if self.validate():
self.submitted = True
self.root.destroy()
def generate_key(self):
key = ''
if self.generate_key_var.get():
key = Fernet.generate_key().decode()
self.secret_key_entry.delete(0, tk.END)
self.secret_key_entry.insert(0, key)
def display_error(self,message):
self.error_label.config(text=message)
def run(self):
self.root.mainloop()
def get_input(self):
if self.submitted:
return {'user' : self.user,
'host' : self.host,
'dir' : self.dir,
'key' : self.key}
else:
return {}
def validate(self):
is_valid = True
if not self.re_valid.match(self.user):
self.display_error('Enter Valid Username')
is_valid = False
elif not self.re_valid.match(self.host):
self.display_error('Enter Valid Hostname')
is_valid = False
elif not self.re_key.match(self.key):
self.display_error('Enter Valid Key (or retoggle Generate Key checkbox)')
is_valid = False
if is_valid:
if not check_ssh_publickey_auth(self.user, self.host):
self.display_error(f'Cannot access {self.user:s}@{self.host:s}:{self.dir:s}')
is_valid = False
elif not check_ssh_dir(self.user,self.host,self.dir):
self.display_error(f'Cannot write to {self.user:s}@{self.host:s}:{self.dir:s}/clipboard.txt')
is_valid = False
return is_valid
def clear_config():
data_dir = appdirs.user_data_dir(app_name, app_author)
data_file = os.path.join(data_dir, "config.json")
if os.path.exists(data_file):
os.remove(data_file)
def store_config(data):
data_dir = appdirs.user_data_dir(app_name, app_author)
if not os.path.exists(data_dir):
os.makedirs(data_dir)
data_file = os.path.join(data_dir, "config.json")
with open(data_file, "w") as f:
f.write(json.dumps(data))
def load_config():
data_dir = appdirs.user_data_dir(app_name, app_author)
data_file = os.path.join(data_dir, "config.json")
if not os.path.exists(data_file):
return {}
with open(data_file, "r") as f:
return json.loads(f.read())
def config_location():
data_dir = appdirs.user_data_dir(app_name, app_author)
return os.path.join(data_dir, "config.json")
def encrypt_clipboard(config):
encrypter = Fernet(config['key'])
encrypted = encrypter.encrypt(bytes(pyperclip.paste(),'utf-8'))
with open ('clipboard.txt', 'wb') as f:
f.write(encrypted)
cmd = "scp clipboard.txt {}@{}:{}".format(config['user'],config['host'],config['dir'])
if os.system(cmd) != 0:
show_error_window('Could Not Upload Clipboard Data. Check config.json')
os.remove('clipboard.txt')
def decrypt_clipboard(config):
cmd = "scp {}@{}:{}/clipboard.txt .".format(config['user'],config['host'],config['dir'])
if os.system(cmd) == 0:
decrypter = Fernet(config['key'])
with open('clipboard.txt', 'rb') as f:
encrypted = f.read()
os.remove('clipboard.txt')
try:
decrypted = decrypter.decrypt(encrypted)
pyperclip.copy(decrypted.decode('utf-8'))
except InvalidToken:
show_error_window('InvalidToken. Are you using the same secret key used to encrypt?')
parser = argparse.ArgumentParser()
parser.add_argument('mode', choices=['copy', 'paste'], help='Indicate whether to copy or paste')
parser.add_argument('--reset', action='store_true', default=False, help='Reprompt set-up configuration')
parser.add_argument('--show-config', action='store_true', default=False, help='Print the configuration info')
args = parser.parse_args()
if args.reset and args.show_config:
print('ERROR: Arguments --reset and --show-config are invalid together')
sys.exit(-1)
config = load_config()
if args.show_config:
if not config:
print('Web Clipboard is not configured.')
print('Run without this argument to configure.')
sys.exit(-1)
else:
print('\nConfiguration:')
print('\n'.join([f'{k} : {v}' for k,v in config.items()]))
print(f'\nPath to Config: {config_location():s}')
print('\nUse the option --reset at the commandline to reconfigure\n')
else:
if not config or args.reset:
if args.reset and config:
clear_config()
try:
import tkinter as tk
except:
print('ERROR: Could not find tkinter. Install and try again')
input_dialog = DialogBox()
input_dialog.run()
config.update(input_dialog.get_input())
if not config:
print('Still not configured. Aborting...')
sys.exit(-1)
store_config(config)
if args.mode == 'copy':
encrypt_clipboard(config)
else:
decrypt_clipboard(config)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment