diff --git a/.gitignore b/.gitignore index a2e0a1518..c46b6486b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ .cache/ .venv/ .idea/ +.ycm_extra_conf.py +.vimspector.json .mypy_cache/ .pytest_cache/ .vscode/ diff --git a/core/embed/extmod/modtrezorio/modtrezorio-fatfs.h b/core/embed/extmod/modtrezorio/modtrezorio-fatfs.h index 98b36b238..203243d76 100644 --- a/core/embed/extmod/modtrezorio/modtrezorio-fatfs.h +++ b/core/embed/extmod/modtrezorio/modtrezorio-fatfs.h @@ -19,6 +19,7 @@ #include "embed/extmod/trezorobj.h" #include "py/mperrno.h" +#include "py/obj.h" #include "py/objstr.h" // clang-format off @@ -94,14 +95,10 @@ const PARTITION VolToPart[] = { {0, 1} // Logical Volume 0 => Physical Disk 0, Partition 1 }; -// Helper function to create a partition spanned over a portition (in -// percentage) of the card. -void make_partition(int disk_portion_p100) { - if ((disk_portion_p100 <= 0) || (disk_portion_p100 > 100)) { - FATFS_RAISE(FatFSError, FR_MKFS_ABORTED); - } +// Helper function to create a partition on a SD card. +void make_partition(int pt_size) { uint8_t working_buf[FF_MAX_SS] = {0}; - LBA_t plist[] = {disk_portion_p100, 0}; + LBA_t plist[] = {pt_size, 0}; FRESULT res = f_fdisk(0, plist, working_buf); if (res != FR_OK) { FATFS_RAISE(FatFSError, res); @@ -562,15 +559,19 @@ STATIC mp_obj_t mod_trezorio_fatfs_mkfs(size_t n_args, const mp_obj_t *args) { // create partition if (n_args > 0 && args[0] == mp_const_true) { - // for SD card backup: we need small partition and keep the rest unallocated - make_partition(60); // TODO decide on the exact portion + // for SD card backup: we make a small partition and keep the rest + // unallocated + // TODO: seems like not big enough for Windows (problem detected popup) + const int n_clusters = 0xFFF5 + 1 + 549; // MAX_FAT16 + 1 + overhead + make_partition(n_clusters); } else { - // for other use (SD salt): make the partitio over the whole space. + // for other use (SD salt): make the partition over the whole space. make_partition(100); } // create FAT volume mapped to the created partition - MKFS_PARM params = {FM_FAT32, 0, 0, 0, 0}; + MKFS_PARM params = { + .fmt = FM_FAT32, .n_fat = 0, .align = 0, .n_root = 0, .au_size = 0}; uint8_t working_buf[FF_MAX_SS] = {0}; FRESULT res = f_mkfs("", ¶ms, working_buf, sizeof(working_buf)); if (res != FR_OK) { @@ -582,6 +583,31 @@ STATIC mp_obj_t mod_trezorio_fatfs_mkfs(size_t n_args, const mp_obj_t *args) { STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorio_fatfs_mkfs_obj, 0, 1, mod_trezorio_fatfs_mkfs); +/// def get_capacity() -> int: +/// """ +/// Get total filesystem size in bytes. +/// """ +STATIC mp_obj_t mod_trezorio_fatfs_get_capacity() { + FATFS_ONLY_MOUNTED; + /* printf("csize: %d\n", fs_instance.csize); */ + /* printf("fatent: %d\n", fs_instance.n_fatent); */ + /* printf("free clusters: %d\n", fs_instance.free_clst); */ + /* printf("volbase: %d\n", fs_instance.volbase); */ + /* printf("fatbase: %d\n", fs_instance.fatbase); */ + /* printf("dirbase: %d\n", fs_instance.dirbase); */ + /* printf("database: %d\n", fs_instance.database); */ + /* printf("winsect: %d\n", fs_instance.winsect); */ + // total number of clusters in the filesystem + DWORD total_clusters = fs_instance.n_fatent - 2; + // size of each cluster in bytes + DWORD cluster_size = fs_instance.csize * SDCARD_BLOCK_SIZE; + DWORD total_size = total_clusters * cluster_size; + + return mp_obj_new_int_from_uint(total_size); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_fatfs_get_capacity_obj, + mod_trezorio_fatfs_get_capacity); + /// def setlabel(label: str) -> None: /// """ /// Set volume label @@ -621,6 +647,8 @@ STATIC const mp_rom_map_elem_t mod_trezorio_fatfs_globals_table[] = { {MP_ROM_QSTR(MP_QSTR_is_mounted), MP_ROM_PTR(&mod_trezorio_fatfs_is_mounted_obj)}, {MP_ROM_QSTR(MP_QSTR_mkfs), MP_ROM_PTR(&mod_trezorio_fatfs_mkfs_obj)}, + {MP_ROM_QSTR(MP_QSTR_get_capacity), + MP_ROM_PTR(&mod_trezorio_fatfs_get_capacity_obj)}, {MP_ROM_QSTR(MP_QSTR_setlabel), MP_ROM_PTR(&mod_trezorio_fatfs_setlabel_obj)}, diff --git a/core/mocks/generated/trezorio/fatfs.pyi b/core/mocks/generated/trezorio/fatfs.pyi index bcc9c254d..5a47e7bc6 100644 --- a/core/mocks/generated/trezorio/fatfs.pyi +++ b/core/mocks/generated/trezorio/fatfs.pyi @@ -174,6 +174,13 @@ def mkfs(for_sd_backup: bool=False) -> None: """ +# extmod/modtrezorio/modtrezorio-fatfs.h +def get_capacity() -> int: + """ + Get total filesystem size in bytes. + """ + + # extmod/modtrezorio/modtrezorio-fatfs.h def setlabel(label: str) -> None: """ diff --git a/core/src/all_modules.py b/core/src/all_modules.py index bc5d4859c..38b4c8fca 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -63,6 +63,8 @@ storage.resident_credentials import storage.resident_credentials storage.sd_salt import storage.sd_salt +storage.sd_seed_backup +import storage.sd_seed_backup trezor import trezor trezor.crypto diff --git a/core/src/apps/common/sdcard.py b/core/src/apps/common/sdcard.py index b1d7c71b5..b4c94dd22 100644 --- a/core/src/apps/common/sdcard.py +++ b/core/src/apps/common/sdcard.py @@ -86,7 +86,9 @@ async def confirm_retry_sd( ) -async def ensure_sdcard(ensure_filesystem: bool = True) -> None: +async def ensure_sdcard( + ensure_filesystem: bool = True, for_sd_backup: bool = False +) -> None: """Ensure a SD card is ready for use. This function runs the UI flow needed to ask the user to insert a SD card if there @@ -95,6 +97,9 @@ async def ensure_sdcard(ensure_filesystem: bool = True) -> None: If `ensure_filesystem` is True (the default), it also tries to mount the SD card filesystem, and allows the user to format the card if a filesystem cannot be mounted. + + In addition, if 'for_sd_backup' is True (False by default), the card is formatted + for SD backup feature. """ from trezor import sdcard @@ -120,9 +125,9 @@ async def ensure_sdcard(ensure_filesystem: bool = True) -> None: # Proceed to formatting. Failure is caught by the outside OSError handler with sdcard.filesystem(mounted=False): - fatfs.mkfs() + fatfs.mkfs(for_sd_backup) fatfs.mount() - fatfs.setlabel("TREZOR") + fatfs.setlabel("BACKUP" if for_sd_backup else "TREZOR") # format and mount succeeded return diff --git a/core/src/apps/management/reset_device/__init__.py b/core/src/apps/management/reset_device/__init__.py index 3d84ad2a4..1162aa21b 100644 --- a/core/src/apps/management/reset_device/__init__.py +++ b/core/src/apps/management/reset_device/__init__.py @@ -214,10 +214,9 @@ def _compute_secret_from_entropy( async def _backup_bip39_sdcard(mnemonic: str) -> None: - from apps.management.sd_backup import sdcard_backup_seed, sdcard_verify_backup - await sdcard_backup_seed(mnemonic) - backup_succes = sdcard_verify_backup(mnemonic) - if not backup_succes: + from apps.management.sd_backup import sdcard_backup_seed + backup_success: bool = await sdcard_backup_seed(mnemonic) + if not backup_success: raise ProcessError("SD Card backup could not be verified.") diff --git a/core/src/apps/management/sd_backup.py b/core/src/apps/management/sd_backup.py index 16de145c3..99a5d3e6f 100644 --- a/core/src/apps/management/sd_backup.py +++ b/core/src/apps/management/sd_backup.py @@ -1,45 +1,21 @@ from trezor import io, utils -from trezor.sdcard import with_filesystem + +from storage.sd_seed_backup import store_seed_on_sdcard, recover_seed_from_sdcard if utils.USE_SD_CARD: fatfs = io.fatfs # global_import_cache -async def sdcard_backup_seed(mnemonic_secret: bytes) -> None: +async def sdcard_backup_seed(mnemonic_secret: bytes) -> bool: from apps.common.sdcard import ensure_sdcard - await ensure_sdcard() - _write_seed_plain_text(mnemonic_secret) + await ensure_sdcard(ensure_filesystem=True, for_sd_backup=True) + return store_seed_on_sdcard(mnemonic_secret) async def sdcard_recover_seed() -> str | None: from apps.common.sdcard import ensure_sdcard await ensure_sdcard(ensure_filesystem=False) - return _read_seed_plain_text() - - -def sdcard_verify_backup(mnemonic_secret: bytes) -> bool: - mnemonic_read = _read_seed_plain_text() - if mnemonic_read is None: - return False - - return mnemonic_read.encode() == mnemonic_secret - -@with_filesystem -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) - -@with_filesystem -def _read_seed_plain_text() -> str | None: - # TODO to be removed, just for testing purposes - mnemonic_read = bytearray(512) - 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') + seed_read = recover_seed_from_sdcard() + return seed_read diff --git a/core/src/storage/sd_seed_backup.py b/core/src/storage/sd_seed_backup.py new file mode 100644 index 000000000..0656dd53c --- /dev/null +++ b/core/src/storage/sd_seed_backup.py @@ -0,0 +1,174 @@ +from micropython import const +from typing import TYPE_CHECKING + +import storage.device +from trezor import io, utils +from trezor.sdcard import with_filesystem +from trezorcrypto import crc + +if utils.USE_SD_CARD: + fatfs = io.fatfs # global_import_cache + sdcard = io.sdcard # global_import_cache + SDCARD_BLOCK_SIZE_B = sdcard.BLOCK_SIZE # global_import_cache + SDBACKUP_BLOCK_START = 66_138 + SDBACKUP_BLOCK_OFFSET = 130 # TODO arbitrary for now + SDBACKUP_MAGIC = b"TRZS" + SDBACKUP_VERSION = b"00" + +# TODO with_filesystem can be just with_sdcard, unnecessary to mount FS for recovery + + +@with_filesystem +def store_seed_on_sdcard(mnemonic_secret: bytes) -> bool: + _write_seed_unalloc(mnemonic_secret) + _write_seed_plain_text(mnemonic_secret) + if _verify_backup(mnemonic_secret): + _write_readme() + return True + else: + return False + + +@with_filesystem +def recover_seed_from_sdcard() -> str | None: + return _read_seed_unalloc() + + +@with_filesystem +def _verify_backup(mnemonic_secret: bytes) -> bool: + mnemonic_read_plain = _read_seed_plain_text() + mnemonic_read_unalloc = _read_seed_unalloc() + if mnemonic_read_plain is None: + return False + if mnemonic_read_unalloc is None: + return False + + return ( + mnemonic_read_plain.encode() == mnemonic_secret + and mnemonic_read_unalloc.encode() == mnemonic_secret + ) + + +@with_filesystem +def _write_seed_unalloc(mnemonic_secret: bytes) -> None: + block_to_write = _encode_mnemonic_to_backup_block(mnemonic_secret) + for block_idx in _storage_blocks_gen(): + # print(f"block_idx: {block_idx}, writing: {block_to_write[10:10+4]}") + sdcard.write(block_idx, block_to_write) + + +@with_filesystem +def _read_seed_unalloc() -> str | None: + block_buffer = bytearray(SDCARD_BLOCK_SIZE_B) + for block_idx in _storage_blocks_gen(): + try: + sdcard.read(block_idx, block_buffer) + mnemonic_read = _decode_mnemonic_from_backup_block(block_buffer) + if mnemonic_read is not None: + break + except fatfs.FatFSError: + return None + # print(f"_read_seed_unalloc: block_read: {block_read}") + mnemonic_read_decoded = mnemonic_read.decode("utf-8").rstrip("\x00") + # print(f"_read_seed_unalloc: mnemonic_read_decoded: {mnemonic_read_decoded}") + return mnemonic_read_decoded + + +def _storage_blocks_gen() -> Generator: + cap = sdcard.capacity() + if cap == 0: + raise ProcessError + BLOCK_END = cap // SDCARD_BLOCK_SIZE_B - 1 + return range(SDBACKUP_BLOCK_START, BLOCK_END, SDBACKUP_BLOCK_OFFSET) + +""" +Backup Memory Block Layout: ++----------------------+----------------------+----------------------+-------------------------------+ +| SDBACKUP_MAGIC (4B) | SDBACKUP_VERSION (2B)| SEED_LENGTH (4B) | MNEMONIC (variable length) | ++----------------------+----------------------+----------------------+-------------------------------+ +| CHECKSUM (4B) | Padding (variable) | ++----------------------------------------------------------------------------+----------------------------+ + +- SDBACKUP_MAGIC: 4 bytes magic number identifying the backup block +- SDBACKUP_VERSION: 2 bytes representing the version of the backup format +- SEED_LENGTH: 4 bytes (big-endian) indicating the length of the mnemonic +- MNEMONIC: Variable length field containing the mnemonic +- CHECKSUM: 4 bytes CRC32 checksum of all previous fields +- Padding: Remaining bytes of the block (if any) are padding + +The total size of the block is defined by SDCARD_BLOCK_SIZE_B. +""" +# Constants for offsets and lengths +MAGIC_OFFSET = 0 +MAGIC_LENGTH = 4 +VERSION_OFFSET = MAGIC_OFFSET + MAGIC_LENGTH +VERSION_LENGTH = 2 +SEED_LEN_OFFSET = VERSION_OFFSET + VERSION_LENGTH +SEED_LEN_LENGTH = 4 +MNEMONIC_OFFSET = SEED_LEN_OFFSET + SEED_LEN_LENGTH +CHECKSUM_LENGTH = 4 + + +def _encode_mnemonic_to_backup_block(mnemonic: bytes) -> bytes: + ret = bytearray(SDCARD_BLOCK_SIZE_B) + magic = SDBACKUP_MAGIC + SDBACKUP_VERSION + seed_len = len(mnemonic) + ret[MAGIC_OFFSET : MAGIC_OFFSET + MAGIC_LENGTH] = magic + ret[SEED_LEN_OFFSET : SEED_LEN_OFFSET + SEED_LEN_LENGTH] = seed_len.to_bytes( + SEED_LEN_LENGTH, "big" + ) + ret[MNEMONIC_OFFSET : MNEMONIC_OFFSET + seed_len] = mnemonic + checksum = crc.crc32(ret[: MNEMONIC_OFFSET + seed_len]) + ret[ + MNEMONIC_OFFSET + seed_len : MNEMONIC_OFFSET + seed_len + CHECKSUM_LENGTH + ] = checksum.to_bytes(CHECKSUM_LENGTH, "big") + return bytes(ret) + + +def _decode_mnemonic_from_backup_block(block: bytes) -> bytes | None: + assert len(block) == SDCARD_BLOCK_SIZE_B + if len(block) != SDCARD_BLOCK_SIZE_B: + return None + if block[MAGIC_OFFSET : MAGIC_OFFSET + MAGIC_LENGTH] != SDBACKUP_MAGIC: + return None + seed_len = int.from_bytes( + block[SEED_LEN_OFFSET : SEED_LEN_OFFSET + SEED_LEN_LENGTH], "big" + ) + checksum_expected = crc.crc32(block[: MNEMONIC_OFFSET + seed_len]) + checksum_read = int.from_bytes( + block[ + MNEMONIC_OFFSET + seed_len : MNEMONIC_OFFSET + seed_len + CHECKSUM_LENGTH + ], + "big", + ) + if checksum_expected == checksum_read: + return block[MNEMONIC_OFFSET : MNEMONIC_OFFSET + seed_len] + else: + return None + + +@with_filesystem +def _write_readme() -> None: + with fatfs.open("README.txt", "w") as f: + f.write("This is a Trezor backup SD card.") + + +@with_filesystem +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) + + +@with_filesystem +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 + # print(f"_read_seed_plain_text: mnemonic_read: {mnemonic_read}") + return mnemonic_read.decode("utf-8").rstrip("\x00") diff --git a/core/tests/test_storage.sd_seed_backup.py b/core/tests/test_storage.sd_seed_backup.py new file mode 100644 index 000000000..5a0923460 --- /dev/null +++ b/core/tests/test_storage.sd_seed_backup.py @@ -0,0 +1,29 @@ +from common import * + +from trezorio import sdcard, fatfs +from storage.sd_seed_backup import * + + +class TestStorageSdSeedBackup(unittest.TestCase): + # TODO add more tests, also with partly damaged backup + + def setUp(self): + self.mnemonic = ( + "crane mesh that gain predict open dice defy lottery toddler coin upgrade" + ) + + def test_backup_and_restore(self): + # with self.assertRaises(fatfs.FatFSError): + # store_seed_on_sdcard(self.mnemonic.encode("utf-8")) + + sdcard.power_on() + fatfs.mkfs(True) + success = store_seed_on_sdcard(self.mnemonic.encode("utf-8")) + self.assertTrue(success) + + restored = recover_seed_from_sdcard() + self.assertEqual(self.mnemonic, restored) + + +if __name__ == "__main__": + unittest.main()