summaryrefslogtreecommitdiff
path: root/fw/pairing.py
diff options
context:
space:
mode:
Diffstat (limited to 'fw/pairing.py')
-rwxr-xr-xfw/pairing.py164
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)
+