diff options
Diffstat (limited to 'fw/pairing.py')
-rwxr-xr-x | fw/pairing.py | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/fw/pairing.py b/fw/pairing.py new file mode 100755 index 0000000..ce50081 --- /dev/null +++ b/fw/pairing.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +import threading +import binascii +import re +import os +import time + +import serial +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk, Pango, GLib + +import hexnoise + +class PairingWindow(Gtk.Window): + def __init__(self, noise, debug=False): + Gtk.Window.__init__(self, title='SecureHID pairing') + self.noise = noise + self.debug = debug + self.trusted = False + + self.set_border_width(10) + self.set_default_size(600, 200) + + self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + + self.label = Gtk.Label() + self.label.set_line_wrap(True) + self.label.set_justify(Gtk.Justification.CENTER) + self.label.set_markup('<b>Step 1</b>\n\nContacting device...') + self.vbox.pack_start(self.label, True, True, 0) + + self.entry = Gtk.Entry() + self.entry.set_editable(False) + self.vbox.pack_start(self.entry, True, True, 0) + + self.confirm_button = Gtk.Button(label='Trust this device') + self.confirm_button.connect('clicked', self.confirm_trust) + self.confirm_button.set_sensitive(False) + self.abort_button = Gtk.Button(label='Abort') + self.abort_button.connect('clicked', lambda _foo: self.destroy()) + self.bbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10) + self.bbox.pack_start(self.confirm_button, True, True, 0) + self.bbox.pack_start(self.abort_button, True, True, 0) + self.vbox.pack_start(self.bbox, True, True, 0) + + self.add(self.vbox) + + self.handshaker = threading.Thread(target=self.pair, daemon=True) + self.handshaker.start() + + def pair(self): + for i in range(10): + try: + self.run_handshake() + break + except hexnoise.ProtocolError as e: + print(e) + + def run_handshake(self): + binding_incantation = self.noise.channel_binding_incantation() + GLib.idle_add(self.label.set_markup, + f'<b>Step 2</b>\n\nPerform channel binding ritual.\n' + f'Enter the following incantation, then press enter.\n' + f'<b>{binding_incantation}</b>') + + def update_text(text): + self.entry.set_text(text) + self.entry.set_position(len(text)) + + clean = lambda s: re.sub('[^a-z0-9-]', '', s.lower()) + if clean(binding_incantation).startswith(clean(text)): + color = 0.9, 1.0, 0.9 # light red + else: + color = 1.0, 0.9, 0.9 # light green + self.entry.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA(*color, 1.0)) + + try: + for user_input in self.noise.pairing_messages(): + GLib.idle_add(update_text, user_input) + + GLib.idle_add(self.finish_pairing) + except hexnoise.ProtocolError as e: + GLib.idle_add(self.label.set_markup, f'<b>Error: {e}</b>') + + def finish_pairing(self): + self.label.set_markup(f'<b>Step 3</b>\n\nConfirm pairing.\n' + f'In case the device did not sound an alarm just now, confirm pairing now using the button below.') + self.confirm_button.set_sensitive(True) + + def confirm_trust(self, _foo): + self.trusted = True + self.destroy() + + +class StatusIcon(Gtk.StatusIcon): + def __init__(self): + Gtk.StatusIcon.__init__(self) + self.set_tooltip_text('SecureHID connected') + self.set_from_file('secureusb_icon.png') + +def run_pairing_gui(port, baudrate, debug=False): + XDG_CONFIG_HOME = os.environ.get('XDG_CONFIG_HOME') or os.path.join(os.path.expandvars('$HOME'), '.config', 'secure_hid') + if not os.path.isdir(XDG_CONFIG_HOME): + os.mkdir(XDG_CONFIG_HOME) + + private_key_file = os.path.join(XDG_CONFIG_HOME, 'host_key.pem') + if not os.path.isfile(private_key_file): + with open(private_key_file, 'w') as f: + f.write(binascii.hexlify(hexnoise.NoiseEngine.generate_private_key_x25519()).decode()) + + known_devices_file = os.path.join(XDG_CONFIG_HOME, 'known_devices') + if not os.path.isfile(known_devices_file): + with open(known_devices_file, 'w') as f: + f.write('# This file contains the hex-encoded SHA-256 fingerprints of the X25519 keys of all trusted SecureHID devices\n') + + with open(private_key_file) as f: + host_key_private = binascii.unhexlify(f.read()) + + ser = serial.Serial(port, baudrate) + packetizer = hexnoise.Packetizer(ser, debug=debug) + noise = hexnoise.NoiseEngine(host_key_private, packetizer, debug=debug) + noise.perform_handshake() + print('Connected.') + print('Device fingerprint:', noise.remote_fingerprint) + + if not noise.paired: + window = PairingWindow(noise, debug=debug) + window.connect('destroy', Gtk.main_quit) + window.show_all() + Gtk.main() + + if not window.trusted: + raise SystemError('User abort') + + if not noise.paired: + raise SystemError('Unknown noise error') + + with open(known_devices_file, 'a') as f: + f.write(f'{noise.remote_fingerprint} # added {time.ctime()}\n') + + else: + with open(known_devices_file) as f: + known_devices = [ l.strip().partition('#')[0].strip() for l in f.readlines() if not l[0] == '#' ] + + if noise.remote_fingerprint not in known_devices: + raise ValueError('Remote host is untrusted but seems to trust us.') + + input_runner = threading.Thread(target=noise.uinput_passthrough, daemon=True) + input_runner.start() + + status_icon = StatusIcon() + Gtk.main() + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('serial') + parser.add_argument('baudrate') + parser.add_argument('-d', '--debug', action='store_true') + args = parser.parse_args() + + run_pairing_gui(args.serial, args.baudrate, args.debug) + |