Skip to content

Hide written chars in passphrase keyboard#6851

Merged
obrusvit merged 4 commits into
mainfrom
obrusvit/delizia/bolt/passphrase-hide-chars
May 2, 2026
Merged

Hide written chars in passphrase keyboard#6851
obrusvit merged 4 commits into
mainfrom
obrusvit/delizia/bolt/passphrase-hide-chars

Conversation

@obrusvit

@obrusvit obrusvit commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #3959 and #6342

Design:

Diff:

  • delizia
image - bolt image

Notes for QA

Properly test passphrase keyboard of Bolt (T2T1) and Delizia (T3T1). The characters should now disappear similarly to Eckhart (T3W1). A reveal passphrase functionality should also be available if you hold finger in the input area.

@obrusvit obrusvit self-assigned this Apr 29, 2026
Copilot AI review requested due to automatic review settings April 29, 2026 14:21
@obrusvit obrusvit added the T2T1 Trezor Model T label Apr 29, 2026
@obrusvit obrusvit added the T3T1 Trezor Safe 5 label Apr 29, 2026
@github-project-automation github-project-automation Bot moved this to 🔎 Needs review in Firmware Apr 29, 2026
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The changes add a passphrase display state machine (Hidden, LastOnly, LastWithMarker, Shown) across bolt, delizia, and eckhart keyboard components. Input now updates display_style and starts/stops a last-char timer for single vs. multi-tap commits; backspace/clear force Hidden. Touch-start on the input reveals a full-text overlay (Shown) that suppresses keypad/page/back/confirm actions until touch-end. Hidden-mode rendering now draws masked asterisks, optional last character and pending marker, and applies an overflow twitch. TextBox gains count() and last_char_str() helpers; minor theme/layout adjustments included.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Keypad
    participant Input
    participant Timer
    participant Renderer

    User->>Keypad: Press key
    Keypad->>Input: Deliver keystroke (single / multi-tap)
    Input->>Input: Update display_style (LastOnly / LastWithMarker)
    alt single-char
        Input->>Timer: Start LAST_DIGIT_TIMEOUT
    else multi-tap
        Input->>Timer: Stop (keep marker visible)
    end
    Input->>Renderer: Request repaint (masked + last/marker)
    Timer-->>Input: Timeout
    Input->>Input: Set display_style = Hidden
    Input->>Renderer: Request repaint (masked asterisks)

    User->>Input: TouchStart in reveal area
    Input->>Input: Set display_style = Shown
    Input->>Keypad: Suppress keypad/page/back/confirm interactions
    Input->>Renderer: Request repaint (overlay full text)

    User->>Input: TouchEnd
    Input->>Input: Set display_style = Hidden
    Input->>Keypad: Re-enable interactions
    Input->>Renderer: Request repaint (masked asterisks)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (1 warning, 2 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Most changes are in-scope (passphrase hiding, touch reveal, rendering), but the removal of public borders() function and unrelated Progress component area changes may have broader impact beyond the stated objective. Clarify whether borders() removal and Progress AREA changes are necessary for passphrase hiding or represent separate concerns that should be in a different PR.
Description check ❓ Inconclusive PR description includes issue references, design links, diffs, and QA instructions, but uses template comments without providing substantive summary of changes or implementation details. Add a clear summary section describing the key changes (hiding passphrase characters with reveal functionality) and implementation approach for better clarity.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Hide written chars in passphrase keyboard' clearly and concisely summarizes the main change: implementing character masking in the passphrase keyboard across multiple device families.
Linked Issues check ✅ Passed The changes fully implement the objective from #3959: hiding written characters in passphrase keyboard by replacing them with asterisks after a brief display (via display_style and timers) across Delizia and Bolt devices.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch obrusvit/delizia/bolt/passphrase-hide-chars

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Apr 29, 2026

Copy link
Copy Markdown

en main(all)

model device_test click_test persistence_test
T2T1 test(all) main(all) test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all) test(all) main(all)
Translations

cs main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

de main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

es main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

fr main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

pt main(all)

model device_test click_test
T2T1 test(all) main(all) test(all) main(all)
T3B1 test(all) main(all) test(all) main(all)
T3T1 test(all) main(all) test(all) main(all)
T3W1 test(all) main(all) test(all) main(all)

Latest CI run: 25258545777

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the on-device passphrase keyboards to improve privacy by masking entered characters (briefly showing the last character, plus touch-to-reveal behavior), aiming to unify behavior across device layouts/models.

Changes:

  • Implement masking/reveal logic for passphrase input on Delizia and Bolt (timer-based “show last char briefly”, hold-to-reveal full passphrase, disable keypad while revealed).
  • Adjust Eckhart passphrase touch-end handling to reliably hide revealed passphrase.
  • Minor Delizia UI/theme cleanup (remove unused borders() helper usage and related no-op insets; tweak keyboard icon color).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
core/src/apps/base.py Modifies handle_Ping button-protection branch (currently includes passphrase-request UI + commented debug code).
core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/passphrase.rs Tweaks touch-end behavior while passphrase is revealed.
core/embed/rust/src/ui/layout_delizia/ui_firmware.rs Removes Border wrapper (previously only applied zero insets).
core/embed/rust/src/ui/layout_delizia/theme/mod.rs Adjusts keyboard button icon color; removes borders() helper (was zero-insets).
core/embed/rust/src/ui/layout_delizia/component/progress.rs Removes no-op inset(theme::borders()) from progress area.
core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs Adds masking/reveal behavior (dots + last-char display with timer; touch-to-reveal overlay).
core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs Adds masking/reveal behavior (asterisks + last-char display with timer; touch-to-reveal overlay).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread core/src/apps/base.py Outdated
Comment thread core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs Outdated
Comment thread core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs Outdated
Comment thread core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs`:
- Around line 349-355: Input::event() can clear multi_tap.pending_key() when a
touch-reveal commits a pending key, but if the textbox is already at max_len the
early return triggered by self.input.display_style == DisplayStyle::Shown
prevents calling update_input_btns_state(ctx), leaving the previously pending
key visually enabled after the overlay hides; ensure that after calling
self.input.event(ctx, event) you always refresh keypad states — call
update_input_btns_state(ctx) when multi_tap.pending_key() was cleared or
whenever display_style transitions from Shown back to Hidden (i.e., after
self.input.event returns) so the button enable/disable rendering is consistent
with the internal pending_key state.

In `@core/src/apps/base.py`:
- Around line 443-462: The Ping handler incorrectly prompts for a passphrase
(request_passphrase_on_device) and discards it; revert to a confirmation flow
for Ping.button_protection instead. Replace the request_passphrase_on_device
call with a protected-call confirmation (e.g., import confirm_action from
trezor.ui.layouts and, if needed, ButtonRequestType as B, then await
confirm_action("ping", TR.words__confirm, "ping", br_code=B.ProtectCall) or
equivalent confirm API) and only proceed to return Success(message=msg.message)
after the confirmation completes; do not request or store a passphrase in this
handler.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 83465176-7dc9-4df5-ab9e-dfec97679883

📥 Commits

Reviewing files that changed from the base of the PR and between 993dbad and 27ae1e3.

📒 Files selected for processing (7)
  • core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs
  • core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs
  • core/embed/rust/src/ui/layout_delizia/component/progress.rs
  • core/embed/rust/src/ui/layout_delizia/theme/mod.rs
  • core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
  • core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/passphrase.rs
  • core/src/apps/base.py

Comment thread core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs Outdated
Comment thread core/src/apps/base.py
@obrusvit obrusvit requested a review from lapohoda April 30, 2026 08:30
@obrusvit obrusvit force-pushed the obrusvit/delizia/bolt/passphrase-hide-chars branch from e4ebe80 to 831d8ed Compare April 30, 2026 09:44
@obrusvit obrusvit linked an issue Apr 30, 2026 that may be closed by this pull request
@obrusvit obrusvit requested a review from romanz April 30, 2026 10:42
@romanz romanz added the translations Put this label on a PR to run tests in all languages label Apr 30, 2026
@obrusvit

Copy link
Copy Markdown
Contributor Author

Adding ff8bc99 per request of @lapohoda

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs (1)

287-293: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Refresh key states after touch-reveal commits a pending key.

If the passphrase is already at max_len, Input::event() clears the pending multi-tap key on reveal, but this path never recomputes the key states. After the overlay hides, the old pending key can still render as enabled even though the event loop now treats it as inactive.

Suggested fix
-        // Handle input touch events (reveal/hide passphrase)
-        self.input.event(ctx, event);
+        // Handle input touch events (reveal/hide passphrase)
+        let had_pending = self.input.inner().multi_tap.pending_key().is_some();
+        self.input.event(ctx, event);
+        if had_pending
+            && self.input.inner().multi_tap.pending_key().is_none()
+            && self.input.inner().textbox.len() >= self.max_len
+        {
+            self.update_input_btns_state(ctx);
+        }
 
         // When passphrase is shown in full, disable all keypad interaction
         if self.input.inner().display_style == DisplayStyle::Shown {
             return None;
         }

Also applies to: 533-540

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs` around
lines 287 - 293, After calling self.input.event(ctx, event) and hitting the
early-return when self.input.inner().display_style == DisplayStyle::Shown,
ensure you refresh the keypad key states so any pending multi-tap key cleared by
Input::event() is reflected in the UI; locate the same pattern around lines
533-540 and add a call to the existing key-state recomputation routine (e.g.,
the method that computes/enables keys on the keypad, such as
recompute_keys/update_keys_state/update_key_states) before returning None so the
overlay hide does not leave an enabled-but-inactive key rendered.
core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs (1)

349-355: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Recompute key states after touch-reveal clears the pending key.

When the textbox is already full, Input::event() can clear multi_tap.pending_key() on reveal, but this path never calls update_input_btns_state(ctx). The previously pending key can then stay rendered as enabled after the overlay hides even though it is no longer actionable.

Suggested fix
-        // Handle input touch events (reveal/hide passphrase)
-        self.input.event(ctx, event);
+        // Handle input touch events (reveal/hide passphrase)
+        let had_pending = self.input.multi_tap.pending_key().is_some();
+        self.input.event(ctx, event);
+        if had_pending
+            && self.input.multi_tap.pending_key().is_none()
+            && self.input.textbox.len() >= self.max_len
+        {
+            self.update_input_btns_state(ctx);
+        }
 
         // When passphrase is shown in full, disable all keypad interaction
         if self.input.display_style == DisplayStyle::Shown {
             return None;
         }

Also applies to: 629-643

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs`
around lines 349 - 355, Input::event can clear multi_tap.pending_key() during a
touch-reveal, but when the textbox is full the code returns early on
DisplayStyle::Shown without recomputing keypad state; call
update_input_btns_state(ctx) after self.input.event(ctx, event) and before
returning when self.input.display_style == DisplayStyle::Shown so any cleared
pending_key() is reflected in the buttons (apply the same fix in the other
occurrence around lines 629-643). Ensure you reference the DisplayStyle::Shown
branch, Input::event, update_input_btns_state(ctx), and multi_tap.pending_key()
when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs`:
- Around line 287-293: After calling self.input.event(ctx, event) and hitting
the early-return when self.input.inner().display_style == DisplayStyle::Shown,
ensure you refresh the keypad key states so any pending multi-tap key cleared by
Input::event() is reflected in the UI; locate the same pattern around lines
533-540 and add a call to the existing key-state recomputation routine (e.g.,
the method that computes/enables keys on the keypad, such as
recompute_keys/update_keys_state/update_key_states) before returning None so the
overlay hide does not leave an enabled-but-inactive key rendered.

In `@core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs`:
- Around line 349-355: Input::event can clear multi_tap.pending_key() during a
touch-reveal, but when the textbox is full the code returns early on
DisplayStyle::Shown without recomputing keypad state; call
update_input_btns_state(ctx) after self.input.event(ctx, event) and before
returning when self.input.display_style == DisplayStyle::Shown so any cleared
pending_key() is reflected in the buttons (apply the same fix in the other
occurrence around lines 629-643). Ensure you reference the DisplayStyle::Shown
branch, Input::event, update_input_btns_state(ctx), and multi_tap.pending_key()
when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a868ec19-904b-48ae-8a5b-e1c8461f8ec8

📥 Commits

Reviewing files that changed from the base of the PR and between 831d8ed and 7c0872b.

📒 Files selected for processing (4)
  • core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs
  • core/embed/rust/src/ui/layout_delizia/component/keyboard/passphrase.rs
  • core/embed/rust/src/ui/layout_eckhart/firmware/keyboard/passphrase.rs
  • tests/ui_tests/fixtures.json

Comment thread core/embed/rust/src/ui/layout_bolt/component/keyboard/passphrase.rs
@romanz

romanz commented May 2, 2026

Copy link
Copy Markdown
Contributor

Tested on T2T1 and T3T1.

When entering 50 characters' passphrase on T2T1, if the last button pressed has more than one character, it is not disabled.

Last button pressed is jkl:

Screenshot from 2026-05-02 10-20-35

Last button pressed is a digit:

Screenshot from 2026-05-02 10-21-01

@obrusvit

obrusvit commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

@romanz thanks for review. I fixed the last key disabling here: f65b949

One more thing, on the physical model T, the "reveal passphrase" functionality is difficult to trigger due to the display shape. So this commit 81f89e6 expands the area to reveal passphrase the whole area above the keyboard (i.e. input field and scrollbar showing the keyboard type shown). This shrinks the area for swipe detector by a little bit but that's not an issue.

@romanz romanz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

obrusvit added 4 commits May 2, 2026 20:13
- hide already written characters in the passphrase keyboard in Delizia
  and Bolt
- holding a finger in the input field reveals the whole passphrase in a
potentially multi-line manner
- update fixtures
- this commit changes all touch display passphrase keyboards to allow
persisting the reveal mode until the user lifts the finger off the
screen anywhere, not just within the extended input shown area

[no changelog]
@obrusvit obrusvit force-pushed the obrusvit/delizia/bolt/passphrase-hide-chars branch from 81f89e6 to f4f0831 Compare May 2, 2026 18:14
@obrusvit obrusvit merged commit ca60997 into main May 2, 2026
166 checks passed
@obrusvit obrusvit deleted the obrusvit/delizia/bolt/passphrase-hide-chars branch May 2, 2026 18:55
@trezor-bot trezor-bot Bot moved this from 🔎 Needs review to 🤝 Needs QA in Firmware May 2, 2026
@Thalarion Thalarion moved this from 🤝 Needs QA to 🧪 QA In progress in Firmware May 21, 2026
@Thalarion Thalarion self-assigned this May 21, 2026
@Thalarion

Thalarion commented May 22, 2026

Copy link
Copy Markdown
Contributor

on T2T1

  • passphrase characters hide behind * after some time ✅
  • passphrase can be revealed by touching the general area of the characters ✅
    • even full length passphrase is revealed whole ✅
  • last button pressed is disabled
  • passphrase feature works in general ✅

on T3T1

  • passphrase characters hide behind after some time ✅
  • passphrase can be revealed by touching the general area of the characters ✅
    • even full length passphrase is revealed whole ✅
  • last button pressed is Hide written chars in passphrase keyboard #6851 (comment)
  • passphrase feature works in general ✅

@Thalarion Thalarion moved this from 🧪 QA In progress to ✅ Approved by QA in Firmware May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T2T1 Trezor Model T T3T1 Trezor Safe 5 translations Put this label on a PR to run tests in all languages

Projects

Status: ✅ Approved by QA

Development

Successfully merging this pull request may close these issues.

T2T1 & T3T1 passphrase characters don't disappear Unify the behaviour of entering PIN/Passphrase on the device

5 participants