diff --git a/core/SConscript.firmware b/core/SConscript.firmware index bb47726ec..d9fc29ec4 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -117,6 +117,7 @@ SOURCE_MOD += [ 'vendor/trezor-crypto/rfc6979.c', 'vendor/trezor-crypto/ripemd160.c', 'vendor/trezor-crypto/secp256k1.c', + 'vendor/trezor-crypto/segwit_addr.c', 'vendor/trezor-crypto/sha2.c', 'vendor/trezor-crypto/sha3.c', 'vendor/trezor-crypto/shamir.c', diff --git a/core/SConscript.unix b/core/SConscript.unix index e31cb51d1..e017df9a5 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -116,6 +116,7 @@ SOURCE_MOD += [ 'vendor/trezor-crypto/rfc6979.c', 'vendor/trezor-crypto/ripemd160.c', 'vendor/trezor-crypto/secp256k1.c', + 'vendor/trezor-crypto/segwit_addr.c', 'vendor/trezor-crypto/sha2.c', 'vendor/trezor-crypto/sha3.c', 'vendor/trezor-crypto/shamir.c', diff --git a/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-bech32.h b/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-bech32.h new file mode 100644 index 000000000..9f754bf2c --- /dev/null +++ b/core/embed/extmod/modtrezorcrypto/modtrezorcrypto-bech32.h @@ -0,0 +1,84 @@ +/* + * 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 . + */ + +#include "embed/extmod/trezorobj.h" +#include "py/objstr.h" + +#include "segwit_addr.h" + +/// package: trezorcrypto.bech32 + +/// def decode( +/// bech: str, +/// max_bech_len: int = 90, +/// ) -> tuple[str, list[int], Encoding]: +/// """ +/// Decode a Bech32 or Bech32m string +/// """ +STATIC mp_obj_t mod_trezorcrypto_bech32_decode(size_t n_args, + const mp_obj_t *args) { + mp_buffer_info_t bech = {0}; + mp_get_buffer_raise(args[0], &bech, MP_BUFFER_READ); + + uint32_t max_bech_len = 90; + if (n_args > 1) { + max_bech_len = trezor_obj_get_uint(args[1]); + } + + if (bech.len > max_bech_len) { + mp_raise_ValueError(NULL); + } + + uint8_t data[bech.len - 8]; + char hrp[BECH32_MAX_HRP_LEN + 1] = {0}; + size_t data_len = 0; + bech32_encoding enc = bech32_decode(hrp, data, &data_len, bech.buf); + if (enc == BECH32_ENCODING_NONE) { + mp_raise_ValueError(NULL); + } + + mp_obj_list_t *data_list = MP_OBJ_TO_PTR(mp_obj_new_list(data_len, NULL)); + for (size_t i = 0; i < data_len; ++i) { + data_list->items[i] = mp_obj_new_int_from_uint(data[i]); + } + + mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR(mp_obj_new_tuple(3, NULL)); + tuple->items[0] = mp_obj_new_str(hrp, strlen(hrp)); + tuple->items[1] = data_list; + tuple->items[2] = MP_OBJ_NEW_SMALL_INT(enc); + return MP_OBJ_FROM_PTR(tuple); +} + +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_bech32_decode_obj, + 1, 2, + mod_trezorcrypto_bech32_decode); + +STATIC const mp_rom_map_elem_t mod_trezorcrypto_bech32_globals_table[] = { + {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_bech32)}, + {MP_ROM_QSTR(MP_QSTR_decode), + MP_ROM_PTR(&mod_trezorcrypto_bech32_decode_obj)}, + {MP_ROM_QSTR(MP_QSTR_BECH32), MP_ROM_INT(BECH32_ENCODING_BECH32)}, + {MP_ROM_QSTR(MP_QSTR_BECH32M), MP_ROM_INT(BECH32_ENCODING_BECH32M)}}; +STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_bech32_globals, + mod_trezorcrypto_bech32_globals_table); + +STATIC const mp_obj_module_t mod_trezorcrypto_bech32_module = { + .base = {&mp_type_module}, + .globals = (mp_obj_dict_t *)&mod_trezorcrypto_bech32_globals, +}; diff --git a/core/embed/extmod/modtrezorcrypto/modtrezorcrypto.c b/core/embed/extmod/modtrezorcrypto/modtrezorcrypto.c index 2b849ea0d..7c00473f2 100644 --- a/core/embed/extmod/modtrezorcrypto/modtrezorcrypto.c +++ b/core/embed/extmod/modtrezorcrypto/modtrezorcrypto.c @@ -37,6 +37,7 @@ static void wrapped_ui_wait_callback(uint32_t current, uint32_t total) { } #include "modtrezorcrypto-aes.h" +#include "modtrezorcrypto-bech32.h" #include "modtrezorcrypto-bip32.h" #ifdef USE_SECP256K1_ZKP #include "modtrezorcrypto-bip340.h" @@ -72,6 +73,7 @@ static void wrapped_ui_wait_callback(uint32_t current, uint32_t total) { STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = { {MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorcrypto)}, {MP_ROM_QSTR(MP_QSTR_aes), MP_ROM_PTR(&mod_trezorcrypto_AES_type)}, + {MP_ROM_QSTR(MP_QSTR_bech32), MP_ROM_PTR(&mod_trezorcrypto_bech32_module)}, {MP_ROM_QSTR(MP_QSTR_bip32), MP_ROM_PTR(&mod_trezorcrypto_bip32_module)}, {MP_ROM_QSTR(MP_QSTR_bip39), MP_ROM_PTR(&mod_trezorcrypto_bip39_module)}, {MP_ROM_QSTR(MP_QSTR_blake256), diff --git a/core/mocks/generated/trezorcrypto/bech32.pyi b/core/mocks/generated/trezorcrypto/bech32.pyi new file mode 100644 index 000000000..330384697 --- /dev/null +++ b/core/mocks/generated/trezorcrypto/bech32.pyi @@ -0,0 +1,11 @@ +from typing import * + + +# extmod/modtrezorcrypto/modtrezorcrypto-bech32.h +def decode( + bech: str, + max_bech_len: int = 90, +) -> tuple[str, list[int], Encoding]: + """ + Decode a Bech32 or Bech32m string + """ diff --git a/core/src/apps/cardano/helpers/bech32.py b/core/src/apps/cardano/helpers/bech32.py index 3b6321fb2..22a65a7ef 100644 --- a/core/src/apps/cardano/helpers/bech32.py +++ b/core/src/apps/cardano/helpers/bech32.py @@ -35,8 +35,6 @@ def get_hrp(bech: str) -> str: def decode(hrp: str, bech: str) -> bytes: decoded_hrp, data, spec = bech32.bech32_decode(bech, 130) - if data is None: - raise ValueError if decoded_hrp != hrp: raise ValueError if spec != bech32.Encoding.BECH32: diff --git a/core/src/apps/zcash/unified_addresses.py b/core/src/apps/zcash/unified_addresses.py index 48e443c99..b820cca1a 100644 --- a/core/src/apps/zcash/unified_addresses.py +++ b/core/src/apps/zcash/unified_addresses.py @@ -83,8 +83,9 @@ def encode(receivers: dict[Typecode, bytes], coin: CoinInfo) -> str: def decode(addr_str: str, coin: CoinInfo) -> dict[int, bytes]: - (hrp, data, encoding) = bech32_decode(addr_str, max_bech_len=1000) - if (hrp, data, encoding) == (None, None, None): + try: + hrp, data, encoding = bech32_decode(addr_str, 1000) + except ValueError: raise DataError("Bech32m decoding failed.") assert hrp is not None # to satisfy typecheckers assert data is not None # to satisfy typecheckers diff --git a/core/src/trezor/crypto/bech32.py b/core/src/trezor/crypto/bech32.py index 1c299513e..71fb8a5bd 100644 --- a/core/src/trezor/crypto/bech32.py +++ b/core/src/trezor/crypto/bech32.py @@ -20,8 +20,12 @@ """Reference implementation for Bech32/Bech32m and segwit addresses.""" +from trezorcrypto import bech32 from typing import TYPE_CHECKING +bech32_decode = bech32.decode + + if TYPE_CHECKING: from enum import IntEnum from typing import Sequence, TypeVar @@ -32,7 +36,6 @@ if TYPE_CHECKING: # usage: OptionalTuple[int, list[int]] is either (None, None) or (someint, somelist) # but not (None, somelist) OptionalTuple2 = tuple[None, None] | tuple[A, B] - OptionalTuple3 = tuple[None, None, None] | tuple[A, B, C] else: IntEnum = object @@ -65,16 +68,6 @@ def bech32_hrp_expand(hrp: str) -> list[int]: return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] -def bech32_verify_checksum(hrp: str, data: list[int]) -> Encoding | None: - """Verify a checksum given HRP and converted data characters.""" - const = bech32_polymod(bech32_hrp_expand(hrp) + data) - if const == 1: - return Encoding.BECH32 - if const == BECH32M_CONST: - return Encoding.BECH32M - return None - - def bech32_create_checksum(hrp: str, data: list[int], spec: Encoding) -> list[int]: """Compute the checksum values given HRP and data.""" values = bech32_hrp_expand(hrp) + data @@ -89,28 +82,6 @@ def bech32_encode(hrp: str, data: list[int], spec: Encoding) -> str: return hrp + "1" + "".join([CHARSET[d] for d in combined]) -def bech32_decode( - bech: str, max_bech_len: int = 90 -) -> OptionalTuple3[str, list[int], Encoding]: - """Validate a Bech32/Bech32m string, and determine HRP and data.""" - if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or ( - bech.lower() != bech and bech.upper() != bech - ): - return (None, None, None) - bech = bech.lower() - pos = bech.rfind("1") - if pos < 1 or pos + 7 > len(bech) or len(bech) > max_bech_len: - return (None, None, None) - if not all(x in CHARSET for x in bech[pos + 1 :]): - return (None, None, None) - hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos + 1 :]] - spec = bech32_verify_checksum(hrp, data) - if spec is None: - return (None, None, None) - return (hrp, data[:-6], spec) - - def convertbits( data: Sequence[int], frombits: int, tobits: int, arbitrary_input: bool = True ) -> list[int]: @@ -156,17 +127,13 @@ def convertbits( def decode(hrp: str, addr: str) -> OptionalTuple2[int, bytes]: """Decode a segwit address.""" - hrpgot, data, spec = bech32_decode(addr) - # the following two lines are strictly not required - # but they make typecheckers happy - if data is None: - return (None, None) - if hrpgot != hrp: - return (None, None) try: + hrpgot, data, spec = bech32_decode(addr) decoded = bytes(convertbits(data[1:], 5, 8, False)) except ValueError: return (None, None) + if hrpgot != hrp: + return (None, None) if not 2 <= len(decoded) <= 40: return (None, None) if data[0] > 16: diff --git a/core/tests/test_trezor.crypto.bech32.py b/core/tests/test_trezor.crypto.bech32.py index 954c0e067..9a9d11fc2 100644 --- a/core/tests/test_trezor.crypto.bech32.py +++ b/core/tests/test_trezor.crypto.bech32.py @@ -153,10 +153,8 @@ class TestCryptoBech32(unittest.TestCase): def test_invalid_checksum(self): """Test validation of invalid checksums.""" for test in INVALID_CHECKSUM: - hrp, data, spec = bech32.bech32_decode(test) - self.assertIsNone(hrp) - self.assertIsNone(data) - self.assertIsNone(spec) + with self.assertRaises(ValueError): + bech32.bech32_decode(test) def test_valid_address(self): """Test whether valid addresses decode to the correct output.""" diff --git a/crypto/segwit_addr.c b/crypto/segwit_addr.c index bd8c775ae..446c0d6e6 100644 --- a/crypto/segwit_addr.c +++ b/crypto/segwit_addr.c @@ -95,7 +95,7 @@ bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const size_t input_len = strlen(input); size_t hrp_len = 0; int have_lower = 0, have_upper = 0; - if (input_len < 8 || input_len > 90) { + if (input_len < 8) { return BECH32_ENCODING_NONE; } *data_len = 0; @@ -103,7 +103,7 @@ bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const ++(*data_len); } hrp_len = input_len - (1 + *data_len); - if (1 + *data_len >= input_len || *data_len < 6) { + if (1 + *data_len >= input_len || *data_len < 6 || hrp_len > BECH32_MAX_HRP_LEN) { return BECH32_ENCODING_NONE; } *(data_len) -= 6; @@ -192,6 +192,7 @@ int segwit_addr_decode(int* witver, uint8_t* witdata, size_t* witdata_len, const uint8_t data[84] = {0}; char hrp_actual[84] = {0}; size_t data_len = 0; + if (strlen(addr) > 90) return 0; bech32_encoding enc = bech32_decode(hrp_actual, data, &data_len, addr); if (enc == BECH32_ENCODING_NONE) return 0; if (data_len == 0 || data_len > 65) return 0; diff --git a/crypto/segwit_addr.h b/crypto/segwit_addr.h index 511970ab8..844874ccf 100644 --- a/crypto/segwit_addr.h +++ b/crypto/segwit_addr.h @@ -24,6 +24,9 @@ #include +// The maximum length of the Bech32 human-readable part according to BIP-173. +#define BECH32_MAX_HRP_LEN 83 + /** Encode a SegWit address * @@ -92,7 +95,7 @@ int bech32_encode( /** Decode a Bech32 or Bech32m string * - * Out: hrp: Pointer to a buffer of size strlen(input) - 6. Will be + * Out: hrp: Pointer to a buffer of size BECH32_MAX_HRP_LEN + 1. Will be * updated to contain the null-terminated human readable part. * data: Pointer to a buffer of size strlen(input) - 8 that will * hold the encoded 5-bit data values.