From d6b0a6cb5496ff212d2eeb58a46dd3fbcb6555ed Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 26 Dec 2020 21:26:38 -0500 Subject: [PATCH 01/13] firmware_flasher: accept firmware in .vfw format --- src/main/python/firmware_flasher.py | 44 +++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index 081d39c..ab342b2 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later import datetime +import hashlib import struct import time import threading @@ -32,8 +33,21 @@ CHUNK = 64 def cmd_flash(device, firmware, log_cb, progress_cb, complete_cb, error_cb): - while len(firmware) % CHUNK != 0: - firmware += b"\x00" + if firmware[0:8] != b"VIALFW00": + return error_cb("Error: Invalid signature") + + fw_uid = firmware[8:16] + fw_ts = struct.unpack(" Date: Sat, 26 Dec 2020 21:51:48 -0500 Subject: [PATCH 02/13] firmware_flasher: automatically restart keyboard in bootloader mode and flash --- src/main/python/firmware_flasher.py | 77 +++++++++++++++++++++++------ src/main/python/keyboard_comm.py | 15 ++++++ src/main/python/util.py | 2 +- 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index ab342b2..f0a7185 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -6,13 +6,16 @@ import struct import time import threading -from PyQt5.QtCore import pyqtSignal +from PyQt5.QtCore import pyqtSignal, QCoreApplication from PyQt5.QtGui import QFontDatabase from PyQt5.QtWidgets import QHBoxLayout, QLineEdit, QToolButton, QPlainTextEdit, QProgressBar,QFileDialog, QDialog from basic_editor import BasicEditor -from util import tr, chunks -from vial_device import VialBootloader +from util import tr, chunks, find_vial_devices +from vial_device import VialBootloader, VialKeyboard + + +BL_SUPPORTED_VERSION = 0 def send_retries(dev, data, retries=20): @@ -32,6 +35,18 @@ def send_retries(dev, data, retries=20): CHUNK = 64 +def bl_get_version(dev): + dev.send(b"VC\x00") + data = dev.recv(8) + return data[0] + + +def bl_get_uid(dev): + dev.send(b"VC\x01") + data = dev.recv(8) + return data + + def cmd_flash(device, firmware, log_cb, progress_cb, complete_cb, error_cb): if firmware[0:8] != b"VIALFW00": return error_cb("Error: Invalid signature") @@ -50,24 +65,22 @@ def cmd_flash(device, firmware, log_cb, progress_cb, complete_cb, error_cb): )) # Check bootloader is correct version - device.send(b"VC\x00") - data = device.recv(8) - log_cb("* Bootloader version: {}".format(data[0])) - if data[0] != 0: + ver = bl_get_version(device) + log_cb("* Bootloader version: {}".format(ver)) + if ver != BL_SUPPORTED_VERSION: return error_cb("Error: Unsupported bootloader version") - device.send(b"VC\x01") - data = device.recv(8) - log_cb("* Vial ID: {}".format(data.hex())) + uid = bl_get_uid(device) + log_cb("* Vial ID: {}".format(uid.hex())) - if data == b"\xFF" * 8: + if uid == b"\xFF" * 8: log_cb("\n\n\n!!! WARNING !!!\nBootloader UID is not set, make sure to configure it" " before releasing production firmware\n!!! WARNING !!!\n\n") - if data != fw_uid: + if uid != fw_uid: return error_cb("Error: Firmware package was built for different device\n\texpected={}\n\tgot={}".format( fw_uid.hex(), - data.hex() + uid.hex() )) # OK all checks complete, we can flash now @@ -139,12 +152,18 @@ class FirmwareFlasher(BasicEditor): def rebuild(self, device): super().rebuild(device) self.txt_logger.clear() + + if not self.valid(): + return + if isinstance(self.device, VialBootloader): self.log("Valid Vial Bootloader device at {}".format(self.device.desc["path"].decode("utf-8"))) + elif isinstance(self.device, VialKeyboard): + self.log("Vial keyboard detected") def valid(self): - # TODO: it is also valid to flash a VialKeyboard which supports optional "vibl-integration" feature - return isinstance(self.device, VialBootloader) + return isinstance(self.device, VialBootloader) or\ + isinstance(self.device, VialKeyboard) and self.device.keyboard.vibl def on_click_select_file(self): dialog = QFileDialog() @@ -170,6 +189,34 @@ class FirmwareFlasher(BasicEditor): self.log("Preparing to flash...") self.lock_ui() + + # TODO: this needs to switch to the secure assisted-reset feature before public release + if isinstance(self.device, VialKeyboard): + uid = self.device.keyboard.get_uid() + + self.log("Restarting in bootloader mode...") + self.device.keyboard.reset() + + # watch for bootloaders to appear and ask them for their UID, return one that matches the keyboard + while True: + self.log("Looking for devices...") + QCoreApplication.processEvents() + time.sleep(1) + devices = find_vial_devices() + found = None + for dev in devices: + if isinstance(dev, VialBootloader): + dev.open() + # TODO: update version check before release + if bl_get_version(dev) != BL_SUPPORTED_VERSION or bl_get_uid(dev) != uid: + dev.close() + found = dev + break + if found: + self.log("Found Vial Bootloader device at {}".format(found.desc["path"].decode("utf-8"))) + self.device = found + break + threading.Thread(target=lambda: cmd_flash( self.device, firmware, self.on_log, self.on_progress, self.on_complete, self.on_error)).start() diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 9d99693..04005d0 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -54,6 +54,7 @@ class Keyboard: self.macro_count = 0 self.macro_memory = 0 self.macro = b"" + self.vibl = False self.vial_protocol = self.keyboard_id = -1 @@ -103,6 +104,10 @@ class Keyboard: payload = json.loads(lzma.decompress(payload)) + if "vial" in payload: + vial = payload["vial"] + self.vibl = vial.get("vibl", False) + self.layouts = payload.get("layouts") self.rows = payload["matrix"]["rows"] @@ -275,3 +280,13 @@ class Keyboard: self.set_layout_options(data["layout_options"]) self.set_macro(base64.b64decode(data["macro"])) + + def reset(self): + self.usb_send(self.dev, struct.pack("B", 0xB)) + self.dev.close() + + def get_uid(self): + """ Retrieve UID from the keyboard, explicitly sending a query packet """ + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_KEYBOARD_ID)) + keyboard_id = data[4:12] + return keyboard_id diff --git a/src/main/python/util.py b/src/main/python/util.py index 0b37b88..c687e56 100644 --- a/src/main/python/util.py +++ b/src/main/python/util.py @@ -32,7 +32,7 @@ def is_rawhid(dev): return dev["interface_number"] == 1 -def find_vial_devices(sideload_vid, sideload_pid): +def find_vial_devices(sideload_vid=None, sideload_pid=None): from vial_device import VialBootloader, VialKeyboard filtered = [] From a397b700442aa0987b621c1c40af80b6f3110511 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 26 Dec 2020 22:11:30 -0500 Subject: [PATCH 03/13] firmware_flasher: support restoring previous layout when flashing new firmware --- src/main/python/firmware_flasher.py | 47 +++++++++++++++++++++++++++-- src/main/python/vial_device.py | 4 +-- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index f0a7185..a89cca0 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -11,7 +11,8 @@ from PyQt5.QtGui import QFontDatabase from PyQt5.QtWidgets import QHBoxLayout, QLineEdit, QToolButton, QPlainTextEdit, QProgressBar,QFileDialog, QDialog from basic_editor import BasicEditor -from util import tr, chunks, find_vial_devices +from keyboard_comm import Keyboard, CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_KEYBOARD_ID +from util import tr, chunks, find_vial_devices, MSG_LEN from vial_device import VialBootloader, VialKeyboard @@ -149,6 +150,8 @@ class FirmwareFlasher(BasicEditor): self.device = None + self.layout_restore = self.uid_restore = None + def rebuild(self, device): super().rebuild(device) self.txt_logger.clear() @@ -190,9 +193,16 @@ class FirmwareFlasher(BasicEditor): self.log("Preparing to flash...") self.lock_ui() + self.layout_restore = self.uid_restore = None + # TODO: this needs to switch to the secure assisted-reset feature before public release if isinstance(self.device, VialKeyboard): - uid = self.device.keyboard.get_uid() + # back up current layout + self.log("Backing up current layout...") + self.layout_restore = self.device.keyboard.save_layout() + + # keep track of which keyboard we should restore saved layout to + self.uid_restore = self.device.keyboard.get_uid() self.log("Restarting in bootloader mode...") self.device.keyboard.reset() @@ -208,8 +218,9 @@ class FirmwareFlasher(BasicEditor): if isinstance(dev, VialBootloader): dev.open() # TODO: update version check before release - if bl_get_version(dev) != BL_SUPPORTED_VERSION or bl_get_uid(dev) != uid: + if bl_get_version(dev) != BL_SUPPORTED_VERSION or bl_get_uid(dev) != self.uid_restore: dev.close() + continue found = dev break if found: @@ -241,6 +252,36 @@ class FirmwareFlasher(BasicEditor): def _on_complete(self, msg): self.log(msg) self.progress_bar.setValue(100) + + # if we were asked to restore a layout, find keyboard with matching UID and restore the layout to it + if self.layout_restore: + while True: + self.log("Looking for devices...") + QCoreApplication.processEvents() + time.sleep(1) + devices = find_vial_devices() + found = None + for dev in devices: + if isinstance(dev, VialKeyboard): + try: + dev.open() + except OSError: + continue + if dev.keyboard.get_uid() != self.uid_restore: + dev.close() + continue + found = dev + break + if found: + self.log("Found Vial keyboard at {}".format(found.desc["path"].decode("utf-8"))) + self.device = found + break + self.log("Restoring saved layout...") + QCoreApplication.processEvents() + found.keyboard.restore_layout(self.layout_restore) + found.close() + self.log("Done!") + self.unlock_ui() def _on_error(self, msg): diff --git a/src/main/python/vial_device.py b/src/main/python/vial_device.py index 2412b36..ea5f4e8 100644 --- a/src/main/python/vial_device.py +++ b/src/main/python/vial_device.py @@ -19,8 +19,8 @@ class VialDevice: # add 00 at start for hidapi report id return self.dev.write(b"\x00" + data) - def recv(self, length): - return bytes(self.dev.read(length)) + def recv(self, length, timeout_ms=0): + return bytes(self.dev.read(length, timeout_ms=timeout_ms)) def close(self): self.dev.close() From e811e50c0daef631e72ca78a2ff668e397aaab6b Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sat, 26 Dec 2020 22:14:52 -0500 Subject: [PATCH 04/13] firmware_flasher: remove unused imports --- src/main/python/firmware_flasher.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index a89cca0..e252285 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -11,8 +11,7 @@ from PyQt5.QtGui import QFontDatabase from PyQt5.QtWidgets import QHBoxLayout, QLineEdit, QToolButton, QPlainTextEdit, QProgressBar,QFileDialog, QDialog from basic_editor import BasicEditor -from keyboard_comm import Keyboard, CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_KEYBOARD_ID -from util import tr, chunks, find_vial_devices, MSG_LEN +from util import tr, chunks, find_vial_devices from vial_device import VialBootloader, VialKeyboard From 5619ccbcd43105bb50bd34bb05cbb5a3c8cc864f Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sun, 27 Dec 2020 06:39:34 -0500 Subject: [PATCH 05/13] firmware_flasher: reduce code duplication when looking for devices --- src/main/python/firmware_flasher.py | 73 ++++++++++------------------- src/main/python/vial_device.py | 21 +++++++++ 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index e252285..5d5e9f6 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -35,18 +35,6 @@ def send_retries(dev, data, retries=20): CHUNK = 64 -def bl_get_version(dev): - dev.send(b"VC\x00") - data = dev.recv(8) - return data[0] - - -def bl_get_uid(dev): - dev.send(b"VC\x01") - data = dev.recv(8) - return data - - def cmd_flash(device, firmware, log_cb, progress_cb, complete_cb, error_cb): if firmware[0:8] != b"VIALFW00": return error_cb("Error: Invalid signature") @@ -65,12 +53,14 @@ def cmd_flash(device, firmware, log_cb, progress_cb, complete_cb, error_cb): )) # Check bootloader is correct version - ver = bl_get_version(device) + device.send(b"VC\x00") + ver = device.recv(8)[0] log_cb("* Bootloader version: {}".format(ver)) if ver != BL_SUPPORTED_VERSION: return error_cb("Error: Unsupported bootloader version") - uid = bl_get_uid(device) + device.send(b"VC\x01") + uid = device.recv(8) log_cb("* Vial ID: {}".format(uid.hex())) if uid == b"\xFF" * 8: @@ -167,6 +157,13 @@ class FirmwareFlasher(BasicEditor): return isinstance(self.device, VialBootloader) or\ isinstance(self.device, VialKeyboard) and self.device.keyboard.vibl + def find_device_with_uid(self, cls, uid): + devices = find_vial_devices() + for dev in devices: + if isinstance(dev, cls) and dev.get_uid() == uid: + return dev + return None + def on_click_select_file(self): dialog = QFileDialog() dialog.setDefaultSuffix("vfw") @@ -207,25 +204,16 @@ class FirmwareFlasher(BasicEditor): self.device.keyboard.reset() # watch for bootloaders to appear and ask them for their UID, return one that matches the keyboard - while True: + found = None + while found is None: self.log("Looking for devices...") QCoreApplication.processEvents() time.sleep(1) - devices = find_vial_devices() - found = None - for dev in devices: - if isinstance(dev, VialBootloader): - dev.open() - # TODO: update version check before release - if bl_get_version(dev) != BL_SUPPORTED_VERSION or bl_get_uid(dev) != self.uid_restore: - dev.close() - continue - found = dev - break - if found: - self.log("Found Vial Bootloader device at {}".format(found.desc["path"].decode("utf-8"))) - self.device = found - break + found = self.find_device_with_uid(VialBootloader, self.uid_restore) + + self.log("Found Vial Bootloader device at {}".format(found.desc["path"].decode("utf-8"))) + found.open() + self.device = found threading.Thread(target=lambda: cmd_flash( self.device, firmware, self.on_log, self.on_progress, self.on_complete, self.on_error)).start() @@ -254,27 +242,16 @@ class FirmwareFlasher(BasicEditor): # if we were asked to restore a layout, find keyboard with matching UID and restore the layout to it if self.layout_restore: - while True: + found = None + while found is None: self.log("Looking for devices...") QCoreApplication.processEvents() time.sleep(1) - devices = find_vial_devices() - found = None - for dev in devices: - if isinstance(dev, VialKeyboard): - try: - dev.open() - except OSError: - continue - if dev.keyboard.get_uid() != self.uid_restore: - dev.close() - continue - found = dev - break - if found: - self.log("Found Vial keyboard at {}".format(found.desc["path"].decode("utf-8"))) - self.device = found - break + found = self.find_device_with_uid(VialKeyboard, self.uid_restore) + + self.log("Found Vial keyboard at {}".format(found.desc["path"].decode("utf-8"))) + found.open() + self.device = found self.log("Restoring saved layout...") QCoreApplication.processEvents() found.keyboard.restore_layout(self.layout_restore) diff --git a/src/main/python/vial_device.py b/src/main/python/vial_device.py index ea5f4e8..65eecbf 100644 --- a/src/main/python/vial_device.py +++ b/src/main/python/vial_device.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later from hidproxy import hid from keyboard_comm import Keyboard +from util import MSG_LEN class VialDevice: @@ -44,8 +45,28 @@ class VialKeyboard(VialDevice): s += " [sideload]" return s + def get_uid(self): + try: + super().open() + except OSError: + return b"" + self.send(b"\xFE\x00" + b"\x00" * 30) + data = self.recv(MSG_LEN, timeout_ms=500) + super().close() + return data[4:12] + class VialBootloader(VialDevice): def title(self): return "Vial Bootloader [{:04X}:{:04X}]".format(self.desc["vendor_id"], self.desc["product_id"]) + + def get_uid(self): + try: + super().open() + except OSError: + return b"" + self.send(b"VC\x01") + data = self.recv(8, timeout_ms=500) + super().close() + return data From a6c42b513f1538c8d48246627f10cb6f877c75e2 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sun, 27 Dec 2020 08:03:45 -0500 Subject: [PATCH 06/13] unlocker: initial implementation of unlocking keyboard to perform security-sensitive actions --- src/main/python/firmware_flasher.py | 6 ++- src/main/python/keyboard_comm.py | 14 +++++++ src/main/python/unlocker.py | 60 +++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/main/python/unlocker.py diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index 5d5e9f6..85bcd76 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -11,6 +11,7 @@ from PyQt5.QtGui import QFontDatabase from PyQt5.QtWidgets import QHBoxLayout, QLineEdit, QToolButton, QPlainTextEdit, QProgressBar,QFileDialog, QDialog from basic_editor import BasicEditor +from unlocker import Unlocker from util import tr, chunks, find_vial_devices from vial_device import VialBootloader, VialKeyboard @@ -141,6 +142,8 @@ class FirmwareFlasher(BasicEditor): self.layout_restore = self.uid_restore = None + self.unlocker = Unlocker() + def rebuild(self, device): super().rebuild(device) self.txt_logger.clear() @@ -191,7 +194,6 @@ class FirmwareFlasher(BasicEditor): self.layout_restore = self.uid_restore = None - # TODO: this needs to switch to the secure assisted-reset feature before public release if isinstance(self.device, VialKeyboard): # back up current layout self.log("Backing up current layout...") @@ -200,6 +202,8 @@ class FirmwareFlasher(BasicEditor): # keep track of which keyboard we should restore saved layout to self.uid_restore = self.device.keyboard.get_uid() + self.unlocker.perform_unlock(self.device.keyboard) + self.log("Restarting in bootloader mode...") self.device.keyboard.reset() diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 04005d0..6335ccf 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -27,6 +27,9 @@ CMD_VIAL_GET_SIZE = 0x01 CMD_VIAL_GET_DEFINITION = 0x02 CMD_VIAL_GET_ENCODER = 0x03 CMD_VIAL_SET_ENCODER = 0x04 +CMD_VIAL_GET_LOCK = 0x05 +CMD_VIAL_UNLOCK_START = 0x06 +CMD_VIAL_UNLOCK_POLL = 0x07 # how much of a macro/keymap buffer we can read/write per packet BUFFER_FETCH_CHUNK = 28 @@ -290,3 +293,14 @@ class Keyboard: data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_KEYBOARD_ID)) keyboard_id = data[4:12] return keyboard_id + + def get_lock(self): + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_LOCK)) + return data[0] + + def unlock_start(self): + self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_UNLOCK_START)) + + def unlock_poll(self): + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_UNLOCK_POLL)) + return data diff --git a/src/main/python/unlocker.py b/src/main/python/unlocker.py new file mode 100644 index 0000000..b3d9b96 --- /dev/null +++ b/src/main/python/unlocker.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +import time + +from PyQt5.QtCore import QCoreApplication, Qt +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QProgressBar + +from util import tr + + +class Unlocker(QWidget): + + def __init__(self): + super().__init__() + self.keyboard = None + + layout = QVBoxLayout() + + self.progress = QProgressBar() + + layout.addWidget(QLabel(tr("Unlocker", "In order to proceed, the keyboard must be set into unlocked mode.\n" + "You should only perform this operation on computers that you trust."))) + layout.addWidget(QLabel(tr("Unlocker", "To exit this mode, you will need to replug the keyboard."))) + layout.addWidget(QLabel(tr("Unlocker", "Press and hold the following keys until the progress bar " + "below fills up:"))) + + # TODO: add image/text reference of keys user needs to hold + + layout.addWidget(self.progress) + + self.setLayout(layout) + self.setWindowFlag(Qt.Dialog) + + def perform_unlock(self, keyboard): + # if it's already unlocked, don't need to do anything + if keyboard.get_lock() == 0: + return + + self.progress.setMaximum(1) + self.progress.setValue(0) + + self.show() + self.keyboard = keyboard + self.keyboard.unlock_start() + + while True: + data = self.keyboard.unlock_poll() + unlocked = data[0] + unlock_counter = data[2] + + self.progress.setMaximum(max(self.progress.maximum(), unlock_counter)) + self.progress.setValue(self.progress.maximum() - unlock_counter) + + if unlocked == 1: + break + + QCoreApplication.processEvents() + time.sleep(0.2) + + # ok all done, the keyboard is now set to insecure state + self.hide() From de476ee63c94dd7edb9eab079fa99e4c7e302ad6 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Sun, 27 Dec 2020 08:09:28 -0500 Subject: [PATCH 07/13] unlocker: also perform unlock in macro editor --- src/main/python/firmware_flasher.py | 4 +--- src/main/python/macro_recorder.py | 2 ++ src/main/python/main_window.py | 4 ++++ src/main/python/unlocker.py | 6 ++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index 85bcd76..42a4951 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -142,8 +142,6 @@ class FirmwareFlasher(BasicEditor): self.layout_restore = self.uid_restore = None - self.unlocker = Unlocker() - def rebuild(self, device): super().rebuild(device) self.txt_logger.clear() @@ -202,7 +200,7 @@ class FirmwareFlasher(BasicEditor): # keep track of which keyboard we should restore saved layout to self.uid_restore = self.device.keyboard.get_uid() - self.unlocker.perform_unlock(self.device.keyboard) + Unlocker.get().perform_unlock(self.device.keyboard) self.log("Restarting in bootloader mode...") self.device.keyboard.reset() diff --git a/src/main/python/macro_recorder.py b/src/main/python/macro_recorder.py index 235c2d5..674009e 100644 --- a/src/main/python/macro_recorder.py +++ b/src/main/python/macro_recorder.py @@ -11,6 +11,7 @@ from macro_action import ActionText, ActionTap, ActionDown, ActionUp, SS_TAP_COD from macro_key import KeyString, KeyDown, KeyUp, KeyTap from macro_line import MacroLine from macro_optimizer import macro_optimize +from unlocker import Unlocker from util import tr from vial_device import VialKeyboard @@ -329,5 +330,6 @@ class MacroRecorder(BasicEditor): self.deserialize(self.keyboard.macro) def on_save(self): + Unlocker.get().perform_unlock(self.device.keyboard) self.keyboard.set_macro(self.serialize()) self.on_change() diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index 9f53c37..6fd77f6 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -10,6 +10,7 @@ from firmware_flasher import FirmwareFlasher from keymap_editor import KeymapEditor from layout_editor import LayoutEditor from macro_recorder import MacroRecorder +from unlocker import Unlocker from util import tr, find_vial_devices @@ -17,6 +18,9 @@ class MainWindow(QMainWindow): def __init__(self): super().__init__() + + self.unlocker = Unlocker() + self.current_device = None self.devices = [] self.sideload_json = None diff --git a/src/main/python/unlocker.py b/src/main/python/unlocker.py index b3d9b96..cdf2c6b 100644 --- a/src/main/python/unlocker.py +++ b/src/main/python/unlocker.py @@ -30,6 +30,12 @@ class Unlocker(QWidget): self.setLayout(layout) self.setWindowFlag(Qt.Dialog) + Unlocker.obj = self + + @classmethod + def get(cls): + return cls.obj + def perform_unlock(self, keyboard): # if it's already unlocked, don't need to do anything if keyboard.get_lock() == 0: From e5487a0ae64dd3f5ad79f684f40f9d1df91cb028 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 29 Dec 2020 14:47:16 -0500 Subject: [PATCH 08/13] unlocker: show image of keys to hold --- src/main/python/keyboard_comm.py | 12 +++++++++++ src/main/python/keyboard_widget.py | 28 +++++++++++++++++++++++--- src/main/python/main_window.py | 3 +-- src/main/python/unlocker.py | 32 +++++++++++++++++++++++++++--- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 6335ccf..64c28bf 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -298,6 +298,18 @@ class Keyboard: data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_LOCK)) return data[0] + def get_lock_keys(self): + """ Return keys users have to hold to unlock the keyboard as a list of rowcols """ + + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_LOCK)) + rowcol = [] + for x in range(15): + row = data[2 + x * 2] + col = data[3 + x * 2] + if row != 255 and col != 255: + rowcol.append((row, col)) + return rowcol + def unlock_start(self): self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_UNLOCK_START)) diff --git a/src/main/python/keyboard_widget.py b/src/main/python/keyboard_widget.py index b50d092..a0c3aa6 100644 --- a/src/main/python/keyboard_widget.py +++ b/src/main/python/keyboard_widget.py @@ -10,6 +10,8 @@ from constants import KEY_WIDTH, KEY_SPACING, KEY_HEIGHT, KEYBOARD_WIDGET_PADDIN class KeyWidget: def __init__(self, desc, shift_x=0, shift_y=0): + self.active = False + self.masked = False self.desc = desc self.text = "" self.mask_text = "" @@ -88,6 +90,9 @@ class KeyWidget: def setToolTip(self, tooltip): self.tooltip = tooltip + def setActive(self, active): + self.active = active + class EncoderWidget(KeyWidget): @@ -117,6 +122,10 @@ class KeyboardWidget(QWidget): def __init__(self, layout_editor): super().__init__() + + self.enabled = True + self.scale = 1 + self.setMouseTracking(True) self.layout_editor = layout_editor @@ -192,8 +201,8 @@ class KeyboardWidget(QWidget): max_w = max_h = 0 for key in self.widgets: p = key.polygon.boundingRect().bottomRight() - max_w = max(max_w, p.x()) - max_h = max(max_h, p.y()) + max_w = max(max_w, p.x() * self.scale) + max_h = max(max_h, p.y() * self.scale) self.width = max_w + 2 * KEYBOARD_WIDGET_PADDING self.height = max_h + 2 * KEYBOARD_WIDGET_PADDING @@ -231,11 +240,12 @@ class KeyboardWidget(QWidget): for idx, key in enumerate(self.widgets): qp.save() + qp.scale(self.scale, self.scale) qp.translate(key.rotation_x, key.rotation_y) qp.rotate(key.rotation_angle) qp.translate(-key.rotation_x, -key.rotation_y) - if self.active_key == key and not self.active_mask: + if key.active or (self.active_key == key and not self.active_mask): qp.setPen(active_pen) qp.setBrush(active_brush) @@ -277,6 +287,9 @@ class KeyboardWidget(QWidget): return None, False def mousePressEvent(self, ev): + if not self.enabled: + return + self.active_key, self.active_mask = self.hit_test(ev.pos()) if self.active_key is not None: self.clicked.emit() @@ -299,6 +312,9 @@ class KeyboardWidget(QWidget): self.update() def event(self, ev): + if not self.enabled: + super().event(ev) + if ev.type() == QEvent.ToolTip: key = self.hit_test(ev.pos())[0] if key is not None: @@ -306,3 +322,9 @@ class KeyboardWidget(QWidget): else: QToolTip.hideText() return super().event(ev) + + def set_enabled(self, val): + self.enabled = val + + def set_scale(self, scale): + self.scale = scale diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index 6fd77f6..0548ae7 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -19,8 +19,6 @@ class MainWindow(QMainWindow): def __init__(self): super().__init__() - self.unlocker = Unlocker() - self.current_device = None self.devices = [] self.sideload_json = None @@ -45,6 +43,7 @@ class MainWindow(QMainWindow): self.editors = [(self.keymap_editor, "Keymap"), (self.layout_editor, "Layout"), (self.macro_recorder, "Macros"), (self.firmware_flasher, "Firmware updater")] + self.unlocker = Unlocker(self.layout_editor) self.tabs = QTabWidget() self.refresh_tabs() diff --git a/src/main/python/unlocker.py b/src/main/python/unlocker.py index cdf2c6b..45253a3 100644 --- a/src/main/python/unlocker.py +++ b/src/main/python/unlocker.py @@ -4,12 +4,13 @@ import time from PyQt5.QtCore import QCoreApplication, Qt from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QProgressBar +from keyboard_widget import KeyboardWidget from util import tr class Unlocker(QWidget): - def __init__(self): + def __init__(self, layout_editor): super().__init__() self.keyboard = None @@ -23,7 +24,11 @@ class Unlocker(QWidget): layout.addWidget(QLabel(tr("Unlocker", "Press and hold the following keys until the progress bar " "below fills up:"))) - # TODO: add image/text reference of keys user needs to hold + self.keyboard_reference = KeyboardWidget(layout_editor) + self.keyboard_reference.set_enabled(False) + self.keyboard_reference.set_scale(0.5) + layout.addWidget(self.keyboard_reference) + layout.setAlignment(self.keyboard_reference, Qt.AlignHCenter) layout.addWidget(self.progress) @@ -36,11 +41,29 @@ class Unlocker(QWidget): def get(cls): return cls.obj + def update_reference(self, keyboard): + """ Updates keycap reference image """ + + self.keyboard_reference.set_keys(keyboard.keys, keyboard.encoders) + + # use "active" background to indicate keys to hold + lock_keys = keyboard.get_lock_keys() + for w in self.keyboard_reference.widgets: + if (w.desc.row, w.desc.col) in lock_keys: + w.setActive(True) + + self.keyboard_reference.update_layout() + self.keyboard_reference.update() + self.keyboard_reference.updateGeometry() + def perform_unlock(self, keyboard): # if it's already unlocked, don't need to do anything - if keyboard.get_lock() == 0: + lock = keyboard.get_lock() + if lock == 0: return + self.update_reference(keyboard) + self.progress.setMaximum(1) self.progress.setValue(0) @@ -64,3 +87,6 @@ class Unlocker(QWidget): # ok all done, the keyboard is now set to insecure state self.hide() + + def closeEvent(self, ev): + ev.ignore() From 9a4825a124508a8fbf2791f5aca4ceef0e8faa32 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 29 Dec 2020 15:01:44 -0500 Subject: [PATCH 09/13] unlocker: allow re-locking keyboard --- src/main/python/keyboard_comm.py | 4 ++++ src/main/python/main_window.py | 22 ++++++++++++++++++++++ src/main/python/unlocker.py | 3 ++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 64c28bf..13e5236 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -30,6 +30,7 @@ CMD_VIAL_SET_ENCODER = 0x04 CMD_VIAL_GET_LOCK = 0x05 CMD_VIAL_UNLOCK_START = 0x06 CMD_VIAL_UNLOCK_POLL = 0x07 +CMD_VIAL_LOCK = 0x08 # how much of a macro/keymap buffer we can read/write per packet BUFFER_FETCH_CHUNK = 28 @@ -316,3 +317,6 @@ class Keyboard: def unlock_poll(self): data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_UNLOCK_POLL)) return data + + def lock(self): + self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_LOCK)) diff --git a/src/main/python/main_window.py b/src/main/python/main_window.py index 0548ae7..5592408 100644 --- a/src/main/python/main_window.py +++ b/src/main/python/main_window.py @@ -12,6 +12,7 @@ from layout_editor import LayoutEditor from macro_recorder import MacroRecorder from unlocker import Unlocker from util import tr, find_vial_devices +from vial_device import VialKeyboard class MainWindow(QMainWindow): @@ -84,6 +85,16 @@ class MainWindow(QMainWindow): file_menu.addSeparator() file_menu.addAction(exit_act) + keyboard_unlock_act = QAction(tr("MenuSecurity", "Unlock"), self) + keyboard_unlock_act.triggered.connect(self.unlock_keyboard) + + keyboard_lock_act = QAction(tr("MenuSecurity", "Lock"), self) + keyboard_lock_act.triggered.connect(self.lock_keyboard) + + self.security_menu = self.menuBar().addMenu(tr("Menu", "Security")) + self.security_menu.addAction(keyboard_unlock_act) + self.security_menu.addAction(keyboard_lock_act) + def on_layout_load(self): dialog = QFileDialog() dialog.setDefaultSuffix("vil") @@ -127,6 +138,9 @@ class MainWindow(QMainWindow): self.refresh_tabs() def rebuild(self): + # don't show "Security" menu for bootloader mode, as the bootloader is inherently insecure + self.security_menu.menuAction().setVisible(isinstance(self.current_device, VialKeyboard)) + for e in [self.layout_editor, self.keymap_editor, self.firmware_flasher, self.macro_recorder]: e.rebuild(self.current_device) @@ -162,3 +176,11 @@ class MainWindow(QMainWindow): self.tabs.setEnabled(True) self.combobox_devices.setEnabled(True) self.btn_refresh_devices.setEnabled(True) + + def unlock_keyboard(self): + if isinstance(self.current_device, VialKeyboard): + self.unlocker.perform_unlock(self.current_device.keyboard) + + def lock_keyboard(self): + if isinstance(self.current_device, VialKeyboard): + self.current_device.keyboard.lock() diff --git a/src/main/python/unlocker.py b/src/main/python/unlocker.py index 45253a3..b11c72f 100644 --- a/src/main/python/unlocker.py +++ b/src/main/python/unlocker.py @@ -20,7 +20,8 @@ class Unlocker(QWidget): layout.addWidget(QLabel(tr("Unlocker", "In order to proceed, the keyboard must be set into unlocked mode.\n" "You should only perform this operation on computers that you trust."))) - layout.addWidget(QLabel(tr("Unlocker", "To exit this mode, you will need to replug the keyboard."))) + layout.addWidget(QLabel(tr("Unlocker", "To exit this mode, you will need to replug the keyboard\n" + "or select Security->Lock from the menu."))) layout.addWidget(QLabel(tr("Unlocker", "Press and hold the following keys until the progress bar " "below fills up:"))) From b3080ec0285b86219954ece56d074d60564f1a54 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 29 Dec 2020 19:30:41 -0500 Subject: [PATCH 10/13] unlocker: flip around the lock/unlock logic --- src/main/python/keyboard_comm.py | 10 +++++----- src/main/python/unlocker.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 13e5236..fecfbff 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -27,7 +27,7 @@ CMD_VIAL_GET_SIZE = 0x01 CMD_VIAL_GET_DEFINITION = 0x02 CMD_VIAL_GET_ENCODER = 0x03 CMD_VIAL_SET_ENCODER = 0x04 -CMD_VIAL_GET_LOCK = 0x05 +CMD_VIAL_GET_UNLOCK_STATUS = 0x05 CMD_VIAL_UNLOCK_START = 0x06 CMD_VIAL_UNLOCK_POLL = 0x07 CMD_VIAL_LOCK = 0x08 @@ -295,14 +295,14 @@ class Keyboard: keyboard_id = data[4:12] return keyboard_id - def get_lock(self): - data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_LOCK)) + def get_unlock_status(self): + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_UNLOCK_STATUS)) return data[0] - def get_lock_keys(self): + def get_unlock_keys(self): """ Return keys users have to hold to unlock the keyboard as a list of rowcols """ - data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_LOCK)) + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_UNLOCK_STATUS)) rowcol = [] for x in range(15): row = data[2 + x * 2] diff --git a/src/main/python/unlocker.py b/src/main/python/unlocker.py index b11c72f..d692b31 100644 --- a/src/main/python/unlocker.py +++ b/src/main/python/unlocker.py @@ -48,7 +48,7 @@ class Unlocker(QWidget): self.keyboard_reference.set_keys(keyboard.keys, keyboard.encoders) # use "active" background to indicate keys to hold - lock_keys = keyboard.get_lock_keys() + lock_keys = keyboard.get_unlock_keys() for w in self.keyboard_reference.widgets: if (w.desc.row, w.desc.col) in lock_keys: w.setActive(True) @@ -59,8 +59,8 @@ class Unlocker(QWidget): def perform_unlock(self, keyboard): # if it's already unlocked, don't need to do anything - lock = keyboard.get_lock() - if lock == 0: + unlock = keyboard.get_unlock_status() + if unlock == 1: return self.update_reference(keyboard) From 5d5126dc381aad0626eff6b0a51e33f6b44c785f Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 29 Dec 2020 19:33:56 -0500 Subject: [PATCH 11/13] keyboard_comm: unlock keyboard before restoring macros --- src/main/python/keyboard_comm.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index fecfbff..178ee5f 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -6,6 +6,7 @@ import lzma from collections import OrderedDict from kle_serial import Serial as KleSerial +from unlocker import Unlocker from util import MSG_LEN, hid_send, chunks CMD_VIA_GET_KEYBOARD_VALUE = 0x02 @@ -283,7 +284,14 @@ class Keyboard: self.set_encoder(l, e, 1, encoder[1]) self.set_layout_options(data["layout_options"]) - self.set_macro(base64.b64decode(data["macro"])) + + # we need to unlock the keyboard before we can restore the macros, lock it afterwards + # only do that if the user actually has macros defined + macro = base64.b64decode(data["macro"]) + if macro: + Unlocker.get().perform_unlock(self) + self.set_macro(macro) + self.lock() def reset(self): self.usb_send(self.dev, struct.pack("B", 0xB)) From 86e4889deecfc32dc1d88405e80779b2d01f5505 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Tue, 29 Dec 2020 19:42:24 -0500 Subject: [PATCH 12/13] keyboard_comm: backwards compat with via for keyboard unlock --- src/main/python/keyboard_comm.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 178ee5f..b951578 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -55,7 +55,6 @@ class Keyboard: self.layout_options = -1 self.keys = [] self.encoders = [] - self.sideload = False self.macro_count = 0 self.macro_memory = 0 self.macro = b"" @@ -86,7 +85,6 @@ class Keyboard: if sideload_json is not None: payload = sideload_json - self.sideload = True else: # get keyboard identification data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_KEYBOARD_ID)) @@ -304,12 +302,20 @@ class Keyboard: return keyboard_id def get_unlock_status(self): + # VIA keyboards are always unlocked + if self.vial_protocol < 0: + return 1 + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_UNLOCK_STATUS)) return data[0] def get_unlock_keys(self): """ Return keys users have to hold to unlock the keyboard as a list of rowcols """ + # VIA keyboards don't have unlock keys + if self.vial_protocol < 0: + return [] + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_GET_UNLOCK_STATUS)) rowcol = [] for x in range(15): @@ -320,11 +326,20 @@ class Keyboard: return rowcol def unlock_start(self): + if self.vial_protocol < 0: + return + self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_UNLOCK_START)) def unlock_poll(self): + if self.vial_protocol < 0: + return b"" + data = self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_UNLOCK_POLL)) return data def lock(self): + if self.vial_protocol < 0: + return + self.usb_send(self.dev, struct.pack("BB", CMD_VIA_VIAL_PREFIX, CMD_VIAL_LOCK)) From c367e1d25c0a82a953b58bc9fefaa69ded6484a7 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Fri, 1 Jan 2021 04:28:55 -0500 Subject: [PATCH 13/13] firmware_flasher: ask bootloader for insecure mode on first boot --- src/main/python/firmware_flasher.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/python/firmware_flasher.py b/src/main/python/firmware_flasher.py index 42a4951..738a5b4 100644 --- a/src/main/python/firmware_flasher.py +++ b/src/main/python/firmware_flasher.py @@ -36,7 +36,7 @@ def send_retries(dev, data, retries=20): CHUNK = 64 -def cmd_flash(device, firmware, log_cb, progress_cb, complete_cb, error_cb): +def cmd_flash(device, firmware, enable_insecure, log_cb, progress_cb, complete_cb, error_cb): if firmware[0:8] != b"VIALFW00": return error_cb("Error: Invalid signature") @@ -92,6 +92,9 @@ def cmd_flash(device, firmware, log_cb, progress_cb, complete_cb, error_cb): # Reboot log_cb("Rebooting...") + # enable insecure mode on first boot in order to restore keymap/macros + if enable_insecure: + device.send(b"VC\x04") device.send(b"VC\x03") complete_cb("Done!") @@ -218,7 +221,8 @@ class FirmwareFlasher(BasicEditor): self.device = found threading.Thread(target=lambda: cmd_flash( - self.device, firmware, self.on_log, self.on_progress, self.on_complete, self.on_error)).start() + self.device, firmware, self.layout_restore is not None, + self.on_log, self.on_progress, self.on_complete, self.on_error)).start() def on_log(self, line): self.log_signal.emit(line)