fix(core): device should be lockable when SD protection is on
What changed, and why it matters
This update fixes a bug where a Trezor hardware wallet with SD-card protection enabled could not be locked if no PIN was set. The device should be lockable whenever SD protection is active, because the SD card itself acts as a security factor. The change makes the homescreen use a broader 'can lock' check instead of only checking whether a PIN is configured. A new automated test also verifies that pressing and holding the screen can lock the device in this configuration.
Treat as a low-to-moderate security fix. Users relying on SD protection without a PIN should install the updated firmware so the device can actually lock. Review whether any other UI flows still use config.has_pin() as a proxy for lockability and consolidate on can_lock_device().
Security signals we found
Access-control logic flaw: lock UI gated on PIN presence instead of actual lock capability
Fix aligns lockability predicate with existing can_lock_device() helper
SD-protect-without-PIN scenario now covered by automated tests
No changelog entry (commit message says [no changelog])
Evidence from the diff
In core/src/apps/homescreen/init.py the homescreen’s lockable flag is changed from config.has_pin() to can_lock_device(). The can_lock_device() helper (already used by the lockscreen) presumably returns true when either a PIN or SD protection is active, so the lock gesture/UI is now offered for SD-protected devices even without a PIN. The test file test_sdcard.py adds a press_lock helper and parametrizes test_sd_protect_lock over session_lock, auto_lock, and press_lock, including cases where the PIN is later removed while SD protection remains on. UI fixture hashes are updated for the new press_lock test case across languages and models.
Changed components
core/src/apps/homescreen/__init__.pytests/device_tests/test_sdcard.pytests/ui_tests/fixtures.jsonInspect captured patch +39 / −19
diff --git a/core/src/apps/homescreen/__init__.py b/core/src/apps/homescreen/__init__.py
index 4c19e260..bb9147cc 100644
--- a/core/src/apps/homescreen/__init__.py
+++ b/core/src/apps/homescreen/__init__.py
@@ -10,7 +10,7 @@ from trezorui_api import NotificationLevel
from apps.base import busy_expiry_ms
from apps.common.authorization import is_set_any_session
-from apps.common.lock_manager import lock_device
+from apps.common.lock_manager import can_lock_device, lock_device
async def busyscreen() -> None:
@@ -66,7 +66,7 @@ async def homescreen() -> None:
)
res = await run_homescreen(
- label=label, notification=notification, lockable=config.has_pin()
+ label=label, notification=notification, lockable=can_lock_device()
)
if utils.INTERNAL_MODEL == "T3W1":
@@ -78,7 +78,7 @@ async def homescreen() -> None:
async def _lockscreen(screensaver: bool = False) -> None:
- from apps.common.lock_manager import can_lock_device, unlock_device
+ from apps.common.lock_manager import unlock_device
# Only show the lockscreen UI if the device can in fact be locked, or if it is
# and OLED device (in which case the lockscreen is a screensaver).
diff --git a/tests/device_tests/test_sdcard.py b/tests/device_tests/test_sdcard.py
index 495d14c6..84caf0ce 100644
--- a/tests/device_tests/test_sdcard.py
+++ b/tests/device_tests/test_sdcard.py
@@ -15,6 +15,7 @@
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.
import time
+import typing as t
import pytest
@@ -134,13 +135,29 @@ def test_sd_protect_unlock(session: Session):
assert e.value.code == messages.FailureType.ProcessError
+def session_lock(session: Session) -> None:
+ session.lock()
+
+
+def auto_lock(session: Session) -> None:
+ time.sleep(10.5)
+ session.refresh_features()
+
+
+def press_lock(session: Session) -> None:
+ buttons = session.debug.screen_buttons
+ center = (buttons._width() // 2, buttons._height() // 2)
+ session.debug.click(center, hold_ms=3500)
+ session.refresh_features()
+
+
@pytest.mark.sd_card
@pytest.mark.setup_client(pin=PIN)
@pytest.mark.parametrize(
- "autolock",
- [pytest.param(False, id="session_lock"), pytest.param(True, id="auto_lock")],
+ "lock_func",
+ [pytest.param(fn, id=fn.__name__) for fn in (session_lock, auto_lock, press_lock)],
)
-def test_sd_protect_lock(session: Session, autolock: bool):
+def test_sd_protect_lock(session: Session, lock_func: "t.Callable[[Session], None]"):
layout = session.debug.read_layout
assert "Lockscreen" in layout().all_components()
@@ -163,22 +180,13 @@ def test_sd_protect_lock(session: Session, autolock: bool):
)
device.sd_protect(session, Op.ENABLE)
- if autolock:
+ if lock_func is auto_lock:
device.apply_settings(session, auto_lock_delay_ms=10 * 1000)
- def lock_func():
- time.sleep(10.5)
- session.refresh_features()
-
- else:
-
- def lock_func():
- session.lock() # features are auto-refreshed
-
assert session.features.pin_protection is True
assert session.features.sd_protection is True
assert session.features.unlocked is True
- lock_func()
+ lock_func(session)
assert "Lockscreen" in layout().all_components()
assert session.features.pin_protection is True
assert session.features.sd_protection is None
@@ -202,7 +210,7 @@ def test_sd_protect_lock(session: Session, autolock: bool):
assert session.features.pin_protection is False
assert session.features.sd_protection is True
assert session.features.unlocked is True
- lock_func()
+ lock_func(session)
assert "Lockscreen" in layout().all_components()
assert session.features.pin_protection is False
assert session.features.sd_protection is None
@@ -226,7 +234,7 @@ def test_sd_protect_lock(session: Session, autolock: bool):
assert session.features.pin_protection is True
assert session.features.sd_protection is True
assert session.features.unlocked is True
- lock_func()
+ lock_func(session)
assert "Lockscreen" in layout().all_components()
assert session.features.pin_protection is True
assert session.features.sd_protection is None
diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json
index 633bb5df..fedffa7d 100644
--- a/tests/ui_tests/fixtures.json
+++ b/tests/ui_tests/fixtures.json
@@ -2953,6 +2953,7 @@
"T2T1_cs_test_sdcard.py::test_sd_format": "11b1d8d8e573bb4df9819049bc30b4a6cbc1a546729d84e9f6d7da87f73992ff",
"T2T1_cs_test_sdcard.py::test_sd_no_format": "9d404b1bac13877e95e52a06a42b892a97880a719dda0bff688f795106d28f71",
"T2T1_cs_test_sdcard.py::test_sd_protect_lock[auto_lock]": "8ee3c079714e542e095fbac320dfbd643ef943e10b43cfe9c7d2ec2d2ebd6080",
+"T2T1_cs_test_sdcard.py::test_sd_protect_lock[press_lock]": "4f3532d679a8245fc6724d09c4950f08ae2207db900701bb040b0a09b84c81b4",
"T2T1_cs_test_sdcard.py::test_sd_protect_lock[session_lock]": "221fc8f59b11ea6e420ec3c3998ddbb4f0ef29ae5ebd1630d504c594b2889e98",
"T2T1_cs_test_sdcard.py::test_sd_protect_unlock": "1a1f02b5f8ee6151940b3944ab7b540ce9ecdf37308e9be707361724e54fcd27",
"T2T1_cs_test_session.py::test_cannot_resume_ended_session": "f2b80196c81ce473b7f24f16f6192ac76d9d2ca23fb0b40053c00d02b53d5111",
@@ -4763,6 +4764,7 @@
"T2T1_de_test_sdcard.py::test_sd_format": "b2fe89f9055c90a0b4bd3e18501a2de994a6cbea28e7d402e909cce17339250b",
"T2T1_de_test_sdcard.py::test_sd_no_format": "2555fec831f83bd969b17990bdb7c26a67cbe72e8380d2454cee866c6f81f184",
"T2T1_de_test_sdcard.py::test_sd_protect_lock[auto_lock]": "867b0ce1b888756ca0dc9c31f3f8d22dd223c22a8a339477f8fde586babd9760",
+"T2T1_de_test_sdcard.py::test_sd_protect_lock[press_lock]": "83459aa02367e7a8bf34d45c03e343ca3596d21b8b60cb1d076629121ccbcd7c",
"T2T1_de_test_sdcard.py::test_sd_protect_lock[session_lock]": "9ac0d5d053f6aa1ea2dd6507d34286f23e809f41174bee86a15c512c3fbe5e5b",
"T2T1_de_test_sdcard.py::test_sd_protect_unlock": "dd2d004e64ed201e92943970cdef76b875e613f69b99601f0f0f7f2f85f9e73c",
"T2T1_de_test_session.py::test_cannot_resume_ended_session": "efeaf51fe18b983e9b1a6947a939ad305a92e57798fe07110bb61c0f65755881",
@@ -6573,6 +6575,7 @@
"T2T1_en_test_sdcard.py::test_sd_format": "1def764a164375b0e54c5646ab4a6c23ebc3a9205fd8805e484df5d2863a799b",
"T2T1_en_test_sdcard.py::test_sd_no_format": "0dfbc1808203fab7694ab584e57e967a2a58b632e208d13fef6d096528a7d715",
"T2T1_en_test_sdcard.py::test_sd_protect_lock[auto_lock]": "136227acaaef1c90cf7d1541fd406d589ed8f030f49300149c30d6dba932b09e",
+"T2T1_en_test_sdcard.py::test_sd_protect_lock[press_lock]": "a4c61b3d04bc101dc68ddc429f1fda98dcd2c1a82be04f2b784edef2172dcd33",
"T2T1_en_test_sdcard.py::test_sd_protect_lock[session_lock]": "298841d6282c426c62e5b559da6869426085fa66e0b2593f98a7c94d98e826d9",
"T2T1_en_test_sdcard.py::test_sd_protect_unlock": "6b58ce05cc3ddb0c5e2607959c7dcc2de1a1c9e3120ead02aa4c7943cf31179b",
"T2T1_en_test_session.py::test_cannot_resume_ended_session": "8aa7c68cd09e700e8c6903dcfc39c4a66214481c12c34dc56762829478399b79",
@@ -8401,6 +8404,7 @@
"T2T1_es_test_sdcard.py::test_sd_format": "be08b71759eb536e7bb0d64ab3a6b5ae8d0602cf987e80db956acdcbf1284787",
"T2T1_es_test_sdcard.py::test_sd_no_format": "c888961397b7214c7e14701227178727f674b10fee37ce1dddd59aa925b539a3",
"T2T1_es_test_sdcard.py::test_sd_protect_lock[auto_lock]": "199186a825f5beaedc887580b36fd7e39a508c085b53848a559cf76fdcebcad0",
+"T2T1_es_test_sdcard.py::test_sd_protect_lock[press_lock]": "7ffcf36f5b1d0b14e99d815cc3ae6cf4aa2a91ee2614f87835aae3ef684c47df",
"T2T1_es_test_sdcard.py::test_sd_protect_lock[session_lock]": "b6c2a8ec5b7e739809bf40cdb87d775c14c1342bb54897a52d5587097bea66f4",
"T2T1_es_test_sdcard.py::test_sd_protect_unlock": "67fd963eb18e84dfe2827d23a6032f48991c17c8241a163fe7c545932ffe7e54",
"T2T1_es_test_session.py::test_cannot_resume_ended_session": "17d836396f272c01cafa175aa87b0c076aedf7698c935d1921c7d3d7e8911903",
@@ -10211,6 +10215,7 @@
"T2T1_fr_test_sdcard.py::test_sd_format": "c13635e29dcba9f15cd300e817ec42d61bc7174680784ba60807a0fbae270e97",
"T2T1_fr_test_sdcard.py::test_sd_no_format": "777e8e73b9db5f658ac2a2bf18f7717e12fb002fc3a48c935a3c37844ccb4a82",
"T2T1_fr_test_sdcard.py::test_sd_protect_lock[auto_lock]": "d899e674b9986ac2c2eb8a35abd3d44b7e1076db832ba99e1d967dab2111e047",
+"T2T1_fr_test_sdcard.py::test_sd_protect_lock[press_lock]": "f3d3348aaf46dbac5c3599eb98d4774400f49be6c8349be767277b77c1a19749",
"T2T1_fr_test_sdcard.py::test_sd_protect_lock[session_lock]": "2749fe83dd7698b2ec05a759d592d54aa50d502188e318db3c7ffd8379fd8a5c",
"T2T1_fr_test_sdcard.py::test_sd_protect_unlock": "8f227b17b6da40fe8a784f5815785aa9bca16e8c6fd873e5aa81e9ce1b02558a",
"T2T1_fr_test_session.py::test_cannot_resume_ended_session": "7e5a99a4b6aad4cdf7a9f9aecf483faa381108ee2108efaba88f261e29ac1f8a",
@@ -12021,6 +12026,7 @@
"T2T1_pt_test_sdcard.py::test_sd_format": "2d069cfc85fd66929415517413f39e45a564269aac273613d0b153f868b94655",
"T2T1_pt_test_sdcard.py::test_sd_no_format": "22ed9cafbe12bfd489fb1521ec6bf11fbd13008ea5212e98cd69ce9aa1c3d2a9",
"T2T1_pt_test_sdcard.py::test_sd_protect_lock[auto_lock]": "96153a6368fbcae78b3fc1cae65b6966736bed4edda3983a0b310693516fcd78",
+"T2T1_pt_test_sdcard.py::test_sd_protect_lock[press_lock]": "7638cc76171969ff572e6dfced18471393539dde2ce64d738b5f0ff7148c248d",
"T2T1_pt_test_sdcard.py::test_sd_protect_lock[session_lock]": "5e6b9d768ed4a266b4a576286a1633453ca6eb05e15c6f3e813aad164fe346e6",
"T2T1_pt_test_sdcard.py::test_sd_protect_unlock": "bff32da777ceb00e8338a3475cd2cddd26a6dbdaa31d460828264cb1ea2ee954",
"T2T1_pt_test_session.py::test_cannot_resume_ended_session": "020b255e04b59e53d2aa87ae960038f4538d350de28df15a1ac41b376e9d04f2",
@@ -24981,6 +24987,7 @@
"T3T1_cs_test_sdcard.py::test_sd_format": "84c8b74e629555ba7a19736b6d6f0d9ea23eda1a143bdda66326ff89bed22ca2",
"T3T1_cs_test_sdcard.py::test_sd_no_format": "051854e6ef881cc7ae9f5d85de77ecc9e45f805f4a2dac12c9e51eb6e464f5e1",
"T3T1_cs_test_sdcard.py::test_sd_protect_lock[auto_lock]": "3588a71c0558a7d47c8ebc9d41f2c9520808e8bc25aef8f4cfa0a3afdab263ac",
+"T3T1_cs_test_sdcard.py::test_sd_protect_lock[press_lock]": "b7ebe4d8c7f39ec11a8ea9f49493c78b29ce1a4de39f4a4a5699c4849e1ea2d7",
"T3T1_cs_test_sdcard.py::test_sd_protect_lock[session_lock]": "2e81ff340ef18fd18effdea3ecc42ec35659f1aae17ee4ee30bbffd97395b43f",
"T3T1_cs_test_sdcard.py::test_sd_protect_unlock": "07eb6650a70d52675a0e2ae79a7febd9a7ec47b7835e7a80fe3b7a3d2a22b7bc",
"T3T1_cs_test_session.py::test_cannot_resume_ended_session": "9c20243bc5dacf2442af5a8778b66a0ab7cb978e11c02fec371f109fbc9d55f6",
@@ -26765,6 +26772,7 @@
"T3T1_de_test_sdcard.py::test_sd_format": "e26ca24b0efa359aa2dd315c73a902d3b9a43b93987107b53aa218bb8dbd8bbd",
"T3T1_de_test_sdcard.py::test_sd_no_format": "11d7e0781b5a6c0ccf48bdda86806907e94f11639827bc198912323cff1b18e6",
"T3T1_de_test_sdcard.py::test_sd_protect_lock[auto_lock]": "5bc12dda47b4f5cc03cf63e3e36a39476034ade946e4049cb90ec561d17df5aa",
+"T3T1_de_test_sdcard.py::test_sd_protect_lock[press_lock]": "ea223b319d5e634f19e7826c22b1e5af3e1f0ee7d524b17c0e38335dab811fec",
"T3T1_de_test_sdcard.py::test_sd_protect_lock[session_lock]": "e7567bad410b3529c625ba9a21c24e13f7bf4c76818b9d85eede653f95c9b891",
"T3T1_de_test_sdcard.py::test_sd_protect_unlock": "2d146714b0dc613325447c4110e17f839272414b47e0b3f8897c2b55d37e177c",
"T3T1_de_test_session.py::test_cannot_resume_ended_session": "68c146f13417be0845fee20b1d8de4a05e0844608c2efb3cfae9f573be39f0c4",
@@ -28549,6 +28557,7 @@
"T3T1_en_test_sdcard.py::test_sd_format": "d56a51a785b40cf9b298a54f07c2beed417b1c0ff6978cfa3a99b90ad93213bf",
"T3T1_en_test_sdcard.py::test_sd_no_format": "0edc58610ac21bb7fb8027bbd5b91ffb331363cda3f716b3f41d59a98946c1db",
"T3T1_en_test_sdcard.py::test_sd_protect_lock[auto_lock]": "b1f2b1db197b02450ce0ceba8e142276472d6f51602ba675dcbd4c022869ccf9",
+"T3T1_en_test_sdcard.py::test_sd_protect_lock[press_lock]": "cb2873179b4c49855b457eea1094dc154ac0c9956a81e096fd850b7e9b93be8b",
"T3T1_en_test_sdcard.py::test_sd_protect_lock[session_lock]": "14dd73b7c29b1df02107723422da5aec8dd25daf9b513356513c4e7e427675db",
"T3T1_en_test_sdcard.py::test_sd_protect_unlock": "63dfb6dbc406519ddd6f5f88efd7e6240ee97e699e1e5360a9a15976a90ece4a",
"T3T1_en_test_session.py::test_cannot_resume_ended_session": "0064e8a52b9bff0c2a31432fa781b800afb21e8910d60f303671bdfc8d8425fc",
@@ -30333,6 +30342,7 @@
"T3T1_es_test_sdcard.py::test_sd_format": "045536da467f3377bc616d33d41f90390a63e650b890a4d40aad2fa9bdfdc704",
"T3T1_es_test_sdcard.py::test_sd_no_format": "a2dcce6910f79e146cdd8cac69dbcf4b661fff74b50c6d80abbcd0c4b0d52b1d",
"T3T1_es_test_sdcard.py::test_sd_protect_lock[auto_lock]": "dd6d1ca6b397b0b24a6abd0dda697b59eb71da62f3209007e980d2c0cd9605bd",
+"T3T1_es_test_sdcard.py::test_sd_protect_lock[press_lock]": "5243d7e777493233bb3755ced880a70f2aa24fa1f76884edb9e458b3b6e468d8",
"T3T1_es_test_sdcard.py::test_sd_protect_lock[session_lock]": "dc3fe92c050f6866722961d2bd9bbcb9dce3229e94bd7a6f6ffaad74a38b63a8",
"T3T1_es_test_sdcard.py::test_sd_protect_unlock": "c2c664ec48550854b19da2c3013cd294a504bd35e723eb2a6c9764735699b819",
"T3T1_es_test_session.py::test_cannot_resume_ended_session": "41b9c9e85d64891dcfd03d675df77c533f035a2d191b161144d00afab69a589d",
@@ -32117,6 +32127,7 @@
"T3T1_fr_test_sdcard.py::test_sd_format": "f655ef18e159778a54f5e4be50343c28969f8dea089b9d66482950bff2626748",
"T3T1_fr_test_sdcard.py::test_sd_no_format": "6ae3bfd66bf3f54137f12adc6006212df440368a1b8df9c667dad814b449a7e5",
"T3T1_fr_test_sdcard.py::test_sd_protect_lock[auto_lock]": "bbf3d115801d3df7693d09a2f3b2cd6b0ec41071b9cbadd6467e4d5e2bd7a5a1",
+"T3T1_fr_test_sdcard.py::test_sd_protect_lock[press_lock]": "259b341807af22ca0bf7e32caea1fe22e00611735d9b3e4c76e79d97b3268393",
"T3T1_fr_test_sdcard.py::test_sd_protect_lock[session_lock]": "3eed0c60b5ebbd5668dbb15e119bcd808f385404f4c7eb53fd2fb510d7eb5f1e",
"T3T1_fr_test_sdcard.py::test_sd_protect_unlock": "55fa41aec8739c24aea358cd66fa30f7315d4bc590d827e2b94854b2397751cf",
"T3T1_fr_test_session.py::test_cannot_resume_ended_session": "116c64aa168d12181b7a5317677ddf135d739fdac22d38ff08571dc3301c2f47",
@@ -33901,6 +33912,7 @@
"T3T1_pt_test_sdcard.py::test_sd_format": "affbe3d33fab83e6146a70e80ca94c7533b9e5345f7ae3ff852c07f2a0b4c245",
"T3T1_pt_test_sdcard.py::test_sd_no_format": "bd8ed58679176a0996aa426b5f289407f2dce9c499f6b56668f089e908259b24",
"T3T1_pt_test_sdcard.py::test_sd_protect_lock[auto_lock]": "4eb55bcae8fff279cbd90d05f51c6d17b4e83ab390b64e4dda57b4353894107e",
+"T3T1_pt_test_sdcard.py::test_sd_protect_lock[press_lock]": "0b1c67ad02400d5ce8865c6b59a93c1454eb6cc535baa32e13b4ca341ef0ead9",
"T3T1_pt_test_sdcard.py::test_sd_protect_lock[session_lock]": "ae72abb8bb7a76db134005dcf565be5a851ba9633cd39c82a7a3856a116eed61",
"T3T1_pt_test_sdcard.py::test_sd_protect_unlock": "6209cac525cad2484a7846f77e647b37754b9d050a00374559454aff2bd9ef9c",
"T3T1_pt_test_session.py::test_cannot_resume_ended_session": "14256cd1b801d1b67a37de1198d61a80d342f39e5c5d6fa991ec7fb526d683be",
Why this scored 43/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.