diff --git a/README.md b/README.md index 3698daff..6b9c027f 100644 --- a/README.md +++ b/README.md @@ -457,6 +457,14 @@ $ octez-client list connected ledgers ## How the app works +### Screen saver + +The screen saver is the one provided by Ledger ([Configure screen saver timeout](https://support.ledger.com/hc/en-us/articles/360017152034-Configure-PIN-lock-and-screen-saver?docs=true)). + +On Nanos devices, the Ledger screensaver can slow down the baking app. This is why it is deactivated during signings. +After a signature, a low-cost screensaver will take over. It will switch the screen off after 20 seconds of inactivity. +Press any button to exit sleep mode. When the sleep mode is exited, the Ledger screen saver will take over again if there are no more signatures. + ### High water mark (HWM) To avoid double baking, double attestation and double pre-attestation, the application maintains a high water mark (HWM) corresponding to the last level/round encountered during signature requests. The HWMs are displayed on the home screen and are updated after each signature. diff --git a/src/apdu_sign.c b/src/apdu_sign.c index 76121c90..13f13867 100644 --- a/src/apdu_sign.c +++ b/src/apdu_sign.c @@ -208,6 +208,10 @@ static int baking_sign_complete(bool const send_hash) { case MAGIC_BYTE_PREATTESTATION: case MAGIC_BYTE_ATTESTATION: TZ_CHECK(guard_baking_authorized(&G.parsed_baking_data, &global.path_with_curve)); +#ifdef TARGET_NANOS + // To be efficient, the signing needs a low-cost display + ux_set_low_cost_display_mode(true); +#endif result = perform_signature(send_hash); #ifdef HAVE_BAGL // Ignore calculation errors diff --git a/src/globals.h b/src/globals.h index 8b707f7b..79ecb598 100644 --- a/src/globals.h +++ b/src/globals.h @@ -28,6 +28,7 @@ #include "operations.h" #include "ui.h" +#include "ui_screensaver.h" /** * @brief Zeros out all globals that can keep track of APDU instruction state @@ -118,8 +119,12 @@ typedef struct { ui_callback_t ok_callback; /// Callback function if user rejected prompt. ui_callback_t cxl_callback; - /// Screensaver is on/off. - bool is_blank_screen; +#ifdef HAVE_BAGL + /// If the low-cost display mode is enabled + bool low_cost_display_mode; + /// Screensaver context + ux_screensaver_state_t screensaver_state; +#endif // TARGET_NANOS } dynamic_display; bip32_path_with_curve_t path_with_curve; ///< holds the bip32 path and curve of the current key diff --git a/src/ui.h b/src/ui.h index 2401b04f..87e2227b 100644 --- a/src/ui.h +++ b/src/ui.h @@ -42,6 +42,17 @@ void __attribute__((noreturn)) app_exit(void); #ifdef HAVE_BAGL +#ifdef TARGET_NANOS +/** + * @brief Sets low-cost display mode + * + * Low-cost display stop handling `TICKER_EVENT` + * + * @param enable: if enable the mode or not + */ +void ux_set_low_cost_display_mode(bool enable); +#endif // HAVE_BAGL + /** * @brief Calculates the chain id for the idle screens * diff --git a/src/ui_bagl.c b/src/ui_bagl.c index 62dd9f21..5f67bbbd 100644 --- a/src/ui_bagl.c +++ b/src/ui_bagl.c @@ -32,12 +32,88 @@ #include "memory.h" #include "os_cx.h" // ui-menu #include "to_string.h" +#include "ui_screensaver.h" #include #include #define G_display global.dynamic_display +#ifdef TARGET_NANOS +#include "io.h" + +void ux_set_low_cost_display_mode(bool enable) { + if (G_display.low_cost_display_mode != enable) { + G_display.low_cost_display_mode = enable; + if (G_display.low_cost_display_mode) { + ux_screensaver_start_clock(); + } else { + ux_screensaver_stop_clock(); + } + } +} + +uint8_t io_event(uint8_t channel); + +/** + * Function similar to the one in `lib_standard_app/` except that the + * `TICKER_EVENT` handling is not enabled on low-cost display mode. + * + * Low-cost display mode is deactivated when the button is pressed and + * activated during signing because `TICKER_EVENT` handling slows down + * the application. + * + */ +uint8_t io_event(uint8_t channel) { + (void) channel; + + switch (G_io_seproxyhal_spi_buffer[0]) { + case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: + ux_set_low_cost_display_mode(false); + UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer); + break; + case SEPROXYHAL_TAG_STATUS_EVENT: + if ((G_io_apdu_media == IO_APDU_MEDIA_USB_HID) && + !(U4BE(G_io_seproxyhal_spi_buffer, 3) & + SEPROXYHAL_TAG_STATUS_EVENT_FLAG_USB_POWERED)) { + THROW(EXCEPTION_IO_RESET); + } + __attribute__((fallthrough)); + case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT: +#ifdef HAVE_BAGL + UX_DISPLAYED_EVENT({}); +#endif // HAVE_BAGL +#ifdef HAVE_NBGL + UX_DEFAULT_EVENT(); +#endif // HAVE_NBGL + break; +#ifdef HAVE_NBGL + case SEPROXYHAL_TAG_FINGER_EVENT: + UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer); + break; +#endif // HAVE_NBGL + case SEPROXYHAL_TAG_TICKER_EVENT: + if (!G_display.low_cost_display_mode) { + app_ticker_event_callback(); + UX_TICKER_EVENT(G_io_seproxyhal_spi_buffer, {}); + } else { + ux_screensaver_apply_tick(); + } + break; + default: + UX_DEFAULT_EVENT(); + break; + } + + if (!io_seproxyhal_spi_is_status_sent()) { + io_seproxyhal_general_status(); + } + + return 1; +} + +#endif // TARGET_NANOS + static void ui_refresh_idle_hwm_screen(void); /** @@ -68,7 +144,11 @@ void ui_menu_init(void); ///> Load main menu page * - Exit screen * */ +#ifdef TARGET_NANOS +UX_STEP_CB(ux_app_is_ready_step, nn, ui_start_screensaver(), {"Application", "is ready"}); +#else // TARGET_NANOS UX_STEP_NOCB(ux_app_is_ready_step, nn, {"Application", "is ready"}); +#endif // TARGET_NANOS UX_STEP_NOCB(ux_version_step, bnnn_paging, {"Tezos Baking", APPVERSION}); UX_STEP_NOCB(ux_chain_id_step, bnnn_paging, {"Chain", home_context.chain_id}); UX_STEP_NOCB(ux_authorized_key_step, bnnn_paging, {"Public Key Hash", home_context.authorized_key}); diff --git a/src/ui_screensaver.c b/src/ui_screensaver.c new file mode 100644 index 00000000..211f27ee --- /dev/null +++ b/src/ui_screensaver.c @@ -0,0 +1,121 @@ +/* Tezos Ledger application - Screen-saver UI functions + + Copyright 2024 TriliTech + Copyright 2024 Functori + Copyright 2023 Ledger + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +#ifdef HAVE_BAGL +#include "ui_screensaver.h" + +#include "bolos_target.h" +#include "globals.h" + +#define G_screensaver_state global.dynamic_display.screensaver_state + +/** + * @brief Blank screen element + * + */ +static const bagl_element_t blank_screen_elements[] = {{{BAGL_RECTANGLE, + BAGL_NONE, + 0, + 0, + BAGL_WIDTH, + BAGL_HEIGHT, + 0, + 0, + BAGL_FILL, + 0x000000, + 0xFFFFFF, + 0, + 0}, + .text = NULL}, + {}}; + +/** + * @brief This structure represents the parameters needed for the blank layout + * + * No parameters required + * + */ +typedef struct ux_layout_blank_params_s { +} ux_layout_blank_params_t; + +/** + * @brief Initializes the blank layout + * + * @param stack_slot: index stack_slot + */ +static void ux_layout_blank_init(unsigned int stack_slot) { + ux_stack_init(stack_slot); + G_ux.stack[stack_slot].element_arrays[0].element_array = blank_screen_elements; + G_ux.stack[stack_slot].element_arrays[0].element_array_count = ARRAYLEN(blank_screen_elements); + G_ux.stack[stack_slot].element_arrays_count = 1; + G_ux.stack[stack_slot].button_push_callback = ux_flow_button_callback; + ux_stack_display(stack_slot); +} + +/** + * @brief Exits the blank screen to home screen + * + */ +static void return_to_idle(void) { + G_screensaver_state.on = false; + ui_initial_screen(); +} + +/** + * @brief Blank screen flow + * + * On any click: return_to_idle + * + */ +UX_STEP_CB(blank_screen_step, blank, return_to_idle(), {}); +UX_STEP_INIT(blank_screen_border, NULL, NULL, { return_to_idle(); }); +UX_FLOW(ux_blank_flow, &blank_screen_step, &blank_screen_border, FLOW_LOOP); + +void ui_start_screensaver(void) { + if (!G_screensaver_state.on) { + G_screensaver_state.on = true; + ux_flow_init(0, ux_blank_flow, NULL); + } +} + +#define MS 100u +#define SCREENSAVER_TIMEOUT 20000u // 20u * 10u * MS + +void ux_screensaver_start_clock(void) { + G_screensaver_state.clock.on = true; + G_screensaver_state.clock.timeout = SCREENSAVER_TIMEOUT; +} + +void ux_screensaver_stop_clock(void) { + G_screensaver_state.clock.on = false; +} + +void ux_screensaver_apply_tick(void) { + if (G_screensaver_state.clock.on) { + if (G_screensaver_state.clock.timeout < MS) { + ui_start_screensaver(); + ux_screensaver_stop_clock(); + } else { + G_screensaver_state.clock.timeout -= MS; + } + } +} + +#endif diff --git a/src/ui_screensaver.h b/src/ui_screensaver.h new file mode 100644 index 00000000..48c10f4b --- /dev/null +++ b/src/ui_screensaver.h @@ -0,0 +1,70 @@ +/* Tezos Ledger application - Screen-saver UI functions + + Copyright 2024 TriliTech + Copyright 2024 Functori + Copyright 2023 Ledger + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ +#pragma once + +#include + +#ifdef HAVE_BAGL + +typedef struct { + /// Clock has started + bool on; + /// Timeout out before saving screen + unsigned int timeout; +} ux_screensaver_clock_t; + +typedef struct { + /// Screensaver is on/off. + bool on; + /// Timeout out before saving screen + ux_screensaver_clock_t clock; +} ux_screensaver_state_t; + +/** + * @brief Empties the screen + * + * Waits a click to return to home screen + * + * Applies only for Nanos devices + * + */ +void ui_start_screensaver(void); + +/** + * @brief Start a timeout before saving screen + * + */ +void ux_screensaver_start_clock(void); + +/** + * @brief Stop the clock + * + */ +void ux_screensaver_stop_clock(void); + +/** + * @brief Apply one tick to the clock + * + * The tick is assumed to be 100 ms + * + */ +void ux_screensaver_apply_tick(void); + +#endif diff --git a/test/snapshots/nanos/test_automatic_low_cost_screensaver/black.png b/test/snapshots/nanos/test_automatic_low_cost_screensaver/black.png new file mode 100644 index 00000000..34037194 Binary files /dev/null and b/test/snapshots/nanos/test_automatic_low_cost_screensaver/black.png differ diff --git a/test/snapshots/nanos/test_automatic_low_cost_screensaver/home_screen.png b/test/snapshots/nanos/test_automatic_low_cost_screensaver/home_screen.png new file mode 100644 index 00000000..ce795f34 Binary files /dev/null and b/test/snapshots/nanos/test_automatic_low_cost_screensaver/home_screen.png differ diff --git a/test/snapshots/nanos/test_low_cost_screensaver/black.png b/test/snapshots/nanos/test_low_cost_screensaver/black.png new file mode 100644 index 00000000..34037194 Binary files /dev/null and b/test/snapshots/nanos/test_low_cost_screensaver/black.png differ diff --git a/test/snapshots/nanos/test_low_cost_screensaver/home_screen.png b/test/snapshots/nanos/test_low_cost_screensaver/home_screen.png new file mode 100644 index 00000000..ce795f34 Binary files /dev/null and b/test/snapshots/nanos/test_low_cost_screensaver/home_screen.png differ diff --git a/test/test_instructions.py b/test/test_instructions.py index 31470863..385d3dee 100644 --- a/test/test_instructions.py +++ b/test/test_instructions.py @@ -189,6 +189,77 @@ def test_review_home(account: Optional[Account], tezos_navigator.assert_screen("home_screen", snap_path) +def test_low_cost_screensaver(firmware: Firmware, + backend: BackendInterface, + tezos_navigator: TezosNavigator) -> None: + """Test if the low-cost screensaver work as intended.""" + + if firmware.name != "nanos": + pytest.skip("Only on nanos devices") + + all_click = [ + backend.both_click, + backend.left_click, + backend.right_click, + ] + tezos_navigator.assert_screen("home_screen") + for click in all_click: + backend.both_click() + backend.wait_for_screen_change() + tezos_navigator.assert_screen("black") + click() + backend.wait_for_screen_change() + tezos_navigator.assert_screen("home_screen") + +def test_automatic_low_cost_screensaver(firmware: Firmware, + backend: BackendInterface, + client: TezosClient, + tezos_navigator: TezosNavigator) -> None: + """Test the low-cost screensaver activate at sign.""" + + if firmware.name != "nanos": + pytest.skip("Only on nanos devices") + + account = DEFAULT_ACCOUNT + + tezos_navigator.authorize_baking(account) + + tezos_navigator.assert_screen("home_screen") + + time.sleep(30) + + # Low-cost screensaver activate only after signing + tezos_navigator.assert_screen("home_screen") + + attestation = build_attestation( + op_level=1, + op_round=0, + chain_id=DEFAULT_CHAIN_ID + ) + + client.sign_message(account, attestation) + + time.sleep(5) + + # Low-cost screensaver activate after 20s after signing + tezos_navigator.assert_screen("home_screen") + + time.sleep(30) + + # Low-cost screensaver has been activated + backend.wait_for_screen_change() + tezos_navigator.assert_screen("black") + + backend.both_click() + backend.wait_for_screen_change() + tezos_navigator.assert_screen("home_screen") + + time.sleep(30) + + # Low-cost screensaver desactivate after button push + tezos_navigator.assert_screen("home_screen") + + def test_version(client: TezosClient) -> None: """Test the VERSION instruction.""" @@ -210,12 +281,21 @@ def test_git(client: TezosClient) -> None: f"Expected {expected_commit} but got {commit}" -def test_ledger_screensaver(client: TezosClient, tezos_navigator: TezosNavigator, backend_name) -> None: +def test_ledger_screensaver(firmware: Firmware, + backend: BackendInterface, + client: TezosClient, + tezos_navigator: TezosNavigator, + backend_name) -> None: # Make sure that ledger device being tested has screensaver time set to 1 minute and PIN lock is disabled. account = DEFAULT_ACCOUNT if backend_name == "speculos": - assert True - return + assert True + return + + time.sleep(70) + + res = input("Has the Ledger screensaver been activated?") + assert (res.find("y") != -1), "Ledger screensaver should have activated" lvl = 0 main_chain_id = DEFAULT_CHAIN_ID @@ -238,6 +318,12 @@ def test_ledger_screensaver(client: TezosClient, tezos_navigator: TezosNavigator client.sign_message(account, attestation) time.sleep(1) + res = input("Has the Ledger screensaver been activated?") + if firmware.device == "nanos": + assert (res.find("y") == -1), "Ledger screensaver should not have been activated" + else: + assert (res.find("y") != -1), "Ledger screensaver should have activated" + @pytest.mark.parametrize("account", ZEBRA_ACCOUNTS) def test_benchmark_attestation_time(account: Account, client: TezosClient, tezos_navigator: TezosNavigator, backend_name) -> None: