Merge branch 'master' into release/23.09

release/23.09
Martin Milata 8 months ago
commit 07027a69e9

@ -214,10 +214,12 @@ core unix regular asan build:
variables:
ADDRESS_SANITIZER: "1"
script:
- $NIX_SHELL --run "poetry run make -C core build_bootloader_emu"
- $NIX_SHELL --run "poetry run make -C core build_unix"
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- core/build/bootloader_emu/bootloader.elf
- core/build/unix # most of it needed by test_rust
expire_in: 1 week
@ -229,10 +231,12 @@ core unix frozen regular build:
<<: *gitlab_caching
needs: []
script:
- $NIX_SHELL --run "poetry run make -C core build_bootloader_emu"
- $NIX_SHELL --run "poetry run make -C core build_unix_frozen"
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
paths:
- core/build/bootloader_emu/bootloader.elf
- core/build/unix/trezor-emu-core
expire_in: 1 week
@ -301,7 +305,8 @@ core unix frozen R debug build:
PYOPT: "0"
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_unix_frozen"
- $NIX_SHELL --run "poetry run make -C core build_bootloader_emu"
- $NIX_SHELL --run "poetry run make -C core build_unix_frozen"
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"
untracked: true
@ -316,7 +321,7 @@ core unix frozen R debug build arm:
PYOPT: "0"
TREZOR_MODEL: "R"
script:
- nix-shell --run "poetry run make -C core build_unix_frozen"
- $NIX_SHELL --run "poetry run make -C core build_unix_frozen"
- mv core/build/unix/trezor-emu-core core/build/unix/trezor-emu-core-arm
artifacts:
name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA"

@ -86,7 +86,7 @@ If you want to add a **wallet link**, modify the file [`wallets.json`](wallets.j
# Support Information
We keep track of support status of each built-in coin over our devices. That is
`trezor1` for Trezor One, `trezor2` for Trezor T, `connect` for [Connect](https://github.com/trezor/connect)
`T1B1` for Trezor One, `T2T1` for Trezor T, `T2B1` for Trezor R, `connect` for [Connect](https://github.com/trezor/connect)
and `suite` for [Trezor Suite](https://suite.trezor.io/). In further description, the word "device"
applies to Connect and Suite as well.

@ -1,88 +1,5 @@
{
"connect": {
"supported": {
"bitcoin:ACM": true,
"bitcoin:AXE": true,
"bitcoin:BCH": true,
"bitcoin:BTC": true,
"bitcoin:BTCP": true,
"bitcoin:BTG": true,
"bitcoin:BTX": true,
"bitcoin:DASH": true,
"bitcoin:DCR": true,
"bitcoin:DGB": true,
"bitcoin:DOGE": true,
"bitcoin:FIRO": true,
"bitcoin:FJC": true,
"bitcoin:FLO": true,
"bitcoin:FTC": true,
"bitcoin:KMD": true,
"bitcoin:KOTO": true,
"bitcoin:LTC": true,
"bitcoin:MONA": true,
"bitcoin:NMC": true,
"bitcoin:PPC": true,
"bitcoin:REGTEST": true,
"bitcoin:RITO": true,
"bitcoin:RVN": true,
"bitcoin:SYS": true,
"bitcoin:TAZ": true,
"bitcoin:TBCH": true,
"bitcoin:TBTG": true,
"bitcoin:TDCR": true,
"bitcoin:TEST": true,
"bitcoin:UNO": true,
"bitcoin:VIA": true,
"bitcoin:VTC": true,
"bitcoin:XPM": true,
"bitcoin:XRC": true,
"bitcoin:XSN": true,
"bitcoin:XVG": true,
"bitcoin:ZCR": true,
"bitcoin:ZEC": true,
"bitcoin:tDASH": true,
"bitcoin:tFIRO": true,
"bitcoin:tLTC": true,
"bitcoin:tPPC": true,
"eth:tETH:3": true,
"misc:ADA": true,
"misc:BNB": true,
"misc:EOS": true,
"misc:XLM": true,
"misc:XRP": true,
"misc:XTZ": true,
"misc:tADA": true,
"misc:tXRP": true,
"nem:BREEZE": true,
"nem:DIM": true,
"nem:DIMTOK": true,
"nem:PAC:CHS": true,
"nem:PAC:HRT": true,
"nem:XEM": true
},
"unsupported": {}
},
"suite": {
"supported": {
"bitcoin:BCH": true,
"bitcoin:BTC": true,
"bitcoin:BTG": true,
"bitcoin:DASH": true,
"bitcoin:DGB": true,
"bitcoin:DOGE": true,
"bitcoin:LTC": true,
"bitcoin:NMC": true,
"bitcoin:REGTEST": true,
"bitcoin:TEST": true,
"bitcoin:VTC": true,
"bitcoin:ZEC": true,
"eth:tETH:3": true,
"misc:XRP": true,
"misc:tXRP": true
},
"unsupported": {}
},
"trezor1": {
"T1B1": {
"supported": {
"bitcoin:ACM": "1.7.2",
"bitcoin:AXE": "1.7.3",
@ -195,7 +112,120 @@
"misc:tXRP": "not implemented"
}
},
"trezor2": {
"T2B1": {
"supported": {
"bitcoin:ACM": "2.6.1",
"bitcoin:AXE": "2.6.1",
"bitcoin:BCH": "2.6.1",
"bitcoin:BTC": "2.6.1",
"bitcoin:BTCP": "2.6.1",
"bitcoin:BTX": "2.6.1",
"bitcoin:CPU": "2.6.1",
"bitcoin:CRW": "2.6.1",
"bitcoin:DOGE": "2.6.1",
"bitcoin:ELEMENTS": "2.6.1",
"bitcoin:FIRO": "2.6.1",
"bitcoin:FJC": "2.6.1",
"bitcoin:FLO": "2.6.1",
"bitcoin:FTC": "2.6.1",
"bitcoin:GRS": "2.6.1",
"bitcoin:KMD": "2.6.1",
"bitcoin:KOTO": "2.6.1",
"bitcoin:LTC": "2.6.1",
"bitcoin:MONA": "2.6.1",
"bitcoin:PPC": "2.6.1",
"bitcoin:QTUM": "2.6.1",
"bitcoin:REGTEST": "2.6.1",
"bitcoin:RITO": "2.6.1",
"bitcoin:RVN": "2.6.1",
"bitcoin:SMART": "2.6.1",
"bitcoin:SYS": "2.6.1",
"bitcoin:TAZ": "2.6.1",
"bitcoin:TBCH": "2.6.1",
"bitcoin:TEST": "2.6.1",
"bitcoin:UNO": "2.6.1",
"bitcoin:VIA": "2.6.1",
"bitcoin:VIPS": "2.6.1",
"bitcoin:XPM": "2.6.1",
"bitcoin:XRC": "2.6.1",
"bitcoin:XSN": "2.6.1",
"bitcoin:XVG": "2.6.1",
"bitcoin:ZCR": "2.6.1",
"bitcoin:ZEC": "2.6.1",
"bitcoin:tFIRO": "2.6.1",
"bitcoin:tGRS": "2.6.1",
"bitcoin:tLTC": "2.6.1",
"bitcoin:tPPC": "2.6.1",
"bitcoin:tQTUM": "2.6.1",
"bitcoin:tRVN": "2.6.1",
"bitcoin:tSMART": "2.6.1",
"erc20:bnb:ATOM": "2.6.1",
"erc20:eth:AAVE": "2.6.1",
"erc20:eth:APE": "2.6.1",
"erc20:eth:AXS": "2.6.1",
"erc20:eth:BUSD": "2.6.1",
"erc20:eth:CHZ": "2.6.1",
"erc20:eth:CRO": "2.6.1",
"erc20:eth:DAI": "2.6.1",
"erc20:eth:FRAX": "2.6.1",
"erc20:eth:LEO": "2.6.1",
"erc20:eth:LINK": "2.6.1",
"erc20:eth:MANA": "2.6.1",
"erc20:eth:MATIC": "2.6.1",
"erc20:eth:OKB": "2.6.1",
"erc20:eth:QNT": "2.6.1",
"erc20:eth:SAND": "2.6.1",
"erc20:eth:SHIB": "2.6.1",
"erc20:eth:STETH": "2.6.1",
"erc20:eth:UNI": "2.6.1",
"erc20:eth:USDC": "2.6.1",
"erc20:eth:USDT": "2.6.1",
"erc20:eth:WBTC": "2.6.1",
"erc20:eth:XCN": "2.6.1",
"erc20:matic:WAVAX": "2.6.1",
"eth:BNB:56": "2.6.1",
"eth:ETC:61": "2.6.1",
"eth:ETH:1": "2.6.1",
"eth:MATIC:137": "2.6.1",
"eth:tETH:3": "2.6.1",
"eth:tETH:4": "2.6.1",
"eth:tETH:5": "2.6.1",
"misc:ADA": "2.6.1",
"misc:BNB": "2.6.1",
"misc:MAID": "2.6.1",
"misc:OMNI": "2.6.1",
"misc:USDT": "2.6.1",
"misc:XLM": "2.6.1",
"misc:XMR": "2.6.1",
"misc:XRP": "2.6.1",
"misc:XTZ": "2.6.1",
"misc:tADA": "2.6.1",
"misc:tXRP": "2.6.1",
"nem:BREEZE": "2.6.1",
"nem:DIM": "2.6.1",
"nem:DIMTOK": "2.6.1",
"nem:PAC:CHS": "2.6.1",
"nem:PAC:HRT": "2.6.1"
},
"unsupported": {
"bitcoin:BTG": "not for T2B1 (#2793)",
"bitcoin:DASH": "not for T2B1 (#2793)",
"bitcoin:DCR": "not for T2B1 (#2793)",
"bitcoin:DGB": "not for T2B1 (#2793)",
"bitcoin:NMC": "not for T2B1 (#2793)",
"bitcoin:PART": "incompatible fork",
"bitcoin:TBTG": "not for T2B1 (#2793)",
"bitcoin:TDCR": "not for T2B1 (#2793)",
"bitcoin:TRC": "address_type collides with Bitcoin",
"bitcoin:VTC": "not for T2B1 (#2793)",
"bitcoin:tDASH": "not for T2B1 (#2793)",
"bitcoin:tPART": "incompatible fork",
"misc:EOS": "not for T2B1 (#2793)",
"misc:LSK": "Incompatible mainnet hard-fork",
"nem:XEM": "not for T2B1 (#2793)"
}
},
"T2T1": {
"supported": {
"bitcoin:ACM": "2.0.10",
"bitcoin:AXE": "2.0.11",
@ -307,5 +337,88 @@
"bitcoin:tPART": "incompatible fork",
"misc:LSK": "Incompatible mainnet hard-fork"
}
},
"connect": {
"supported": {
"bitcoin:ACM": true,
"bitcoin:AXE": true,
"bitcoin:BCH": true,
"bitcoin:BTC": true,
"bitcoin:BTCP": true,
"bitcoin:BTG": true,
"bitcoin:BTX": true,
"bitcoin:DASH": true,
"bitcoin:DCR": true,
"bitcoin:DGB": true,
"bitcoin:DOGE": true,
"bitcoin:FIRO": true,
"bitcoin:FJC": true,
"bitcoin:FLO": true,
"bitcoin:FTC": true,
"bitcoin:KMD": true,
"bitcoin:KOTO": true,
"bitcoin:LTC": true,
"bitcoin:MONA": true,
"bitcoin:NMC": true,
"bitcoin:PPC": true,
"bitcoin:REGTEST": true,
"bitcoin:RITO": true,
"bitcoin:RVN": true,
"bitcoin:SYS": true,
"bitcoin:TAZ": true,
"bitcoin:TBCH": true,
"bitcoin:TBTG": true,
"bitcoin:TDCR": true,
"bitcoin:TEST": true,
"bitcoin:UNO": true,
"bitcoin:VIA": true,
"bitcoin:VTC": true,
"bitcoin:XPM": true,
"bitcoin:XRC": true,
"bitcoin:XSN": true,
"bitcoin:XVG": true,
"bitcoin:ZCR": true,
"bitcoin:ZEC": true,
"bitcoin:tDASH": true,
"bitcoin:tFIRO": true,
"bitcoin:tLTC": true,
"bitcoin:tPPC": true,
"eth:tETH:3": true,
"misc:ADA": true,
"misc:BNB": true,
"misc:EOS": true,
"misc:XLM": true,
"misc:XRP": true,
"misc:XTZ": true,
"misc:tADA": true,
"misc:tXRP": true,
"nem:BREEZE": true,
"nem:DIM": true,
"nem:DIMTOK": true,
"nem:PAC:CHS": true,
"nem:PAC:HRT": true,
"nem:XEM": true
},
"unsupported": {}
},
"suite": {
"supported": {
"bitcoin:BCH": true,
"bitcoin:BTC": true,
"bitcoin:BTG": true,
"bitcoin:DASH": true,
"bitcoin:DGB": true,
"bitcoin:DOGE": true,
"bitcoin:LTC": true,
"bitcoin:NMC": true,
"bitcoin:REGTEST": true,
"bitcoin:TEST": true,
"bitcoin:VTC": true,
"bitcoin:ZEC": true,
"eth:tETH:3": true,
"misc:XRP": true,
"misc:tXRP": true
},
"unsupported": {}
}
}

@ -0,0 +1,17 @@
{
"T1B1": {
"name": "Trezor Model One",
"colors": {}
},
"T2T1": {
"name": "Trezor Model T",
"colors": {}
},
"T2B1": {
"name": "Trezor Model R",
"colors": {
"0": "Black",
"1": "White"
}
}
}

@ -22,6 +22,7 @@ chance that somebody is relying on the behavior.
message BinanceGetAddress {
repeated uint32 address_n = 1; // BIP-32-style path to derive the key from master node
optional bool show_display = 2; // optionally prompt for confirmation on trezor display
optional bool chunkify = 3; // display the address in chunks of 4 characters
}
/**
@ -66,6 +67,7 @@ message BinanceSignTx {
optional string memo = 5;
required sint64 sequence = 6;
required sint64 source = 7;
optional bool chunkify = 8; // display the address in chunks of 4 characters
}
/**
@ -85,6 +87,7 @@ message BinanceTxRequest {
message BinanceTransferMsg {
repeated BinanceInputOutput inputs = 1;
repeated BinanceInputOutput outputs = 2;
optional bool chunkify = 3; // display the address in chunks of 4 characters
message BinanceInputOutput {
required string address = 1;

@ -110,6 +110,7 @@ message GetAddress {
optional MultisigRedeemScriptType multisig = 4; // filled if we are showing a multisig address
optional InputScriptType script_type = 5 [default=SPENDADDRESS]; // used to distinguish between various address formats (non-segwit, segwit, etc.)
optional bool ignore_xpub_magic = 6; // ignore SLIP-0132 XPUB magic, use xpub/tpub prefix for all account types
optional bool chunkify = 7; // display the address in chunks of 4 characters
}
/**
@ -199,6 +200,7 @@ message SignTx {
optional bool decred_staking_ticket = 12 [default=false]; // only for Decred, this is signing a ticket purchase
optional bool serialize = 13 [default=true]; // serialize the full transaction, as opposed to only outputting the signatures
optional CoinJoinRequest coinjoin_request = 14; // only for preauthorized CoinJoins
optional bool chunkify = 15; // display the address in chunks of 4 characters
/**
* Signing request for a CoinJoin transaction.

@ -164,6 +164,7 @@ message CardanoGetAddress {
required uint32 network_id = 4; // network id - mainnet or testnet
required CardanoAddressParametersType address_parameters = 5; // parameters used to derive the address
required CardanoDerivationType derivation_type = 6;
optional bool chunkify = 7; // display the address in chunks of 4 characters
}
/**
@ -223,6 +224,7 @@ message CardanoSignTxInit {
optional bool has_collateral_return = 19 [default=false];
optional uint64 total_collateral = 20;
optional uint32 reference_inputs_count = 21 [default=0];
optional bool chunkify = 22; // display the address in chunks of 4 characters
}
/**

@ -14,6 +14,7 @@ option java_outer_classname = "TrezorMessageEos";
message EosGetPublicKey {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node 44'/194'/0'
optional bool show_display = 2; // optionally show on display before sending the result
optional bool chunkify = 3; // display the address in chunks of 4 characters
}
/**
@ -36,6 +37,7 @@ message EosSignTx {
required bytes chain_id = 2; // 256-bit long chain id
required EosTxHeader header = 3; // EOS transaction header
required uint32 num_actions = 4; // number of actions
optional bool chunkify = 5; // display the address in chunks of 4 characters
/**
* Structure representing EOS transaction header

@ -39,6 +39,7 @@ message EthereumGetAddress {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bool show_display = 2; // optionally show on display before sending the result
optional bytes encoded_network = 3; // encoded Ethereum network, see ethereum-definitions.md for details
optional bool chunkify = 4; // display the address in chunks of 4 characters
}
/**
@ -71,6 +72,7 @@ message EthereumSignTx {
required uint64 chain_id = 9; // Chain Id for EIP 155
optional uint32 tx_type = 10; // Used for Wanchain
optional ethereum_definitions.EthereumDefinitions definitions = 12; // network and/or token definitions for tx
optional bool chunkify = 13; // display the address in chunks of 4 characters
}
/**
@ -93,6 +95,7 @@ message EthereumSignTxEIP1559 {
required uint64 chain_id = 10; // Chain Id for EIP 155
repeated EthereumAccessList access_list = 11; // Access List
optional ethereum_definitions.EthereumDefinitions definitions = 12; // network and/or token definitions for tx
optional bool chunkify = 13; // display the address in chunks of 4 characters
message EthereumAccessList {
required string address = 1;

@ -129,6 +129,7 @@ message Features {
optional bool unit_btconly = 46; // unit/device is intended as bitcoin only
optional uint32 homescreen_width = 47; // homescreen width in pixels
optional uint32 homescreen_height = 48; // homescreen height in pixels
optional bool bootloader_locked = 49; // bootloader is locked
}
/**
@ -280,6 +281,25 @@ message FirmwareHash {
required bytes hash = 1;
}
/**
* Request: Request a signature of the provided challenge.
* @start
* @next AuthenticityProof
* @next Failure
*/
message AuthenticateDevice {
required bytes challenge = 1; // A random challenge to sign.
}
/**
* Response: Signature of the provided challenge along with a certificate issued by the Trezor company.
* @end
*/
message AuthenticityProof {
repeated bytes certificates = 1; // A certificate chain starting with the device certificate, followed by intermediate CA certificates, the last of which is signed by Trezor company's root CA.
required bytes signature = 2; // A DER-encoded signature of "\0x13AuthenticateDevice:" + length-prefixed challenge that should be verified using the device certificate.
}
/**
* Request: Request device to wipe all sensitive data and settings
* @start

@ -90,6 +90,7 @@ message MoneroGetAddress {
optional uint32 account = 4; // Major subaddr index
optional uint32 minor = 5; // Minor subaddr index
optional bytes payment_id = 6; // Payment ID for integrated address
optional bool chunkify = 7; // display the address in chunks of 4 characters
}
/**
@ -149,6 +150,7 @@ message MoneroTransactionInitRequest {
optional uint32 client_version = 13; // connected client version
optional uint32 hard_fork = 14; // transaction hard fork number
optional bytes monero_version = 15; // monero software version
optional bool chunkify = 16; // display the address in chunks of 4 characters
}
}

@ -15,6 +15,7 @@ message NEMGetAddress {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional uint32 network = 2 [default=0x68]; // Network ID (0x68 = Mainnet, 0x98 = Testnet, 0x60 = Mijin)
optional bool show_display = 3; // Optionally show on display before sending the result
optional bool chunkify = 4; // display the address in chunks of 4 characters
}
/**
@ -41,6 +42,8 @@ message NEMSignTx {
optional NEMMosaicSupplyChange supply_change = 7; // Mosaic supply change part
optional NEMAggregateModification aggregate_modification = 8; // Aggregate modification part
optional NEMImportanceTransfer importance_transfer = 9; // Importance transfer part
optional bool chunkify = 10; // display the address in chunks of 4 characters
/**
* Structure representing the common part for NEM transactions
*/

@ -13,6 +13,7 @@ option java_outer_classname = "TrezorMessageRipple";
message RippleGetAddress {
repeated uint32 address_n = 1; // BIP-32 path. For compatibility with other wallets, must be m/44'/144'/index'
optional bool show_display = 2; // optionally show on display before sending the result
optional bool chunkify = 3; // display the address in chunks of 4 characters
}
/**
@ -35,6 +36,7 @@ message RippleSignTx {
required uint32 sequence = 4; // transaction sequence number
optional uint32 last_ledger_sequence = 5; // see https://developers.ripple.com/reliable-transaction-submission.html#lastledgersequence
required RipplePayment payment = 6; // Payment transaction type
optional bool chunkify = 7; // display the address in chunks of 4 characters
/**
* Payment transaction type

@ -31,6 +31,7 @@ message StellarAsset {
message StellarGetAddress {
repeated uint32 address_n = 1; // BIP-32 path. For compatibility with other wallets, must be m/44'/148'/index'
optional bool show_display = 2; // optionally show on display before sending the result
optional bool chunkify = 3; // display the address in chunks of 4 characters
}
/**

@ -14,6 +14,7 @@ option java_outer_classname = "TrezorMessageTezos";
message TezosGetAddress {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bool show_display = 2; // optionally show on display before sending the result
optional bool chunkify = 3; // display the address in chunks of 4 characters
}
/**
@ -32,6 +33,7 @@ message TezosAddress {
message TezosGetPublicKey {
repeated uint32 address_n = 1; // BIP-32 path to derive the key from master node
optional bool show_display = 2; // Optionally show on display before sending the result
optional bool chunkify = 3; // display the public key in chunks of 4 characters
}
/**
@ -57,6 +59,8 @@ message TezosSignTx {
optional TezosDelegationOp delegation = 6; // Tezos delegation operation
optional TezosProposalOp proposal = 7; // Tezos proposal operation
optional TezosBallotOp ballot = 8; // Tezos ballot operation
optional bool chunkify = 9; // display the address in chunks of 4 characters
/*
* Tezos contract ID
*/

@ -122,6 +122,8 @@ enum MessageType {
MessageType_UnlockedPathRequest = 94 [(bitcoin_only) = true, (wire_out) = true];
MessageType_ShowDeviceTutorial = 95 [(bitcoin_only) = true, (wire_in) = true];
MessageType_UnlockBootloader = 96 [(bitcoin_only) = true, (wire_in) = true];
MessageType_AuthenticateDevice = 97 [(bitcoin_only) = true, (wire_out) = true];
MessageType_AuthenticityProof = 98 [(bitcoin_only) = true, (wire_in) = true];
MessageType_SetU2FCounter = 63 [(wire_in) = true];
MessageType_GetNextU2FCounter = 80 [(wire_in) = true];

@ -55,7 +55,7 @@ for token in defs.erc20:
support_info = coin_info.support_info(defs.misc)
for key, support in support_info.values():
t2_support = support["trezor2"]
t2_support = support["T2T1"]
coin_name = dict_by_coin_key[key]
if t2_support:
print(coin_name, "is supported since version", t2_support)
@ -94,15 +94,15 @@ support statuses at the same time:
$ ./support.py show Ontology
misc:ONT - Ontology (ONT)
* connect : NO
* trezor1 : support info missing
* trezor2 : support info missing
* T1B1 : support info missing
* T2T1 : support info missing
* suite : NO
$ ./support.py set misc:ONT trezor1=no -r "not planned on T1" trezor2=2.4.7
$ ./support.py set misc:ONT T1B1=no -r "not planned on T1" T2T1=2.4.7
misc:ONT - Ontology (ONT)
* connect : NO
* trezor1 : NO (reason: not planned on T1)
* trezor2 : 2.4.7
* T1B1 : NO (reason: not planned on T1)
* T2T1 : 2.4.7
* suite : NO
```

@ -39,15 +39,17 @@ class SupportItemVersion(TypedDict):
class SupportData(TypedDict):
connect: SupportItemBool
suite: SupportItemBool
trezor1: SupportItemVersion
trezor2: SupportItemVersion
t1b1: SupportItemVersion
t2t1: SupportItemVersion
t2b1: SupportItemVersion
class SupportInfoItem(TypedDict):
connect: bool
suite: bool
trezor1: Literal[False] | str
trezor2: Literal[False] | str
t1b1: Literal[False] | str
t2t1: Literal[False] | str
t2b1: Literal[False] | str
SupportInfo = Dict[str, SupportInfoItem]
@ -452,7 +454,7 @@ def _load_fido_apps() -> FidoApps:
RELEASES_URL = "https://data.trezor.io/firmware/{}/releases.json"
MISSING_SUPPORT_MEANS_NO = ("connect", "suite")
VERSIONED_SUPPORT_INFO = ("trezor1", "trezor2")
VERSIONED_SUPPORT_INFO = ("T1B1", "T2T1", "T2B1")
def get_support_data() -> SupportData:
@ -461,14 +463,16 @@ def get_support_data() -> SupportData:
def latest_releases() -> dict[str, Any]:
"""Get latest released firmware versions for Trezor 1 and 2"""
"""Get latest released firmware versions for all models"""
if not requests:
raise RuntimeError("requests library is required for getting release info")
latest: dict[str, Any] = {}
for v in ("1", "2"):
releases = requests.get(RELEASES_URL.format(v)).json()
latest["trezor" + v] = max(tuple(r["version"]) for r in releases)
for model in VERSIONED_SUPPORT_INFO:
# TODO: support new UPPERCASE model names in RELEASES_URL
url_model = model.lower() # need to be e.g. t1b1 for now
releases = requests.get(RELEASES_URL.format(url_model)).json()
latest[model] = max(tuple(r["version"]) for r in releases)
return latest
@ -505,7 +509,7 @@ def support_info(coins: Iterable[Coin] | CoinsInfo | dict[str, Coin]) -> Support
Takes a collection of coins and generates a support-info entry for each.
The support-info is a dict with keys based on `support.json` keys.
These are usually: "trezor1", "trezor2", "connect" and "suite".
These are usually: "T1B1", "T2T1", "T2B1", "connect" and "suite".
The `coins` argument can be a `CoinsInfo` object, a list or a dict of
coin items.

@ -675,7 +675,7 @@ def check(backend: bool, icons: bool) -> None:
type_choice = click.Choice(["bitcoin", "eth", "erc20", "nem", "misc"])
device_choice = click.Choice(["connect", "suite", "trezor1", "trezor2"])
device_choice = click.Choice(["connect", "suite", "T1B1", "T2T1", "T2B1"])
@cli.command()
@ -692,8 +692,8 @@ device_choice = click.Choice(["connect", "suite", "trezor1", "trezor2"])
@click.option("-f", "--filter", metavar="FIELD=FILTER", multiple=True, help="Include only coins that match a filter (-f taproot=true -f maintainer='*stick*')")
@click.option("-F", "--filter-exclude", metavar="FIELD=FILTER", multiple=True, help="Exclude coins that match a filter (-F 'blockbook=[]' -F 'slip44=*')")
@click.option("-t", "--exclude-tokens", is_flag=True, help="Exclude ERC20 tokens. Equivalent to '-E erc20'")
@click.option("-d", "--device-include", metavar="NAME", multiple=True, type=device_choice, help="Only include coins supported on these given devices (-d connect -d trezor1)")
@click.option("-D", "--device-exclude", metavar="NAME", multiple=True, type=device_choice, help="Only include coins not supported on these given devices (-D suite -D trezor2)")
@click.option("-d", "--device-include", metavar="NAME", multiple=True, type=device_choice, help="Only include coins supported on these given devices (-d connect -d T1B1)")
@click.option("-D", "--device-exclude", metavar="NAME", multiple=True, type=device_choice, help="Only include coins not supported on these given devices (-D suite -D T2T1)")
# fmt: on
def dump(
outfile: TextIO,
@ -742,7 +742,7 @@ def dump(
Also devices can be used as filters. For example to find out which coins are
supported in Suite and connect but not on Trezor 1, it is possible to say
'-d suite -d connect -D trezor1'.
'-d suite -d connect -D T1B1'.
Includes even the wallet data, unless turned off by '-W'.
These can be filtered by using '-f', for example `-f 'wallet=*exodus*'` (* are necessary)

@ -1,4 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import re
@ -225,8 +227,7 @@ def check(ignore_missing):
@cli.command()
# fmt: off
@click.option("--v1", help="Version for T1 release (default: guess from latest)")
@click.option("--v2", help="Version for TT release (default: guess from latest)")
@click.option("-r", '--releases', multiple=True, type=str, help='Key-value pairs of model and version. E.g. "T2B1=2.6.1"')
@click.option("-n", "--dry-run", is_flag=True, help="Do not write changes")
@click.option("-f", "--force", is_flag=True, help="Proceed even with bad version/device info")
@click.option("--skip-testnets/--no-skip-testnets", default=True, help="Automatically exclude testnets")
@ -234,11 +235,10 @@ def check(ignore_missing):
@click.pass_context
def release(
ctx,
v1,
v2,
dry_run,
force,
skip_testnets,
releases: list[str],
dry_run: bool,
force: bool,
skip_testnets: bool,
):
"""Release a new Trezor firmware.
@ -248,27 +248,36 @@ def release(
The tool will ask you to confirm each added coin.
"""
latest_releases = coin_info.latest_releases()
# Transforming the user release input into a dict and validating
user_releases_dict = {
key: val for key, val in (release.split("=") for release in releases)
}
for key in user_releases_dict:
if key not in coin_info.VERSIONED_SUPPORT_INFO:
raise click.ClickException(
f"Unknown device: {key} - allowed are: {coin_info.VERSIONED_SUPPORT_INFO}"
)
def bump_version(version_tuple):
def bump_version(version_tuple: tuple[int]) -> str:
version_list = list(version_tuple)
version_list[-1] += 1
return ".".join(str(n) for n in version_list)
# guess `version` if not given
if not v1:
v1 = bump_version(latest_releases["trezor1"])
if not v2:
v2 = bump_version(latest_releases["trezor2"])
latest_releases = coin_info.latest_releases()
versions = {"trezor1": v1, "trezor2": v2}
# Take version either from user or guess it from latest releases info
device_release_version: dict[str, str] = {}
for device in coin_info.VERSIONED_SUPPORT_INFO:
if device in user_releases_dict:
device_release_version[device] = user_releases_dict[device]
else:
device_release_version[device] = bump_version(latest_releases[device])
for number in "1", "2":
device = f"trezor{number}"
version = versions[device]
if not force and not version.startswith(number + "."):
for device, version in device_release_version.items():
version_starting_num = device[1] # "T1B1" -> "1", "T2B1" -> "2"
if not force and not version.startswith(version_starting_num + "."):
raise click.ClickException(
f"Device trezor{device} should not be version {version}. "
f"Device {device} should not be version {version}. "
"Use --force to proceed anyway."
)
@ -295,7 +304,7 @@ def release(
if not unsupport_reason:
return
for device, version in versions.items():
for device, version in device_release_version.items():
if add:
support_setdefault(device, coin["key"], version)
else:
@ -311,7 +320,7 @@ def release(
for coin in missing_list:
if skip_testnets and coin["is_testnet"]:
for device, version in versions.items():
for device, version in device_release_version.items():
support_setdefault(device, coin["key"], False, "(AUTO) exclude testnet")
else:
maybe_add(coin)
@ -346,13 +355,13 @@ def set_support_value(key, entries, reason):
"""Set a support info variable.
Examples:
support.py set coin:BTC trezor1=1.10.5 trezor2=2.4.7 suite=yes connect=no
support.py set coin:LTC trezor1=yes connect=
support.py set coin:BTC T1B1=1.10.5 T2T1=2.4.7 suite=yes connect=no
support.py set coin:LTC T1B1=yes connect=
Setting a variable to "yes", "true" or "1" sets support to true.
Setting a variable to "no", "false" or "0" sets support to false.
(or null, in case of trezor1/2)
Setting variable to empty ("trezor1=") will set to null, or clear the entry.
(or null, in case of T1B1/T2T1)
Setting variable to empty ("T1B1=") will set to null, or clear the entry.
Setting a variable to a particular version string (e.g., "2.4.7") will set that
particular version.
"""

@ -0,0 +1 @@
Changed design of the path warning screen (model T only).

@ -0,0 +1 @@
Introduce multiple account warning to BTC send flow.

@ -0,0 +1 @@
Introduce multisig warning to BTC receive flow.

@ -0,0 +1 @@
QR code display when exporting XPUBs.

@ -0,0 +1,2 @@
Added firmware update without interaction.
Split builds of different parts to use simple util.s assembler, while FW+bootloader use interconnected ones.

@ -0,0 +1 @@
Fix more info button on shamir recovery screen.

@ -0,0 +1 @@
Add support for address chunkification in Receive and Sign flow.

@ -0,0 +1 @@
Implement device authentication for Model R.

@ -0,0 +1 @@
Use Optiga as a source of randomness in seed generation for Model R.

@ -141,7 +141,7 @@ test_emu_ui_record: ## record and hash screens for ui integration tests
test_emu_ui_record_multicore: ## quickly record all screens
make test_emu_ui_multicore || echo "All errors are recorded in fixtures.json"
../tests/update_fixtures.py -r
../tests/update_fixtures.py local -r
pylint: ## run pylint on application sources and tests
pylint -E $(shell find src tests -name *.py)

@ -22,7 +22,7 @@ FEATURES_WANTED = ["sd_card"]
CCFLAGS_MOD = ''
CPPPATH_MOD = []
CPPDEFINES_MOD = []
CPPDEFINES_MOD = ["BOARDLOADER"]
SOURCE_MOD = []
CPPDEFINES_HAL = []
SOURCE_HAL = []
@ -71,7 +71,7 @@ SOURCE_BOARDLOADER = [
'embed/boardloader/main.c',
]
env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s' % (ARGUMENTS.get('CFLAGS', ''), ARGUMENTS.get('PRODUCTION', '0')))
env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s' % (ARGUMENTS.get('CFLAGS', ''), ARGUMENTS.get('PRODUCTION', '0')), CONSTRAINTS=["limited_util_s"])
FEATURES_AVAILABLE = tools.configure_board(TREZOR_MODEL, FEATURES_WANTED, env, CPPDEFINES_HAL, SOURCE_HAL, PATH_HAL)
@ -95,7 +95,7 @@ env.Replace(
CCFLAGS='$COPT '
'-g3 '
'-nostdlib '
'-std=gnu99 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-std=gnu11 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-fsingle-precision-constant -fdata-sections -ffunction-sections '
'-ffreestanding '
'-fstack-protector-all '

@ -140,7 +140,7 @@ env.Replace(
CCFLAGS='$COPT '
'-g3 '
'-nostdlib '
'-std=gnu99 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-std=gnu11 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-fsingle-precision-constant -fdata-sections -ffunction-sections '
'-ffreestanding '
'-fstack-protector-all '

@ -133,7 +133,7 @@ env.Replace(
CCFLAGS='$COPT '
'-g3 '
'-nostdlib '
'-std=gnu99 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-std=gnu11 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-fsingle-precision-constant -fdata-sections -ffunction-sections '
'-ffreestanding '
'-fstack-protector-all '

@ -128,9 +128,16 @@ SOURCE_TREZORHAL = [
'embed/trezorhal/unix/rng.c',
'embed/trezorhal/unix/usb.c',
'embed/trezorhal/unix/random_delays.c',
'embed/trezorhal/unix/secret.c',
]
if TREZOR_MODEL in ('R', ):
CPPDEFINES_MOD += [
('USE_OPTIGA', '1'),
]
SOURCE_TREZORHAL += [
'embed/trezorhal/unix/secret.c',
]
SOURCE_UNIX = [
'embed/unix/profile.c',
]
@ -176,7 +183,7 @@ env.Replace(
CCFLAGS='$COPT '
'-g3 '
'-nostdlib '
'-std=gnu99 -Wall -Werror -Wpointer-arith -Wno-missing-braces -fno-common '
'-std=gnu11 -Wall -Werror -Wpointer-arith -Wno-missing-braces -fno-common '
'-fsingle-precision-constant -fdata-sections -ffunction-sections '
'-ffreestanding '
'-fstack-protector-all '

@ -70,8 +70,8 @@ CPPDEFINES_MOD += [
('USE_ETHEREUM', '1' if EVERYTHING else '0'),
('USE_MONERO', '1' if EVERYTHING else '0'),
('USE_CARDANO', '1' if EVERYTHING else '0'),
('USE_NEM', '1' if EVERYTHING else '0'),
('USE_EOS', '1' if EVERYTHING else '0'),
('USE_NEM', '1' if (EVERYTHING and TREZOR_MODEL != "R") else '0'),
('USE_EOS', '1' if (EVERYTHING and TREZOR_MODEL != "R") else '0'),
]
SOURCE_MOD += [
'embed/extmod/trezorobj.c',
@ -80,6 +80,7 @@ SOURCE_MOD += [
'embed/extmod/modtrezorcrypto/rand.c',
'vendor/trezor-crypto/address.c',
'vendor/trezor-crypto/aes/aes_modes.c',
'vendor/trezor-crypto/aes/aesccm.c',
'vendor/trezor-crypto/aes/aescrypt.c',
'vendor/trezor-crypto/aes/aeskey.c',
'vendor/trezor-crypto/aes/aestab.c',
@ -127,6 +128,7 @@ SOURCE_MOD += [
'vendor/trezor-crypto/shamir.c',
'vendor/trezor-crypto/slip39.c',
'vendor/trezor-crypto/slip39_english.c',
'vendor/trezor-crypto/tls_prf.c',
]
if EVERYTHING:
SOURCE_MOD += [
@ -406,7 +408,7 @@ env.Replace(
CCFLAGS='$COPT '
'-g3 '
'-nostdlib '
'-std=gnu99 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-std=gnu11 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-fsingle-precision-constant -fdata-sections -ffunction-sections '
'-ffreestanding '
'-fstack-protector-all '
@ -599,9 +601,10 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/cardano/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Cardano*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/eos/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/eos/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Eos*.py'))
if TREZOR_MODEL != "R":
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/eos/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/eos/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Eos*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ethereum/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Ethereum*.py'))
@ -612,9 +615,10 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/DebugMonero*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Monero*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/NEM*.py'))
if TREZOR_MODEL != "R":
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/NEM*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ripple/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Ripple*.py'))
@ -630,7 +634,8 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/webauthn/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py'))
if TREZOR_MODEL != "R":
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/bitcoinlike.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Zcash*.py'))
@ -639,7 +644,8 @@ if FROZEN:
source=SOURCE_PY,
source_dir=SOURCE_PY_DIR,
bitcoin_only=BITCOIN_ONLY,
backlight='backlight' in FEATURES_AVAILABLE
backlight='backlight' in FEATURES_AVAILABLE,
optiga='optiga' in FEATURES_AVAILABLE
)
source_mpyc = env.FrozenCFile(
@ -744,12 +750,12 @@ cmake_gen = env.Command(
MODEL_IDENTIFIER = tools.get_model_identifier(TREZOR_MODEL)
BOOTLOADER_SUFFIX = MODEL_IDENTIFIER
if BOOTLOADER_QA:
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_qa_DO_NOT_SIGN_signed_dev.bin'
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_dev_DO_NOT_SIGN_signed_dev.bin'
BOOTLOADER_SUFFIX = MODEL_IDENTIFIER + '_qa'
elif PRODUCTION:
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_satoshilabs_signed_prod.bin'
elif BOOTLOADER_DEVEL:
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_unsafe_signed_dev.bin'
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_dev_DO_NOT_SIGN_signed_dev.bin'
else:
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_unsafe_signed_prod.bin'

@ -24,6 +24,7 @@ FEATURES_WANTED = ["input", "sbu", "sd_card", "rdb_led", "usb", "consumption_mas
CCFLAGS_MOD = ''
CPPPATH_MOD = []
CPPDEFINES_MOD = [
'AES_128',
'USE_INSECURE_PRNG',
]
SOURCE_MOD = []
@ -50,11 +51,24 @@ CPPPATH_MOD += [
'vendor/trezor-storage',
]
SOURCE_MOD += [
'vendor/trezor-crypto/aes/aes_modes.c',
'vendor/trezor-crypto/aes/aesccm.c',
'vendor/trezor-crypto/aes/aescrypt.c',
'vendor/trezor-crypto/aes/aeskey.c',
'vendor/trezor-crypto/aes/aestab.c',
'vendor/trezor-crypto/bignum.c',
'vendor/trezor-crypto/chacha_drbg.c',
'vendor/trezor-crypto/chacha20poly1305/chacha_merged.c',
'vendor/trezor-crypto/ecdsa.c',
'vendor/trezor-crypto/hmac.c',
'vendor/trezor-crypto/hmac_drbg.c',
'vendor/trezor-crypto/memzero.c',
'vendor/trezor-crypto/nist256p1.c',
'vendor/trezor-crypto/rand.c',
'vendor/trezor-crypto/rfc6979.c',
'vendor/trezor-crypto/secp256k1.c',
'vendor/trezor-crypto/sha2.c',
'vendor/trezor-crypto/tls_prf.c',
]
# modtrezorui
@ -80,8 +94,14 @@ SOURCE_PRODTEST = [
'embed/prodtest/startup.s',
'embed/prodtest/header.S',
'embed/prodtest/main.c',
'embed/prodtest/prodtest_common.c',
]
if TREZOR_MODEL in ('R',):
SOURCE_PRODTEST += [
'embed/prodtest/optiga_prodtest.c',
]
# fonts
tools.add_font('NORMAL', FONT_NORMAL, CPPDEFINES_MOD, SOURCE_MOD)
tools.add_font('BOLD', FONT_BOLD, CPPDEFINES_MOD, SOURCE_MOD)
@ -89,7 +109,7 @@ tools.add_font('DEMIBOLD', FONT_DEMIBOLD, CPPDEFINES_MOD, SOURCE_MOD)
tools.add_font('MONO', FONT_MONO, CPPDEFINES_MOD, SOURCE_MOD)
tools.add_font('BIG', FONT_BIG, CPPDEFINES_MOD, SOURCE_MOD)
env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s' % (ARGUMENTS.get('CFLAGS', ''), ARGUMENTS.get('PRODUCTION', '0')))
env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s' % (ARGUMENTS.get('CFLAGS', ''), ARGUMENTS.get('PRODUCTION', '0')), CONSTRAINTS=["limited_util_s"])
FEATURES_AVAILABLE = tools.configure_board(TREZOR_MODEL, FEATURES_WANTED, env, CPPDEFINES_HAL, SOURCE_HAL, PATH_HAL)
@ -113,7 +133,7 @@ env.Replace(
CCFLAGS='$COPT '
'-g3 '
'-nostdlib '
'-std=gnu99 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-std=gnu11 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-fsingle-precision-constant -fdata-sections -ffunction-sections '
'-ffreestanding '
'-fstack-protector-all '
@ -167,7 +187,7 @@ MODEL_IDENTIFIER = tools.get_model_identifier(TREZOR_MODEL)
if PRODUCTION:
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_prodtest_signed_prod.bin'
elif BOOTLOADER_DEVEL:
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_unsafe_signed_dev.bin'
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_dev_DO_NOT_SIGN_signed_dev.bin'
else:
VENDORHEADER = f'embed/vendorheader/{MODEL_IDENTIFIER}/vendorheader_unsafe_signed_prod.bin'

@ -79,7 +79,7 @@ tools.add_font('DEMIBOLD', FONT_DEMIBOLD, CPPDEFINES_MOD, SOURCE_MOD)
tools.add_font('MONO', FONT_MONO, CPPDEFINES_MOD, SOURCE_MOD)
tools.add_font('BIG', FONT_BIG, CPPDEFINES_MOD, SOURCE_MOD)
env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s' % (ARGUMENTS.get('CFLAGS', ''), ARGUMENTS.get('PRODUCTION', '0')))
env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s' % (ARGUMENTS.get('CFLAGS', ''), ARGUMENTS.get('PRODUCTION', '0')), CONSTRAINTS=["limited_util_s"])
FEATURES_AVAILABLE = tools.configure_board(TREZOR_MODEL, FEATURES_WANTED, env, CPPDEFINES_HAL, SOURCE_HAL, PATH_HAL)
@ -103,7 +103,7 @@ env.Replace(
CCFLAGS='$COPT '
'-g3 '
'-nostdlib '
'-std=gnu99 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-std=gnu11 -Wall -Werror -Wdouble-promotion -Wpointer-arith -Wno-missing-braces -fno-common '
'-fsingle-precision-constant -fdata-sections -ffunction-sections '
'-ffreestanding '
'-fstack-protector-all '

@ -75,8 +75,8 @@ CPPDEFINES_MOD += [
('USE_ETHEREUM', '1' if EVERYTHING else '0'),
('USE_MONERO', '1' if EVERYTHING else '0'),
('USE_CARDANO', '1' if EVERYTHING else '0'),
('USE_NEM', '1' if EVERYTHING else '0'),
('USE_EOS', '1' if EVERYTHING else '0'),
('USE_NEM', '1' if (EVERYTHING and TREZOR_MODEL != "R") else '0'),
('USE_EOS', '1' if (EVERYTHING and TREZOR_MODEL != "R") else '0'),
]
SOURCE_MOD += [
'embed/extmod/trezorobj.c',
@ -390,6 +390,14 @@ if TREZOR_MODEL in ('T', 'R'):
'embed/trezorhal/unix/sdcard.c',
]
if TREZOR_MODEL == 'R':
CPPDEFINES_MOD += [
('USE_OPTIGA', '1'),
]
SOURCE_UNIX += [
'embed/trezorhal/unix/optiga.c',
]
if DMA2D:
CPPDEFINES_MOD += [
'USE_DMA2D',
@ -476,7 +484,7 @@ else:
env.Replace(
CCFLAGS='$COPT '
'-g3 '
'-std=gnu99 -Wall -Werror -Wuninitialized -Wno-missing-braces '
'-std=gnu11 -Wall -Werror -Wuninitialized -Wno-missing-braces '
'-fdata-sections -ffunction-sections -fPIE ' + CCFLAGS_MOD,
CCFLAGS_QSTR='-DNO_QSTR -DN_X64 -DN_X86 -DN_THUMB',
LIBS=['m'],
@ -653,7 +661,9 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*.py',
exclude=[
SOURCE_PY_DIR + 'apps/management/sd_protect.py',
] if TREZOR_MODEL not in ('T',) else [])
] if TREZOR_MODEL not in ('T',) else [] + [
SOURCE_PY_DIR + 'apps/management/authenticate_device.py',
] if TREZOR_MODEL not in ('R',) else [])
)
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/management/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/misc/*.py'))
@ -674,9 +684,10 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/cardano/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Cardano*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/eos/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/eos/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Eos*.py'))
if TREZOR_MODEL != "R":
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/eos/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/eos/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Eos*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ethereum/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Ethereum*.py'))
@ -687,9 +698,10 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/DebugMonero*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Monero*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/NEM*.py'))
if TREZOR_MODEL != "R":
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/NEM*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ripple/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Ripple*.py'))
@ -705,7 +717,8 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/webauthn/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py'))
if TREZOR_MODEL != "R":
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/bitcoinlike.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Zcash*.py'))
@ -714,7 +727,8 @@ if FROZEN:
source=SOURCE_PY,
source_dir=SOURCE_PY_DIR,
bitcoin_only=BITCOIN_ONLY,
backlight=TREZOR_MODEL in ('T',)
backlight=TREZOR_MODEL in ('T',),
optiga=TREZOR_MODEL in ('R',)
)
source_mpyc = env.FrozenCFile(

@ -1 +0,0 @@
Fixed gamma correction settings for Model T

@ -1 +0,0 @@
Added support for STM32F429I-DISC1 board

@ -0,0 +1,2 @@
Added firmware update without interaction.
Split builds of different parts to use simple util.s assembler, while FW+bootloader use interconnected ones.

@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.1.1 [September 2023]
### Added
- Added support for STM32F429I-DISC1 board [#2989]
### Fixed
- Fixed gamma correction settings for Model T [#2955]
- Removed unwanted delay when resetting LCD on the Model R. [#3222]
## 2.1.0 [June 2023]
Internal only release for Model R prototypes.
@ -30,4 +40,7 @@ Internal only release for Model R prototypes.
[#2587]: https://github.com/trezor/trezor-firmware/pull/2587
[#2595]: https://github.com/trezor/trezor-firmware/pull/2595
[#2623]: https://github.com/trezor/trezor-firmware/pull/2623
[#2955]: https://github.com/trezor/trezor-firmware/pull/2955
[#2989]: https://github.com/trezor/trezor-firmware/pull/2989
[#3048]: https://github.com/trezor/trezor-firmware/pull/3048
[#3222]: https://github.com/trezor/trezor-firmware/pull/3222

@ -1,4 +1,4 @@
#define VERSION_MAJOR 2
#define VERSION_MINOR 1
#define VERSION_PATCH 0
#define VERSION_PATCH 2
#define VERSION_BUILD 0

@ -0,0 +1,2 @@
Added firmware update without interaction.
Split builds of different parts to use simple util.s assembler, while FW+bootloader use interconnected ones.

@ -7,6 +7,9 @@
#include "flash.h"
#include "model.h"
#include "rust_ui.h"
#ifdef USE_OPTIGA
#include "secret.h"
#endif
#include "emulator.h"
@ -40,9 +43,17 @@ __attribute__((noreturn)) int main(int argc, char **argv) {
(void)ret;
}
if (argc == 2 && argv[1][0] == 's') {
// Run the firmware
stay_in_bootloader_flag = STAY_IN_BOOTLOADER_FLAG;
if (argc == 2) {
if (argv[1][0] == 's') {
// Run the firmware
stay_in_bootloader_flag = STAY_IN_BOOTLOADER_FLAG;
}
#ifdef USE_OPTIGA
else if (argv[1][0] == 'l') {
// write bootloader-lock secret
secret_write_header();
}
#endif
} else if (argc == 4) {
display_init();
display_backlight(180);

@ -23,6 +23,9 @@ ccmram_end = ORIGIN(CCMRAM) + LENGTH(CCMRAM);
sram_start = ORIGIN(SRAM);
sram_end = ORIGIN(SRAM) + LENGTH(SRAM);
/* IMAGE_HEADER_SIZE is 0x400, this is for interaction-less firmware update start */
firmware_header_start = ccmram_end - 0x400;
_codelen = SIZEOF(.flash) + SIZEOF(.data);
SECTIONS {

@ -44,6 +44,10 @@
#include "emulator.h"
#endif
#if USE_OPTIGA
#include "secret.h"
#endif
#define MSG_HEADER1_LEN 9
#define MSG_HEADER2_LEN 1
@ -311,6 +315,11 @@ static void send_msg_features(uint8_t iface_num,
MSG_SEND_ASSIGN_VALUE(unit_color, unit_variant_get_color());
MSG_SEND_ASSIGN_VALUE(unit_btconly, unit_variant_get_btconly());
}
#if USE_OPTIGA
MSG_SEND_ASSIGN_VALUE(bootloader_locked,
(secret_bootloader_locked() == sectrue));
#endif
MSG_SEND(Features);
}

@ -103,6 +103,8 @@ typedef struct _Features {
uint32_t unit_color;
bool has_unit_btconly;
bool unit_btconly;
bool has_bootloader_locked;
bool bootloader_locked;
} Features;
typedef struct _FirmwareErase {
@ -154,7 +156,7 @@ extern "C" {
/* Initializer values for message structs */
#define Initialize_init_default {0}
#define GetFeatures_init_default {0}
#define Features_init_default {false, "", 0, 0, 0, false, 0, false, "", false, "", false, "", false, 0, false, {0, {0}}, false, 0, false, "", false, 0, false, 0, false, 0, false, "", false, "", false, 0, false, 0}
#define Features_init_default {false, "", 0, 0, 0, false, 0, false, "", false, "", false, "", false, 0, false, {0, {0}}, false, 0, false, "", false, 0, false, 0, false, 0, false, "", false, "", false, 0, false, 0, false, 0}
#define Ping_init_default {false, ""}
#define Success_init_default {false, ""}
#define Failure_init_default {false, _FailureType_MIN, false, ""}
@ -166,7 +168,7 @@ extern "C" {
#define UnlockBootloader_init_default {0}
#define Initialize_init_zero {0}
#define GetFeatures_init_zero {0}
#define Features_init_zero {false, "", 0, 0, 0, false, 0, false, "", false, "", false, "", false, 0, false, {0, {0}}, false, 0, false, "", false, 0, false, 0, false, 0, false, "", false, "", false, 0, false, 0}
#define Features_init_zero {false, "", 0, 0, 0, false, 0, false, "", false, "", false, "", false, 0, false, {0, {0}}, false, 0, false, "", false, 0, false, 0, false, 0, false, "", false, "", false, 0, false, 0, false, 0}
#define Ping_init_zero {false, ""}
#define Success_init_zero {false, ""}
#define Failure_init_zero {false, _FailureType_MIN, false, ""}
@ -200,6 +202,7 @@ extern "C" {
#define Features_internal_model_tag 44
#define Features_unit_color_tag 45
#define Features_unit_btconly_tag 46
#define Features_bootloader_locked_tag 49
#define FirmwareErase_length_tag 1
#define FirmwareRequest_offset_tag 1
#define FirmwareRequest_length_tag 2
@ -238,7 +241,8 @@ X(a, STATIC, OPTIONAL, UINT32, fw_patch, 24) \
X(a, STATIC, OPTIONAL, STRING, fw_vendor, 25) \
X(a, STATIC, OPTIONAL, STRING, internal_model, 44) \
X(a, STATIC, OPTIONAL, UINT32, unit_color, 45) \
X(a, STATIC, OPTIONAL, BOOL, unit_btconly, 46)
X(a, STATIC, OPTIONAL, BOOL, unit_btconly, 46) \
X(a, STATIC, OPTIONAL, BOOL, bootloader_locked, 49)
#define Features_CALLBACK NULL
#define Features_DEFAULT NULL
@ -322,7 +326,7 @@ extern const pb_msgdesc_t UnlockBootloader_msg;
#define ButtonAck_size 0
#define ButtonRequest_size 2
#define Failure_size 260
#define Features_size 487
#define Features_size 490
#define FirmwareErase_size 6
#define FirmwareRequest_size 12
#define GetFeatures_size 0

@ -61,6 +61,7 @@ message Features {
optional string internal_model = 44; // internal model name
optional uint32 unit_color = 45; // color of the unit/device
optional bool unit_btconly = 46; // unit/device is intended as bitcoin only
optional bool bootloader_locked = 49; // bootloader is locked
}
/**

@ -7,7 +7,7 @@
reset_handler:
// setup environment for subsequent stage of code
ldr r0, =ccmram_start // r0 - point to beginning of CCMRAM
ldr r1, =ccmram_end // r1 - point to byte after the end of CCMRAM
ldr r1, =firmware_header_start // r1 - point to byte where firmware image header might start
ldr r2, =0 // r2 - the word-sized value to be written
bl memset_reg

@ -0,0 +1,2 @@
Added firmware update without interaction.
Split builds of different parts to use simple util.s assembler, while FW+bootloader use interconnected ones.

@ -23,6 +23,9 @@ ccmram_end = ORIGIN(CCMRAM) + LENGTH(CCMRAM);
sram_start = ORIGIN(SRAM);
sram_end = ORIGIN(SRAM) + LENGTH(SRAM);
/* IMAGE_HEADER_SIZE is 0x400, this is for interaction-less firmware update start */
firmware_header_start = ccmram_end - 0x400;
_codelen = SIZEOF(.flash) + SIZEOF(.data);
SECTIONS {

@ -7,7 +7,7 @@
reset_handler:
// setup environment for subsequent stage of code
ldr r0, =ccmram_start // r0 - point to beginning of CCMRAM
ldr r1, =ccmram_end // r1 - point to byte after the end of CCMRAM
ldr r1, =firmware_header_start // r1 - point to byte where firmware header starts
ldr r2, =0 // r2 - the word-sized value to be written
bl memset_reg

@ -26,15 +26,6 @@
#define NORCOW_HEADER_LEN 0
#define NORCOW_SECTOR_COUNT 2
#if defined TREZOR_MODEL_T || defined TREZOR_MODEL_R || \
defined TREZOR_MODEL_DISC1
#define NORCOW_SECTOR_SIZE (64 * 1024)
#elif defined TREZOR_MODEL_1
#define NORCOW_SECTOR_SIZE (16 * 1024)
#else
#error Unknown Trezor model
#endif
/*
* Current storage version.
*/

@ -358,6 +358,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_HDNode_address_obj,
#if !BITCOIN_ONLY
#if USE_NEM
/// def nem_address(self, network: int) -> str:
/// """
/// Compute a NEM address string from the HD node.
@ -425,6 +426,8 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
mod_trezorcrypto_HDNode_nem_encrypt_obj, 5, 5,
mod_trezorcrypto_HDNode_nem_encrypt);
#endif
/// def ethereum_pubkeyhash(self) -> bytes:
/// """
/// Compute an Ethereum pubkeyhash (aka address) from the HD node.
@ -484,10 +487,12 @@ STATIC const mp_rom_map_elem_t mod_trezorcrypto_HDNode_locals_dict_table[] = {
{MP_ROM_QSTR(MP_QSTR_address),
MP_ROM_PTR(&mod_trezorcrypto_HDNode_address_obj)},
#if !BITCOIN_ONLY
#if USE_NEM
{MP_ROM_QSTR(MP_QSTR_nem_address),
MP_ROM_PTR(&mod_trezorcrypto_HDNode_nem_address_obj)},
{MP_ROM_QSTR(MP_QSTR_nem_encrypt),
MP_ROM_PTR(&mod_trezorcrypto_HDNode_nem_encrypt_obj)},
#endif
{MP_ROM_QSTR(MP_QSTR_ethereum_pubkeyhash),
MP_ROM_PTR(&mod_trezorcrypto_HDNode_ethereum_pubkeyhash_obj)},
#endif

@ -0,0 +1,134 @@
/*
* This file is part of the Trezor project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if USE_OPTIGA
#include "py/objstr.h"
#include "optiga.h"
#include "optiga_commands.h"
/// package: trezorcrypto.optiga
#define MAX_DER_SIGNATURE_SIZE 72
/// class OptigaError(Exception):
/// """Error returned by the Optiga chip."""
MP_DEFINE_EXCEPTION(OptigaError, Exception)
/// class SigningInaccessible(OptigaError):
/// """The signing key is inaccessible.
/// Typically, this will happen after the bootloader has been unlocked.
/// """
MP_DEFINE_EXCEPTION(SigningInaccessible, OptigaError)
/// mock:global
/// def get_certificate(cert_index: int) -> bytes:
/// """
/// Return the certificate stored at the given index.
/// """
STATIC mp_obj_t mod_trezorcrypto_optiga_get_certificate(mp_obj_t cert_index) {
mp_int_t idx = mp_obj_get_int(cert_index);
if (idx < 0 || idx >= OPTIGA_CERT_COUNT) {
mp_raise_ValueError("Invalid index.");
}
size_t cert_size = 0;
if (!optiga_cert_size(idx, &cert_size)) {
mp_raise_msg(&mp_type_OptigaError, "Failed to get certificate size.");
}
vstr_t cert = {0};
vstr_init_len(&cert, cert_size);
if (!optiga_read_cert(idx, (uint8_t *)cert.buf, cert.alloc, &cert_size)) {
vstr_clear(&cert);
mp_raise_msg(&mp_type_OptigaError, "Failed to read certificate.");
}
cert.len = cert_size;
return mp_obj_new_str_from_vstr(&mp_type_bytes, &cert);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_optiga_get_certificate_obj,
mod_trezorcrypto_optiga_get_certificate);
/// def sign(
/// key_index: int,
/// digest: bytes,
/// ) -> bytes:
/// """
/// Uses the private key at key_index to produce a DER-encoded signature of
/// the digest.
/// """
STATIC mp_obj_t mod_trezorcrypto_optiga_sign(mp_obj_t key_index,
mp_obj_t digest) {
mp_int_t idx = mp_obj_get_int(key_index);
if (idx < 0 || idx >= OPTIGA_ECC_KEY_COUNT) {
mp_raise_ValueError("Invalid index.");
}
mp_buffer_info_t dig = {0};
mp_get_buffer_raise(digest, &dig, MP_BUFFER_READ);
if (dig.len != 32) {
mp_raise_ValueError("Invalid length of digest.");
}
vstr_t sig = {0};
vstr_init_len(&sig, MAX_DER_SIGNATURE_SIZE);
size_t sig_size = 0;
int ret = optiga_sign(idx, (const uint8_t *)dig.buf, dig.len,
((uint8_t *)sig.buf), sig.alloc, &sig_size);
if (ret != 0) {
vstr_clear(&sig);
if (ret == OPTIGA_ERR_ACCESS_COND_NOT_SAT) {
mp_raise_msg(&mp_type_SigningInaccessible, "Signing inaccessible.");
} else {
mp_raise_msg(&mp_type_OptigaError, "Signing failed.");
}
}
sig.len = sig_size;
return mp_obj_new_str_from_vstr(&mp_type_bytes, &sig);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_optiga_sign_obj,
mod_trezorcrypto_optiga_sign);
/// DEVICE_CERT_INDEX: int
/// DEVICE_ECC_KEY_INDEX: int
STATIC const mp_rom_map_elem_t mod_trezorcrypto_optiga_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_optiga)},
{MP_ROM_QSTR(MP_QSTR_get_certificate),
MP_ROM_PTR(&mod_trezorcrypto_optiga_get_certificate_obj)},
{MP_ROM_QSTR(MP_QSTR_sign), MP_ROM_PTR(&mod_trezorcrypto_optiga_sign_obj)},
{MP_ROM_QSTR(MP_QSTR_DEVICE_CERT_INDEX),
MP_ROM_INT(OPTIGA_DEVICE_CERT_INDEX)},
{MP_ROM_QSTR(MP_QSTR_DEVICE_ECC_KEY_INDEX),
MP_ROM_INT(OPTIGA_DEVICE_ECC_KEY_INDEX)},
{MP_ROM_QSTR(MP_QSTR_OptigaError), MP_ROM_PTR(&mp_type_OptigaError)},
{MP_ROM_QSTR(MP_QSTR_SigningInaccessible),
MP_ROM_PTR(&mp_type_SigningInaccessible)}};
STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_optiga_globals,
mod_trezorcrypto_optiga_globals_table);
STATIC const mp_obj_module_t mod_trezorcrypto_optiga_module = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t *)&mod_trezorcrypto_optiga_globals,
};
#endif

@ -23,6 +23,10 @@
#include "rand.h"
#if USE_OPTIGA
#include "optiga.h"
#endif
/// package: trezorcrypto.random
/// def uniform(n: int) -> int:
@ -40,22 +44,52 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_random_uniform_obj,
mod_trezorcrypto_random_uniform);
/// import builtins
/// def bytes(len: int) -> builtins.bytes:
/// def bytes(len: int, strong: bool = False) -> builtins.bytes:
/// """
/// Generate random bytes sequence of length len.
/// Generate random bytes sequence of length len. If `strong` is set then
/// maximum sources of entropy are used.
/// """
STATIC mp_obj_t mod_trezorcrypto_random_bytes(mp_obj_t len) {
uint32_t l = trezor_obj_get_uint(len);
if (l > 1024) {
STATIC mp_obj_t mod_trezorcrypto_random_bytes(size_t n_args,
const mp_obj_t *args) {
uint32_t len = trezor_obj_get_uint(args[0]);
if (len > 1024) {
mp_raise_ValueError("Maximum requested size is 1024");
}
vstr_t vstr = {0};
vstr_init_len(&vstr, l);
random_buffer((uint8_t *)vstr.buf, l);
vstr_init_len(&vstr, len);
#if USE_OPTIGA
if (n_args > 1 && mp_obj_is_true(args[1])) {
uint8_t *dest = (uint8_t *)vstr.buf;
if (!optiga_random_buffer(dest, len)) {
vstr_clear(&vstr);
mp_raise_msg(&mp_type_RuntimeError,
"Failed to get randomness from Optiga.");
}
uint8_t buffer[4] = {0};
while (len > sizeof(buffer)) {
random_buffer(buffer, sizeof(buffer));
for (int i = 0; i < sizeof(buffer); ++i) {
*dest ^= buffer[i];
++dest;
}
len -= sizeof(buffer);
}
random_buffer(buffer, len);
for (int i = 0; i < len; ++i) {
*dest ^= buffer[i];
++dest;
}
} else
#endif
{
random_buffer((uint8_t *)vstr.buf, len);
}
return mp_obj_new_str_from_vstr(&mp_type_bytes, &vstr);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_random_bytes_obj,
mod_trezorcrypto_random_bytes);
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_random_bytes_obj, 1,
2, mod_trezorcrypto_random_bytes);
/// def shuffle(data: list) -> None:
/// """

@ -64,6 +64,9 @@ static void wrapped_ui_wait_callback(uint32_t current, uint32_t total) {
#include "modtrezorcrypto-sha512.h"
#include "modtrezorcrypto-shamir.h"
#include "modtrezorcrypto-slip39.h"
#ifdef USE_OPTIGA
#include "modtrezorcrypto-optiga.h"
#endif
#if !BITCOIN_ONLY
#include "modtrezorcrypto-cardano.h"
#include "modtrezorcrypto-monero.h"
@ -120,6 +123,9 @@ STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = {
MP_ROM_PTR(&mod_trezorcrypto_Sha3_512_type)},
{MP_ROM_QSTR(MP_QSTR_shamir), MP_ROM_PTR(&mod_trezorcrypto_shamir_module)},
{MP_ROM_QSTR(MP_QSTR_slip39), MP_ROM_PTR(&mod_trezorcrypto_slip39_module)},
#if USE_OPTIGA
{MP_ROM_QSTR(MP_QSTR_optiga), MP_ROM_PTR(&mod_trezorcrypto_optiga_module)},
#endif
};
STATIC MP_DEFINE_CONST_DICT(mp_module_trezorcrypto_globals,
mp_module_trezorcrypto_globals_table);

@ -43,6 +43,10 @@
#include "image.h"
#endif
#if USE_OPTIGA && !defined(TREZOR_EMULATOR)
#include "secret.h"
#endif
static void ui_progress(mp_obj_t ui_wait_callback, uint32_t current,
uint32_t total) {
if (mp_obj_is_callable(ui_wait_callback)) {
@ -254,6 +258,26 @@ STATIC mp_obj_t mod_trezorutils_reboot_to_bootloader() {
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorutils_reboot_to_bootloader_obj,
mod_trezorutils_reboot_to_bootloader);
/// def bootloader_locked() -> bool | None:
/// """
/// Returns True/False if the the bootloader is locked/unlocked and None if
/// the feature is not supported.
/// """
STATIC mp_obj_t mod_trezorutils_bootloader_locked() {
#if USE_OPTIGA
#ifdef TREZOR_EMULATOR
return mp_const_true;
#else
return (secret_bootloader_locked() == sectrue) ? mp_const_true
: mp_const_false;
#endif
#else
return mp_const_none;
#endif
}
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorutils_bootloader_locked_obj,
mod_trezorutils_bootloader_locked);
STATIC mp_obj_str_t mod_trezorutils_revision_obj = {
{&mp_type_bytes}, 0, sizeof(SCM_REVISION) - 1, (const byte *)SCM_REVISION};
@ -266,6 +290,7 @@ STATIC mp_obj_str_t mod_trezorutils_model_name_obj = {
/// VERSION_PATCH: int
/// USE_SD_CARD: bool
/// USE_BACKLIGHT: bool
/// USE_OPTIGA: bool
/// MODEL: str
/// INTERNAL_MODEL: str
/// EMULATOR: bool
@ -282,6 +307,8 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
MP_ROM_PTR(&mod_trezorutils_firmware_vendor_obj)},
{MP_ROM_QSTR(MP_QSTR_reboot_to_bootloader),
MP_ROM_PTR(&mod_trezorutils_reboot_to_bootloader_obj)},
{MP_ROM_QSTR(MP_QSTR_bootloader_locked),
MP_ROM_PTR(&mod_trezorutils_bootloader_locked_obj)},
{MP_ROM_QSTR(MP_QSTR_unit_color),
MP_ROM_PTR(&mod_trezorutils_unit_color_obj)},
{MP_ROM_QSTR(MP_QSTR_unit_btconly),
@ -301,6 +328,11 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_USE_BACKLIGHT), mp_const_true},
#else
{MP_ROM_QSTR(MP_QSTR_USE_BACKLIGHT), mp_const_false},
#endif
#ifdef USE_OPTIGA
{MP_ROM_QSTR(MP_QSTR_USE_OPTIGA), mp_const_true},
#else
{MP_ROM_QSTR(MP_QSTR_USE_OPTIGA), mp_const_false},
#endif
{MP_ROM_QSTR(MP_QSTR_MODEL), MP_ROM_PTR(&mod_trezorutils_model_name_obj)},
{MP_ROM_QSTR(MP_QSTR_INTERNAL_MODEL),

@ -43,6 +43,7 @@
#include "display.h"
#include "flash.h"
#include "image.h"
#include "memzero.h"
#include "model.h"
#include "mpu.h"
#include "random_delays.h"
@ -71,7 +72,9 @@
#include "sdcard.h"
#endif
#ifdef USE_OPTIGA
#include "optiga_commands.h"
#include "optiga_transport.h"
#include "secret.h"
#endif
#include "unit_variant.h"
@ -113,6 +116,12 @@ int main(void) {
unit_variant_init();
#ifdef USE_OPTIGA
uint8_t secret[SECRET_OPTIGA_KEY_LEN] = {0};
secbool secret_ok =
secret_read(secret, SECRET_OPTIGA_KEY_OFFSET, SECRET_OPTIGA_KEY_LEN);
#endif
#if PRODUCTION || BOOTLOADER_QA
check_and_replace_bootloader();
#endif
@ -162,6 +171,11 @@ int main(void) {
#ifdef USE_OPTIGA
optiga_init();
optiga_open_application();
if (sectrue == secret_ok) {
optiga_sec_chan_handshake(secret, sizeof(secret));
}
memzero(secret, sizeof(secret));
#endif
#if !defined TREZOR_MODEL_1
@ -231,14 +245,20 @@ void BusFault_Handler(void) { error_shutdown("INTERNAL ERROR", "(BF)"); }
void UsageFault_Handler(void) { error_shutdown("INTERNAL ERROR", "(UF)"); }
__attribute__((noreturn)) void reboot_to_bootloader() {
mpu_config_bootloader();
jump_to_with_flag(BOOTLOADER_START + IMAGE_HEADER_SIZE,
STAY_IN_BOOTLOADER_FLAG);
for (;;)
;
}
void copy_image_header_for_bootloader(const uint8_t *image_header) {
memcpy(&firmware_header_start, image_header, IMAGE_HEADER_SIZE);
}
void SVC_C_Handler(uint32_t *stack) {
uint8_t svc_number = ((uint8_t *)stack[6])[-2];
bool clear_firmware_header = true;
switch (svc_number) {
case SVC_ENABLE_IRQ:
HAL_NVIC_EnableIRQ(stack[0]);
@ -259,9 +279,19 @@ void SVC_C_Handler(uint32_t *stack) {
for (;;)
;
break;
case SVC_REBOOT_COPY_IMAGE_HEADER:
copy_image_header_for_bootloader((uint8_t *)stack[0]);
clear_firmware_header = false;
// break is omitted here because we want to continue to reboot below
case SVC_REBOOT_TO_BOOTLOADER:
// if not going from copy image header & reboot, clean preventively this
// part of CCMRAM
if (clear_firmware_header) {
explicit_bzero(&firmware_header_start, IMAGE_HEADER_SIZE);
}
ensure_compatible_settings();
mpu_config_bootloader();
__asm__ volatile("msr control, %0" ::"r"(0x0));
__asm__ volatile("isb");
// See stack layout in

@ -22,6 +22,9 @@ data_size = SIZEOF(.data);
ccmram_start = ORIGIN(CCMRAM);
ccmram_end = ORIGIN(CCMRAM) + LENGTH(CCMRAM);
/* IMAGE_HEADER_SIZE is 0x400, this is for interaction-less firmware update start */
firmware_header_start = ccmram_end - 0x400;
/* used by the startup code to wipe memory */
sram_start = ORIGIN(SRAM);
sram_end = ORIGIN(SRAM) + LENGTH(SRAM);

@ -23,5 +23,6 @@
#define BOOTLOADER_IMAGE_MAXSIZE (128 * 1024 * 1) // 128 KB
#define FIRMWARE_IMAGE_MAXSIZE (128 * 1024 * 13) // 1664 KB
#define NORCOW_SECTOR_SIZE (64 * 1024)
#endif

@ -11,5 +11,6 @@
#define BOOTLOADER_IMAGE_MAXSIZE (32 * 1024 * 1) // 32 KB
#define FIRMWARE_IMAGE_MAXSIZE (64 * 1024 * 15) // 960 KB
#define NORCOW_SECTOR_SIZE (16 * 1024)
#endif

@ -23,5 +23,6 @@
#define BOOTLOADER_IMAGE_MAXSIZE (128 * 1024 * 1) // 128 KB
#define FIRMWARE_IMAGE_MAXSIZE (128 * 1024 * 13) // 1664 KB
#define NORCOW_SECTOR_SIZE (64 * 1024)
#endif

@ -23,5 +23,6 @@
#define BOOTLOADER_IMAGE_MAXSIZE (128 * 1024 * 1) // 128 KB
#define FIRMWARE_IMAGE_MAXSIZE (128 * 1024 * 13) // 1664 KB
#define NORCOW_SECTOR_SIZE (64 * 1024)
#endif

@ -28,17 +28,24 @@
#include "display.h"
#include "flash.h"
#include "i2c.h"
#include "mini_printf.h"
#include "model.h"
#include "mpu.h"
#include "prodtest_common.h"
#include "random_delays.h"
#include "rng.h"
#include "sbu.h"
#include "sdcard.h"
#include "secbool.h"
#include "touch.h"
#include "usb.h"
#ifdef USE_OPTIGA
#include "optiga_commands.h"
#include "optiga_prodtest.h"
#include "optiga_transport.h"
#endif
#include "memzero.h"
#include "stm32f4xx_ll_utils.h"
#ifdef TREZOR_MODEL_T
#define MODEL_IDENTIFIER "TREZOR2-"
@ -46,8 +53,6 @@
#define MODEL_IDENTIFIER "T2B1-"
#endif
enum { VCP_IFACE = 0x00 };
static secbool startswith(const char *s, const char *prefix) {
return sectrue * (0 == strncmp(s, prefix, strlen(prefix)));
}
@ -57,11 +62,6 @@ static void vcp_intr(void) {
ensure(secfalse, "vcp_intr");
}
static void vcp_puts(const char *s, size_t len) {
int r = usb_vcp_write_blocking(VCP_IFACE, (const uint8_t *)s, len, -1);
(void)r;
}
static char vcp_getchar(void) {
uint8_t c = 0;
int r = usb_vcp_read_blocking(VCP_IFACE, &c, 1, -1);
@ -91,16 +91,6 @@ static void vcp_readline(char *buf, size_t len) {
}
}
static void vcp_printf(const char *fmt, ...) {
static char buf[128];
va_list va;
va_start(va, fmt);
int r = mini_vsnprintf(buf, sizeof(buf), fmt, va);
va_end(va);
vcp_puts(buf, r);
vcp_puts("\r\n", 2);
}
static void usb_init_all(void) {
enum {
VCP_PACKET_LEN = 64,
@ -160,7 +150,7 @@ static void draw_border(int width, int padding) {
static void test_border(void) {
draw_border(2, 0);
vcp_printf("OK");
vcp_println("OK");
}
static void test_display(const char *colors) {
@ -185,10 +175,10 @@ static void test_display(const char *colors) {
c = 0xFFFF;
break;
}
display_bar(i * w, 0, i * w + w, 240, c);
display_bar(i * w, 0, i * w + w, DISPLAY_RESY, c);
}
display_refresh();
vcp_printf("OK");
vcp_println("OK");
}
#ifdef USE_BUTTON
@ -196,13 +186,13 @@ static void test_display(const char *colors) {
static secbool test_btn_press(uint32_t deadline, uint32_t btn) {
while (button_read() != (btn | BTN_EVT_DOWN)) {
if (HAL_GetTick() > deadline) {
vcp_printf("ERROR TIMEOUT");
vcp_println("ERROR TIMEOUT");
return secfalse;
}
}
while (button_read() != (btn | BTN_EVT_UP)) {
if (HAL_GetTick() > deadline) {
vcp_printf("ERROR TIMEOUT");
vcp_println("ERROR TIMEOUT");
return secfalse;
}
}
@ -231,7 +221,7 @@ static secbool test_btn_all(uint32_t deadline) {
break;
}
if (HAL_GetTick() > deadline) {
vcp_printf("ERROR TIMEOUT");
vcp_println("ERROR TIMEOUT");
return secfalse;
}
}
@ -254,7 +244,7 @@ static secbool test_btn_all(uint32_t deadline) {
break;
}
if (HAL_GetTick() > deadline) {
vcp_printf("ERROR TIMEOUT");
vcp_println("ERROR TIMEOUT");
return secfalse;
}
}
@ -268,21 +258,21 @@ static void test_button(const char *args) {
timeout = args[5] - '0';
uint32_t deadline = HAL_GetTick() + timeout * 1000;
secbool r = test_btn_press(deadline, BTN_LEFT);
if (r == sectrue) vcp_printf("OK");
if (r == sectrue) vcp_println("OK");
}
if (startswith(args, "RIGHT ")) {
timeout = args[6] - '0';
uint32_t deadline = HAL_GetTick() + timeout * 1000;
secbool r = test_btn_press(deadline, BTN_RIGHT);
if (r == sectrue) vcp_printf("OK");
if (r == sectrue) vcp_println("OK");
}
if (startswith(args, "BOTH ")) {
timeout = args[5] - '0';
uint32_t deadline = HAL_GetTick() + timeout * 1000;
secbool r = test_btn_all(deadline);
if (r == sectrue) vcp_printf("OK");
if (r == sectrue) vcp_println("OK");
}
}
@ -335,9 +325,9 @@ static void test_touch(const char *args) {
if (touch_click_timeout(&evt, timeout * 1000)) {
uint16_t x = touch_unpack_x(evt);
uint16_t y = touch_unpack_y(evt);
vcp_printf("OK %d %d", x, y);
vcp_println("OK %d %d", x, y);
} else {
vcp_printf("ERROR TIMEOUT");
vcp_println("ERROR TIMEOUT");
}
display_clear();
display_refresh();
@ -377,7 +367,7 @@ static void test_pwm(const char *args) {
display_backlight(v);
display_refresh();
vcp_printf("OK");
vcp_println("OK");
}
#ifdef USE_SD_CARD
@ -387,13 +377,13 @@ static void test_sd(void) {
static uint32_t buf2[BLOCK_SIZE / sizeof(uint32_t)];
if (sectrue != sdcard_is_present()) {
vcp_printf("ERROR NOCARD");
vcp_println("ERROR NOCARD");
return;
}
ensure(sdcard_power_on(), NULL);
if (sectrue != sdcard_read_blocks(buf1, 0, BLOCK_SIZE / SDCARD_BLOCK_SIZE)) {
vcp_printf("ERROR sdcard_read_blocks (0)");
vcp_println("ERROR sdcard_read_blocks (0)");
goto power_off;
}
for (int j = 1; j <= 2; j++) {
@ -402,21 +392,21 @@ static void test_sd(void) {
}
if (sectrue !=
sdcard_write_blocks(buf1, 0, BLOCK_SIZE / SDCARD_BLOCK_SIZE)) {
vcp_printf("ERROR sdcard_write_blocks (%d)", j);
vcp_println("ERROR sdcard_write_blocks (%d)", j);
goto power_off;
}
HAL_Delay(1000);
if (sectrue !=
sdcard_read_blocks(buf2, 0, BLOCK_SIZE / SDCARD_BLOCK_SIZE)) {
vcp_printf("ERROR sdcard_read_blocks (%d)", j);
vcp_println("ERROR sdcard_read_blocks (%d)", j);
goto power_off;
}
if (0 != memcmp(buf1, buf2, sizeof(buf1))) {
vcp_printf("ERROR DATA MISMATCH");
vcp_println("ERROR DATA MISMATCH");
goto power_off;
}
}
vcp_printf("OK");
vcp_println("OK");
power_off:
sdcard_power_off();
@ -436,7 +426,7 @@ static void test_wipe(void) {
display_text_center(DISPLAY_RESX / 2, DISPLAY_RESY / 2 + 10, "WIPED", -1,
FONT_BOLD, COLOR_WHITE, COLOR_BLACK);
display_refresh();
vcp_printf("OK");
vcp_println("OK");
}
#ifdef USE_SBU
@ -444,7 +434,7 @@ static void test_sbu(const char *args) {
secbool sbu1 = sectrue * (args[0] == '1');
secbool sbu2 = sectrue * (args[1] == '1');
sbu_set(sbu1, sbu2);
vcp_printf("OK");
vcp_println("OK");
}
#endif
@ -463,9 +453,9 @@ static void test_otp_read(void) {
// use (null) for empty data
if (data[0] == 0x00) {
vcp_printf("OK (null)");
vcp_println("OK (null)");
} else {
vcp_printf("OK %s", (const char *)data);
vcp_println("OK %s", (const char *)data);
}
}
@ -477,10 +467,23 @@ static void test_otp_write(const char *args) {
sizeof(data)),
NULL);
ensure(flash_otp_lock(FLASH_OTP_BLOCK_BATCH), NULL);
vcp_printf("OK");
vcp_println("OK");
}
static void test_otp_write_device_variant(const char *args) {
#ifdef USE_OPTIGA
optiga_locked_status status = get_optiga_locked_status();
if (status == OPTIGA_LOCKED_FALSE) {
vcp_println("ERROR NOT LOCKED");
return;
}
if (status != OPTIGA_LOCKED_TRUE) {
// Error reported by get_optiga_locked_status().
return;
}
#endif
volatile char data[32];
memzero((char *)data, sizeof(data));
data[0] = 1;
@ -513,7 +516,17 @@ static void test_otp_write_device_variant(const char *args) {
(const uint8_t *)data, sizeof(data)),
NULL);
ensure(flash_otp_lock(FLASH_OTP_BLOCK_DEVICE_VARIANT), NULL);
vcp_printf("OK");
vcp_println("OK");
}
void cpuid_read(void) {
uint32_t cpuid[3];
cpuid[0] = LL_GetUID_Word0();
cpuid[1] = LL_GetUID_Word1();
cpuid[2] = LL_GetUID_Word2();
vcp_print("OK ");
vcp_println_hex((uint8_t *)cpuid, sizeof(cpuid));
}
#define BACKLIGHT_NORMAL 150
@ -528,8 +541,10 @@ int main(void) {
#ifdef USE_BUTTON
button_init();
#endif
#ifdef USE_TOUCH
#ifdef USE_I2C
i2c_init();
#endif
#ifdef USE_TOUCH
touch_init();
#endif
#ifdef USE_SBU
@ -537,6 +552,15 @@ int main(void) {
#endif
usb_init_all();
#ifdef USE_OPTIGA
optiga_init();
optiga_open_application();
pair_optiga();
#endif
mpu_config_prodtest();
drop_privileges();
display_clear();
draw_border(1, 3);
@ -551,13 +575,17 @@ int main(void) {
display_fade(0, BACKLIGHT_NORMAL, 1000);
char line[128];
char line[2048]; // expecting hundreds of bytes represented as hexadecimal
// characters
for (;;) {
vcp_readline(line, sizeof(line));
if (startswith(line, "PING")) {
vcp_printf("OK");
vcp_println("OK");
} else if (startswith(line, "CPUID READ")) {
cpuid_read();
} else if (startswith(line, "BORDER")) {
test_border();
@ -584,6 +612,29 @@ int main(void) {
#ifdef USE_SBU
} else if (startswith(line, "SBU ")) {
test_sbu(line + 4);
#endif
#ifdef USE_OPTIGA
} else if (startswith(line, "OPTIGAID READ")) {
optigaid_read();
} else if (startswith(line, "CERTINF READ")) {
cert_read(OID_CERT_INF);
} else if (startswith(line, "CERTDEV WRITE ")) {
cert_write(OID_CERT_DEV, line + 14);
} else if (startswith(line, "CERTDEV READ")) {
cert_read(OID_CERT_DEV);
} else if (startswith(line, "CERTFIDO WRITE ")) {
cert_write(OID_CERT_FIDO, line + 15);
} else if (startswith(line, "CERTFIDO READ")) {
cert_read(OID_CERT_FIDO);
} else if (startswith(line, "KEYFIDO WRITE ")) {
keyfido_write(line + 14);
} else if (startswith(line, "KEYFIDO READ")) {
pubkey_read(OID_KEY_FIDO);
} else if (startswith(line, "LOCK")) {
optiga_lock();
} else if (startswith(line, "CHECK LOCKED")) {
check_locked();
#endif
} else if (startswith(line, "OTP READ")) {
@ -599,7 +650,7 @@ int main(void) {
test_wipe();
} else {
vcp_printf("UNKNOWN");
vcp_println("UNKNOWN");
}
}

@ -0,0 +1,500 @@
/*
* This file is part of the Trezor project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "optiga_prodtest.h"
#include "aes/aes.h"
#include "ecdsa.h"
#include "memzero.h"
#include "nist256p1.h"
#include "optiga_commands.h"
#include "optiga_transport.h"
#include "prodtest_common.h"
#include "rand.h"
#include "secret.h"
#include "sha2.h"
typedef enum {
OPTIGA_PAIRING_UNPAIRED = 0,
OPTIGA_PAIRING_PAIRED,
OPTIGA_PAIRING_ERR_RNG,
OPTIGA_PAIRING_ERR_READ,
OPTIGA_PAIRING_ERR_HANDSHAKE,
} optiga_pairing;
static optiga_pairing optiga_pairing_state = OPTIGA_PAIRING_UNPAIRED;
// Data object access conditions.
static const optiga_metadata_item ACCESS_PAIRED =
OPTIGA_ACCESS_CONDITION(OPTIGA_ACCESS_COND_CONF, OID_KEY_PAIRING);
static const optiga_metadata_item KEY_USE_SIGN = {
(const uint8_t[]){OPTIGA_KEY_USAGE_SIGN}, 1};
static const optiga_metadata_item TYPE_PTFBIND = {
(const uint8_t[]){OPTIGA_DATA_TYPE_PTFBIND}, 1};
static bool optiga_paired(void) {
const char *details = "";
switch (optiga_pairing_state) {
case OPTIGA_PAIRING_PAIRED:
return true;
case OPTIGA_PAIRING_ERR_RNG:
details = "optiga_get_random error";
break;
case OPTIGA_PAIRING_ERR_READ:
details = "failed to read pairing secret";
break;
case OPTIGA_PAIRING_ERR_HANDSHAKE:
details = "optiga_sec_chan_handshake";
break;
default:
break;
}
vcp_println("ERROR Optiga not paired (%s).", details);
return false;
}
static bool set_metadata(uint16_t oid, const optiga_metadata *metadata) {
uint8_t serialized[OPTIGA_MAX_METADATA_SIZE] = {0};
size_t size = 0;
optiga_result ret = optiga_serialize_metadata(metadata, serialized,
sizeof(serialized), &size);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_serialize_metadata error %d for OID 0x%04x.", ret,
oid);
return false;
}
optiga_set_data_object(oid, true, serialized, size);
ret =
optiga_get_data_object(oid, true, serialized, sizeof(serialized), &size);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_get_metadata error %d for OID 0x%04x.", ret, oid);
return false;
}
optiga_metadata metadata_stored = {0};
ret = optiga_parse_metadata(serialized, size, &metadata_stored);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_parse_metadata error %d.", ret);
return false;
}
if (!optiga_compare_metadata(metadata, &metadata_stored)) {
vcp_println("ERROR optiga_compare_metadata failed.");
return false;
}
return true;
}
void pair_optiga(void) {
// The pairing key may already be written and locked. The success of the
// pairing procedure is determined by optiga_sec_chan_handshake(). Therefore
// it is OK for some of the intermediate operations to fail.
// Enable writing the pairing secret to OPTIGA.
optiga_metadata metadata = {0};
metadata.change = OPTIGA_ACCESS_ALWAYS;
metadata.execute = OPTIGA_ACCESS_ALWAYS;
metadata.data_type = TYPE_PTFBIND;
set_metadata(OID_KEY_PAIRING, &metadata); // Ignore result.
// Generate pairing secret.
uint8_t secret[SECRET_OPTIGA_KEY_LEN] = {0};
optiga_result ret = optiga_get_random(secret, sizeof(secret));
if (OPTIGA_SUCCESS != ret) {
optiga_pairing_state = OPTIGA_PAIRING_ERR_RNG;
return;
}
// Store pairing secret.
ret = optiga_set_data_object(OID_KEY_PAIRING, false, secret, sizeof(secret));
if (OPTIGA_SUCCESS == ret) {
secret_erase();
secret_write_header();
secret_write(secret, SECRET_OPTIGA_KEY_OFFSET, SECRET_OPTIGA_KEY_LEN);
}
// Verify whether the secret was stored correctly in flash and OPTIGA.
memzero(secret, sizeof(secret));
if (secret_read(secret, SECRET_OPTIGA_KEY_OFFSET, SECRET_OPTIGA_KEY_LEN) !=
sectrue) {
optiga_pairing_state = OPTIGA_PAIRING_ERR_READ;
return;
}
ret = optiga_sec_chan_handshake(secret, sizeof(secret));
memzero(secret, sizeof(secret));
if (OPTIGA_SUCCESS != ret) {
optiga_pairing_state = OPTIGA_PAIRING_ERR_HANDSHAKE;
return;
}
optiga_pairing_state = OPTIGA_PAIRING_PAIRED;
return;
}
void optiga_lock(void) {
if (!optiga_paired()) return;
// Delete trust anchor.
optiga_result ret =
optiga_set_data_object(OID_TRUST_ANCHOR, false, (const uint8_t *)"\0", 1);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_set_data error %d for 0x%04x.", ret,
OID_TRUST_ANCHOR);
return;
}
// Set data object metadata.
optiga_metadata metadata = {0};
// Set metadata for device certificate.
memzero(&metadata, sizeof(metadata));
metadata.lcso = OPTIGA_LCS_OPERATIONAL;
metadata.change = OPTIGA_ACCESS_NEVER;
metadata.read = OPTIGA_ACCESS_ALWAYS;
metadata.execute = OPTIGA_ACCESS_ALWAYS;
if (!set_metadata(OID_CERT_DEV, &metadata)) {
return;
}
// Set metadata for FIDO attestation certificate.
memzero(&metadata, sizeof(metadata));
metadata.lcso = OPTIGA_LCS_OPERATIONAL;
metadata.change = OPTIGA_ACCESS_NEVER;
metadata.read = OPTIGA_ACCESS_ALWAYS;
metadata.execute = OPTIGA_ACCESS_ALWAYS;
if (!set_metadata(OID_CERT_FIDO, &metadata)) {
return;
}
// Set metadata for device private key.
memzero(&metadata, sizeof(metadata));
metadata.lcso = OPTIGA_LCS_OPERATIONAL;
metadata.change = OPTIGA_ACCESS_NEVER;
metadata.read = OPTIGA_ACCESS_NEVER;
metadata.execute = ACCESS_PAIRED;
metadata.key_usage = KEY_USE_SIGN;
if (!set_metadata(OID_KEY_DEV, &metadata)) {
return;
}
// Set metadata for FIDO attestation private key.
memzero(&metadata, sizeof(metadata));
metadata.lcso = OPTIGA_LCS_OPERATIONAL;
metadata.change = OPTIGA_ACCESS_NEVER;
metadata.read = OPTIGA_ACCESS_NEVER;
metadata.execute = ACCESS_PAIRED;
metadata.key_usage = KEY_USE_SIGN;
if (!set_metadata(OID_KEY_FIDO, &metadata)) {
return;
}
// Set metadata for pairing key.
memzero(&metadata, sizeof(metadata));
metadata.lcso = OPTIGA_LCS_OPERATIONAL;
metadata.change = OPTIGA_ACCESS_NEVER;
metadata.read = OPTIGA_ACCESS_NEVER;
metadata.execute = OPTIGA_ACCESS_ALWAYS;
metadata.data_type = TYPE_PTFBIND;
if (!set_metadata(OID_KEY_PAIRING, &metadata)) {
return;
}
vcp_println("OK");
}
optiga_locked_status get_optiga_locked_status(void) {
if (!optiga_paired()) return OPTIGA_LOCKED_ERROR;
const uint16_t oids[] = {OID_CERT_DEV, OID_CERT_FIDO, OID_KEY_DEV,
OID_KEY_FIDO, OID_KEY_PAIRING};
optiga_metadata locked_metadata = {0};
locked_metadata.lcso = OPTIGA_LCS_OPERATIONAL;
for (size_t i = 0; i < sizeof(oids) / sizeof(oids[0]); ++i) {
uint8_t metadata_buffer[OPTIGA_MAX_METADATA_SIZE] = {0};
size_t metadata_size = 0;
optiga_result ret =
optiga_get_data_object(oids[i], true, metadata_buffer,
sizeof(metadata_buffer), &metadata_size);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_get_metadata error %d for OID 0x%04x.", ret,
oids[i]);
return OPTIGA_LOCKED_ERROR;
}
optiga_metadata stored_metadata = {0};
ret =
optiga_parse_metadata(metadata_buffer, metadata_size, &stored_metadata);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_parse_metadata error %d.", ret);
return OPTIGA_LOCKED_ERROR;
}
if (!optiga_compare_metadata(&locked_metadata, &stored_metadata)) {
return OPTIGA_LOCKED_FALSE;
}
}
return OPTIGA_LOCKED_TRUE;
}
void check_locked(void) {
switch (get_optiga_locked_status()) {
case OPTIGA_LOCKED_TRUE:
vcp_println("OK YES");
break;
case OPTIGA_LOCKED_FALSE:
vcp_println("OK NO");
break;
default:
// Error reported by get_optiga_locked_status().
break;
}
}
void optigaid_read(void) {
if (!optiga_paired()) return;
uint8_t optiga_id[27] = {0};
size_t optiga_id_size = 0;
optiga_result ret =
optiga_get_data_object(OPTIGA_OID_COPROC_UID, false, optiga_id,
sizeof(optiga_id), &optiga_id_size);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_get_data_object error %d for 0x%04x.", ret,
OPTIGA_OID_COPROC_UID);
return;
}
vcp_print("OK ");
vcp_println_hex(optiga_id, optiga_id_size);
}
void cert_read(uint16_t oid) {
if (!optiga_paired()) return;
static uint8_t cert[2048] = {0};
size_t cert_size = 0;
optiga_result ret =
optiga_get_data_object(oid, false, cert, sizeof(cert), &cert_size);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_get_data_object error %d for 0x%04x.", ret, oid);
return;
}
size_t offset = 0;
if (cert[0] == 0xC0) {
// TLS identity certificate chain.
size_t tls_identity_size = (cert[1] << 8) + cert[2];
size_t cert_chain_size = (cert[3] << 16) + (cert[4] << 8) + cert[5];
size_t first_cert_size = (cert[6] << 16) + (cert[7] << 8) + cert[8];
if (tls_identity_size + 3 > cert_size ||
cert_chain_size + 3 > tls_identity_size ||
first_cert_size > cert_chain_size) {
vcp_println("ERROR invalid TLS identity in 0x%04x.", oid);
return;
}
offset = 9;
cert_size = first_cert_size;
}
if (cert_size == 0) {
vcp_println("ERROR no certificate in 0x%04x.", oid);
return;
}
vcp_print("OK ");
vcp_println_hex(cert + offset, cert_size);
}
void cert_write(uint16_t oid, char *data) {
if (!optiga_paired()) return;
// Enable writing to the certificate slot.
optiga_metadata metadata = {0};
metadata.change = OPTIGA_ACCESS_ALWAYS;
set_metadata(oid, &metadata); // Ignore result.
uint8_t data_bytes[1024];
int len = get_from_hex(data_bytes, sizeof(data_bytes), data);
if (len < 0) {
vcp_println("ERROR Hexadecimal decoding error %d.", len);
return;
}
optiga_result ret = optiga_set_data_object(oid, false, data_bytes, len);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_set_data error %d for 0x%04x.", ret, oid);
return;
}
vcp_println("OK");
}
void pubkey_read(uint16_t oid) {
if (!optiga_paired()) return;
// Enable key agreement usage.
optiga_metadata metadata = {0};
uint8_t key_usage = OPTIGA_KEY_USAGE_KEYAGREE;
metadata.key_usage.ptr = &key_usage;
metadata.key_usage.len = 1;
metadata.execute = OPTIGA_ACCESS_ALWAYS;
if (!set_metadata(oid, &metadata)) {
return;
}
// Execute ECDH with base point to get the x-coordinate of the public key.
static const uint8_t BASE_POINT[] = {
0x03, 0x42, 0x00, 0x04, 0x6b, 0x17, 0xd1, 0xf2, 0xe1, 0x2c, 0x42, 0x47,
0xf8, 0xbc, 0xe6, 0xe5, 0x63, 0xa4, 0x40, 0xf2, 0x77, 0x03, 0x7d, 0x81,
0x2d, 0xeb, 0x33, 0xa0, 0xf4, 0xa1, 0x39, 0x45, 0xd8, 0x98, 0xc2, 0x96,
0x4f, 0xe3, 0x42, 0xe2, 0xfe, 0x1a, 0x7f, 0x9b, 0x8e, 0xe7, 0xeb, 0x4a,
0x7c, 0x0f, 0x9e, 0x16, 0x2b, 0xce, 0x33, 0x57, 0x6b, 0x31, 0x5e, 0xce,
0xcb, 0xb6, 0x40, 0x68, 0x37, 0xbf, 0x51, 0xf5};
uint8_t public_key[32] = {0};
size_t public_key_size = 0;
optiga_result ret =
optiga_calc_ssec(OPTIGA_CURVE_P256, oid, BASE_POINT, sizeof(BASE_POINT),
public_key, sizeof(public_key), &public_key_size);
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_calc_ssec error %d.", ret);
return;
}
vcp_print("OK ");
vcp_println_hex(public_key, public_key_size);
}
void keyfido_write(char *data) {
if (!optiga_paired()) return;
const size_t EPH_PUB_KEY_SIZE = 33;
const size_t PAYLOAD_SIZE = 32;
const size_t CIPHERTEXT_OFFSET = EPH_PUB_KEY_SIZE;
const size_t EXPECTED_SIZE = EPH_PUB_KEY_SIZE + PAYLOAD_SIZE;
// Enable key agreement usage for device key.
optiga_metadata metadata = {0};
uint8_t key_usage = OPTIGA_KEY_USAGE_KEYAGREE;
metadata.key_usage.ptr = &key_usage;
metadata.key_usage.len = 1;
metadata.execute = OPTIGA_ACCESS_ALWAYS;
if (!set_metadata(OID_KEY_DEV, &metadata)) {
return;
}
// Read encrypted FIDO attestation private key.
uint8_t data_bytes[EXPECTED_SIZE];
int len = get_from_hex(data_bytes, sizeof(data_bytes), data);
if (len < 0) {
vcp_println("ERROR Hexadecimal decoding error %d.", len);
return;
}
if (len != EXPECTED_SIZE) {
vcp_println("ERROR Unexpected input length.");
return;
}
// Expand sender's ephemeral public key.
uint8_t public_key[3 + 65] = {0x03, 0x42, 0x00};
if (ecdsa_uncompress_pubkey(&nist256p1, data_bytes, &public_key[3]) != 1) {
vcp_println("ERROR Failed to decode public key.");
return;
}
// Execute ECDH with device private key.
uint8_t secret[32] = {0};
size_t secret_size = 0;
optiga_result ret = optiga_calc_ssec(OPTIGA_CURVE_P256, OID_KEY_DEV,
public_key, sizeof(public_key), secret,
sizeof(secret), &secret_size);
if (OPTIGA_SUCCESS != ret) {
memzero(secret, sizeof(secret));
vcp_println("ERROR optiga_calc_ssec error %d.", ret);
return;
}
// Hash the shared secret. Use the result as the decryption key.
sha256_Raw(secret, secret_size, secret);
aes_decrypt_ctx ctx = {0};
AES_RETURN aes_ret = aes_decrypt_key256(secret, &ctx);
if (EXIT_SUCCESS != aes_ret) {
vcp_println("ERROR aes_decrypt_key256 error.");
memzero(&ctx, sizeof(ctx));
memzero(secret, sizeof(secret));
return;
}
// Decrypt the FIDO attestation key.
uint8_t fido_key[PAYLOAD_SIZE];
// The IV is intentionally all-zero, which is not a problem, because the
// encryption key is unique for each ciphertext.
uint8_t iv[AES_BLOCK_SIZE] = {0};
aes_ret = aes_cbc_decrypt(&data_bytes[CIPHERTEXT_OFFSET], fido_key,
sizeof(fido_key), iv, &ctx);
memzero(&ctx, sizeof(ctx));
memzero(secret, sizeof(secret));
if (EXIT_SUCCESS != aes_ret) {
memzero(fido_key, sizeof(fido_key));
vcp_println("ERROR aes_cbc_decrypt error.");
return;
}
// Write trust anchor certificate to OID 0xE0E8
ret = optiga_set_trust_anchor();
if (OPTIGA_SUCCESS != ret) {
memzero(fido_key, sizeof(fido_key));
vcp_println("ERROR optiga_set_trust_anchor error %d.", ret);
return;
}
// Set change access condition for the FIDO key to Int(0xE0E8), so that we
// can write the FIDO key using the trust anchor in OID 0xE0E8.
memzero(&metadata, sizeof(metadata));
metadata.change.ptr = (const uint8_t *)"\x21\xe0\xe8";
metadata.change.len = 3;
if (!set_metadata(OID_KEY_FIDO, &metadata)) {
return;
}
// Store the FIDO attestation key.
ret = optiga_set_priv_key(OID_KEY_FIDO, fido_key);
memzero(fido_key, sizeof(fido_key));
if (OPTIGA_SUCCESS != ret) {
vcp_println("ERROR optiga_set_priv_key error %d.", ret);
return;
}
vcp_println("OK");
}

@ -0,0 +1,51 @@
/*
* This file is part of the Trezor project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PRODTEST_OPTIGA_PRODTESTS_H
#define PRODTEST_OPTIGA_PRODTEST_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define OID_CERT_INF OPTIGA_OID_CERT + 0
#define OID_CERT_DEV OPTIGA_OID_CERT + 1
#define OID_CERT_FIDO OPTIGA_OID_CERT + 2
#define OID_KEY_DEV OPTIGA_OID_ECC_KEY + 0
#define OID_KEY_FIDO OPTIGA_OID_ECC_KEY + 2
#define OID_KEY_PAIRING OPTIGA_OID_PTFBIND_SECRET
#define OID_TRUST_ANCHOR OPTIGA_OID_CA_CERT + 0
typedef enum {
OPTIGA_LOCKED_TRUE,
OPTIGA_LOCKED_FALSE,
OPTIGA_LOCKED_ERROR,
} optiga_locked_status;
void pair_optiga(void);
void optigaid_read(void);
void cert_read(uint16_t oid);
void cert_write(uint16_t oid, char *data);
void keyfido_write(char *data);
void pubkey_read(uint16_t oid);
void optiga_lock(void);
optiga_locked_status get_optiga_locked_status(void);
void check_locked(void);
#endif

@ -0,0 +1,102 @@
/*
* This file is part of the Trezor project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "prodtest_common.h"
#include "mini_printf.h"
#include "usb.h"
void vcp_puts(const char *s, size_t len) {
int r = usb_vcp_write_blocking(VCP_IFACE, (const uint8_t *)s, len, -1);
(void)r;
}
void vcp_print(const char *fmt, ...) {
static char buf[128];
va_list va;
va_start(va, fmt);
int r = mini_vsnprintf(buf, sizeof(buf), fmt, va);
va_end(va);
vcp_puts(buf, r);
}
void vcp_println(const char *fmt, ...) {
static char buf[128];
va_list va;
va_start(va, fmt);
int r = mini_vsnprintf(buf, sizeof(buf), fmt, va);
va_end(va);
vcp_puts(buf, r);
vcp_puts("\r\n", 2);
}
void vcp_println_hex(uint8_t *data, uint16_t len) {
for (int i = 0; i < len; i++) {
vcp_print("%02X", data[i]);
}
vcp_puts("\r\n", 2);
}
static uint16_t get_byte_from_hex(const char **hex) {
uint8_t result = 0;
// Skip whitespace.
while (**hex == ' ') {
*hex += 1;
}
for (int i = 0; i < 2; i++) {
result <<= 4;
char c = **hex;
if (c >= '0' && c <= '9') {
result |= c - '0';
} else if (c >= 'A' && c <= 'F') {
result |= c - 'A' + 10;
} else if (c >= 'a' && c <= 'f') {
result |= c - 'a' + 10;
} else if (c == '\0') {
return 0x100;
} else {
return 0xFFFF;
}
*hex += 1;
}
return result;
}
int get_from_hex(uint8_t *buf, uint16_t buf_len, const char *hex) {
int len = 0;
uint16_t b = get_byte_from_hex(&hex);
for (len = 0; len < buf_len && b <= 0xff; ++len) {
buf[len] = b;
b = get_byte_from_hex(&hex);
}
if (b == 0x100) {
// Success.
return len;
}
if (b > 0xff) {
// Non-hexadecimal character.
return -1;
}
// Buffer too small.
return -2;
}

@ -0,0 +1,34 @@
/*
* This file is part of the Trezor project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PRODTEST_COMMON_H
#define PRODTEST_COMMON_H
#include <stdint.h>
#include <stdlib.h>
enum { VCP_IFACE = 0x00 };
void vcp_puts(const char *s, size_t len);
void vcp_print(const char *fmt, ...);
void vcp_println(const char *fmt, ...);
void vcp_println_hex(uint8_t *data, uint16_t len);
int get_from_hex(uint8_t *buf, uint16_t buf_len, const char *hex);
#endif

@ -1,10 +1,10 @@
#define VERSION_MAJOR 0
#define VERSION_MINOR 1
#define VERSION_PATCH 0
#define VERSION_MINOR 2
#define VERSION_PATCH 2
#define VERSION_BUILD 0
#define FIX_VERSION_MAJOR 0
#define FIX_VERSION_MINOR 1
#define FIX_VERSION_MINOR 2
#define FIX_VERSION_PATCH 0
#define FIX_VERSION_BUILD 0

@ -29,6 +29,7 @@ sd_card = []
rgb_led = []
backlight = []
usb = []
optiga = []
test = [
"button",
"cc",
@ -39,7 +40,8 @@ test = [
"ui",
"dma2d",
"touch",
"backlight"
"backlight",
"optiga"
]
[lib]

@ -94,7 +94,7 @@ fn prepare_bindings() -> bindgen::Builder {
let mut clang_args: Vec<&str> = Vec::new();
let includes = env::var("RUST_INCLUDES").unwrap();
let args = includes.split(";");
let args = includes.split(';');
for arg in args {
clang_args.push(arg);

@ -34,20 +34,24 @@ static void _librust_qstrs(void) {
MP_QSTR_bounds;
MP_QSTR_button;
MP_QSTR_button_event;
MP_QSTR_cancel_arrow;
MP_QSTR_case_sensitive;
MP_QSTR_chunkify;
MP_QSTR_confirm_action;
MP_QSTR_confirm_address;
MP_QSTR_confirm_backup;
MP_QSTR_confirm_blob;
MP_QSTR_confirm_coinjoin;
MP_QSTR_confirm_emphasized;
MP_QSTR_confirm_ethereum_tx;
MP_QSTR_confirm_fido;
MP_QSTR_confirm_homescreen;
MP_QSTR_confirm_joint_total;
MP_QSTR_confirm_modify_fee;
MP_QSTR_confirm_modify_output;
MP_QSTR_confirm_more;
MP_QSTR_confirm_output;
MP_QSTR_confirm_output_address;
MP_QSTR_confirm_output_amount;
MP_QSTR_confirm_properties;
MP_QSTR_confirm_recovery;
MP_QSTR_confirm_reset_device;
@ -58,6 +62,7 @@ static void _librust_qstrs(void) {
MP_QSTR_data;
MP_QSTR_decode;
MP_QSTR_description;
MP_QSTR_details_title;
MP_QSTR_disable_animation;
MP_QSTR_draw_welcome_screen;
MP_QSTR_dry_run;
@ -66,9 +71,7 @@ static void _librust_qstrs(void) {
MP_QSTR_extra;
MP_QSTR_fee_amount;
MP_QSTR_fee_label;
MP_QSTR_fee_rate;
MP_QSTR_fee_rate_amount;
MP_QSTR_fee_rate_title;
MP_QSTR_hold;
MP_QSTR_hold_danger;
MP_QSTR_icon_name;
@ -85,6 +88,7 @@ static void _librust_qstrs(void) {
MP_QSTR_max_feerate;
MP_QSTR_max_len;
MP_QSTR_max_rounds;
MP_QSTR_maximum_fee;
MP_QSTR_min_count;
MP_QSTR_multiple_pages_texts;
MP_QSTR_notification;
@ -95,6 +99,8 @@ static void _librust_qstrs(void) {
MP_QSTR_path;
MP_QSTR_progress_event;
MP_QSTR_prompt;
MP_QSTR_qr_title;
MP_QSTR_recipient;
MP_QSTR_request_bip39;
MP_QSTR_request_complete_repaint;
MP_QSTR_request_number;
@ -111,6 +117,7 @@ static void _librust_qstrs(void) {
MP_QSTR_show_group_share_success;
MP_QSTR_show_homescreen;
MP_QSTR_show_info;
MP_QSTR_show_info_with_cancel;
MP_QSTR_show_lockscreen;
MP_QSTR_show_mismatch;
MP_QSTR_show_passphrase;
@ -119,7 +126,6 @@ static void _librust_qstrs(void) {
MP_QSTR_show_remaining_shares;
MP_QSTR_show_share_words;
MP_QSTR_show_simple;
MP_QSTR_show_spending_details;
MP_QSTR_show_success;
MP_QSTR_show_warning;
MP_QSTR_sign;

@ -52,6 +52,40 @@ pub struct TextLayout {
pub continues_from_prev_page: bool,
}
/// Configuration for chunkifying the text into smaller parts.
#[derive(Copy, Clone)]
pub struct Chunks {
/// How many characters will be grouped in one chunk.
pub chunk_size: usize,
/// How big will be the space between chunks (in pixels).
pub x_offset: i16,
/// Optional characters that are wider and should be accounted for
pub wider_chars: Option<&'static str>,
}
impl Chunks {
pub const fn new(chunk_size: usize, x_offset: i16) -> Self {
Chunks {
chunk_size,
x_offset,
wider_chars: None,
}
}
pub const fn with_wider_chars(mut self, wider_chars: &'static str) -> Self {
self.wider_chars = Some(wider_chars);
self
}
pub fn is_char_wider(self, ch: char) -> bool {
if let Some(wider_chars) = self.wider_chars {
wider_chars.contains(ch)
} else {
false
}
}
}
#[derive(Copy, Clone)]
pub struct TextStyle {
/// Text font ID.
@ -76,6 +110,14 @@ pub struct TextStyle {
pub line_breaking: LineBreaking,
/// Specifies what to do at the end of the page.
pub page_breaking: PageBreaking,
/// Optionally chunkify all the text with a specified chunk
/// size and pixel offset for the next chunk.
pub chunks: Option<Chunks>,
/// Optionally increase the vertical space between text lines
/// (can be even negative, in which case it will decrease it).
pub line_spacing: i16,
}
impl TextStyle {
@ -96,6 +138,8 @@ impl TextStyle {
prev_page_ellipsis_icon: None,
line_breaking: LineBreaking::BreakAtWhitespace,
page_breaking: PageBreaking::CutAndInsertEllipsis,
chunks: None,
line_spacing: 0,
}
}
@ -121,6 +165,18 @@ impl TextStyle {
self
}
/// Adding optional chunkification to the text.
pub const fn with_chunks(mut self, chunks: Chunks) -> Self {
self.chunks = Some(chunks);
self
}
/// Adding optional change of vertical line spacing.
pub const fn with_line_spacing(mut self, line_spacing: i16) -> Self {
self.line_spacing = line_spacing;
self
}
fn ellipsis_width(&self) -> i16 {
if let Some((icon, margin)) = self.ellipsis_icon {
icon.toif.width() + margin
@ -220,12 +276,13 @@ impl TextLayout {
};
let remaining_width = self.bounds.x1 - cursor.x;
let span = Span::fit_horizontally(
let mut span = Span::fit_horizontally(
remaining_text,
remaining_width,
self.style.text_font,
self.style.line_breaking,
line_ending_space,
self.style.chunks,
);
cursor.x += match self.align {
@ -251,6 +308,9 @@ impl TextLayout {
if span.advance.y > 0 {
// We're advancing to the next line.
// Possibly making a bigger/smaller vertical jump
span.advance.y += self.style.line_spacing;
// Check if we should be appending a hyphen at this point.
if span.insert_hyphen_before_line_break {
sink.hyphen(*cursor, self);
@ -488,6 +548,7 @@ impl Span {
text_font: impl GlyphMetrics,
breaking: LineBreaking,
line_ending_space: i16,
chunks: Option<Chunks>,
) -> Self {
const ASCII_LF: char = '\n';
const ASCII_CR: char = '\r';
@ -537,6 +598,7 @@ impl Span {
let mut span_width = 0;
let mut found_any_whitespace = false;
let mut chunks_wider_chars = 0;
let mut char_indices_iter = text.char_indices().peekable();
// Iterating manually because we need a reference to the iterator inside the
@ -544,6 +606,22 @@ impl Span {
while let Some((i, ch)) = char_indices_iter.next() {
let char_width = text_font.char_width(ch);
// When there is a set chunk size and we reach it,
// adjust the line advances and return the line.
if let Some(chunkify_config) = chunks {
if i == chunkify_config.chunk_size {
line.advance.y = 0;
// Decreasing the offset for each wider character in the chunk
line.advance.x += chunkify_config.x_offset - chunks_wider_chars;
return line;
} else {
// Counting all the wider characters in the chunk
if chunkify_config.is_char_wider(ch) {
chunks_wider_chars += 1;
}
}
}
// Consider if we could be breaking the line at this position.
if is_whitespace(ch) && span_width + complete_word_end_width <= max_width {
// Break before the whitespace, without hyphen.
@ -679,6 +757,7 @@ mod tests {
FIXED_FONT,
LineBreaking::BreakAtWhitespace,
0,
None,
);
spans.push((
&remaining_text[..span.length],

@ -8,7 +8,7 @@ use crate::{
};
use super::{
layout::{LayoutFit, LayoutSink, TextLayout},
layout::{Chunks, LayoutFit, LayoutSink, TextLayout},
LineBreaking, TextStyle,
};
@ -16,7 +16,7 @@ use heapless::Vec;
// So that there is only one implementation, and not multiple generic ones
// as would be via `const N: usize` generics.
const MAX_OPS: usize = 15;
const MAX_OPS: usize = 20;
/// To account for operations that are not made of characters
/// but need to be accounted for somehow.
@ -39,6 +39,10 @@ impl<'a, T: StringType + Clone + 'a> OpTextLayout<T> {
}
}
pub fn is_empty(&self) -> bool {
self.ops.len() == 0
}
pub fn place(&mut self, bounds: Rect) -> Rect {
self.layout.bounds = bounds;
bounds
@ -86,6 +90,12 @@ impl<'a, T: StringType + Clone + 'a> OpTextLayout<T> {
cursor.x += offset.x;
cursor.y += offset.y;
}
Op::Chunkify(chunks) => {
self.layout.style.chunks = chunks;
}
Op::LineSpacing(line_spacing) => {
self.layout.style.line_spacing = line_spacing;
}
// Moving to the next page
Op::NextPage => {
// Pretending that nothing more fits on current page to force
@ -215,6 +225,14 @@ impl<T: StringType + Clone> OpTextLayout<T> {
pub fn line_breaking(self, line_breaking: LineBreaking) -> Self {
self.with_new_item(Op::LineBreaking(line_breaking))
}
pub fn chunks(self, chunks: Option<Chunks>) -> Self {
self.with_new_item(Op::Chunkify(chunks))
}
pub fn line_spacing(self, spacing: i16) -> Self {
self.with_new_item(Op::LineSpacing(spacing))
}
}
// Op-adding aggregation operations
@ -234,6 +252,14 @@ impl<T: StringType + Clone> OpTextLayout<T> {
pub fn text_demibold(self, text: T) -> Self {
self.font(Font::DEMIBOLD).text(text)
}
pub fn chunkify_text(self, chunks: Option<(Chunks, i16)>) -> Self {
if let Some(chunks) = chunks {
self.chunks(Some(chunks.0)).line_spacing(chunks.1)
} else {
self.chunks(None).line_spacing(0)
}
}
}
#[derive(Clone)]
@ -254,4 +280,8 @@ pub enum Op<T: StringType> {
CursorOffset(Offset),
/// Force continuing on the next page.
NextPage,
/// Render the following text in a chunkified way. None will disable that.
Chunkify(Option<Chunks>),
/// Change the line vertical line spacing.
LineSpacing(i16),
}

@ -108,7 +108,7 @@ where
/// Button layout for the current page.
/// Normally there are arrows everywhere, apart from the right side of the
/// last page. On xpub pages there is VIEW FULL middle button when it
/// last page. On xpub pages there is SHOW ALL middle button when it
/// cannot fit one page. On xpub subpages there are wide arrows to
/// scroll.
fn get_button_layout(&mut self) -> ButtonLayout<T> {
@ -123,7 +123,7 @@ where
} else {
let left = Some(ButtonDetails::left_arrow_icon());
let middle = if self.is_xpub_page() && self.subpages_in_current_page() > 1 {
Some(ButtonDetails::armed_text("VIEW FULL".into()))
Some(ButtonDetails::armed_text("SHOW ALL".into()))
} else {
None
};

@ -13,7 +13,7 @@ use super::{loader::DEFAULT_DURATION_MS, theme};
const HALF_SCREEN_BUTTON_WIDTH: i16 = constant::WIDTH / 2 - 1;
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum ButtonPos {
Left,
Middle,
@ -351,7 +351,10 @@ pub struct ButtonDetails<T> {
offset: Offset,
}
impl<T> ButtonDetails<T> {
impl<T> ButtonDetails<T>
where
T: StringType,
{
/// Text button.
pub fn text(text: T) -> Self {
Self {
@ -376,6 +379,16 @@ impl<T> ButtonDetails<T> {
}
}
/// Resolves text and finds possible icon names.
pub fn from_text_possible_icon(text: T) -> Self {
match text.as_ref() {
"" => Self::cancel_icon(),
"<" => Self::left_arrow_icon(),
"^" => Self::up_arrow_icon(),
_ => Self::text(text),
}
}
/// Text with arms signalling double press.
pub fn armed_text(text: T) -> Self {
Self::text(text).with_arms()
@ -399,7 +412,7 @@ impl<T> ButtonDetails<T> {
/// Up arrow to signal paginating back. No outline. Offsetted little right
/// to not be on the boundary.
pub fn up_arrow_icon() -> Self {
Self::icon(theme::ICON_ARROW_UP).with_offset(Offset::new(2, -3))
Self::icon(theme::ICON_ARROW_UP).with_offset(Offset::new(3, -4))
}
/// Down arrow to signal paginating forward. Takes half the screen's width
@ -529,6 +542,15 @@ where
)
}
/// Left text, armed text and right info icon/text.
pub fn text_armed_info(left: T, middle: T) -> Self {
Self::new(
Some(ButtonDetails::from_text_possible_icon(left)),
Some(ButtonDetails::armed_text(middle)),
Some(ButtonDetails::text("i".into()).with_fixed_width(theme::BUTTON_ICON_WIDTH)),
)
}
/// Left cancel, armed text and right info icon/text.
pub fn cancel_armed_info(middle: T) -> Self {
Self::new(
@ -559,16 +581,16 @@ where
/// Left and right texts.
pub fn text_none_text(left: T, right: T) -> Self {
Self::new(
Some(ButtonDetails::text(left)),
Some(ButtonDetails::from_text_possible_icon(left)),
None,
Some(ButtonDetails::text(right)),
Some(ButtonDetails::from_text_possible_icon(right)),
)
}
/// Left text and right arrow.
pub fn text_none_arrow(text: T) -> Self {
Self::new(
Some(ButtonDetails::text(text)),
Some(ButtonDetails::from_text_possible_icon(text)),
None,
Some(ButtonDetails::right_arrow_icon()),
)
@ -577,7 +599,7 @@ where
/// Left text and WIDE right arrow.
pub fn text_none_arrow_wide(text: T) -> Self {
Self::new(
Some(ButtonDetails::text(text)),
Some(ButtonDetails::from_text_possible_icon(text)),
None,
Some(ButtonDetails::down_arrow_icon_wide()),
)
@ -585,7 +607,11 @@ where
/// Only right text.
pub fn none_none_text(text: T) -> Self {
Self::new(None, None, Some(ButtonDetails::text(text)))
Self::new(
None,
None,
Some(ButtonDetails::from_text_possible_icon(text)),
)
}
/// Left and right arrow icons for navigation.
@ -602,7 +628,7 @@ where
Self::new(
Some(ButtonDetails::left_arrow_icon()),
None,
Some(ButtonDetails::text(text)),
Some(ButtonDetails::from_text_possible_icon(text)),
)
}
@ -611,7 +637,7 @@ where
Self::new(
Some(ButtonDetails::up_arrow_icon()),
None,
Some(ButtonDetails::text(text)),
Some(ButtonDetails::from_text_possible_icon(text)),
)
}
@ -633,7 +659,7 @@ where
)
}
/// Cancel cross on left and right arrow facing down.
/// Up arrow on left and right arrow facing down.
pub fn up_arrow_none_arrow_wide() -> Self {
Self::new(
Some(ButtonDetails::up_arrow_icon()),
@ -642,6 +668,15 @@ where
)
}
/// Up arrow on left, middle text and info on the right.
pub fn up_arrow_armed_info(text: T) -> Self {
Self::new(
Some(ButtonDetails::up_arrow_icon()),
Some(ButtonDetails::armed_text(text)),
Some(ButtonDetails::text("i".into()).with_fixed_width(theme::BUTTON_ICON_WIDTH)),
)
}
/// Cancel cross on left and right arrow facing down.
pub fn cancel_none_arrow_down() -> Self {
Self::new(
@ -656,7 +691,7 @@ where
Self::new(
Some(ButtonDetails::cancel_icon()),
None,
Some(ButtonDetails::text(text)),
Some(ButtonDetails::from_text_possible_icon(text)),
)
}

@ -3,8 +3,9 @@ use super::{
};
use crate::{
strutil::StringType,
time::Duration,
ui::{
component::{base::Event, Component, EventCtx, Pad},
component::{base::Event, Component, EventCtx, Pad, TimerToken},
event::{ButtonEvent, PhysicalButton},
geometry::Rect,
},
@ -195,6 +196,9 @@ where
///
/// Only "final" button events are returned in `ButtonControllerMsg::Triggered`,
/// based upon the buttons being long-press or not.
///
/// There is optional complexity with IgnoreButtonDelay, which gets executed
/// only in cases where clients request it.
pub struct ButtonController<T>
where
T: StringType,
@ -204,10 +208,12 @@ where
middle_btn: ButtonContainer<T>,
right_btn: ButtonContainer<T>,
state: ButtonState,
// Button area is needed so the buttons
// can be "re-placed" after their text is changed
// Will be set in `place`
/// Button area is needed so the buttons
/// can be "re-placed" after their text is changed
/// Will be set in `place`
button_area: Rect,
/// Handling optional ignoring of buttons after pressing the other button.
ignore_btn_delay: Option<IgnoreButtonDelay>,
}
impl<T> ButtonController<T>
@ -222,9 +228,17 @@ where
right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right),
state: ButtonState::Nothing,
button_area: Rect::zero(),
ignore_btn_delay: None,
}
}
/// Set up the logic to ignore a button after some time from pressing
/// the other button.
pub fn with_ignore_btn_delay(mut self, delay_ms: u32) -> Self {
self.ignore_btn_delay = Some(IgnoreButtonDelay::new(delay_ms));
self
}
/// Updating all the three buttons to the wanted states.
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
self.pad.clear();
@ -240,6 +254,14 @@ where
self.right_btn.set_pressed(ctx, right);
}
pub fn highlight_button(&mut self, ctx: &mut EventCtx, pos: ButtonPos) {
match pos {
ButtonPos::Left => self.left_btn.set_pressed(ctx, true),
ButtonPos::Middle => self.middle_btn.set_pressed(ctx, true),
ButtonPos::Right => self.right_btn.set_pressed(ctx, true),
}
}
/// Handle middle button hold-to-confirm start.
/// We need to cancel possible holds in both other buttons.
fn middle_hold_started(&mut self, ctx: &mut EventCtx) {
@ -291,22 +313,29 @@ where
// _ _
ButtonState::Nothing => match button_event {
// ▼ * | * ▼
ButtonEvent::ButtonPressed(which) => (
ButtonEvent::ButtonPressed(which) => {
// ↓ _ | _ ↓
ButtonState::OneDown(which),
match which {
// ▼ *
PhysicalButton::Left => {
self.left_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
}
// * ▼
PhysicalButton::Right => {
self.right_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
}
},
),
// Initial button press will set the timer for second button,
// and after some delay, it will become un-clickable
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.handle_button_press(ctx, which);
}
(
ButtonState::OneDown(which),
match which {
// ▼ *
PhysicalButton::Left => {
self.left_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
}
// * ▼
PhysicalButton::Right => {
self.right_btn.hold_started(ctx);
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
}
},
)
}
_ => (self.state, None),
},
// ↓ _ | _ ↓
@ -314,18 +343,32 @@ where
// ▲ * | * ▲
ButtonEvent::ButtonReleased(b) if b == which_down => match which_down {
// ▲ *
// Trigger the button and make the second one clickable in all cases
PhysicalButton::Left => {
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.make_button_clickable(ButtonPos::Right);
}
// _ _
(ButtonState::Nothing, self.left_btn.maybe_trigger(ctx))
}
// * ▲
PhysicalButton::Right => {
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.make_button_clickable(ButtonPos::Left);
}
// _ _
(ButtonState::Nothing, self.right_btn.maybe_trigger(ctx))
}
},
// * ▼ | ▼ *
ButtonEvent::ButtonPressed(b) if b != which_down => {
// Buttons may be non-clickable (caused by long-holding the other
// button)
if let Some(ignore_btn_delay) = &self.ignore_btn_delay {
if ignore_btn_delay.ignore_button(b) {
return None;
}
}
self.middle_hold_started(ctx);
(
// ↓ ↓
@ -356,6 +399,11 @@ where
// ▲ * | * ▲
ButtonEvent::ButtonReleased(b) if b != which_up => {
// _ _
// Both button needs to be clickable now
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.make_button_clickable(ButtonPos::Left);
ignore_btn_delay.make_button_clickable(ButtonPos::Right);
}
(ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx))
}
_ => (self.state, None),
@ -394,8 +442,13 @@ where
self.state = new_state;
event
}
// HoldToConfirm expiration
Event::Timer(_) => self.handle_htc_expiration(ctx, event),
// Timer - handle clickable properties and HoldToConfirm expiration
Event::Timer(token) => {
if let Some(ignore_btn_delay) = &mut self.ignore_btn_delay {
ignore_btn_delay.handle_timer_token(token);
}
self.handle_htc_expiration(ctx, event)
}
_ => None,
}
}
@ -421,6 +474,179 @@ where
}
}
/// When one button is pressed, the other one becomes un-clickable after some
/// small time period. This is to prevent accidental clicks when user is holding
/// the button to automatically move through items.
struct IgnoreButtonDelay {
/// How big is the delay after the button is inactive
delay: Duration,
/// Whether left button is currently clickable
left_clickable: bool,
/// Whether right button is currently clickable
right_clickable: bool,
/// Timer for setting the left_clickable
left_clickable_timer: Option<TimerToken>,
/// Timer for setting the right_clickable
right_clickable_timer: Option<TimerToken>,
}
impl IgnoreButtonDelay {
pub fn new(delay_ms: u32) -> Self {
Self {
delay: Duration::from_millis(delay_ms),
left_clickable: true,
right_clickable: true,
left_clickable_timer: None,
right_clickable_timer: None,
}
}
pub fn make_button_clickable(&mut self, pos: ButtonPos) {
match pos {
ButtonPos::Left => {
self.left_clickable = true;
self.left_clickable_timer = None;
}
ButtonPos::Right => {
self.right_clickable = true;
self.right_clickable_timer = None;
}
ButtonPos::Middle => {}
}
}
pub fn handle_button_press(&mut self, ctx: &mut EventCtx, button: PhysicalButton) {
if matches!(button, PhysicalButton::Left) {
self.right_clickable_timer = Some(ctx.request_timer(self.delay));
}
if matches!(button, PhysicalButton::Right) {
self.left_clickable_timer = Some(ctx.request_timer(self.delay));
}
}
pub fn ignore_button(&self, button: PhysicalButton) -> bool {
if matches!(button, PhysicalButton::Left) && !self.left_clickable {
return true;
}
if matches!(button, PhysicalButton::Right) && !self.right_clickable {
return true;
}
false
}
pub fn handle_timer_token(&mut self, token: TimerToken) {
if self.left_clickable_timer == Some(token) {
self.left_clickable = false;
self.left_clickable_timer = None;
}
if self.right_clickable_timer == Some(token) {
self.right_clickable = false;
self.right_clickable_timer = None;
}
}
}
/// Component allowing for automatically moving through items (e.g. Choice
/// items).
///
/// Users are in full control of starting/stopping the movement.
///
/// Can be started e.g. by holding left/right button.
pub struct AutomaticMover {
/// For requesting timer events repeatedly
timer_token: Option<TimerToken>,
/// Which direction should we go (which button is down)
moving_direction: Option<ButtonPos>,
/// How many screens were moved automatically
auto_moved_screens: usize,
/// Function to get duration of each movement according to the already moved
/// steps
duration_func: fn(usize) -> u32,
}
impl AutomaticMover {
pub fn new() -> Self {
fn default_duration_func(steps: usize) -> u32 {
match steps {
x if x < 3 => 200,
x if x < 10 => 150,
_ => 100,
}
}
Self {
timer_token: None,
moving_direction: None,
auto_moved_screens: 0,
duration_func: default_duration_func,
}
}
pub fn with_duration_func(mut self, duration_func: fn(usize) -> u32) -> Self {
self.duration_func = duration_func;
self
}
/// Determines how long to wait between automatic movements.
/// Moves quicker with increasing number of screens moved.
/// Can be forced to be always the same (e.g. for animation purposes).
fn get_auto_move_duration(&self) -> Duration {
// Calculating duration from function
let ms_duration = (self.duration_func)(self.auto_moved_screens);
Duration::from_millis(ms_duration)
}
/// In which direction we are moving, if any
pub fn moving_direction(&self) -> Option<ButtonPos> {
self.moving_direction
}
// Whether we are currently moving.
pub fn is_moving(&self) -> bool {
self.moving_direction.is_some()
}
/// Whether we have done at least one automatic movement.
pub fn was_moving(&self) -> bool {
self.auto_moved_screens > 0
}
pub fn start_moving(&mut self, ctx: &mut EventCtx, button: ButtonPos) {
self.auto_moved_screens = 0;
self.moving_direction = Some(button);
self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration()));
}
pub fn stop_moving(&mut self) {
self.moving_direction = None;
self.timer_token = None;
}
}
impl Component for AutomaticMover {
type Msg = ButtonPos;
fn place(&mut self, bounds: Rect) -> Rect {
bounds
}
fn paint(&mut self) {}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Moving automatically only when we receive a TimerToken that we have
// requested before
if let Event::Timer(token) = event {
if self.timer_token == Some(token) && self.moving_direction.is_some() {
// Request new token and send the appropriate button trigger event
self.timer_token = Some(ctx.request_timer(self.get_auto_move_duration()));
self.auto_moved_screens += 1;
return self.moving_direction;
}
}
None
}
}
// DEBUG-ONLY SECTION BELOW
#[cfg(feature = "ui_debug")]
@ -443,3 +669,10 @@ impl<T: StringType> crate::trace::Trace for ButtonController<T> {
t.child("right_btn", &self.right_btn);
}
}
#[cfg(feature = "ui_debug")]
impl crate::trace::Trace for AutomaticMover {
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
t.component("AutomaticMover");
}
}

@ -19,6 +19,8 @@ pub struct ChangingTextLine<T> {
/// What to show in front of the text if it doesn't fit.
ellipsis: &'static str,
alignment: Alignment,
/// Whether to show the text completely aligned to the top of the bounds
text_at_the_top: bool,
}
impl<T> ChangingTextLine<T>
@ -33,6 +35,7 @@ where
show_content: true,
ellipsis: "...",
alignment,
text_at_the_top: false,
}
}
@ -50,6 +53,12 @@ where
self
}
/// Showing text at the very top
pub fn with_text_at_the_top(mut self) -> Self {
self.text_at_the_top = true;
self
}
/// Update the text to be displayed in the line.
pub fn update_text(&mut self, text: T) {
self.text = text;
@ -60,6 +69,11 @@ where
&self.text
}
/// Changing the current font
pub fn update_font(&mut self, font: Font) {
self.font = font;
}
/// Whether we should display the text content.
/// If not, the whole area (Pad) will still be cleared.
/// Is valid until this function is called again.
@ -76,7 +90,13 @@ where
/// Y coordinate of text baseline, is the same for all paints.
fn y_baseline(&self) -> i16 {
self.pad.area.y0 + self.font.line_height()
let y_coord = self.pad.area.y0 + self.font.line_height();
if self.text_at_the_top {
// Shifting the text up by 2 pixels.
y_coord - 2
} else {
y_coord
}
}
/// Whether the whole text can be painted in the available space

@ -30,6 +30,9 @@ where
page_counter: usize,
return_confirmed_index: bool,
show_scrollbar: bool,
/// Possibly enforcing the second button to be ignored after some time after
/// pressing the first button
ignore_second_button_ms: Option<u32>,
}
impl<F, T> Flow<F, T>
@ -55,6 +58,7 @@ where
page_counter: 0,
return_confirmed_index: false,
show_scrollbar: true,
ignore_second_button_ms: None,
}
}
@ -77,6 +81,12 @@ where
self
}
/// Ignoring the second button duration
pub fn with_ignore_second_button_ms(mut self, ignore_second_button_ms: u32) -> Self {
self.ignore_second_button_ms = Some(ignore_second_button_ms);
self
}
pub fn confirmed_index(&self) -> Option<usize> {
self.return_confirmed_index.then_some(self.page_counter)
}
@ -225,7 +235,14 @@ where
// We finally found how long is the first page, and can set its button layout.
self.current_page.place(content_area);
self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout()));
if let Some(ignore_ms) = self.ignore_second_button_ms {
self.buttons = Child::new(
ButtonController::new(self.current_page.btn_layout())
.with_ignore_btn_delay(ignore_ms),
);
} else {
self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout()));
}
self.pad.place(title_content_area);
self.buttons.place(button_area);

@ -84,6 +84,7 @@ where
current_page: usize,
page_count: usize,
title: Option<T>,
slim_arrows: bool,
}
// For `layout.rs`
@ -103,6 +104,7 @@ where
current_page: 0,
page_count: 1,
title: None,
slim_arrows: false,
}
}
}
@ -118,6 +120,12 @@ where
self
}
/// Using slim arrows instead of wide buttons.
pub fn with_slim_arrows(mut self) -> Self {
self.slim_arrows = true;
self
}
pub fn paint(&mut self) {
self.change_page(self.current_page);
self.formatted.paint();
@ -137,17 +145,29 @@ where
// On the last page showing only the narrow arrow, so the right
// button with possibly long text has enough space.
let btn_left = if self.has_prev_page() && !self.has_next_page() {
Some(ButtonDetails::up_arrow_icon())
if self.slim_arrows {
Some(ButtonDetails::left_arrow_icon())
} else {
Some(ButtonDetails::up_arrow_icon())
}
} else if self.has_prev_page() {
Some(ButtonDetails::up_arrow_icon_wide())
if self.slim_arrows {
Some(ButtonDetails::left_arrow_icon())
} else {
Some(ButtonDetails::up_arrow_icon_wide())
}
} else {
current.btn_left
};
// Middle button should be shown only on the last page, not to collide
// with the fat right button.
// with the possible fat right button.
let (btn_middle, btn_right) = if self.has_next_page() {
(None, Some(ButtonDetails::down_arrow_icon_wide()))
if self.slim_arrows {
(None, Some(ButtonDetails::right_arrow_icon()))
} else {
(None, Some(ButtonDetails::down_arrow_icon_wide()))
}
} else {
(current.btn_middle, current.btn_right)
};

@ -102,7 +102,7 @@ where
let mut outset = Insets::uniform(LABEL_OUTSET);
// the margin at top is bigger (caused by text-height vs line-height?)
// compensate by shrinking the outset
outset.top -= 1;
outset.top -= 2;
rect_fill(self.label.text_area().outset(outset), theme::BG);
self.label.paint();
}

@ -3,10 +3,13 @@ use crate::{
ui::{
component::{Child, Component, Event, EventCtx, Pad},
geometry::{Insets, Offset, Rect},
util::animation_disabled,
},
};
use super::super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos};
use super::super::{
constant, theme, AutomaticMover, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos,
};
const DEFAULT_ITEMS_DISTANCE: i16 = 10;
@ -79,6 +82,13 @@ where
/// Whether the middle selected item should be painted with
/// inverse colors - black on white.
inverse_selected_item: bool,
/// For moving through the items when holding left/right button
holding_mover: AutomaticMover,
/// For doing quick animations when changing the page counter.
animation_mover: AutomaticMover,
/// How many animated steps we should still do (positive for right, negative
/// for left).
animated_steps_to_do: i16,
}
impl<F, T, A> ChoicePage<F, T, A>
@ -89,16 +99,30 @@ where
pub fn new(choices: F) -> Self {
let initial_btn_layout = choices.get(0).0.btn_layout();
/// First move happens immediately, then in 35 ms intervals
fn animation_duration_func(steps: usize) -> u32 {
match steps {
0 => 0,
_ => 35,
}
}
Self {
choices,
pad: Pad::with_background(theme::BG),
buttons: Child::new(ButtonController::new(initial_btn_layout)),
buttons: Child::new(
ButtonController::new(initial_btn_layout)
.with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS),
),
page_counter: 0,
items_distance: DEFAULT_ITEMS_DISTANCE,
is_carousel: false,
show_incomplete: false,
show_only_one_item: false,
inverse_selected_item: false,
holding_mover: AutomaticMover::new(),
animation_mover: AutomaticMover::new().with_duration_func(animation_duration_func),
animated_steps_to_do: 0,
}
}
@ -107,7 +131,10 @@ where
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
self.page_counter = page_counter;
let initial_btn_layout = self.get_current_choice().0.btn_layout();
self.buttons = Child::new(ButtonController::new(initial_btn_layout));
self.buttons = Child::new(
ButtonController::new(initial_btn_layout)
.with_ignore_btn_delay(constant::IGNORE_OTHER_BTN_MS),
);
self
}
@ -156,9 +183,33 @@ where
}
/// Navigating to the chosen page index.
pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: usize) {
self.page_counter = page_counter;
self.update(ctx);
pub fn set_page_counter(
&mut self,
ctx: &mut EventCtx,
page_counter: usize,
do_animation: bool,
) {
// Either moving with animation or just jumping to the final position directly.
if do_animation && !animation_disabled() {
let diff = page_counter as i16 - self.page_counter as i16;
// When there would be a small number of animation frames (3 or less),
// animating in the opposite direction to make the animation longer.
self.animated_steps_to_do = match diff {
-3..=0 => diff + self.choices.count() as i16,
1..=3 => diff - self.choices.count() as i16,
_ => diff,
};
// Starting the movement immediately - either left or right.
let pos = if self.animated_steps_to_do > 0 {
ButtonPos::Right
} else {
ButtonPos::Left
};
self.animation_mover.start_moving(ctx, pos);
} else {
self.page_counter = page_counter;
self.update(ctx);
}
}
/// Display current, previous and next choices according to
@ -356,14 +407,66 @@ where
/// whether they are long-pressed, and painting them.
fn set_buttons(&mut self, ctx: &mut EventCtx) {
let btn_layout = self.get_current_choice().0.btn_layout();
self.buttons.mutate(ctx, |_ctx, buttons| {
self.buttons.mutate(ctx, |ctx, buttons| {
buttons.set(btn_layout);
// When user holds one of the buttons, highlighting it.
if let Some(btn_down) = self.holding_mover.moving_direction() {
buttons.highlight_button(ctx, btn_down);
}
ctx.request_paint();
});
}
pub fn choice_factory(&self) -> &F {
&self.choices
}
/// Go to the choice visually on the left.
fn move_left(&mut self, ctx: &mut EventCtx) {
if self.has_previous_choice() {
self.decrease_page_counter();
self.update(ctx);
} else if self.is_carousel {
self.page_counter_to_max();
self.update(ctx);
}
}
/// Go to the choice visually on the right.
fn move_right(&mut self, ctx: &mut EventCtx) {
if self.has_next_choice() {
self.increase_page_counter();
self.update(ctx);
} else if self.is_carousel {
self.page_counter_to_zero();
self.update(ctx);
}
}
/// Possibly doing an animation movement with the choice - either left or
/// right.
fn animation_event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<ButtonPos> {
if animation_disabled() {
return None;
}
// Stopping the movement if it is moving and there are no steps left
if self.animated_steps_to_do == 0 {
if self.animation_mover.is_moving() {
self.animation_mover.stop_moving();
}
return None;
}
let animation_result = self.animation_mover.event(ctx, event);
// When about to animate, decreasing the number of steps to do.
if animation_result.is_some() {
if self.animated_steps_to_do > 0 {
self.animated_steps_to_do -= 1;
} else {
self.animated_steps_to_do += 1;
}
}
animation_result
}
}
impl<F, T, A> Component for ChoicePage<F, T, A>
@ -381,32 +484,67 @@ where
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Possible animation movement when setting (randomizing) the page counter.
if let Some(animation_direction) = self.animation_event(ctx, event) {
match animation_direction {
ButtonPos::Left => self.move_left(ctx),
ButtonPos::Right => self.move_right(ctx),
_ => {}
}
return None;
}
// When animation is running, ignoring all user events
if self.animation_mover.is_moving() {
return None;
}
// Possible automatic movement when user is holding left or right button.
if let Some(auto_move_direction) = self.holding_mover.event(ctx, event) {
match auto_move_direction {
ButtonPos::Left => self.move_left(ctx),
ButtonPos::Right => self.move_right(ctx),
_ => {}
}
return None;
}
let button_event = self.buttons.event(ctx, event);
// Button was "triggered" - released. Doing the appropriate action.
// Possibly stopping or starting the automatic mover.
if let Some(moving_direction) = self.holding_mover.moving_direction() {
// Stopping the automatic movement when the released button is the same as the
// direction we were moving, or when the pressed button is the
// opposite one (user does middle-click).
if matches!(button_event, Some(ButtonControllerMsg::Triggered(pos)) if pos == moving_direction)
|| matches!(button_event, Some(ButtonControllerMsg::Pressed(pos)) if pos != moving_direction)
{
self.holding_mover.stop_moving();
// Ignoring the event when it already did some automatic movements. (Otherwise
// it would do one more movement.)
if self.holding_mover.was_moving() {
return None;
}
}
} else if let Some(ButtonControllerMsg::Pressed(pos)) = button_event {
// Starting the movement when left/right button is pressed.
if matches!(pos, ButtonPos::Left | ButtonPos::Right) {
self.holding_mover.start_moving(ctx, pos);
}
}
// There was a legitimate button event - doing some action
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
match pos {
ButtonPos::Left => {
if self.has_previous_choice() {
// Clicked BACK. Decrease the page counter.
self.decrease_page_counter();
self.update(ctx);
} else if self.is_carousel {
// In case of carousel going to the right end.
self.page_counter_to_max();
self.update(ctx);
}
// Clicked BACK. Decrease the page counter.
// In case of carousel going to the right end.
self.move_left(ctx);
}
ButtonPos::Right => {
if self.has_next_choice() {
// Clicked NEXT. Increase the page counter.
self.increase_page_counter();
self.update(ctx);
} else if self.is_carousel {
// In case of carousel going to the left end.
self.page_counter_to_zero();
self.update(ctx);
}
// Clicked NEXT. Increase the page counter.
// In case of carousel going to the left end.
self.move_right(ctx);
}
ButtonPos::Middle => {
// Clicked SELECT. Send current choice index
@ -415,7 +553,7 @@ where
}
}
};
// The middle button was "pressed", highlighting the current choice by color
// The middle button was pressed, highlighting the current choice by color
// inversion.
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {
self.inverse_selected_item = true;

@ -236,6 +236,7 @@ pub struct PassphraseEntry<T: StringType + Clone> {
choice_page: ChoicePage<ChoiceFactoryPassphrase, T, PassphraseAction>,
passphrase_dots: Child<ChangingTextLine<String<MAX_PASSPHRASE_LENGTH>>>,
show_plain_passphrase: bool,
show_last_digit: bool,
textbox: TextBox<MAX_PASSPHRASE_LENGTH>,
current_category: ChoiceCategory,
}
@ -251,6 +252,7 @@ where
.with_initial_page_counter(random_menu_position()),
passphrase_dots: Child::new(ChangingTextLine::center_mono(String::new())),
show_plain_passphrase: false,
show_last_digit: false,
textbox: TextBox::empty(),
current_category: ChoiceCategory::Menu,
}
@ -259,11 +261,20 @@ where
fn update_passphrase_dots(&mut self, ctx: &mut EventCtx) {
let text_to_show = if self.show_plain_passphrase {
String::from(self.passphrase())
} else if self.is_empty() {
String::from("")
} else {
// Showing asterisks and possibly the last digit.
let mut dots: String<MAX_PASSPHRASE_LENGTH> = String::new();
for _ in 0..self.textbox.len() {
unwrap!(dots.push_str("*"));
for _ in 0..self.textbox.len() - 1 {
unwrap!(dots.push('*'));
}
let last_char = if self.show_last_digit {
unwrap!(self.textbox.content().chars().last())
} else {
'*'
};
unwrap!(dots.push(last_char));
dots
};
self.passphrase_dots.mutate(ctx, |ctx, passphrase_dots| {
@ -312,8 +323,11 @@ where
/// Randomly choose an index in the current category
fn randomize_category_position(&mut self, ctx: &mut EventCtx) {
self.choice_page
.set_page_counter(ctx, random_category_position(&self.current_category));
self.choice_page.set_page_counter(
ctx,
random_category_position(&self.current_category),
true,
);
}
}
@ -332,10 +346,17 @@ where
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Any event when showing real passphrase should hide it
if self.show_plain_passphrase {
self.show_plain_passphrase = false;
self.update_passphrase_dots(ctx);
// Any non-timer event when showing real passphrase should hide it
// Same with showing last digit
if !matches!(event, Event::Timer(_)) {
if self.show_plain_passphrase {
self.show_plain_passphrase = false;
self.update_passphrase_dots(ctx);
}
if self.show_last_digit {
self.show_last_digit = false;
self.update_passphrase_dots(ctx);
}
}
if let Some(action) = self.choice_page.event(ctx, event) {
@ -373,6 +394,7 @@ where
}
PassphraseAction::Character(ch) if !self.is_full() => {
self.append_char(ctx, ch);
self.show_last_digit = true;
self.update_passphrase_dots(ctx);
self.randomize_category_position(ctx);
ctx.request_paint();

@ -3,7 +3,7 @@ use crate::{
trezorhal::random,
ui::{
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
display::Icon,
display::{Font, Icon},
geometry::Rect,
},
};
@ -80,8 +80,12 @@ impl<T: StringType + Clone> ChoiceFactory<T> for ChoiceFactoryPIN {
/// Component for entering a PIN.
pub struct PinEntry<T: StringType + Clone> {
choice_page: ChoicePage<ChoiceFactoryPIN, T, PinAction>,
header_line: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
pin_line: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
prompt: T,
subprompt: T,
/// Whether we already show the "real" prompt (not the warning).
showing_real_prompt: bool,
show_real_pin: bool,
show_last_digit: bool,
textbox: TextBox<MAX_PIN_LENGTH>,
@ -91,22 +95,45 @@ impl<T> PinEntry<T>
where
T: StringType + Clone,
{
pub fn new(subprompt: T) -> Self {
let pin_line_content = if !subprompt.as_ref().is_empty() {
String::from(subprompt.as_ref())
pub fn new(prompt: T, subprompt: T) -> Self {
// When subprompt is not empty, it means that the user has entered bad PIN
// before. In this case we show the warning together with the subprompt
// at the beginning. (WRONG PIN will be replaced by real prompt after
// any button click.)
let show_subprompt = !subprompt.as_ref().is_empty();
let (showing_real_prompt, header_line_content, pin_line_content) = if show_subprompt {
(
false,
String::from("WRONG PIN"),
String::from(subprompt.as_ref()),
)
} else {
String::from(EMPTY_PIN_STR)
(
true,
String::from(prompt.as_ref()),
String::from(EMPTY_PIN_STR),
)
};
let mut pin_line = ChangingTextLine::center_bold(pin_line_content).without_ellipsis();
if show_subprompt {
pin_line.update_font(Font::NORMAL);
}
Self {
// Starting at a random digit.
choice_page: ChoicePage::new(ChoiceFactoryPIN)
.with_initial_page_counter(get_random_digit_position())
.with_carousel(true),
pin_line: Child::new(
ChangingTextLine::center_bold(pin_line_content).without_ellipsis(),
header_line: Child::new(
ChangingTextLine::center_bold(header_line_content)
.without_ellipsis()
.with_text_at_the_top(),
),
pin_line: Child::new(pin_line),
subprompt,
prompt,
showing_real_prompt,
show_real_pin: false,
show_last_digit: false,
textbox: TextBox::empty(),
@ -122,7 +149,10 @@ where
/// Show updated content in the changing line.
/// Many possibilities, according to the PIN state.
fn update_pin_line(&mut self, ctx: &mut EventCtx) {
let mut used_font = Font::BOLD;
let pin_line_text = if self.is_empty() && !self.subprompt.as_ref().is_empty() {
// Showing the subprompt in NORMAL font
used_font = Font::NORMAL;
String::from(self.subprompt.as_ref())
} else if self.is_empty() {
String::from(EMPTY_PIN_STR)
@ -144,11 +174,20 @@ where
};
self.pin_line.mutate(ctx, |ctx, pin_line| {
pin_line.update_font(used_font);
pin_line.update_text(pin_line_text);
pin_line.request_complete_repaint(ctx);
});
}
/// Showing the real prompt instead of WRONG PIN
fn show_prompt(&mut self, ctx: &mut EventCtx) {
self.header_line.mutate(ctx, |ctx, header_line| {
header_line.update_text(String::from(self.prompt.as_ref()));
header_line.request_complete_repaint(ctx);
});
}
pub fn pin(&self) -> &str {
self.textbox.content()
}
@ -169,23 +208,36 @@ where
type Msg = CancelConfirmMsg;
fn place(&mut self, bounds: Rect) -> Rect {
let header_height = self.header_line.inner().needed_height();
let (header_area, rest) = bounds.split_top(header_height);
let pin_height = self.pin_line.inner().needed_height();
let (pin_area, choice_area) = bounds.split_top(pin_height);
let (pin_area, choice_area) = rest.split_top(pin_height);
self.header_line.place(header_area);
self.pin_line.place(pin_area);
self.choice_page.place(choice_area);
bounds
}
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
// Any event when showing real PIN should hide it
// Any non-timer event when showing real PIN should hide it
// Same with showing last digit
if self.show_real_pin {
self.show_real_pin = false;
self.update(ctx)
if !matches!(event, Event::Timer(_)) {
if self.show_real_pin {
self.show_real_pin = false;
self.update(ctx)
}
if self.show_last_digit {
self.show_last_digit = false;
self.update(ctx)
}
}
if self.show_last_digit {
self.show_last_digit = false;
self.update(ctx)
// Any button event will show the "real" prompt
if !self.showing_real_prompt {
if let Event::Button(_) = event {
self.show_prompt(ctx);
self.showing_real_prompt = true;
}
}
match self.choice_page.event(ctx, event) {
@ -204,7 +256,7 @@ where
self.textbox.append(ctx, ch);
// Choosing random digit to be shown next
self.choice_page
.set_page_counter(ctx, get_random_digit_position());
.set_page_counter(ctx, get_random_digit_position(), true);
self.show_last_digit = true;
self.update(ctx);
None
@ -214,6 +266,7 @@ where
}
fn paint(&mut self) {
self.header_line.paint();
self.pin_line.paint();
self.choice_page.paint();
}

@ -13,7 +13,7 @@ pub use button::{
Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonPos,
ButtonStyle, ButtonStyleSheet,
};
pub use button_controller::{ButtonController, ButtonControllerMsg};
pub use button_controller::{AutomaticMover, ButtonController, ButtonControllerMsg};
pub use common_messages::CancelConfirmMsg;
pub use error::ErrorScreen;
pub use hold_to_confirm::{HoldToConfirm, HoldToConfirmMsg};

@ -27,8 +27,12 @@ where
T: Component,
U: StringType + Clone,
{
pub fn new(content: T) -> Self {
let btn_layout = ButtonLayout::cancel_armed_info("CONFIRM".into());
pub fn new(content: T, cancel_button: Option<U>, button: U) -> Self {
let btn_layout = if let Some(cancel_text) = cancel_button {
ButtonLayout::text_armed_info(cancel_text, button)
} else {
ButtonLayout::cancel_armed_info(button)
};
Self {
content: Child::new(content),
buttons: Child::new(ButtonController::new(btn_layout)),

@ -22,3 +22,5 @@ pub const fn screen() -> Rect {
Rect::from_top_left_and_size(Point::zero(), SIZE)
}
pub const SCREEN: Rect = screen();
pub const IGNORE_OTHER_BTN_MS: u32 = 200;

@ -22,7 +22,7 @@ use crate::{
},
TextStyle,
},
ComponentExt, FormattedText, LineBreaking, Timeout,
ComponentExt, FormattedText, Timeout,
},
display, geometry,
layout::{
@ -254,15 +254,7 @@ fn content_in_button_page<T: Component + Paginate + MaybeTrace + 'static>(
hold: bool,
) -> Result<Obj, Error> {
// Left button - icon, text or nothing.
let cancel_btn = if let Some(verb_cancel) = verb_cancel {
if !verb_cancel.is_empty() {
Some(ButtonDetails::text(verb_cancel))
} else {
Some(ButtonDetails::cancel_icon())
}
} else {
None
};
let cancel_btn = verb_cancel.map(ButtonDetails::from_text_possible_icon);
// Right button - text or nothing.
// Optional HoldToConfirm
@ -407,8 +399,7 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs:
let ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL)
.text_normal("By continuing you agree to Trezor Company's terms and conditions.".into())
.newline()
.newline()
.next_page()
.text_normal("More info at".into())
.newline()
.text_bold("trezor.io/tos".into());
@ -559,44 +550,61 @@ extern "C" fn new_confirm_modify_output(n_args: usize, args: *const Obj, kwargs:
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_output(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
extern "C" fn new_confirm_output_address(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let address_label: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_label)?.try_into()?;
let amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?;
let address_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_address_title)?.try_into()?;
let amount_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_title)?.try_into()?;
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
let get_page = move |page_index| {
// Showing two screens - the recipient address and summary confirmation
match page_index {
0 => {
// RECIPIENT + address
let btn_layout = ButtonLayout::cancel_none_text("CONTINUE".into());
let btn_actions = ButtonActions::cancel_none_next();
// Not putting hyphens in the address.
// Potentially adding address label in different font.
let mut ops = OpTextLayout::new(theme::TEXT_MONO)
.line_breaking(LineBreaking::BreakWordsNoHyphen);
if !address_label.is_empty() {
ops = ops.text_normal(address_label.clone()).newline();
}
ops = ops.text_mono(address.clone());
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(address_title.clone())
}
1 => {
// AMOUNT + amount
let btn_layout = ButtonLayout::up_arrow_none_text("CONFIRM".into());
let btn_actions = ButtonActions::prev_none_confirm();
let ops = OpTextLayout::new(theme::TEXT_MONO).text_mono(amount.clone());
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(amount_title.clone())
assert!(page_index == 0);
// RECIPIENT + address
let btn_layout = ButtonLayout::cancel_none_text("CONTINUE".into());
let btn_actions = ButtonActions::cancel_none_confirm();
// Not putting hyphens in the address.
// Potentially adding address label in different font.
let mut ops = OpTextLayout::new(theme::TEXT_MONO_DATA);
if !address_label.is_empty() {
// NOTE: need to explicitly turn off the chunkification before rendering the
// address label (for some reason it does not help to turn it off after
// rendering the chunks)
if chunkify {
ops = ops.chunkify_text(None);
}
_ => unreachable!(),
ops = ops.text_normal(address_label.clone()).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());
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(address_title.clone())
};
let pages = FlowPages::new(get_page, 2);
let pages = FlowPages::new(get_page, 1);
let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_output_amount(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount)?.try_into()?;
let amount_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_amount_title)?.try_into()?;
let get_page = move |page_index| {
assert!(page_index == 0);
// AMOUNT + amount
let btn_layout = ButtonLayout::up_arrow_none_text("CONFIRM".into());
let btn_actions = ButtonActions::cancel_none_confirm();
let ops = OpTextLayout::new(theme::TEXT_MONO).text_mono(amount.clone());
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(amount_title.clone())
};
let pages = FlowPages::new(get_page, 1);
let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into())
@ -687,20 +695,95 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_ethereum_tx(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = |_args: &[Obj], kwargs: &Map| {
let recipient: StrBuffer = kwargs.get(Qstr::MP_QSTR_recipient)?.try_into()?;
let total_amount: StrBuffer = kwargs.get(Qstr::MP_QSTR_total_amount)?.try_into()?;
let maximum_fee: StrBuffer = kwargs.get(Qstr::MP_QSTR_maximum_fee)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let get_page = move |page_index| {
match page_index {
0 => {
// RECIPIENT
let btn_layout = ButtonLayout::cancel_none_text("CONTINUE".into());
let btn_actions = ButtonActions::cancel_none_next();
let ops = OpTextLayout::new(theme::TEXT_MONO_DATA).text_mono(recipient.clone());
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title("RECIPIENT".into())
}
1 => {
// Total amount + fee
let btn_layout = ButtonLayout::up_arrow_armed_info("CONFIRM".into());
let btn_actions = ButtonActions::prev_confirm_next();
let ops = OpTextLayout::new(theme::TEXT_MONO)
.text_mono(total_amount.clone())
.newline()
.newline_half()
.text_bold("Maximum fee:".into())
.newline()
.text_mono(maximum_fee.clone());
let formatted = FormattedText::new(ops);
Page::new(btn_layout, btn_actions, formatted).with_title("Amount:".into())
}
2 => {
// Fee information
let btn_layout = ButtonLayout::arrow_none_none();
let btn_actions = ButtonActions::prev_none_none();
let mut ops = OpTextLayout::new(theme::TEXT_MONO);
for item in unwrap!(IterBuf::new().try_iterate(items)) {
let [key, value]: [Obj; 2] = unwrap!(iter_into_array(item));
if !ops.is_empty() {
// Each key-value pair is on its own page
ops = ops.next_page();
}
ops = ops
.text_bold(unwrap!(key.try_into()))
.newline()
.text_mono(unwrap!(value.try_into()));
}
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted)
.with_title("FEE INFORMATION".into())
.with_slim_arrows()
}
_ => unreachable!(),
}
};
let pages = FlowPages::new(get_page, 3);
let obj = LayoutObj::new(Flow::new(pages).with_scrollbar(false))?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_data)?.try_into()?;
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
let get_page = move |page_index| {
assert!(page_index == 0);
let btn_layout = ButtonLayout::cancel_armed_info("CONFIRM".into());
let btn_actions = ButtonActions::cancel_confirm_info();
let ops = OpTextLayout::new(theme::TEXT_MONO)
.line_breaking(LineBreaking::BreakWordsNoHyphen)
.text_mono(address.clone());
let formatted = FormattedText::new(ops);
let style = if chunkify {
// Chunkifying the address into smaller pieces when requested
theme::TEXT_MONO_ADDRESS_CHUNKS
} else {
theme::TEXT_MONO_DATA
};
let ops = OpTextLayout::new(style).text_mono(address.clone());
let formatted = FormattedText::new(ops).vertically_centered();
Page::new(btn_layout, btn_actions, formatted).with_title(title.clone())
};
let pages = FlowPages::new(get_page, 1);
@ -798,7 +881,14 @@ extern "C" fn tutorial(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj
let pages = FlowPages::new(get_page, PAGE_COUNT);
let obj = LayoutObj::new(Flow::new(pages).with_scrollbar(false))?;
// Setting the ignore-second-button to mimic all the Choice pages, to teach user
// that they should really press both buttons at the same time to achieve
// middle-click.
let obj = LayoutObj::new(
Flow::new(pages)
.with_scrollbar(false)
.with_ignore_second_button_ms(constant::IGNORE_OTHER_BTN_MS),
)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -1028,15 +1118,16 @@ extern "C" fn new_show_passphrase() -> Obj {
unsafe { util::try_or_raise(block) }
}
extern "C" fn new_show_mismatch() -> Obj {
let block = move || {
extern "C" fn new_show_mismatch(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let get_page = move |page_index| {
assert!(page_index == 0);
let btn_layout = ButtonLayout::arrow_none_text("QUIT".into());
let btn_actions = ButtonActions::cancel_none_confirm();
let ops = OpTextLayout::<StrBuffer>::new(theme::TEXT_NORMAL)
.text_bold("ADDRESS MISMATCH?".into())
.text_bold(title.clone())
.newline()
.newline_half()
.text_normal("Please contact Trezor support at".into())
@ -1050,19 +1141,24 @@ extern "C" fn new_show_mismatch() -> Obj {
let obj = LayoutObj::new(Flow::new(pages))?;
Ok(obj.into())
};
unsafe { util::try_or_raise(block) }
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
let verb_cancel: Option<StrBuffer> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let mut paragraphs = ParagraphVecShort::new();
for para in IterBuf::new().try_iterate(items)? {
let [font, text]: [Obj; 2] = iter_into_array(para)?;
let style: &TextStyle = theme::textstyle_number_bold_or_mono(font.try_into()?);
let style: &TextStyle = theme::textstyle_number(font.try_into()?);
let text: StrBuffer = text.try_into()?;
paragraphs.add(Paragraph::new(style, text));
if paragraphs.is_full() {
@ -1074,6 +1170,8 @@ extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mu
title,
ShowMore::<Paragraphs<ParagraphVecShort<StrBuffer>>, StrBuffer>::new(
paragraphs.into_paragraphs(),
verb_cancel,
button,
),
))?;
Ok(obj.into())
@ -1081,6 +1179,32 @@ extern "C" fn new_confirm_with_info(n_args: usize, args: *const Obj, kwargs: *mu
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_more(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let button: StrBuffer = kwargs.get(Qstr::MP_QSTR_button)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let mut paragraphs = ParagraphVecLong::new();
for para in IterBuf::new().try_iterate(items)? {
let [font, text]: [Obj; 2] = iter_into_array(para)?;
let style: &TextStyle = theme::textstyle_number(font.try_into()?);
let text: StrBuffer = text.try_into()?;
paragraphs.add(Paragraph::new(style, text));
}
content_in_button_page(
title,
paragraphs.into_paragraphs(),
button,
Some("<".into()),
false,
)
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_confirm_coinjoin(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let max_rounds: StrBuffer = kwargs.get(Qstr::MP_QSTR_max_rounds)?.try_into()?;
@ -1112,8 +1236,7 @@ extern "C" fn new_request_pin(n_args: usize, args: *const Obj, kwargs: *mut Map)
let prompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_prompt)?.try_into()?;
let subprompt: StrBuffer = kwargs.get(Qstr::MP_QSTR_subprompt)?.try_into()?;
let obj =
LayoutObj::new(Frame::new(prompt, PinEntry::new(subprompt)).with_title_centered())?;
let obj = LayoutObj::new(PinEntry::new(prompt, subprompt))?;
Ok(obj.into())
};
@ -1483,6 +1606,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// data: str,
/// description: str | None, # unused on TR
/// extra: str | None, # unused on TR
/// chunkify: bool = False,
/// ) -> object:
/// """Confirm address."""
Qstr::MP_QSTR_confirm_address => obj_fn_kw!(0, new_confirm_address).as_obj(),
@ -1551,16 +1675,23 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Decrease or increase amount for given address."""
Qstr::MP_QSTR_confirm_modify_output => obj_fn_kw!(0, new_confirm_modify_output).as_obj(),
/// def confirm_output(
/// def confirm_output_address(
/// *,
/// address: str,
/// address_label: str,
/// amount: str,
/// address_title: str,
/// chunkify: bool = False,
/// ) -> object:
/// """Confirm output address."""
Qstr::MP_QSTR_confirm_output_address => obj_fn_kw!(0, new_confirm_output_address).as_obj(),
/// def confirm_output_amount(
/// *,
/// amount: str,
/// amount_title: str,
/// ) -> object:
/// """Confirm output."""
Qstr::MP_QSTR_confirm_output => obj_fn_kw!(0, new_confirm_output).as_obj(),
/// """Confirm output amount."""
Qstr::MP_QSTR_confirm_output_amount => obj_fn_kw!(0, new_confirm_output_amount).as_obj(),
/// def confirm_total(
/// *,
@ -1574,6 +1705,16 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Confirm summary of a transaction."""
Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(),
/// def confirm_ethereum_tx(
/// *,
/// recipient: str,
/// total_amount: str,
/// maximum_fee: str,
/// items: Iterable[Tuple[str, str]],
/// ) -> object:
/// """Confirm details about Ethereum transaction."""
Qstr::MP_QSTR_confirm_ethereum_tx => obj_fn_kw!(0, new_confirm_ethereum_tx).as_obj(),
/// def tutorial() -> object:
/// """Show user how to interact with the device."""
Qstr::MP_QSTR_tutorial => obj_fn_kw!(0, tutorial).as_obj(),
@ -1633,21 +1774,32 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Show passphrase on host dialog."""
Qstr::MP_QSTR_show_passphrase => obj_fn_0!(new_show_passphrase).as_obj(),
/// def show_mismatch() -> object:
/// def show_mismatch(*, title: str) -> object:
/// """Warning modal, receiving address mismatch."""
Qstr::MP_QSTR_show_mismatch => obj_fn_0!(new_show_mismatch).as_obj(),
Qstr::MP_QSTR_show_mismatch => obj_fn_kw!(0, new_show_mismatch).as_obj(),
/// def confirm_with_info(
/// *,
/// title: str,
/// button: str, # unused on TR
/// button: str,
/// info_button: str, # unused on TR
/// items: Iterable[Tuple[int, str]],
/// verb_cancel: str | None = None,
/// ) -> object:
/// """Confirm given items but with third button. Always single page
/// without scrolling."""
Qstr::MP_QSTR_confirm_with_info => obj_fn_kw!(0, new_confirm_with_info).as_obj(),
/// def confirm_more(
/// *,
/// title: str,
/// button: str,
/// items: Iterable[tuple[int, str]],
/// ) -> object:
/// """Confirm long content with the possibility to go back from any page.
/// Meant to be used with confirm_with_info."""
Qstr::MP_QSTR_confirm_more => obj_fn_kw!(0, new_confirm_more).as_obj(),
/// def confirm_coinjoin(
/// *,
/// max_rounds: str,

@ -1,5 +1,8 @@
use crate::ui::{
component::{text::TextStyle, LineBreaking, PageBreaking},
component::{
text::{layout::Chunks, TextStyle},
LineBreaking, PageBreaking,
},
display::{toif::Icon, Color, Font},
geometry::Offset,
};
@ -35,15 +38,22 @@ pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, FG, FG)
/// Mono data text does not have hyphens
pub const TEXT_MONO_DATA: TextStyle =
TEXT_MONO.with_line_breaking(LineBreaking::BreakWordsNoHyphen);
pub const TEXT_MONO_ADDRESS_CHUNKS: TextStyle = TEXT_MONO_DATA
.with_chunks(MONO_CHUNKS)
.with_line_spacing(2)
.with_ellipsis_icon(ICON_NEXT_PAGE, -2);
// Chunks for this model, with accounting for some wider characters in MONO font
pub const MONO_CHUNKS: Chunks = Chunks::new(4, 4).with_wider_chars("mMwW");
/// Convert Python-side numeric id to a `TextStyle`.
/// Using only BOLD or MONO fonts.
pub fn textstyle_number_bold_or_mono(num: i32) -> &'static TextStyle {
pub fn textstyle_number(num: i32) -> &'static TextStyle {
let font = Font::from_i32(-num);
match font {
Some(Font::BOLD) => &TEXT_BOLD,
Some(Font::DEMIBOLD) => &TEXT_BOLD,
_ => &TEXT_MONO,
Some(Font::NORMAL) => &TEXT_NORMAL,
_ => &TEXT_MONO_DATA,
}
}

@ -30,8 +30,10 @@ where
T: StringType,
{
pub fn new(
qr_title: T,
qr_address: T,
case_sensitive: bool,
details_title: T,
account: Option<T>,
path: Option<T>,
) -> Result<Self, Error>
@ -53,14 +55,14 @@ where
let result = Self {
qr_code: Frame::left_aligned(
theme::label_title(),
"RECEIVE ADDRESS".into(),
qr_title,
Qr::new(qr_address, case_sensitive)?.with_border(7),
)
.with_cancel_button()
.with_border(theme::borders_horizontal_scroll()),
details: Frame::left_aligned(
theme::label_title(),
"RECEIVING TO".into(),
details_title,
para.into_paragraphs(),
)
.with_cancel_button()

@ -119,19 +119,25 @@ where
}
}
pub fn with_text(mut self, style: &'static TextStyle, text: T) -> Self {
if !text.as_ref().is_empty() {
self.paragraphs
.inner_mut()
.add(Paragraph::new(style, text).centered());
pub fn with_paragraph(mut self, para: Paragraph<T>) -> Self {
if !para.content().as_ref().is_empty() {
self.paragraphs.inner_mut().add(para);
}
self
}
pub fn with_text(self, style: &'static TextStyle, text: T) -> Self {
self.with_paragraph(Paragraph::new(style, text).centered())
}
pub fn with_description(self, description: T) -> Self {
self.with_text(&theme::TEXT_NORMAL_OFF_WHITE, description)
}
pub fn with_value(self, value: T) -> Self {
self.with_text(&theme::TEXT_MONO, value)
}
pub fn new_shares(lines: [T; 4], controls: U) -> Self {
let [l0, l1, l2, l3] = lines;
Self {

@ -133,6 +133,15 @@ impl CancelHold {
})
}
pub fn with_cancel_arrow() -> FixedHeightBar<Self> {
theme::button_bar(Self {
cancel: Some(Button::with_icon(theme::ICON_UP).into_child()),
hold: Button::with_text("HOLD TO CONFIRM")
.styled(theme::button_confirm())
.into_child(),
})
}
pub fn without_cancel() -> FixedHeightBar<Self> {
theme::button_bar(Self {
cancel: None,

@ -404,6 +404,15 @@ where
}
}
pub fn with_cancel_arrow(content: T, background: Color) -> Self {
let buttons = CancelHold::with_cancel_arrow();
Self {
inner: SwipePage::new(content, buttons, background),
loader: Loader::new(),
pad: Pad::with_background(background),
}
}
pub fn with_swipe_left(mut self) -> Self {
self.inner = self.inner.with_swipe_left();
self

@ -496,6 +496,7 @@ struct ConfirmBlobParams {
verb_cancel: Option<StrBuffer>,
info_button: bool,
hold: bool,
chunkify: bool,
}
impl ConfirmBlobParams {
@ -517,6 +518,7 @@ impl ConfirmBlobParams {
verb_cancel,
info_button: false,
hold,
chunkify: false,
}
}
@ -535,6 +537,11 @@ impl ConfirmBlobParams {
self
}
fn with_chunkify(mut self, chunkify: bool) -> Self {
self.chunkify = chunkify;
self
}
fn into_layout(self) -> Result<Obj, Error> {
let paragraphs = ConfirmBlob {
description: self.description.unwrap_or_else(StrBuffer::empty),
@ -542,7 +549,11 @@ impl ConfirmBlobParams {
data: self.data.try_into()?,
description_font: &theme::TEXT_NORMAL,
extra_font: &theme::TEXT_DEMIBOLD,
data_font: &theme::TEXT_MONO,
data_font: if self.chunkify {
&theme::TEXT_MONO_ADDRESS_CHUNKS
} else {
&theme::TEXT_MONO
},
}
.into_paragraphs();
@ -611,6 +622,21 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
let extra: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_extra)?.try_into_option()?;
let data: Obj = kwargs.get(Qstr::MP_QSTR_data)?;
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
let data_style = if chunkify {
// Longer addresses have smaller x_offset so they fit even with scrollbar
// (as they will be shown on more than one page)
const FITS_ON_ONE_PAGE: usize = 16 * 4;
let address: StrBuffer = data.try_into()?;
if address.len() <= FITS_ON_ONE_PAGE {
&theme::TEXT_MONO_ADDRESS_CHUNKS
} else {
&theme::TEXT_MONO_ADDRESS_CHUNKS_SMALLER_X_OFFSET
}
} else {
&theme::TEXT_MONO
};
let paragraphs = ConfirmBlob {
description: description.unwrap_or_else(StrBuffer::empty),
@ -618,7 +644,7 @@ extern "C" fn new_confirm_address(n_args: usize, args: *const Obj, kwargs: *mut
data: data.try_into()?,
description_font: &theme::TEXT_NORMAL,
extra_font: &theme::TEXT_DEMIBOLD,
data_font: &theme::TEXT_MONO,
data_font: data_style,
}
.into_paragraphs();
@ -730,6 +756,8 @@ extern "C" fn new_confirm_reset_device(n_args: usize, args: *const Obj, kwargs:
extern "C" fn new_show_address_details(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let qr_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_qr_title)?.try_into()?;
let details_title: StrBuffer = kwargs.get(Qstr::MP_QSTR_details_title)?.try_into()?;
let address: StrBuffer = kwargs.get(Qstr::MP_QSTR_address)?.try_into()?;
let case_sensitive: bool = kwargs.get(Qstr::MP_QSTR_case_sensitive)?.try_into()?;
let account: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_account)?.try_into_option()?;
@ -737,7 +765,14 @@ extern "C" fn new_show_address_details(n_args: usize, args: *const Obj, kwargs:
let xpubs: Obj = kwargs.get(Qstr::MP_QSTR_xpubs)?;
let mut ad = AddressDetails::new(address, case_sensitive, account, path)?;
let mut ad = AddressDetails::new(
qr_title,
address,
case_sensitive,
details_title,
account,
path,
)?;
for i in IterBuf::new().try_iterate(xpubs)? {
let [xtitle, text]: [StrBuffer; 2] = iter_into_array(i)?;
@ -750,25 +785,19 @@ extern "C" fn new_show_address_details(n_args: usize, args: *const Obj, kwargs:
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_spending_details(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
extern "C" fn new_show_info_with_cancel(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_title, "INFORMATION".into())?;
let account: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_account)?.try_into_option()?;
let fee_rate: Option<StrBuffer> = kwargs.get(Qstr::MP_QSTR_fee_rate)?.try_into_option()?;
let fee_rate_title: StrBuffer =
kwargs.get_or(Qstr::MP_QSTR_fee_rate_title, "Fee rate:".into())?;
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let mut paragraphs = ParagraphVecShort::new();
if let Some(a) = account {
paragraphs.add(Paragraph::new(
&theme::TEXT_NORMAL,
"Sending from account:".into(),
));
paragraphs.add(Paragraph::new(&theme::TEXT_MONO, a));
}
if let Some(f) = fee_rate {
paragraphs.add(Paragraph::new(&theme::TEXT_NORMAL, fee_rate_title));
paragraphs.add(Paragraph::new(&theme::TEXT_MONO, f));
for para in IterBuf::new().try_iterate(items)? {
let [key, value]: [Obj; 2] = iter_into_array(para)?;
let key: StrBuffer = key.try_into()?;
let value: StrBuffer = value.try_into()?;
paragraphs.add(Paragraph::new(&theme::TEXT_NORMAL, key));
paragraphs.add(Paragraph::new(&theme::TEXT_MONO, value));
}
let obj = LayoutObj::new(
@ -802,10 +831,12 @@ extern "C" fn new_confirm_value(n_args: usize, args: *const Obj, kwargs: *mut Ma
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
let hold: bool = kwargs.get_or(Qstr::MP_QSTR_hold, false)?;
let chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
ConfirmBlobParams::new(title, value, description, verb, verb_cancel, hold)
.with_subtitle(subtitle)
.with_info_button(info_button)
.with_chunkify(chunkify)
.into_layout()
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@ -816,6 +847,7 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let items: Obj = kwargs.get(Qstr::MP_QSTR_items)?;
let info_button: bool = kwargs.get_or(Qstr::MP_QSTR_info_button, false)?;
let cancel_arrow: bool = kwargs.get_or(Qstr::MP_QSTR_cancel_arrow, false)?;
let mut paragraphs = ParagraphVecShort::new();
@ -824,8 +856,11 @@ extern "C" fn new_confirm_total(n_args: usize, args: *const Obj, kwargs: *mut Ma
paragraphs.add(Paragraph::new(&theme::TEXT_NORMAL, label));
paragraphs.add(Paragraph::new(&theme::TEXT_MONO, value));
}
let mut page = SwipeHoldPage::new(paragraphs.into_paragraphs(), theme::BG);
let mut page = if cancel_arrow {
SwipeHoldPage::with_cancel_arrow(paragraphs.into_paragraphs(), theme::BG)
} else {
SwipeHoldPage::new(paragraphs.into_paragraphs(), theme::BG)
};
if info_button {
page = page.with_swipe_left();
}
@ -912,6 +947,7 @@ fn new_show_modal(
button_style: ButtonStyleSheet,
) -> Result<Obj, Error> {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let value: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_value, StrBuffer::empty())?;
let description: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_description, StrBuffer::empty())?;
let button: StrBuffer = kwargs.get_or(Qstr::MP_QSTR_button, "CONTINUE".into())?;
let allow_cancel: bool = kwargs.get_or(Qstr::MP_QSTR_allow_cancel, true)?;
@ -921,7 +957,12 @@ fn new_show_modal(
let obj = if no_buttons && time_ms == 0 {
// No buttons and no timer, used when we only want to draw the dialog once and
// then throw away the layout object.
LayoutObj::new(IconDialog::new(icon, title, Empty).with_description(description))?.into()
LayoutObj::new(
IconDialog::new(icon, title, Empty)
.with_value(value)
.with_description(description),
)?
.into()
} else if no_buttons && time_ms > 0 {
// Timeout, no buttons.
LayoutObj::new(
@ -930,6 +971,7 @@ fn new_show_modal(
title,
Timeout::new(time_ms).map(|_| Some(CancelConfirmMsg::Confirmed)),
)
.with_value(value)
.with_description(description),
)?
.into()
@ -945,6 +987,7 @@ fn new_show_modal(
false,
),
)
.with_value(value)
.with_description(description),
)?
.into()
@ -958,6 +1001,7 @@ fn new_show_modal(
(matches!(msg, ButtonMsg::Clicked)).then(|| CancelConfirmMsg::Confirmed)
})),
)
.with_value(value)
.with_description(description),
)?
.into()
@ -1053,9 +1097,9 @@ extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_mismatch() -> Obj {
let block = move || {
let title: StrBuffer = "Address mismatch?".into();
extern "C" fn new_show_mismatch(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
let block = move |_args: &[Obj], kwargs: &Map| {
let title: StrBuffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
let description: StrBuffer = "Please contact Trezor support at".into();
let url: StrBuffer = "trezor.io/support".into();
let button = "QUIT";
@ -1077,13 +1121,20 @@ extern "C" fn new_show_mismatch() -> Obj {
true,
),
)
.with_description(description)
.with_paragraph(
Paragraph::new(&theme::TEXT_NORMAL, description)
.centered()
.with_bottom_padding(
theme::TEXT_NORMAL.text_font.text_height()
- theme::TEXT_DEMIBOLD.text_font.text_height(),
),
)
.with_text(&theme::TEXT_DEMIBOLD, url),
)?;
Ok(obj.into())
};
unsafe { util::try_or_raise(block) }
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
}
extern "C" fn new_show_simple(n_args: usize, args: *const Obj, kwargs: *mut Map) -> Obj {
@ -1657,6 +1708,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// data: str | bytes,
/// description: str | None,
/// extra: str | None,
/// chunkify: bool = False,
/// ) -> object:
/// """Confirm address. Similar to `confirm_blob` but has corner info button
/// and allows left swipe which does the same thing as the button."""
@ -1682,8 +1734,10 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// def show_address_details(
/// *,
/// qr_title: str,
/// address: str,
/// case_sensitive: bool,
/// details_title: str,
/// account: str | None,
/// path: str | None,
/// xpubs: list[tuple[str, str]],
@ -1691,15 +1745,13 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Show address details - QR code, account, path, cosigner xpubs."""
Qstr::MP_QSTR_show_address_details => obj_fn_kw!(0, new_show_address_details).as_obj(),
/// def show_spending_details(
/// def show_info_with_cancel(
/// *,
/// title: str = "INFORMATION",
/// account: str | None,
/// fee_rate: str | None,
/// fee_rate_title: str = "Fee rate:",
/// title: str,
/// items: Iterable[Tuple[str, str]],
/// ) -> object:
/// """Show metadata when for outgoing transaction."""
Qstr::MP_QSTR_show_spending_details => obj_fn_kw!(0, new_show_spending_details).as_obj(),
Qstr::MP_QSTR_show_info_with_cancel => obj_fn_kw!(0, new_show_info_with_cancel).as_obj(),
/// def confirm_value(
/// *,
@ -1711,6 +1763,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// verb_cancel: str | None = None,
/// info_button: bool = False,
/// hold: bool = False,
/// chunkify: bool = False,
/// ) -> object:
/// """Confirm value. Merge of confirm_total and confirm_output."""
Qstr::MP_QSTR_confirm_value => obj_fn_kw!(0, new_confirm_value).as_obj(),
@ -1720,6 +1773,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// title: str,
/// items: list[tuple[str, str]],
/// info_button: bool = False,
/// cancel_arrow: bool = False,
/// ) -> object:
/// """Transaction summary. Always hold to confirm."""
Qstr::MP_QSTR_confirm_total => obj_fn_kw!(0, new_confirm_total).as_obj(),
@ -1773,6 +1827,7 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// *,
/// title: str,
/// button: str = "CONTINUE",
/// value: str = "",
/// description: str = "",
/// allow_cancel: bool = False,
/// time_ms: int = 0,
@ -1802,9 +1857,9 @@ pub static mp_module_trezorui2: Module = obj_module! {
/// """Info modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(),
/// def show_mismatch() -> object:
/// def show_mismatch(*, title: str) -> object:
/// """Warning modal, receiving address mismatch."""
Qstr::MP_QSTR_show_mismatch => obj_fn_0!(new_show_mismatch).as_obj(),
Qstr::MP_QSTR_show_mismatch => obj_fn_kw!(0, new_show_mismatch).as_obj(),
/// def show_simple(
/// *,

@ -2,7 +2,7 @@ use crate::{
time::Duration,
ui::{
component::{
text::{LineBreaking, PageBreaking, TextStyle},
text::{layout::Chunks, LineBreaking, PageBreaking, TextStyle},
FixedHeightBar,
},
display::{Color, Font, Icon},
@ -517,6 +517,18 @@ pub const TEXT_MONO: TextStyle = TextStyle::new(Font::MONO, FG, BG, GREY_LIGHT,
.with_page_breaking(PageBreaking::CutAndInsertEllipsisBoth)
.with_ellipsis_icon(ICON_PAGE_NEXT, 0)
.with_prev_page_icon(ICON_PAGE_PREV, 0);
/// Makes sure that the displayed text (usually address) will get divided into
/// smaller chunks.
pub const TEXT_MONO_ADDRESS_CHUNKS: TextStyle = TEXT_MONO
.with_chunks(Chunks::new(4, 9))
.with_line_spacing(5);
/// Smaller horizontal chunk offset, used e.g. for long Cardano addresses.
/// Also moving the next page ellipsis to the left (as there is a space on the
/// left).
pub const TEXT_MONO_ADDRESS_CHUNKS_SMALLER_X_OFFSET: TextStyle = TEXT_MONO
.with_chunks(Chunks::new(4, 7))
.with_line_spacing(5)
.with_ellipsis_icon(ICON_PAGE_NEXT, -12);
/// Convert Python-side numeric id to a `TextStyle`.
pub fn textstyle_number(num: i32) -> &'static TextStyle {

@ -53,6 +53,11 @@
#define STAY_IN_BOOTLOADER_FLAG 0x0FC35A96
// from linker script
extern uint8_t firmware_header_start;
extern uint8_t ccmram_start;
extern uint8_t ccmram_end;
void __attribute__((noreturn)) trezor_shutdown(void);
void __attribute__((noreturn))

@ -23,5 +23,6 @@
void mpu_config_off(void);
void mpu_config_bootloader(void);
void mpu_config_firmware(void);
void mpu_config_prodtest(void);
#endif

@ -0,0 +1,44 @@
/*
* This file is part of the Trezor project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TREZORHAL_OPTIGA_H
#define TREZORHAL_OPTIGA_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define OPTIGA_DEVICE_CERT_INDEX 1
#define OPTIGA_DEVICE_ECC_KEY_INDEX 0
#define OPTIGA_COMMAND_ERROR_OFFSET 0x100
// Error code 7: Access conditions not satisfied
#define OPTIGA_ERR_ACCESS_COND_NOT_SAT (OPTIGA_COMMAND_ERROR_OFFSET + 0x07)
int optiga_sign(uint8_t index, const uint8_t *digest, size_t digest_size,
uint8_t *signature, size_t max_sig_size, size_t *sig_size);
bool optiga_cert_size(uint8_t index, size_t *cert_size);
bool optiga_read_cert(uint8_t index, uint8_t *cert, size_t max_cert_size,
size_t *cert_size);
bool optiga_random_buffer(uint8_t *dest, size_t size);
#endif

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

Loading…
Cancel
Save