feat(core/sdbackup): backup to unallocated space.

- backup and restore
- basic unit test
- WIP
pull/3441/head
obrusvit 6 months ago
parent 8f26f9d9a8
commit 83b4066f55

2
.gitignore vendored

@ -2,6 +2,8 @@
.cache/
.venv/
.idea/
.ycm_extra_conf.py
.vimspector.json
.mypy_cache/
.pytest_cache/
.vscode/

@ -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("", &params, 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)},

@ -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:
"""

@ -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

@ -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

@ -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.")

@ -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

@ -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")

@ -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()
Loading…
Cancel
Save