diff --git a/src/krux/krux_settings.py b/src/krux/krux_settings.py index b10719f74..60e5fd6e8 100644 --- a/src/krux/krux_settings.py +++ b/src/krux/krux_settings.py @@ -51,6 +51,10 @@ DEFAULT_LOCALE = "en-US" +TAMPER_CHECK_INPUT_SCAN_QR = "Scan QR" +TAMPER_CHECK_INPUT_MANUAL = "Manual" +TAMPER_CHECK_INPUT_ASK_EVERY_TIME = "Ask every time" + DEFAULT_TX_PIN = ( board.config["board_info"]["CONNEXT_A"] if "CONNEXT_A" in board.config["board_info"] @@ -475,6 +479,15 @@ class SecuritySettings(SettingsNamespace): auto_shutdown = NumberSetting(int, "auto_shutdown", 10, [0, 60]) hide_mnemonic = CategorySetting("hide_mnemonic", False, [False, True]) boot_flash_hash = CategorySetting("boot_flash_hash", False, [False, True]) + tamper_check_code_input_mode = CategorySetting( + "tamper_check_code_input_mode", + TAMPER_CHECK_INPUT_ASK_EVERY_TIME, + [ + TAMPER_CHECK_INPUT_SCAN_QR, + TAMPER_CHECK_INPUT_MANUAL, + TAMPER_CHECK_INPUT_ASK_EVERY_TIME, + ], + ) def label(self, attr): """Returns a label for UI when given a setting name or namespace""" @@ -482,6 +495,7 @@ def label(self, attr): "auto_shutdown": t("Shutdown Time"), "hide_mnemonic": t("Hide Mnemonics"), "boot_flash_hash": t("TC Flash Hash at Boot"), + "tamper_check_code_input_mode": t("TC Input Mode"), }[attr] diff --git a/src/krux/pages/settings_page.py b/src/krux/pages/settings_page.py index 16045a923..9688623e5 100644 --- a/src/krux/pages/settings_page.py +++ b/src/krux/pages/settings_page.py @@ -260,15 +260,36 @@ def handler(): ) for ns in namespace_list ] - items.extend( - [ - ( - settings_namespace.label(setting.attr), - self.setting(settings_namespace, setting), - ) - for setting in setting_list - ] - ) + if settings_namespace.namespace == "settings.security": + regular_settings = [] + tc_input_item = None + for setting in setting_list: + if setting.attr == "tamper_check_code_input_mode": + tc_input_item = ( + settings_namespace.label(setting.attr), + self.setting(settings_namespace, setting), + ) + else: + regular_settings.append( + ( + settings_namespace.label(setting.attr), + self.setting(settings_namespace, setting), + ) + ) + items.extend(regular_settings) + items.append((t("Tamper Check Code"), self.enter_modify_tc_code)) + if tc_input_item: + items.append(tc_input_item) + else: + items.extend( + [ + ( + settings_namespace.label(setting.attr), + self.setting(settings_namespace, setting), + ) + for setting in setting_list + ] + ) # If there is only one item in the namespace, don't show a submenu # and instead jump straight to the item's menu @@ -281,10 +302,6 @@ def handler(): items.append((t("Factory Settings"), self.restore_settings)) back_status = self._settings_exit_check - # Case for security settings - if settings_namespace.namespace == "settings.security": - items.append((t("Tamper Check Code"), self.enter_modify_tc_code)) - submenu = Menu(self.ctx, items, back_status=back_status) index, status = submenu.run_loop() if index == submenu.back_index: diff --git a/src/krux/pages/tc_code_verification.py b/src/krux/pages/tc_code_verification.py index 00eab5b61..b89b60f86 100644 --- a/src/krux/pages/tc_code_verification.py +++ b/src/krux/pages/tc_code_verification.py @@ -27,8 +27,17 @@ UPPERCASE_LETTERS, NUM_SPECIAL_1, NUM_SPECIAL_2, + Menu, +) +from ..krux_settings import ( + t, + TC_CODE_PATH, + TC_CODE_PBKDF2_ITERATIONS, + Settings, + TAMPER_CHECK_INPUT_SCAN_QR, + TAMPER_CHECK_INPUT_MANUAL, + TAMPER_CHECK_INPUT_ASK_EVERY_TIME, ) -from ..krux_settings import t, TC_CODE_PATH, TC_CODE_PBKDF2_ITERATIONS class TCCodeVerification(Page): @@ -43,15 +52,16 @@ def capture(self, changing_tc_code=False, return_hash=False): import uhashlib_hw from machine import unique_id - label = ( - t("Current Tamper Check Code") - if changing_tc_code - else t("Tamper Check Code") - ) - tc_code = self.capture_from_keypad( - label, [NUM_SPECIAL_1, LETTERS, UPPERCASE_LETTERS, NUM_SPECIAL_2] - ) - if tc_code == ESC_KEY: + method = self._select_input_method(changing_tc_code) + if method is None: + return False + + if method == "qr": + tc_code = self._capture_from_qr() + else: + tc_code = self._capture_from_keypad(changing_tc_code) + + if not tc_code: return False # Hashes the tamper check code tc_code_bytes = tc_code.encode() @@ -76,3 +86,68 @@ def capture(self, changing_tc_code=False, return_hash=False): self.flash_error(t("Invalid Tamper Check Code")) return False + + def _select_input_method(self, changing_tc_code): + if changing_tc_code: + return "manual" + + mode = Settings().security.tamper_check_code_input_mode + if mode == TAMPER_CHECK_INPUT_ASK_EVERY_TIME: + return self._prompt_input_method() + if mode == TAMPER_CHECK_INPUT_SCAN_QR: + return "qr" + return "manual" + + def _prompt_input_method(self): + menu = Menu( + self.ctx, + [ + (t("Scan QR"), lambda: "qr"), + (t("Manual"), lambda: "manual"), + ], + back_label=None, + ) + _, status = menu.run_loop() + if status == ESC_KEY: + return None + return status + + def _capture_from_keypad(self, changing_tc_code): + label = ( + t("Current Tamper Check Code") + if changing_tc_code + else t("Tamper Check Code") + ) + tc_code = self.capture_from_keypad( + label, [NUM_SPECIAL_1, LETTERS, UPPERCASE_LETTERS, NUM_SPECIAL_2] + ) + if tc_code == ESC_KEY: + return False + return tc_code + + def _capture_from_qr(self): + from .qr_capture import QRCodeCapture + + self.flash_text(t("Point camera at TC Code QR")) + qr_capture = QRCodeCapture(self.ctx) + data, _ = qr_capture.qr_capture_loop() + tc_code = self._sanitize_tc_code(data) + if tc_code: + return tc_code + return False + + def _sanitize_tc_code(self, tc_code): + if tc_code is None: + return None + if isinstance(tc_code, bytes): + try: + tc_code = tc_code.decode() + except: + return None + if not isinstance(tc_code, str): + return None + + cleaned = "".join(tc_code.splitlines()).strip() + if cleaned == "": + return None + return cleaned