Skip to content

Commit 4353da1

Browse files
committed
feat(core): add N1W1 backup/recovery demo flows
Can be tested by invoking: ``` $ core/emu.py -ea -c trezorctl device setup -b shamir # will run multi-share backup $ core/tools/n1w1-emu.py run 127.0.0.1:21325 /tmp/tagN # simulate tag connection and I/O ``` [no changelog]
1 parent 408a9e3 commit 4353da1

3 files changed

Lines changed: 229 additions & 14 deletions

File tree

.github/workflows/core.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ jobs:
152152
retention-days: 7
153153

154154
core_emu_arm:
155-
if: github.event_name == 'schedule'
156155
name: Build emu arm
157156
runs-on: ubuntu-latest-arm64
158157
needs: param
@@ -781,8 +780,8 @@ jobs:
781780
if: github.event_name != 'schedule' && github.repository == 'trezor/trezor-firmware'
782781
runs-on: ubuntu-latest
783782
needs:
784-
# Do not include ARM, they are only built on nightly
785783
- core_emu
784+
- core_emu_arm
786785
steps:
787786
- uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # actions/download-artifact@v8.0.0
788787
with:

core/src/apps/management/recovery_device/layout.py

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import storage.recovery as storage_recovery
44
import storage.recovery_shares as storage_recovery_shares
5-
from trezor import TR
5+
from trezor import TR, utils
66
from trezor.ui.layouts.recovery import ( # noqa: F401
77
request_word_count,
88
show_already_added,
@@ -278,12 +278,117 @@ async def request_mnemonic(self) -> str | None:
278278
return None
279279

280280

281-
async def choose_handler(method: BackupMethod | None) -> type[RecoveryHandler]:
282-
from trezor.enums import BackupMethod
281+
if not utils.USE_N4W1:
283282

284-
if method is not BackupMethod.Display and __debug__:
285-
from trezor import log
283+
async def choose_handler(method: BackupMethod | None) -> type[RecoveryHandler]:
284+
from trezor.enums import BackupMethod
286285

287-
log.warning(__name__, "Unsupported backup method: %s", method)
286+
if method is not BackupMethod.Display and __debug__:
287+
from trezor import log
288288

289-
return _DisplayHandler
289+
log.warning(__name__, "Unsupported backup method: %s", method)
290+
291+
return _DisplayHandler
292+
293+
else:
294+
295+
if TYPE_CHECKING:
296+
from trezor.messages import BackupMethod
297+
298+
from .recover import Slip39State
299+
300+
async def choose_handler(method: BackupMethod | None) -> type[RecoveryHandler]:
301+
from trezor.enums import BackupMethod
302+
303+
if method is None:
304+
method = await _choose_method()
305+
306+
if method is BackupMethod.N4W1:
307+
return _N4W1Handler
308+
309+
if method not in (None, BackupMethod.Display):
310+
from trezor import log
311+
312+
if __debug__:
313+
log.warning(__name__, "Unsupported backup method: %s", method)
314+
315+
return _DisplayHandler
316+
317+
async def _choose_method() -> BackupMethod:
318+
import trezorui_api
319+
from trezor.enums import BackupMethod
320+
from trezor.ui.layouts import interact
321+
322+
index = await interact(
323+
trezorui_api.select_word(
324+
title=TR.recovery__title,
325+
description="Which type of wallet backup do you have?",
326+
words=("N4W1 backup", "Wordlist backup", ""),
327+
),
328+
br_name="backup_choose",
329+
)
330+
return (BackupMethod.N4W1, BackupMethod.Display)[index]
331+
332+
class _N4W1Handler:
333+
def __init__(
334+
self,
335+
recovery_type: RecoveryType,
336+
slip39_state: Slip39State | None,
337+
) -> None:
338+
super().__init__()
339+
self.recovery_type = recovery_type
340+
# `slip39_state is None` indicates that we are (re)starting the first recovery step.
341+
self.slip39_state = slip39_state
342+
343+
@classmethod
344+
async def load(cls, recovery_type: RecoveryType) -> "RecoveryHandler":
345+
return cls(recovery_type, load_slip39_state())
346+
347+
async def show_state(self, is_retry: bool) -> None:
348+
if is_retry or self.slip39_state is None:
349+
# don't show recovery state on retries and before the first share is entered
350+
return
351+
word_count = self.slip39_state[0]
352+
await _request_share_first_screen(word_count, self.recovery_type)
353+
354+
async def request_mnemonic(self) -> str | None:
355+
"""Return the mnemonic or `None` on cancellation/validation error."""
356+
from apps.debug import n4w1_mock
357+
358+
with n4w1_mock.ctx as ctx:
359+
# returns `None` on cancellation or retriable error.
360+
await ctx.confirm_connect(
361+
title=TR.recovery__title,
362+
description=TR.n4w1__hold_next,
363+
button=TR.n4w1__footer_next,
364+
br_name="backup_read",
365+
)
366+
# continue N4W1 communication (the tag is connected)
367+
368+
# TODO: animate during read?
369+
# TODO: run mnemonic checks during animation
370+
# TODO: show empty tag warning
371+
if (blob := await ctx.read(key="mnemonic")) is None:
372+
return None
373+
374+
# TODO: use protobuf?
375+
share = bytes(blob).decode()
376+
return await self.check_words(share)
377+
378+
async def check_words(self, share: str) -> str | None:
379+
# Can be `None` when checking the first share.
380+
backup_type = self.slip39_state and self.slip39_state[1]
381+
share_words = share.split(" ")
382+
383+
try:
384+
from .word_validity import WordValidityResult, check
385+
386+
# Re-verify relevant prefixes
387+
# TODO: can it be encapsulated too?
388+
for prefix_len in range(1, 1 + len(share_words)):
389+
check(backup_type, partial_mnemonic=share_words[:prefix_len])
390+
return share
391+
except WordValidityResult as exc:
392+
# if they were invalid or some checks failed we continue and request them again
393+
await exc.show_error()
394+
return None

core/src/apps/management/reset_device/layout.py

Lines changed: 116 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from micropython import const
22
from typing import TYPE_CHECKING, Iterable, Protocol, Sequence
33

4+
from trezor import utils
45
from trezor.ui.layouts.reset import ( # noqa: F401
56
show_share_words,
67
slip39_advanced_prompt_group_threshold,
@@ -195,13 +196,123 @@ async def _backup_share(self, share: ShareInfo) -> None:
195196

196197

197198
async def choose_backup_handler(method: BackupMethod | None) -> BackupHandler:
198-
# TODO: prompt the user if method is `None`.
199-
if __debug__:
200-
from trezor.enums import BackupMethod
199+
from trezor.enums import BackupMethod
200+
201+
if utils.USE_N4W1:
202+
if method is None:
203+
method = await _choose_method()
204+
205+
if method is BackupMethod.N4W1:
206+
return _N4W1Backup()
201207

202-
if method not in (None, BackupMethod.Display):
203-
from trezor import log
208+
if method not in (None, BackupMethod.Display):
209+
from trezor import log
204210

211+
if __debug__:
205212
log.warning(__name__, "Unsupported backup method: %s", method)
206213

207214
return _DisplayBackup()
215+
216+
217+
if utils.USE_N4W1:
218+
219+
from trezor import TR
220+
from trezor.ui.layouts.common import interact
221+
222+
if TYPE_CHECKING:
223+
from buffer_types import AnyBytes
224+
225+
from apps.debug.n4w1_mock import N4W1Context
226+
227+
class Retry(Exception):
228+
def __init__(self, msg: str) -> None:
229+
self.msg = msg
230+
231+
class _N4W1Backup:
232+
233+
async def intro(self, num_of_words: int | None = None) -> None:
234+
# TODO: design/copy
235+
pass
236+
237+
async def backup(self, iter_shares: Iterable[ShareInfo]) -> None:
238+
# TODO: warn user about safety
239+
240+
# backup all shares
241+
for share in iter_shares:
242+
await self._backup_share(share)
243+
244+
async def _backup_share(self, share: ShareInfo) -> None:
245+
import trezorui_api
246+
from trezor import TR
247+
from trezor.ui.layouts.common import raise_if_not_confirmed
248+
249+
from apps.debug import n4w1_mock
250+
251+
# TODO: use protobuf?
252+
blob = " ".join(share.words).encode()
253+
254+
if share.index == 0 or share.num_of_shares is None:
255+
description, button = TR.n4w1__hold_first, TR.n4w1__footer_first
256+
elif share.index == share.num_of_shares - 1:
257+
description, button = TR.n4w1__hold_last, TR.n4w1__footer_last
258+
else:
259+
description, button = TR.n4w1__hold_next, TR.n4w1__footer_next
260+
261+
while True:
262+
try:
263+
with n4w1_mock.ctx as ctx:
264+
# may raise a retriable error
265+
return await _backup_share(ctx, description, button, blob)
266+
except Retry as exc:
267+
await raise_if_not_confirmed(
268+
trezorui_api.show_warning(
269+
title=TR.words__important,
270+
button=TR.buttons__continue,
271+
description=exc.msg,
272+
danger=True,
273+
),
274+
br_name="backup_retry",
275+
)
276+
# wait for a new N4W1 tag
277+
continue
278+
279+
async def _backup_share(
280+
ctx: N4W1Context, description: str, button: str, blob: AnyBytes
281+
) -> None:
282+
from trezor.ui.layouts.progress import progress
283+
284+
await ctx.confirm_connect(
285+
title=TR.backup__title_create_wallet_backup,
286+
description=description,
287+
button=button,
288+
br_name="backup_write",
289+
)
290+
# continue N4W1 communication (the tag is connected)
291+
result = await ctx.read(key="mnemonic")
292+
if result is not None:
293+
raise Retry("Non-empty N4W1 tag.")
294+
295+
progress_obj = progress(description=TR.n4w1__writing)
296+
progress_obj.start()
297+
progress_obj.report(100)
298+
try:
299+
await ctx.write(key="mnemonic", value=blob)
300+
# TODO: animate during I/O?
301+
progress_obj.report(1000)
302+
finally:
303+
progress_obj.stop()
304+
progress_obj = None
305+
306+
async def _choose_method() -> BackupMethod:
307+
import trezorui_api
308+
from trezor.enums import BackupMethod
309+
310+
index = await interact(
311+
trezorui_api.select_word(
312+
title=TR.backup__title_create_wallet_backup,
313+
description="Select the type of wallet backup you want to create.",
314+
words=("N4W1 backup", "Wordlist backup", ""),
315+
),
316+
br_name="backup_choose",
317+
)
318+
return (BackupMethod.N4W1, BackupMethod.Display)[index]

0 commit comments

Comments
 (0)