|
|
|
@ -1,65 +1,131 @@
|
|
|
|
|
from micropython import const
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
|
|
from trezor.wire import DataError
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from trezor.messages import ChangeLanguage, Success, TranslationDataAck
|
|
|
|
|
from trezor.messages import ChangeLanguage, Success
|
|
|
|
|
|
|
|
|
|
_CHUNK_SIZE = const(1024)
|
|
|
|
|
_DELIMITER = b"\x00"
|
|
|
|
|
_HEADER_SIZE = const(256)
|
|
|
|
|
_FILL_BYTE = b"\x00"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TranslationsHeader:
|
|
|
|
|
MAGIC = b"TRTR"
|
|
|
|
|
LANG_LEN = 32
|
|
|
|
|
VERSION_LEN = 16
|
|
|
|
|
DATA_HASH_LEN = 32
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
language: str,
|
|
|
|
|
version: str,
|
|
|
|
|
data_length: int,
|
|
|
|
|
items_num: int,
|
|
|
|
|
data_hash: bytes,
|
|
|
|
|
):
|
|
|
|
|
self.language = language
|
|
|
|
|
self.version = version
|
|
|
|
|
self.data_length = data_length
|
|
|
|
|
self.items_num = items_num
|
|
|
|
|
self.data_hash = data_hash
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def from_bytes(cls, data: bytes) -> "TranslationsHeader":
|
|
|
|
|
from trezor.utils import BufferReader
|
|
|
|
|
|
|
|
|
|
from apps.common import readers
|
|
|
|
|
|
|
|
|
|
if len(data) != _HEADER_SIZE:
|
|
|
|
|
raise DataError("Invalid header length")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
r = BufferReader(data)
|
|
|
|
|
|
|
|
|
|
magic = r.read(len(cls.MAGIC))
|
|
|
|
|
if magic != cls.MAGIC:
|
|
|
|
|
raise DataError("Invalid header magic")
|
|
|
|
|
|
|
|
|
|
version = r.read(cls.VERSION_LEN).rstrip(_FILL_BYTE).decode()
|
|
|
|
|
language = r.read(cls.LANG_LEN).rstrip(_FILL_BYTE).decode()
|
|
|
|
|
data_length = readers.read_uint16_le(r)
|
|
|
|
|
items_num = readers.read_uint16_le(r)
|
|
|
|
|
data_hash = r.read(cls.DATA_HASH_LEN)
|
|
|
|
|
|
|
|
|
|
# Rest must be empty bytes
|
|
|
|
|
for b in r.read():
|
|
|
|
|
if b != 0:
|
|
|
|
|
raise DataError("Invalid header data")
|
|
|
|
|
|
|
|
|
|
return cls(
|
|
|
|
|
language=language,
|
|
|
|
|
version=version,
|
|
|
|
|
data_length=data_length,
|
|
|
|
|
items_num=items_num,
|
|
|
|
|
data_hash=data_hash,
|
|
|
|
|
)
|
|
|
|
|
except EOFError:
|
|
|
|
|
raise DataError("Invalid header data")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def change_language(msg: ChangeLanguage) -> Success:
|
|
|
|
|
import storage.device as storage_device
|
|
|
|
|
import storage.translations as storage_translations
|
|
|
|
|
from trezor import wire
|
|
|
|
|
from trezor import translations
|
|
|
|
|
from trezor.messages import Success
|
|
|
|
|
|
|
|
|
|
language = msg.language # local_cache_attribute
|
|
|
|
|
data_length = msg.data_length # local_cache_attribute
|
|
|
|
|
|
|
|
|
|
if len(language) > storage_device.LANGUAGE_MAXLENGTH:
|
|
|
|
|
raise wire.DataError("Language identifier too long")
|
|
|
|
|
# When empty data, reverting the language to default (english)
|
|
|
|
|
if data_length == 0:
|
|
|
|
|
await _require_confirm_change_language("")
|
|
|
|
|
translations.wipe()
|
|
|
|
|
return Success(message="Language reverted to default")
|
|
|
|
|
|
|
|
|
|
if _DELIMITER.decode() in language:
|
|
|
|
|
raise wire.DataError(f"Language name contains delimiter '{_DELIMITER}'")
|
|
|
|
|
if data_length > translations.DATA_MAXLENGTH:
|
|
|
|
|
raise DataError("Translations too long")
|
|
|
|
|
if data_length < _HEADER_SIZE:
|
|
|
|
|
raise DataError("Translations too short")
|
|
|
|
|
|
|
|
|
|
if data_length > storage_translations.TRANSLATIONS_MAXLENGTH:
|
|
|
|
|
raise wire.DataError("Translations too long")
|
|
|
|
|
data_left = data_length
|
|
|
|
|
offset = 0
|
|
|
|
|
|
|
|
|
|
# When empty data, reverting the language to default (english)
|
|
|
|
|
if data_length == 0:
|
|
|
|
|
language = ""
|
|
|
|
|
# Getting and parsing the header
|
|
|
|
|
header_data = await get_data_chunk(_HEADER_SIZE, offset)
|
|
|
|
|
header = TranslationsHeader.from_bytes(header_data)
|
|
|
|
|
|
|
|
|
|
if header.data_length + _HEADER_SIZE != data_length:
|
|
|
|
|
raise DataError("Invalid header data length")
|
|
|
|
|
|
|
|
|
|
# TODO: verify the hash of the data (get all of them and hash them)
|
|
|
|
|
# TODO: verify the header signature (signature of sha256(header))
|
|
|
|
|
|
|
|
|
|
# Confirm with user and wipe old data
|
|
|
|
|
await _require_confirm_change_language(header.language)
|
|
|
|
|
translations.wipe()
|
|
|
|
|
|
|
|
|
|
await _require_confirm_change_language(language)
|
|
|
|
|
storage_translations.wipe()
|
|
|
|
|
# Write the header
|
|
|
|
|
translations.write(header_data, offset)
|
|
|
|
|
offset += len(header_data)
|
|
|
|
|
data_left -= len(header_data)
|
|
|
|
|
|
|
|
|
|
# Requesting the data in chunks and saving them
|
|
|
|
|
if data_length > 0:
|
|
|
|
|
offset = 0
|
|
|
|
|
data_left = data_length
|
|
|
|
|
# Store the language name as the first item
|
|
|
|
|
# (Done so that we can get the language name even after device/storage is wiped)
|
|
|
|
|
language_entry = language.encode() + _DELIMITER
|
|
|
|
|
storage_translations.write(language_entry, offset)
|
|
|
|
|
offset += len(language_entry)
|
|
|
|
|
while data_left > 0:
|
|
|
|
|
resp = await send_request_chunk(data_left)
|
|
|
|
|
data_left -= len(resp.data_chunk)
|
|
|
|
|
storage_translations.write(resp.data_chunk, offset)
|
|
|
|
|
offset += len(resp.data_chunk)
|
|
|
|
|
|
|
|
|
|
storage_device.set_language(language)
|
|
|
|
|
while data_left > 0:
|
|
|
|
|
data_chunk = await get_data_chunk(data_left, offset)
|
|
|
|
|
translations.write(data_chunk, offset)
|
|
|
|
|
data_left -= len(data_chunk)
|
|
|
|
|
offset += len(data_chunk)
|
|
|
|
|
|
|
|
|
|
return Success(message="Language changed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_request_chunk(data_left: int) -> TranslationDataAck:
|
|
|
|
|
async def get_data_chunk(data_left: int, offset: int) -> bytes:
|
|
|
|
|
from trezor.messages import TranslationDataAck, TranslationDataRequest
|
|
|
|
|
from trezor.wire.context import call
|
|
|
|
|
|
|
|
|
|
req = TranslationDataRequest()
|
|
|
|
|
req.data_length = min(data_left, _CHUNK_SIZE)
|
|
|
|
|
return await call(req, TranslationDataAck)
|
|
|
|
|
data_length = min(data_left, _CHUNK_SIZE)
|
|
|
|
|
req = TranslationDataRequest(data_length=data_length, data_offset=offset)
|
|
|
|
|
res = await call(req, TranslationDataAck)
|
|
|
|
|
return res.data_chunk
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _require_confirm_change_language(language: str) -> None:
|
|
|
|
|