Last active
January 9, 2023 07:24
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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