mirror of
https://github.com/trezor/trezor-firmware.git
synced 2024-12-22 14:28:07 +00:00
Merge branch 'master' into release/23.09
This commit is contained in:
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": {}
|
||||
}
|
||||
}
|
||||
|
17
common/models.json
Normal file
17
common/models.json
Normal file
@ -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.
|
||||
"""
|
||||
|
1
core/.changelog.d/2161.changed
Normal file
1
core/.changelog.d/2161.changed
Normal file
@ -0,0 +1 @@
|
||||
Changed design of the path warning screen (model T only).
|
1
core/.changelog.d/2937.changed
Normal file
1
core/.changelog.d/2937.changed
Normal file
@ -0,0 +1 @@
|
||||
Introduce multiple account warning to BTC send flow.
|
1
core/.changelog.d/2937.changed.1
Normal file
1
core/.changelog.d/2937.changed.1
Normal file
@ -0,0 +1 @@
|
||||
Introduce multisig warning to BTC receive flow.
|
1
core/.changelog.d/3047.added
Normal file
1
core/.changelog.d/3047.added
Normal file
@ -0,0 +1 @@
|
||||
QR code display when exporting XPUBs.
|
2
core/.changelog.d/3205.added
Normal file
2
core/.changelog.d/3205.added
Normal file
@ -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.
|
1
core/.changelog.d/3218.fixed
Normal file
1
core/.changelog.d/3218.fixed
Normal file
@ -0,0 +1 @@
|
||||
Fix more info button on shamir recovery screen.
|
1
core/.changelog.d/3237.added
Normal file
1
core/.changelog.d/3237.added
Normal file
@ -0,0 +1 @@
|
||||
Add support for address chunkification in Receive and Sign flow.
|
1
core/.changelog.d/3255.added
Normal file
1
core/.changelog.d/3255.added
Normal file
@ -0,0 +1 @@
|
||||
Implement device authentication for Model R.
|
1
core/.changelog.d/3256.added
Normal file
1
core/.changelog.d/3256.added
Normal file
@ -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
|
2
core/embed/boardloader/.changelog.d/3205.added
Normal file
2
core/embed/boardloader/.changelog.d/3205.added
Normal file
@ -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
|
||||
|
2
core/embed/bootloader/.changelog.d/3205.added
Normal file
2
core/embed/bootloader/.changelog.d/3205.added
Normal file
@ -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
|
||||
|
||||
|
2
core/embed/bootloader_ci/.changelog.d/3205.added
Normal file
2
core/embed/bootloader_ci/.changelog.d/3205.added
Normal file
@ -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
|
||||
|
134
core/embed/extmod/modtrezorcrypto/modtrezorcrypto-optiga.h
Normal file
134
core/embed/extmod/modtrezorcrypto/modtrezorcrypto-optiga.h
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
500
core/embed/prodtest/optiga_prodtest.c
Normal file
500
core/embed/prodtest/optiga_prodtest.c
Normal file
@ -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");
|
||||
}
|
51
core/embed/prodtest/optiga_prodtest.h
Normal file
51
core/embed/prodtest/optiga_prodtest.h
Normal file
@ -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
|
102
core/embed/prodtest/prodtest_common.c
Normal file
102
core/embed/prodtest/prodtest_common.c
Normal file
@ -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;
|
||||
}
|
34
core/embed/prodtest/prodtest_common.h
Normal file
34
core/embed/prodtest/prodtest_common.h
Normal file
@ -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 chunkify: bool = kwargs.get_or(Qstr::MP_QSTR_chunkify, false)?;
|
||||
|
||||
let get_page = move |page_index| {
|
||||
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);
|
||||
}
|
||||
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, 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| {
|
||||
// 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())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
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, 2);
|
||||
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
|
||||
|
44
core/embed/trezorhal/optiga.h
Normal file
44
core/embed/trezorhal/optiga.h
Normal file
@ -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…
Reference in New Issue
Block a user