1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-07-20 13:38:11 +00:00

feat(core/sdbackup): improve encode/decode

- remove raw bytearray access.
- use sha256 instead of crc32
- store also BackupType information
- storage/sd_seed_backup.py deals only with bytes
- remove seed storage in plain text
- WIP: UX code needs better handling of SD card workflow
This commit is contained in:
obrusvit 2023-12-07 13:36:03 +01:00
parent 4a69393d59
commit b2d820c273
6 changed files with 117 additions and 129 deletions

View File

@ -21,7 +21,7 @@
#include "py/mperrno.h" #include "py/mperrno.h"
#include "py/obj.h" #include "py/obj.h"
#include "py/objstr.h" #include "py/objstr.h"
#include "stdio.h" /* #include "stdio.h" */
// clang-format off // clang-format off
#include "ff.h" #include "ff.h"
@ -590,14 +590,14 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorio_fatfs_mkfs_obj, 0, 1,
/// """ /// """
STATIC mp_obj_t mod_trezorio_fatfs_get_capacity() { STATIC mp_obj_t mod_trezorio_fatfs_get_capacity() {
FATFS_ONLY_MOUNTED; FATFS_ONLY_MOUNTED;
printf("csize: %d\n", fs_instance.csize); /* printf("csize: %d\n", fs_instance.csize); */
printf("fatent: %d\n", fs_instance.n_fatent); /* printf("fatent: %d\n", fs_instance.n_fatent); */
printf("free clusters: %d\n", fs_instance.free_clst); /* printf("free clusters: %d\n", fs_instance.free_clst); */
printf("volbase: %d\n", fs_instance.volbase); /* printf("volbase: %d\n", fs_instance.volbase); */
printf("fatbase: %d\n", fs_instance.fatbase); /* printf("fatbase: %d\n", fs_instance.fatbase); */
printf("dirbase: %d\n", fs_instance.dirbase); /* printf("dirbase: %d\n", fs_instance.dirbase); */
printf("database: %d\n", fs_instance.database); /* printf("database: %d\n", fs_instance.database); */
printf("winsect: %d\n", fs_instance.winsect); /* printf("winsect: %d\n", fs_instance.winsect); */
// total number of clusters in the filesystem // total number of clusters in the filesystem
DWORD total_clusters = fs_instance.n_fatent - 2; DWORD total_clusters = fs_instance.n_fatent - 2;
// size of each cluster in bytes // size of each cluster in bytes

View File

@ -78,13 +78,14 @@ async def _continue_recovery_process() -> Success:
secret = None secret = None
words = None words = None
recovered_from_sd = None
backup_medium = "words" backup_medium = "words"
while secret is None: while secret is None:
if is_first_step: if is_first_step:
backup_medium: str = await _choose_backup_medium() backup_medium: str = await _choose_backup_medium()
if utils.USE_SD_CARD and backup_medium == "sdcard": if utils.USE_SD_CARD and backup_medium == "sdcard":
# attempt to recover words from sd card # attempt to recover words from sd card
words = await sdcard_recover_seed() words, backup_type = await sdcard_recover_seed()
if words is None: if words is None:
continue continue
word_count = len(words.split()) word_count = len(words.split())

View File

@ -213,10 +213,10 @@ def _compute_secret_from_entropy(
return secret return secret
async def _backup_bip39_sdcard(mnemonic: bytes) -> None: async def _backup_bip39_sdcard(mnemonic: bytes, bak_t: BackupType) -> None:
from apps.management.sd_backup import sdcard_backup_seed from apps.management.sd_backup import sdcard_backup_seed
backup_success: bool = await sdcard_backup_seed(mnemonic) backup_success: bool = await sdcard_backup_seed(mnemonic, bak_t)
if not backup_success: if not backup_success:
raise ProcessError("SD Card backup could not be verified.") raise ProcessError("SD Card backup could not be verified.")
@ -236,7 +236,7 @@ async def backup_seed(backup_type: BackupType, mnemonic_secret: bytes) -> None:
else: else:
backup_medium: str = "words" backup_medium: str = "words"
if backup_medium == "sdcard": if backup_medium == "sdcard":
await _backup_bip39_sdcard(mnemonic_secret) await _backup_bip39_sdcard(mnemonic_secret, backup_type)
elif backup_medium == "words": elif backup_medium == "words":
await layout.bip39_show_and_confirm_mnemonic(mnemonic_secret.decode()) await layout.bip39_show_and_confirm_mnemonic(mnemonic_secret.decode())
else: else:

View File

@ -1,8 +1,9 @@
from storage.sd_seed_backup import recover_seed_from_sdcard, store_seed_on_sdcard from storage.sd_seed_backup import recover_seed_from_sdcard, store_seed_on_sdcard
from trezor import io, utils
if utils.USE_SD_CARD: from typing import TYPE_CHECKING
fatfs = io.fatfs # global_import_cache
if TYPE_CHECKING:
from trezor.enums import BackupType
async def bip39_choose_backup_medium(recovery: bool = False) -> str: async def bip39_choose_backup_medium(recovery: bool = False) -> str:
@ -12,16 +13,18 @@ async def bip39_choose_backup_medium(recovery: bool = False) -> str:
return await choose_backup_medium(recovery) return await choose_backup_medium(recovery)
async def sdcard_backup_seed(mnemonic_secret: bytes) -> bool: async def sdcard_backup_seed(mnemonic_secret: bytes, bak_t: BackupType) -> bool:
from apps.common.sdcard import ensure_sdcard from apps.common.sdcard import ensure_sdcard
await ensure_sdcard(ensure_filesystem=True, for_sd_backup=True) await ensure_sdcard(ensure_filesystem=True, for_sd_backup=True)
return store_seed_on_sdcard(mnemonic_secret) return store_seed_on_sdcard(mnemonic_secret, bak_t)
async def sdcard_recover_seed() -> str | None: async def sdcard_recover_seed() -> tuple[str | None, BackupType | None]:
from apps.common.sdcard import ensure_sdcard from apps.common.sdcard import ensure_sdcard
await ensure_sdcard(ensure_filesystem=False) await ensure_sdcard(ensure_filesystem=False)
seed_read = recover_seed_from_sdcard() mnemonic_bytes, backup_type = recover_seed_from_sdcard()
return seed_read if mnemonic_bytes is None or backup_type is None:
return (None, None)
return mnemonic_bytes.decode("utf-8"), backup_type

View File

@ -1,11 +1,16 @@
from micropython import const from micropython import const
from trezorcrypto import crc from trezorcrypto import sha256
from typing import Generator from typing import TYPE_CHECKING
from trezor import io, utils from trezor import io, utils
from trezor.sdcard import with_filesystem, with_sdcard from trezor.sdcard import with_filesystem, with_sdcard
from trezor.wire import ProcessError from trezor.wire import ProcessError
if TYPE_CHECKING:
from typing import Generator
from trezor.enums import BackupType
from trezor.utils import BufferReader
if utils.USE_SD_CARD: if utils.USE_SD_CARD:
fatfs = io.fatfs # global_import_cache fatfs = io.fatfs # global_import_cache
sdcard = io.sdcard # global_import_cache sdcard = io.sdcard # global_import_cache
@ -14,14 +19,13 @@ if utils.USE_SD_CARD:
SDBACKUP_BLOCK_OFFSET = 130 # TODO arbitrary for now SDBACKUP_BLOCK_OFFSET = 130 # TODO arbitrary for now
SDBACKUP_N_WRITINGS = 100 # TODO decide between offset/writings SDBACKUP_N_WRITINGS = 100 # TODO decide between offset/writings
SDBACKUP_MAGIC = b"TRZM" SDBACKUP_MAGIC = b"TRZM"
SDBACKUP_VERSION = b"00" SDBACKUP_VERSION = b"0"
@with_filesystem @with_filesystem
def store_seed_on_sdcard(mnemonic_secret: bytes) -> bool: def store_seed_on_sdcard(mnemonic_secret: bytes, backup_type: BackupType) -> bool:
_write_seed_unalloc(mnemonic_secret) _write_seed_unalloc(mnemonic_secret, backup_type)
_write_seed_plain_text(mnemonic_secret) if _verify_backup(mnemonic_secret, backup_type):
if _verify_backup(mnemonic_secret):
_write_readme() _write_readme()
return True return True
else: else:
@ -29,45 +33,39 @@ def store_seed_on_sdcard(mnemonic_secret: bytes) -> bool:
@with_sdcard @with_sdcard
def recover_seed_from_sdcard() -> str | None: def recover_seed_from_sdcard() -> tuple[bytes | None, BackupType | None]:
return _read_seed_unalloc() return _read_seed_unalloc()
def _verify_backup(mnemonic_secret: bytes) -> bool: def _verify_backup(mnemonic_secret: bytes, backup_type: BackupType) -> bool:
mnemonic_read_plain = _read_seed_plain_text() decoded_mnemonic, decoded_backup_type = _read_seed_unalloc()
mnemonic_read_unalloc = _read_seed_unalloc() if decoded_mnemonic is None or decoded_backup_type is None:
if mnemonic_read_plain is None:
return False return False
if mnemonic_read_unalloc is None: return decoded_mnemonic == mnemonic_secret and decoded_backup_type == backup_type
return False
return (
mnemonic_read_plain.encode() == mnemonic_secret
and mnemonic_read_unalloc.encode() == mnemonic_secret
)
def _write_seed_unalloc(mnemonic_secret: bytes) -> None: def _write_seed_unalloc(mnemonic_secret: bytes, backup_type: BackupType) -> None:
block_to_write = _encode_mnemonic_to_backup_block(mnemonic_secret) block_to_write = _encode_backup_block(mnemonic_secret, backup_type)
for block_idx in _storage_blocks_gen(): for block_idx in _storage_blocks_gen():
sdcard.write(block_idx, block_to_write) sdcard.write(block_idx, block_to_write)
def _read_seed_unalloc() -> str | None: def _read_seed_unalloc() -> tuple[bytes | None, BackupType | None]:
block_buffer = bytearray(SDCARD_BLOCK_SIZE_B) block_buffer = bytearray(SDCARD_BLOCK_SIZE_B)
mnemonic_read = None restored_block = None
for block_idx in _storage_blocks_gen(): for block_idx in _storage_blocks_gen():
try: try:
sdcard.read(block_idx, block_buffer) sdcard.read(block_idx, block_buffer)
mnemonic_read = _decode_mnemonic_from_backup_block(block_buffer) restored_block = _decode_backup_block(block_buffer)
if mnemonic_read is not None: if restored_block is not None:
break break
except Exception: except Exception:
return None return (None, None)
if mnemonic_read is None: if restored_block is None:
return None return (None, None)
mnemonic_read_decoded = mnemonic_read.decode("utf-8").rstrip("\x00") decoded_mnemonic, decoded_backup_type = restored_block
return mnemonic_read_decoded # decoded_mnemonic_str = decoded_mnemonic.decode("utf-8").rstrip("\x00")
return (decoded_mnemonic, decoded_backup_type)
def _storage_blocks_gen() -> Generator: def _storage_blocks_gen() -> Generator:
@ -97,88 +95,74 @@ def _storage_blocks_gen_by_n() -> Generator[int, None, None]:
""" """
Backup Memory Block Layout: Backup Memory Block Layout:
+----------------------+----------------------+----------------------+-------------------------------+ +----------------------+------------------------+--------------------+-------------------------------+
| SDBACKUP_MAGIC (4B) | SDBACKUP_VERSION (2B)| SEED_LENGTH (4B) | MNEMONIC (variable length) | | SDBACKUP_MAGIC (4B) | SDBACKUP_VERSION (1B) | BACKUP_TYPE (1B) | SEED_LENGTH (4B) |
+----------------------+----------------------+----------------------+-------------------------------+ +----------------------+------------------------+--------------------+-------------------------------+
| CHECKSUM (4B) | Padding (variable) | | MNEMONIC (variable length) | HASH (32B) | Padding (variable) |
+-----------------------------------------------------------------------+----------------------------+ +-----------------------------------------------+--------------------+-------------------------------+
- SDBACKUP_MAGIC: 4 bytes magic number identifying the backup block - SDBACKUP_MAGIC: 4 bytes magic number identifying the backup block
- SDBACKUP_VERSION: 2 bytes representing the version of the backup format - SDBACKUP_VERSION: 1 bytes representing the version of the backup format (for future compatibility)
- BACKUP_TYPE: 1 bytes representing the version of the backup format
- SEED_LENGTH: 4 bytes (big-endian) indicating the length of the mnemonic - SEED_LENGTH: 4 bytes (big-endian) indicating the length of the mnemonic
- MNEMONIC: Variable length field containing the mnemonic - MNEMONIC: Variable length field containing the mnemonic
- CHECKSUM: 4 bytes CRC32 checksum of all previous fields - HASH: 32 bytes sha256 hash of all previous fields
- Padding: Remaining bytes of the block (if any) are padding - Padding: Remaining bytes of the block (if any) are padding
The total size of the block is defined by SDCARD_BLOCK_SIZE_B. The total size of the block is defined by SDCARD_BLOCK_SIZE_B.
""" """
# Constants for offsets and lengths # Constants lengths
MAGIC_OFFSET = const(0) MAGIC_LEN = const(4)
MAGIC_LENGTH = const(4) VERSION_LEN = const(1)
VERSION_OFFSET = const(MAGIC_OFFSET + MAGIC_LENGTH) BACKUPTYPE_LEN = const(1)
VERSION_LENGTH = const(2) SEEDLEN_LEN = const(4)
SEED_LEN_OFFSET = const(VERSION_OFFSET + VERSION_LENGTH) HASH_LEN = const(32)
SEED_LEN_LENGTH = const(4)
MNEMONIC_OFFSET = const(SEED_LEN_OFFSET + SEED_LEN_LENGTH)
CHECKSUM_LENGTH = const(4)
def _encode_mnemonic_to_backup_block(mnemonic: bytes) -> bytes: def _encode_backup_block(mnemonic: bytes, backup_type: BackupType) -> bytes:
ret = bytearray(SDCARD_BLOCK_SIZE_B) from trezor.utils import empty_bytearray
magic = SDBACKUP_MAGIC + SDBACKUP_VERSION
ret = empty_bytearray(SDCARD_BLOCK_SIZE_B)
ret.extend(SDBACKUP_MAGIC)
ret.extend(SDBACKUP_VERSION)
ret.extend(backup_type.to_bytes(BACKUPTYPE_LEN, "big"))
seed_len = len(mnemonic) seed_len = len(mnemonic)
ret[MAGIC_OFFSET : MAGIC_OFFSET + MAGIC_LENGTH] = magic ret.extend(seed_len.to_bytes(SEEDLEN_LEN, "big"))
ret[SEED_LEN_OFFSET : SEED_LEN_OFFSET + SEED_LEN_LENGTH] = seed_len.to_bytes( ret.extend(mnemonic)
SEED_LEN_LENGTH, "big" blockhash = sha256(ret[:]).digest()
) ret.extend(blockhash)
ret[MNEMONIC_OFFSET : MNEMONIC_OFFSET + seed_len] = mnemonic assert len(ret) <= SDCARD_BLOCK_SIZE_B
checksum = crc.crc32(ret[: MNEMONIC_OFFSET + seed_len]) padding_len = SDCARD_BLOCK_SIZE_B - len(ret)
ret[ ret.extend(b"\x00" * padding_len)
MNEMONIC_OFFSET + seed_len : MNEMONIC_OFFSET + seed_len + CHECKSUM_LENGTH
] = checksum.to_bytes(CHECKSUM_LENGTH, "big")
return bytes(ret) return bytes(ret)
def _decode_mnemonic_from_backup_block(block: bytes) -> bytes | None: def _decode_backup_block(block: bytes) -> tuple[bytes, BackupType] | None:
assert len(block) == SDCARD_BLOCK_SIZE_B assert len(block) == SDCARD_BLOCK_SIZE_B
if len(block) != SDCARD_BLOCK_SIZE_B: try:
return None r = utils.BufferReader(block)
if block[MAGIC_OFFSET : MAGIC_OFFSET + MAGIC_LENGTH] != SDBACKUP_MAGIC: if r.read_memoryview(MAGIC_LEN) != SDBACKUP_MAGIC:
return None return None
seed_len = int.from_bytes( r.read_memoryview(VERSION_LEN) # skip the version for now
block[SEED_LEN_OFFSET : SEED_LEN_OFFSET + SEED_LEN_LENGTH], "big" backup_type = int.from_bytes(r.read_memoryview(BACKUPTYPE_LEN), "big")
) seed_len = int.from_bytes(r.read_memoryview(SEEDLEN_LEN), "big")
checksum_expected = crc.crc32(block[: MNEMONIC_OFFSET + seed_len]) mnemonic = r.read(seed_len)
checksum_read = int.from_bytes( blockhash_read = r.read(HASH_LEN)
block[ r.seek(0)
MNEMONIC_OFFSET + seed_len : MNEMONIC_OFFSET + seed_len + CHECKSUM_LENGTH blockhash_expected = sha256(
], r.read_memoryview(
"big", MAGIC_LEN + VERSION_LEN + BACKUPTYPE_LEN + SEEDLEN_LEN + seed_len
) )
if checksum_expected == checksum_read: ).digest()
return block[MNEMONIC_OFFSET : MNEMONIC_OFFSET + seed_len] if blockhash_read == blockhash_expected:
else: return (mnemonic, backup_type)
return None else:
return None
except (ValueError, EOFError):
raise DataError("Trying to decode invalid SD card block.")
def _write_readme() -> None: def _write_readme() -> None:
with fatfs.open("README.txt", "w") as f: with fatfs.open("README.txt", "w") as f:
f.write(b"This is a Trezor backup SD card.") f.write(b"This is a Trezor backup SD card.")
def _write_seed_plain_text(mnemonic_secret: bytes) -> None:
# TODO to be removed, just for testing purposes
fatfs.mkdir("/trezor", True)
with fatfs.open("/trezor/seed.txt", "w") as f:
f.write(mnemonic_secret)
def _read_seed_plain_text() -> str | None:
# TODO to be removed, just for testing purposes
mnemonic_read = bytearray(SDCARD_BLOCK_SIZE_B)
try:
with fatfs.open("/trezor/seed.txt", "r") as f:
f.read(mnemonic_read)
except fatfs.FatFSError:
return None
return mnemonic_read.decode("utf-8").rstrip("\x00")

View File

@ -2,6 +2,7 @@ from common import *
from storage.sd_seed_backup import * from storage.sd_seed_backup import *
from trezor import io, sdcard from trezor import io, sdcard
from trezor.enums import BackupType
class TestStorageSdSeedBackup(unittest.TestCase): class TestStorageSdSeedBackup(unittest.TestCase):
@ -9,29 +10,27 @@ class TestStorageSdSeedBackup(unittest.TestCase):
def setUp(self): def setUp(self):
self.mnemonic = ( self.mnemonic = (
"crane mesh that gain predict open dice defy lottery toddler coin upgrade" b"crane mesh that gain predict open dice defy lottery toddler coin upgrade"
) )
def test_backup_and_restore(self): def test_backup_and_restore(self):
# with self.assertRaises(fatfs.FatFSError):
# store_seed_on_sdcard(self.mnemonic.encode("utf-8"))
io.sdcard.power_on() io.sdcard.power_on()
io.fatfs.mkfs(True) io.fatfs.mkfs(True)
io.fatfs.mount() io.fatfs.mount()
success = store_seed_on_sdcard(self.mnemonic.encode("utf-8")) success = store_seed_on_sdcard(self.mnemonic, BackupType.Bip39)
self.assertTrue(success) self.assertTrue(success)
restored = recover_seed_from_sdcard() restored_mnemonic, restored_backup_type = recover_seed_from_sdcard()
self.assertEqual(self.mnemonic, restored) self.assertEqual(restored_mnemonic, self.mnemonic)
self.assertEqual(restored_backup_type, BackupType.Bip39)
io.fatfs.unmount() io.fatfs.unmount()
io.sdcard.power_off() io.sdcard.power_off()
def test_backup_partlywipe_restore(self): def test_backup_partlywipe_restore(self):
with sdcard.filesystem(mounted=True): with sdcard.filesystem(mounted=True):
success = store_seed_on_sdcard(self.mnemonic.encode("utf-8")) success = store_seed_on_sdcard(self.mnemonic, BackupType.Bip39)
self.assertTrue(success) self.assertTrue(success)
# wipe half of the card, restore must succeed # wipe half of the card, restore must succeed
@ -41,9 +40,9 @@ class TestStorageSdSeedBackup(unittest.TestCase):
io.sdcard.write(block_num, block_buffer) io.sdcard.write(block_num, block_buffer)
with sdcard.filesystem(mounted=False): with sdcard.filesystem(mounted=False):
restored = recover_seed_from_sdcard() restored_mnemonic, restored_backup_type = recover_seed_from_sdcard()
self.assertEqual(self.mnemonic, restored) self.assertEqual(restored_mnemonic, self.mnemonic)
self.assertEqual(restored_backup_type, BackupType.Bip39)
# remove everything, restore fails # remove everything, restore fails
with sdcard.filesystem(mounted=False): with sdcard.filesystem(mounted=False):
@ -51,8 +50,9 @@ class TestStorageSdSeedBackup(unittest.TestCase):
io.sdcard.write(block_num, block_buffer) io.sdcard.write(block_num, block_buffer)
with sdcard.filesystem(mounted=False): with sdcard.filesystem(mounted=False):
restored = recover_seed_from_sdcard() restored_mnemonic, restored_backup_type = recover_seed_from_sdcard()
self.assertEqual(None, restored) self.assertEqual(restored_mnemonic, None)
self.assertEqual(restored_backup_type, None)
if __name__ == "__main__": if __name__ == "__main__":