mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-11-15 20:19:23 +00:00
feat(core/sdbackup): backup to unallocated space.
- backup and restore - basic unit test - WIP
This commit is contained in:
parent
8f26f9d9a8
commit
83b4066f55
2
.gitignore
vendored
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("", ¶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)},
|
||||
|
||||
|
@ -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
|
||||
|
174
core/src/storage/sd_seed_backup.py
Normal file
174
core/src/storage/sd_seed_backup.py
Normal file
@ -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")
|
29
core/tests/test_storage.sd_seed_backup.py
Normal file
29
core/tests/test_storage.sd_seed_backup.py
Normal file
@ -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…
Reference in New Issue
Block a user