WIP matejcik

WIP - translations now really build

WIP

WIP: Python part

WIP: core part

WIP

WIP - change_language.py

WIP - python imports

WIP - C side verification of write validity

WIP - loader for (at least) USB transfer part

WIP - tweaks

WIP - fix compilation on older nightly, clippy lints & style

WIP - translation data from upstream

WIP - improve blob generation

WIP - reorganize tests

WIP - ignore generated translation blobs
matejcik 4 months ago committed by grdddj
parent c5b76a90ae
commit 452633ef48

@ -10,6 +10,7 @@ static void _librust_qstrs(void) {
MP_QSTR_CONFIRMED;
MP_QSTR_INFO;
MP_QSTR_Layout;
MP_QSTR_MAX_HEADER_LEN;
MP_QSTR_MESSAGE_NAME;
MP_QSTR_MESSAGE_WIRE_TYPE;
MP_QSTR_Msg;
@ -29,7 +30,7 @@ static void _librust_qstrs(void) {
MP_QSTR_addr_mismatch__support_url;
MP_QSTR_addr_mismatch__title;
MP_QSTR_addr_mismatch__title_key_mismatch;
MP_QSTR_addr_mismatch__wrong_derication_path;
MP_QSTR_addr_mismatch__wrong_derivation_path;
MP_QSTR_addr_mismatch__xpub_mismatch;
MP_QSTR_address;
MP_QSTR_address__address;
@ -51,6 +52,7 @@ static void _librust_qstrs(void) {
MP_QSTR_amount_title;
MP_QSTR_amount_value;
MP_QSTR_app_name;
MP_QSTR_area_bytesize;
MP_QSTR_attach_timer_fn;
MP_QSTR_authenticate__confirm_template;
MP_QSTR_authenticate__header;
@ -179,6 +181,7 @@ static void _librust_qstrs(void) {
MP_QSTR_cardano__confirm_signing_stake_pool;
MP_QSTR_cardano__confirm_transaction;
MP_QSTR_cardano__confirming_a_multisig_transaction;
MP_QSTR_cardano__confirming_a_plutus_transaction;
MP_QSTR_cardano__confirming_pool_registration;
MP_QSTR_cardano__confirming_transction;
MP_QSTR_cardano__cost;
@ -307,10 +310,12 @@ static void _librust_qstrs(void) {
MP_QSTR_confirm_with_info;
MP_QSTR_count;
MP_QSTR_data;
MP_QSTR_data_length;
MP_QSTR_data_hash;
MP_QSTR_data_len;
MP_QSTR_debug__loading_seed;
MP_QSTR_debug__loading_seed_not_recommended;
MP_QSTR_decode;
MP_QSTR_deinit;
MP_QSTR_description;
MP_QSTR_details_title;
MP_QSTR_device_name__change_template;
@ -366,6 +371,7 @@ static void _librust_qstrs(void) {
MP_QSTR_eos__vote_for_proxy;
MP_QSTR_eos__voter;
MP_QSTR_eos__yes;
MP_QSTR_erase;
MP_QSTR_ethereum__amount_sent;
MP_QSTR_ethereum__confirm_fee;
MP_QSTR_ethereum__contract;
@ -424,7 +430,7 @@ static void _librust_qstrs(void) {
MP_QSTR_fingerprint;
MP_QSTR_firmware_update__title;
MP_QSTR_firmware_update__title_fingerprint;
MP_QSTR_header_length;
MP_QSTR_get_language;
MP_QSTR_hold;
MP_QSTR_hold_danger;
MP_QSTR_homescreen__click_to_connect;
@ -443,6 +449,7 @@ static void _librust_qstrs(void) {
MP_QSTR_image;
MP_QSTR_indeterminate;
MP_QSTR_info_button;
MP_QSTR_init;
MP_QSTR_inputs__back;
MP_QSTR_inputs__cancel;
MP_QSTR_inputs__delete;
@ -456,10 +463,10 @@ static void _librust_qstrs(void) {
MP_QSTR_joint__to_the_total_amount;
MP_QSTR_joint__you_are_contributing;
MP_QSTR_label;
MP_QSTR_language;
MP_QSTR_language__change_template;
MP_QSTR_language__set_default;
MP_QSTR_language__title_change;
MP_QSTR_language_name;
MP_QSTR_lines;
MP_QSTR_load_from_flash;
MP_QSTR_lockscreen__tap_to_connect;
@ -937,6 +944,7 @@ static void _librust_qstrs(void) {
MP_QSTR_total_amount;
MP_QSTR_total_fee_new;
MP_QSTR_total_label;
MP_QSTR_total_len;
MP_QSTR_touch_event;
MP_QSTR_trace;
MP_QSTR_trezorproto;
@ -964,6 +972,7 @@ static void _librust_qstrs(void) {
MP_QSTR_value;
MP_QSTR_verb;
MP_QSTR_verb_cancel;
MP_QSTR_verify;
MP_QSTR_version;
MP_QSTR_warning;
MP_QSTR_wipe__info;
@ -989,6 +998,7 @@ static void _librust_qstrs(void) {
MP_QSTR_word_count__title;
MP_QSTR_words;
MP_QSTR_words__account;
MP_QSTR_words__account_colon;
MP_QSTR_words__address;
MP_QSTR_words__amount;
MP_QSTR_words__are_you_sure;
@ -1027,6 +1037,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__warning;
MP_QSTR_words__writable;
MP_QSTR_words__yes;
MP_QSTR_write;
MP_QSTR_wrong_pin;
MP_QSTR_xpubs;
}

@ -30,7 +30,7 @@ pub enum Error {
#[macro_export]
macro_rules! value_error {
($msg:expr) => {
crate::error::Error::ValueError(cstr_core::cstr!($msg))
$crate::error::Error::ValueError(cstr_core::cstr!($msg))
};
}

@ -10,13 +10,16 @@ impl<'a> InputStream<'a> {
Self { buf, pos: 0 }
}
pub fn remaining(&self) -> usize {
self.buf.len().saturating_sub(self.pos)
}
pub fn tell(&self) -> usize {
self.pos
}
pub fn read_stream(&mut self, len: usize) -> Result<Self, Error> {
let buf = self
.buf
.get(self.pos..self.pos + len)
.ok_or(Error::EOFError)?;
self.pos += len;
Ok(Self::new(buf))
self.read(len).map(Self::new)
}
pub fn read(&mut self, len: usize) -> Result<&'a [u8], Error> {
@ -37,9 +40,9 @@ impl<'a> InputStream<'a> {
}
pub fn read_byte(&mut self) -> Result<u8, Error> {
let val = self.buf.get(self.pos).copied().ok_or(Error::EOFError)?;
let val = self.buf.get(self.pos).ok_or(Error::EOFError)?;
self.pos += 1;
Ok(val)
Ok(*val)
}
pub fn read_u16_le(&mut self) -> Result<u16, Error> {

@ -0,0 +1,381 @@
use core::{mem, str};
use crate::{
crypto::{cosi, ed25519, merkle::merkle_root, sha256},
error::Error,
io::InputStream,
};
use super::public_keys;
pub const MAX_HEADER_LEN: u16 = 1024;
pub const EMPTY_BYTE: u8 = 0xFF;
const SENTINEL_ID: u16 = 0xFFFF;
const SIGNATURE_THRESHOLD: u8 = 2;
// Maximum padding at the end of an offsets table (typically for alignment
// purposes). We allow at most 3 for alignment 4. In practice right now this
// should be max 1.
const MAX_TABLE_PADDING: usize = 3;
const INVALID_TRANSLATIONS_BLOB: Error = value_error!("Invalid translations blob");
#[repr(packed)]
struct OffsetEntry {
pub id: u16,
pub offset: u16,
}
pub struct Table<'a> {
offsets: &'a [OffsetEntry],
data: &'a [u8],
}
fn validate_offset_table(
data_len: usize,
mut iter: impl Iterator<Item = u16>,
) -> Result<(), Error> {
// every offset table must have at least the sentinel
let mut prev = iter.next().ok_or(value_error!("offset table too short"))?;
for next in iter {
// offsets must be in ascending order
if prev > next {
return Err(value_error!("offsets not in ascending order"));
}
prev = next;
}
// sentinel needs to be at least data_len - MAX_TABLE_PADDING, and at most
// data_len
let sentinel = prev as usize;
if sentinel < data_len - MAX_TABLE_PADDING || sentinel > data_len {
return Err(value_error!("invalid sentinel offset"));
}
Ok(())
}
impl<'a> Table<'a> {
pub fn new(mut reader: InputStream<'a>) -> Result<Self, Error> {
let count = reader.read_u16_le()?;
// The offsets table is (count + 1) entries long, the last entry is a sentinel.
let offsets_data = reader.read((count + 1) as usize * mem::size_of::<OffsetEntry>())?;
// SAFETY: OffsetEntry is repr(packed) of two u16 values, so any four bytes are
// a valid OffsetEntry value.
let (_prefix, offsets, _suffix) = unsafe { offsets_data.align_to::<OffsetEntry>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(value_error!("misaligned offsets table"));
}
Ok(Self {
offsets,
data: reader.rest(),
})
}
pub fn validate(&self) -> Result<(), Error> {
validate_offset_table(self.data.len(), self.offsets.iter().map(|it| it.offset))?;
if !matches!(
self.offsets.iter().last().map(|it| it.id),
Some(SENTINEL_ID)
) {
return Err(value_error!("invalid sentinel id"));
}
Ok(())
}
pub fn get(&self, id: u16) -> Option<&'a [u8]> {
self.offsets
.iter()
.position(|it| it.id == id)
.and_then(|idx| {
let start = self.offsets[idx].offset as usize;
let end = self.offsets[idx + 1].offset as usize;
self.data.get(start..end)
})
}
pub fn iter(&self) -> impl Iterator<Item = (u16, &'a [u8])> + '_ {
let mut prev_offset = 0usize;
self.offsets.iter().skip(1).map(move |entry| {
let start = prev_offset;
let end = entry.offset as usize;
prev_offset = end;
(entry.id, &self.data[start..end])
})
}
}
pub struct Translations<'a> {
pub header: TranslationsHeader<'a>,
translations: &'a [u8],
translations_offsets: &'a [u16],
fonts: Table<'a>,
}
fn read_u16_prefixed_block<'a>(reader: &mut InputStream<'a>) -> Result<InputStream<'a>, Error> {
let len = reader.read_u16_le()? as usize;
reader.read_stream(len)
}
impl<'a> Translations<'a> {
const MAGIC: &'static [u8] = b"TRTR00";
pub fn new(blob: &'a [u8]) -> Result<Self, Error> {
let mut blob_reader = InputStream::new(blob);
let (header, payload_reader) = TranslationsHeader::parse_from(&mut blob_reader)?;
// validate that the trailing bytes, if any, are empty
let remaining = blob_reader.rest();
if !remaining.iter().all(|&b| b == EMPTY_BYTE) {
// TODO optimize to quadwords?
return Err(value_error!("Trailing data in translations blob"));
}
let payload_bytes = payload_reader.rest();
let payload_digest = sha256::digest(payload_bytes);
if payload_digest != header.data_hash {
return Err(value_error!("hash mismatch"));
}
let mut payload_reader = InputStream::new(payload_bytes);
let mut translations_reader = read_u16_prefixed_block(&mut payload_reader)?;
let fonts_reader = read_u16_prefixed_block(&mut payload_reader)?;
if payload_reader.remaining() > 0 {
return Err(value_error!("Trailing data in translations blob"));
}
// construct translations data
let translations_count = translations_reader.read_u16_le()? as usize;
let translations_offsets_bytes =
translations_reader.read((translations_count + 1) * mem::size_of::<u16>())?;
// SAFETY: any bytes are valid u16 values, so casting any data to
// a sequence of u16 values is safe.
let (_prefix, translations_offsets, _suffix) =
unsafe { translations_offsets_bytes.align_to::<u16>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(value_error!("Invalid translations table"));
}
let translations = translations_reader.rest();
validate_offset_table(translations.len(), translations_offsets.iter().copied())?;
// construct and validate font table
let fonts = Table::new(fonts_reader)?;
fonts.validate()?;
for (_, font_data) in fonts.iter() {
let reader = InputStream::new(font_data);
let font_table = Table::new(reader)?;
font_table.validate()?;
}
Ok(Self {
header,
translations,
translations_offsets,
fonts,
})
}
/// Returns the translation at the given index.
pub fn translation(&self, index: usize) -> Option<&str> {
if index + 1 >= self.translations_offsets.len() {
// The index is out of bounds.
// (The last entry is a sentinel, so the last valid index is len - 2)
// May happen when new firmware is using older translations and the string
// is not defined yet.
// Fallback to english.
return None;
}
let start_offset = self.translations_offsets[index] as usize;
let end_offset = self.translations_offsets[index + 1] as usize;
// Construct the relevant slice
let string = &self.translations[start_offset..end_offset];
if string.is_empty() {
// The string is not defined in the blob.
// May happen when old firmware is using newer translations and the string
// was deleted in the newer version.
// Fallback to english.
return None;
}
str::from_utf8(string).ok()
}
pub fn font(&'a self, index: u16) -> Option<Table<'a>> {
self.fonts
.get(index)
.and_then(|data| Table::new(InputStream::new(data)).ok())
}
}
pub struct TranslationsHeader<'a> {
/// Raw content of the header, for signature verification
pub header_bytes: &'a [u8],
/// Human readable language identifier (e.g. "cs" of "fr")
pub language: &'a str,
/// 4 bytes of version (major, minor, patch, build)
pub version: [u8; 4],
/// Length of the raw data, i.e. translations section + fonts section
pub data_len: usize,
/// Hash of the data blob (excluding the header)
pub data_hash: sha256::Digest,
/// Title to show user before changing the language
pub change_language_title: &'a str,
/// Text to show user before changing the language
pub change_language_prompt: &'a str,
/// Merkle proof items
pub merkle_proof: &'a [sha256::Digest],
/// CoSi signature
pub signature: cosi::Signature,
/// Expected total length of the blob
pub total_len: usize,
}
fn read_fixedsize_str<'a>(reader: &mut InputStream<'a>, len: usize) -> Result<&'a str, Error> {
let bytes = reader.read(len)?;
core::str::from_utf8(bytes).map_err(|_| value_error!("invalid fixedsize string"))
}
fn read_pascal_str<'a>(reader: &mut InputStream<'a>) -> Result<&'a str, Error> {
let len = reader.read_byte()? as usize;
read_fixedsize_str(reader, len)
}
impl<'a> TranslationsHeader<'a> {
const BLOB_MAGIC: &'static [u8] = b"TRTR00";
const HEADER_MAGIC: &'static [u8] = b"TR";
/// Parse a translations header out of a stream.
///
/// The returned tuple consists of:
/// (a) the parsed header and
/// (b) reader of the payload section of the translations blob.
/// The caller can use the returned reader to parse the payload.
///
/// The input stream is positioned at the end of the translations blob (or
/// at the end of stream, whichever comes sooner). The caller can use this
/// to verify that there is no unexpected trailing data in the input
/// stream. (Also, you cannot make a mistake and read the payload out of
/// the input stream).
pub fn parse_from(reader: &mut InputStream<'a>) -> Result<(Self, InputStream<'a>), Error> {
//
// 1. parse outer container
//
let magic = reader.read(Self::BLOB_MAGIC.len())?;
if magic != Self::BLOB_MAGIC {
return Err(value_error!("invalid header magic"));
}
// read length of contained data
let container_length = reader.read_u16_le()? as usize;
// continue working on the contained data (i.e., read beyond the bounds of
// container_length will result in EOF).
let mut reader = reader.read_stream(container_length.min(reader.remaining()))?;
//
// 2. parse the header section
//
let header_bytes = read_u16_prefixed_block(&mut reader)?.rest();
let mut header_reader = InputStream::new(header_bytes);
let magic = header_reader.read(Self::HEADER_MAGIC.len())?;
if magic != Self::HEADER_MAGIC {
return Err(value_error!("bad header magic"));
}
let language = read_fixedsize_str(&mut header_reader, 4)?;
let model = read_fixedsize_str(&mut header_reader, 4)?;
if model != crate::trezorhal::model::INTERNAL_NAME {
return Err(value_error!("Wrong Trezor model"));
}
let version_bytes = header_reader.read(4)?;
let version = unwrap!(version_bytes.try_into());
let data_len = header_reader.read_u16_le()? as usize;
let data_hash: sha256::Digest =
unwrap!(header_reader.read(sha256::DIGEST_SIZE)?.try_into());
let change_language_title = read_pascal_str(&mut header_reader)?;
let change_language_prompt = read_pascal_str(&mut header_reader)?;
// ignore the rest of the header reader - this allows older firmware to
// understand newer header if there are only added items
_ = header_reader.rest();
//
// 3. parse the proof section
//
let mut proof_reader = read_u16_prefixed_block(&mut reader)?;
let proof_count = proof_reader.read_byte()? as usize;
let proof_length = proof_count * sha256::DIGEST_SIZE;
let proof_bytes = proof_reader.read(proof_length)?;
// create a list of the proof items
// SAFETY: sha256::Digest is a plain array of u8, so any bytes are valid
let (_prefix, merkle_proof, _suffix) = unsafe { proof_bytes.align_to::<sha256::Digest>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(value_error!("misaligned proof table"));
}
let signature = cosi::Signature::new(
proof_reader.read_byte()?,
unwrap!(proof_reader.read(ed25519::SIGNATURE_SIZE)?.try_into()),
);
// check that there is no trailing data in the proof section
if proof_reader.remaining() > 0 {
return Err(value_error!("trailing data in proof"));
}
// check that the declared data section length matches the container size
if container_length - reader.tell() != data_len {
println!("container length: ", heapless::String::<10>::from(container_length as u32).as_str());
println!("reader pos: ", heapless::String::<10>::from(reader.tell() as u32).as_str());
println!("data_len: ", heapless::String::<10>::from(data_len as u32).as_str());
return Err(value_error!("data length mismatch"));
}
let new = Self {
header_bytes,
language,
version,
data_len,
data_hash,
change_language_title,
change_language_prompt,
merkle_proof,
signature,
total_len: container_length + Self::BLOB_MAGIC.len() + mem::size_of::<u16>(),
};
new.verify()?;
Ok((new, reader))
}
fn verify_with_keys(&self, public_keys: &[ed25519::PublicKey]) -> Result<(), Error> {
let merkle_root = merkle_root(self.header_bytes, self.merkle_proof);
Ok(cosi::verify(
SIGNATURE_THRESHOLD,
&merkle_root,
public_keys,
&self.signature,
)?)
}
pub fn verify(&self) -> Result<(), Error> {
let mut result = self.verify_with_keys(&public_keys::PUBLIC_KEYS);
#[cfg(feature = "debug")]
if result.is_err() {
// allow development keys
result = self.verify_with_keys(&public_keys::PUBLIC_KEYS_DEVEL);
}
result
}
}

@ -1,6 +1,6 @@
use crate::{error::Error, trezorhal::translations};
use super::Translations;
use super::blob::Translations;
static mut TRANSLATIONS_ON_FLASH: Option<Translations> = None;
@ -22,22 +22,46 @@ pub fn write(data: &[u8], offset: usize) -> Result<(), Error> {
}
// SAFETY: The blob is not set, so there are no references to it.
unsafe { translations::write(data, offset) };
Ok(())
let result = unsafe { translations::write(data, offset) };
if result {
Ok(())
} else {
Err(value_error!("Failed to write translations blob"))
}
}
/// Load translations from flash, validate, and cache references to lookup
/// tables.
pub fn init() -> Result<(), Error> {
if unsafe { TRANSLATIONS_ON_FLASH.is_some() } {
return Ok(());
}
unsafe fn try_init<'a>() -> Result<Option<Translations<'a>>, Error> {
// load from flash
let flash_data = unsafe { translations::get_blob() };
todo!();
let blob = Translations::new(flash_data);
// SAFETY: TODO
unsafe { TRANSLATIONS_ON_FLASH = blob.ok() };
Ok(())
// check if flash is empty
// TODO perhaps we should check the full area?
if flash_data[0..16] == [super::blob::EMPTY_BYTE; 16] {
return Ok(None);
}
// try to parse the data
Translations::new(flash_data).map(Some)
}
pub fn init() {
// unsafe block because every individual operation here is unsafe
unsafe {
// SAFETY: it is OK to look
if TRANSLATIONS_ON_FLASH.is_some() {
return;
}
// SAFETY: try_init unconditionally loads the translations from flash.
// No other reference exists (TRANSLATIONS_ON_FLASH is None) so this is safe.
match try_init() {
// SAFETY: We are in a single-threaded environment so setting is OK.
// (note that from this point on a reference to flash data is held)
Ok(Some(t)) => TRANSLATIONS_ON_FLASH = Some(t),
Ok(None) => {}
// SAFETY: No reference to flash data exists so it is OK to erase it.
Err(_) => translations::erase(),
}
}
}
// SAFETY: Invalidates all references coming from the flash-based blob.

@ -1,12 +1,13 @@
//! generated from translated_string.rs.mako
//! (by running `make templates` in `core`)
//! do not edit manually!
#![cfg_attr(rustfmt, rustfmt_skip)]
#[cfg(feature = "micropython")]
use crate::micropython::qstr::Qstr;
#[derive(Debug, Copy, Clone, FromPrimitive)]
#[repr(u16)]
#[rustfmt::skip]
#[allow(non_camel_case_types)]
pub enum TranslatedString {
addr_mismatch__contact_support_at = 0,
@ -15,7 +16,7 @@ pub enum TranslatedString {
addr_mismatch__support_url = 3,
addr_mismatch__title = 4,
addr_mismatch__title_key_mismatch = 5,
addr_mismatch__wrong_derication_path = 6,
addr_mismatch__wrong_derivation_path = 6,
addr_mismatch__xpub_mismatch = 7,
address__address = 8,
address__public_key = 9,
@ -869,6 +870,8 @@ pub enum TranslatedString {
words__confirm_fee = 857,
words__no = 858,
words__yes = 859,
words__account_colon = 860,
cardano__confirming_a_plutus_transaction = 861,
}
impl TranslatedString {
@ -878,16 +881,16 @@ impl TranslatedString {
Self::addr_mismatch__key_mismatch => "Key mismatch?",
Self::addr_mismatch__mismatch => "Address mismatch?",
Self::addr_mismatch__support_url => "trezor.io/support",
Self::addr_mismatch__title => "ADDRESS MISMATCH?",
Self::addr_mismatch__title_key_mismatch => "KEY MISMATCH?",
Self::addr_mismatch__wrong_derication_path => "Wrong derivation path for selected account.",
Self::addr_mismatch__title => "\"\"",
Self::addr_mismatch__title_key_mismatch => "\"\"",
Self::addr_mismatch__wrong_derivation_path => "Wrong derivation path for selected account.",
Self::addr_mismatch__xpub_mismatch => "XPUB mismatch?",
Self::address__address => "",
Self::address__address => "\"\"",
Self::address__public_key => "Public key",
Self::address__title_cosigner => "COSIGNER",
Self::address__title_receive_address => "RECEIVE ADDRESS",
Self::address__title_yours => "YOURS",
Self::address_details__account => "",
Self::address_details__account => "\"\"",
Self::address_details__derivation_path => "Derivation path:",
Self::address_details__title_receive_address => "RECEIVE ADDRESS",
Self::address_details__title_receiving_to => "RECEIVING TO",
@ -916,7 +919,7 @@ impl TranslatedString {
Self::binance__sell => "Sell",
Self::binance__sender_address => "Sender address:",
Self::binance__side => "Side:",
Self::binance__unknown => "",
Self::binance__unknown => "\"\"",
Self::bitcoin__commitment_data => "Commitment data:",
Self::bitcoin__confirm_locktime => "Confirm locktime",
Self::bitcoin__create_proof_of_ownership => "Do you want to create a proof of ownership?",
@ -986,7 +989,7 @@ impl TranslatedString {
Self::cardano__addr_pointer => "Pointer",
Self::cardano__addr_reward => "Reward",
Self::cardano__address_no_staking => "address - no staking rewards.",
Self::cardano__amount => "",
Self::cardano__amount => "\"\"",
Self::cardano__amount_burned_decimals_unknown => "Amount burned (decimals unknown):",
Self::cardano__amount_minted_decimals_unknown => "Amount minted (decimals unknown):",
Self::cardano__amount_sent_decimals_unknown => "Amount sent (decimals unknown):",
@ -996,10 +999,10 @@ impl TranslatedString {
Self::cardano__block => "Block",
Self::cardano__catalyst => "Catalyst",
Self::cardano__certificate => "Certificate",
Self::cardano__certificate_path => "Certificate path",
Self::cardano__certificate_path => "\"\"",
Self::cardano__change_output => "Change output",
Self::cardano__change_output_path => "Change output path",
Self::cardano__change_output_staking_path => "Change output staking path",
Self::cardano__change_output_path => "\"\"",
Self::cardano__change_output_staking_path => "\"\"",
Self::cardano__check_all_items => "Check all items carefully.",
Self::cardano__choose_level_of_details => "Choose level of details:",
Self::cardano__collateral_input_id => "Collateral input ID:",
@ -1023,7 +1026,7 @@ impl TranslatedString {
Self::cardano__inline_datum => "Inline datum",
Self::cardano__input_id => "Input ID:",
Self::cardano__input_index => "Input index:",
Self::cardano__intro_text_address => "",
Self::cardano__intro_text_address => "\"\"",
Self::cardano__intro_text_change => "The following address is a change address. Its",
Self::cardano__intro_text_owned_by_device => "The following address is owned by this device. Its",
Self::cardano__intro_text_registration_payment => "The vote key registration payment address is owned by this device. Its",
@ -1042,7 +1045,7 @@ impl TranslatedString {
Self::cardano__pool_metadata_hash => "Pool metadata hash:",
Self::cardano__pool_metadata_url => "Pool metadata url:",
Self::cardano__pool_owner => "Pool owner:",
Self::cardano__pool_owner_path => "Pool owner staking path",
Self::cardano__pool_owner_path => "\"\"",
Self::cardano__pool_reward_account => "Pool reward account:",
Self::cardano__reference_input_id => "Reference input ID:",
Self::cardano__reference_input_index => "Reference input index:",
@ -1083,7 +1086,7 @@ impl TranslatedString {
Self::cardano__transaction_no_script_data_hash => "The transaction contains no script data hash. Plutus script will not be able to run.",
Self::cardano__transaction_output_contains_tokens => "The following transaction output contains tokens.",
Self::cardano__ttl => "TTL:",
Self::cardano__unknown => "",
Self::cardano__unknown => "\"\"",
Self::cardano__unknown_collateral_amount => "Unknown collateral amount.",
Self::cardano__unusual_path => "Path is unusual.",
Self::cardano__valid_since => "Valid since:",
@ -1094,7 +1097,7 @@ impl TranslatedString {
Self::cardano__warning => "Warning",
Self::cardano__weight => "Weight:",
Self::cardano__withdrawal_for_address_template => "Confirm withdrawal for {} address:",
Self::cardano__witness_path => "Witness path",
Self::cardano__witness_path => "\"\"",
Self::cardano__x_of_y_signatures_template => "Requires {} out of {} signatures.",
Self::coinjoin__access_account => "Access your coinjoin account?",
Self::coinjoin__do_not_disconnect => "Do not disconnect your Trezor!",
@ -1104,7 +1107,7 @@ impl TranslatedString {
Self::coinjoin__title_do_not_disconnect => "DO NOT DISCONNECT YOUR TREZOR!",
Self::coinjoin__title_progress => "COINJOIN IN PROGRESS",
Self::coinjoin__waiting_for_others => "Waiting for others",
Self::confirm_total__account => "",
Self::confirm_total__account => "\"\"",
Self::confirm_total__fee_rate => "Fee rate:",
Self::confirm_total__sending_from_account => "Sending from account:",
Self::confirm_total__title_fee => "FEE INFORMATION",
@ -1117,9 +1120,9 @@ impl TranslatedString {
Self::entropy__title => "INTERNAL ENTROPY",
Self::entropy__title_confirm => "CONFIRM ENTROPY",
Self::eos__about_to_sign_template => "You are about to sign {}.",
Self::eos__account => "",
Self::eos__account => "\"\"",
Self::eos__action_name => "Action Name:",
Self::eos__amount => "",
Self::eos__amount => "\"\"",
Self::eos__arbitrary_data => "Arbitrary data",
Self::eos__buy_ram => "Buy RAM",
Self::eos__bytes => "Bytes:",
@ -1137,7 +1140,7 @@ impl TranslatedString {
Self::eos__name => "Name:",
Self::eos__net => "NET:",
Self::eos__new_account => "New account",
Self::eos__no => "",
Self::eos__no => "\"\"",
Self::eos__owner => "Owner:",
Self::eos__parent => "Parent:",
Self::eos__payer => "Payer:",
@ -1159,9 +1162,9 @@ impl TranslatedString {
Self::eos__vote_for_producers => "Vote for producers",
Self::eos__vote_for_proxy => "Vote for proxy",
Self::eos__voter => "Voter:",
Self::eos__yes => "",
Self::eos__yes => "\"\"",
Self::ethereum__amount_sent => "Amount sent:",
Self::ethereum__confirm_fee => "",
Self::ethereum__confirm_fee => "\"\"",
Self::ethereum__contract => "Contract:",
Self::ethereum__data_size_template => "Size: {} bytes",
Self::ethereum__gas_limit => "Gas limit:",
@ -1221,7 +1224,7 @@ impl TranslatedString {
Self::homescreen__title_no_usb_connection => "NO USB CONNECTION",
Self::homescreen__title_pin_not_set => "PIN NOT SET",
Self::homescreen__title_seedless => "SEEDLESS",
Self::homescreen__title_set => "CHANGE HOMESCREEN?",
Self::homescreen__title_set => "CHANGE HOMESCREEN",
Self::inputs__back => "BACK",
Self::inputs__cancel => "CANCEL",
Self::inputs__delete => "DELETE",
@ -1232,9 +1235,9 @@ impl TranslatedString {
Self::joint__title => "JOINT TRANSACTION",
Self::joint__to_the_total_amount => "To the total amount:",
Self::joint__you_are_contributing => "You are contributing:",
Self::language__change_template => "",
Self::language__set_default => "",
Self::language__title_change => "",
Self::language__change_template => "\"\"",
Self::language__set_default => "\"\"",
Self::language__title_change => "\"\"",
Self::lockscreen__tap_to_connect => "Tap to connect",
Self::lockscreen__tap_to_unlock => "Tap to unlock",
Self::lockscreen__title_locked => "LOCKED",
@ -1242,7 +1245,7 @@ impl TranslatedString {
Self::misc__decrypt_value => "Decrypt value",
Self::misc__encrypt_value => "Encrypt value",
Self::misc__title_suite_labeling => "SUITE LABELING",
Self::modify_amount__address => "",
Self::modify_amount__address => "\"\"",
Self::modify_amount__decrease_amount => "Decrease amount by:",
Self::modify_amount__increase_amount => "Increase amount by:",
Self::modify_amount__new_amount => "New amount:",
@ -1251,11 +1254,11 @@ impl TranslatedString {
Self::modify_fee__fee_rate => "Fee rate:",
Self::modify_fee__increase_fee => "Increase fee by:",
Self::modify_fee__new_transaction_fee => "New transaction fee:",
Self::modify_fee__no_change => "Your fee did not change.",
Self::modify_fee__no_change => "Fee did not change.",
Self::modify_fee__title => "MODIFY FEE",
Self::modify_fee__transaction_fee => "Transaction fee:",
Self::monero__confirm_export => "Confirm export",
Self::monero__confirm_fee => "",
Self::monero__confirm_fee => "\"\"",
Self::monero__confirm_ki_sync => "Confirm ki sync",
Self::monero__confirm_refresh => "Confirm refresh",
Self::monero__confirm_unlock_time => "Confirm unlock time",
@ -1279,7 +1282,7 @@ impl TranslatedString {
Self::nem__confirm_action => "Confirm action",
Self::nem__confirm_address => "Confirm address",
Self::nem__confirm_creation_fee => "Confirm creation fee",
Self::nem__confirm_fee => "",
Self::nem__confirm_fee => "\"\"",
Self::nem__confirm_mosaic => "Confirm mosaic",
Self::nem__confirm_multisig_fee => "Confirm multisig fee",
Self::nem__confirm_namespace => "Confirm namespace",
@ -1312,7 +1315,7 @@ impl TranslatedString {
Self::nem__modify_supply_for => "Modify supply for",
Self::nem__modify_the_number_of_cosignatories_by => "Modify the number of cosignatories by ",
Self::nem__mutable => "mutable",
Self::nem__no => "",
Self::nem__no => "\"\"",
Self::nem__of => "of",
Self::nem__percentile => "percentile",
Self::nem__raw_units_template => "{} raw units",
@ -1326,7 +1329,7 @@ impl TranslatedString {
Self::nem__under_namespace => "under namespace",
Self::nem__unencrypted => "Unencrypted:",
Self::nem__unknown_mosaic => "Unknown mosaic!",
Self::nem__yes => "",
Self::nem__yes => "\"\"",
Self::passphrase__access_hidden_wallet => "Access hidden wallet?",
Self::passphrase__always_on_device => "Do you really want to enter passphrase always on the device?",
Self::passphrase__from_host_not_shown => "Passphrase provided by host will be used but will not be displayed due to the device settings.",
@ -1354,7 +1357,7 @@ impl TranslatedString {
Self::pin__info => "PIN will be required to access this device.",
Self::pin__invalid_pin => "Invalid PIN",
Self::pin__last_attempt => "Last attempt",
Self::pin__mismatch => "Entered PINs do not match!",
Self::pin__mismatch => "The PINs you entered do not match.",
Self::pin__pin_mismatch => "PIN mismatch",
Self::pin__please_check_again => "Please check again.",
Self::pin__reenter_new => "Re-enter new PIN",
@ -1524,17 +1527,17 @@ impl TranslatedString {
Self::sd_card__wanna_format => "Do you really want to format the SD card?",
Self::sd_card__wrong_sd_card => "Wrong SD card.",
Self::send__address_path => "address path",
Self::send__amount => "",
Self::send__amount => "\"\"",
Self::send__confirm_sending => "SENDING AMOUNT",
Self::send__from_multiple_accounts => "Sending from multiple accounts.",
Self::send__including_fee => "Including fee:",
Self::send__maximum_fee => "Maximum fee:",
Self::send__receiving_to_multisig => "Receiving to a multisig address.",
Self::send__title_amount => "",
Self::send__title_amount => "\"\"",
Self::send__title_confirm_sending => "CONFIRM SENDING",
Self::send__title_joint_transaction => "JOINT TRANSACTION",
Self::send__title_receiving_to => "RECEIVING TO",
Self::send__title_recipient => "",
Self::send__title_recipient => "\"\"",
Self::send__title_sending => "SENDING",
Self::send__title_sending_amount => "SENDING AMOUNT",
Self::send__title_sending_to => "SENDING TO",
@ -1549,7 +1552,7 @@ impl TranslatedString {
Self::sign_message__confirm_message => "CONFIRM MESSAGE",
Self::sign_message__message_size => "Message size:",
Self::sign_message__verify_address => "VERIFY ADDRESS",
Self::stellar__account => "",
Self::stellar__account => "\"\"",
Self::stellar__account_merge => "Account Merge",
Self::stellar__account_thresholds => "Account Thresholds",
Self::stellar__add_signer => "Add Signer",
@ -1616,15 +1619,15 @@ impl TranslatedString {
Self::stellar__value_sha256 => "Value (SHA-256):",
Self::stellar__wanna_clean_value_key_template => "Do you want to clear value key {}?",
Self::stellar__your_account => " your account",
Self::tezos__address => "",
Self::tezos__amount => "",
Self::tezos__address => "\"\"",
Self::tezos__amount => "\"\"",
Self::tezos__baker_address => "Baker address:",
Self::tezos__balance => "Balance:",
Self::tezos__ballot => "Ballot:",
Self::tezos__confirm_delegation => "Confirm delegation",
Self::tezos__confirm_origination => "Confirm origination",
Self::tezos__delegator => "Delegator:",
Self::tezos__fee => "",
Self::tezos__fee => "\"\"",
Self::tezos__proposal => "Proposal",
Self::tezos__register_delegate => "Register delegate",
Self::tezos__remove_delegation => "Remove delegation",
@ -1657,7 +1660,7 @@ impl TranslatedString {
Self::wipe_code__enter_new => "Enter new wipe code",
Self::wipe_code__info => "Wipe code can be used to erase all data from this device.",
Self::wipe_code__invalid => "Invalid wipe code",
Self::wipe_code__mismatch => "Entered wipe codes do not match!",
Self::wipe_code__mismatch => "The wipe codes you entered do not match.",
Self::wipe_code__reenter => "Re-enter wipe code",
Self::wipe_code__reenter_to_confirm => "Please re-enter wipe code to confirm.",
Self::wipe_code__title_check => "CHECK WIPE CODE",
@ -1734,6 +1737,8 @@ impl TranslatedString {
Self::words__confirm_fee => "Confirm fee",
Self::words__no => "No",
Self::words__yes => "Yes",
Self::words__account_colon => "Account:",
Self::cardano__confirming_a_plutus_transaction => "Confirming a Plutus transaction.",
}
}
@ -1746,7 +1751,7 @@ impl TranslatedString {
Qstr::MP_QSTR_addr_mismatch__support_url => Some(Self::addr_mismatch__support_url),
Qstr::MP_QSTR_addr_mismatch__title => Some(Self::addr_mismatch__title),
Qstr::MP_QSTR_addr_mismatch__title_key_mismatch => Some(Self::addr_mismatch__title_key_mismatch),
Qstr::MP_QSTR_addr_mismatch__wrong_derication_path => Some(Self::addr_mismatch__wrong_derication_path),
Qstr::MP_QSTR_addr_mismatch__wrong_derivation_path => Some(Self::addr_mismatch__wrong_derivation_path),
Qstr::MP_QSTR_addr_mismatch__xpub_mismatch => Some(Self::addr_mismatch__xpub_mismatch),
Qstr::MP_QSTR_address__address => Some(Self::address__address),
Qstr::MP_QSTR_address__public_key => Some(Self::address__public_key),
@ -2600,6 +2605,8 @@ impl TranslatedString {
Qstr::MP_QSTR_words__confirm_fee => Some(Self::words__confirm_fee),
Qstr::MP_QSTR_words__no => Some(Self::words__no),
Qstr::MP_QSTR_words__yes => Some(Self::words__yes),
Qstr::MP_QSTR_words__account_colon => Some(Self::words__account_colon),
Qstr::MP_QSTR_cardano__confirming_a_plutus_transaction => Some(Self::cardano__confirming_a_plutus_transaction),
_ => None,
}
}

@ -1,6 +1,8 @@
//! generated from ${THIS_FILE.name}
//! (by running `make templates` in `core`)
//! do not edit manually!
#![cfg_attr(rustfmt, rustfmt_skip)]
<%
import json
@ -14,23 +16,12 @@ order = {int(k): v for k, v in order_index_name.items()}
en_file = TR_DIR / "en.json"
en_data = json.loads(en_file.read_text())["translations"]
def get_en_strings(data: dict) -> dict[str, str]:
res = {}
for section_name, section in data.items():
for k, v in section.items():
key = f"{section_name}__{k}"
res[key] = json.dumps(v)
return res
en_strings = get_en_strings(en_data)
%>\
#[cfg(feature = "micropython")]
use crate::micropython::qstr::Qstr;
#[derive(Debug, Copy, Clone, FromPrimitive)]
#[repr(u16)]
#[rustfmt::skip]
#[allow(non_camel_case_types)]
pub enum TranslatedString {
% for idx, name in order.items():
@ -42,7 +33,7 @@ impl TranslatedString {
pub fn untranslated(self) -> &'static str {
match self {
% for name in order.values():
Self::${name} => ${en_strings.get(name, '""')},
Self::${name} => ${json.dumps(en_data.get(name, '""'))},
% endfor
}
}

@ -1,241 +1,30 @@
mod blob;
mod flash;
mod generated;
#[cfg(feature = "micropython")]
mod micropython;
mod obj;
mod public_keys;
mod translated_string;
pub use blob::MAX_HEADER_LEN;
pub use translated_string::TranslatedString as TR;
pub const DEFAULT_LANGUAGE: &str = "enUS";
use crate::{error::Error, io::InputStream};
use core::{ptr::null, str};
const HEADER_LEN: usize = 256;
// TODO: somehow make it a singleton object, so it is loaded just once
// TODO: split it into header.rs, translations.rs and font.rs?
// TODO: --- put the {en,cs,fr}.* files to `languages` dir?
// TODO: not generate fr/cs.rs files not to confuse, rather do .txt?
// TODO: add some tests?
#[repr(packed)]
struct OffsetEntry {
pub id: u16,
pub offset: u16,
}
struct Table<'a> {
offsets: &'a [OffsetEntry],
data: &'a [u8],
}
impl<'a> Table<'a> {
pub fn new(data: &'a [u8]) -> Result<Self, Error> {
let mut reader = crate::io::InputStream::new(data);
let count = reader.read_u16_le()?;
// The offsets table is (count + 1) entries long, the last entry is a sentinel.
let offsets_data =
reader.read((count + 1) as usize * core::mem::size_of::<OffsetEntry>())?;
// SAFETY: OffsetEntry is repr(packed) of two u16 values, so any four bytes are
// a valid OffsetEntry value.
let (_prefix, offsets, _suffix) = unsafe { offsets_data.align_to::<OffsetEntry>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(value_error!("Invalid offsets table"));
}
Ok(Self {
offsets,
data: reader.rest(),
})
}
pub fn get(&self, id: u16) -> Option<&'a [u8]> {
self.offsets
.iter()
.position(|it| it.id == id)
.and_then(|idx| {
let start = self.offsets[idx].offset as usize;
let end = self.offsets[idx + 1].offset as usize;
self.data.get(start..end)
})
}
}
struct Translations<'a> {
pub header: TranslationsHeader<'a>,
translations: &'a [u8],
translation_offsets: &'a [u16],
fonts: Table<'a>,
}
impl<'a> Translations<'a> {
pub fn new(blob: &'a [u8]) -> Result<Self, Error> {
let header = TranslationsHeader::parse(&blob[..HEADER_LEN])?;
let data_len = header.data_length as usize;
if blob.len() < HEADER_LEN + data_len {
return Err(Error::EOFError);
}
let data = &blob[HEADER_LEN..HEADER_LEN + data_len];
if header.translations_length > data.len() as u16 {
return Err(value_error!("Invalid translations length field"));
}
let translations = &data[..header.translations_length as usize];
// length of offsets table is (count + 1) entries, the last one is a sentinel
let table_len = (header.translations_count + 1) as usize * core::mem::size_of::<u16>();
if table_len > translations.len() {
return Err(value_error!("Invalid translations table"));
}
// SAFETY: any bytes are valid u16 values, so casting any data to
// a sequence of u16 values is safe.
let (_prefix, translation_offsets, _suffix) =
unsafe { data[..table_len].align_to::<u16>() };
if !_prefix.is_empty() || !_suffix.is_empty() {
return Err(value_error!("Invalid translations table"));
}
let fonts_data = &data[header.translations_length as usize..];
Ok(Self {
header,
translations,
translation_offsets,
fonts: Table::new(fonts_data)?,
})
}
/// Returns the translation at the given index.
pub fn translation(&self, index: usize) -> Option<&str> {
if index >= self.translation_offsets.len() + 1 {
// The index is out of bounds.
// May happen when new firmware is using older translations and the string
// is not defined yet.
// Fallback to english.
return None;
}
let start_offset = self.translation_offsets[index] as usize;
let end_offset = self.translation_offsets[index + 1] as usize;
// Construct the relevant slice
let string = &self.translations[start_offset..end_offset];
if string.is_empty() {
// The string is not defined in the blob.
// May happen when old firmware is using newer translations and the string
// was deleted in the newer version.
// Fallback to english.
return None;
}
str::from_utf8(string).ok()
}
pub fn font(&'a self, index: u16) -> Option<Table<'a>> {
self.fonts.get(index).and_then(|data| Table::new(data).ok())
}
}
struct TranslationsHeader<'a> {
/// Human readable language identifier (e.g. "cs" of "fr")
pub language: &'a str,
/// 4 bytes of version (major, minor, patch, build)
pub version: [u8; 4],
/// Overall length of the data blob (excluding the header)
pub data_length: u16,
/// Length of the header
pub header_length: u16,
/// Length of the translation data
pub translations_length: u16,
/// Number of translation items
pub translations_count: u16,
/// Hash of the data blob (excluding the header)
pub data_hash: [u8; 32],
/// Title to show user before changing the language
pub change_language_title: &'a str,
/// Text to show user before changing the language
pub change_language_prompt: &'a str,
}
impl<'a> TranslationsHeader<'a> {
const MAGIC: &'static [u8] = b"TRTR00";
const VERSION_LEN: usize = 16;
const LANG_LEN: usize = 32;
const DATA_HASH_LEN: usize = 32;
const CHANGE_LANG_TITLE_LEN: usize = 20;
const CHANGE_LANG_PROMPT_LEN: usize = 40;
const SIGNATURE_LEN: usize = 65;
fn read_fixedsize_str<'b, 'c: 'b>(reader: &'b mut InputStream<'c>, len: usize) -> Result<&'c str, Error> {
let bytes = reader.read(len)?;
let string = core::str::from_utf8(bytes)
.map_err(|_| value_error!("Invalid string"))?
.trim_end_matches(0 as char);
Ok(string)
}
pub fn parse(data: &'a [u8]) -> Result<Self, Error> {
if data.len() != HEADER_LEN {
return Err(value_error!("Invalid header length"));
}
let mut reader = crate::io::InputStream::new(data);
let magic = reader.read(Self::MAGIC.len())?;
if magic != Self::MAGIC {
return Err(value_error!("Invalid header magic"));
}
let version_bytes = reader.read(4)?;
let version = unwrap!(version_bytes.try_into());
let language = Self::read_fixedsize_str(&mut reader, Self::LANG_LEN)?;
let data_length = reader.read_u16_le()?;
let translations_length = reader.read_u16_le()?;
let translations_count = reader.read_u16_le()?;
let data_hash = unwrap!(reader.read(32)?.try_into());
let change_language_title =
Self::read_fixedsize_str(&mut reader, Self::CHANGE_LANG_TITLE_LEN)?;
let change_language_prompt =
Self::read_fixedsize_str(&mut reader, Self::CHANGE_LANG_PROMPT_LEN)?;
if translations_length > data_length {
return Err(value_error!("Invalid translations length field"));
}
let trailing_data = reader.rest();
if trailing_data.len() < Self::SIGNATURE_LEN {
return Err(value_error!("Invalid header signature"));
}
// TODO check signature
Ok(Self {
language,
version,
data_length,
header_length: 0,
translations_length,
translations_count,
data_hash,
change_language_title,
change_language_prompt,
})
}
}
// SAFETY: Returned pointer will only point to valid font data for as long as the flash
// content is not invalidated by `erase()` or `write()`.
#[no_mangle]
pub unsafe extern "C" fn get_utf8_glyph(codepoint: cty::uint16_t, font: cty::c_int) -> *const u8 {
// C will send a negative number
let font_abs = font.unsigned_abs() as u16;
// SAFETY:
// We rely on the fact that the caller (`display_text_render` and friends) uses
// and discards this data immediately.
// SAFETY: Reference is discarded at the end of the function.
// We do return a _pointer_ to the same memory location, but the pointer is always valid.
let Some(tr) = (unsafe { flash::get() }) else {
return null();
return core::ptr::null();
};
if let Some(glyph) = tr.font(font_abs).and_then(|t| t.get(codepoint)) {
glyph.as_ptr()
} else {
null()
core::ptr::null()
}
}

@ -1,5 +1,6 @@
use crate::{
error::Error,
io::InputStream,
micropython::{
buffer::{get_buffer, StrBuffer},
ffi,
@ -11,6 +12,7 @@ use crate::{
typ::Type,
util,
},
trezorhal::translations,
};
use super::translated_string::TranslatedString;
@ -23,8 +25,8 @@ impl TryFrom<TranslatedString> for StrBuffer {
// data is discarded at the end of this function.
let translated = value.translate(unsafe { super::flash::get() });
StrBuffer::alloc(translated)
// TODO fall back to English (which is static and can be converted infallibly)
// if the allocation fails?
// TODO fall back to English (which is static and can be converted
// infallibly) if the allocation fails?
}
}
@ -98,15 +100,16 @@ pub struct TranslationsHeader {
version: Obj,
change_language_title: Obj,
change_language_prompt: Obj,
header_length: Obj,
data_length: Obj,
data_len: Obj,
data_hash: Obj,
total_len: Obj,
}
// SAFETY: We are in a single-threaded environment.
unsafe impl Sync for TranslationsHeader {}
impl TranslationsHeader {
pub(super) fn new(header: &super::TranslationsHeader<'_>) -> Result<Self, Error> {
pub(super) fn new(header: &super::blob::TranslationsHeader<'_>) -> Result<Self, Error> {
let version_objs: [Obj; 4] = {
let v = header.version;
[v[0].into(), v[1].into(), v[2].into(), v[3].into()]
@ -117,19 +120,21 @@ impl TranslationsHeader {
version: util::new_tuple(&version_objs)?,
change_language_title: header.change_language_title.try_into()?,
change_language_prompt: header.change_language_prompt.try_into()?,
header_length: header.header_length.into(),
data_length: header.data_length.into(),
data_len: header.data_len.try_into()?,
data_hash: header.data_hash.as_ref().try_into()?,
total_len: header.total_len.try_into()?,
})
}
pub fn getattr(&self, attr: Qstr) -> Result<Obj, Error> {
let obj = match attr {
Qstr::MP_QSTR_language_name => self.language,
Qstr::MP_QSTR_language => self.language,
Qstr::MP_QSTR_version => self.version,
Qstr::MP_QSTR_change_language_title => self.change_language_title,
Qstr::MP_QSTR_change_language_prompt => self.change_language_prompt,
Qstr::MP_QSTR_header_length => self.header_length,
Qstr::MP_QSTR_data_length => self.data_length,
Qstr::MP_QSTR_data_len => self.data_len,
Qstr::MP_QSTR_data_hash => self.data_hash,
Qstr::MP_QSTR_total_len => self.total_len,
Qstr::MP_QSTR_load_from_flash => LOAD_FROM_FLASH_FN.as_obj(),
_ => return Err(Error::AttributeError(attr)),
};
@ -153,7 +158,8 @@ impl TranslationsHeader {
}
// SAFETY: reference is discarded at the end of this function.
let buffer = unsafe { get_buffer(args[0])? };
let header = super::TranslationsHeader::parse(buffer)?;
let (header, _) =
super::blob::TranslationsHeader::parse_from(&mut InputStream::new(buffer))?;
let new = Self::new(&header)?;
Ok(Gc::new(new)?.into())
};
@ -224,21 +230,121 @@ impl TryFrom<Obj> for Gc<TranslationsHeader> {
}
}
extern "C" fn area_bytesize() -> Obj {
let bytesize = translations::area_bytesize();
unsafe { util::try_or_raise(|| bytesize.try_into()) }
}
extern "C" fn get_language() -> Obj {
let block = || {
// SAFETY: reference is discarded at the end of the block
let lang_name = unsafe { super::flash::get() }.map(|t| t.header.language);
lang_name.unwrap_or(super::DEFAULT_LANGUAGE).try_into()
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn init() -> Obj {
let block = || {
super::flash::init();
Ok(Obj::const_none())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn deinit() -> Obj {
// SAFETY: Safe by itself. Any unsafety stems from some other piece of code
// not upholding the safety parameters.
unsafe { super::flash::deinit() };
Obj::const_none()
}
extern "C" fn erase() -> Obj {
let block = || {
super::flash::erase()?;
Ok(Obj::const_none())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn write(data: Obj, offset: Obj) -> Obj {
let block = || {
// SAFETY: reference is discarded at the end of the block
let data = unsafe { get_buffer(data)? };
let offset: usize = offset.try_into()?;
super::flash::write(data, offset)?;
Ok(Obj::const_none())
};
unsafe { util::try_or_raise(block) }
}
extern "C" fn verify(data: Obj) -> Obj {
let block = || {
// SAFETY: reference is discarded at the end of the block
let data = unsafe { get_buffer(data)? };
super::blob::Translations::new(data)?;
Ok(Obj::const_none())
};
unsafe { util::try_or_raise(block) }
}
#[no_mangle]
#[rustfmt::skip]
pub static mp_module_trezortranslate: Module = obj_module! {
// TODO: add function to get all the translations keys in order
// - so that client can validate it is sending correct keys in correct order
/// from trezortranslate_keys import TR as TR # noqa: F401
/// """Translation object with attributes."""
Qstr::MP_QSTR_TR => TR_OBJ.as_obj(),
/// MAX_HEADER_LEN: int
/// """Maximum length of the translations header."""
Qstr::MP_QSTR_MAX_HEADER_LEN => Obj::small_int(super::MAX_HEADER_LEN),
/// def area_bytesize() -> int:
/// """Maximum size of the translation blob that can be stored."""
Qstr::MP_QSTR_area_bytesize => obj_fn_0!(area_bytesize).as_obj(),
/// def get_language() -> str:
/// """Get the current language."""
Qstr::MP_QSTR_get_language => obj_fn_0!(get_language).as_obj(),
/// def init() -> None:
/// """Initialize the translations system.
///
/// Loads and verifies translation data from flash. If the verification passes,
/// Trezor UI is translated from that point forward.
/// """
Qstr::MP_QSTR_init => obj_fn_0!(init).as_obj(),
/// def deinit() -> None:
/// """Deinitialize the translations system.
///
/// Translations must be deinitialized before erasing or writing to flash.
/// """
Qstr::MP_QSTR_deinit => obj_fn_0!(deinit).as_obj(),
/// def erase() -> None:
/// """Erase the translations blob from flash."""
Qstr::MP_QSTR_erase => obj_fn_0!(erase).as_obj(),
/// def write(data: bytes, offset: int) -> None:
/// """Write data to the translations blob in flash."""
Qstr::MP_QSTR_write => obj_fn_2!(write).as_obj(),
/// def verify(data: bytes) -> None:
/// """Verify the translations blob."""
Qstr::MP_QSTR_verify => obj_fn_1!(verify).as_obj(),
/// class TranslationsHeader:
/// """Metadata about the translations blob."""
///
/// language_name: str
/// language: str
/// version: tuple[int, int, int, int]
/// change_language_title: str
/// change_language_prompt: str
/// header_length: int
/// data_length: int
/// data_len: int
/// data_hash: bytes
/// total_len: int
///
/// def __init__(self, header_bytes: bytes) -> None:
/// """Parse header from bytes.
@ -249,8 +355,4 @@ pub static mp_module_trezortranslate: Module = obj_module! {
/// def load_from_flash() -> TranslationsHeader | None:
/// """Load translations from flash."""
Qstr::MP_QSTR_TranslationsHeader => TRANSLATIONS_HEADER_TYPE.as_obj(),
/// from trezortranslate_keys import TR # noqa: F401
/// """Translation object with attributes."""
Qstr::MP_QSTR_TR => TR_OBJ.as_obj(),
};

@ -0,0 +1,14 @@
use crate::crypto::ed25519;
#[cfg(feature = "debug")]
pub const PUBLIC_KEYS_DEVEL: [ed25519::PublicKey; 3] = [
*b"\x68\x46\x0e\xbe\xf3\xb1\x38\x16\x4e\xc7\xfd\x86\x10\xe9\x58\x00\xdf\x75\x98\xf7\x0f\x2f\x2e\xa7\xdb\x51\x72\xac\x74\xeb\xc1\x44",
*b"\x8d\x4a\xbe\x07\x4f\xef\x92\x29\xd3\xb4\x41\xdf\xea\x4f\x98\xf8\x05\xb1\xa2\xb3\xa0\x6a\xe6\x45\x81\x0e\xfe\xce\x77\xfd\x50\x44",
*b"\x97\xf7\x13\x5a\x9a\x26\x90\xe7\x3b\xeb\x26\x55\x6f\x1c\xb1\x63\xbe\xa2\x53\x2a\xff\xa1\xe7\x78\x24\x30\xbe\x98\xc0\xe5\x68\x12",
];
pub const PUBLIC_KEYS: [ed25519::PublicKey; 3] = [
*b"\x43\x34\x99\x63\x43\x62\x3e\x46\x2f\x0f\xc9\x33\x11\xfe\xf1\x48\x4c\xa2\x3d\x2f\xf1\xee\xc6\xdf\x1f\xa8\xeb\x7e\x35\x73\xb3\xdb",
*b"\xa9\xa2\x2c\xc2\x65\xa0\xcb\x1d\x6c\xb3\x29\xbc\x0e\x60\xbc\x45\xdf\x76\xb9\xab\x28\xfb\x87\xb6\x11\x36\xfe\xaf\x8d\x8f\xdc\x96",
*b"\xb8\xd2\xb2\x1d\xe2\x71\x24\xf0\x51\x1f\x90\x3a\xe7\xe6\x0e\x07\x96\x18\x10\xa0\xb8\xf2\x8e\xa7\x55\xfa\x50\x36\x7a\x8a\x2b\x8b",
];

@ -1,7 +1,7 @@
use crate::strutil::TString;
use super::blob::Translations;
pub use super::generated::translated_string::TranslatedString;
use super::Translations;
impl TranslatedString {
pub(super) fn translate<'a>(self, source: Option<&'a Translations>) -> &'a str {

@ -26,6 +26,6 @@ pub fn area_bytesize() -> usize {
// SAFETY: This call may invalidate the reference to the blob returned by
// `get_blob()`.
pub unsafe fn write(data: &[u8], offset: usize) {
unsafe { ffi::translations_write(data.as_ptr(), offset as u32, data.len() as u32) };
pub unsafe fn write(data: &[u8], offset: usize) -> bool {
unsafe { ffi::translations_write(data.as_ptr(), offset as u32, data.len() as u32) }
}

@ -146,20 +146,12 @@ impl Font {
/// Supports UTF8 characters
fn get_first_glyph_from_text(self, text: &str) -> Option<Glyph> {
if let Some(c) = text.chars().next() {
Some(self.get_glyph(c))
} else {
None
}
text.chars().next().map(|c| self.get_glyph(c))
}
/// Supports UTF8 characters
fn get_last_glyph_from_text(self, text: &str) -> Option<Glyph> {
if let Some(c) = text.chars().next_back() {
Some(self.get_glyph(c))
} else {
None
}
text.chars().next_back().map(|c| self.get_glyph(c))
}
/// Width of the text that is visible.

@ -47,7 +47,7 @@ impl AddressDetails {
&theme::TEXT_BOLD,
TR::words__account_colon.try_into()?,
));
para.add(Paragraph::new(&theme::TEXT_MONO, account.into()));
para.add(Paragraph::new(&theme::TEXT_MONO, account));
}
if let Some(path) = path {
para.add(Paragraph::new(
@ -152,9 +152,9 @@ impl AddressDetails {
fn fill_xpub_page(&mut self, ctx: &mut EventCtx) {
let i = self.current_page - 2;
self.xpub_view.update_title(ctx, self.xpubs[i].0.clone());
self.xpub_view.update_title(ctx, self.xpubs[i].0);
self.xpub_view.update_content(ctx, |p| {
p.inner_mut().update(self.xpubs[i].1.clone());
p.inner_mut().update(self.xpubs[i].1);
p.change_page(0)
});
}

@ -1,5 +1,4 @@
use crate::{
micropython::buffer::StrBuffer,
strutil::TString,
ui::{
component::{Child, Component, ComponentExt, Event, EventCtx, Label, Pad},
@ -50,7 +49,7 @@ where
) -> Self {
let button_text = button_text.into();
let btn_layout =
Self::get_button_layout_general(false, button_text.clone(), false, two_btn_confirm);
Self::get_button_layout_general(false, button_text, false, two_btn_confirm);
Self {
bg: Pad::with_background(bg_color).with_clear(),
bg_color,
@ -85,7 +84,7 @@ where
fn get_button_layout(&self) -> ButtonLayout {
Self::get_button_layout_general(
self.showing_info_screen,
self.button_text.clone(),
self.button_text,
self.has_info_screen(),
self.two_btn_confirm,
)
@ -208,7 +207,7 @@ where
fn paint(&mut self) {
self.bg.paint();
let display_top_left = |text: &TString<'static>| {
let display_top_left = |text: TString<'static>| {
text.map(|t| {
display::text_top_left(Point::zero(), t, Font::BOLD, WHITE, self.bg_color)
});
@ -216,10 +215,12 @@ where
// We are either on the info screen or on the "main" screen
if self.showing_info_screen {
self.info_title.map(|title| display_top_left(&title));
if let Some(title) = self.info_title {
display_top_left(title);
}
self.info_text.paint();
} else {
display_top_left(&self.title);
display_top_left(self.title);
self.message.paint();
self.alert.paint();
}

@ -1,5 +1,5 @@
use crate::{
strutil::{StringType, TString},
strutil::TString,
time::Duration,
ui::{
component::{Component, Event, EventCtx, Never},

@ -2,7 +2,6 @@ use super::{
theme, Button, ButtonDetails, ButtonLayout, ButtonPos, HoldToConfirm, HoldToConfirmMsg,
};
use crate::{
strutil::StringType,
time::{Duration, Instant},
ui::{
component::{base::Event, Component, EventCtx, Pad, TimerToken},

@ -1,5 +1,5 @@
use crate::{
strutil::{StringType, TString},
strutil::TString,
time::{Duration, Instant},
ui::{
component::{Component, Event, EventCtx},

@ -93,7 +93,7 @@ where
.map_translated(|t| display_center(baseline, t, NOTIFICATION_FONT));
} else if let Some((notification, _level)) = &self.notification {
self.fill_notification_background();
display_center(baseline, &notification.as_ref(), NOTIFICATION_FONT);
display_center(baseline, notification.as_ref(), NOTIFICATION_FONT);
// Painting warning icons in top corners when the text is short enough not to
// collide with them
let icon_width = NOTIFICATION_ICON.toif.width();

@ -1,10 +1,7 @@
use crate::{
strutil::StringType,
ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::{Insets, Offset, Rect},
util::animation_disabled,
},
use crate::ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::{Insets, Offset, Rect},
util::animation_disabled,
};
use super::super::{

@ -1,5 +1,5 @@
use crate::{
strutil::{ShortString, StringType},
strutil::ShortString,
ui::{
display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
geometry::{Alignment2D, Offset, Rect},

@ -1,5 +1,4 @@
use crate::{
micropython::buffer::StrBuffer,
strutil::TString,
translations::TR,
trezorhal::random,

@ -1,5 +1,5 @@
use crate::{
strutil::{StringType, TString},
strutil::TString,
time::{Duration, Instant},
ui::{
animation::Animation,

@ -109,7 +109,7 @@ where
let baseline = self.area.top_left() + Offset::y(y_offset);
let ordinal = build_string!(5, inttostr!(index as u8 + 1), ".");
display_left(baseline + Offset::x(NUMBER_X_OFFSET), &ordinal, NUMBER_FONT);
display_left(baseline + Offset::x(WORD_X_OFFSET), &word, WORD_FONT);
display_left(baseline + Offset::x(WORD_X_OFFSET), word, WORD_FONT);
}
}
}

@ -1,5 +1,5 @@
use crate::{
strutil::{StringType, TString},
strutil::TString,
ui::{
component::{Child, Component, Event, EventCtx},
geometry::{Insets, Rect},

@ -625,15 +625,15 @@ extern "C" fn new_confirm_output_address(n_args: usize, args: *const Obj, kwargs
if chunkify {
ops = ops.chunkify_text(None);
}
ops = ops.text_normal(address_label.clone()).newline();
ops = ops.text_normal(address_label).newline();
}
if chunkify {
// Chunkifying the address into smaller pieces when requested
ops = ops.chunkify_text(Some((theme::MONO_CHUNKS, 2)));
}
ops = ops.text_mono(address.clone());
ops = ops.text_mono(address);
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(address_title.clone())
Page::new(btn_layout, btn_actions, formatted).with_title(address_title)
};
let pages = FlowPages::new(get_page, 1);
@ -653,9 +653,9 @@ extern "C" fn new_confirm_output_amount(n_args: usize, args: *const Obj, kwargs:
// AMOUNT + amount
let btn_layout = ButtonLayout::up_arrow_none_text(TR::buttons__confirm.into());
let btn_actions = ButtonActions::cancel_none_confirm();
let ops = OpTextLayout::new(theme::TEXT_MONO).text_mono(amount.clone());
let ops = OpTextLayout::new(theme::TEXT_MONO).text_mono(amount);
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(amount_title.clone())
Page::new(btn_layout, btn_actions, formatted).with_title(amount_title)
};
let pages = FlowPages::new(get_page, 1);
@ -691,14 +691,14 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
let btn_actions = ButtonActions::cancel_confirm_next();
let ops = OpTextLayout::new(theme::TEXT_MONO)
.text_bold(total_label.clone())
.text_bold(total_label)
.newline()
.text_mono(total_amount.clone())
.text_mono(total_amount)
.newline()
.newline()
.text_bold(fee_label.clone())
.text_bold(fee_label)
.newline()
.text_mono(fee_amount.clone());
.text_mono(fee_amount);
let formatted = FormattedText::new(ops);
Page::new(btn_layout, btn_actions, formatted)
@ -708,7 +708,7 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
let btn_layout = ButtonLayout::arrow_none_arrow();
let btn_actions = ButtonActions::prev_none_next();
let fee_rate_amount = fee_rate_amount.clone().unwrap_or_default();
let fee_rate_amount = fee_rate_amount.unwrap_or_default();
let ops = OpTextLayout::new(theme::TEXT_MONO)
.text_bold(tr_title_fee)
@ -727,7 +727,7 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
let btn_layout = ButtonLayout::arrow_none_none();
let btn_actions = ButtonActions::prev_none_none();
let account_label = account_label.clone().unwrap_or_default();
let account_label = account_label.unwrap_or_default();
// TODO: include wallet info when available
@ -778,15 +778,15 @@ extern "C" fn new_altcoin_tx_summary(n_args: usize, args: *const Obj, kwargs: *m
let btn_actions = ButtonActions::cancel_confirm_next();
let ops = OpTextLayout::new(theme::TEXT_MONO)
.text_mono(amount_value.clone())
.text_mono(amount_value)
.newline()
.newline_half()
.text_bold(fee_title.clone())
.text_bold(fee_title)
.newline()
.text_mono(fee_value.clone());
.text_mono(fee_value);
let formatted = FormattedText::new(ops);
Page::new(btn_layout, btn_actions, formatted).with_title(amount_title.clone())
Page::new(btn_layout, btn_actions, formatted).with_title(amount_title)
}
1 => {
// Other information
@ -834,7 +834,7 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
let get_page = move |page_index| {
assert!(page_index == 0);
let btn_layout = ButtonLayout::cancel_armed_info(verb.clone());
let btn_layout = ButtonLayout::cancel_armed_info(verb);
let btn_actions = ButtonActions::cancel_confirm_info();
let style = if chunkify {
// Chunkifying the address into smaller pieces when requested
@ -842,9 +842,9 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
} else {
theme::TEXT_MONO_DATA
};
let ops = OpTextLayout::new(style).text_mono(address.clone());
let ops = OpTextLayout::new(style).text_mono(address);
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(title.clone())
Page::new(btn_layout, btn_actions, formatted).with_title(title)
};
let pages = FlowPages::new(get_page, 1);
@ -862,9 +862,9 @@ fn tutorial_screen(
btn_layout: ButtonLayout,
btn_actions: ButtonActions,
) -> Page<StrBuffer> {
let ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL).text_normal(text.into());
let ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL).text_normal(text);
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(title.into())
Page::new(btn_layout, btn_actions, formatted).with_title(title)
}
extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
@ -1026,7 +1026,7 @@ extern "C" fn new_multiple_pages_texts(n_args: usize, args: *const Obj, kwargs:
let (btn_layout, btn_actions) = if page_count == 1 {
// There is only one page
(
ButtonLayout::cancel_none_text(verb.clone()),
ButtonLayout::cancel_none_text(verb),
ButtonActions::cancel_none_confirm(),
)
} else if page_index == 0 {
@ -1038,7 +1038,7 @@ extern "C" fn new_multiple_pages_texts(n_args: usize, args: *const Obj, kwargs:
} else if page_index == page_count - 1 {
// Last page
(
ButtonLayout::up_arrow_none_text(verb.clone()),
ButtonLayout::up_arrow_none_text(verb),
ButtonActions::prev_none_confirm(),
)
} else {
@ -1106,7 +1106,7 @@ extern "C" fn new_confirm_fido(n_args: usize, args: *const Obj, kwargs: *mut Map
let ops = OpTextLayout::new(theme::TEXT_NORMAL)
.newline()
.text_normal(app_name.clone())
.text_normal(app_name)
.newline()
.text_bold(account);
let formatted = FormattedText::new(ops);
@ -1135,15 +1135,16 @@ extern "C" fn new_show_warning(n_args: usize, args: *const Obj, kwargs: *mut Map
let get_page = move |page_index| {
assert!(page_index == 0);
let btn_layout = ButtonLayout::none_armed_none(button.clone());
let btn_layout = ButtonLayout::none_armed_none(button);
let btn_actions = ButtonActions::none_confirm_none();
let mut ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL);
ops = ops.alignment(geometry::Alignment::Center);
if !warning.is_empty() {
ops = ops.text_bold(warning.clone()).newline();
// TODO: add the second newline conditionally, when all fits
ops = ops.text_bold(warning).newline();
}
if !description.is_empty() {
ops = ops.text_normal(description.clone());
ops = ops.text_normal(description);
}
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted)
@ -1206,7 +1207,7 @@ extern "C" fn new_show_mismatch(n_args: usize, args: *const Obj, kwargs: *mut Ma
let btn_layout = ButtonLayout::arrow_none_text(TR::buttons__quit.into());
let btn_actions = ButtonActions::cancel_none_confirm();
let ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL)
.text_bold(title.clone())
.text_bold(title)
.newline()
.newline_half()
.text_normal(tr_contact_support_at)
@ -1631,7 +1632,7 @@ extern "C" fn new_confirm_firmware_update(
let description: StrBuffer = kwargs.get(Qstr::MP_QSTR_description)?.try_into()?;
let fingerprint: StrBuffer = kwargs.get(Qstr::MP_QSTR_fingerprint)?.try_into()?;
let title = TR::firmware_update__title.try_into()?;
let title = TR::firmware_update__title;
let message = Label::left_aligned(description, theme::TEXT_NORMAL).vertically_centered();
let fingerprint = Label::left_aligned(
fingerprint,

@ -1,11 +1,15 @@
use crate::{
error::Error, micropython::buffer::StrBuffer, time::Instant, translations::TR, ui::{
error::Error,
micropython::buffer::StrBuffer,
time::Instant,
translations::TR,
ui::{
component::{paginated::PageMsg, Component, ComponentExt, Event, EventCtx, Pad, Paginate},
constant,
display::{self, Color},
geometry::{Insets, Rect},
util::animation_disabled,
}
},
};
use super::{

@ -1,12 +1,17 @@
#include "translations.h"
#include <assert.h>
#include <stdbool.h>
#include <string.h>
#include "common.h"
#include "flash.h"
#include "model.h"
void translations_write(const uint8_t* data, uint32_t offset, uint32_t len) {
// TODO maybe return errors from here?
bool translations_write(const uint8_t* data, uint32_t offset, uint32_t len) {
uint32_t size = translations_area_bytesize();
if (offset > size || size - offset < len) {
return false;
}
ensure(flash_unlock_write(), "translations_write unlock");
for (int i = 0; i < len; i++) {
// TODO optimize by writing by (quad)words
@ -14,6 +19,7 @@ void translations_write(const uint8_t* data, uint32_t offset, uint32_t len) {
"translations_write write");
}
ensure(flash_lock_write(), "translations_write lock");
return true;
}
const uint8_t* translations_read(uint32_t* len, uint32_t offset) {

@ -1,6 +1,7 @@
#include <stdbool.h>
#include <stdint.h>
void translations_write(const uint8_t* data, uint32_t offset, uint32_t len);
bool translations_write(const uint8_t* data, uint32_t offset, uint32_t len);
const uint8_t* translations_read(uint32_t* len, uint32_t offset);

@ -7,3 +7,66 @@ def language_name() -> str:
from trezortranslate_keys import TR # noqa: F401
"""Translation object with attributes."""
MAX_HEADER_LEN: int
"""Maximum length of the translations header."""
# rust/src/translations/obj.rs
def area_bytesize() -> int:
"""Maximum size of the translation blob that can be stored."""
# rust/src/translations/obj.rs
def get_language() -> str:
"""Get the current language."""
# rust/src/translations/obj.rs
def init() -> None:
"""Initialize the translations system.
Loads and verifies translation data from flash. If the verification passes,
Trezor UI is translated from that point forward.
"""
# rust/src/translations/obj.rs
def deinit() -> None:
"""Deinitialize the translations system.
Translations must be deinitialized before erasing or writing to flash.
"""
# rust/src/translations/obj.rs
def erase() -> None:
"""Erase the translations blob from flash."""
# rust/src/translations/obj.rs
def write(data: bytes, offset: int) -> None:
"""Write data to the translations blob in flash."""
# rust/src/translations/obj.rs
def verify(data: bytes) -> None:
"""Verify the translations blob."""
# rust/src/translations/obj.rs
class TranslationsHeader:
"""Metadata about the translations blob."""
language: str
version: tuple[int, int, int, int]
change_language_title: str
change_language_prompt: str
data_len: int
data_hash: bytes
total_len: int
def __init__(self, header_bytes: bytes) -> None:
"""Parse header from bytes.
The header has variable length.
"""
@staticmethod
def load_from_flash() -> TranslationsHeader | None:
"""Load translations from flash."""

@ -6,9 +6,7 @@ class TR:
addr_mismatch__key_mismatch: str = "Key mismatch?"
addr_mismatch__mismatch: str = "Address mismatch?"
addr_mismatch__support_url: str = "trezor.io/support"
addr_mismatch__title: str = "ADDRESS MISMATCH?"
addr_mismatch__title_key_mismatch: str = "KEY MISMATCH?"
addr_mismatch__wrong_derication_path: str = "Wrong derivation path for selected account."
addr_mismatch__wrong_derivation_path: str = "Wrong derivation path for selected account."
addr_mismatch__xpub_mismatch: str = "XPUB mismatch?"
address__public_key: str = "Public key"
address__title_cosigner: str = "COSIGNER"
@ -122,10 +120,7 @@ class TR:
cardano__block: str = "Block"
cardano__catalyst: str = "Catalyst"
cardano__certificate: str = "Certificate"
cardano__certificate_path: str = "Certificate path"
cardano__change_output: str = "Change output"
cardano__change_output_path: str = "Change output path"
cardano__change_output_staking_path: str = "Change output staking path"
cardano__check_all_items: str = "Check all items carefully."
cardano__choose_level_of_details: str = "Choose level of details:"
cardano__collateral_input_id: str = "Collateral input ID:"
@ -136,6 +131,7 @@ class TR:
cardano__confirm_signing_stake_pool: str = "Confirm signing the stake pool registration as an owner."
cardano__confirm_transaction: str = "Confirm transaction"
cardano__confirming_a_multisig_transaction: str = "Confirming a multisig transaction."
cardano__confirming_a_plutus_transaction: str = "Confirming a Plutus transaction."
cardano__confirming_pool_registration: str = "Confirming pool registration as owner."
cardano__confirming_transction: str = "Confirming a transaction."
cardano__cost: str = "Cost"
@ -167,7 +163,6 @@ class TR:
cardano__pool_metadata_hash: str = "Pool metadata hash:"
cardano__pool_metadata_url: str = "Pool metadata url:"
cardano__pool_owner: str = "Pool owner:"
cardano__pool_owner_path: str = "Pool owner staking path"
cardano__pool_reward_account: str = "Pool reward account:"
cardano__reference_input_id: str = "Reference input ID:"
cardano__reference_input_index: str = "Reference input index:"
@ -218,7 +213,6 @@ class TR:
cardano__warning: str = "Warning"
cardano__weight: str = "Weight:"
cardano__withdrawal_for_address_template: str = "Confirm withdrawal for {} address:"
cardano__witness_path: str = "Witness path"
cardano__x_of_y_signatures_template: str = "Requires {} out of {} signatures."
coinjoin__access_account: str = "Access your coinjoin account?"
coinjoin__do_not_disconnect: str = "Do not disconnect your Trezor!"
@ -339,7 +333,7 @@ class TR:
homescreen__title_no_usb_connection: str = "NO USB CONNECTION"
homescreen__title_pin_not_set: str = "PIN NOT SET"
homescreen__title_seedless: str = "SEEDLESS"
homescreen__title_set: str = "CHANGE HOMESCREEN?"
homescreen__title_set: str = "CHANGE HOMESCREEN"
inputs__back: str = "BACK"
inputs__cancel: str = "CANCEL"
inputs__delete: str = "DELETE"
@ -365,7 +359,7 @@ class TR:
modify_fee__fee_rate: str = "Fee rate:"
modify_fee__increase_fee: str = "Increase fee by:"
modify_fee__new_transaction_fee: str = "New transaction fee:"
modify_fee__no_change: str = "Your fee did not change."
modify_fee__no_change: str = "Fee did not change."
modify_fee__title: str = "MODIFY FEE"
modify_fee__transaction_fee: str = "Transaction fee:"
monero__confirm_export: str = "Confirm export"
@ -464,7 +458,7 @@ class TR:
pin__info: str = "PIN will be required to access this device."
pin__invalid_pin: str = "Invalid PIN"
pin__last_attempt: str = "Last attempt"
pin__mismatch: str = "Entered PINs do not match!"
pin__mismatch: str = "The PINs you entered do not match."
pin__pin_mismatch: str = "PIN mismatch"
pin__please_check_again: str = "Please check again."
pin__reenter_new: str = "Re-enter new PIN"
@ -786,7 +780,7 @@ class TR:
wipe_code__enter_new: str = "Enter new wipe code"
wipe_code__info: str = "Wipe code can be used to erase all data from this device."
wipe_code__invalid: str = "Invalid wipe code"
wipe_code__mismatch: str = "Entered wipe codes do not match!"
wipe_code__mismatch: str = "The wipe codes you entered do not match."
wipe_code__reenter: str = "Re-enter wipe code"
wipe_code__reenter_to_confirm: str = "Please re-enter wipe code to confirm."
wipe_code__title_check: str = "CHECK WIPE CODE"
@ -797,6 +791,7 @@ class TR:
wipe_code__wipe_code_mismatch: str = "Wipe code mismatch"
word_count__title: str = "NUMBER OF WORDS"
words__account: str = "Account"
words__account_colon: str = "Account:"
words__address: str = "Address"
words__amount: str = "Amount"
words__are_you_sure: str = "Are you sure?"

@ -7,19 +7,9 @@ import json
en_file = ROOT / "core" / "translations" / "en.json"
en_data = json.loads(en_file.read_text())["translations"]
def get_en_strings(data: dict) -> dict[str, str]:
res = {}
for section_name, section in data.items():
for k, v in section.items():
key = f"{section_name}__{k}"
res[key] = json.dumps(v)
return res
en_strings = get_en_strings(en_data)
%>\
class TR:
% for name, text in sorted(en_strings.items()):
${name}: str = ${text}
% for name, text in sorted(en_data.items()):
${name}: str = ${json.dumps(text)}
% endfor

@ -147,8 +147,6 @@ trezor.sdcard
import trezor.sdcard
trezor.strings
import trezor.strings
trezor.translations
import trezor.translations
trezor.ui
import trezor.ui
trezor.ui.layouts

@ -2,11 +2,10 @@ from typing import TYPE_CHECKING
import storage.cache as storage_cache
import storage.device as storage_device
from trezor import config, utils, wire, workflow
from trezor import TR, config, utils, wire, workflow
from trezor.enums import HomescreenFormat, MessageType
from trezor.messages import Success, UnlockPath
from trezor.ui.layouts import confirm_action
from trezortranslate import TR
from . import workflow_handlers
@ -46,9 +45,9 @@ def busy_expiry_ms() -> int:
def get_features() -> Features:
import storage.recovery as storage_recovery
from trezor import translations
from trezor.enums import Capability
from trezor.messages import Features
from trezor.translations import get_language
from trezor.ui import HEIGHT, WIDTH
from apps.common import mnemonic, safety_checks
@ -56,7 +55,7 @@ def get_features() -> Features:
f = Features(
vendor="trezor.io",
fw_vendor=utils.firmware_vendor(),
language=get_language(),
language=translations.get_language(),
major_version=utils.VERSION_MAJOR,
minor_version=utils.VERSION_MINOR,
patch_version=utils.VERSION_PATCH,

@ -1,9 +1,9 @@
from typing import TYPE_CHECKING, Sequence
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.strings import format_amount
from trezor.ui.layouts import confirm_properties
from trezortranslate import TR
from .helpers import DECIMALS

@ -19,11 +19,11 @@ _MAX_COORDINATOR_FEE_RATE = 5 * pow(10, FEE_RATE_DECIMALS) # 5 %
async def authorize_coinjoin(
msg: AuthorizeCoinJoin, keychain: Keychain, coin: CoinInfo
) -> Success:
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.messages import Success
from trezor.ui.layouts import confirm_coinjoin, confirm_metadata
from trezor.wire import DataError
from trezortranslate import TR
from apps.common import authorization, safety_checks
from apps.common.keychain import FORBIDDEN_KEY_PATH

@ -31,10 +31,10 @@ def _get_xpubs(
@with_keychain
async def get_address(msg: GetAddress, keychain: Keychain, coin: CoinInfo) -> Address:
from trezor import TR
from trezor.enums import InputScriptType
from trezor.messages import Address
from trezor.ui.layouts import show_address, show_warning
from trezortranslate import TR
from apps.common.address_mac import get_address_mac
from apps.common.paths import address_n_to_str, validate_path

@ -18,11 +18,11 @@ async def get_ownership_proof(
coin: CoinInfo,
authorization: CoinJoinAuthorization | None = None,
) -> OwnershipProof:
from trezor import TR
from trezor.enums import InputScriptType
from trezor.messages import OwnershipProof
from trezor.ui.layouts import confirm_action, confirm_blob
from trezor.wire import DataError, ProcessError
from trezortranslate import TR
from apps.common.paths import validate_path

@ -8,10 +8,9 @@ if TYPE_CHECKING:
async def get_public_key(
msg: GetPublicKey, auth_msg: MessageType | None = None
) -> PublicKey:
from trezor import wire
from trezor import TR, wire
from trezor.enums import InputScriptType
from trezor.messages import HDNodeType, PublicKey, UnlockPath
from trezortranslate import TR
from apps.common import coininfo, paths
from apps.common.keychain import FORBIDDEN_KEY_PATH, get_keychain

@ -464,7 +464,7 @@ def address_n_to_name_or_unknown(
account_level: bool = False,
show_account_str: bool = False,
) -> str:
from trezortranslate import TR
from trezor import TR
account_name = address_n_to_name(coin, address_n, script_type)
if account_name is None:

@ -257,7 +257,7 @@ class BasicApprover(Approver):
def _replacement_title(
self, tx_info: TxInfo, orig_txs: list[OriginalTxInfo]
) -> str:
from trezortranslate import TR
from trezor import TR
if self.is_payjoin():
return TR.bitcoin__title_payjoin

@ -1,11 +1,11 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.strings import format_amount
from trezor.ui import layouts
from trezor.ui.layouts import confirm_metadata
from trezortranslate import TR
from apps.common.paths import address_n_to_str

@ -17,8 +17,8 @@ def is_valid(data: bytes) -> bool:
def parse(data: bytes) -> str:
from ustruct import unpack
from trezor import TR
from trezor.strings import format_amount
from trezortranslate import TR
if not is_valid(data):
raise ValueError # tried to parse data that fails validation

@ -112,9 +112,8 @@ class Progress:
self.report()
def report_init(self) -> None:
from trezor import workflow
from trezor import TR, workflow
from trezor.ui.layouts.progress import bitcoin_progress, coinjoin_progress
from trezortranslate import TR
progress_layout = coinjoin_progress if self.is_coinjoin else bitcoin_progress
workflow.close_others()

@ -51,13 +51,12 @@ def _address_to_script_type(address: str, coin: CoinInfo) -> InputScriptType:
async def verify_message(msg: VerifyMessage) -> Success:
from trezor import utils
from trezor import TR, utils
from trezor.crypto.curve import secp256k1
from trezor.enums import InputScriptType
from trezor.messages import Success
from trezor.ui.layouts import confirm_signverify, show_success
from trezor.wire import ProcessError
from trezortranslate import TR
from apps.common import coins
from apps.common.signverify import decode_message, message_digest

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from ubinascii import hexlify
from trezortranslate import TR
from trezor import TR
from . import seed

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.enums import CardanoAddressType
from trezortranslate import TR
from .paths import SCHEMA_PAYMENT

@ -1,6 +1,6 @@
from micropython import const
from trezortranslate import TR
from trezor import TR
# https://book.world.dev.cardano.org/environments.html
MAINNET = const(764824073)

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from trezor import ui
from trezor import TR, ui
from trezor.enums import (
ButtonRequestType,
CardanoAddressType,
@ -10,7 +10,6 @@ from trezor.enums import (
from trezor.strings import format_amount
from trezor.ui import layouts
from trezor.ui.layouts import confirm_metadata, confirm_properties
from trezortranslate import TR
from apps.common.paths import address_n_to_str

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.wire import ProcessError
from trezortranslate import TR
from .signer import Signer

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.wire import ProcessError
from trezortranslate import TR
from .. import layout
from ..helpers.paths import SCHEMA_MINT

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.wire import ProcessError
from trezortranslate import TR
from .signer import Signer

@ -30,10 +30,10 @@ async def get() -> str:
async def _request_on_host() -> str:
from trezor import TR
from trezor.messages import PassphraseAck, PassphraseRequest
from trezor.ui.layouts import request_passphrase_on_host
from trezor.wire.context import call
from trezortranslate import TR
request_passphrase_on_host()

@ -2,9 +2,8 @@ import utime
from typing import Any, NoReturn
import storage.cache as storage_cache
from trezor import config, utils, wire
from trezor import TR, config, utils, wire
from trezor.ui.layouts import show_error_and_raise
from trezortranslate import TR
async def _request_sd_salt(

@ -1,7 +1,6 @@
from storage.sd_salt import SD_CARD_HOT_SWAPPABLE
from trezor import io, wire
from trezor import TR, io, wire
from trezor.ui.layouts import confirm_action, show_error_and_raise
from trezortranslate import TR
class SdCardUnavailable(wire.ProcessError):

@ -6,13 +6,12 @@ if TYPE_CHECKING:
async def load_device(msg: LoadDevice) -> Success:
import storage.device as storage_device
from trezor import config
from trezor import TR, config
from trezor.crypto import bip39, slip39
from trezor.enums import BackupType
from trezor.messages import Success
from trezor.ui.layouts import confirm_action
from trezor.wire import ProcessError, UnexpectedMessage
from trezortranslate import TR
from apps.management import backup_types

@ -1,8 +1,8 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import confirm_properties
from trezortranslate import TR
from ..helpers import eos_asset_to_string, eos_name_to_string

@ -7,10 +7,10 @@ async def require_get_public_key(
async def require_sign_tx(num_actions: int) -> None:
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.strings import format_plural
from trezor.ui.layouts import confirm_action
from trezortranslate import TR
await confirm_action(
"confirm_tx",

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from trezor import ui
from trezor import TR, ui
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import (
confirm_blob,
@ -8,7 +8,6 @@ from trezor.ui.layouts import (
confirm_text,
should_show_more,
)
from trezortranslate import TR
from .helpers import address_from_bytes, decode_typed_data

@ -67,7 +67,7 @@ async def _generate_typed_data_hash(
metamask_v4_compat - a flag that enables compatibility with MetaMask's signTypedData_v4 method
"""
from trezortranslate import TR
from trezor import TR
from .layout import (
confirm_empty_typed_message,

@ -5,12 +5,12 @@ if TYPE_CHECKING:
async def verify_message(msg: EthereumVerifyMessage) -> Success:
from trezor import TR
from trezor.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha3_256
from trezor.messages import Success
from trezor.ui.layouts import confirm_signverify, show_success
from trezor.wire import DataError
from trezortranslate import TR
from apps.common.signverify import decode_message

@ -16,7 +16,7 @@ async def busyscreen() -> None:
async def homescreen() -> None:
from trezortranslate import TR
from trezor import TR
if storage.device.is_initialized():
label = storage.device.get_label()

@ -2,10 +2,10 @@ from typing import TYPE_CHECKING
import storage.device as storage_device
import trezorui2
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import confirm_action
from trezor.wire import DataError
from trezortranslate import TR
if TYPE_CHECKING:
from trezor.enums import SafetyCheckLevel

@ -5,7 +5,7 @@ if TYPE_CHECKING:
async def authenticate_device(msg: AuthenticateDevice) -> AuthenticityProof:
from trezor import utils, wire
from trezor import TR, utils, wire
from trezor.crypto import optiga
from trezor.crypto.der import read_length
from trezor.crypto.hashlib import sha256
@ -14,7 +14,6 @@ async def authenticate_device(msg: AuthenticateDevice) -> AuthenticityProof:
from trezor.ui.layouts import confirm_action
from trezor.ui.layouts.progress import progress
from trezor.utils import BufferReader, bootloader_locked
from trezortranslate import TR
from apps.common.writers import write_compact_size

@ -1,157 +1,12 @@
from micropython import const
from typing import TYPE_CHECKING
from trezor.crypto.hashlib import sha256
from trezor.wire import DataError
if TYPE_CHECKING:
from trezor.messages import ChangeLanguage, Success
_CHUNK_SIZE = const(1024)
_HEADER_SIZE = const(256)
_FILL_BYTE = b"\x00"
THRESHOLD = 2
PUBLIC_KEYS = (
b"\x43\x34\x99\x63\x43\x62\x3e\x46\x2f\x0f\xc9\x33\x11\xfe\xf1\x48\x4c\xa2\x3d\x2f\xf1\xee\xc6\xdf\x1f\xa8\xeb\x7e\x35\x73\xb3\xdb",
b"\xa9\xa2\x2c\xc2\x65\xa0\xcb\x1d\x6c\xb3\x29\xbc\x0e\x60\xbc\x45\xdf\x76\xb9\xab\x28\xfb\x87\xb6\x11\x36\xfe\xaf\x8d\x8f\xdc\x96",
b"\xb8\xd2\xb2\x1d\xe2\x71\x24\xf0\x51\x1f\x90\x3a\xe7\xe6\x0e\x07\x96\x18\x10\xa0\xb8\xf2\x8e\xa7\x55\xfa\x50\x36\x7a\x8a\x2b\x8b",
)
if __debug__:
DEV_PUBLIC_KEYS = (
b"\x68\x46\x0e\xbe\xf3\xb1\x38\x16\x4e\xc7\xfd\x86\x10\xe9\x58\x00\xdf\x75\x98\xf7\x0f\x2f\x2e\xa7\xdb\x51\x72\xac\x74\xeb\xc1\x44",
b"\x8d\x4a\xbe\x07\x4f\xef\x92\x29\xd3\xb4\x41\xdf\xea\x4f\x98\xf8\x05\xb1\xa2\xb3\xa0\x6a\xe6\x45\x81\x0e\xfe\xce\x77\xfd\x50\x44",
b"\x97\xf7\x13\x5a\x9a\x26\x90\xe7\x3b\xeb\x26\x55\x6f\x1c\xb1\x63\xbe\xa2\x53\x2a\xff\xa1\xe7\x78\x24\x30\xbe\x98\xc0\xe5\x68\x12",
)
class TranslationsHeader:
MAGIC = b"TRTR"
VERSION_LEN = 16
LANG_LEN = 32
DATA_HASH_LEN = 32
CHANGE_LANGUAGE_TITLE_LEN = 20
CHANGE_LANGUAGE_PROMPT_LEN = 40
SIGNATURE_LEN = 64 + 1
def __init__(
self,
raw_data: bytes,
version: str,
language: str,
data_length: int,
translations_length: int,
translations_num: int,
data_hash: bytes,
change_language_title: str,
change_language_prompt: str,
sigmask: int,
signature: bytes,
):
self.raw_data = raw_data
self.version = version
self.language = language
self.data_length = data_length
self.translations_length = translations_length
self.translations_num = translations_num
self.data_hash = data_hash
self.change_language_title = change_language_title
self.change_language_prompt = change_language_prompt
self.sigmask = sigmask
self.signature = signature
@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)
translations_length = readers.read_uint16_le(r)
translations_num = readers.read_uint16_le(r)
data_hash = r.read(cls.DATA_HASH_LEN)
change_language_title = (
r.read(cls.CHANGE_LANGUAGE_TITLE_LEN).rstrip(_FILL_BYTE).decode()
)
change_language_prompt = (
r.read(cls.CHANGE_LANGUAGE_PROMPT_LEN).rstrip(_FILL_BYTE).decode()
)
# Signature occupies last 65 bytes (sigmask + signature itself)
rest = r.read()
if len(rest) < cls.SIGNATURE_LEN:
raise DataError("Invalid header data")
zeros = rest[: -cls.SIGNATURE_LEN]
signature_part = rest[-cls.SIGNATURE_LEN :]
sigmask = signature_part[0]
signature = signature_part[1:]
# Rest must be empty bytes
for b in zeros:
if b != 0:
raise DataError("Invalid header data")
return cls(
raw_data=data,
language=language,
version=version,
data_length=data_length,
translations_length=translations_length,
translations_num=translations_num,
data_hash=data_hash,
change_language_title=change_language_title,
change_language_prompt=change_language_prompt,
sigmask=sigmask,
signature=signature,
)
except EOFError:
raise DataError("Invalid header data")
def version_tuple(self) -> tuple[int, int, int]:
try:
version_parts = self.version.split(".")
major = int(version_parts[0])
minor = int(version_parts[1])
patch = int(version_parts[2])
return major, minor, patch
except (ValueError, IndexError):
raise DataError("Invalid header version")
def check_signature(self) -> bool:
from trezor.crypto.cosi import verify as cosi_verify
# Nullifying the signature data themselves
value_to_hash = (
self.raw_data[: -self.SIGNATURE_LEN] + b"\x00" * self.SIGNATURE_LEN
)
hasher = sha256()
hasher.update(value_to_hash)
hash: bytes = hasher.digest()
sig_result = cosi_verify(
self.signature, hash, THRESHOLD, PUBLIC_KEYS, self.sigmask
)
if __debug__:
debug_sig_result = cosi_verify(
self.signature, hash, THRESHOLD, DEV_PUBLIC_KEYS, self.sigmask
)
sig_result = sig_result or debug_sig_result
return sig_result
async def change_language(msg: ChangeLanguage) -> Success:
@ -166,70 +21,82 @@ async def change_language(msg: ChangeLanguage) -> Success:
await _require_confirm_change_language(
"Change language", "Do you want to change language to English?"
)
translations.wipe()
translations.deinit()
translations.erase()
# translations.init() would be a no-op here
return Success(message="Language reverted to default")
if data_length > translations.data_max_size():
if data_length > translations.area_bytesize():
raise DataError("Translations too long")
if data_length < _HEADER_SIZE:
if data_length < translations.MAX_HEADER_LEN:
raise DataError("Translations too short")
# Getting and parsing the header
header_data = await get_data_chunk(_HEADER_SIZE, 0)
header = TranslationsHeader.from_bytes(header_data[:])
header_data = await get_data_chunk(msg.data_length, 0)
try:
header = translations.TranslationsHeader(header_data)
except ValueError as e:
if e.args:
raise DataError("Invalid header: " + e.args[0]) from None
else:
raise DataError("Invalid header") from None
# Verifying header information
if header.data_length + _HEADER_SIZE != data_length:
if header.total_len != data_length:
raise DataError("Invalid header data length")
# TODO: how to handle the version updates - numbers have to be bumped in cs.json and others
# (or have this logic in a separate blob-creating tool)
# (have some static check in make gen_check?)
if header.version_tuple() != (
if header.version != (
utils.VERSION_MAJOR,
utils.VERSION_MINOR,
utils.VERSION_PATCH,
0,
):
raise DataError("Invalid translations version")
# Verify signature
if not header.check_signature():
raise DataError("Invalid translations signature")
# Confirm with user
await _require_confirm_change_language(
header.change_language_title, header.change_language_prompt
)
# Show indeterminate loader
progress(None, None, True)
# Initiate loader
loader = progress(None, None)
loader.report(0)
# Loading all the data at once, so we can verify its fingerprint
# If we saved it gradually to the storage and only checked the fingerprint at the end
# (with the idea of deleting the data if the fingerprint does not match),
# attackers could still write some data into storage and then unplug the device.
blob = utils.empty_bytearray(translations.data_max_size())
blob = utils.empty_bytearray(translations.area_bytesize())
# Write the header
blob.extend(header_data)
# Requesting the data in chunks and storing them in the blob
# Also checking the hash of the data for consistency
data_left = data_length - len(header_data)
data_to_fetch = data_length - len(header_data)
data_left = data_to_fetch
offset = len(header_data)
hash_writer = utils.HashWriter(sha256())
while data_left > 0:
data_chunk = await get_data_chunk(data_left, offset)
loader.report(len(blob) * 1000 // data_length)
blob.extend(data_chunk)
hash_writer.write(data_chunk)
data_left -= len(data_chunk)
offset += len(data_chunk)
# When the data do not match the hash, do not write anything
if hash_writer.get_digest() != header.data_hash:
raise DataError("Invalid data hash")
try:
translations.verify(blob)
except Exception:
raise DataError("Translation data verification failed.")
translations.wipe()
translations.deinit()
translations.erase()
translations.write(blob, 0)
translations.init()
loader.report(1000)
return Success(message="Language changed")

@ -1,7 +1,6 @@
from typing import TYPE_CHECKING
from trezor import config, wire
from trezortranslate import TR
from trezor import TR, config, wire
if TYPE_CHECKING:
from typing import Awaitable

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from trezortranslate import TR
from trezor import TR
if TYPE_CHECKING:
from typing import Awaitable

@ -6,11 +6,11 @@ if TYPE_CHECKING:
async def get_next_u2f_counter(msg: GetNextU2FCounter) -> NextU2FCounter:
import storage.device as storage_device
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.messages import NextU2FCounter
from trezor.ui.layouts import confirm_action
from trezor.wire import NotInitialized
from trezortranslate import TR
if not storage_device.is_initialized():
raise NotInitialized("Device is not initialized")

@ -9,12 +9,11 @@ if TYPE_CHECKING:
async def reboot_to_bootloader(msg: RebootToBootloader) -> NoReturn:
from ubinascii import hexlify
from trezor import io, loop, utils, wire
from trezor import TR, io, loop, utils, wire
from trezor.enums import BootCommand
from trezor.messages import Success
from trezor.ui.layouts import confirm_action, confirm_firmware_update
from trezor.wire.context import get_context
from trezortranslate import TR
# Bootloader will only allow the INSTALL_UPGRADE flow for official images.
# This is to prevent a problematic custom signed firmware from self-updating

@ -19,10 +19,9 @@ async def recovery_device(msg: RecoveryDevice) -> Success:
import storage
import storage.device as storage_device
import storage.recovery as storage_recovery
from trezor import config, wire, workflow
from trezor import TR, config, wire, workflow
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import confirm_action, confirm_reset_device
from trezortranslate import TR
from apps.common.request_pin import (
error_pin_invalid,

@ -3,9 +3,8 @@ from typing import TYPE_CHECKING
import storage.device as storage_device
import storage.recovery as storage_recovery
import storage.recovery_shares as storage_recovery_shares
from trezor import wire
from trezor import TR, wire
from trezor.messages import Success
from trezortranslate import TR
from .. import backup_types
from . import layout, recover

@ -1,5 +1,6 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import confirm_action
from trezor.ui.layouts.recovery import ( # noqa: F401
@ -8,7 +9,6 @@ from trezor.ui.layouts.recovery import ( # noqa: F401
show_recovery_warning,
show_remaining_shares,
)
from trezortranslate import TR
from .. import backup_types

@ -22,13 +22,12 @@ _DEFAULT_BACKUP_TYPE = BAK_T_BIP39
async def reset_device(msg: ResetDevice) -> Success:
from trezor import config
from trezor import TR, config
from trezor.crypto import bip39, random
from trezor.messages import EntropyAck, EntropyRequest, Success
from trezor.pin import render_empty_loader
from trezor.ui.layouts import confirm_reset_device, prompt_backup
from trezor.wire.context import call
from trezortranslate import TR
from apps.common.request_pin import request_pin_confirm

@ -1,6 +1,7 @@
from micropython import const
from typing import Sequence
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import show_success
from trezor.ui.layouts.reset import ( # noqa: F401
@ -11,7 +12,6 @@ from trezor.ui.layouts.reset import ( # noqa: F401
slip39_prompt_threshold,
slip39_show_checklist,
)
from trezortranslate import TR
_NUM_OF_CHOICES = const(3)

@ -2,12 +2,11 @@ from typing import TYPE_CHECKING
import storage.device as storage_device
import storage.sd_salt as storage_sd_salt
from trezor import config
from trezor import TR, config
from trezor.enums import SdProtectOperationType
from trezor.messages import Success
from trezor.ui.layouts import show_success
from trezor.wire import ProcessError
from trezortranslate import TR
from apps.common.request_pin import error_pin_invalid, request_pin_and_sd_salt
from apps.common.sdcard import ensure_sdcard

@ -6,11 +6,10 @@ if TYPE_CHECKING:
async def set_u2f_counter(msg: SetU2FCounter) -> Success:
import storage.device as storage_device
from trezor import wire
from trezor import TR, wire
from trezor.enums import ButtonRequestType
from trezor.messages import Success
from trezor.ui.layouts import confirm_action
from trezortranslate import TR
if not storage_device.is_initialized():
raise wire.NotInitialized("Device is not initialized")

@ -6,10 +6,10 @@ if TYPE_CHECKING:
async def wipe_device(msg: WipeDevice) -> Success:
import storage
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.messages import Success
from trezor.ui.layouts import confirm_action
from trezortranslate import TR
from apps.base import reload_settings_from_storage

@ -8,11 +8,11 @@ if TYPE_CHECKING:
async def cipher_key_value(msg: CipherKeyValue) -> CipheredKeyValue:
from trezor import TR
from trezor.crypto import aes, hmac
from trezor.messages import CipheredKeyValue
from trezor.ui.layouts import confirm_action
from trezor.wire import DataError
from trezortranslate import TR
from apps.common.keychain import get_keychain
from apps.common.paths import AlwaysMatchingSchema

@ -5,11 +5,11 @@ if TYPE_CHECKING:
async def get_entropy(msg: GetEntropy) -> Entropy:
from trezor import TR
from trezor.crypto import random
from trezor.enums import ButtonRequestType
from trezor.messages import Entropy
from trezor.ui.layouts import confirm_action
from trezortranslate import TR
await confirm_action(
"get_entropy",

@ -1,5 +1,6 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import confirm_action, confirm_metadata # noqa: F401
from trezor.ui.layouts.progress import ( # noqa: F401
@ -7,7 +8,6 @@ from trezor.ui.layouts.progress import ( # noqa: F401
monero_live_refresh_progress,
monero_transaction_progress_inner,
)
from trezortranslate import TR
DUMMY_PAYMENT_ID = b"\x00\x00\x00\x00\x00\x00\x00\x00"

@ -1,7 +1,7 @@
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.strings import format_amount
from trezor.ui.layouts import confirm_metadata
from trezortranslate import TR
from .helpers import NEM_MAX_DIVISIBILITY

@ -1,6 +1,6 @@
from typing import TYPE_CHECKING
from trezortranslate import TR
from trezor import TR
from ..layout import require_confirm_content, require_confirm_final

@ -1,7 +1,7 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.crypto import nem
from trezortranslate import TR
if TYPE_CHECKING:
from trezor.messages import (

@ -7,7 +7,7 @@ if TYPE_CHECKING:
async def ask_provision_namespace(
common: NEMTransactionCommon, namespace: NEMProvisionNamespace
) -> None:
from trezortranslate import TR
from trezor import TR
from ..layout import (
require_confirm_content,

@ -1,8 +1,8 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.strings import format_amount
from trezortranslate import TR
from ..helpers import NEM_MOSAIC_AMOUNT_DIVISOR
from ..layout import require_confirm_final

@ -13,7 +13,7 @@ async def require_confirm_total(total: int, fee: int) -> None:
async def require_confirm_destination_tag(tag: int) -> None:
from trezortranslate import TR
from trezor import TR
await confirm_metadata(
"confirm_destination_tag",

@ -1,5 +1,6 @@
from typing import TYPE_CHECKING
from trezor import TR
from trezor.crypto import base58
from trezor.enums import ButtonRequestType
from trezor.strings import format_amount
@ -9,7 +10,6 @@ from trezor.ui.layouts import (
confirm_solana_tx,
confirm_value,
)
from trezortranslate import TR
from apps.common.paths import address_n_to_str

@ -18,11 +18,11 @@ async def sign_tx(
msg: SolanaSignTx,
keychain: Keychain,
) -> SolanaTxSignature:
from trezor import TR
from trezor.crypto.curve import ed25519
from trezor.enums import ButtonRequestType
from trezor.messages import SolanaTxSignature
from trezor.ui.layouts import confirm_metadata, show_warning
from trezortranslate import TR
from apps.common import seed

@ -1,9 +1,8 @@
from typing import TYPE_CHECKING
import trezor.ui.layouts as layouts
from trezor import strings
from trezor import TR, strings
from trezor.enums import ButtonRequestType
from trezortranslate import TR
from . import consts

@ -1,6 +1,7 @@
from typing import TYPE_CHECKING
from ubinascii import hexlify
from trezor import TR
from trezor.ui.layouts import (
confirm_address,
confirm_amount,
@ -9,7 +10,6 @@ from trezor.ui.layouts import (
confirm_properties,
)
from trezor.wire import DataError, ProcessError
from trezortranslate import TR
from ..layout import format_amount

@ -1,6 +1,6 @@
from trezor import TR
from trezor.enums import ButtonRequestType
from trezor.ui.layouts import confirm_address, confirm_metadata, confirm_properties
from trezortranslate import TR
BR_SIGN_TX = ButtonRequestType.SignTx # global_import_cache

@ -6,11 +6,10 @@ if TYPE_CHECKING:
async def add_resident_credential(msg: WebAuthnAddResidentCredential) -> Success:
import storage.device as storage_device
from trezor import wire
from trezor import TR, wire
from trezor.messages import Success
from trezor.ui.layouts import show_error_and_raise
from trezor.ui.layouts.fido import confirm_fido
from trezortranslate import TR
from .credential import Fido2Credential
from .resident_credentials import store_resident_credential

@ -5,11 +5,10 @@ from micropython import const
from typing import TYPE_CHECKING
import storage.device as storage_device
from trezor import config, io, log, loop, utils, wire, workflow
from trezor import TR, config, io, log, loop, utils, wire, workflow
from trezor.crypto import hashlib
from trezor.crypto.curve import nist256p1
from trezor.ui.layouts import show_error_popup
from trezortranslate import TR
from apps.base import set_homescreen
from apps.common import cbor

@ -7,9 +7,9 @@ if TYPE_CHECKING:
async def list_resident_credentials(
msg: WebAuthnListResidentCredentials,
) -> WebAuthnCredentials:
from trezor import TR
from trezor.messages import WebAuthnCredential, WebAuthnCredentials
from trezor.ui.layouts import confirm_action
from trezortranslate import TR
from . import resident_credentials

@ -7,10 +7,9 @@ if TYPE_CHECKING:
async def remove_resident_credential(msg: WebAuthnRemoveResidentCredential) -> Success:
import storage.device
import storage.resident_credentials
from trezor import wire
from trezor import TR, wire
from trezor.messages import Success
from trezor.ui.layouts.fido import confirm_fido
from trezortranslate import TR
from .resident_credentials import get_resident_credential

@ -10,7 +10,7 @@ welcome_screen_start_ms = utime.ticks_ms()
import storage
import storage.device
from trezor import config, log, loop, ui, utils, wire
from trezor import config, log, loop, ui, utils, wire, translations
from trezor.pin import (
allow_all_loader_messages,
ignore_nonpin_loader_messages,
@ -82,6 +82,7 @@ async def bootscreen() -> None:
ignore_nonpin_loader_messages()
config.init(show_pin_timeout)
translations.init()
if __debug__ and not utils.EMULATOR:
config.wipe()

@ -1,2 +1,6 @@
import trezorconfig as config # noqa: F401
import trezorio as io # noqa: F401
import trezortranslate as translations # noqa: F401
TR = translations.TR

@ -39,8 +39,8 @@ def render_empty_loader(message: str, description: str) -> None:
def show_pin_timeout(seconds: int, progress: int, message: str) -> bool:
from trezor import TR
from trezor.ui.layouts.progress import pin_progress
from trezortranslate import TR
# Possibility to ignore certain messages - not showing loader for them
if message in _ignore_loader_messages:

@ -1,29 +0,0 @@
from trezor import config
DEFAULT_LANGUAGE = "en-US"
def get_language() -> str:
from trezortranslate import language_name
translation_lang = language_name()
if translation_lang:
return translation_lang
return DEFAULT_LANGUAGE
def write(data: bytes | bytearray, offset: int) -> None:
from trezor import wire
if offset + len(data) > data_max_size():
raise wire.DataError("Language data too long")
config.translations_set(data, offset)
def wipe() -> None:
config.translations_wipe()
def data_max_size() -> int:
return config.translations_max_bytesize()

@ -1,11 +1,10 @@
from typing import TYPE_CHECKING
import trezorui2
from trezor import io, loop, ui
from trezor import TR, io, loop, ui
from trezor.enums import ButtonRequestType
from trezor.wire import ActionCancelled
from trezor.wire.context import wait as ctx_wait
from trezortranslate import TR
from ..common import button_request, interact

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save