diff --git a/.gitignore b/.gitignore index 1d8824a32..d174f8bb2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ _attic/ build/ build-docker/ emu.config +venv/ +cmake-build-* +.idea diff --git a/SConscript.firmware b/SConscript.firmware index 9eb7fde8d..5b6550e73 100644 --- a/SConscript.firmware +++ b/SConscript.firmware @@ -27,6 +27,7 @@ CPPDEFINES_MOD += [ 'RAND_PLATFORM_INDEPENDENT', ('USE_KECCAK', '1'), ('USE_ETHEREUM', '1'), + ('USE_MONERO', '1'), ('USE_CARDANO', '1'), ('USE_NEM', '1'), ] @@ -63,6 +64,11 @@ SOURCE_MOD += [ 'vendor/trezor-crypto/ed25519-donna/ed25519-keccak.c', 'vendor/trezor-crypto/ed25519-donna/ed25519-sha3.c', 'vendor/trezor-crypto/ed25519-donna/modm-donna-32bit.c', + 'vendor/trezor-crypto/monero/base58.c', + 'vendor/trezor-crypto/monero/crypto.c', + 'vendor/trezor-crypto/monero/serialize.c', + 'vendor/trezor-crypto/monero/range_proof.c', + 'vendor/trezor-crypto/monero/xmr.c', 'vendor/trezor-crypto/groestl.c', 'vendor/trezor-crypto/hasher.c', 'vendor/trezor-crypto/hmac.c', diff --git a/SConscript.unix b/SConscript.unix index 237275fea..0d5670bac 100644 --- a/SConscript.unix +++ b/SConscript.unix @@ -25,6 +25,7 @@ CPPDEFINES_MOD += [ 'AES_192', ('USE_KECCAK', '1'), ('USE_ETHEREUM', '1'), + ('USE_MONERO', '1'), ('USE_CARDANO', '1'), ('USE_NEM', '1'), ] @@ -60,6 +61,11 @@ SOURCE_MOD += [ 'vendor/trezor-crypto/ed25519-donna/ed25519-keccak.c', 'vendor/trezor-crypto/ed25519-donna/ed25519-sha3.c', 'vendor/trezor-crypto/ed25519-donna/modm-donna-32bit.c', + 'vendor/trezor-crypto/monero/base58.c', + 'vendor/trezor-crypto/monero/crypto.c', + 'vendor/trezor-crypto/monero/serialize.c', + 'vendor/trezor-crypto/monero/range_proof.c', + 'vendor/trezor-crypto/monero/xmr.c', 'vendor/trezor-crypto/groestl.c', 'vendor/trezor-crypto/hasher.c', 'vendor/trezor-crypto/hmac.c', diff --git a/embed/extmod/modtrezorcrypto/modtrezorcrypto-monero.h b/embed/extmod/modtrezorcrypto/modtrezorcrypto-monero.h new file mode 100644 index 000000000..52e789be0 --- /dev/null +++ b/embed/extmod/modtrezorcrypto/modtrezorcrypto-monero.h @@ -0,0 +1,1037 @@ +/* + * 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 "py/objstr.h" +#include "py/objint.h" +#include "py/mpz.h" + +#include "monero/monero.h" +#define RSIG_SIZE 6176 + +typedef struct _mp_obj_hasher_t { + mp_obj_base_t base; + Hasher h; +} mp_obj_hasher_t; + +typedef struct _mp_obj_ge25519_t { + mp_obj_base_t base; + ge25519 p; +} mp_obj_ge25519_t; + +typedef struct _mp_obj_bignum256modm_t { + mp_obj_base_t base; + bignum256modm p; +} mp_obj_bignum256modm_t; + +typedef union { + xmr_range_sig_t r; + unsigned char d[RSIG_SIZE]; +} rsig_union; + + +// +// Helpers +// + +STATIC const mp_obj_type_t mod_trezorcrypto_monero_ge25519_type; +STATIC const mp_obj_type_t mod_trezorcrypto_monero_bignum256modm_type; +STATIC const mp_obj_type_t mod_trezorcrypto_monero_hasher_type; + + +static uint64_t mp_obj_uint64_get_checked(mp_const_obj_t self_in) { +#if MICROPY_LONGINT_IMPL != MICROPY_LONGINT_IMPL_MPZ +# error "MPZ supported only" +#endif + + if (MP_OBJ_IS_SMALL_INT(self_in)) { + return MP_OBJ_SMALL_INT_VALUE(self_in); + } else { + byte buff[8]; + uint64_t res = 0; + mp_obj_t * o = MP_OBJ_TO_PTR(self_in); + + mp_obj_int_to_bytes_impl(o, true, 8, buff); + for (int i = 0; i<8; i++){ + res <<= i > 0 ? 8 : 0; + res |= (uint64_t)(buff[i] & 0xff); + } + return res; + } +} + +static uint64_t mp_obj_get_uint64(mp_const_obj_t arg) { + if (arg == mp_const_false) { + return 0; + } else if (arg == mp_const_true) { + return 1; + } else if (MP_OBJ_IS_SMALL_INT(arg)) { + return MP_OBJ_SMALL_INT_VALUE(arg); + } else if (MP_OBJ_IS_TYPE(arg, &mp_type_int)) { + return mp_obj_uint64_get_checked(arg); + } else { + if (MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_TERSE) { + mp_raise_TypeError("can't convert to int"); + } else { + nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_TypeError, + "can't convert %s to int", mp_obj_get_type_str(arg))); + } + } +} + +STATIC mp_obj_t mp_obj_new_scalar(){ + mp_obj_bignum256modm_t *o = m_new_obj(mp_obj_bignum256modm_t); + o->base.type = &mod_trezorcrypto_monero_bignum256modm_type; + set256_modm(o->p, 0); + return MP_OBJ_FROM_PTR(o); +} + +STATIC mp_obj_t mp_obj_new_ge25519(){ + mp_obj_ge25519_t *o = m_new_obj(mp_obj_ge25519_t); + o->base.type = &mod_trezorcrypto_monero_ge25519_type; + ge25519_set_neutral(&o->p); + return MP_OBJ_FROM_PTR(o); +} + +STATIC mp_obj_t mp_obj_from_scalar(const bignum256modm in){ + mp_obj_bignum256modm_t *o = m_new_obj(mp_obj_bignum256modm_t); + o->base.type = &mod_trezorcrypto_monero_bignum256modm_type; + memcpy(&o->p, in, sizeof(bignum256modm)); + return MP_OBJ_FROM_PTR(o); +} + +STATIC mp_obj_t mp_obj_from_ge25519(const ge25519 * in){ + mp_obj_ge25519_t *o = m_new_obj(mp_obj_ge25519_t); + o->base.type = &mod_trezorcrypto_monero_ge25519_type; + memcpy(&o->p, in, sizeof(ge25519)); + return MP_OBJ_FROM_PTR(o); +} + +STATIC void mp_unpack_ge25519(ge25519 * r, const mp_obj_t arg){ + mp_buffer_info_t buff; + mp_get_buffer_raise(arg, &buff, MP_BUFFER_READ); + if (buff.len != 32) { + mp_raise_ValueError("Invalid length of the EC point"); + } + + const int res = ge25519_unpack_vartime(r, buff.buf); + if (res != 1){ + mp_raise_ValueError("Point decoding error"); + } +} + +STATIC void mp_unpack_scalar(bignum256modm r, const mp_obj_t arg){ + mp_buffer_info_t buff; + mp_get_buffer_raise(arg, &buff, MP_BUFFER_READ); + if (buff.len < 32 || buff.len > 64) { + mp_raise_ValueError("Invalid length of secret key"); + } + expand256_modm(r, buff.buf, buff.len); +} + +#define MP_OBJ_IS_GE25519(o) MP_OBJ_IS_TYPE((o), &mod_trezorcrypto_monero_ge25519_type) +#define MP_OBJ_IS_SCALAR(o) MP_OBJ_IS_TYPE((o), &mod_trezorcrypto_monero_bignum256modm_type) +#define MP_OBJ_PTR_MPC_GE25519(o) ((const mp_obj_ge25519_t*) (o)) +#define MP_OBJ_PTR_MPC_SCALAR(o) ((const mp_obj_bignum256modm_t*) (o)) +#define MP_OBJ_PTR_MP_GE25519(o) ((mp_obj_ge25519_t*) (o)) +#define MP_OBJ_PTR_MP_SCALAR(o) ((mp_obj_bignum256modm_t*) (o)) +#define MP_OBJ_C_GE25519(o) (MP_OBJ_PTR_MPC_GE25519(o)->p) +#define MP_OBJ_GE25519(o) (MP_OBJ_PTR_MP_GE25519(o)->p) +#define MP_OBJ_C_SCALAR(o) (MP_OBJ_PTR_MPC_SCALAR(o)->p) +#define MP_OBJ_SCALAR(o) (MP_OBJ_PTR_MP_SCALAR(o)->p) + +STATIC inline void assert_ge25519(const mp_obj_t o){ + if (!MP_OBJ_IS_GE25519(o)){ + mp_raise_ValueError("ge25519 expected"); + } +} + +STATIC inline void assert_scalar(const mp_obj_t o){ + if (!MP_OBJ_IS_SCALAR(o)){ + mp_raise_ValueError("scalar expected"); + } +} + +// +// Constructors +// + + +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + mp_arg_check_num(n_args, n_kw, 0, 1, false); + mp_obj_ge25519_t *o = m_new_obj(mp_obj_ge25519_t); + o->base.type = type; + + if (n_args == 0) { + ge25519_set_neutral(&o->p); + } else if (n_args == 1 && MP_OBJ_IS_GE25519(args[0])) { + ge25519_copy(&o->p, &MP_OBJ_C_GE25519(args[0])); + } else if (n_args == 1 && MP_OBJ_IS_STR_OR_BYTES(args[0])) { + mp_unpack_ge25519(&o->p, args[0]); + } else { + mp_raise_ValueError("Invalid ge25519 constructor"); + } + + return MP_OBJ_FROM_PTR(o); +} + +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519___del__(mp_obj_t self) { + mp_obj_ge25519_t *o = MP_OBJ_TO_PTR(self); + memzero(&(o->p), sizeof(ge25519)); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_ge25519___del___obj, mod_trezorcrypto_monero_ge25519___del__); + +STATIC mp_obj_t mod_trezorcrypto_monero_bignum256modm_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + mp_arg_check_num(n_args, n_kw, 0, 1, false); + mp_obj_bignum256modm_t *o = m_new_obj(mp_obj_bignum256modm_t); + o->base.type = type; + + if (n_args == 0) { + set256_modm(o->p, 0); + } else if (n_args == 1 && MP_OBJ_IS_SCALAR(args[0])) { + copy256_modm(o->p, MP_OBJ_C_SCALAR(args[0])); + } else if (n_args == 1 && MP_OBJ_IS_STR_OR_BYTES(args[0])) { + mp_unpack_scalar(o->p, args[0]); + } else if (n_args == 1 && mp_obj_is_integer(args[0])) { + uint64_t v = mp_obj_get_uint64(args[0]); + set256_modm(o->p, v); + } else { + mp_raise_ValueError("Invalid scalar constructor"); + } + + return MP_OBJ_FROM_PTR(o); +} + +STATIC mp_obj_t mod_trezorcrypto_monero_bignum256modm___del__(mp_obj_t self) { + mp_obj_bignum256modm_t *o = MP_OBJ_TO_PTR(self); + memzero(o->p, sizeof(bignum256modm)); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_bignum256modm___del___obj, mod_trezorcrypto_monero_bignum256modm___del__); + + +STATIC mp_obj_t mod_trezorcrypto_monero_hasher_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) { + mp_arg_check_num(n_args, n_kw, 0, 1, false); + mp_obj_hasher_t *o = m_new_obj(mp_obj_hasher_t); + o->base.type = type; + xmr_hasher_init(&(o->h)); + + if (n_args == 1 && MP_OBJ_IS_STR_OR_BYTES(args[0])) { + mp_buffer_info_t buff; + mp_get_buffer_raise(args[0], &buff, MP_BUFFER_READ); + xmr_hasher_update(&o->h, buff.buf, buff.len); + } + + return MP_OBJ_FROM_PTR(o); +} + +STATIC mp_obj_t mod_trezorcrypto_monero_hasher___del__(mp_obj_t self) { + mp_obj_hasher_t *o = MP_OBJ_TO_PTR(self); + memzero(&(o->h), sizeof(Hasher)); + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_hasher___del___obj, mod_trezorcrypto_monero_hasher___del__); + + +// +// Scalar defs +// + +// init256_modm_r +STATIC mp_obj_t mod_trezorcrypto_monero_init256_modm(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 2 ? 0 : -1; + assert_scalar(res); + + if (n_args == 0) { + set256_modm(MP_OBJ_SCALAR(res), 0); + } else if (n_args > 0 && MP_OBJ_IS_SCALAR(args[1+off])) { + copy256_modm(MP_OBJ_SCALAR(res), MP_OBJ_C_SCALAR(args[1+off])); + } else if (n_args > 0 && MP_OBJ_IS_STR_OR_BYTES(args[1+off])) { + mp_unpack_scalar(MP_OBJ_SCALAR(res), args[1+off]); + } else if (n_args > 0 && mp_obj_is_integer(args[1+off])) { + uint64_t v = mp_obj_get_uint64(args[1+off]); + set256_modm(MP_OBJ_SCALAR(res), v); + } else { + mp_raise_ValueError("Invalid scalar def"); + } + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_init256_modm_obj, 0, 2, mod_trezorcrypto_monero_init256_modm); + +//int check256_modm +STATIC mp_obj_t mod_trezorcrypto_monero_check256_modm(const mp_obj_t arg){ + assert_scalar(arg); + if (check256_modm(MP_OBJ_C_SCALAR(arg)) != 1){ + mp_raise_ValueError("Ed25519 scalar invalid"); + } + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_check256_modm_obj, mod_trezorcrypto_monero_check256_modm); + +//int iszero256_modm +STATIC mp_obj_t mod_trezorcrypto_monero_iszero256_modm(const mp_obj_t arg){ + assert_scalar(arg); + const int r = iszero256_modm(MP_OBJ_C_SCALAR(arg)); + return mp_obj_new_int(r); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_iszero256_modm_obj, mod_trezorcrypto_monero_iszero256_modm); + +//int eq256_modm +STATIC mp_obj_t mod_trezorcrypto_monero_eq256_modm(const mp_obj_t a, const mp_obj_t b){ + assert_scalar(a); + assert_scalar(b); + int r = eq256_modm(MP_OBJ_C_SCALAR(a), MP_OBJ_C_SCALAR(b)); + return MP_OBJ_NEW_SMALL_INT(r); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_monero_eq256_modm_obj, mod_trezorcrypto_monero_eq256_modm); + +//int get256_modm_r +STATIC mp_obj_t mod_trezorcrypto_monero_get256_modm(const mp_obj_t arg){ + assert_scalar(arg); + uint64_t v; + if (!get256_modm(&v, MP_OBJ_C_SCALAR(arg))){ + mp_raise_ValueError("Ed25519 scalar too big"); + } + return mp_obj_new_int_from_ull(v); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_get256_modm_obj, mod_trezorcrypto_monero_get256_modm); + +// barrett_reduce256_modm_r, 1arg = lo, 2args = hi, lo, 3args = r, hi, lo +STATIC mp_obj_t mod_trezorcrypto_monero_reduce256_modm(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 3 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 3 ? 0 : -1; + const bignum256modm hi_z = {0}; + const bignum256modm *hi = &hi_z; + const bignum256modm *lo = NULL; + + assert_scalar(res); + if (n_args > 1){ + assert_scalar(args[2+off]); + lo = &MP_OBJ_C_SCALAR(args[2+off]); + + if (args[1+off] == NULL || MP_OBJ_IS_TYPE(args[1+off], &mp_type_NoneType)){ + ; + } else { + assert_scalar(args[1+off]); + hi = &MP_OBJ_C_SCALAR(args[1+off]); + } + } else { + assert_scalar(args[1+off]); + lo = &MP_OBJ_C_SCALAR(args[1+off]); + } + + barrett_reduce256_modm(MP_OBJ_SCALAR(res), *hi, *lo); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_reduce256_modm_obj, 1, 3, mod_trezorcrypto_monero_reduce256_modm); + +//void add256_modm +STATIC mp_obj_t mod_trezorcrypto_monero_add256_modm(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 3 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 3 ? 0 : -1; + + assert_scalar(res); + assert_scalar(args[1+off]); + assert_scalar(args[2+off]); + add256_modm(MP_OBJ_SCALAR(res), MP_OBJ_C_SCALAR(args[1+off]), MP_OBJ_C_SCALAR(args[2+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_add256_modm_obj, 2, 3, mod_trezorcrypto_monero_add256_modm); + +//void sub256_modm +STATIC mp_obj_t mod_trezorcrypto_monero_sub256_modm(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 3 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 3 ? 0 : -1; + + assert_scalar(res); + assert_scalar(args[1+off]); + assert_scalar(args[2+off]); + sub256_modm(MP_OBJ_SCALAR(res), MP_OBJ_C_SCALAR(args[1+off]), MP_OBJ_C_SCALAR(args[2+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_sub256_modm_obj, 2, 3, mod_trezorcrypto_monero_sub256_modm); + +//void mulsub256_modm +STATIC mp_obj_t mod_trezorcrypto_monero_mulsub256_modm(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 4 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 4 ? 0 : -1; + + assert_scalar(res); + assert_scalar(args[1+off]); + assert_scalar(args[2+off]); + assert_scalar(args[3+off]); + mulsub256_modm(MP_OBJ_SCALAR(res), MP_OBJ_C_SCALAR(args[1+off]), MP_OBJ_C_SCALAR(args[2+off]), MP_OBJ_C_SCALAR(args[3+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_mulsub256_modm_obj, 3, 4, mod_trezorcrypto_monero_mulsub256_modm); + +//void contract256_modm_r +STATIC mp_obj_t mod_trezorcrypto_monero_pack256_modm(const mp_obj_t arg){ + assert_scalar(arg); + uint8_t buff[32]; + contract256_modm(buff, MP_OBJ_C_SCALAR(arg)); + return mp_obj_new_bytes(buff, 32); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_pack256_modm_obj, mod_trezorcrypto_monero_pack256_modm); + +//void contract256_modm_r +STATIC mp_obj_t mod_trezorcrypto_monero_pack256_modm_into(const mp_obj_t arg, const mp_obj_t buf){ + assert_scalar(arg); + mp_buffer_info_t bufm; + mp_get_buffer_raise(buf, &bufm, MP_BUFFER_WRITE); + if (bufm.len < 32) { + mp_raise_ValueError("Buffer too small"); + } + + contract256_modm(bufm.buf, MP_OBJ_C_SCALAR(arg)); + return buf; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_monero_pack256_modm_into_obj, mod_trezorcrypto_monero_pack256_modm_into); + +//expand256_modm_r +STATIC mp_obj_t mod_trezorcrypto_monero_unpack256_modm(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 2 ? 0 : -1; + assert_scalar(res); + mp_unpack_scalar(MP_OBJ_SCALAR(res), args[1+off]); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_unpack256_modm_obj, 1, 2, mod_trezorcrypto_monero_unpack256_modm); + +// +// GE25519 Defs +// + +//void ge25519_set_neutral(ge25519 *r); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_set_neutral(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 1 ? args[0] : mp_obj_new_ge25519(); + assert_ge25519(res); + ge25519_set_neutral(&MP_OBJ_GE25519(res)); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_set_neutral_obj, 0, 1, mod_trezorcrypto_monero_ge25519_set_neutral); + +//void ge25519_set_xmr_h(ge25519 *r); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_set_xmr_h(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 1 ? args[0] : mp_obj_new_ge25519(); + assert_ge25519(res); + ge25519_set_xmr_h(&MP_OBJ_GE25519(res)); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_set_xmr_h_obj, 0, 1, mod_trezorcrypto_monero_ge25519_set_xmr_h); + +//int ge25519_check(const ge25519 *r); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_check(const mp_obj_t arg){ + assert_ge25519(arg); + if (ge25519_check(&MP_OBJ_C_GE25519(arg)) != 1){ + mp_raise_ValueError("Ed25519 point not on curve"); + } + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_ge25519_check_obj, mod_trezorcrypto_monero_ge25519_check); + +//int ge25519_eq(const ge25519 *a, const ge25519 *b); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_eq(const mp_obj_t a, const mp_obj_t b){ + assert_ge25519(a); + assert_ge25519(b); + int r = ge25519_eq(&MP_OBJ_C_GE25519(a), &MP_OBJ_C_GE25519(b)); + return MP_OBJ_NEW_SMALL_INT(r); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_monero_ge25519_eq_obj, mod_trezorcrypto_monero_ge25519_eq); + +//void ge25519_norm(ge25519 *r, const ge25519 *t); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_norm(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_ge25519(); + mp_obj_t src = n_args == 2 ? args[1] : args[0]; + assert_ge25519(res); + assert_ge25519(src); + ge25519_norm(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(src)); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_norm_obj, 1, 2, mod_trezorcrypto_monero_ge25519_norm); + +//void ge25519_add(ge25519 *r, const ge25519 *a, const ge25519 *b, unsigned char signbit); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_add(size_t n_args, const mp_obj_t *args){ + mp_int_t s = 0; + int off = 0; + mp_obj_t res = args[0]; + + if (n_args == 2){ // a, b + off = -1; + } else if (n_args == 3){ // r, a, b || a, b, s + if (mp_obj_is_integer(args[2])){ + s = mp_obj_get_int(args[2]); + off = -1; + } + } else if (n_args == 4){ // r, a, b, s + s = mp_obj_get_int(args[3]); + } else { + mp_raise_ValueError(NULL); + } + + if (off == -1){ + res = mp_obj_new_ge25519(); + } + + assert_ge25519(res); + assert_ge25519(args[1+off]); + assert_ge25519(args[2+off]); + + ge25519_add(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(args[1+off]), &MP_OBJ_C_GE25519(args[2+off]), s); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_add_obj, 3, 4, mod_trezorcrypto_monero_ge25519_add); + +//void ge25519_double(ge25519 *r, const ge25519 *p); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_double(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_ge25519(); + mp_obj_t src = n_args == 2 ? args[1] : args[0]; + assert_ge25519(src); + assert_ge25519(res); + + ge25519_double(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(src)); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_double_obj, 1, 2, mod_trezorcrypto_monero_ge25519_double); + +//void ge25519_mul8(ge25519 *r, const ge25519 *p); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_mul8(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_ge25519(); + mp_obj_t src = n_args == 2 ? args[1] : args[0]; + assert_ge25519(src); + assert_ge25519(res); + + ge25519_mul8(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(src)); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_mul8_obj, 1, 2, mod_trezorcrypto_monero_ge25519_mul8); + +//void ge25519_double_scalarmult_vartime(ge25519 *r, const ge25519 *p1, const bignum256modm s1, const bignum256modm s2); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_double_scalarmult_vartime(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 4 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 4 ? 0 : -1; + + assert_ge25519(res); + assert_ge25519(args[1+off]); + assert_scalar(args[2+off]); + assert_scalar(args[3+off]); + + ge25519_double_scalarmult_vartime(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(args[1+off]), + MP_OBJ_C_SCALAR(args[2+off]), MP_OBJ_C_SCALAR(args[3+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_double_scalarmult_vartime_obj, 3, 4, mod_trezorcrypto_monero_ge25519_double_scalarmult_vartime); + +//void ge25519_double_scalarmult_vartime2(ge25519 *r, const ge25519 *p1, const bignum256modm s1, const ge25519 *p2, const bignum256modm s2); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_double_scalarmult_vartime2(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 5 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 5 ? 0 : -1; + + assert_ge25519(res); + assert_ge25519(args[1+off]); + assert_scalar(args[2+off]); + assert_ge25519(args[3+off]); + assert_scalar(args[4+off]); + + ge25519_double_scalarmult_vartime2(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(args[1+off]), MP_OBJ_C_SCALAR(args[2+off]), + &MP_OBJ_C_GE25519(args[3+off]), MP_OBJ_C_SCALAR(args[4+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_double_scalarmult_vartime2_obj, 4, 5, mod_trezorcrypto_monero_ge25519_double_scalarmult_vartime2); + +//void ge25519_scalarmult_base_wrapper(ge25519 *r, const bignum256modm s); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_scalarmult_base(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 2 ? 0 : -1; + assert_ge25519(res); + if (MP_OBJ_IS_SCALAR(args[1+off])){ + ge25519_scalarmult_base_wrapper(&MP_OBJ_GE25519(res), MP_OBJ_C_SCALAR(args[1+off])); + } else if (mp_obj_is_integer(args[1+off])){ + bignum256modm mlt; + set256_modm(mlt, mp_obj_get_int(args[1+off])); + ge25519_scalarmult_base_wrapper(&MP_OBJ_GE25519(res), mlt); + } else { + mp_raise_ValueError("unknown base mult type"); + } + + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_scalarmult_base_obj, 1, 2, mod_trezorcrypto_monero_ge25519_scalarmult_base); + +//void ge25519_scalarmult_wrapper(ge25519 *r, const ge25519 *P, const bignum256modm a); +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_scalarmult(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 3 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 3 ? 0 : -1; + assert_ge25519(res); + assert_ge25519(args[1+off]); + + if (MP_OBJ_IS_SCALAR(args[2+off])){ + ge25519_scalarmult_wrapper(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(args[1+off]), MP_OBJ_C_SCALAR(args[2+off])); + } else if (mp_obj_is_integer(args[2+off])){ + bignum256modm mlt; + set256_modm(mlt, mp_obj_get_int(args[2+off])); + ge25519_scalarmult_wrapper(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(args[1+off]), mlt); + } else { + mp_raise_ValueError("unknown mult type"); + } + + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_scalarmult_obj, 2, 3, mod_trezorcrypto_monero_ge25519_scalarmult); + +//void ge25519_pack(unsigned char r[32], const ge25519 *p) +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_pack(const mp_obj_t arg){ + assert_ge25519(arg); + uint8_t buff[32]; + ge25519_pack(buff, &MP_OBJ_C_GE25519(arg)); + + return mp_obj_new_bytes(buff, 32); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_ge25519_pack_obj, mod_trezorcrypto_monero_ge25519_pack); + +//void ge25519_pack(unsigned char r[32], const ge25519 *p) +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_pack_into(const mp_obj_t arg, const mp_obj_t buf){ + assert_ge25519(arg); + mp_buffer_info_t bufm; + mp_get_buffer_raise(buf, &bufm, MP_BUFFER_WRITE); + if (bufm.len < 32) { + mp_raise_ValueError("Buffer too small"); + } + + ge25519_pack(bufm.buf, &MP_OBJ_C_GE25519(arg)); + return buf; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_monero_ge25519_pack_into_obj, mod_trezorcrypto_monero_ge25519_pack_into); + +//int ge25519_unpack_vartime(ge25519 *r, const unsigned char *s) +STATIC mp_obj_t mod_trezorcrypto_monero_ge25519_unpack_vartime(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 2 ? 0 : -1; + assert_ge25519(res); + mp_unpack_ge25519(&MP_OBJ_GE25519(res), args[1+off]); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_ge25519_unpack_vartime_obj, 1, 2, mod_trezorcrypto_monero_ge25519_unpack_vartime); + +// +// XMR defs +// + +// int xmr_base58_addr_encode_check(uint64_t tag, const uint8_t *data, size_t binsz, char *b58, size_t b58sz); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_base58_addr_encode_check(size_t n_args, const mp_obj_t *args){ + uint8_t out[128]; + mp_buffer_info_t data; + mp_get_buffer_raise(args[1], &data, MP_BUFFER_READ); + + int sz = xmr_base58_addr_encode_check(mp_obj_get_int(args[0]), data.buf, data.len, (char *)out, sizeof(out)); + if (sz == 0){ + mp_raise_ValueError("b58 encoding error"); + } + + return mp_obj_new_bytes(out, sz); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_base58_addr_encode_check_obj, 2, 2, mod_trezorcrypto_monero_xmr_base58_addr_encode_check); + +// int xmr_base58_addr_decode_check(const char *addr, size_t sz, uint64_t *tag, void *data, size_t datalen); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_base58_addr_decode_check(size_t n_args, const mp_obj_t *args){ + uint8_t out[128]; + uint64_t tag; + + mp_buffer_info_t data; + mp_get_buffer_raise(args[0], &data, MP_BUFFER_READ); + + int sz = xmr_base58_addr_decode_check(data.buf, data.len, &tag, out, sizeof(out)); + if (sz == 0){ + mp_raise_ValueError("b58 decoding error"); + } + + mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR(mp_obj_new_tuple(2, NULL)); + tuple->items[0] = mp_obj_new_bytes(out, sz); + tuple->items[1] = mp_obj_new_int_from_ull(tag); + return MP_OBJ_FROM_PTR(tuple); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_base58_addr_decode_check_obj, 1, 1, mod_trezorcrypto_monero_xmr_base58_addr_decode_check); + +// xmr_random_scalar +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_random_scalar(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 1 ? args[0] : mp_obj_new_scalar(); + assert_scalar(res); + xmr_random_scalar(MP_OBJ_SCALAR(res)); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_random_scalar_obj, 0, 1, mod_trezorcrypto_monero_xmr_random_scalar); + +//xmr_fast_hash +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_fast_hash(const mp_obj_t arg){ + uint8_t buff[32]; + mp_buffer_info_t data; + mp_get_buffer_raise(arg, &data, MP_BUFFER_READ); + xmr_fast_hash(buff, data.buf, data.len); + return mp_obj_new_bytes(buff, 32); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_xmr_fast_hash_obj, mod_trezorcrypto_monero_xmr_fast_hash); + +//xmr_hash_to_ec +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_hash_to_ec(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 2 ? 0 : -1; + mp_buffer_info_t data; + assert_ge25519(res); + mp_get_buffer_raise(args[1+off], &data, MP_BUFFER_READ); + xmr_hash_to_ec(&MP_OBJ_GE25519(res), data.buf, data.len); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_hash_to_ec_obj, 1, 2, mod_trezorcrypto_monero_xmr_hash_to_ec); + +//xmr_hash_to_scalar +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_hash_to_scalar(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 2 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 2 ? 0 : -1; + mp_buffer_info_t data; + assert_scalar(res); + mp_get_buffer_raise(args[1+off], &data, MP_BUFFER_READ); + xmr_hash_to_scalar(MP_OBJ_SCALAR(res), data.buf, data.len); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_hash_to_scalar_obj, 1, 2, mod_trezorcrypto_monero_xmr_hash_to_scalar); + +//void xmr_derivation_to_scalar(bignum256modm s, const ge25519 * p, uint32_t output_index); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_derivation_to_scalar(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 3 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 3 ? 0 : -1; + assert_scalar(res); + assert_ge25519(args[1+off]); + xmr_derivation_to_scalar(MP_OBJ_SCALAR(res), &MP_OBJ_C_GE25519(args[1+off]), mp_obj_get_int(args[2+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_derivation_to_scalar_obj, 2, 3, mod_trezorcrypto_monero_xmr_derivation_to_scalar); + +//void xmr_generate_key_derivation(ge25519 * r, const ge25519 * A, const bignum256modm b); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_generate_key_derivation(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 3 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 3 ? 0 : -1; + assert_ge25519(res); + assert_ge25519(args[1+off]); + assert_scalar(args[2+off]); + xmr_generate_key_derivation(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(args[1+off]), MP_OBJ_C_SCALAR(args[2+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_generate_key_derivation_obj, 2, 3, mod_trezorcrypto_monero_xmr_generate_key_derivation); + +//void xmr_derive_private_key(bignum256modm s, const ge25519 * deriv, uint32_t idx, const bignum256modm base); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_derive_private_key(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 4 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 4 ? 0 : -1; + assert_scalar(res); + assert_ge25519(args[1+off]); + assert_scalar(args[3+off]); + xmr_derive_private_key(MP_OBJ_SCALAR(res), &MP_OBJ_C_GE25519(args[1+off]), mp_obj_get_int(args[2+off]), MP_OBJ_C_SCALAR(args[3+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_derive_private_key_obj, 3, 4, mod_trezorcrypto_monero_xmr_derive_private_key); + +//void xmr_derive_public_key(ge25519 * r, const ge25519 * deriv, uint32_t idx, const ge25519 * base); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_derive_public_key(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 4 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 4 ? 0 : -1; + assert_ge25519(res); + assert_ge25519(args[1+off]); + assert_ge25519(args[3+off]); + xmr_derive_public_key(&MP_OBJ_GE25519(res), &MP_OBJ_C_GE25519(args[1+off]), mp_obj_get_int(args[2+off]), &MP_OBJ_C_GE25519(args[3+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_derive_public_key_obj, 3, 4, mod_trezorcrypto_monero_xmr_derive_public_key); + +//void xmr_add_keys2(ge25519 * r, const bignum256modm a, const bignum256modm b, const ge25519 * B); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_add_keys2(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 4 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 4 ? 0 : -1; + assert_ge25519(res); + assert_scalar(args[1+off]); + assert_scalar(args[2+off]); + assert_ge25519(args[3+off]); + xmr_add_keys2(&MP_OBJ_GE25519(res), MP_OBJ_SCALAR(args[1+off]), MP_OBJ_SCALAR(args[2+off]), &MP_OBJ_C_GE25519(args[3+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_add_keys2_obj, 3, 4, mod_trezorcrypto_monero_xmr_add_keys2); + +//void xmr_add_keys2_vartime(ge25519 * r, const bignum256modm a, const bignum256modm b, const ge25519 * B); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_add_keys2_vartime(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 4 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 4 ? 0 : -1; + assert_ge25519(res); + assert_scalar(args[1+off]); + assert_scalar(args[2+off]); + assert_ge25519(args[3+off]); + xmr_add_keys2_vartime(&MP_OBJ_GE25519(res), MP_OBJ_SCALAR(args[1+off]), MP_OBJ_SCALAR(args[2+off]), &MP_OBJ_C_GE25519(args[3+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_add_keys2_vartime_obj, 3, 4, mod_trezorcrypto_monero_xmr_add_keys2_vartime); + +//void xmr_add_keys3(ge25519 * r, const bignum256modm a, const ge25519 * A, const bignum256modm b, const ge25519 * B); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_add_keys3(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 5 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 5 ? 0 : -1; + assert_ge25519(res); + assert_scalar(args[1+off]); + assert_ge25519(args[2+off]); + assert_scalar(args[3+off]); + assert_ge25519(args[4+off]); + xmr_add_keys3(&MP_OBJ_GE25519(res), + MP_OBJ_SCALAR(args[1+off]), &MP_OBJ_C_GE25519(args[2+off]), + MP_OBJ_SCALAR(args[3+off]), &MP_OBJ_C_GE25519(args[4+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_add_keys3_obj, 4, 5, mod_trezorcrypto_monero_xmr_add_keys3); + +//void xmr_add_keys3_vartime(ge25519 * r, const bignum256modm a, const ge25519 * A, const bignum256modm b, const ge25519 * B); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_add_keys3_vartime(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 5 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 5 ? 0 : -1; + assert_ge25519(res); + assert_scalar(args[1+off]); + assert_ge25519(args[2+off]); + assert_scalar(args[3+off]); + assert_ge25519(args[4+off]); + xmr_add_keys3_vartime(&MP_OBJ_GE25519(res), + MP_OBJ_SCALAR(args[1+off]), &MP_OBJ_C_GE25519(args[2+off]), + MP_OBJ_SCALAR(args[3+off]), &MP_OBJ_C_GE25519(args[4+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_add_keys3_vartime_obj, 4, 5, mod_trezorcrypto_monero_xmr_add_keys3_vartime); + +//void xmr_get_subaddress_secret_key(bignum256modm r, uint32_t major, uint32_t minor, const bignum256modm m); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_get_subaddress_secret_key(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 4 ? args[0] : mp_obj_new_scalar(); + const int off = n_args == 4 ? 0 : -1; + assert_scalar(res); + assert_scalar(args[3+off]); + xmr_get_subaddress_secret_key(MP_OBJ_SCALAR(res), mp_obj_get_int(args[1+off]), mp_obj_get_int(args[2+off]), MP_OBJ_C_SCALAR(args[3+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_get_subaddress_secret_key_obj, 3, 4, mod_trezorcrypto_monero_xmr_get_subaddress_secret_key); + +//void xmr_gen_c(ge25519 * r, const bignum256modm a, uint64_t amount); +STATIC mp_obj_t mod_trezorcrypto_monero_xmr_gen_c(size_t n_args, const mp_obj_t *args){ + mp_obj_t res = n_args == 3 ? args[0] : mp_obj_new_ge25519(); + const int off = n_args == 3 ? 0 : -1; + assert_ge25519(res); + assert_scalar(args[1+off]); + xmr_gen_c(&MP_OBJ_GE25519(res), MP_OBJ_C_SCALAR(args[1+off]), mp_obj_get_uint64(args[2+off])); + return res; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_xmr_gen_c_obj, 2, 3, mod_trezorcrypto_monero_xmr_gen_c); + +/// def +STATIC mp_obj_t mod_trezorcrypto_monero_gen_range_proof(size_t n_args, const mp_obj_t *args) { + uint64_t amount; + ge25519 C; + bignum256modm mask; + + if (sizeof(xmr_range_sig_t) != RSIG_SIZE){ + mp_raise_ValueError("rsize invalid"); + } + + mp_buffer_info_t rsig_buff; + mp_get_buffer_raise(args[0], &rsig_buff, MP_BUFFER_WRITE); + if (rsig_buff.len < RSIG_SIZE){ + mp_raise_ValueError("rsize buff too small"); + } + + xmr_range_sig_t * rsig = (xmr_range_sig_t*)rsig_buff.buf; + bignum256modm * last_mask = NULL; + amount = mp_obj_get_uint64(args[1]); + if (n_args > 2 && MP_OBJ_IS_SCALAR(args[2])){ + last_mask = &MP_OBJ_SCALAR(args[2]); + } + + if (n_args > 4){ + const size_t mem_limit = sizeof(bignum256modm)*64; + mp_buffer_info_t buf_ai, buf_alpha; + mp_get_buffer_raise(args[3], &buf_ai, MP_BUFFER_WRITE); + mp_get_buffer_raise(args[4], &buf_alpha, MP_BUFFER_WRITE); + if (buf_ai.len < mem_limit || buf_alpha.len < mem_limit) { + mp_raise_ValueError("Buffer too small"); + } + + xmr_gen_range_sig_ex(rsig, &C, mask, amount, last_mask, buf_ai.buf, buf_alpha.buf); + } else { + xmr_gen_range_sig(rsig, &C, mask, amount, last_mask); + } + + mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR(mp_obj_new_tuple(3, NULL)); + tuple->items[0] = mp_obj_from_ge25519(&C); + tuple->items[1] = mp_obj_from_scalar(mask); + tuple->items[2] = args[0]; + return MP_OBJ_FROM_PTR(tuple); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_trezorcrypto_monero_gen_range_proof_obj, 2, 5, mod_trezorcrypto_monero_gen_range_proof); + + +/// def +STATIC mp_obj_t mod_trezorcrypto_ct_equals(const mp_obj_t a, const mp_obj_t b){ + mp_buffer_info_t buff_a, buff_b; + mp_get_buffer_raise(a, &buff_a, MP_BUFFER_READ); + mp_get_buffer_raise(b, &buff_b, MP_BUFFER_READ); + + if (buff_a.len != buff_b.len) { + return MP_OBJ_NEW_SMALL_INT(0); + } + + int r = ed25519_verify(buff_a.buf, buff_b.buf, buff_a.len); + return MP_OBJ_NEW_SMALL_INT(r); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_ct_equals_obj, mod_trezorcrypto_ct_equals); + +// Hasher +STATIC mp_obj_t mod_trezorcrypto_monero_hasher_update(mp_obj_t self, const mp_obj_t arg){ + mp_obj_hasher_t *o = MP_OBJ_TO_PTR(self); + mp_buffer_info_t buff; + mp_get_buffer_raise(arg, &buff, MP_BUFFER_READ); + if (buff.len > 0) { + xmr_hasher_update(&o->h, buff.buf, buff.len); + } + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_trezorcrypto_monero_hasher_update_obj, mod_trezorcrypto_monero_hasher_update); + +STATIC mp_obj_t mod_trezorcrypto_monero_hasher_digest(mp_obj_t self){ + mp_obj_hasher_t *o = MP_OBJ_TO_PTR(self); + uint8_t out[SHA3_256_DIGEST_LENGTH]; + Hasher ctx; + memcpy(&ctx, &(o->h), sizeof(Hasher)); + + xmr_hasher_final(&ctx, out); + memset(&ctx, 0, sizeof(SHA3_CTX)); + return mp_obj_new_bytes(out, sizeof(out)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_hasher_digest_obj, mod_trezorcrypto_monero_hasher_digest); + +STATIC mp_obj_t mod_trezorcrypto_monero_hasher_copy(mp_obj_t self){ + mp_obj_hasher_t *o = MP_OBJ_TO_PTR(self); + mp_obj_hasher_t *cp = m_new_obj(mp_obj_hasher_t); + cp->base.type = o->base.type; + memcpy(&(cp->h), &(o->h), sizeof(Hasher)); + return MP_OBJ_FROM_PTR(o); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_monero_hasher_copy_obj, mod_trezorcrypto_monero_hasher_copy); + + +// +// Type defs +// + +STATIC const mp_rom_map_elem_t mod_trezorcrypto_monero_ge25519_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519___del___obj) }, +}; +STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_monero_ge25519_locals_dict, mod_trezorcrypto_monero_ge25519_locals_dict_table); + +STATIC const mp_obj_type_t mod_trezorcrypto_monero_ge25519_type = { + { &mp_type_type }, + .name = MP_QSTR_ge25519, + .make_new = mod_trezorcrypto_monero_ge25519_make_new, + .locals_dict = (void*)&mod_trezorcrypto_monero_ge25519_locals_dict, +}; + +STATIC const mp_rom_map_elem_t mod_trezorcrypto_monero_bignum256modm_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mod_trezorcrypto_monero_bignum256modm___del___obj) }, +}; +STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_monero_bignum256modm_locals_dict, mod_trezorcrypto_monero_bignum256modm_locals_dict_table); + + +STATIC const mp_obj_type_t mod_trezorcrypto_monero_bignum256modm_type = { + { &mp_type_type }, + .name = MP_QSTR_bignum256modm, + .make_new = mod_trezorcrypto_monero_bignum256modm_make_new, + .locals_dict = (void*)&mod_trezorcrypto_monero_bignum256modm_locals_dict, +}; + +STATIC const mp_rom_map_elem_t mod_trezorcrypto_monero_hasher_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&mod_trezorcrypto_monero_hasher_update_obj) }, + { MP_ROM_QSTR(MP_QSTR_digest), MP_ROM_PTR(&mod_trezorcrypto_monero_hasher_digest_obj) }, + { MP_ROM_QSTR(MP_QSTR_copy), MP_ROM_PTR(&mod_trezorcrypto_monero_hasher_copy_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mod_trezorcrypto_monero_hasher___del___obj) }, + { MP_ROM_QSTR(MP_QSTR_block_size), MP_OBJ_NEW_SMALL_INT(SHA3_256_BLOCK_LENGTH) }, + { MP_ROM_QSTR(MP_QSTR_digest_size), MP_OBJ_NEW_SMALL_INT(SHA3_256_DIGEST_LENGTH) }, +}; +STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_monero_hasher_locals_dict, mod_trezorcrypto_monero_hasher_locals_dict_table); + + +STATIC const mp_obj_type_t mod_trezorcrypto_monero_hasher_type = { + { &mp_type_type }, + .name = MP_QSTR_hasher, + .make_new = mod_trezorcrypto_monero_hasher_make_new, + .locals_dict = (void*)&mod_trezorcrypto_monero_hasher_locals_dict, +}; + +STATIC const mp_rom_map_elem_t mod_trezorcrypto_monero_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_monero) }, + { MP_ROM_QSTR(MP_QSTR_init256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_init256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_check256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_check256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_iszero256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_iszero256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_eq256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_eq256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_get256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_get256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_reduce256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_reduce256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_add256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_add256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_sub256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_sub256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_mulsub256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_mulsub256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_pack256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_pack256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_pack256_modm_into), MP_ROM_PTR(&mod_trezorcrypto_monero_pack256_modm_into_obj) }, + { MP_ROM_QSTR(MP_QSTR_unpack256_modm), MP_ROM_PTR(&mod_trezorcrypto_monero_unpack256_modm_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_set_neutral), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_set_neutral_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_set_h), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_set_xmr_h_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_pack), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_pack_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_pack_into), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_pack_into_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_unpack_vartime), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_unpack_vartime_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_check), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_check_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_eq), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_eq_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_norm), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_norm_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_add), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_add_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_double), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_double_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_mul8), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_mul8_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_double_scalarmult_vartime), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_double_scalarmult_vartime_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_double_scalarmult_vartime2), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_double_scalarmult_vartime2_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_scalarmult_base), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_scalarmult_base_obj) }, + { MP_ROM_QSTR(MP_QSTR_ge25519_scalarmult), MP_ROM_PTR(&mod_trezorcrypto_monero_ge25519_scalarmult_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_base58_addr_encode_check), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_base58_addr_encode_check_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_base58_addr_decode_check), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_base58_addr_decode_check_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_random_scalar), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_random_scalar_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_fast_hash), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_fast_hash_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_hash_to_ec), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_hash_to_ec_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_hash_to_scalar), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_hash_to_scalar_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_derivation_to_scalar), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_derivation_to_scalar_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_generate_key_derivation), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_generate_key_derivation_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_derive_private_key), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_derive_private_key_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_derive_public_key), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_derive_public_key_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_add_keys2), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_add_keys2_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_add_keys2_vartime), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_add_keys2_vartime_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_add_keys3), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_add_keys3_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_add_keys3_vartime), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_add_keys3_vartime_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_get_subaddress_secret_key), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_get_subaddress_secret_key_obj) }, + { MP_ROM_QSTR(MP_QSTR_xmr_gen_c), MP_ROM_PTR(&mod_trezorcrypto_monero_xmr_gen_c_obj) }, + { MP_ROM_QSTR(MP_QSTR_gen_range_proof), MP_ROM_PTR(&mod_trezorcrypto_monero_gen_range_proof_obj) }, + { MP_ROM_QSTR(MP_QSTR_ct_equals), MP_ROM_PTR(&mod_trezorcrypto_ct_equals_obj) }, +}; +STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_monero_globals, mod_trezorcrypto_monero_globals_table); + +STATIC const mp_obj_module_t mod_trezorcrypto_monero_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t*)&mod_trezorcrypto_monero_globals, +}; diff --git a/embed/extmod/modtrezorcrypto/modtrezorcrypto.c b/embed/extmod/modtrezorcrypto/modtrezorcrypto.c index 2ecc86f89..be24f2f59 100644 --- a/embed/extmod/modtrezorcrypto/modtrezorcrypto.c +++ b/embed/extmod/modtrezorcrypto/modtrezorcrypto.c @@ -48,6 +48,7 @@ #include "modtrezorcrypto-sha512.h" #include "modtrezorcrypto-sha3-256.h" #include "modtrezorcrypto-sha3-512.h" +#include "modtrezorcrypto-monero.h" STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorcrypto) }, @@ -61,6 +62,7 @@ STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_crc), MP_ROM_PTR(&mod_trezorcrypto_crc_module) }, { MP_ROM_QSTR(MP_QSTR_curve25519), MP_ROM_PTR(&mod_trezorcrypto_curve25519_module) }, { MP_ROM_QSTR(MP_QSTR_ed25519), MP_ROM_PTR(&mod_trezorcrypto_ed25519_module) }, + { MP_ROM_QSTR(MP_QSTR_monero), MP_ROM_PTR(&mod_trezorcrypto_monero_module) }, { MP_ROM_QSTR(MP_QSTR_nist256p1), MP_ROM_PTR(&mod_trezorcrypto_nist256p1_module) }, { MP_ROM_QSTR(MP_QSTR_groestl512), MP_ROM_PTR(&mod_trezorcrypto_Groestl512_type) }, { MP_ROM_QSTR(MP_QSTR_nem), MP_ROM_PTR(&mod_trezorcrypto_nem_module) }, diff --git a/src/apps/monero/__init__.py b/src/apps/monero/__init__.py new file mode 100644 index 000000000..77c901dae --- /dev/null +++ b/src/apps/monero/__init__.py @@ -0,0 +1,61 @@ +import gc + +from trezor import log +from trezor.messages.MessageType import ( + DebugMoneroDiagRequest, + MoneroGetAddress, + MoneroGetWatchKey, + MoneroKeyImageSyncRequest, + MoneroTransactionSignRequest, +) +from trezor.wire import protobuf_workflow, register + + +# persistent state objects +class Holder(object): + def __init__(self): + self.ctx_sign = None + self.ctx_ki = None + + +STATE = Holder() + + +def dispatch_MoneroGetAddress(*args, **kwargs): + from apps.monero.get_address import layout_monero_get_address + + return layout_monero_get_address(*args, **kwargs) + + +def dispatch_MoneroGetWatchKey(*args, **kwargs): + from apps.monero.get_watch_only import layout_monero_get_watch_only + + return layout_monero_get_watch_only(*args, **kwargs) + + +def dispatch_MoneroTsxSign(*args, **kwargs): + from apps.monero.sign_tx import layout_sign_tx + + return layout_sign_tx(STATE, *args, **kwargs) + + +def dispatch_MoneroKeyImageSync(*args, **kwargs): + from apps.monero.key_image_sync import layout_key_image_sync + + return layout_key_image_sync(STATE, *args, **kwargs) + + +def dispatch_MoneroDiag(*args, **kwargs): + log.debug(__name__, "----diagnostics") + gc.collect() + from apps.monero.diag import dispatch_diag + + return dispatch_diag(*args, **kwargs) + + +def boot(): + register(MoneroGetAddress, protobuf_workflow, dispatch_MoneroGetAddress) + register(MoneroGetWatchKey, protobuf_workflow, dispatch_MoneroGetWatchKey) + register(MoneroTransactionSignRequest, protobuf_workflow, dispatch_MoneroTsxSign) + register(MoneroKeyImageSyncRequest, protobuf_workflow, dispatch_MoneroKeyImageSync) + register(DebugMoneroDiagRequest, protobuf_workflow, dispatch_MoneroDiag) diff --git a/src/apps/monero/controller/__init__.py b/src/apps/monero/controller/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/monero/controller/iface.py b/src/apps/monero/controller/iface.py new file mode 100644 index 000000000..876f8f943 --- /dev/null +++ b/src/apps/monero/controller/iface.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + + +class TrezorInterface(object): + def __init__(self, ctx=None): + self.ctx = ctx + + def gctx(self, ctx): + return ctx if ctx is not None else self.ctx + + async def restore_default(self): + from trezor import workflow + + workflow.restartdefault() + + async def confirm_transaction(self, tsx_data, creds=None, ctx=None): + """ + Ask for confirmation from user + :param tsx_data: + :param creds: + :param ctx: + :return: + """ + from apps.monero.xmr.sub.addr import encode_addr, get_change_addr_idx + from apps.monero.xmr.sub.xmr_net import net_version + + outs = tsx_data.outputs + change_idx = get_change_addr_idx(outs, tsx_data.change_dts) + + if change_idx is not None: + outs = [x for i, x in enumerate(outs) if i != change_idx] + [ + outs[change_idx] + ] + change_idx = len(outs) - 1 + + from apps.monero import layout + + for idx, dst in enumerate(outs): + addr = encode_addr( + net_version(creds.network_type), + dst.addr.m_spend_public_key, + dst.addr.m_view_public_key, + ) + is_change = change_idx and idx == change_idx + await layout.require_confirm_tx( + self.gctx(ctx), addr.decode("ascii"), dst.amount, is_change + ) + + await layout.require_confirm_fee(self.gctx(ctx), tsx_data.fee) + + from trezor.ui.text import Text + from trezor import ui + from trezor import loop + from trezor import log + from trezor import workflow + from trezor.ui import BACKLIGHT_DIM, BACKLIGHT_NORMAL + + await ui.backlight_slide(BACKLIGHT_DIM) + slide = ui.backlight_slide(BACKLIGHT_NORMAL) + # await ui.backlight_slide(BACKLIGHT_NORMAL) + + text = Text("Signing transaction", ui.ICON_SEND, icon_color=ui.BLUE) + text.normal("Signing...") + + try: + layout = await layout.simple_text(text, tm=1000) + log.debug(__name__, "layout: %s", layout) + workflow.closedefault() + workflow.onlayoutstart(layout) + loop.schedule(slide) + # display.clear() + + finally: + pass + # loop.close(slide) + # workflow.onlayoutclose(layout) + + await loop.sleep(500 * 1000) + return True + + async def transaction_error(self, *args, **kwargs): + """ + Transaction error + :return: + """ + from trezor import ui + from trezor.ui.text import Text + from apps.monero import layout + + text = Text("Error", ui.ICON_SEND, icon_color=ui.RED) + text.normal("Transaction failed") + + await layout.ui_text(text, tm=3 * 1000 * 1000) + await self.restore_default() + + async def transaction_signed(self, ctx=None): + """ + Notifies the transaction was completely signed + :return: + """ + + async def transaction_finished(self, ctx=None): + """ + Notifies the transaction has been completed (all data were sent) + :return: + """ + from trezor import ui + from trezor.ui.text import Text + from apps.monero import layout + + text = Text("Success", ui.ICON_SEND, icon_color=ui.GREEN) + text.normal("Transaction signed") + + await layout.ui_text(text, tm=3 * 1000 * 1000) + await self.restore_default() + + async def transaction_step(self, step, sub_step=None, sub_step_total=None): + """ + Transaction progress + :param step: + :param sub_step: + :param sub_step_total: + :return: + """ + from trezor import ui + from trezor.ui.text import Text + from apps.monero import layout + + info = [] + if step == 100: + info = ["Processing inputs", "%d/%d" % (sub_step + 1, sub_step_total)] + elif step == 200: + info = ["Sorting"] + elif step == 300: + info = [ + "Processing inputs", + "phase 2", + "%d/%d" % (sub_step + 1, sub_step_total), + ] + elif step == 400: + info = ["Processing outputs", "%d/%d" % (sub_step + 1, sub_step_total)] + elif step == 500: + info = ["Postprocessing..."] + elif step == 600: + info = ["Postprocessing..."] + elif step == 700: + info = ["Signing inputs", "%d/%d" % (sub_step + 1, sub_step_total)] + else: + info = ["Processing..."] + + text = Text("Signing transaction", ui.ICON_SEND, icon_color=ui.BLUE) + text.normal(*info) + + await layout.simple_text(text, tm=10 * 1000) + + async def confirm_ki_sync(self, init_msg, ctx=None): + """ + Ask confirmation on key image sync + :param init_msg: + :return: + """ + from apps.monero import layout + + await layout.require_confirm_keyimage_sync(self.gctx(ctx)) + return True + + async def ki_error(self, e, ctx=None): + """ + Key image sync error + :param e: + :return: + """ + + async def ki_step(self, i, ctx=None): + """ + Key image sync step + :param i: + :return: + """ + + async def ki_finished(self, ctx=None): + """ + Ki sync finished + :return: + """ + + +def get_iface(ctx=None): + return TrezorInterface(ctx) diff --git a/src/apps/monero/controller/misc.py b/src/apps/monero/controller/misc.py new file mode 100644 index 000000000..832b41f9a --- /dev/null +++ b/src/apps/monero/controller/misc.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + + +class TrezorError(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for kw in kwargs: + setattr(self, kw, kwargs[kw]) + + +class TrezorSecurityError(TrezorError): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class TrezorTxPrefixHashNotMatchingError(TrezorError): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class StdObj(object): + def __init__(self, **kwargs): + for kw in kwargs: + setattr(self, kw, kwargs[kw]) + + +def compute_tx_key(spend_key_private, tx_prefix_hash, salt=None, rand_mult=None): + """ + + :param spend_key_private: + :param tx_prefix_hash: + :param salt: + :param rand_mult: + :return: + """ + from apps.monero.xmr import crypto + + if not salt: + salt = crypto.random_bytes(32) + + if not rand_mult: + rand_mult_num = crypto.random_scalar() + rand_mult = crypto.encodeint(rand_mult_num) + else: + rand_mult_num = crypto.decodeint(rand_mult) + + rand_inp = crypto.sc_add(spend_key_private, rand_mult_num) + passwd = crypto.keccak_2hash(crypto.encodeint(rand_inp) + tx_prefix_hash) + tx_key = crypto.compute_hmac(salt, passwd) + return tx_key, salt, rand_mult + + +def translate_monero_dest_entry(dst_entry): + from apps.monero.xmr.serialize_messages.tx_dest_entry import TxDestinationEntry + from apps.monero.xmr.serialize_messages.addr import AccountPublicAddress + + d = TxDestinationEntry() + d.amount = dst_entry.amount + d.is_subaddress = dst_entry.is_subaddress + d.addr = AccountPublicAddress( + m_spend_public_key=dst_entry.addr.spend_public_key, + m_view_public_key=dst_entry.addr.view_public_key, + ) + return d + + +async def translate_tsx_data(tsx_data): + from apps.monero.xmr.tsx_data import TsxData + + tsxd = TsxData() + for fld in TsxData.f_specs(): + fname = fld[0] + if hasattr(tsx_data, fname): + setattr(tsxd, fname, getattr(tsx_data, fname)) + + if tsx_data.change_dts: + tsxd.change_dts = translate_monero_dest_entry(tsx_data.change_dts) + + tsxd.outputs = [translate_monero_dest_entry(x) for x in tsx_data.outputs] + return tsxd + + +async def parse_msg(bts, msg): + from apps.monero.xmr.serialize import xmrserialize + from apps.monero.xmr.serialize.readwriter import MemoryReaderWriter + + reader = MemoryReaderWriter(memoryview(bts)) + ar = xmrserialize.Archive(reader, False) + return await ar.message(msg) + + +async def parse_src_entry(bts): + from apps.monero.xmr.serialize_messages.tx_src_entry import TxSourceEntry + + return await parse_msg(bts, TxSourceEntry()) + + +async def parse_dst_entry(bts): + from apps.monero.xmr.serialize_messages.tx_dest_entry import TxDestinationEntry + + return await parse_msg(bts, TxDestinationEntry()) + + +async def parse_vini(bts): + from apps.monero.xmr.serialize_messages.tx_prefix import TxinToKey + + return await parse_msg(bts, TxinToKey()) + + +async def dump_msg(msg, preallocate=None, msg_type=None): + from apps.monero.xmr.serialize import xmrserialize + from apps.monero.xmr.serialize.readwriter import MemoryReaderWriter + + writer = MemoryReaderWriter(preallocate=preallocate) + ar = xmrserialize.Archive(writer, True) + await ar.message(msg, msg_type=msg_type) + return writer.get_buffer() + + +async def dump_msg_gc(msg, preallocate=None, msg_type=None, del_msg=False): + b = await dump_msg(msg, preallocate=preallocate, msg_type=msg_type) + if del_msg: + del msg + + import gc + + gc.collect() + return b + + +def dst_entry_to_stdobj(dst): + if dst is None: + return None + + addr = StdObj( + m_spend_public_key=dst.addr.m_spend_public_key, + m_view_public_key=dst.addr.m_view_public_key, + ) + return StdObj(amount=dst.amount, addr=addr, is_subaddress=dst.is_subaddress) diff --git a/src/apps/monero/controller/wrapper.py b/src/apps/monero/controller/wrapper.py new file mode 100644 index 000000000..c5bf6e4a5 --- /dev/null +++ b/src/apps/monero/controller/wrapper.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + + +MONERO_CURVE = "secp256k1" # 'ed25519-keccak' + + +async def monero_get_creds(ctx, address_n=None, network_type=None): + from apps.common import seed + from apps.monero.xmr import crypto + from apps.monero.xmr import monero + from apps.monero.xmr.sub.creds import AccountCreds + + address_n = address_n or () + node = await seed.derive_node(ctx, address_n, MONERO_CURVE) + + key_seed = crypto.cn_fast_hash(node.private_key()) + keys = monero.generate_monero_keys( + key_seed + ) # spend_sec, spend_pub, view_sec, view_pub + + creds = AccountCreds.new_wallet(keys[2], keys[0], network_type) + return creds + + +def get_interface(ctx): + from apps.monero.controller import iface + + return iface.get_iface(ctx) + + +def exc2str(e): + return str(e) diff --git a/src/apps/monero/diag.py b/src/apps/monero/diag.py new file mode 100644 index 000000000..6b9aaa022 --- /dev/null +++ b/src/apps/monero/diag.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + +import gc +import micropython +import sys + +from trezor import log + +PREV_MEM = gc.mem_free() +CUR_MES = 0 + + +def check_mem(x): + global PREV_MEM, CUR_MES + + gc.collect() + free = gc.mem_free() + diff = PREV_MEM - free + log.debug( + __name__, + "======= {} {} Diff: {} Free: {} Allocated: {}".format( + CUR_MES, x, diff, free, gc.mem_alloc() + ), + ) + micropython.mem_info() + gc.collect() + CUR_MES += 1 + PREV_MEM = free + + +def retit(**kwargs): + from trezor.messages.Failure import Failure + + return Failure(**kwargs) + + +async def dispatch_diag(ctx, msg, **kwargs): + if msg.ins == 0: + check_mem(0) + return retit() + + elif msg.ins == 1: + check_mem(1) + micropython.mem_info(1) + return retit() + + elif msg.ins == 2: + log.debug(__name__, "_____________________________________________") + log.debug(__name__, "_____________________________________________") + log.debug(__name__, "_____________________________________________") + return retit() + + elif msg.ins == 3: + pass + + elif msg.ins == 4: + total = 0 + monero = 0 + + for k, v in sys.modules.items(): + log.info(__name__, "Mod[%s]: %s", k, v) + total += 1 + if k.startswith("apps.monero"): + monero += 1 + log.info(__name__, "Total modules: %s, Monero modules: %s", total, monero) + return retit() + + return retit() diff --git a/src/apps/monero/get_address.py b/src/apps/monero/get_address.py new file mode 100644 index 000000000..18e4018a4 --- /dev/null +++ b/src/apps/monero/get_address.py @@ -0,0 +1,18 @@ +from trezor.messages.MoneroAddress import MoneroAddress + +from apps.common.display_address import show_address, show_qr +from apps.monero.controller import wrapper + + +async def layout_monero_get_address(ctx, msg): + address_n = msg.address_n or () + creds = await wrapper.monero_get_creds(ctx, address_n, msg.network_type) + + if msg.show_display: + while True: + if await show_address(ctx, creds.address.decode("ascii")): + break + if await show_qr(ctx, creds.address.decode("ascii")): + break + + return MoneroAddress(address=creds.address) diff --git a/src/apps/monero/get_watch_only.py b/src/apps/monero/get_watch_only.py new file mode 100644 index 000000000..bf45f8c43 --- /dev/null +++ b/src/apps/monero/get_watch_only.py @@ -0,0 +1,15 @@ +from trezor.messages.MoneroGetWatchKey import MoneroGetWatchKey +from trezor.messages.MoneroWatchKey import MoneroWatchKey + +from apps.monero import layout +from apps.monero.controller import wrapper +from apps.monero.xmr import crypto + + +async def layout_monero_get_watch_only(ctx, msg: MoneroGetWatchKey): + address_n = msg.address_n or () + await layout.require_confirm_watchkey(ctx) + creds = await wrapper.monero_get_creds(ctx, address_n, msg.network_type) + return MoneroWatchKey( + watch_key=crypto.encodeint(creds.view_key_private), address=creds.address + ) diff --git a/src/apps/monero/key_image_sync.py b/src/apps/monero/key_image_sync.py new file mode 100644 index 000000000..0d459bfab --- /dev/null +++ b/src/apps/monero/key_image_sync.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + +import gc +import micropython + +from trezor import log + + +async def layout_key_image_sync(state, ctx, msg): + log.debug( + __name__, + "### KI SYNC. Free: {} Allocated: {}".format(gc.mem_free(), gc.mem_alloc()), + ) + log.debug(__name__, "KI sync state: %s", state.ctx_ki) + + from apps.monero.protocol import key_image_sync + + log.debug( + __name__, + "### KI sync imported. Free: {} Allocated: {}".format( + gc.mem_free(), gc.mem_alloc() + ), + ) + + gc.collect() + micropython.mem_info() + micropython.mem_info(1) + + try: + if msg.init: + log.debug(__name__, "ki_sync, init") + from apps.monero.controller import iface + + state.ctx_ki = key_image_sync.KeyImageSync( + ctx=ctx, iface=iface.get_iface(ctx) + ) + return await state.ctx_ki.init(ctx, msg.init) + + elif msg.step: + log.debug(__name__, "ki_sync, step") + return await state.ctx_ki.sync(ctx, msg.step) + + elif msg.final_msg: + log.debug(__name__, "ki_sync, final") + res = await state.ctx_ki.final(ctx, msg.final_msg) + state.ctx_ki = None + return res + + else: + raise ValueError("Unknown error") + + except Exception as e: + state.ctx_ki = None + + log.debug(__name__, "KI error, %s: %s", type(e), e) + + from trezor.messages.Failure import Failure + + return Failure() diff --git a/src/apps/monero/layout.py b/src/apps/monero/layout.py new file mode 100644 index 000000000..fc7fa821b --- /dev/null +++ b/src/apps/monero/layout.py @@ -0,0 +1,180 @@ +from trezor import ui +from trezor.messages import ButtonRequestType +from trezor.ui.text import Text +from trezor.utils import chunks + +from apps.common.confirm import require_confirm, require_hold_to_confirm + + +async def require_confirm_watchkey(ctx): + content = Text("Confirm export", ui.ICON_SEND, icon_color=ui.GREEN) + content.normal(*["Do you really want to", "export watch-only", "credentials?"]) + return await require_confirm(ctx, content, ButtonRequestType.SignTx) + + +async def require_confirm_keyimage_sync(ctx): + content = Text("Confirm ki sync", ui.ICON_SEND, icon_color=ui.GREEN) + content.normal(*["Do you really want to", "sync key images?"]) + return await require_confirm(ctx, content, ButtonRequestType.SignTx) + + +async def require_confirm_tx_plain(ctx, to, value, is_change=False): + content = Text( + "Confirm " + ("sending" if not is_change else "change"), + ui.ICON_SEND, + icon_color=ui.GREEN, + ) + content.bold(format_amount(value)) + content.normal("to") + content.mono(*split_address(to)) + return await require_confirm(ctx, content, code=ButtonRequestType.SignTx) + + +@ui.layout +async def tx_dialog( + ctx, code, content, cancel_btn, confirm_btn, cancel_style, confirm_style +): + from trezor.messages import MessageType + from trezor.messages.ButtonRequest import ButtonRequest + from trezor.ui.confirm import ConfirmDialog + + await ctx.call(ButtonRequest(code=code), MessageType.ButtonAck) + dialog = ConfirmDialog( + content, + cancel=cancel_btn, + confirm=confirm_btn, + cancel_style=cancel_style, + confirm_style=confirm_style, + ) + return await ctx.wait(dialog) + + +async def require_confirm_tx(ctx, to, value, is_change=False): + from trezor import loop + + len_addr = (len(to) + 15) // 16 + if len_addr <= 2: + return await require_confirm_tx_plain(ctx, to, value, is_change) + + else: + to_chunks = list(split_address(to)) + from trezor import res, wire + from trezor.ui.confirm import ( + CONFIRMED, + CANCELLED, + DEFAULT_CANCEL, + DEFAULT_CONFIRM, + ) + + npages = 1 + ((len_addr - 2) + 3) // 4 + cur_step = 0 + code = ButtonRequestType.SignTx + iback = res.load(ui.ICON_BACK) + inext = res.load(ui.ICON_CLICK) + + while cur_step <= npages: + text = [] + if cur_step == 0: + text = [ + ui.BOLD, + format_amount(value), + ui.NORMAL, + "to", + ui.MONO, + ] + to_chunks[:2] + else: + off = 4 * (cur_step - 1) + cur_chunks = to_chunks[2 + off : 2 + off + 4] + ctext = [list(x) for x in zip([ui.MONO] * len(cur_chunks), cur_chunks)] + for x in ctext: + text += x + + if cur_step == 0: + cancel_btn = DEFAULT_CANCEL + cancel_style = ui.BTN_CANCEL + confirm_btn = inext + confirm_style = ui.BTN_DEFAULT + elif cur_step + 1 < npages: + cancel_btn = iback + cancel_style = ui.BTN_DEFAULT + confirm_btn = inext + confirm_style = ui.BTN_DEFAULT + else: + cancel_btn = iback + cancel_style = ui.BTN_DEFAULT + confirm_btn = DEFAULT_CONFIRM + confirm_style = ui.BTN_CONFIRM + + conf_text = "Confirm send" if not is_change else "Con. change" + content = Text( + "%s %d/%d" % (conf_text, cur_step + 1, npages), + ui.ICON_SEND, + icon_color=ui.GREEN, + ) + content.normal(*text) + + reaction = await tx_dialog( + ctx, code, content, cancel_btn, confirm_btn, cancel_style, confirm_style + ) + + if cur_step == 0 and reaction == CANCELLED: + raise wire.ActionCancelled("Cancelled") + elif cur_step + 1 < npages and reaction == CONFIRMED: + cur_step += 1 + elif cur_step + 1 >= npages and reaction == CONFIRMED: + await loop.sleep(1000 * 1000) + return + elif reaction == CANCELLED: + cur_step -= 1 + elif reaction == CONFIRMED: + cur_step += 1 + + +async def require_confirm_fee(ctx, fee): + content = Text("Confirm fee", ui.ICON_SEND, icon_color=ui.GREEN) + content.normal("Fee: ") + content.bold(format_amount(fee)) + await require_hold_to_confirm(ctx, content, ButtonRequestType.ConfirmOutput) + + +@ui.layout +async def simple_wait(tm): + from trezor import loop + + await loop.sleep(tm) + + +async def light_on(): + from trezor import loop + + slide = await ui.backlight_slide(ui.BACKLIGHT_NORMAL, delay=0) + loop.schedule(slide) + + +@ui.layout +async def ui_text(text, tm=None) -> None: + from trezor import loop + + text.render() + + if tm is not None: + await loop.sleep(tm) + + +async def simple_text(text, tm=None) -> None: + from trezor import loop + from trezor.ui import display + + display.clear() + text.render() + + if tm is not None: + await loop.sleep(tm) + + +def format_amount(value): + return "%f XMR" % (value / 1000000000000) + + +def split_address(address): + return chunks(address, 16) diff --git a/src/apps/monero/protocol/__init__.py b/src/apps/monero/protocol/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/monero/protocol/key_image_sync.py b/src/apps/monero/protocol/key_image_sync.py new file mode 100644 index 000000000..2181b3db3 --- /dev/null +++ b/src/apps/monero/protocol/key_image_sync.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + +from trezor import log + +from apps.monero.controller import wrapper as twrap + + +class KeyImageSync(object): + def __init__(self, ctx=None, iface=None, creds=None): + from apps.monero.xmr import crypto + from apps.monero.xmr.sub.keccak_hasher import HashWrapper + + self.ctx = ctx + self.iface = iface + self.creds = creds # type: monero.AccountCreds + + self.num = 0 + self.c_idx = -1 + self.hash = None + self.blocked = None + self.enc_key = None + self.subaddresses = {} + self.hasher = HashWrapper(crypto.get_keccak()) + + async def derive_creds(self, msg): + self.creds = await twrap.monero_get_creds( + self.ctx, msg.address_n or (), msg.network_type + ) + + async def init(self, ctx, msg): + from apps.monero.xmr import crypto + from apps.monero.xmr import monero + from trezor.messages import FailureType + from trezor.messages.Failure import Failure + from trezor.messages.MoneroKeyImageExportInitAck import ( + MoneroKeyImageExportInitAck + ) + + self.ctx = ctx + await self.derive_creds(msg) + + confirmation = await self.iface.confirm_ki_sync(msg, ctx=ctx) + if not confirmation: + return Failure(code=FailureType.ActionCancelled, message="rejected") + + self.num = msg.num + self.hash = msg.hash + self.enc_key = crypto.random_bytes(32) + + # Sub address precomputation + if msg.subs and len(msg.subs) > 0: + for sub in msg.subs: # type: MoneroSubAddressIndicesList + monero.compute_subaddresses( + self.creds, sub.account, sub.minor_indices, self.subaddresses + ) + return MoneroKeyImageExportInitAck() + + async def sync(self, ctx, tds): + from apps.monero.xmr import crypto + from apps.monero.xmr.enc import chacha_poly + from apps.monero.xmr import key_image + from trezor.messages.MoneroExportedKeyImage import MoneroExportedKeyImage + from trezor.messages.MoneroKeyImageSyncStepAck import MoneroKeyImageSyncStepAck + + log.debug(__name__, "ki_sync, step i") + + self.ctx = ctx + if self.blocked: + raise ValueError("Blocked") + if len(tds.tdis) == 0: + raise ValueError("Empty") + + resp = [] + buff = bytearray(32 * 3) + buff_mv = memoryview(buff) + + for td in tds.tdis: + self.c_idx += 1 + if self.c_idx >= self.num: + raise ValueError("Too many outputs") + + log.debug(__name__, "ki_sync, step i: %d", self.c_idx) + chash = key_image.compute_hash(td) + + self.hasher.update(chash) + ki, sig = await key_image.export_key_image( + self.creds, self.subaddresses, td + ) + + crypto.encodepoint_into(ki, buff_mv[0:32]) + crypto.encodeint_into(sig[0][0], buff_mv[32:64]) + crypto.encodeint_into(sig[0][1], buff_mv[64:]) + + nonce, ciph, _ = chacha_poly.encrypt(self.enc_key, buff) + eki = MoneroExportedKeyImage(iv=nonce, tag=b"", blob=ciph) + resp.append(eki) + + return MoneroKeyImageSyncStepAck(kis=resp) + + async def final(self, ctx, msg=None): + from trezor.messages.MoneroKeyImageSyncFinalAck import ( + MoneroKeyImageSyncFinalAck + ) + + self.ctx = ctx + if self.blocked: + raise ValueError("Blocked") + + if self.c_idx + 1 != self.num: + await self.iface.ki_error("Invalid number of outputs", ctx=self.ctx) + raise ValueError("Invalid number of outputs") + + final_hash = self.hasher.digest() + if final_hash != self.hash: + await self.iface.ki_error("Invalid hash", ctx=self.ctx) + raise ValueError("Invalid hash") + + return MoneroKeyImageSyncFinalAck(enc_key=self.enc_key) diff --git a/src/apps/monero/protocol/tsx_sign.py b/src/apps/monero/protocol/tsx_sign.py new file mode 100644 index 000000000..08422013e --- /dev/null +++ b/src/apps/monero/protocol/tsx_sign.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 +import gc + +from trezor import log + +from apps.monero.controller import misc + + +class TsxSigner(object): + """ + Monero Transaction signer. + Provides interface to the host, packages messages. + """ + + def __init__(self): + from apps.monero.controller import iface + + self.ctx = None + self.tsx_ctr = 0 + self.err_ctr = 0 + self.tsx_obj = None # type: TTransactionBuilder + self.creds = None # type: apps.monero.xmr.sub.creds.AccountCreds + self.iface = iface.get_iface() + self.debug = True + self.purge = False + + async def tsx_exc_handler(self, e): + """ + Handles the exception thrown in the Trezor processing. Clears transaction state. + We could use decorator/wrapper for message calls but not sure how uPython handles them + so now are entry points wrapped in try-catch. + + :param e: + :return: + """ + if self.debug: + log.warning(__name__, "Transaction exception: %s: %s", type(e), e) + + self.err_ctr += 1 + self.purge = True + self.tsx_obj = None # clear transaction object + await self.iface.transaction_error(e) + + async def should_purge(self): + """ + Delete global state? + :return: + """ + return self.purge or (self.tsx_obj and self.tsx_obj.is_terminal()) + + async def setup(self, msg): + from apps.monero.controller import wrapper + + self.creds = await wrapper.monero_get_creds( + self.ctx, msg.address_n or (), msg.network_type + ) + + async def restore(self, state): + from apps.monero.protocol.tsx_sign_builder import TTransactionBuilder + + self.tsx_obj = TTransactionBuilder(self, creds=self.creds, state=state) + + async def state_save(self): + try: + s = self.tsx_obj.state_save() + self.tsx_obj = None + finally: + gc.collect() + return s + + async def sign(self, ctx, state, msg): + """ + Main multiplex point + :param ctx: + :param state: + :param msg: + :return: + """ + from apps.monero.controller import iface + + self.ctx = ctx + self.iface = iface.get_iface(ctx) + gc.collect() + + log.debug(__name__, "sign()") + log.debug( + __name__, "Mem Free: {} Allocated: {}".format(gc.mem_free(), gc.mem_alloc()) + ) + + if msg.init: + log.debug(__name__, "setup") + await self.setup(msg.init) + await self.restore(state if not msg.init else None) + + if msg.init: + log.debug(__name__, "sign_init") + return await self.tsx_init(msg.init.tsx_data) + elif msg.set_input: + log.debug(__name__, "sign_inp") + return await self.tsx_set_input(msg.set_input) + elif msg.input_permutation: + log.debug(__name__, "sign_perm") + return await self.tsx_inputs_permutation(msg.input_permutation) + elif msg.input_vini: + log.debug(__name__, "sign_vin") + return await self.tsx_input_vini(msg.input_vini) + elif msg.set_output: + log.debug(__name__, "sign_out") + return await self.tsx_set_output1(msg.set_output) + elif msg.all_out_set: + log.debug(__name__, "sign_out_set") + return await self.tsx_all_out1_set(msg.all_out_set) + elif msg.mlsag_done: + log.debug(__name__, "sign_done") + return await self.tsx_mlsag_done() + elif msg.sign_input: + log.debug(__name__, "sign_sinp") + return await self.tsx_sign_input(msg.sign_input) + elif msg.final_msg: + log.debug(__name__, "sign_final") + return await self.tsx_sign_final(msg.final_msg) + else: + raise ValueError("Unknown message") + + async def tsx_init(self, tsx_data): + """ + Initialize transaction state. + :param tsx_data: + :return: + """ + self.tsx_ctr += 1 + try: + tsxd = await misc.translate_tsx_data(tsx_data) + del tsx_data + + return await self.tsx_obj.init_transaction(tsxd, self.tsx_ctr) + except Exception as e: + await self.tsx_exc_handler(e) + raise + + async def tsx_set_input(self, msg): + """ + Sets UTXO one by one. + Computes spending secret key, key image. tx.vin[i] + HMAC, Pedersen commitment on amount. + + If number of inputs is small, in-memory mode is used = alpha, pseudo_outs are kept in the Trezor. + Otherwise pseudo_outs are offloaded with HMAC, alpha is offloaded encrypted under AES-GCM() with + key derived for exactly this purpose. + + :param msg + :return: + """ + try: + src_entr = await misc.parse_src_entry(msg.src_entr) + del msg.src_entr + + return await self.tsx_obj.set_input(src_entr) + except Exception as e: + await self.tsx_exc_handler(e) + raise + + async def tsx_inputs_permutation(self, msg): + """ + Set permutation on the inputs - sorted by key image on host. + + :return: + """ + try: + return await self.tsx_obj.tsx_inputs_permutation(msg.perm) + except Exception as e: + await self.tsx_exc_handler(e) + raise + + async def tsx_input_vini(self, msg): + """ + Set tx.vin[i] for incremental tx prefix hash computation. + After sorting by key images on host. + + :return: + """ + try: + src_entr = await misc.parse_src_entry(msg.src_entr) + vini = await misc.parse_vini(msg.vini) + del msg.src_entr + del msg.vini + + return await self.tsx_obj.input_vini( + src_entr, vini, msg.vini_hmac, msg.pseudo_out, msg.pseudo_out_hmac + ) + except Exception as e: + await self.tsx_exc_handler(e) + raise + + async def tsx_set_output1(self, msg): + """ + Set destination entry one by one. + Computes destination stealth address, amount key, range proof + HMAC, out_pk, ecdh_info. + + :param msg + :return: + """ + try: + dst_entr = await misc.parse_dst_entry(msg.dst_entr) + del msg.dst_entr + + return await self.tsx_obj.set_out1(dst_entr, msg.dst_entr_hmac) + except Exception as e: + await self.tsx_exc_handler(e) + raise + + async def tsx_all_out1_set(self, msg=None): + """ + All outputs were set in this phase. Computes additional public keys (if needed), tx.extra and + transaction prefix hash. + Adds additional public keys to the tx.extra + + :return: tx.extra, tx_prefix_hash + """ + try: + return await self.tsx_obj.all_out1_set() + + except misc.TrezorTxPrefixHashNotMatchingError as e: + await self.tsx_exc_handler(e) + + from trezor.messages.Failure import Failure + from apps.monero.controller.wrapper import exc2str + + return Failure(code=10, message=exc2str(e)) + + except Exception as e: + await self.tsx_exc_handler(e) + raise + + async def tsx_mlsag_done(self, msg=None): + """ + MLSAG message computed. + + :return: + """ + try: + return await self.tsx_obj.mlsag_done() + except Exception as e: + await self.tsx_exc_handler(e) + raise + + async def tsx_sign_input(self, msg): + """ + Generates a signature for one input. + + :return: + """ + try: + src_entr = await misc.parse_src_entry(msg.src_entr) + vini = await misc.parse_vini(msg.vini) + del msg.src_entr + del msg.vini + + return await self.tsx_obj.sign_input( + src_entr, + vini, + msg.vini_hmac, + msg.pseudo_out, + msg.pseudo_out_hmac, + msg.alpha_enc, + msg.spend_enc, + ) + except Exception as e: + await self.tsx_exc_handler(e) + raise + + async def tsx_sign_final(self, msg=None): + """ + Final message. + Offloading tx related data, encrypted. + + :return: + """ + try: + return await self.tsx_obj.final_msg() + except Exception as e: + await self.tsx_exc_handler(e) + raise diff --git a/src/apps/monero/protocol/tsx_sign_builder.py b/src/apps/monero/protocol/tsx_sign_builder.py new file mode 100644 index 000000000..028583094 --- /dev/null +++ b/src/apps/monero/protocol/tsx_sign_builder.py @@ -0,0 +1,1476 @@ +import gc +import micropython +from micropython import const + +from trezor import log + +from apps.monero.controller import misc +from apps.monero.xmr import common, crypto, monero + + +class TprefixStub(object): + __slots__ = ("version", "unlock_time", "vin", "vout", "extra") + + def __init__(self, **kwargs): + for kw in kwargs: + setattr(self, kw, kwargs[kw]) + + +class TTransactionBuilder(object): + """ + Transaction builder + """ + + STEP_INP = const(100) + STEP_PERM = const(200) + STEP_VINI = const(300) + STEP_OUT = const(400) + STEP_ALL_OUT = const(500) + STEP_MLSAG = const(600) + STEP_SIGN = const(700) + + def __init__(self, trezor=None, creds=None, state=None, **kwargs): + self.trezor = trezor + self.creds = creds + self.key_master = None + self.key_hmac = None + self.key_enc = None + + self.r = None # txkey + self.r_pub = None + self.state = None + + self.multi_sig = False + self.need_additional_txkeys = False + self.use_bulletproof = False + self.use_rct = True + self.use_simple_rct = False + self.input_count = 0 + self.output_count = 0 + self.output_change = None + self.mixin = 0 + self.fee = 0 + + self.additional_tx_private_keys = [] + self.additional_tx_public_keys = [] + self.inp_idx = -1 + self.out_idx = -1 + self.summary_inputs_money = 0 + self.summary_outs_money = 0 + self.input_secrets = [] + self.input_alphas = [] + self.input_pseudo_outs = [] + self.output_sk = [] + self.output_pk = [] + self.sumout = crypto.sc_0() + self.sumpouts_alphas = crypto.sc_0() + self.subaddresses = {} + self.tx = None + self.source_permutation = [] # sorted by key images + self.tx_prefix_hasher = None + self.tx_prefix_hash = None + self.full_message_hasher = None + self.full_message = None + self.exp_tx_prefix_hash = None + + if state is None: + self._init() + else: + self.state_load(state) + + def _init(self): + from apps.monero.xmr.sub.keccak_hasher import KeccakArchive + from apps.monero.xmr.sub.mlsag_hasher import PreMlsagHasher + from apps.monero.protocol.tsx_sign_state import TState + + self.state = TState() + self.tx = TprefixStub(vin=[], vout=[], extra=b"") + self.tx_prefix_hasher = KeccakArchive() + self.full_message_hasher = PreMlsagHasher() + + def state_load(self, t): + from apps.monero.xmr.sub.keccak_hasher import KeccakArchive + from apps.monero.xmr.sub.mlsag_hasher import PreMlsagHasher + from apps.monero.protocol.tsx_sign_state import TState + + self._log_trace(t.state) + + for attr in t.__dict__: + if attr.startswith("_"): + continue + + cval = getattr(t, attr) + if cval is None: + setattr(self, attr, cval) + continue + + if attr == "state": + self.state = TState() + self.state.state_load(t.state) + elif attr == "tx_prefix_hasher": + self.tx_prefix_hasher = KeccakArchive(ctx=t.tx_prefix_hasher) + elif attr == "full_message_hasher": + self.full_message_hasher = PreMlsagHasher(state=t.full_message_hasher) + else: + setattr(self, attr, cval) + + def state_save(self): + from apps.monero.protocol.tsx_sign_state_holder import TsxSignStateHolder + + t = TsxSignStateHolder() + + for attr in self.__dict__: + if attr.startswith("_"): + continue + + cval = getattr(self, attr) + if cval is None: + setattr(t, attr, cval) + continue + + if attr == "state": + t.state = self.state.state_save() + elif attr in ["trezor"]: + continue + elif attr.startswith("STEP"): + continue + elif attr == "tx_prefix_hasher": + t.tx_prefix_hasher = self.tx_prefix_hasher.ctx() + elif attr == "full_message_hasher": + t.full_message_hasher = self.full_message_hasher.state_save() + else: + setattr(t, attr, cval) + return t + + def _log_trace(self, x=None): + log.debug( + __name__, + "Log trace %s, ... F: %s A: %s, S: %s", + x, + gc.mem_free(), + gc.mem_alloc(), + micropython.stack_use(), + ) + + def assrt(self, condition, msg=None): + """ + Asserts condition + :param condition: + :param msg: + :return: + """ + if condition: + return + raise ValueError("Assertion error%s" % (" : %s" % msg if msg else "")) + + def is_terminal(self): + """ + Returns true if the state is terminal + :return: + """ + return self.state.is_terminal() + + def gen_r(self, use_r=None): + """ + Generates a new transaction key pair. + :param use_r: + :return: + """ + self.r = crypto.random_scalar() if use_r is None else use_r + self.r_pub = crypto.scalarmult_base(self.r) + + def check_change(self, outputs): + """ + Checks if the change address is among tx outputs. + :param outputs: + :return: + """ + from apps.monero.xmr.sub.addr import addr_eq + + change_addr = self.change_address() + if change_addr is None: + return + + for out in outputs: + if addr_eq(out.addr, change_addr): + return True + + raise ValueError("Change address not found in outputs") + + def in_memory(self): + """ + Returns true if the input transaction can be processed whole in-memory + :return: + """ + return False and self.input_count <= 1 + + def many_inputs(self): + """ + Returns true if number of inputs > 10 (secret spending key offloaded) + :return: + """ + return self.input_count >= 10 + + def many_outputs(self): + """ + Returns true if number of outputs > 10 (increases number of roundtrips of the protocol) + :return: + """ + return self.output_count >= 10 + + def num_inputs(self): + """ + Number of inputs + :return: + """ + return self.input_count + + def num_dests(self): + """ + Number of destinations + :return: + """ + return self.output_count + + def get_fee(self): + """ + Txn fee + :return: + """ + return self.fee if self.fee > 0 else 0 + + def change_address(self): + """ + Returns change address if change dst is set + :return: + """ + return self.output_change.addr if self.output_change else None + + def get_rct_type(self): + """ + RCTsig type (simple/full x Borromean/Bulletproof) + :return: + """ + from apps.monero.xmr.serialize_messages.tx_rsig import RctType + + if self.use_simple_rct: + return RctType.SimpleBulletproof if self.use_bulletproof else RctType.Simple + else: + return RctType.FullBulletproof if self.use_bulletproof else RctType.Full + + def init_rct_sig(self): + """ + Initializes RCTsig structure (fee, tx prefix hash, type) + :return: + """ + rv = misc.StdObj( + txnFee=self.get_fee(), message=self.tx_prefix_hash, type=self.get_rct_type() + ) + return rv + + def hmac_key_txin(self, idx): + """ + (TxSourceEntry[i] || tx.vin[i]) hmac key + :param idx: + :return: + """ + from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + return crypto.keccak_2hash(self.key_hmac + b"txin" + dump_uvarint_b(idx)) + + def hmac_key_txin_comm(self, idx): + """ + pseudo_outputs[i] hmac key. Pedersen commitment for inputs. + :param idx: + :return: + """ + from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + return crypto.keccak_2hash(self.key_hmac + b"txin-comm" + dump_uvarint_b(idx)) + + def hmac_key_txdst(self, idx): + """ + TxDestinationEntry[i] hmac key + :param idx: + :return: + """ + from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + return crypto.keccak_2hash(self.key_hmac + b"txdest" + dump_uvarint_b(idx)) + + def hmac_key_txout(self, idx): + """ + (TxDestinationEntry[i] || tx.vout[i]) hmac key + :param idx: + :return: + """ + from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + return crypto.keccak_2hash(self.key_hmac + b"txout" + dump_uvarint_b(idx)) + + def hmac_key_txout_asig(self, idx): + """ + rsig[i] hmac key. Range signature HMAC + :param idx: + :return: + """ + from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + return crypto.keccak_2hash(self.key_hmac + b"txout-asig" + dump_uvarint_b(idx)) + + def enc_key_txin_alpha(self, idx): + """ + Chacha20Poly1305 encryption key for alpha[i] used in Pedersen commitment in pseudo_outs[i] + :param idx: + :return: + """ + from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + return crypto.keccak_2hash(self.key_enc + b"txin-alpha" + dump_uvarint_b(idx)) + + def enc_key_spend(self, idx): + """ + Chacha20Poly1305 encryption key for alpha[i] used in Pedersen commitment in pseudo_outs[i] + :param idx: + :return: + """ + from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + return crypto.keccak_2hash(self.key_enc + b"txin-spend" + dump_uvarint_b(idx)) + + def enc_key_cout(self, idx=None): + """ + Chacha20Poly1305 encryption key for multisig C values from MLASG. + :param idx: + :return: + """ + from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + return crypto.keccak_2hash( + self.key_enc + b"cout" + (dump_uvarint_b(idx) if idx else b"") + ) + + async def gen_hmac_vini(self, src_entr, vini, idx): + """ + Computes hmac (TxSourceEntry[i] || tx.vin[i]) + :param src_entr: + :param vini: + :param idx: + :return: + """ + from apps.monero.xmr.sub.keccak_hasher import get_keccak_writer + from apps.monero.xmr.serialize import xmrserialize + from apps.monero.xmr.serialize_messages.tx_src_entry import TxSourceEntry + from apps.monero.xmr.serialize_messages.tx_prefix import TxinToKey + + kwriter = get_keccak_writer() + ar = xmrserialize.Archive(kwriter, True) + await ar.message(src_entr, TxSourceEntry) + await ar.message(vini, TxinToKey) + + hmac_key_vini = self.hmac_key_txin(idx) + hmac_vini = crypto.compute_hmac(hmac_key_vini, kwriter.get_digest()) + return hmac_vini + + async def gen_hmac_vouti(self, dst_entr, tx_out, idx): + """ + Generates HMAC for (TxDestinationEntry[i] || tx.vout[i]) + :param dst_entr: + :param tx_out: + :param idx: + :return: + """ + from apps.monero.xmr.sub.keccak_hasher import get_keccak_writer + from apps.monero.xmr.serialize import xmrserialize + from apps.monero.xmr.serialize_messages.tx_dest_entry import TxDestinationEntry + from apps.monero.xmr.serialize_messages.tx_prefix import TxOut + + kwriter = get_keccak_writer() + ar = xmrserialize.Archive(kwriter, True) + await ar.message(dst_entr, TxDestinationEntry) + await ar.message(tx_out, TxOut) + + hmac_key_vouti = self.hmac_key_txout(idx) + hmac_vouti = crypto.compute_hmac(hmac_key_vouti, kwriter.get_digest()) + return hmac_vouti + + async def gen_hmac_tsxdest(self, dst_entr, idx): + """ + Generates HMAC for TxDestinationEntry[i] + :param dst_entr: + :param idx: + :return: + """ + from apps.monero.xmr.sub.keccak_hasher import get_keccak_writer + from apps.monero.xmr.serialize import xmrserialize + from apps.monero.xmr.serialize_messages.tx_dest_entry import TxDestinationEntry + + kwriter = get_keccak_writer() + ar = xmrserialize.Archive(kwriter, True) + await ar.message(dst_entr, TxDestinationEntry) + + hmac_key = self.hmac_key_txdst(idx) + hmac_tsxdest = crypto.compute_hmac(hmac_key, kwriter.get_digest()) + return hmac_tsxdest + + async def _tprefix_update(self): + from apps.monero.xmr.serialize_messages.tx_prefix import TransactionPrefix + + tx_fields = TransactionPrefix.f_specs() + await self.tx_prefix_hasher.ar.message_field(self.tx, tx_fields[0]) + await self.tx_prefix_hasher.ar.message_field(self.tx, tx_fields[1]) + await self.tx_prefix_hasher.ar.container_size( + self.num_inputs(), tx_fields[2][1] + ) + self._log_trace(10) + + async def init_transaction(self, tsx_data, tsx_ctr): + """ + Initializes a new transaction. + :param tsx_data: + :type tsx_data: TsxData + :param tsx_ctr: + :return: + """ + from apps.monero.xmr.sub.addr import classify_subaddresses + + self.gen_r() + self.state.init_tsx() + self._log_trace(1) + + # Ask for confirmation + confirmation = await self.trezor.iface.confirm_transaction(tsx_data, self.creds) + if not confirmation: + from trezor.messages import FailureType + from trezor.messages.Failure import Failure + + return Failure(code=FailureType.ActionCancelled, message="rejected") + + gc.collect() + self._log_trace(3) + + # Basic transaction parameters + self.input_count = tsx_data.num_inputs + self.output_count = len(tsx_data.outputs) + self.output_change = misc.dst_entry_to_stdobj(tsx_data.change_dts) + self.mixin = tsx_data.mixin + self.fee = tsx_data.fee + self.use_simple_rct = self.input_count > 1 + self.use_bulletproof = tsx_data.is_bulletproof + self.multi_sig = tsx_data.is_multisig + self.state.inp_cnt(self.in_memory()) + self.check_change(tsx_data.outputs) + self.exp_tx_prefix_hash = common.defval_empty(tsx_data.exp_tx_prefix_hash, None) + + # Provided tx key, used mostly in multisig. + if len(tsx_data.use_tx_keys) > 0: + for ckey in tsx_data.use_tx_keys: + crypto.check_sc(crypto.decodeint(ckey)) + + self.gen_r(use_r=crypto.decodeint(tsx_data.use_tx_keys[0])) + self.additional_tx_private_keys = [ + crypto.decodeint(x) for x in tsx_data.use_tx_keys[1:] + ] + + # Additional keys w.r.t. subaddress destinations + class_res = classify_subaddresses(tsx_data.outputs, self.change_address()) + num_stdaddresses, num_subaddresses, single_dest_subaddress = class_res + + # if this is a single-destination transfer to a subaddress, we set the tx pubkey to R=s*D + if num_stdaddresses == 0 and num_subaddresses == 1: + self.r_pub = crypto.ge_scalarmult( + self.r, crypto.decodepoint(single_dest_subaddress.m_spend_public_key) + ) + + self.need_additional_txkeys = num_subaddresses > 0 and ( + num_stdaddresses > 0 or num_subaddresses > 1 + ) + self._log_trace(4) + + # Extra processing, payment id + self.tx.version = 2 + self.tx.unlock_time = tsx_data.unlock_time + await self.process_payment_id(tsx_data) + await self.compute_sec_keys(tsx_data, tsx_ctr) + gc.collect() + + # Iterative tx_prefix_hash hash computation + await self._tprefix_update() + gc.collect() + + # Final message hasher + self.full_message_hasher.init(self.use_simple_rct) + await self.full_message_hasher.set_type_fee(self.get_rct_type(), self.get_fee()) + + # Sub address precomputation + if tsx_data.account is not None and tsx_data.minor_indices: + self.precompute_subaddr(tsx_data.account, tsx_data.minor_indices) + self._log_trace(5) + + # HMAC outputs - pinning + hmacs = [] + for idx in range(self.num_dests()): + c_hmac = await self.gen_hmac_tsxdest(tsx_data.outputs[idx], idx) + hmacs.append(c_hmac) + gc.collect() + + self._log_trace(6) + + from trezor.messages.MoneroTransactionInitAck import MoneroTransactionInitAck + + return MoneroTransactionInitAck( + in_memory=self.in_memory(), + many_inputs=self.many_inputs(), + many_outputs=self.many_outputs(), + hmacs=hmacs, + ) + + async def process_payment_id(self, tsx_data): + """ + Payment id -> extra + :return: + """ + if common.is_empty(tsx_data.payment_id): + return + + from apps.monero.xmr.sub import tsx_helper + + if len(tsx_data.payment_id) == 8: + view_key_pub_enc = tsx_helper.get_destination_view_key_pub( + tsx_data.outputs, self.change_address() + ) + if view_key_pub_enc == crypto.NULL_KEY_ENC: + raise ValueError( + "Destinations have to have exactly one output to support encrypted payment ids" + ) + + view_key_pub = crypto.decodepoint(view_key_pub_enc) + payment_id_encr = tsx_helper.encrypt_payment_id( + tsx_data.payment_id, view_key_pub, self.r + ) + + extra_nonce = tsx_helper.set_encrypted_payment_id_to_tx_extra_nonce( + payment_id_encr + ) + + elif len(tsx_data.payment_id) == 32: + extra_nonce = tsx_helper.set_payment_id_to_tx_extra_nonce( + tsx_data.payment_id + ) + + else: + raise ValueError("Payment ID size invalid") + + self.tx.extra = tsx_helper.add_extra_nonce_to_tx_extra(b"", extra_nonce) + + async def compute_sec_keys(self, tsx_data, tsx_ctr): + """ + Generate master key H(TsxData || r || c_tsx) + :return: + """ + from apps.monero.xmr.sub.keccak_hasher import get_keccak_writer + from apps.monero.xmr.serialize import xmrserialize + + writer = get_keccak_writer() + ar1 = xmrserialize.Archive(writer, True) + await ar1.message(tsx_data) + await writer.awrite(crypto.encodeint(self.r)) + await xmrserialize.dump_uvarint(writer, tsx_ctr) + self.key_master = crypto.keccak_2hash( + writer.get_digest() + crypto.encodeint(crypto.random_scalar()) + ) + self.key_hmac = crypto.keccak_2hash(b"hmac" + self.key_master) + self.key_enc = crypto.keccak_2hash(b"enc" + self.key_master) + + def precompute_subaddr(self, account, indices): + """ + Precomputes subaddresses for account (major) and list of indices (minors) + Subaddresses have to be stored in encoded form - unique representation. + Single point can have multiple extended coordinates representation - would not match during subaddress search. + :param account: + :param indices: + :return: + """ + monero.compute_subaddresses(self.creds, account, indices, self.subaddresses) + + async def set_input(self, src_entr): + """ + Sets UTXO one by one. + Computes spending secret key, key image. tx.vin[i] + HMAC, Pedersen commitment on amount. + + If number of inputs is small, in-memory mode is used = alpha, pseudo_outs are kept in the Trezor. + Otherwise pseudo_outs are offloaded with HMAC, alpha is offloaded encrypted under Chacha20Poly1305() + with key derived for exactly this purpose. + + :param src_entr: + :type src_entr: apps.monero.xmr.serialize_messages.tx_construct.TxSourceEntry + :return: + """ + from trezor.messages.MoneroTransactionSetInputAck import ( + MoneroTransactionSetInputAck + ) + from apps.monero.xmr.enc import chacha_poly + from apps.monero.xmr.sub import tsx_helper + from apps.monero.xmr.serialize_messages.tx_prefix import TxinToKey + + self.state.input() + self.inp_idx += 1 + + await self.trezor.iface.transaction_step( + self.STEP_INP, self.inp_idx, self.num_inputs() + ) + + if self.inp_idx >= self.num_inputs(): + raise ValueError("Too many inputs") + if src_entr.real_output >= len(src_entr.outputs): + raise ValueError( + "real_output index %s bigger than output_keys.size() %s" + % (src_entr.real_output, len(src_entr.outputs)) + ) + self.summary_inputs_money += src_entr.amount + + # Secrets derivation + out_key = crypto.decodepoint(src_entr.outputs[src_entr.real_output][1].dest) + tx_key = crypto.decodepoint(src_entr.real_out_tx_key) + additional_keys = [ + crypto.decodepoint(x) for x in src_entr.real_out_additional_tx_keys + ] + + secs = monero.generate_key_image_helper( + self.creds, + self.subaddresses, + out_key, + tx_key, + additional_keys, + src_entr.real_output_in_tx_index, + ) + xi, ki, di = secs + + # Construct tx.vin + ki_real = src_entr.multisig_kLRki.ki if self.multi_sig else ki + vini = TxinToKey(amount=src_entr.amount, k_image=crypto.encodepoint(ki_real)) + vini.key_offsets = [x[0] for x in src_entr.outputs] + vini.key_offsets = tsx_helper.absolute_output_offsets_to_relative( + vini.key_offsets + ) + + if src_entr.rct: + vini.amount = 0 + + if self.in_memory(): + self.tx.vin.append(vini) + + # HMAC(T_in,i || vin_i) + hmac_vini = await self.gen_hmac_vini(src_entr, vini, self.inp_idx) + + # PseudoOuts commitment, alphas stored to state + pseudo_out = None + pseudo_out_hmac = None + alpha_enc = None + spend_enc = None + + if self.use_simple_rct: + alpha, pseudo_out = await self.commitment(src_entr.amount) + pseudo_out = crypto.encodepoint(pseudo_out) + + # In full version the alpha is encrypted and passed back for storage + if self.in_memory(): + self.input_alphas.append(alpha) + self.input_pseudo_outs.append(pseudo_out) + else: + pseudo_out_hmac = crypto.compute_hmac( + self.hmac_key_txin_comm(self.inp_idx), pseudo_out + ) + alpha_enc = chacha_poly.encrypt_pack( + self.enc_key_txin_alpha(self.inp_idx), crypto.encodeint(alpha) + ) + + if self.many_inputs(): + spend_enc = chacha_poly.encrypt_pack( + self.enc_key_spend(self.inp_idx), crypto.encodeint(xi) + ) + else: + self.input_secrets.append(xi) + + # All inputs done? + if self.inp_idx + 1 == self.num_inputs(): + await self.tsx_inputs_done() + + return MoneroTransactionSetInputAck( + vini=await misc.dump_msg(vini, preallocate=64), + vini_hmac=hmac_vini, + pseudo_out=pseudo_out, + pseudo_out_hmac=pseudo_out_hmac, + alpha_enc=alpha_enc, + spend_enc=spend_enc, + ) + + async def tsx_inputs_done(self): + """ + All inputs set + :return: + """ + self.state.input_done() + self.subaddresses = None + + if self.inp_idx + 1 != self.num_inputs(): + raise ValueError("Input count mismatch") + + if self.in_memory(): + return await self.tsx_inputs_done_inm() + + async def tsx_inputs_done_inm(self): + """ + In-memory post processing - tx.vin[i] sorting by key image. + Used only if number of inputs is small - computable in Trezor without offloading. + + :return: + """ + # Sort tx.in by key image + self.source_permutation = list(range(self.num_inputs())) + self.source_permutation.sort(key=lambda x: self.tx.vin[x].k_image, reverse=True) + await self._tsx_inputs_permutation(self.source_permutation) + + async def tsx_inputs_permutation(self, permutation): + """ + Set permutation on the inputs - sorted by key image on host. + + :param permutation: + :return: + """ + from trezor.messages.MoneroTransactionInputsPermutationAck import ( + MoneroTransactionInputsPermutationAck + ) + + await self.trezor.iface.transaction_step(self.STEP_PERM) + + if self.in_memory(): + return + await self._tsx_inputs_permutation(permutation) + return MoneroTransactionInputsPermutationAck() + + async def _tsx_inputs_permutation(self, permutation): + """ + Set permutation on the inputs - sorted by key image on host. + + :param permutation: + :return: + """ + self.state.input_permutation() + self.source_permutation = permutation + + def swapper(x, y): + if not self.many_inputs(): + self.input_secrets[x], self.input_secrets[y] = ( + self.input_secrets[y], + self.input_secrets[x], + ) + if self.in_memory() and self.use_simple_rct: + self.input_alphas[x], self.input_alphas[y] = ( + self.input_alphas[y], + self.input_alphas[x], + ) + self.input_pseudo_outs[x], self.input_pseudo_outs[y] = ( + self.input_pseudo_outs[y], + self.input_pseudo_outs[x], + ) + if self.in_memory(): + self.tx.vin[x], self.tx.vin[y] = self.tx.vin[y], self.tx.vin[x] + + common.apply_permutation(self.source_permutation, swapper) + self.inp_idx = -1 + + # Incremental hashing + if self.in_memory(): + for idx in range(self.num_inputs()): + await self.hash_vini_pseudo_out(self.tx.vin[idx], idx) + + async def input_vini(self, src_entr, vini, hmac, pseudo_out, pseudo_out_hmac): + """ + Set tx.vin[i] for incremental tx prefix hash computation. + After sorting by key images on host. + Hashes pseudo_out to the final_message. + + :param src_entr: + :param vini: tx.vin[i] + :param hmac: HMAC of tx.vin[i] + :param pseudo_out: pseudo_out for the current entry + :param pseudo_out_hmac: hmac of pseudo_out + :return: + """ + from trezor.messages.MoneroTransactionInputViniAck import ( + MoneroTransactionInputViniAck + ) + + await self.trezor.iface.transaction_step( + self.STEP_VINI, self.inp_idx + 1, self.num_inputs() + ) + + if self.in_memory(): + return + if self.inp_idx >= self.num_inputs(): + raise ValueError("Too many inputs") + + self.state.input_vins() + self.inp_idx += 1 + + # HMAC(T_in,i || vin_i) + hmac_vini = await self.gen_hmac_vini( + src_entr, vini, self.source_permutation[self.inp_idx] + ) + if not common.ct_equal(hmac_vini, hmac): + raise ValueError("HMAC is not correct") + + await self.hash_vini_pseudo_out(vini, self.inp_idx, pseudo_out, pseudo_out_hmac) + return MoneroTransactionInputViniAck() + + async def hash_vini_pseudo_out( + self, vini, inp_idx, pseudo_out=None, pseudo_out_hmac=None + ): + """ + Incremental hasing of tx.vin[i] and pseudo output + :param vini: + :param inp_idx: + :param pseudo_out: + :param pseudo_out_hmac: + :return: + """ + # Serialize particular input type + from apps.monero.xmr.serialize import xmrserialize + from apps.monero.xmr.serialize_messages.tx_prefix import TxInV + + self.tx_prefix_hasher.refresh(xser=xmrserialize) + + await self.tx_prefix_hasher.ar.field(vini, TxInV) + + # Pseudo_out incremental hashing - applicable only in simple rct + if not self.use_simple_rct: + return + + if not self.in_memory(): + idx = self.source_permutation[inp_idx] + pseudo_out_hmac_comp = crypto.compute_hmac( + self.hmac_key_txin_comm(idx), pseudo_out + ) + if not common.ct_equal(pseudo_out_hmac, pseudo_out_hmac_comp): + raise ValueError("HMAC invalid for pseudo outs") + else: + pseudo_out = self.input_pseudo_outs[inp_idx] + + await self.full_message_hasher.set_pseudo_out(pseudo_out) + + async def commitment(self, in_amount): + """ + Computes Pedersen commitment - pseudo outs + Here is slight deviation from the original protocol. + We want that \sum Alpha = \sum A_{i,j} where A_{i,j} is a mask from range proof for output i, bit j. + + Previously this was computed in such a way that Alpha_{last} = \sum A{i,j} - \sum_{i=0}^{last-1} Alpha + But we would prefer to compute commitment before range proofs so alphas are generated completely randomly + and the last A mask is computed in this special way. + Returns pseudo_out + :return: + """ + alpha = crypto.random_scalar() + self.sumpouts_alphas = crypto.sc_add(self.sumpouts_alphas, alpha) + return alpha, crypto.gen_c(alpha, in_amount) + + async def range_proof(self, idx, dest_pub_key, amount, amount_key): + """ + Computes rangeproof and related information - out_sk, out_pk, ecdh_info. + In order to optimize incremental transaction build, the mask computation is changed compared + to the official Monero code. In the official code, the input pedersen commitments are computed + after range proof in such a way summed masks for commitments (alpha) and rangeproofs (ai) are equal. + + In order to save roundtrips we compute commitments randomly and then for the last rangeproof + a[63] = (\sum_{i=0}^{num_inp}alpha_i - \sum_{i=0}^{num_outs-1} amasks_i) - \sum_{i=0}^{62}a_i + + The range proof is incrementally hashed to the final_message. + + :param idx: + :param dest_pub_key: + :param amount: + :param amount_key: + :return: + """ + from apps.monero.xmr import ring_ct + + rsig = bytearray(32 * (64 + 64 + 64 + 1)) + rsig_mv = memoryview(rsig) + + out_pk = misc.StdObj(dest=dest_pub_key, mask=None) + is_last = idx + 1 == self.num_dests() + last_mask = ( + None + if not is_last or not self.use_simple_rct + else crypto.sc_sub(self.sumpouts_alphas, self.sumout) + ) + + # Pedersen commitment on the value, mask from the commitment, range signature. + C, mask, rsig = None, 0, None + + # Rangeproof + gc.collect() + if self.use_bulletproof: + raise ValueError("Bulletproof not yet supported") + + else: + C, mask, rsig = ring_ct.prove_range( + amount, last_mask, backend_impl=True, byte_enc=True, rsig=rsig_mv + ) + rsig = memoryview(rsig) + + self.assrt( + crypto.point_eq( + C, + crypto.point_add( + crypto.scalarmult_base(mask), crypto.scalarmult_h(amount) + ), + ), + "rproof", + ) + + # Incremental hashing + await self.full_message_hasher.rsig_val( + rsig, self.use_bulletproof, raw=True + ) + gc.collect() + self._log_trace("rproof") + + # Mask sum + out_pk.mask = crypto.encodepoint(C) + self.sumout = crypto.sc_add(self.sumout, mask) + self.output_sk.append(misc.StdObj(mask=mask)) + + # ECDH masking + from apps.monero.xmr.sub.recode import recode_ecdh + from apps.monero.xmr.serialize_messages.tx_ecdh import EcdhTuple + + ecdh_info = EcdhTuple(mask=mask, amount=crypto.sc_init(amount)) + ecdh_info = ring_ct.ecdh_encode( + ecdh_info, derivation=crypto.encodeint(amount_key) + ) + recode_ecdh(ecdh_info, encode=True) + gc.collect() + + return rsig, out_pk, ecdh_info + + async def _set_out1_prefix(self): + from apps.monero.xmr.serialize_messages.tx_prefix import TransactionPrefix + + await self.tx_prefix_hasher.ar.container_size( + self.num_dests(), TransactionPrefix.f_specs()[3][1] + ) + + async def _set_out1_additional_keys(self, dst_entr): + additional_txkey = None + additional_txkey_priv = None + if self.need_additional_txkeys: + use_provided = self.num_dests() == len(self.additional_tx_private_keys) + additional_txkey_priv = ( + self.additional_tx_private_keys[self.out_idx] + if use_provided + else crypto.random_scalar() + ) + + if dst_entr.is_subaddress: + additional_txkey = crypto.ge_scalarmult( + additional_txkey_priv, + crypto.decodepoint(dst_entr.addr.m_spend_public_key), + ) + else: + additional_txkey = crypto.ge_scalarmult_base(additional_txkey_priv) + + self.additional_tx_public_keys.append(crypto.encodepoint(additional_txkey)) + if not use_provided: + self.additional_tx_private_keys.append(additional_txkey_priv) + return additional_txkey_priv + + async def _set_out1_derivation(self, dst_entr, additional_txkey_priv): + from apps.monero.xmr.sub.addr import addr_eq + + change_addr = self.change_address() + if change_addr and addr_eq(dst_entr.addr, change_addr): + # sending change to yourself; derivation = a*R + derivation = monero.generate_key_derivation( + self.r_pub, self.creds.view_key_private + ) + + else: + # sending to the recipient; derivation = r*A (or s*C in the subaddress scheme) + deriv_priv = ( + additional_txkey_priv + if dst_entr.is_subaddress and self.need_additional_txkeys + else self.r + ) + derivation = monero.generate_key_derivation( + crypto.decodepoint(dst_entr.addr.m_view_public_key), deriv_priv + ) + return derivation + + async def set_out1(self, dst_entr, dst_entr_hmac): + """ + Set destination entry one by one. + Computes destination stealth address, amount key, range proof + HMAC, out_pk, ecdh_info. + + :param dst_entr + :type dst_entr: TxDestinationEntry + :param dst_entr_hmac + :return: + """ + from apps.monero.xmr.serialize import xmrserialize + + await self.trezor.iface.transaction_step( + self.STEP_OUT, self.out_idx + 1, self.num_dests() + ) + self._log_trace(1) + + if self.state.is_input_vins() and self.inp_idx + 1 != self.num_inputs(): + raise ValueError("Invalid number of inputs") + + self.state.set_output() + self.out_idx += 1 + self._log_trace(2) + + if dst_entr.amount <= 0 and self.tx.version <= 1: + raise ValueError("Destination with wrong amount: %s" % dst_entr.amount) + + # HMAC check of the destination + dst_entr_hmac_computed = await self.gen_hmac_tsxdest(dst_entr, self.out_idx) + if not common.ct_equal(dst_entr_hmac, dst_entr_hmac_computed): + raise ValueError("HMAC invalid") + gc.collect() + self._log_trace(3) + + # First output - tx prefix hasher - size of the container + self.tx_prefix_hasher.refresh(xser=xmrserialize) + if self.out_idx == 0: + await self._set_out1_prefix() + gc.collect() + + self._log_trace(4) + additional_txkey_priv = await self._set_out1_additional_keys(dst_entr) + derivation = await self._set_out1_derivation(dst_entr, additional_txkey_priv) + + gc.collect() + self._log_trace(5) + + amount_key = crypto.derivation_to_scalar(derivation, self.out_idx) + tx_out_key = crypto.derive_public_key( + derivation, + self.out_idx, + crypto.decodepoint(dst_entr.addr.m_spend_public_key), + ) + + from apps.monero.xmr.serialize_messages.tx_prefix import TxoutToKey + from apps.monero.xmr.serialize_messages.tx_prefix import TxOut + + tk = TxoutToKey(key=crypto.encodepoint(tx_out_key)) + tx_out = TxOut(amount=0, target=tk) + self.summary_outs_money += dst_entr.amount + self._log_trace(6) + + # Tx header prefix hashing + await self.tx_prefix_hasher.ar.field(tx_out, TxOut) + gc.collect() + + # Hmac dest_entr. + hmac_vouti = await self.gen_hmac_vouti(dst_entr, tx_out, self.out_idx) + gc.collect() + self._log_trace(7) + + # Range proof, out_pk, ecdh_info + rsig, out_pk, ecdh_info = await self.range_proof( + self.out_idx, + dest_pub_key=tk.key, + amount=dst_entr.amount, + amount_key=amount_key, + ) + gc.collect() + self._log_trace(8) + + # Incremental hashing of the ECDH info. + # RctSigBase allows to hash only one of the (ecdh, out_pk) as they are serialized + # as whole vectors. Hashing ECDH info saves state space. + await self.full_message_hasher.set_ecdh(ecdh_info) + self._log_trace(9) + + # Output_pk is stored to the state as it is used during the signature and hashed to the + # RctSigBase later. + self.output_pk.append(out_pk) + gc.collect() + + self._log_trace(10) + from trezor.messages.MoneroTransactionSetOutputAck import ( + MoneroTransactionSetOutputAck + ) + from apps.monero.xmr.serialize_messages.ct_keys import CtKey + + return MoneroTransactionSetOutputAck( + tx_out=await misc.dump_msg(tx_out, preallocate=34), + vouti_hmac=hmac_vouti, + rsig=rsig, # rsig is already byte-encoded + out_pk=await misc.dump_msg(out_pk, preallocate=64, msg_type=CtKey), + ecdh_info=await misc.dump_msg(ecdh_info, preallocate=64), + ) + + async def all_out1_set_tx_extra(self): + from apps.monero.xmr.sub import tsx_helper + + self.tx.extra = tsx_helper.add_tx_pub_key_to_extra(self.tx.extra, self.r_pub) + + # Not needed to remove - extra is clean + # self.tx.extra = await monero.remove_field_from_tx_extra(self.tx.extra, xmrtypes.TxExtraAdditionalPubKeys) + if self.need_additional_txkeys: + self.tx.extra = await tsx_helper.add_additional_tx_pub_keys_to_extra( + self.tx.extra, pub_enc=self.additional_tx_public_keys + ) + + async def all_out1_set_tx_prefix(self): + from apps.monero.xmr.serialize.message_types import BlobType + + await self.tx_prefix_hasher.ar.message_field( + self.tx, ("extra", BlobType) + ) # extra + + self.tx_prefix_hash = self.tx_prefix_hasher.kwriter.get_digest() + self.tx_prefix_hasher = None + + # Hash message to the final_message + await self.full_message_hasher.set_message(self.tx_prefix_hash) + + async def all_out1_set(self): + """ + All outputs were set in this phase. Computes additional public keys (if needed), tx.extra and + transaction prefix hash. + Adds additional public keys to the tx.extra + + :return: tx.extra, tx_prefix_hash + """ + self._log_trace(0) + self.state.set_output_done() + await self.trezor.iface.transaction_step(self.STEP_ALL_OUT) + self._log_trace(1) + + if self.out_idx + 1 != self.num_dests(): + raise ValueError("Invalid out num") + + # Test if \sum Alpha == \sum A + if self.use_simple_rct: + self.assrt(crypto.sc_eq(self.sumout, self.sumpouts_alphas)) + + # Fee test + if self.fee != (self.summary_inputs_money - self.summary_outs_money): + raise ValueError( + "Fee invalid %s vs %s, out: %s" + % ( + self.fee, + self.summary_inputs_money - self.summary_outs_money, + self.summary_outs_money, + ) + ) + self._log_trace(2) + + # Set public key to the extra + # Not needed to remove - extra is clean + await self.all_out1_set_tx_extra() + self.additional_tx_public_keys = None + + gc.collect() + self._log_trace(3) + + if self.summary_outs_money > self.summary_inputs_money: + raise ValueError( + "Transaction inputs money (%s) less than outputs money (%s)" + % (self.summary_inputs_money, self.summary_outs_money) + ) + + # Hashing transaction prefix + await self.all_out1_set_tx_prefix() + extra_b = self.tx.extra + self.tx = None + gc.collect() + self._log_trace(4) + + # Txprefix match check for multisig + if not common.is_empty(self.exp_tx_prefix_hash) and not common.ct_equal( + self.exp_tx_prefix_hash, self.tx_prefix_hash + ): + self.state.set_fail() + raise misc.TrezorTxPrefixHashNotMatchingError("Tx prefix invalid") + + gc.collect() + self._log_trace(5) + + from trezor.messages.MoneroRingCtSig import MoneroRingCtSig + from trezor.messages.MoneroTransactionAllOutSetAck import ( + MoneroTransactionAllOutSetAck + ) + + rv = self.init_rct_sig() + rv_pb = MoneroRingCtSig(txn_fee=rv.txnFee, message=rv.message, rv_type=rv.type) + return MoneroTransactionAllOutSetAck( + extra=extra_b, tx_prefix_hash=self.tx_prefix_hash, rv=rv_pb + ) + + async def tsx_mlsag_ecdh_info(self): + """ + Sets ecdh info for the incremental hashing mlsag. + + :return: + """ + pass + + async def tsx_mlsag_out_pk(self): + """ + Sets out_pk for the incremental hashing mlsag. + + :return: + """ + if self.num_dests() != len(self.output_pk): + raise ValueError("Invalid number of ecdh") + + for out in self.output_pk: + await self.full_message_hasher.set_out_pk(out) + + async def mlsag_done(self): + """ + MLSAG message computed. + + :return: + """ + from trezor.messages.MoneroTransactionMlsagDoneAck import ( + MoneroTransactionMlsagDoneAck + ) + + self.state.set_final_message_done() + await self.trezor.iface.transaction_step(self.STEP_MLSAG) + + await self.tsx_mlsag_ecdh_info() + await self.tsx_mlsag_out_pk() + await self.full_message_hasher.rctsig_base_done() + self.out_idx = -1 + self.inp_idx = -1 + + self.full_message = await self.full_message_hasher.get_digest() + self.full_message_hasher = None + + return MoneroTransactionMlsagDoneAck(full_message_hash=self.full_message) + + async def sign_input( + self, + src_entr, + vini, + hmac_vini, + pseudo_out, + pseudo_out_hmac, + alpha_enc, + spend_enc, + ): + """ + Generates a signature for one input. + + :param src_entr: Source entry + :type src_entr: apps.monero.xmr.serialize_messages.tx_construct.TxSourceEntry + :param vini: tx.vin[i] for the transaction. Contains key image, offsets, amount (usually zero) + :param hmac_vini: HMAC for the tx.vin[i] as returned from Trezor + :param pseudo_out: pedersen commitment for the current input, uses alpha as the mask. + Only in memory offloaded scenario. Tuple containing HMAC, as returned from the Trezor. + :param pseudo_out_hmac: + :param alpha_enc: alpha mask for the current input. Only in memory offloaded scenario, + tuple as returned from the Trezor + :param spend_enc: + :return: Generated signature MGs[i] + """ + self.state.set_signature() + await self.trezor.iface.transaction_step( + self.STEP_SIGN, self.inp_idx + 1, self.num_inputs() + ) + + self.inp_idx += 1 + if self.inp_idx >= self.num_inputs(): + raise ValueError("Invalid ins") + if self.use_simple_rct and (not self.in_memory() and alpha_enc is None): + raise ValueError("Inconsistent1") + if self.use_simple_rct and (not self.in_memory() and pseudo_out is None): + raise ValueError("Inconsistent2") + if self.inp_idx >= 1 and not self.use_simple_rct: + raise ValueError("Inconsistent3") + + inv_idx = self.source_permutation[self.inp_idx] + + # Check HMAC of all inputs + hmac_vini_comp = await self.gen_hmac_vini(src_entr, vini, inv_idx) + if not common.ct_equal(hmac_vini_comp, hmac_vini): + raise ValueError("HMAC is not correct") + + gc.collect() + self._log_trace(1) + + if self.use_simple_rct and not self.in_memory(): + pseudo_out_hmac_comp = crypto.compute_hmac( + self.hmac_key_txin_comm(inv_idx), pseudo_out + ) + if not common.ct_equal(pseudo_out_hmac_comp, pseudo_out_hmac): + raise ValueError("HMAC is not correct") + + gc.collect() + self._log_trace(2) + + from apps.monero.xmr.enc import chacha_poly + + alpha_c = crypto.decodeint( + chacha_poly.decrypt_pack( + self.enc_key_txin_alpha(inv_idx), bytes(alpha_enc) + ) + ) + pseudo_out_c = crypto.decodepoint(pseudo_out) + + elif self.use_simple_rct: + alpha_c = self.input_alphas[self.inp_idx] + pseudo_out_c = crypto.decodepoint(self.input_pseudo_outs[self.inp_idx]) + + else: + alpha_c = None + pseudo_out_c = None + + # Spending secret + if self.many_inputs(): + from apps.monero.xmr.enc import chacha_poly + + input_secret = crypto.decodeint( + chacha_poly.decrypt_pack(self.enc_key_spend(inv_idx), bytes(spend_enc)) + ) + else: + input_secret = self.input_secrets[self.inp_idx] + + gc.collect() + self._log_trace(3) + + # Basic setup, sanity check + index = src_entr.real_output + in_sk = misc.StdObj(dest=input_secret, mask=crypto.decodeint(src_entr.mask)) + kLRki = src_entr.multisig_kLRki if self.multi_sig else None + + # Private key correctness test + self.assrt( + crypto.point_eq( + crypto.decodepoint(src_entr.outputs[src_entr.real_output][1].dest), + crypto.scalarmult_base(in_sk.dest), + ), + "a1", + ) + self.assrt( + crypto.point_eq( + crypto.decodepoint(src_entr.outputs[src_entr.real_output][1].mask), + crypto.gen_c(in_sk.mask, src_entr.amount), + ), + "a2", + ) + + gc.collect() + self._log_trace(4) + + # RCT signature + gc.collect() + from apps.monero.xmr import mlsag2 + + mg = None + if self.use_simple_rct: + # Simple RingCT + mix_ring = [x[1] for x in src_entr.outputs] + mg, msc = mlsag2.prove_rct_mg_simple( + self.full_message, + mix_ring, + in_sk, + alpha_c, + pseudo_out_c, + kLRki, + None, + index, + ) + + else: + # Full RingCt, only one input + txn_fee_key = crypto.scalarmult_h(self.get_fee()) + mix_ring = [[x[1]] for x in src_entr.outputs] + + mg, msc = mlsag2.prove_rct_mg( + self.full_message, + mix_ring, + [in_sk], + self.output_sk, + self.output_pk, + kLRki, + None, + index, + txn_fee_key, + ) + + gc.collect() + self._log_trace(5) + + # Encode + from apps.monero.xmr.sub.recode import recode_msg + + mgs = recode_msg([mg]) + cout = None + + gc.collect() + self._log_trace(6) + + # Multisig values returned encrypted, keys returned after finished successfully. + if self.multi_sig: + from apps.monero.xmr.enc import chacha_poly + + cout = chacha_poly.encrypt_pack(self.enc_key_cout(), crypto.encodeint(msc)) + + # Final state transition + if self.inp_idx + 1 == self.num_inputs(): + self.state.set_signature_done() + await self.trezor.iface.transaction_signed() + + gc.collect() + self._log_trace() + + from trezor.messages.MoneroTransactionSignInputAck import ( + MoneroTransactionSignInputAck + ) + + return MoneroTransactionSignInputAck( + signature=await misc.dump_msg_gc(mgs[0], preallocate=488, del_msg=True), + cout=cout, + ) + + async def final_msg(self, *args, **kwargs): + """ + Final step after transaction signing. + + :param args: + :param kwargs: + :return: + """ + from trezor.messages.MoneroTransactionFinalAck import MoneroTransactionFinalAck + from apps.monero.xmr.enc import chacha_poly + + self.state.set_final() + + cout_key = self.enc_key_cout() if self.multi_sig else None + + # Encrypted tx keys under transaction specific key, derived from txhash and spend key. + # Deterministic transaction key, so we can recover it just from transaction and the spend key. + tx_key, salt, rand_mult = misc.compute_tx_key( + self.creds.spend_key_private, self.tx_prefix_hash + ) + + key_buff = crypto.encodeint(self.r) + b"".join( + [crypto.encodeint(x) for x in self.additional_tx_private_keys] + ) + tx_enc_keys = chacha_poly.encrypt_pack(tx_key, key_buff) + + await self.trezor.iface.transaction_finished() + gc.collect() + + return MoneroTransactionFinalAck( + cout_key=cout_key, salt=salt, rand_mult=rand_mult, tx_enc_keys=tx_enc_keys + ) diff --git a/src/apps/monero/protocol/tsx_sign_state.py b/src/apps/monero/protocol/tsx_sign_state.py new file mode 100644 index 000000000..54d447fe5 --- /dev/null +++ b/src/apps/monero/protocol/tsx_sign_state.py @@ -0,0 +1,105 @@ +from micropython import const + + +class TState(object): + """ + Transaction state + """ + + START = const(0) + INIT = const(1) + INP_CNT = const(2) + INPUT = const(3) + INPUT_DONE = const(4) + INPUT_PERM = const(5) + INPUT_VINS = const(6) + OUTPUT = const(7) + OUTPUT_DONE = const(8) + FINAL_MESSAGE = const(9) + SIGNATURE = const(10) + SIGNATURE_DONE = const(11) + FINAL = const(12) + FAIL = const(250) + + def __init__(self): + self.s = self.START + self.in_mem = False + + def state_save(self): + return self.s, self.in_mem + + def state_load(self, x): + self.s, self.in_mem = x + + def init_tsx(self): + if self.s != self.START: + raise ValueError("Illegal state") + self.s = self.INIT + + def inp_cnt(self, in_mem): + if self.s != self.INIT: + raise ValueError("Illegal state") + self.s = self.INP_CNT + self.in_mem = in_mem + + def input(self): + if self.s != self.INP_CNT and self.s != self.INPUT: + raise ValueError("Illegal state") + self.s = self.INPUT + + def input_done(self): + if self.s != self.INPUT: + raise ValueError("Illegal state") + self.s = self.INPUT_DONE + + def input_permutation(self): + if self.s != self.INPUT_DONE: + raise ValueError("Illegal state") + self.s = self.INPUT_PERM + + def input_vins(self): + if self.s != self.INPUT_PERM and self.s != self.INPUT_VINS: + raise ValueError("Illegal state") + self.s = self.INPUT_VINS + + def is_input_vins(self): + return self.s == self.INPUT_VINS + + def set_output(self): + if ( + (not self.in_mem and self.s != self.INPUT_VINS) + or (self.in_mem and self.s != self.INPUT_PERM) + ) and self.s != self.OUTPUT: + raise ValueError("Illegal state") + self.s = self.OUTPUT + + def set_output_done(self): + if self.s != self.OUTPUT: + raise ValueError("Illegal state") + self.s = self.OUTPUT_DONE + + def set_final_message_done(self): + if self.s != self.OUTPUT_DONE: + raise ValueError("Illegal state") + self.s = self.FINAL_MESSAGE + + def set_signature(self): + if self.s != self.FINAL_MESSAGE and self.s != self.SIGNATURE: + raise ValueError("Illegal state") + self.s = self.SIGNATURE + + def set_signature_done(self): + if self.s != self.SIGNATURE: + raise ValueError("Illegal state") + self.s = self.SIGNATURE_DONE + + def set_final(self): + if self.s != self.SIGNATURE_DONE: + raise ValueError("Illegal state") + self.s = self.FINAL + + def set_fail(self): + self.s = self.FAIL + + def is_terminal(self): + return self.s in [self.FINAL, self.FAIL] diff --git a/src/apps/monero/protocol/tsx_sign_state_holder.py b/src/apps/monero/protocol/tsx_sign_state_holder.py new file mode 100644 index 000000000..3d0cd12fa --- /dev/null +++ b/src/apps/monero/protocol/tsx_sign_state_holder.py @@ -0,0 +1,50 @@ +class TsxSignStateHolder(object): + """ + Simple transaction signer state holder. + Externalized state uses smaller amount of memory compared to storing the builder instance in the state. + Moreover the state contains stripped down attributes, i.e., instead of heavy hashers only sha3 context + is preserved and hashers are re-initialized on the next protocol step. + """ + + def __init__(self, **kwargs): + self.creds = None + self.key_master = None + self.key_hmac = None + self.key_enc = None + + self.r = None # txkey + self.r_pub = None + self.state = None + + self.multi_sig = False + self.need_additional_txkeys = False + self.use_bulletproof = False + self.use_rct = True + self.use_simple_rct = False + self.input_count = 0 + self.output_count = 0 + self.output_change = None + self.mixin = 0 + self.fee = 0 + + self.additional_tx_private_keys = [] + self.additional_tx_public_keys = [] + self.inp_idx = -1 + self.out_idx = -1 + self.summary_inputs_money = 0 + self.summary_outs_money = 0 + self.input_secrets = [] + self.input_alphas = [] + self.input_pseudo_outs = [] + self.output_sk = [] + self.output_pk = [] + self.sumout = None + self.sumpouts_alphas = None + self.subaddresses = {} + self.tx = None + self.source_permutation = [] # sorted by key images + self.tx_prefix_hasher = None + self.tx_prefix_hash = None + self.full_message_hasher = None + self.full_message = None + self.exp_tx_prefix_hash = None diff --git a/src/apps/monero/sign_tx.py b/src/apps/monero/sign_tx.py new file mode 100644 index 000000000..8541a5ce9 --- /dev/null +++ b/src/apps/monero/sign_tx.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + +import gc +import micropython + +from trezor import log + + +async def layout_sign_tx(state, ctx, msg): + gc.threshold(gc.mem_free() // 4 + gc.mem_alloc()) + log.debug( + __name__, + "############################ TSX. Free: {} Allocated: {} thr: {}".format( + gc.mem_free(), gc.mem_alloc(), gc.mem_free() // 4 + gc.mem_alloc() + ), + ) + gc.collect() + micropython.mem_info() + + from apps.monero.protocol.tsx_sign import TsxSigner + + log.debug( + __name__, + "TsxSigner. Free: {} Allocated: {}".format(gc.mem_free(), gc.mem_alloc()), + ) + log.debug(__name__, "TsxState: %s", state.ctx_sign) + gc.collect() + + try: + signer = TsxSigner() + res = await signer.sign(ctx, state.ctx_sign, msg) + if await signer.should_purge(): + state.ctx_sign = None + else: + state.ctx_sign = await signer.state_save() + + return res + + except Exception as e: + state.ctx_sign = None + log.error(__name__, "Tsx exception: %s %s", type(e), e) + raise diff --git a/src/apps/monero/xmr/__init__.py b/src/apps/monero/xmr/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/monero/xmr/common.py b/src/apps/monero/xmr/common.py new file mode 100644 index 000000000..c0e52c580 --- /dev/null +++ b/src/apps/monero/xmr/common.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + + +from trezor.crypto import monero, random + + +class XmrException(Exception): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +def random_bytes(by): + """ + Generates X random bytes, returns byte-string + :param by: + :return: + """ + return random.bytes(by) + + +def ct_equal(a, b): + """ + Constant time a,b comparison + :param a: + :param b: + :return: + """ + return monero.ct_equals(a, b) + + +def memcpy(dst, dst_from, src, src_from, length): + from trezor.utils import memcpy + + return memcpy(dst, dst_from, src, src_from, length) + + +def check_permutation(permutation): + """ + Check permutation sanity + :param permutation: + :return: + """ + for n in range(len(permutation)): + if n not in permutation: + raise ValueError("Invalid permutation") + + +def apply_permutation(permutation, swapper): + """ + Apply permutation from idx. Used for in-place permutation application with swapper. + Ported from Monero. + :param permutation: + :param swapper: function(x,y) + :return: + """ + check_permutation(permutation) + perm = list(permutation) + for i in range(len(perm)): + current = i + while i != perm[current]: + nxt = perm[current] + swapper(current, nxt) + perm[current] = current + current = nxt + perm[current] = current + + +def is_empty(inp): + """ + True if none or empty + :param inp: + :return: + """ + return inp is None or len(inp) == 0 + + +def defval(val, default=None): + """ + Returns val if is not None, default instead + :param val: + :param default: + :return: + """ + return val if val is not None else default + + +def defval_empty(val, default=None): + """ + Returns val if is not None, default instead + :param val: + :param default: + :return: + """ + return val if not is_empty(val) else default + + +def chunk(arr, size=1): + res = [] + idx = 0 + while True: + c = arr[idx : idx + size] + res.append(c) + idx += size + if len(c) != size: + break + return res diff --git a/src/apps/monero/xmr/crypto.py b/src/apps/monero/xmr/crypto.py new file mode 100644 index 000000000..7fa8908f1 --- /dev/null +++ b/src/apps/monero/xmr/crypto.py @@ -0,0 +1,647 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 +# +# Resources: +# https://cr.yp.to +# https://github.com/monero-project/mininero +# https://godoc.org/github.com/agl/ed25519/edwards25519 +# https://tools.ietf.org/html/draft-josefsson-eddsa-ed25519-00#section-4 +# https://github.com/monero-project/research-lab + +import ubinascii as binascii + +from trezor.crypto import hmac, monero as tcry, pbkdf2 as tpbkdf2, random +from trezor.crypto.hashlib import sha3_256 + +NULL_KEY_ENC = [0] * 32 + + +def random_bytes(by): + """ + Generates X random bytes, returns byte-string + :param by: + :return: + """ + return random.bytes(by) + + +def keccak_factory(data=None): + return sha3_256(data=data, keccak=True) + + +def get_keccak(): + """ + Simple keccak 256 + :return: + """ + return keccak_factory() + + +def keccak_hash(inp): + """ + Hashesh input in one call + :return: + """ + return tcry.xmr_fast_hash(inp) + + +def keccak_2hash(inp): + """ + Keccak double hashing + :param inp: + :return: + """ + return keccak_hash(keccak_hash(inp)) + + +def get_hmac(key, msg=None): + """ + Returns HMAC object (uses Keccak256) + :param key: + :param msg: + :return: + """ + return hmac.new(key, msg=msg, digestmod=keccak_factory) + + +def compute_hmac(key, msg=None): + """ + Computes and returns HMAC of the msg using Keccak256 + :param key: + :param msg: + :return: + """ + h = hmac.new(key, msg=msg, digestmod=keccak_factory) + return h.digest() + + +def pbkdf2(inp, salt, length=32, count=1000, prf=None): + """ + PBKDF2 with default PRF as HMAC-KECCAK-256 + :param inp: + :param salt: + :param length: + :param count: + :param prf: + :return: + """ + pb = tpbkdf2("hmac-sha256", inp, salt) + pb.update(count) + return pb.key() + + +# +# EC +# + + +def decodepoint(x): + return tcry.ge25519_unpack_vartime(x) + + +def encodepoint(pt): + return tcry.ge25519_pack(pt) + + +def encodepoint_into(pt, b): + return tcry.ge25519_pack_into(pt, b) + + +def decodeint(x): + return tcry.unpack256_modm(x) + + +def encodeint(x): + return tcry.pack256_modm(x) + + +def encodeint_into(x, b): + return tcry.pack256_modm_into(x, b) + + +def check_ed25519point(x): + return tcry.ge25519_check(x) + + +def scalarmult_base(a): + return tcry.ge25519_scalarmult_base(a) + + +def scalarmult(P, e): + return tcry.ge25519_scalarmult(P, e) + + +def point_add(P, Q): + return tcry.ge25519_add(P, Q, 0) + + +def point_sub(P, Q): + return tcry.ge25519_add(P, Q, 1) + + +def point_eq(P, Q): + return tcry.ge25519_eq(P, Q) + + +def point_double(P): + return tcry.ge25519_double(P) + + +def point_norm(P): + """ + Normalizes point after multiplication + Extended edwards coordinates (X,Y,Z,T) + :param P: + :return: + """ + return tcry.ge25519_norm(P) + + +# +# Zmod(order), scalar values field +# + + +def sc_0(): + """ + Sets 0 to the scalar value Zmod(m) + :return: + """ + return tcry.init256_modm(0) + + +def sc_init(x): + """ + Sets x to the scalar value Zmod(m) + :return: + """ + if x >= (1 << 64): + raise ValueError("Initialization works up to 64-bit only") + return tcry.init256_modm(x) + + +def sc_get64(x): + """ + Returns 64bit value from the sc + :param x: + :return: + """ + return tcry.get256_modm(x) + + +def sc_check(key): + """ + sc_check is not relevant for long-integer scalar representation. + + :param key: + :return: + """ + tcry.check256_modm(key) + return 0 + + +def check_sc(key): + """ + throws exception on invalid key + :param key: + :return: + """ + if sc_check(key) != 0: + raise ValueError("Invalid scalar value") + + +def sc_reduce32(data): + """ + Exactly the same as sc_reduce (which is default lib sodium) + except it is assumed that your input s is alread in the form: + s[0]+256*s[1]+...+256^31*s[31] = s + + And the rest is reducing mod l, + so basically take a 32 byte input, and reduce modulo the prime. + :param data: + :return: + """ + return tcry.reduce256_modm(data) + + +def sc_add(aa, bb): + """ + Scalar addition + :param aa: + :param bb: + :return: + """ + return tcry.add256_modm(aa, bb) + + +def sc_sub(aa, bb): + """ + Scalar subtraction + :param aa: + :param bb: + :return: + """ + return tcry.sub256_modm(aa, bb) + + +def sc_isnonzero(c): + """ + Returns true if scalar is non-zero + :param c: + :return: + """ + return not tcry.iszero256_modm(c) + + +def sc_eq(a, b): + """ + Returns true if scalars are equal + :param a: + :param b: + :return: + """ + return tcry.eq256_modm(a, b) + + +def sc_mulsub(aa, bb, cc): + """ + (cc - aa * bb) % l + :param aa: + :param bb: + :param cc: + :return: + """ + return tcry.mulsub256_modm(aa, bb, cc) + + +def random_scalar(): + return tcry.xmr_random_scalar() + + +# +# GE - ed25519 group +# + + +def ge_scalarmult(a, A): + check_ed25519point(A) + return scalarmult(A, a) + + +def ge_mul8(P): + check_ed25519point(P) + return tcry.ge25519_mul8(P) + + +def ge_scalarmult_base(a): + a = sc_reduce32(a) + return scalarmult_base(a) + + +def ge_double_scalarmult_base_vartime(a, A, b): + """ + void ge25519_double_scalarmult_vartime(ge25519 *r, const ge25519 *p1, const bignum256modm s1, const bignum256modm s2); + r = a * A + b * B + where a = a[0]+256*a[1]+...+256^31 a[31]. + and b = b[0]+256*b[1]+...+256^31 b[31]. + B is the Ed25519 base point (x,4/5) with x positive. + + :param a: + :param A: + :param b: + :return: + """ + R = tcry.ge25519_double_scalarmult_vartime(A, a, b) + tcry.ge25519_norm(R, R) + return R + + +def ge_double_scalarmult_base_vartime2(a, A, b, B): + """ + void ge25519_double_scalarmult_vartime2(ge25519 *r, const ge25519 *p1, const bignum256modm s1, const ge25519 *p2, const bignum256modm s2); + r = a * A + b * B + + :param a: + :param A: + :param b: + :param B: + :return: + """ + R = tcry.ge25519_double_scalarmult_vartime2(A, a, B, b) + tcry.ge25519_norm(R, R) + return R + + +def ge_double_scalarmult_precomp_vartime(a, A, b, Bi): + """ + void ge_double_scalarmult_precomp_vartime(ge_p2 *r, const unsigned char *a, const ge_p3 *A, const unsigned char *b, const ge_dsmp Bi) + :return: + """ + return ge_double_scalarmult_precomp_vartime2(a, A, b, Bi) + + +def ge_double_scalarmult_precomp_vartime2(a, Ai, b, Bi): + """ + void ge_double_scalarmult_precomp_vartime2(ge_p2 *r, const unsigned char *a, const ge_dsmp Ai, const unsigned char *b, const ge_dsmp Bi) + :param a: + :param Ai: + :param b: + :param Bi: + :return: + """ + return tcry.xmr_add_keys3(a, Ai, b, Bi) + + +def identity(byte_enc=False): + """ + Identity point + :return: + """ + idd = tcry.ge25519_set_neutral() + return idd if not byte_enc else encodepoint(idd) + + +def ge_frombytes_vartime_check(point): + """ + https://www.imperialviolet.org/2013/12/25/elligator.html + http://elligator.cr.yp.to/ + http://elligator.cr.yp.to/elligator-20130828.pdf + + Basically it takes some bytes of data + converts to a point on the edwards curve + if the bytes aren't on the curve + also does some checking on the numbers + ex. your secret key has to be at least >= 4294967277 + also it rejects certain curve points, i.e. "if x = 0, sign must be positive" + + sqrt(s) = s^((q+3) / 8) if s^((q+3)/4) == s + = sqrt(-1) s ^((q+3) / 8) otherwise + + :param point: + :return: + """ + # if tcry.ge25519_check(point) != 1: + # raise ValueError('Point check failed') + # + # return 0 + tcry.ge25519_check(point) + return 0 + + +def ge_frombytes_vartime(point): + """ + https://www.imperialviolet.org/2013/12/25/elligator.html + + :param point: + :return: + """ + ge_frombytes_vartime_check(point) + return point + + +def precomp(point): + """ + Precomputation placeholder + :param point: + :return: + """ + return point + + +def ge_dsm_precomp(point): + """ + void ge_dsm_precomp(ge_dsmp r, const ge_p3 *s) + :param point: + :return: + """ + return point + + +# +# Monero specific +# + + +def cn_fast_hash(buff): + """ + Keccak 256, original one (before changes made in SHA3 standard) + :param buff: + :return: + """ + return keccak_hash(buff) + + +def hash_to_scalar(data, length=None): + """ + H_s(P) + :param data: + :param length: + :return: + """ + dt = data[:length] if length else data + return tcry.xmr_hash_to_scalar(bytes(dt)) + + +def hash_to_ec(buf): + """ + H_p(buf) + + Code adapted from MiniNero: https://github.com/monero-project/mininero + https://github.com/monero-project/research-lab/blob/master/whitepaper/ge_fromfe_writeup/ge_fromfe.pdf + http://archive.is/yfINb + :param buf: + :return: + """ + return tcry.xmr_hash_to_ec(buf) + + +# +# XMR +# + + +def gen_H(): + """ + Returns point H + 8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94 + :return: + """ + return tcry.ge25519_set_h() + + +def scalarmult_h(i): + return scalarmult(gen_H(), sc_init(i) if isinstance(i, int) else i) + + +def add_keys2(a, b, B): + """ + aG + bB, G is basepoint + :param a: + :param b: + :param B: + :return: + """ + return tcry.xmr_add_keys2_vartime(a, b, B) + + +def add_keys3(a, A, b, B): + """ + aA + bB + :param a: + :param A: + :param b: + :param B: + :return: + """ + return tcry.xmr_add_keys3_vartime(a, A, b, B) + + +def gen_c(a, amount): + """ + Generates Pedersen commitment + C = aG + bH + + :param a: + :param amount: + :return: + """ + return tcry.xmr_gen_c(a, amount) + + +def generate_key_derivation(key1, key2): + """ + Key derivation: 8*(key2*key1) + + :param key1: public key of receiver Bob (see page 7) + :param key2: Alice's private + :return: + """ + if sc_check(key2) != 0: + # checks that the secret key is uniform enough... + raise ValueError("error in sc_check in keyder") + if ge_frombytes_vartime_check(key1) != 0: + raise ValueError("didn't pass curve checks in keyder") + + return tcry.xmr_generate_key_derivation(key1, key2) + + +def derivation_to_scalar(derivation, output_index): + """ + H_s(derivation || varint(output_index)) + :param derivation: + :param output_index: + :return: + """ + check_ed25519point(derivation) + return tcry.xmr_derivation_to_scalar(derivation, output_index) + + +def derive_public_key(derivation, output_index, base): + """ + H_s(derivation || varint(output_index))G + base + + :param derivation: + :param output_index: + :param base: + :return: + """ + if ge_frombytes_vartime_check(base) != 0: # check some conditions on the point + raise ValueError("derive pub key bad point") + check_ed25519point(base) + + return tcry.xmr_derive_public_key(derivation, output_index, base) + + +def derive_secret_key(derivation, output_index, base): + """ + base + H_s(derivation || varint(output_index)) + :param derivation: + :param output_index: + :param base: + :return: + """ + if sc_check(base) != 0: + raise ValueError("cs_check in derive_secret_key") + return tcry.xmr_derive_private_key(derivation, output_index, base) + + +def prove_range(amount, last_mask=None, *args, **kwargs): + """ + Range proof provided by the backend. Implemented in C for speed. + + :param amount: + :param last_mask: + :return: + """ + C, a, R = tcry.gen_range_proof(amount, last_mask, *args, **kwargs) + + # Trezor micropython extmod returns byte-serialized/flattened rsig + return C, a, R + + +def b16_to_scalar(bts): + """ + Converts hexcoded bytearray to the scalar + :param bts: + :return: + """ + return decodeint(binascii.unhexlify(bts)) + + +# +# Repr invariant +# + + +def hmac_point(key, point): + """ + HMAC single point + :param key: + :param point: + :return: + """ + return compute_hmac(key, encodepoint(point)) + + +def generate_signature(data, priv): + """ + Generate EC signature + crypto_ops::generate_signature(const hash &prefix_hash, const public_key &pub, const secret_key &sec, signature &sig) + + :param data: + :param priv: + :return: + """ + pub = scalarmult_base(priv) + + k = random_scalar() + comm = scalarmult_base(k) + + buff = data + encodepoint(pub) + encodepoint(comm) + c = hash_to_scalar(buff) + r = sc_mulsub(priv, c, k) + return c, r, pub + + +def check_signature(data, c, r, pub): + """ + EC signature verification + + :param data: + :param pub: + :param c: + :param r: + :return: + """ + check_ed25519point(pub) + c = sc_reduce32(c) + r = sc_reduce32(r) + if sc_check(c) != 0 or sc_check(r) != 0: + raise ValueError("Signature error") + + tmp2 = point_add(scalarmult(pub, c), scalarmult_base(r)) + buff = data + encodepoint(pub) + encodepoint(tmp2) + tmp_c = hash_to_scalar(buff) + res = sc_sub(tmp_c, c) + return not sc_isnonzero(res) diff --git a/src/apps/monero/xmr/enc/__init__.py b/src/apps/monero/xmr/enc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/monero/xmr/enc/chacha_poly.py b/src/apps/monero/xmr/enc/chacha_poly.py new file mode 100644 index 000000000..73964fb23 --- /dev/null +++ b/src/apps/monero/xmr/enc/chacha_poly.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + +from trezor.crypto import chacha20poly1305 as ChaCha20Poly1305, monero, random + + +def encrypt(key, plaintext, associated_data=None): + """ + Uses ChaCha20Poly1305 for encryption + :param key: + :param plaintext: + :param associated_data: + :return: iv, ciphertext, tag + """ + nonce = random.bytes(12) + cipher = ChaCha20Poly1305(key, nonce) + if associated_data: + cipher.auth(associated_data) + ciphertext = cipher.encrypt(plaintext) + tag = cipher.finish() + return nonce, ciphertext + tag, b"" + + +def decrypt(key, iv, ciphertext, tag=None, associated_data=None): + """ + ChaCha20Poly1305 decryption + :param key: + :param iv: + :param ciphertext: + :param tag: + :param associated_data: + :return: + """ + cipher = ChaCha20Poly1305(key, iv) + if associated_data: + cipher.auth(associated_data) + exp_tag, ciphertext = ciphertext[-16:], ciphertext[:-16] + plaintext = cipher.decrypt(ciphertext) + tag = cipher.finish() + if not monero.ct_equals(tag, exp_tag): + raise ValueError("tag invalid") + + return plaintext + + +def encrypt_pack(key, plaintext, associated_data=None): + b = encrypt(key, plaintext, associated_data) + return b[0] + b[1] + + +def decrypt_pack(key, ciphertext): + cp = memoryview(ciphertext) + return decrypt(key, cp[:12], cp[12:], None) diff --git a/src/apps/monero/xmr/key_image.py b/src/apps/monero/xmr/key_image.py new file mode 100644 index 000000000..347fca6ea --- /dev/null +++ b/src/apps/monero/xmr/key_image.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + +from apps.monero.xmr import common, crypto, ring_ct +from apps.monero.xmr.serialize.int_serialize import dump_uvarint_b + + +def compute_hash(rr): + """ + Hash over output to ki-sync + :param rr: + :type rr: TransferDetails + :return: + """ + kck = crypto.get_keccak() + kck.update(rr.out_key) + kck.update(rr.tx_pub_key) + if rr.additional_tx_pub_keys: + for x in rr.additional_tx_pub_keys: + kck.update(x) + kck.update(dump_uvarint_b(rr.internal_output_index)) + return kck.digest() + + +async def export_key_image(creds, subaddresses, td): + """ + Key image export + :param creds: + :param subaddresses: + :param td: + :return: + """ + out_key = crypto.decodepoint(td.out_key) + tx_pub_key = crypto.decodepoint(td.tx_pub_key) + additional_tx_pub_keys = [] + if not common.is_empty(td.additional_tx_pub_keys): + additional_tx_pub_keys = [ + crypto.decodepoint(x) for x in td.additional_tx_pub_keys + ] + + ki, sig = ring_ct.export_key_image( + creds, + subaddresses, + out_key, + tx_pub_key, + additional_tx_pub_keys, + td.internal_output_index, + ) + + return ki, sig diff --git a/src/apps/monero/xmr/mlsag2.py b/src/apps/monero/xmr/mlsag2.py new file mode 100644 index 000000000..89d5dd4d8 --- /dev/null +++ b/src/apps/monero/xmr/mlsag2.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: https://github.com/monero-project/mininero +# Author: Dusan Klinec, ph4r05, 2018 +# see https://eprint.iacr.org/2015/1098.pdf + +from apps.monero.xmr import crypto + + +def key_zero_vector(rows): + """ + Empty key vector + :param rows: + :return: + """ + vct = [] + for i in range(rows): + vct.append(crypto.sc_0()) + return vct + + +def key_vector(rows): + """ + Empty key vector + :param rows: + :return: + """ + return [None] * rows + + +def key_matrix(rows, cols): + """ + first index is columns (so slightly backward from math) + :param rows: + :param cols: + :return: + """ + rv = [None] * cols + for i in range(0, cols): + rv[i] = key_vector(rows) + return rv + + +def hash_key_vector(v): + """ + Hashes key vector + :param v: + :return: + """ + return [crypto.hash_to_ec(vi) for vi in v] + + +def key_image_vector(x): + """ + Takes as input a keyvector, returns the keyimage-vector + TODO: use crypto for generating key images + :param x: + :return: + """ + return [ + crypto.scalarmult(crypto.hash_to_ec(crypto.scalarmult_base(xx)), xx) for xx in x + ] + + +def scalar_gen_vector(n): + """ + Generates vector of scalars + :param n: + :return: + """ + return [crypto.random_scalar() for i in range(0, n)] + + +# +# Optimized versions with incremental hashing, +# Simple and full variants for Monero +# + + +def hasher_message(message): + """ + Returns incremental hasher for MLSAG + :param message: + :return: + """ + from apps.monero.xmr.sub.keccak_hasher import HashWrapper + + ctx = HashWrapper(crypto.get_keccak()) + ctx.update(message) + ctx.zbuff = bytearray(32) + return ctx + + +def hash_point(hasher, point): + crypto.encodepoint_into(point, hasher.zbuff) + hasher.update(hasher.zbuff) + + +def gen_mlsag_assert(pk, xx, kLRki, mscout, index, dsRows): + """ + Conditions check for gen_mlsag_ext. + :param pk: + :param xx: + :param kLRki: + :param mscout: + :param index: + :param dsRows: + :return: + """ + cols = len(pk) + if cols <= 1: + raise ValueError("Cols == 1") + if index >= cols: + raise ValueError("Index out of range") + + rows = len(pk[0]) + if rows == 0: + raise ValueError("Empty pk") + + for i in range(cols): + if len(pk[i]) != rows: + raise ValueError("pk is not rectangular") + if len(xx) != rows: + raise ValueError("Bad xx size") + if dsRows > rows: + raise ValueError("Bad dsRows size") + if (not kLRki or not mscout) and (kLRki or mscout): + raise ValueError("Only one of kLRki/mscout is present") + if kLRki and dsRows != 1: + raise ValueError("Multisig requires exactly 1 dsRows") + return rows, cols + + +def gen_mlsag_rows(message, rv, pk, xx, kLRki, index, dsRows, rows, cols): + """ + MLSAG computation - the part with secret keys + :param message: + :param rv: + :param pk: + :param xx: + :param kLRki: + :param index: + :param dsRows: + :param rows: + :param cols: + :return: + """ + Ip = key_vector(dsRows) + rv.II = key_vector(dsRows) + alpha = key_vector(rows) + rv.ss = key_matrix(rows, cols) + + hasher = hasher_message(message) + + for i in range(dsRows): + hasher.update(crypto.encodepoint(pk[index][i])) + if kLRki: + alpha[i] = kLRki.k + rv.II[i] = kLRki.ki + hash_point(hasher, kLRki.L) + hash_point(hasher, kLRki.R) + + else: + Hi = crypto.hash_to_ec( + crypto.encodepoint(pk[index][i]) + ) # originally hashToPoint() + alpha[i] = crypto.random_scalar() + aGi = crypto.scalarmult_base(alpha[i]) + aHPi = crypto.scalarmult(Hi, alpha[i]) + rv.II[i] = crypto.scalarmult(Hi, xx[i]) + hash_point(hasher, aGi) + hash_point(hasher, aHPi) + + Ip[i] = crypto.precomp(rv.II[i]) + + for i in range(dsRows, rows): + alpha[i] = crypto.random_scalar() + aGi = crypto.scalarmult_base(alpha[i]) + hash_point(hasher, pk[index][i]) + hash_point(hasher, aGi) + + c_old = hasher.digest() + c_old = crypto.sc_reduce32(crypto.decodeint(c_old)) + return c_old, Ip, alpha + + +def gen_mlsag_ext(message, pk, xx, kLRki, mscout, index, dsRows): + """ + Multilayered Spontaneous Anonymous Group Signatures (MLSAG signatures) + + :param message: + :param pk: matrix of points, point form (not encoded) + :param xx: + :param kLRki: + :param mscout: + :param index: + :param dsRows: + :return: + """ + from apps.monero.xmr.serialize_messages.tx_full import MgSig + + rows, cols = gen_mlsag_assert(pk, xx, kLRki, mscout, index, dsRows) + + rv = MgSig() + c, L, R, Hi = 0, None, None, None + + c_old, Ip, alpha = gen_mlsag_rows( + message, rv, pk, xx, kLRki, index, dsRows, rows, cols + ) + + i = (index + 1) % cols + if i == 0: + rv.cc = c_old + + while i != index: + rv.ss[i] = scalar_gen_vector(rows) + hasher = hasher_message(message) + + for j in range(dsRows): + L = crypto.add_keys2(rv.ss[i][j], c_old, pk[i][j]) + Hi = crypto.hash_to_ec( + crypto.encodepoint(pk[i][j]) + ) # originally hashToPoint() + R = crypto.add_keys3(rv.ss[i][j], Hi, c_old, Ip[j]) + hash_point(hasher, pk[i][j]) + hash_point(hasher, L) + hash_point(hasher, R) + + for j in range(dsRows, rows): + L = crypto.add_keys2(rv.ss[i][j], c_old, pk[i][j]) + hash_point(hasher, pk[i][j]) + hash_point(hasher, L) + + c = crypto.sc_reduce32(crypto.decodeint(hasher.digest())) + c_old = c + i = (i + 1) % cols + + if i == 0: + rv.cc = c_old + + for j in range(rows): + rv.ss[index][j] = crypto.sc_mulsub( + c, xx[j], alpha[j] + ) # alpha[j] - c * xx[j]; sc_mulsub in original does c-ab + + if mscout: + mscout(c) + + return rv, c + + +def prove_rct_mg( + message, pubs, in_sk, out_sk, out_pk, kLRki, mscout, index, txn_fee_key +): + """ + c.f. http://eprint.iacr.org/2015/1098 section 4. definition 10. + This does the MG sig on the "dest" part of the given key matrix, and + the last row is the sum of input commitments from that column - sum output commitments + this shows that sum inputs = sum outputs + :param message: + :param pubs: matrix of CtKeys. points are encoded. + :param in_sk: + :param out_sk: + :param out_pk: + :param kLRki: + :param mscout: + :param index: + :param txn_fee_key: + :return: + """ + cols = len(pubs) + if cols == 0: + raise ValueError("Empty pubs") + rows = len(pubs[0]) + if rows == 0: + raise ValueError("Empty pub row") + for i in range(cols): + if len(pubs[i]) != rows: + raise ValueError("pub is not rectangular") + + if len(in_sk) != rows: + raise ValueError("Bad inSk size") + if len(out_sk) != len(out_pk): + raise ValueError("Bad outsk/putpk size") + if (not kLRki or not mscout) and (kLRki and mscout): + raise ValueError("Only one of kLRki/mscout is present") + + sk = key_vector(rows + 1) + M = key_matrix(rows + 1, cols) + for i in range(rows + 1): + sk[i] = crypto.sc_0() + + for i in range(cols): + M[i][rows] = crypto.identity() + for j in range(rows): + M[i][j] = crypto.decodepoint(pubs[i][j].dest) + M[i][rows] = crypto.point_add( + M[i][rows], crypto.decodepoint(pubs[i][j].mask) + ) + + sk[rows] = crypto.sc_0() + for j in range(rows): + sk[j] = in_sk[j].dest + sk[rows] = crypto.sc_add(sk[rows], in_sk[j].mask) # add masks in last row + + for i in range(cols): + for j in range(len(out_pk)): + M[i][rows] = crypto.point_sub( + M[i][rows], crypto.decodepoint(out_pk[j].mask) + ) # subtract output Ci's in last row + + # Subtract txn fee output in last row + M[i][rows] = crypto.point_sub(M[i][rows], txn_fee_key) + + for j in range(len(out_pk)): + sk[rows] = crypto.sc_sub( + sk[rows], out_sk[j].mask + ) # subtract output masks in last row + + return gen_mlsag_ext(message, M, sk, kLRki, mscout, index, rows) + + +def prove_rct_mg_simple(message, pubs, in_sk, a, cout, kLRki, mscout, index): + """ + Simple version for when we assume only + post rct inputs + here pubs is a vector of (P, C) length mixin + + :param message: + :param pubs: vector of CtKeys, public, point values, encoded form. (dest, mask) = (P, C) + :param in_sk: CtKey, private. (spending private key, input commitment mask (original)) + :param a: mask from the pseudo_output commitment (alpha) + :param cout: point, decoded. Pseudo output public key. + :param kLRki: + :param mscout: lambda accepting c + :param index: + :return: + """ + rows = 1 + cols = len(pubs) + if cols == 0: + raise ValueError("Empty pubs") + if (not kLRki or not mscout) and (kLRki and mscout): + raise ValueError("Only one of kLRki/mscout is present") + + sk = key_vector(rows + 1) + M = key_matrix(rows + 1, cols) + + sk[0] = in_sk.dest + sk[1] = crypto.sc_sub(in_sk.mask, a) + + for i in range(cols): + M[i][0] = crypto.decodepoint(pubs[i].dest) + M[i][1] = crypto.point_sub(crypto.decodepoint(pubs[i].mask), cout) + + return gen_mlsag_ext(message, M, sk, kLRki, mscout, index, rows) diff --git a/src/apps/monero/xmr/monero.py b/src/apps/monero/xmr/monero.py new file mode 100644 index 000000000..9aca64c73 --- /dev/null +++ b/src/apps/monero/xmr/monero.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 + +import ustruct as struct +from micropython import const + +from apps.monero.xmr import common, crypto + +DISPLAY_DECIMAL_POINT = const(12) + + +class XmrNoSuchAddressException(common.XmrException): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +def get_subaddress_secret_key( + secret_key, index=None, major=None, minor=None, little_endian=True +): + """ + Builds subaddress secret key from the subaddress index + Hs(SubAddr || a || index_major || index_minor) + + UPDATE: Monero team fixed this problem. Always use little endian. + Note: need to handle endianity in the index + C-code simply does: memcpy(data + sizeof(prefix) + sizeof(crypto::secret_key), &index, sizeof(subaddress_index)); + Where the index has the following form: + + struct subaddress_index { + uint32_t major; + uint32_t minor; + } + + https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment + :param secret_key: + :param index: + :param major: + :param minor: + :param little_endian: + :return: + """ + if index: + major = index.major + minor = index.minor + endianity = "<" if little_endian else ">" + prefix = b"SubAddr" + buffer = bytearray(len(prefix) + 1 + 32 + 4 + 4) + struct.pack_into( + "%s7sb32sLL" % endianity, + buffer, + 0, + prefix, + 0, + crypto.encodeint(secret_key), + major, + minor, + ) + return crypto.hash_to_scalar(buffer) + + +def get_subaddress_spend_public_key(view_private, spend_public, major, minor): + """ + Generates subaddress spend public key D_{major, minor} + :param view_private: + :param spend_public: + :param major: + :param minor: + :return: + """ + m = get_subaddress_secret_key(view_private, major=major, minor=minor) + M = crypto.scalarmult_base(m) + D = crypto.point_add(spend_public, M) + return D + + +def generate_key_derivation(pub_key, priv_key): + """ + Generates derivation priv_key * pub_key. + Simple ECDH. + :param pub_key: + :param priv_key: + :return: + """ + return crypto.generate_key_derivation(pub_key, priv_key) + + +def derive_subaddress_public_key(out_key, derivation, output_index): + """ + out_key - H_s(derivation || varint(output_index))G + :param out_key: + :param derivation: + :param output_index: + :return: + """ + crypto.check_ed25519point(out_key) + scalar = crypto.derivation_to_scalar(derivation, output_index) + point2 = crypto.scalarmult_base(scalar) + point4 = crypto.point_sub(out_key, point2) + return point4 + + +def generate_key_image(public_key, secret_key): + """ + Key image: secret_key * H_p(pub_key) + :param public_key: encoded point + :param secret_key: + :return: + """ + point = crypto.hash_to_ec(public_key) + point2 = crypto.ge_scalarmult(secret_key, point) + return point2 + + +def is_out_to_acc_precomp( + subaddresses, out_key, derivation, additional_derivations, output_index +): + """ + Searches subaddresses for the computed subaddress_spendkey. + If found, returns (major, minor), derivation. + + :param subaddresses: + :param out_key: + :param derivation: + :param additional_derivations: + :param output_index: + :return: + """ + subaddress_spendkey = crypto.encodepoint( + derive_subaddress_public_key(out_key, derivation, output_index) + ) + if subaddress_spendkey in subaddresses: + return subaddresses[subaddress_spendkey], derivation + + if additional_derivations and len(additional_derivations) > 0: + if output_index >= len(additional_derivations): + raise ValueError("Wrong number of additional derivations") + + subaddress_spendkey = derive_subaddress_public_key( + out_key, additional_derivations[output_index], output_index + ) + subaddress_spendkey = crypto.encodepoint(subaddress_spendkey) + if subaddress_spendkey in subaddresses: + return ( + subaddresses[subaddress_spendkey], + additional_derivations[output_index], + ) + + return None + + +def generate_key_image_helper_precomp( + ack, out_key, recv_derivation, real_output_index, received_index +): + """ + Generates UTXO spending key and key image. + + :param ack: sender credentials + :type ack: apps.monero.xmr.sub.creds.AccountCreds + :param out_key: real output (from input RCT) destination key + :param recv_derivation: + :param real_output_index: + :param received_index: subaddress index this payment was received to + :return: + """ + if ack.spend_key_private == 0: + raise ValueError("Watch-only wallet not supported") + + # derive secret key with subaddress - step 1: original CN derivation + scalar_step1 = crypto.derive_secret_key( + recv_derivation, real_output_index, ack.spend_key_private + ) + + # step 2: add Hs(SubAddr || a || index_major || index_minor) + subaddr_sk = None + scalar_step2 = None + if received_index == (0, 0): + scalar_step2 = scalar_step1 + else: + subaddr_sk = get_subaddress_secret_key( + ack.view_key_private, major=received_index[0], minor=received_index[1] + ) + scalar_step2 = crypto.sc_add(scalar_step1, subaddr_sk) + + # when not in multisig, we know the full spend secret key, so the output pubkey can be obtained by scalarmultBase + if len(ack.multisig_keys) == 0: + pub_ver = crypto.scalarmult_base(scalar_step2) + + else: + # When in multisig, we only know the partial spend secret key. But we do know the full spend public key, + # so the output pubkey can be obtained by using the standard CN key derivation. + pub_ver = crypto.derive_public_key( + recv_derivation, real_output_index, ack.spend_key_public + ) + + # Add the contribution from the subaddress part + if received_index != (0, 0): + subaddr_pk = crypto.scalarmult_base(subaddr_sk) + pub_ver = crypto.point_add(pub_ver, subaddr_pk) + + if not crypto.point_eq(pub_ver, out_key): + raise ValueError( + "key image helper precomp: given output pubkey doesn't match the derived one" + ) + + ki = generate_key_image(crypto.encodepoint(pub_ver), scalar_step2) + return scalar_step2, ki + + +def generate_key_image_helper( + creds, + subaddresses, + out_key, + tx_public_key, + additional_tx_public_keys, + real_output_index, +): + """ + Generates UTXO spending key and key image. + Supports subaddresses. + + :param creds: + :param subaddresses: + :param out_key: real output (from input RCT) destination key + :param tx_public_key: real output (from input RCT) public key + :param additional_tx_public_keys: + :param real_output_index: index of the real output in the RCT + :return: + """ + recv_derivation = generate_key_derivation(tx_public_key, creds.view_key_private) + + additional_recv_derivations = [] + for add_pub_key in additional_tx_public_keys: + additional_recv_derivations.append( + generate_key_derivation(add_pub_key, creds.view_key_private) + ) + + subaddr_recv_info = is_out_to_acc_precomp( + subaddresses, + out_key, + recv_derivation, + additional_recv_derivations, + real_output_index, + ) + if subaddr_recv_info is None: + raise XmrNoSuchAddressException("No such addr") + + xi, ki = generate_key_image_helper_precomp( + creds, out_key, subaddr_recv_info[1], real_output_index, subaddr_recv_info[0] + ) + return xi, ki, recv_derivation + + +def compute_subaddresses(creds, account, indices, subaddresses=None): + """ + Computes subaddress public spend key for receiving transactions. + + :param creds: credentials + :param account: major index + :param indices: array of minor indices + :param subaddresses: subaddress dict. optional. + :return: + """ + if subaddresses is None: + subaddresses = {} + + for idx in indices: + if account == 0 and idx == 0: + subaddresses[crypto.encodepoint(creds.spend_key_public)] = (0, 0) + continue + + pub = get_subaddress_spend_public_key( + creds.view_key_private, creds.spend_key_public, major=account, minor=idx + ) + pub = crypto.encodepoint(pub) + subaddresses[pub] = (account, idx) + return subaddresses + + +def generate_keys(recovery_key): + """ + Wallet gen. + :param recovery_key: + :return: + """ + sec = crypto.sc_reduce32(recovery_key) + pub = crypto.scalarmult_base(sec) + return sec, pub + + +def generate_monero_keys(seed): + """ + Generates spend key / view key from the seed in the same manner as Monero code does. + + account.cpp: + crypto::secret_key account_base::generate(const crypto::secret_key& recovery_key, bool recover, bool two_random). + :param seed: + :return: + """ + spend_sec, spend_pub = generate_keys(crypto.decodeint(seed)) + hash = crypto.cn_fast_hash(crypto.encodeint(spend_sec)) + view_sec, view_pub = generate_keys(crypto.decodeint(hash)) + return spend_sec, spend_pub, view_sec, view_pub diff --git a/src/apps/monero/xmr/ring_ct.py b/src/apps/monero/xmr/ring_ct.py new file mode 100644 index 000000000..420e727ac --- /dev/null +++ b/src/apps/monero/xmr/ring_ct.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: https://github.com/monero-project/mininero +# Author: Dusan Klinec, ph4r05, 2018 + +from apps.monero.xmr import crypto + + +def prove_range( + amount, last_mask=None, decode=False, backend_impl=True, byte_enc=True, rsig=None +): + """ + Range proof generator. + In order to minimize the memory consumption and CPU usage during transaction generation the returned values + are returned encoded. + + :param amount: + :param last_mask: + :param backend_impl: backend implementation, if available + :param decode: decodes output + :param byte_enc: byte encoded + :param rsig: buffer for rsig + :return: + """ + if not backend_impl or not byte_enc or decode: + raise ValueError("Unsupported params") + + C, a, R = None, None, None + try: + if rsig is None: + rsig = bytearray(32 * (64 + 64 + 64 + 1)) + + buf_ai = bytearray(4 * 9 * 64) + buf_alpha = bytearray(4 * 9 * 64) + C, a, R = crypto.prove_range( + rsig, amount, last_mask, buf_ai, buf_alpha + ) # backend returns encoded + + finally: + import gc + + buf_ai = None + buf_alpha = None + gc.collect() + + return C, a, R + + +# Ring-ct MG sigs +# Prove: +# c.f. http:#eprint.iacr.org/2015/1098 section 4. definition 10. +# This does the MG sig on the "dest" part of the given key matrix, and +# the last row is the sum of input commitments from that column - sum output commitments +# this shows that sum inputs = sum outputs +# Ver: +# verifies the above sig is created corretly + + +def ecdh_encode(unmasked, receiver_pk=None, derivation=None): + """ + Elliptic Curve Diffie-Helman: encodes and decodes the amount b and mask a + where C= aG + bH + :param unmasked: + :param receiver_pk: + :param derivation: + :return: + """ + from apps.monero.xmr.serialize_messages.tx_ecdh import EcdhTuple + + rv = EcdhTuple() + if derivation is None: + esk = crypto.random_scalar() + rv.senderPk = crypto.scalarmult_base(esk) + derivation = crypto.encodepoint(crypto.scalarmult(receiver_pk, esk)) + + sharedSec1 = crypto.hash_to_scalar(derivation) + sharedSec2 = crypto.hash_to_scalar(crypto.encodeint(sharedSec1)) + + rv.mask = crypto.sc_add(unmasked.mask, sharedSec1) + rv.amount = crypto.sc_add(unmasked.amount, sharedSec2) + return rv + + +def ecdh_decode(masked, receiver_sk=None, derivation=None): + """ + Elliptic Curve Diffie-Helman: encodes and decodes the amount b and mask a + where C= aG + bH + :param masked: + :param receiver_sk: + :param derivation: + :return: + """ + from apps.monero.xmr.serialize_messages.tx_ecdh import EcdhTuple + + rv = EcdhTuple() + + if derivation is None: + derivation = crypto.scalarmult(masked.senderPk, receiver_sk) + + sharedSec1 = crypto.hash_to_scalar(derivation) + sharedSec2 = crypto.hash_to_scalar(crypto.encodeint(sharedSec1)) + + rv.mask = crypto.sc_sub(masked.mask, sharedSec1) + rv.amount = crypto.sc_sub(masked.amount, sharedSec2) + return rv + + +# +# Key image import / export +# + + +def generate_ring_signature(prefix_hash, image, pubs, sec, sec_idx, test=False): + """ + Generates ring signature with key image. + void crypto_ops::generate_ring_signature() + + :param prefix_hash: + :param image: + :param pubs: + :param sec: + :param sec_idx: + :param test: + :return: + """ + from apps.monero.xmr.common import memcpy + + if test: + from apps.monero.xmr import monero + + t = crypto.scalarmult_base(sec) + if not crypto.point_eq(t, pubs[sec_idx]): + raise ValueError("Invalid sec key") + + k_i = monero.generate_key_image(crypto.encodepoint(pubs[sec_idx]), sec) + if not crypto.point_eq(k_i, image): + raise ValueError("Key image invalid") + for k in pubs: + crypto.ge_frombytes_vartime_check(k) + + image_unp = crypto.ge_frombytes_vartime(image) + image_pre = crypto.ge_dsm_precomp(image_unp) + + buff_off = len(prefix_hash) + buff = bytearray(buff_off + 2 * 32 * len(pubs)) + memcpy(buff, 0, prefix_hash, 0, buff_off) + mvbuff = memoryview(buff) + + sum = crypto.sc_0() + k = crypto.sc_0() + sig = [] + for i in range(len(pubs)): + sig.append([crypto.sc_0(), crypto.sc_0()]) # c, r + + for i in range(len(pubs)): + if i == sec_idx: + k = crypto.random_scalar() + tmp3 = crypto.scalarmult_base(k) + crypto.encodepoint_into(tmp3, mvbuff[buff_off : buff_off + 32]) + buff_off += 32 + + tmp3 = crypto.hash_to_ec(crypto.encodepoint(pubs[i])) + tmp2 = crypto.scalarmult(tmp3, k) + crypto.encodepoint_into(tmp2, mvbuff[buff_off : buff_off + 32]) + buff_off += 32 + + else: + sig[i] = [crypto.random_scalar(), crypto.random_scalar()] + tmp3 = crypto.ge_frombytes_vartime(pubs[i]) + tmp2 = crypto.ge_double_scalarmult_base_vartime(sig[i][0], tmp3, sig[i][1]) + crypto.encodepoint_into(tmp2, mvbuff[buff_off : buff_off + 32]) + buff_off += 32 + + tmp3 = crypto.hash_to_ec(crypto.encodepoint(tmp3)) + tmp2 = crypto.ge_double_scalarmult_precomp_vartime( + sig[i][1], tmp3, sig[i][0], image_pre + ) + crypto.encodepoint_into(tmp2, mvbuff[buff_off : buff_off + 32]) + buff_off += 32 + + sum = crypto.sc_add(sum, sig[i][0]) + + h = crypto.hash_to_scalar(buff) + sig[sec_idx][0] = crypto.sc_sub(h, sum) + sig[sec_idx][1] = crypto.sc_mulsub(sig[sec_idx][0], sec, k) + return sig + + +def check_ring_singature(prefix_hash, image, pubs, sig): + """ + Checks ring signature generated with generate_ring_signature + :param prefix_hash: + :param image: + :param pubs: + :param sig: + :return: + """ + from apps.monero.xmr.common import memcpy + + image_unp = crypto.ge_frombytes_vartime(image) + image_pre = crypto.ge_dsm_precomp(image_unp) + + buff_off = len(prefix_hash) + buff = bytearray(buff_off + 2 * 32 * len(pubs)) + memcpy(buff, 0, prefix_hash, 0, buff_off) + mvbuff = memoryview(buff) + + sum = crypto.sc_0() + for i in range(len(pubs)): + if crypto.sc_check(sig[i][0]) != 0 or crypto.sc_check(sig[i][1]) != 0: + return False + + tmp3 = crypto.ge_frombytes_vartime(pubs[i]) + tmp2 = crypto.ge_double_scalarmult_base_vartime(sig[i][0], tmp3, sig[i][1]) + crypto.encodepoint_into(tmp2, mvbuff[buff_off : buff_off + 32]) + buff_off += 32 + + tmp3 = crypto.hash_to_ec(crypto.encodepoint(pubs[i])) + tmp2 = crypto.ge_double_scalarmult_precomp_vartime( + sig[i][1], tmp3, sig[i][0], image_pre + ) + crypto.encodepoint_into(tmp2, mvbuff[buff_off : buff_off + 32]) + buff_off += 32 + + sum = crypto.sc_add(sum, sig[i][0]) + + h = crypto.hash_to_scalar(buff) + h = crypto.sc_sub(h, sum) + return crypto.sc_isnonzero(h) == 0 + + +def export_key_image( + creds, + subaddresses, + pkey, + tx_pub_key, + additional_tx_pub_keys, + out_idx, + test=True, + verify=True, +): + """ + Generates key image for the TXO + signature for the key image + :param creds: + :param subaddresses: + :param pkey: + :param tx_pub_key: + :param additional_tx_pub_keys: + :param out_idx: + :param test: + :param verify: + :return: + """ + from apps.monero.xmr import monero + + r = monero.generate_key_image_helper( + creds, subaddresses, pkey, tx_pub_key, additional_tx_pub_keys, out_idx + ) + xi, ki, recv_derivation = r[:3] + + phash = crypto.encodepoint(ki) + sig = generate_ring_signature(phash, ki, [pkey], xi, 0, test) + + if verify: + if check_ring_singature(phash, ki, [pkey], sig) != 1: + raise ValueError("Signature error") + + return ki, sig diff --git a/src/apps/monero/xmr/serialize/__init__.py b/src/apps/monero/xmr/serialize/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/monero/xmr/serialize/base_types.py b/src/apps/monero/xmr/serialize/base_types.py new file mode 100644 index 000000000..ff66ca28d --- /dev/null +++ b/src/apps/monero/xmr/serialize/base_types.py @@ -0,0 +1,64 @@ +class XmrType: + VERSION = 0 + + +class UVarintType(XmrType): + pass + + +class IntType(XmrType): + WIDTH = 0 + SIGNED = 0 + VARIABLE = 0 + + def __repr__(self): + return "%s:" % ( + self.__class__, + self.WIDTH, + self.SIGNED, + self.VARIABLE, + ) + + +class BoolType(IntType): + WIDTH = 1 + + +class UInt8(IntType): + WIDTH = 1 + + +class Int8(IntType): + SIGNED = 1 + WIDTH = 1 + + +class UInt16(IntType): + WIDTH = 2 + + +class Int16(IntType): + SIGNED = 1 + WIDTH = 2 + + +class UInt32(IntType): + WIDTH = 4 + + +class Int32(IntType): + SIGNED = 1 + WIDTH = 4 + + +class UInt64(IntType): + WIDTH = 8 + + +class SizeT(UInt64): + WIDTH = 8 + + +class Int64(IntType): + SIGNED = 1 + WIDTH = 8 diff --git a/src/apps/monero/xmr/serialize/erefs.py b/src/apps/monero/xmr/serialize/erefs.py new file mode 100644 index 000000000..9c8d30603 --- /dev/null +++ b/src/apps/monero/xmr/serialize/erefs.py @@ -0,0 +1,89 @@ +class ElemRefObj: + def __repr__(self): + return "RefObj" + + +class ElemRefArr: + def __repr__(self): + return "RefAssoc" + + +def is_elem_ref(elem_ref): + """ + Returns true if the elem_ref is an element reference + + :param elem_ref: + :return: + """ + return ( + elem_ref + and isinstance(elem_ref, tuple) + and len(elem_ref) == 3 + and (elem_ref[0] == ElemRefObj or elem_ref[0] == ElemRefArr) + ) + + +def has_elem(elem_ref): + """ + Has element? + :param elem_ref: + :return: + """ + if not is_elem_ref(elem_ref): + return False + elif elem_ref[0] == ElemRefObj: + return hasattr(elem_ref[1], elem_ref[2]) + elif elem_ref[0] == ElemRefArr: + return elem_ref[2] in elem_ref[1] + + +def get_elem(elem_ref, default=None): + """ + Gets the element referenced by elem_ref or returns the elem_ref directly if its not a reference. + + :param elem_ref: + :param default: + :return: + """ + if not is_elem_ref(elem_ref): + return elem_ref + elif elem_ref[0] == ElemRefObj: + return getattr(elem_ref[1], elem_ref[2], default) + elif elem_ref[0] == ElemRefArr: + return elem_ref[1][elem_ref[2]] + + +def set_elem(elem_ref, elem): + """ + Sets element referenced by the elem_ref. Returns the elem. + + :param elem_ref: + :param elem: + :return: + """ + if elem_ref is None or elem_ref == elem or not is_elem_ref(elem_ref): + return elem + + elif elem_ref[0] == ElemRefObj: + setattr(elem_ref[1], elem_ref[2], elem) + return elem + + elif elem_ref[0] == ElemRefArr: + elem_ref[1][elem_ref[2]] = elem + return elem + + +def eref(obj, key, is_assoc=None): + """ + Returns element reference + :param obj: + :param key: + :param is_assoc: + :return: + """ + if obj is None: + return None + if isinstance(key, int) or (is_assoc is not None and is_assoc): + return ElemRefArr, get_elem(obj), key + else: + return ElemRefObj, get_elem(obj), key diff --git a/src/apps/monero/xmr/serialize/int_serialize.py b/src/apps/monero/xmr/serialize/int_serialize.py new file mode 100644 index 000000000..a20d7c7cd --- /dev/null +++ b/src/apps/monero/xmr/serialize/int_serialize.py @@ -0,0 +1,126 @@ +_UINT_BUFFER = bytearray(1) + + +async def load_uint(reader, width): + """ + Constant-width integer serialization + :param reader: + :param width: + :return: + """ + buffer = _UINT_BUFFER + result = 0 + shift = 0 + for _ in range(width): + await reader.areadinto(buffer) + result += buffer[0] << shift + shift += 8 + return result + + +async def dump_uint(writer, n, width): + """ + Constant-width integer serialization + :param writer: + :param n: + :param width: + :return: + """ + buffer = _UINT_BUFFER + for _ in range(width): + buffer[0] = n & 0xff + await writer.awrite(buffer) + n >>= 8 + + +def uvarint_size(n): + """ + Returns size in bytes n would occupy serialized as varint + :param n: + :return: + """ + bts = 0 if n != 0 else 1 + while n: + n >>= 7 + bts += 1 + return bts + + +def load_uvarint_b(buffer): + """ + Variable int deserialization, synchronous from buffer. + :param buffer: + :return: + """ + result = 0 + idx = 0 + byte = 0x80 + while byte & 0x80: + byte = buffer[idx] + result += (byte & 0x7F) << (7 * idx) + idx += 1 + return result + + +def dump_uvarint_b(n): + """ + Serializes uvarint to the buffer + :param n: + :return: + """ + buffer = bytearray(uvarint_size(n)) + return dump_uvarint_b_into(n, buffer, 0) + + +def dump_uvarint_b_into(n, buffer, offset=0): + """ + Serializes n as variable size integer to the provided buffer. + Buffer has to ha + :param n: + :param buffer: + :param offset: + :return: + """ + shifted = True + while shifted: + shifted = n >> 7 + buffer[offset] = (n & 0x7F) | (0x80 if shifted else 0x00) + offset += 1 + n = shifted + return buffer + + +def load_uint_b(buffer, width): + """ + Loads fixed size integer from the buffer + :param buffer: + :return: + """ + result = 0 + for idx in range(width): + result += buffer[idx] << (8 * idx) + return result + + +def dump_uint_b(n, width): + """ + Serializes fixed size integer to the buffer + :param n: + :param width: + :return: + """ + buffer = bytearray(width) + return dump_uvarint_b_into(n, buffer, 0) + + +def dump_uint_b_into(n, width, buffer, offset=0): + """ + Serializes fixed size integer to the buffer + :param n: + :param width: + :return: + """ + for idx in range(width): + buffer[idx + offset] = n & 0xff + n >>= 8 + return buffer diff --git a/src/apps/monero/xmr/serialize/message_types.py b/src/apps/monero/xmr/serialize/message_types.py new file mode 100644 index 000000000..c36366cd8 --- /dev/null +++ b/src/apps/monero/xmr/serialize/message_types.py @@ -0,0 +1,141 @@ +from apps.monero.xmr.serialize.base_types import XmrType +from apps.monero.xmr.serialize.obj_helper import eq_obj_contents, is_type, slot_obj_dict + + +class BlobType(XmrType): + """ + Binary data + + Represented as bytearray() or a list of values in data structures. + Not wrapped in the BlobType, the BlobType is only a scheme descriptor. + Behaves in the same way as primitive types + + Supports also the wrapped version (__init__, DATA_ATTR, eq, repr...), + """ + + DATA_ATTR = "data" + FIX_SIZE = 0 + SIZE = 0 + + def __eq__(self, rhs): + return eq_obj_contents(self, rhs) + + def __repr__(self): + dct = slot_obj_dict(self) if hasattr(self, "__slots__") else self.__dict__ + return "<%s: %s>" % (self.__class__.__name__, dct) + + +class UnicodeType(XmrType): + pass + + +class VariantType(XmrType): + """ + Union of types, variant tags needed. is only one of the types. List in typedef, enum. + Wraps the variant type in order to unambiguously support variant of variants. + Supports also unwrapped value using type system to distinguish variants - simplifies the construction. + """ + + WRAPS_VALUE = False + + def __init__(self): + self.variant_elem = None + self.variant_elem_type = None + + @classmethod + def f_specs(cls): + return () + + def set_variant(self, fname, fvalue): + self.variant_elem = fname + self.variant_elem_type = fvalue.__class__ + setattr(self, fname, fvalue) + + def __eq__(self, rhs): + return eq_obj_contents(self, rhs) + + def __repr__(self): + dct = slot_obj_dict(self) if hasattr(self, "__slots__") else self.__dict__ + return "<%s: %s>" % (self.__class__.__name__, dct) + + +class ContainerType(XmrType): + """ + Array of elements + Represented as a real array in the data structures, not wrapped in the ContainerType. + The Container type is used only as a schema descriptor for serialization. + """ + + FIX_SIZE = 0 + SIZE = 0 + ELEM_TYPE = None + + +class TupleType(XmrType): + @classmethod + def f_specs(cls): + return () + + +class MessageType(XmrType): + def __init__(self, **kwargs): + for kw in kwargs: + setattr(self, kw, kwargs[kw]) + + def __eq__(self, rhs): + return eq_obj_contents(self, rhs) + + def __repr__(self): + dct = slot_obj_dict(self) if hasattr(self, "__slots__") else self.__dict__ + return "<%s: %s>" % (self.__class__.__name__, dct) + + @classmethod + def f_specs(cls): + return () + + def _field(self, fname=None, idx=None): + fld = None + specs = self.f_specs() + if fname is not None: + fld = [x for x in specs if x[0] == fname][0] + elif idx is not None: + fld = specs[idx] + return fld + + async def _msg_field(self, ar, fname=None, idx=None, **kwargs): + return await ar.message_field(self, self._field(fname=fname, idx=idx), **kwargs) + + +def container_elem_type(container_type, params): + """ + Returns container element type + + :param container_type: + :param params: + :return: + """ + elem_type = params[0] if params else None + if elem_type is None: + elem_type = container_type.ELEM_TYPE + return elem_type + + +def gen_elem_array(size, elem_type=None): + """ + Generates element array of given size and initializes with given type. + Supports container type, used for pre-allocation before deserialization. + :param size: + :param elem_type: + :return: + """ + if elem_type is None or not callable(elem_type): + return [elem_type] * size + if is_type(elem_type, ContainerType): + + def elem_type(): + return [] + + res = [] + for _ in range(size): + res.append(elem_type()) + return res diff --git a/src/apps/monero/xmr/serialize/obj_helper.py b/src/apps/monero/xmr/serialize/obj_helper.py new file mode 100644 index 000000000..877b9495a --- /dev/null +++ b/src/apps/monero/xmr/serialize/obj_helper.py @@ -0,0 +1,66 @@ +def eq_obj_slots(l, r): + """ + Compares objects with __slots__ defined + :param l: + :param r: + :return: + """ + for f in l.__slots__: + if getattr(l, f, None) != getattr(r, f, None): + return False + return True + + +def eq_obj_contents(l, r): + """ + Compares object contents, supports slots + :param l: + :param r: + :return: + """ + if l.__class__ is not r.__class__: + return False + if hasattr(l, "__slots__"): + return eq_obj_slots(l, r) + else: + return l.__dict__ == r.__dict__ + + +def slot_obj_dict(o): + """ + Builds dict for o with __slots__ defined + :param o: + :return: + """ + d = {} + for f in o.__slots__: + d[f] = getattr(o, f, None) + return d + + +def is_type(x, types, full=False): + """ + Returns true if x is of type in types tuple + :param x: + :param types: + :param full: + :return: + """ + types = types if isinstance(types, tuple) else (types,) + ins = isinstance(x, types) + sub = False + try: + sub = issubclass(x, types) + except Exception: + pass + res = ins or sub + return res if not full else (res, ins) + + +def get_ftype_params(field): + """ + Convenient getter + :param field: + :return: + """ + return field[1], field[2:] diff --git a/src/apps/monero/xmr/serialize/readwriter.py b/src/apps/monero/xmr/serialize/readwriter.py new file mode 100644 index 000000000..0eba726e1 --- /dev/null +++ b/src/apps/monero/xmr/serialize/readwriter.py @@ -0,0 +1,100 @@ +import gc + + +class MemoryReaderWriter: + def __init__( + self, + buffer=None, + read_empty=False, + threshold=None, + do_gc=False, + preallocate=None, + **kwargs + ): + self.buffer = buffer + self.nread = 0 + self.nwritten = 0 + + self.ndata = 0 + self.offset = 0 + self.woffset = 0 + + self.read_empty = read_empty + self.threshold = threshold + self.do_gc = do_gc + + if preallocate is not None: + self.preallocate(preallocate) + elif self.buffer is None: + self.buffer = bytearray(0) + else: + self.woffset = len(buffer) + + def is_empty(self): + return self.offset == len(self.buffer) or self.offset == self.woffset + + def preallocate(self, size): + self.buffer = bytearray(size) + self.offset = 0 + self.woffset = 0 + + async def areadinto(self, buf): + ln = len(buf) + if not self.read_empty and ln > 0 and self.offset == len(self.buffer): + raise EOFError + + nread = min(ln, len(self.buffer) - self.offset) + for idx in range(nread): + buf[idx] = self.buffer[self.offset + idx] + + self.offset += nread + self.nread += nread + self.ndata -= nread + + # Deallocation threshold triggered + if self.threshold is not None and self.offset >= self.threshold: + self.buffer = self.buffer[self.offset :] + self.woffset -= self.offset + self.offset = 0 + + if self.do_gc: + gc.collect() + + return nread + + async def awrite(self, buf): + nwritten = len(buf) + nall = len(self.buffer) + towrite = nwritten + bufoff = 0 + + # Fill existing place in the buffer + while towrite > 0 and nall - self.woffset > 0: + self.buffer[self.woffset] = buf[bufoff] + self.woffset += 1 + bufoff += 1 + towrite -= 1 + + # Allocate next chunk if needed + while towrite > 0: + _towrite = min(32, towrite) + chunk = bytearray(32) # chunk size typical for EC point + + for i in range(_towrite): + chunk[i] = buf[bufoff] + self.woffset += 1 + bufoff += 1 + towrite -= 1 + + self.buffer.extend(chunk) + if self.do_gc: + chunk = None # dereference + gc.collect() + + self.nwritten += nwritten + self.ndata += nwritten + return nwritten + + def get_buffer(self): + mv = memoryview(self.buffer) + return mv[self.offset : self.woffset] diff --git a/src/apps/monero/xmr/serialize/xmrserialize.py b/src/apps/monero/xmr/serialize/xmrserialize.py new file mode 100644 index 000000000..7d9c4a00d --- /dev/null +++ b/src/apps/monero/xmr/serialize/xmrserialize.py @@ -0,0 +1,843 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' +Minimal streaming codec for a Monero binary serialization. +Used for a binary serialization in blockchain and for hash computation for signatures. + +Equivalent of BEGIN_SERIALIZE_OBJECT(), /src/serialization/serialization.h + +- The wire binary format does not use tags. Structure has to be read from the binary stream +with the scheme specified in order to parse the structure. + +- Heavily uses variable integer serialization - similar to the UTF8 or LZ4 number encoding. + +- Supports: blob, string, integer types - variable or fixed size, containers of elements, + variant types, messages of elements + +For de-serializing (loading) types, object with `AsyncReader` +interface is required: + +>>> class AsyncReader: +>>> async def areadinto(self, buffer): +>>> """ +>>> Reads `len(buffer)` bytes into `buffer`, or raises `EOFError`. +>>> """ + +For serializing (dumping) types, object with `AsyncWriter` interface is +required: + +>>> class AsyncWriter: +>>> async def awrite(self, buffer): +>>> """ +>>> Writes all bytes from `buffer`, or raises `EOFError`. +>>> """ +''' + +import sys + +from protobuf import dump_uvarint, load_uvarint +from trezor import log + +from apps.monero.xmr.serialize.base_types import IntType, UVarintType, XmrType +from apps.monero.xmr.serialize.erefs import eref, get_elem, set_elem +from apps.monero.xmr.serialize.int_serialize import dump_uint, load_uint +from apps.monero.xmr.serialize.message_types import ( + BlobType, + ContainerType, + MessageType, + TupleType, + UnicodeType, + VariantType, + container_elem_type, + gen_elem_array, +) + + +def import_def(module, name): + if module not in sys.modules: + if not module.startswith("apps.monero"): + raise ValueError("Module not allowed: %s" % module) + + log.debug(__name__, "Importing: from %s import %s", module, name) + __import__(module, None, None, (name,), 0) + + r = getattr(sys.modules[module], name) + return r + + +class Archive(object): + """ + Archive object for object binary serialization / deserialization. + Resembles Archive API from the Monero codebase or Boost serialization archive. + + The design goal is to provide uniform API both for serialization and deserialization + so the code is not duplicated for serialization and deserialization but the same + for both ways in order to minimize potential bugs in the code. + + In order to use the archive for both ways we have to use so-called field references + as we cannot directly modify given element as a parameter (value-passing) as its performed + in C++ code. see: eref(), get_elem(), set_elem() + """ + + def __init__(self, iobj, writing=True, **kwargs): + self.writing = writing + self.iobj = iobj + + async def prepare_container(self, size, container, elem_type=None): + """ + Prepares container for serialization + :param size: + :param container: + :return: + """ + if not self.writing: + if container is None: + return gen_elem_array(size, elem_type) + + fvalue = get_elem(container) + if fvalue is None: + fvalue = [] + fvalue += gen_elem_array(max(0, size - len(fvalue)), elem_type) + set_elem(container, fvalue) + return fvalue + + async def prepare_message(self, msg, msg_type): + """ + Prepares message for serialization + :param msg: + :param msg_type: + :return: + """ + if self.writing: + return + return set_elem(msg, msg_type()) + + async def uvarint(self, elem): + """ + Uvarint + :param elem: + :return: + """ + if self.writing: + return await dump_uvarint(self.iobj, elem) + else: + return await load_uvarint(self.iobj) + + async def uint(self, elem, elem_type, params=None): + """ + Fixed size int + :param elem: + :param elem_type: + :param params: + :return: + """ + if self.writing: + return await dump_uint(self.iobj, elem, elem_type.WIDTH) + else: + return await load_uint(self.iobj, elem_type.WIDTH) + + async def unicode_type(self, elem): + """ + Unicode type + :param elem: + :return: + """ + if self.writing: + return await dump_unicode(self.iobj, elem) + else: + return await load_unicode(self.iobj) + + async def blob(self, elem=None, elem_type=None, params=None): + """ + Loads/dumps blob + :return: + """ + elem_type = elem_type if elem_type else elem.__class__ + if hasattr(elem_type, "serialize_archive"): + elem = elem_type() if elem is None else elem + return await elem.serialize_archive( + self, elem=elem, elem_type=elem_type, params=params + ) + + if self.writing: + return await dump_blob( + self.iobj, elem=elem, elem_type=elem_type, params=params + ) + else: + return await load_blob( + self.iobj, elem_type=elem_type, params=params, elem=elem + ) + + async def container(self, container=None, container_type=None, params=None): + """ + Loads/dumps container + :return: + """ + if hasattr(container_type, "serialize_archive"): + container = container_type() if container is None else container + return await container.serialize_archive( + self, elem=container, elem_type=container_type, params=params + ) + + if self.writing: + return await dump_container( + self.iobj, + container, + container_type, + params, + field_archiver=self.dump_field, + ) + else: + return await load_container( + self.iobj, + container_type, + params=params, + container=container, + field_archiver=self.load_field, + ) + + async def container_size( + self, container_len=None, container_type=None, params=None + ): + """ + Container size + :param container_len: + :param container_type: + :param params: + :return: + """ + if hasattr(container_type, "serialize_archive"): + raise ValueError("not supported") + + if self.writing: + return await dump_container_size( + self.iobj, container_len, container_type, params + ) + else: + raise ValueError("Not supported") + + async def container_val(self, elem, container_type, params=None): + """ + Single cont value + :param elem: + :param container_type: + :param params: + :return: + """ + if hasattr(container_type, "serialize_archive"): + raise ValueError("not supported") + if self.writing: + return await dump_container_val(self.iobj, elem, container_type, params) + else: + raise ValueError("Not supported") + + async def tuple(self, elem=None, elem_type=None, params=None): + """ + Loads/dumps tuple + :return: + """ + if hasattr(elem_type, "serialize_archive"): + container = elem_type() if elem is None else elem + return await container.serialize_archive( + self, elem=elem, elem_type=elem_type, params=params + ) + + if self.writing: + return await dump_tuple( + self.iobj, elem, elem_type, params, field_archiver=self.dump_field + ) + else: + return await load_tuple( + self.iobj, + elem_type, + params=params, + elem=elem, + field_archiver=self.load_field, + ) + + async def variant(self, elem=None, elem_type=None, params=None): + """ + Loads/dumps variant type + :param elem: + :param elem_type: + :param params: + :return: + """ + elem_type = elem_type if elem_type else elem.__class__ + if hasattr(elem_type, "serialize_archive"): + elem = elem_type() if elem is None else elem + return await elem.serialize_archive( + self, elem=elem, elem_type=elem_type, params=params + ) + + if self.writing: + return await dump_variant( + self.iobj, + elem=elem, + elem_type=elem_type if elem_type else elem.__class__, + params=params, + field_archiver=self.dump_field, + ) + else: + return await load_variant( + self.iobj, + elem_type=elem_type if elem_type else elem.__class__, + params=params, + elem=elem, + field_archiver=self.load_field, + ) + + async def message(self, msg, msg_type=None): + """ + Loads/dumps message + :param msg: + :param msg_type: + :return: + """ + elem_type = msg_type if msg_type is not None else msg.__class__ + if hasattr(elem_type, "serialize_archive"): + msg = elem_type() if msg is None else msg + return await msg.serialize_archive(self) + + if self.writing: + return await dump_message( + self.iobj, msg, msg_type=msg_type, field_archiver=self.dump_field + ) + else: + return await load_message( + self.iobj, msg_type, msg=msg, field_archiver=self.load_field + ) + + async def message_field(self, msg, field, fvalue=None): + """ + Dumps/Loads message field + :param msg: + :param field: + :param fvalue: explicit value for dump + :return: + """ + if self.writing: + await dump_message_field( + self.iobj, msg, field, fvalue=fvalue, field_archiver=self.dump_field + ) + else: + await load_message_field( + self.iobj, msg, field, field_archiver=self.load_field + ) + + async def message_fields(self, msg, fields): + """ + Load/dump individual message fields + :param msg: + :param fields: + :return: + """ + for field in fields: + await self.message_field(msg, field) + return msg + + def _get_type(self, elem_type): + # log.info(__name__, 'elem: %s %s %s %s %s | %s %s', + # type(elem_type), elem_type.__name__, elem_type.__module__, elem_type, issubclass(elem_type, XmrType), id(elem_type), id(XmrType)) + + # If part of our hierarchy - return the object + if issubclass(elem_type, XmrType): + return elem_type + + # Basic decision types + etypes = ( + UVarintType, + IntType, + BlobType, + UnicodeType, + VariantType, + ContainerType, + TupleType, + MessageType, + ) + cname = elem_type.__name__ + for e in etypes: + if cname == e.__name__: + return e + + # Inferred type: need to translate it to the current + try: + m = elem_type.__module__ + r = import_def(m, cname) + sub_test = issubclass(r, XmrType) + log.debug( + __name__, + "resolved %s, sub: %s, id_e: %s, id_mod: %s", + r, + sub_test, + id(r), + id(sys.modules[m]), + ) + if not sub_test: + log.warning(__name__, "resolution hierarchy broken") + + return r + + except Exception as e: + raise ValueError( + "Could not translate elem type: %s %s, exc: %s %s" + % (type(elem_type), elem_type, type(e), e) + ) + + def _is_type(self, elem_type, test_type): + return issubclass(elem_type, test_type) + + async def field(self, elem=None, elem_type=None, params=None): + """ + Archive field + :param elem: + :param elem_type: + :param params: + :return: + """ + elem_type = elem_type if elem_type else elem.__class__ + fvalue = None + + etype = self._get_type(elem_type) + if self._is_type(etype, UVarintType): + fvalue = await self.uvarint(get_elem(elem)) + + elif self._is_type(etype, IntType): + fvalue = await self.uint( + elem=get_elem(elem), elem_type=elem_type, params=params + ) + + elif self._is_type(etype, BlobType): + fvalue = await self.blob( + elem=get_elem(elem), elem_type=elem_type, params=params + ) + + elif self._is_type(etype, UnicodeType): + fvalue = await self.unicode_type(get_elem(elem)) + + elif self._is_type(etype, VariantType): + fvalue = await self.variant( + elem=get_elem(elem), elem_type=elem_type, params=params + ) + + elif self._is_type(etype, ContainerType): # container ~ simple list + fvalue = await self.container( + container=get_elem(elem), container_type=elem_type, params=params + ) + + elif self._is_type(etype, TupleType): # tuple ~ simple list + fvalue = await self.tuple( + elem=get_elem(elem), elem_type=elem_type, params=params + ) + + elif self._is_type(etype, MessageType): + fvalue = await self.message(get_elem(elem), msg_type=elem_type) + + else: + raise TypeError( + "unknown type: %s %s %s" % (elem_type, type(elem_type), elem) + ) + + return fvalue if self.writing else set_elem(elem, fvalue) + + async def dump_field(self, writer, elem, elem_type, params=None): + assert self.iobj == writer + return await self.field(elem=elem, elem_type=elem_type, params=params) + + async def load_field(self, reader, elem_type, params=None, elem=None): + assert self.iobj == reader + return await self.field(elem=elem, elem_type=elem_type, params=params) + + async def root(self): + """ + Root level archive init + :return: + """ + + +async def dump_blob(writer, elem, elem_type, params=None): + """ + Dumps blob message to the writer. + Supports both blob and raw value. + + :param writer: + :param elem: + :param elem_type: + :param params: + :return: + """ + elem_is_blob = isinstance(elem, BlobType) + elem_params = elem if elem_is_blob or elem_type is None else elem_type + data = bytes(getattr(elem, BlobType.DATA_ATTR) if elem_is_blob else elem) + + if not elem_params.FIX_SIZE: + await dump_uvarint(writer, len(elem)) + elif len(data) != elem_params.SIZE: + raise ValueError("Fixed size blob has not defined size: %s" % elem_params.SIZE) + await writer.awrite(data) + + +async def load_blob(reader, elem_type, params=None, elem=None): + """ + Loads blob from reader to the element. Returns the loaded blob. + + :param reader: + :param elem_type: + :param params: + :param elem: + :return: + """ + ivalue = elem_type.SIZE if elem_type.FIX_SIZE else await load_uvarint(reader) + fvalue = bytearray(ivalue) + await reader.areadinto(fvalue) + + if elem is None: + return fvalue # array by default + + elif isinstance(elem, BlobType): + setattr(elem, elem_type.DATA_ATTR, fvalue) + return elem + + else: + elem.extend(fvalue) + + return elem + + +async def dump_unicode(writer, elem): + """ + Dumps string as UTF8 encoded string + :param writer: + :param elem: + :return: + """ + await dump_uvarint(writer, len(elem)) + await writer.awrite(bytes(elem, "utf8")) + + +async def load_unicode(reader): + """ + Loads UTF8 string + :param reader: + :return: + """ + ivalue = await load_uvarint(reader) + fvalue = bytearray(ivalue) + await reader.areadinto(fvalue) + return str(fvalue, "utf8") + + +async def dump_container_size(writer, container_len, container_type, params=None): + """ + Dumps container size - per element streaming + :param writer: + :param container_len: + :param container_type: + :param params: + :return: + """ + if not container_type or not container_type.FIX_SIZE: + await dump_uvarint(writer, container_len) + elif container_len != container_type.SIZE: + raise ValueError( + "Fixed size container has not defined size: %s" % container_type.SIZE + ) + + +async def dump_container_val( + writer, elem, container_type, params=None, field_archiver=None +): + """ + Single elem dump + :param writer: + :param elem: + :param container_type: + :param params: + :return: + """ + field_archiver = field_archiver if field_archiver else dump_field + elem_type = container_elem_type(container_type, params) + await field_archiver(writer, elem, elem_type, params[1:] if params else None) + + +async def dump_container( + writer, container, container_type, params=None, field_archiver=None +): + """ + Dumps container of elements to the writer. + + :param writer: + :param container: + :param container_type: + :param params: + :param field_archiver: + :return: + """ + await dump_container_size(writer, len(container), container_type) + + field_archiver = field_archiver if field_archiver else dump_field + elem_type = container_elem_type(container_type, params) + + for elem in container: + await field_archiver(writer, elem, elem_type, params[1:] if params else None) + + +async def load_container( + reader, container_type, params=None, container=None, field_archiver=None +): + """ + Loads container of elements from the reader. Supports the container ref. + Returns loaded container. + + :param reader: + :param container_type: + :param params: + :param container: + :param field_archiver: + :return: + """ + field_archiver = field_archiver if field_archiver else load_field + + c_len = ( + container_type.SIZE if container_type.FIX_SIZE else await load_uvarint(reader) + ) + if container and c_len != len(container): + raise ValueError("Size mismatch") + + elem_type = container_elem_type(container_type, params) + res = container if container else [] + for i in range(c_len): + fvalue = await field_archiver( + reader, + elem_type, + params[1:] if params else None, + eref(res, i) if container else None, + ) + if not container: + res.append(fvalue) + return res + + +async def dump_tuple(writer, elem, elem_type, params=None, field_archiver=None): + """ + Dumps tuple of elements to the writer. + + :param writer: + :param elem: + :param elem_type: + :param params: + :param field_archiver: + :return: + """ + if len(elem) != len(elem_type.f_specs()): + raise ValueError( + "Fixed size tuple has not defined size: %s" % len(elem_type.f_specs()) + ) + await dump_uvarint(writer, len(elem)) + + field_archiver = field_archiver if field_archiver else dump_field + elem_fields = params[0] if params else None + if elem_fields is None: + elem_fields = elem_type.f_specs() + for idx, elem in enumerate(elem): + await field_archiver( + writer, elem, elem_fields[idx], params[1:] if params else None + ) + + +async def load_tuple(reader, elem_type, params=None, elem=None, field_archiver=None): + """ + Loads tuple of elements from the reader. Supports the tuple ref. + Returns loaded tuple. + + :param reader: + :param elem_type: + :param params: + :param container: + :param field_archiver: + :return: + """ + field_archiver = field_archiver if field_archiver else load_field + + c_len = await load_uvarint(reader) + if elem and c_len != len(elem): + raise ValueError("Size mismatch") + if c_len != len(elem_type.f_specs()): + raise ValueError("Tuple size mismatch") + + elem_fields = params[0] if params else None + if elem_fields is None: + elem_fields = elem_type.f_specs() + + res = elem if elem else [] + for i in range(c_len): + fvalue = await field_archiver( + reader, + elem_fields[i], + params[1:] if params else None, + eref(res, i) if elem else None, + ) + if not elem: + res.append(fvalue) + return res + + +async def dump_message_field(writer, msg, field, fvalue=None, field_archiver=None): + """ + Dumps a message field to the writer. Field is defined by the message field specification. + + :param writer: + :param msg: + :param field: + :param fvalue: + :param field_archiver: + :return: + """ + fname, ftype, params = field[0], field[1], field[2:] + fvalue = getattr(msg, fname, None) if fvalue is None else fvalue + field_archiver = field_archiver if field_archiver else dump_field + await field_archiver(writer, fvalue, ftype, params) + + +async def load_message_field(reader, msg, field, field_archiver=None): + """ + Loads message field from the reader. Field is defined by the message field specification. + Returns loaded value, supports field reference. + + :param reader: + :param msg: + :param field: + :param field_archiver: + :return: + """ + fname, ftype, params = field[0], field[1], field[2:] + field_archiver = field_archiver if field_archiver else load_field + await field_archiver(reader, ftype, params, eref(msg, fname)) + + +async def dump_message(writer, msg, msg_type=None, field_archiver=None): + """ + Dumps message to the writer. + + :param writer: + :param msg: + :param msg_type: + :param field_archiver: + :return: + """ + mtype = msg.__class__ if msg_type is None else msg_type + fields = mtype.f_specs() + if hasattr(mtype, "serialize_archive"): + raise ValueError("Cannot directly load, has to use archive with %s" % mtype) + + for field in fields: + await dump_message_field( + writer, msg=msg, field=field, field_archiver=field_archiver + ) + + +async def load_message(reader, msg_type, msg=None, field_archiver=None): + """ + Loads message if the given type from the reader. + Supports reading directly to existing message. + + :param reader: + :param msg_type: + :param msg: + :param field_archiver: + :return: + """ + msg = msg_type() if msg is None else msg + fields = msg_type.f_specs() if msg_type else msg.__class__.f_specs() + if hasattr(msg_type, "serialize_archive"): + raise ValueError("Cannot directly load, has to use archive with %s" % msg_type) + + for field in fields: + await load_message_field(reader, msg, field, field_archiver=field_archiver) + + return msg + + +def find_variant_fdef(elem_type, elem): + fields = elem_type.f_specs() + for x in fields: + if isinstance(elem, x[1]): + return x + + # Not direct hierarchy + name = elem.__class__.__name__ + for x in fields: + if name == x[1].__name__: + return x + + raise ValueError("Unrecognized variant: %s" % elem) + + +async def dump_variant(writer, elem, elem_type=None, params=None, field_archiver=None): + """ + Dumps variant type to the writer. + Supports both wrapped and raw variant. + + :param writer: + :param elem: + :param elem_type: + :param params: + :param field_archiver: + :return: + """ + field_archiver = field_archiver if field_archiver else dump_field + if isinstance(elem, VariantType) or elem_type.WRAPS_VALUE: + await dump_uint(writer, elem.variant_elem_type.VARIANT_CODE, 1) + await field_archiver( + writer, getattr(elem, elem.variant_elem), elem.variant_elem_type + ) + + else: + fdef = find_variant_fdef(elem_type, elem) + await dump_uint(writer, fdef[1].VARIANT_CODE, 1) + await field_archiver(writer, elem, fdef[1]) + + +async def load_variant( + reader, elem_type, params=None, elem=None, wrapped=None, field_archiver=None +): + """ + Loads variant type from the reader. + Supports both wrapped and raw variant. + + :param reader: + :param elem_type: + :param params: + :param elem: + :param wrapped: + :param field_archiver: + :return: + """ + is_wrapped = ( + (isinstance(elem, VariantType) or elem_type.WRAPS_VALUE) + if wrapped is None + else wrapped + ) + if is_wrapped: + elem = elem_type() if elem is None else elem + + field_archiver = field_archiver if field_archiver else load_field + tag = await load_uint(reader, 1) + for field in elem_type.f_specs(): + ftype = field[1] + if ftype.VARIANT_CODE == tag: + fvalue = await field_archiver( + reader, ftype, field[2:], elem if not is_wrapped else None + ) + if is_wrapped: + elem.set_variant(field[0], fvalue) + return elem if is_wrapped else fvalue + raise ValueError("Unknown tag: %s" % tag) + + +async def dump_field(writer, elem, elem_type, params=None): + raise TypeError("type") + + +async def load_field(reader, elem_type, params=None, elem=None): + raise TypeError("type") diff --git a/src/apps/monero/xmr/serialize_messages/__init__.py b/src/apps/monero/xmr/serialize_messages/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/monero/xmr/serialize_messages/addr.py b/src/apps/monero/xmr/serialize_messages/addr.py new file mode 100644 index 000000000..f5f69b5fc --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/addr.py @@ -0,0 +1,19 @@ +from apps.monero.xmr.serialize.base_types import UInt32 +from apps.monero.xmr.serialize.message_types import MessageType +from apps.monero.xmr.serialize_messages.base import ECPublicKey + + +class AccountPublicAddress(MessageType): + __slots__ = ("m_spend_public_key", "m_view_public_key") + + @classmethod + def f_specs(cls): + return (("m_spend_public_key", ECPublicKey), ("m_view_public_key", ECPublicKey)) + + +class SubaddressIndex(MessageType): + __slots__ = ("major", "minor") + + @classmethod + def f_specs(cls): + return (("major", UInt32), ("minor", UInt32)) diff --git a/src/apps/monero/xmr/serialize_messages/base.py b/src/apps/monero/xmr/serialize_messages/base.py new file mode 100644 index 000000000..1846a3027 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/base.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +XMR types +""" + +from micropython import const + +from apps.monero.xmr.serialize.message_types import BlobType + +_c0 = const(0) +_c1 = const(1) +_c32 = const(32) +_c64 = const(64) + +# +# cryptonote_basic.h +# + + +class Hash(BlobType): + __slots__ = ("data",) + DATA_ATTR = "data" + FIX_SIZE = _c1 + SIZE = _c32 + + +class ECKey(BlobType): + __slots__ = ("bytes",) + DATA_ATTR = "bytes" + FIX_SIZE = _c1 + SIZE = _c32 + + +ECPoint = Hash +SecretKey = ECKey +ECPublicKey = ECPoint +KeyImage = ECPoint +KeyDerivation = ECPoint diff --git a/src/apps/monero/xmr/serialize_messages/ct_keys.py b/src/apps/monero/xmr/serialize_messages/ct_keys.py new file mode 100644 index 000000000..4d677fc68 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/ct_keys.py @@ -0,0 +1,53 @@ +from micropython import const + +from apps.monero.xmr.serialize.message_types import ContainerType, MessageType +from apps.monero.xmr.serialize_messages.base import ECKey + +_c0 = const(0) +_c1 = const(1) +_c32 = const(32) +_c64 = const(64) + + +class Key64(ContainerType): + FIX_SIZE = _c1 + SIZE = _c64 + ELEM_TYPE = ECKey + + +class KeyV(ContainerType): + FIX_SIZE = _c0 + ELEM_TYPE = ECKey + + +class KeyM(ContainerType): + FIX_SIZE = _c0 + ELEM_TYPE = KeyV + + +class KeyVFix(ContainerType): + FIX_SIZE = _c1 + ELEM_TYPE = ECKey + + +class KeyMFix(ContainerType): + FIX_SIZE = _c1 + ELEM_TYPE = KeyVFix + + +class CtKey(MessageType): + __slots__ = ("dest", "mask") + + @classmethod + def f_specs(cls): + return (("dest", ECKey), ("mask", ECKey)) + + +class CtkeyV(ContainerType): + FIX_SIZE = 0 + ELEM_TYPE = CtKey + + +class CtkeyM(ContainerType): + FIX_SIZE = 0 + ELEM_TYPE = CtkeyV diff --git a/src/apps/monero/xmr/serialize_messages/tx_construct.py b/src/apps/monero/xmr/serialize_messages/tx_construct.py new file mode 100644 index 000000000..26c5d4ede --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_construct.py @@ -0,0 +1,96 @@ +from apps.monero.xmr.serialize.base_types import ( + BoolType, + SizeT, + UInt8, + UInt32, + UInt64, + UVarintType, +) +from apps.monero.xmr.serialize.message_types import ContainerType, MessageType +from apps.monero.xmr.serialize_messages.addr import SubaddressIndex +from apps.monero.xmr.serialize_messages.base import ECKey, ECPublicKey, Hash, KeyImage +from apps.monero.xmr.serialize_messages.tx_dest_entry import TxDestinationEntry +from apps.monero.xmr.serialize_messages.tx_full import RctSig +from apps.monero.xmr.serialize_messages.tx_prefix import TransactionPrefix +from apps.monero.xmr.serialize_messages.tx_src_entry import TxSourceEntry + + +class MultisigOut(MessageType): + @classmethod + def f_specs(cls): + return (("c", ContainerType, ECKey),) + + +class MultisigLR(MessageType): + __slots__ = ("L", "R") + + @classmethod + def f_specs(cls): + return (("L", ECKey), ("R", ECKey)) + + +class MultisigInfo(MessageType): + __slots__ = ("signer", "LR", "partial_key_images") + + @classmethod + def f_specs(cls): + return ( + ("signer", ECPublicKey), + ("LR", ContainerType, MultisigLR), + ("partial_key_images", ContainerType, KeyImage), + ) + + +class MultisigStruct(MessageType): + __slots__ = ("sigs", "ignore", "used_L", "signing_keys", "msout") + + @classmethod + def f_specs(cls): + return ( + ("sigs", RctSig), + ("ignore", ECPublicKey), + ("used_L", ContainerType, ECKey), + ("signing_keys", ContainerType, ECPublicKey), + ("msout", MultisigOut), + ) + + +class TransferDetails(MessageType): + @classmethod + def f_specs(cls): + return ( + ("m_block_height", UInt64), + ("m_tx", TransactionPrefix), + ("m_txid", Hash), + ("m_internal_output_index", SizeT), + ("m_global_output_index", UInt64), + ("m_spent", BoolType), + ("m_spent_height", UInt64), + ("m_key_image", KeyImage), + ("m_mask", ECKey), + ("m_amount", UInt64), + ("m_rct", BoolType), + ("m_key_image_known", BoolType), + ("m_pk_index", SizeT), + ("m_subaddr_index", SubaddressIndex), + ("m_key_image_partial", BoolType), + ("m_multisig_k", ContainerType, ECKey), + ("m_multisig_info", ContainerType, MultisigInfo), + ) + + +class TxConstructionData(MessageType): + @classmethod + def f_specs(cls): + return ( + ("sources", ContainerType, TxSourceEntry), + ("change_dts", TxDestinationEntry), + ("splitted_dsts", ContainerType, TxDestinationEntry), + ("selected_transfers", ContainerType, SizeT), + ("extra", ContainerType, UInt8), + ("unlock_time", UInt64), + ("use_rct", BoolType), + ("dests", ContainerType, TxDestinationEntry), + ("subaddr_account", UInt32), + ("subaddr_indices", ContainerType, UVarintType), # original: x.UInt32 + ) diff --git a/src/apps/monero/xmr/serialize_messages/tx_dest_entry.py b/src/apps/monero/xmr/serialize_messages/tx_dest_entry.py new file mode 100644 index 000000000..e0f3b4348 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_dest_entry.py @@ -0,0 +1,15 @@ +from apps.monero.xmr.serialize.base_types import BoolType, UVarintType +from apps.monero.xmr.serialize.message_types import MessageType +from apps.monero.xmr.serialize_messages.addr import AccountPublicAddress + + +class TxDestinationEntry(MessageType): + __slots__ = ("amount", "addr", "is_subaddress") + + @classmethod + def f_specs(cls): + return ( + ("amount", UVarintType), # original: UInt64 + ("addr", AccountPublicAddress), + ("is_subaddress", BoolType), + ) diff --git a/src/apps/monero/xmr/serialize_messages/tx_ecdh.py b/src/apps/monero/xmr/serialize_messages/tx_ecdh.py new file mode 100644 index 000000000..c3990ddf5 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_ecdh.py @@ -0,0 +1,14 @@ +from apps.monero.xmr.serialize.message_types import ContainerType, MessageType +from apps.monero.xmr.serialize_messages.base import ECKey + + +class EcdhTuple(MessageType): + __slots__ = ("mask", "amount") + + @classmethod + def f_specs(cls): + return (("mask", ECKey), ("amount", ECKey)) + + +class EcdhInfo(ContainerType): + ELEM_TYPE = EcdhTuple diff --git a/src/apps/monero/xmr/serialize_messages/tx_extra.py b/src/apps/monero/xmr/serialize_messages/tx_extra.py new file mode 100644 index 000000000..c9bbbb487 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_extra.py @@ -0,0 +1,112 @@ + +from apps.monero.xmr.serialize.base_types import SizeT, UInt8, UVarintType +from apps.monero.xmr.serialize.message_types import ( + BlobType, + ContainerType, + MessageType, + VariantType, +) +from apps.monero.xmr.serialize_messages.base import ECPublicKey, Hash + + +class TxExtraPadding(MessageType): + __slots__ = ("size",) + TX_EXTRA_PADDING_MAX_COUNT = 255 + + VARIANT_CODE = 0x0 + + @classmethod + def f_specs(cls): + return (("size", SizeT),) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.size = 0 + + async def serialize_archive(self, ar): + if ar.writing: + if self.size > self.TX_EXTRA_PADDING_MAX_COUNT: + raise ValueError("Padding too big") + for i in range(self.size): + ar.uint(0, UInt8) + + else: + self.size = 0 + buffer = bytearray(1) + for i in range(self.TX_EXTRA_PADDING_MAX_COUNT + 1): + self.size += 1 + try: + nread = await ar.iobj.areadinto(buffer) + if nread == 0: + break + except EOFError: + break + + if buffer[0] != 0: + raise ValueError("Padding error") + return self + + +class TxExtraPubKey(MessageType): + __slots__ = ("pub_key",) + VARIANT_CODE = 0x1 + + @classmethod + def f_specs(cls): + return (("pub_key", ECPublicKey),) + + +class TxExtraNonce(MessageType): + __slots__ = ("nonce",) + VARIANT_CODE = 0x2 + + @classmethod + def f_specs(cls): + return (("nonce", BlobType),) + + +class TxExtraMergeMiningTag(MessageType): + VARIANT_CODE = 0x3 + + @classmethod + def f_specs(cls): + return ( + ("field_len", UVarintType), + ("depth", UVarintType), + ("merkle_root", Hash), + ) + + +class TxExtraAdditionalPubKeys(MessageType): + __slots__ = ("data",) + VARIANT_CODE = 0x4 + + @classmethod + def f_specs(cls): + return (("data", ContainerType, ECPublicKey),) + + +class TxExtraMysteriousMinergate(MessageType): + __slots__ = ("data",) + VARIANT_CODE = 0xde + + @classmethod + def f_specs(cls): + return (("data", BlobType),) + + +class TxExtraField(VariantType): + @classmethod + def f_specs(cls): + return ( + ("tx_extra_padding", TxExtraPadding), + ("tx_extra_pub_key", TxExtraPubKey), + ("tx_extra_nonce", TxExtraNonce), + ("tx_extra_merge_mining_tag", TxExtraMergeMiningTag), + ("tx_extra_additional_pub_keys", TxExtraAdditionalPubKeys), + ("tx_extra_mysterious_minergate", TxExtraMysteriousMinergate), + ) + + +class TxExtraFields(ContainerType): + ELEM_TYPE = TxExtraField diff --git a/src/apps/monero/xmr/serialize_messages/tx_full.py b/src/apps/monero/xmr/serialize_messages/tx_full.py new file mode 100644 index 000000000..a8547c5b6 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_full.py @@ -0,0 +1,264 @@ + +from apps.monero.xmr.serialize.base_types import UInt8, UVarintType +from apps.monero.xmr.serialize.erefs import eref +from apps.monero.xmr.serialize.message_types import ContainerType, MessageType +from apps.monero.xmr.serialize_messages.base import ECKey +from apps.monero.xmr.serialize_messages.ct_keys import CtKey, CtkeyM, CtkeyV, KeyM, KeyV +from apps.monero.xmr.serialize_messages.tx_ecdh import EcdhInfo, EcdhTuple +from apps.monero.xmr.serialize_messages.tx_prefix import TransactionPrefix, TxinToKey +from apps.monero.xmr.serialize_messages.tx_rsig import RctType +from apps.monero.xmr.serialize_messages.tx_rsig_boro import RangeSig +from apps.monero.xmr.serialize_messages.tx_rsig_bulletproof import Bulletproof +from apps.monero.xmr.serialize_messages.tx_sig import ( + Signature, + SignatureArray, + get_signature_size, +) + + +class MgSig(MessageType): + __slots__ = ("ss", "cc", "II") + + @classmethod + def f_specs(cls): + return (("ss", KeyM), ("cc", ECKey)) + + +class RctSigBase(MessageType): + __slots__ = ( + "type", + "txnFee", + "message", + "mixRing", + "pseudoOuts", + "ecdhInfo", + "outPk", + ) + + @classmethod + def f_specs(cls): + return ( + ("type", UInt8), + ("txnFee", UVarintType), + ("message", ECKey), + ("mixRing", CtkeyM), + ("pseudoOuts", KeyV), + ("ecdhInfo", EcdhInfo), + ("outPk", CtkeyV), + ) + + async def serialize_rctsig_base(self, ar, inputs, outputs): + """ + Custom serialization + :param ar: + :type ar: x.Archive + :return: + """ + await self._msg_field(ar, idx=0) + if self.type == RctType.Null: + return + if ( + self.type != RctType.Full + and self.type != RctType.FullBulletproof + and self.type != RctType.Simple + and self.type != RctType.SimpleBulletproof + ): + raise ValueError("Unknown type") + + await self._msg_field(ar, idx=1) + if self.type == RctType.Simple: + await ar.prepare_container(inputs, eref(self, "pseudoOuts"), KeyV) + if ar.writing and len(self.pseudoOuts) != inputs: + raise ValueError("pseudoOuts size mismatch") + + for i in range(inputs): + await ar.field(eref(self.pseudoOuts, i), KeyV.ELEM_TYPE) + + await ar.prepare_container(outputs, eref(self, "ecdhInfo"), EcdhTuple) + if ar.writing and len(self.ecdhInfo) != outputs: + raise ValueError("EcdhInfo size mismatch") + + for i in range(outputs): + await ar.field(eref(self.ecdhInfo, i), EcdhInfo.ELEM_TYPE) + + await ar.prepare_container((outputs), eref(self, "outPk"), CtKey) + if ar.writing and len(self.outPk) != outputs: + raise ValueError("outPk size mismatch") + + for i in range(outputs): + await ar.field(eref(self.outPk[i], "mask"), ECKey) + + +class RctSigPrunable(MessageType): + __slots__ = ("rangeSigs", "bulletproofs", "MGs", "pseudoOuts") + + @classmethod + def f_specs(cls): + return ( + ("rangeSigs", ContainerType, RangeSig), + ("bulletproofs", ContainerType, Bulletproof), + ("MGs", ContainerType, MgSig), + ("pseudoOuts", KeyV), + ) + + async def serialize_rctsig_prunable(self, ar, type, inputs, outputs, mixin): + """ + Serialize rct sig + :param ar: + :type ar: x.Archive + :param type: + :param inputs: + :param outputs: + :param mixin: + :return: + """ + if type == RctType.Null: + return True + + if ( + type != RctType.Full + and type != RctType.FullBulletproof + and type != RctType.Simple + and type != RctType.SimpleBulletproof + ): + raise ValueError("Unknown type") + + if type == RctType.SimpleBulletproof or type == RctType.FullBulletproof: + if len(self.bulletproofs) != outputs: + raise ValueError("Bulletproofs size mismatch") + + await ar.prepare_container( + outputs, eref(self, "bulletproofs"), elem_type=Bulletproof + ) + for i in range(len(self.bulletproofs)): + await ar.field(elem=eref(self.bulletproofs, i), elem_type=Bulletproof) + + else: + await ar.prepare_container( + outputs, eref(self, "rangeSigs"), elem_type=RangeSig + ) + if len(self.rangeSigs) != outputs: + raise ValueError("rangeSigs size mismatch") + + for i in range(len(self.rangeSigs)): + await ar.field(elem=eref(self.rangeSigs, i), elem_type=RangeSig) + + # We keep a byte for size of MGs, because we don't know whether this is + # a simple or full rct signature, and it's starting to annoy the hell out of me + mg_elements = ( + inputs if type == RctType.Simple or type == RctType.SimpleBulletproof else 1 + ) + await ar.prepare_container(mg_elements, eref(self, "MGs"), elem_type=MgSig) + if len(self.MGs) != mg_elements: + raise ValueError("MGs size mismatch") + + for i in range(mg_elements): + # We save the MGs contents directly, because we want it to save its + # arrays and matrices without the size prefixes, and the load can't + # know what size to expect if it's not in the data + + await ar.prepare_container( + mixin + 1, eref(self.MGs[i], "ss"), elem_type=KeyM + ) + if ar.writing and len(self.MGs[i].ss) != mixin + 1: + raise ValueError("MGs size mismatch") + + for j in range(mixin + 1): + mg_ss2_elements = 1 + ( + 1 + if type == RctType.Simple or type == RctType.SimpleBulletproof + else inputs + ) + await ar.prepare_container( + mg_ss2_elements, eref(self.MGs[i].ss, j), elem_type=KeyM.ELEM_TYPE + ) + + if ar.writing and len(self.MGs[i].ss[j]) != mg_ss2_elements: + raise ValueError("MGs size mismatch 2") + + for k in range(mg_ss2_elements): + await ar.field(eref(self.MGs[i].ss[j], k), elem_type=KeyV.ELEM_TYPE) + + await ar.field(eref(self.MGs[i], "cc"), elem_type=ECKey) + + if type == RctType.SimpleBulletproof: + await ar.prepare_container(inputs, eref(self, "pseudoOuts"), elem_type=KeyV) + if ar.writing and len(self.pseudoOuts) != inputs: + raise ValueError("pseudoOuts size mismatch") + + for i in range(inputs): + await ar.field(eref(self.pseudoOuts, i), elem_type=KeyV.ELEM_TYPE) + + +class RctSig(RctSigBase): + @classmethod + def f_specs(cls): + return RctSigBase.f_specs() + (("p", RctSigPrunable),) + + +class Transaction(TransactionPrefix): + @classmethod + def f_specs(cls): + return TransactionPrefix.f_specs() + ( + ("signatures", ContainerType, SignatureArray), + ("rct_signatures", RctSig), + ) + + async def serialize_archive(self, ar): + """ + Serialize the transaction + :param ar: + :type ar: x.Archive + :return: + """ + # Transaction prefix serialization first. + await ar.message(self, TransactionPrefix) + + if self.version == 1: + await ar.prepare_container( + len(self.vin), eref(self, "signatures"), elem_type=SignatureArray + ) + signatures_not_expected = len(self.signatures) == 0 + if not signatures_not_expected and len(self.vin) != len(self.signatures): + raise ValueError("Signature size mismatch") + + for i in range(len(self.vin)): + sig_size = get_signature_size(self.vin[i]) + if signatures_not_expected: + if 0 == sig_size: + continue + else: + raise ValueError("Unexpected sig") + + await ar.prepare_container( + sig_size, eref(self.signatures, i), elem_type=Signature + ) + if sig_size != len(self.signatures[i]): + raise ValueError("Unexpected sig size") + + await ar.message(self.signatures[i], Signature) + + else: + if len(self.vin) == 0: + return + + await ar.prepare_message(eref(self, "rct_signatures"), RctSig) + await self.rct_signatures.serialize_rctsig_base( + ar, len(self.vin), len(self.vout) + ) + + if self.rct_signatures.type != RctType.Null: + mixin_size = ( + len(self.vin[0].key_offsets) - 1 + if len(self.vin) > 0 and isinstance(self.vin[0], TxinToKey) + else 0 + ) + await ar.prepare_message(eref(self.rct_signatures, "p"), RctSigPrunable) + await self.rct_signatures.p.serialize_rctsig_prunable( + ar, + self.rct_signatures.type, + len(self.vin), + len(self.vout), + mixin_size, + ) + return self diff --git a/src/apps/monero/xmr/serialize_messages/tx_prefix.py b/src/apps/monero/xmr/serialize_messages/tx_prefix.py new file mode 100644 index 000000000..55037a189 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_prefix.py @@ -0,0 +1,122 @@ +from micropython import const + +from apps.monero.xmr.serialize.base_types import UInt8, UVarintType +from apps.monero.xmr.serialize.message_types import ( + BlobType, + ContainerType, + MessageType, + VariantType, +) +from apps.monero.xmr.serialize_messages.base import ECPublicKey, Hash, KeyImage + +_c0 = const(0) +_c1 = const(1) +_c32 = const(32) +_c64 = const(64) + + +class TxoutToScript(MessageType): + __slots__ = ("keys", "script") + VARIANT_CODE = 0x0 + + @classmethod + def f_specs(cls): + return (("keys", ContainerType, ECPublicKey), ("script", ContainerType, UInt8)) + + +class TxoutToKey(MessageType): + __slots__ = ("key",) + VARIANT_CODE = 0x2 + + @classmethod + def f_specs(cls): + return (("key", ECPublicKey),) + + +class TxoutToScriptHash(MessageType): + __slots__ = ("hash",) + VARIANT_CODE = 0x1 + + @classmethod + def f_specs(cls): + return (("hash", Hash),) + + +class TxoutTargetV(VariantType): + @classmethod + def f_specs(cls): + return ( + ("txout_to_script", TxoutToScript), + ("txout_to_scripthash", TxoutToScriptHash), + ("txout_to_key", TxoutToKey), + ) + + +class TxinGen(MessageType): + __slots__ = ("height",) + VARIANT_CODE = 0xff + + @classmethod + def f_specs(cls): + return (("height", UVarintType),) + + +class TxinToKey(MessageType): + __slots__ = ("amount", "key_offsets", "k_image") + VARIANT_CODE = 0x2 + + @classmethod + def f_specs(cls): + return ( + ("amount", UVarintType), + ("key_offsets", ContainerType, UVarintType), + ("k_image", KeyImage), + ) + + +class TxinToScript(MessageType): + __slots__ = () + VARIANT_CODE = _c0 + + +class TxinToScriptHash(MessageType): + __slots__ = () + VARIANT_CODE = _c1 + + +class TxInV(VariantType): + @classmethod + def f_specs(cls): + return ( + ("txin_gen", TxinGen), + ("txin_to_script", TxinToScript), + ("txin_to_scripthash", TxinToScriptHash), + ("txin_to_key", TxinToKey), + ) + + +class TxOut(MessageType): + __slots__ = ("amount", "target") + + @classmethod + def f_specs(cls): + return (("amount", UVarintType), ("target", TxoutTargetV)) + + +class TransactionPrefix(MessageType): + @classmethod + def f_specs(cls): + return ( + ("version", UVarintType), + ("unlock_time", UVarintType), + ("vin", ContainerType, TxInV), + ("vout", ContainerType, TxOut), + ("extra", ContainerType, UInt8), + ) + + +class TransactionPrefixExtraBlob(TransactionPrefix): + # noinspection PyTypeChecker + @classmethod + def f_specs(cls): + return TransactionPrefix.f_specs()[:-1] + (("extra", BlobType),) diff --git a/src/apps/monero/xmr/serialize_messages/tx_rsig.py b/src/apps/monero/xmr/serialize_messages/tx_rsig.py new file mode 100644 index 000000000..c1c8c0944 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_rsig.py @@ -0,0 +1,6 @@ +class RctType(object): + Null = 0 + Full = 1 + Simple = 2 + FullBulletproof = 3 + SimpleBulletproof = 4 diff --git a/src/apps/monero/xmr/serialize_messages/tx_rsig_boro.py b/src/apps/monero/xmr/serialize_messages/tx_rsig_boro.py new file mode 100644 index 000000000..740971750 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_rsig_boro.py @@ -0,0 +1,19 @@ +from apps.monero.xmr.serialize.message_types import MessageType +from apps.monero.xmr.serialize_messages.base import ECKey +from apps.monero.xmr.serialize_messages.ct_keys import Key64 + + +class BoroSig(MessageType): + __slots__ = ("s0", "s1", "ee") + + @classmethod + def f_specs(cls): + return (("s0", Key64), ("s1", Key64), ("ee", ECKey)) + + +class RangeSig(MessageType): + __slots__ = ("asig", "Ci") + + @classmethod + def f_specs(cls): + return (("asig", BoroSig), ("Ci", Key64)) diff --git a/src/apps/monero/xmr/serialize_messages/tx_rsig_bulletproof.py b/src/apps/monero/xmr/serialize_messages/tx_rsig_bulletproof.py new file mode 100644 index 000000000..40a3d9eb8 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_rsig_bulletproof.py @@ -0,0 +1,23 @@ +from apps.monero.xmr.serialize.message_types import MessageType +from apps.monero.xmr.serialize_messages.base import ECKey +from apps.monero.xmr.serialize_messages.ct_keys import KeyV + + +class Bulletproof(MessageType): + __slots__ = ("V", "A", "S", "T1", "T2", "taux", "mu", "L", "R", "a", "b", "t") + + @classmethod + def f_specs(cls): + return ( + ("A", ECKey), + ("S", ECKey), + ("T1", ECKey), + ("T2", ECKey), + ("taux", ECKey), + ("mu", ECKey), + ("L", KeyV), + ("R", KeyV), + ("a", ECKey), + ("b", ECKey), + ("t", ECKey), + ) diff --git a/src/apps/monero/xmr/serialize_messages/tx_sig.py b/src/apps/monero/xmr/serialize_messages/tx_sig.py new file mode 100644 index 000000000..3823f9238 --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_sig.py @@ -0,0 +1,41 @@ +from apps.monero.xmr.serialize.erefs import eref +from apps.monero.xmr.serialize.message_types import ContainerType, MessageType +from apps.monero.xmr.serialize_messages.base import ECKey +from apps.monero.xmr.serialize_messages.tx_prefix import ( + TxinGen, + TxinToKey, + TxinToScript, + TxinToScriptHash, +) + + +class Signature(MessageType): + __slots__ = ("c", "r") + + @classmethod + def f_specs(cls): + return (("c", ECKey), ("r", ECKey)) + + async def serialize_archive(self, ar): + ar.field(eref(self, "c"), ECKey) + ar.field(eref(self, "r"), ECKey) + return self + + +class SignatureArray(ContainerType): + FIX_SIZE = 0 + ELEM_TYPE = Signature + + +def get_signature_size(msg): + """ + Returns a signature size for the input + :param msg: + :return: + """ + if isinstance(msg, (TxinGen, TxinToScript, TxinToScriptHash)): + return 0 + elif isinstance(msg, TxinToKey): + return len(msg.key_offsets) + else: + raise ValueError("Unknown tx in") diff --git a/src/apps/monero/xmr/serialize_messages/tx_src_entry.py b/src/apps/monero/xmr/serialize_messages/tx_src_entry.py new file mode 100644 index 000000000..f3778d7fb --- /dev/null +++ b/src/apps/monero/xmr/serialize_messages/tx_src_entry.py @@ -0,0 +1,36 @@ +from apps.monero.xmr.serialize.base_types import BoolType, SizeT, UInt64, UVarintType +from apps.monero.xmr.serialize.message_types import ( + ContainerType, + MessageType, + TupleType, +) +from apps.monero.xmr.serialize_messages.base import ECKey, ECPublicKey +from apps.monero.xmr.serialize_messages.ct_keys import CtKey + + +class MultisigKLRki(MessageType): + @classmethod + def f_specs(cls): + return (("K", ECKey), ("L", ECKey), ("R", ECKey), ("ki", ECKey)) + + +class OutputEntry(TupleType): + @classmethod + def f_specs(cls): + return (UVarintType, CtKey) # original: x.UInt64 + + +class TxSourceEntry(MessageType): + @classmethod + def f_specs(cls): + return ( + ("outputs", ContainerType, OutputEntry), + ("real_output", SizeT), + ("real_out_tx_key", ECPublicKey), + ("real_out_additional_tx_keys", ContainerType, ECPublicKey), + ("real_output_in_tx_index", UInt64), + ("amount", UInt64), + ("rct", BoolType), + ("mask", ECKey), + ("multisig_kLRki", MultisigKLRki), + ) diff --git a/src/apps/monero/xmr/sub/__init__.py b/src/apps/monero/xmr/sub/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/monero/xmr/sub/addr.py b/src/apps/monero/xmr/sub/addr.py new file mode 100644 index 000000000..f00d5303f --- /dev/null +++ b/src/apps/monero/xmr/sub/addr.py @@ -0,0 +1,131 @@ +from trezor.crypto import monero as tcry + +from apps.monero.xmr.sub.xmr_net import NetworkTypes, net_version + + +def addr_to_hash(addr): + """ + Creates hashable address representation + :param addr: + :return: + """ + return bytes(addr.m_spend_public_key + addr.m_view_public_key) + + +def encode_addr(version, spend_pub, view_pub): + """ + Encodes public keys as versions + :param version: + :param spend_pub: + :param view_pub: + :return: + """ + buf = spend_pub + view_pub + return tcry.xmr_base58_addr_encode_check(ord(version), bytes(buf)) + + +def decode_addr(addr): + """ + Given address, get version and public spend and view keys. + + :param addr: + :return: + """ + d, version = tcry.xmr_base58_addr_decode_check(bytes(addr)) + pub_spend_key = d[0:32] + pub_view_key = d[32:64] + return version, pub_spend_key, pub_view_key + + +def public_addr_encode(pub_addr, is_sub=False, net=NetworkTypes.MAINNET): + """ + Encodes public address to Monero address + :param pub_addr: + :type pub_addr: apps.monero.xmr.serialize_messages.addr.AccountPublicAddress + :param is_sub: + :param net: + :return: + """ + net_ver = net_version(net, is_sub) + return encode_addr(net_ver, pub_addr.m_spend_public_key, pub_addr.m_view_public_key) + + +def classify_subaddresses(tx_dests, change_addr): + """ + Classify destination subaddresses + void classify_addresses() + :param tx_dests: + :type tx_dests: list[apps.monero.xmr.serialize_messages.tx_construct.TxDestinationEntry] + :param change_addr: + :return: + """ + num_stdaddresses = 0 + num_subaddresses = 0 + single_dest_subaddress = None + addr_set = set() + for tx in tx_dests: + if change_addr and addr_eq(change_addr, tx.addr): + continue + addr_hashed = addr_to_hash(tx.addr) + if addr_hashed in addr_set: + continue + addr_set.add(addr_hashed) + if tx.is_subaddress: + num_subaddresses += 1 + single_dest_subaddress = tx.addr + else: + num_stdaddresses += 1 + return num_stdaddresses, num_subaddresses, single_dest_subaddress + + +def addr_eq(a, b): + """ + Address comparisson. Allocation free. + :param a: + :param b: + :return: + """ + return pub_eq(a.m_spend_public_key, b.m_spend_public_key) and pub_eq( + a.m_view_public_key, b.m_view_public_key + ) + + +def pub_eq(a, b): + """ + Simple non-constant time public key compare + :param a: + :param b: + :return: + """ + if a == b: + return True + if (a is None and b is not None) or (a is not None and b is None): + return False + if len(a) != len(b): + return False + for i in range(len(a)): + if a[i] != b[i]: + return False + return True + + +def get_change_addr_idx(outputs, change_dts): + """ + Returns ID of the change output from the change_dts and outputs + :param tsx_data: + :return: + """ + if change_dts is None: + return None + + change_idx = None + change_coord = change_dts.amount, change_dts.addr + for idx, dst in enumerate(outputs): + if ( + change_coord + and change_coord[0] + and change_coord[0] == dst.amount + and addr_eq(change_coord[1], dst.addr) + ): + change_idx = idx + return change_idx diff --git a/src/apps/monero/xmr/sub/creds.py b/src/apps/monero/xmr/sub/creds.py new file mode 100644 index 000000000..84336c602 --- /dev/null +++ b/src/apps/monero/xmr/sub/creds.py @@ -0,0 +1,46 @@ +from apps.monero.xmr import crypto +from apps.monero.xmr.sub.addr import encode_addr +from apps.monero.xmr.sub.xmr_net import NetworkTypes, net_version + + +class AccountCreds(object): + """ + Stores account private keys + """ + + def __init__( + self, + view_key_private=None, + spend_key_private=None, + view_key_public=None, + spend_key_public=None, + address=None, + network_type=NetworkTypes.MAINNET, + ): + self.view_key_private = view_key_private + self.view_key_public = view_key_public + self.spend_key_private = spend_key_private + self.spend_key_public = spend_key_public + self.address = address + self.network_type = network_type + self.multisig_keys = [] + + @classmethod + def new_wallet( + cls, priv_view_key, priv_spend_key, network_type=NetworkTypes.MAINNET + ): + pub_view_key = crypto.scalarmult_base(priv_view_key) + pub_spend_key = crypto.scalarmult_base(priv_spend_key) + addr = encode_addr( + net_version(network_type), + crypto.encodepoint(pub_spend_key), + crypto.encodepoint(pub_view_key), + ) + return cls( + view_key_private=priv_view_key, + spend_key_private=priv_spend_key, + view_key_public=pub_view_key, + spend_key_public=pub_spend_key, + address=addr, + network_type=network_type, + ) diff --git a/src/apps/monero/xmr/sub/keccak_hasher.py b/src/apps/monero/xmr/sub/keccak_hasher.py new file mode 100644 index 000000000..00ace5930 --- /dev/null +++ b/src/apps/monero/xmr/sub/keccak_hasher.py @@ -0,0 +1,67 @@ +from apps.monero.xmr import crypto +from apps.monero.xmr.serialize import xmrserialize + + +class KeccakArchive(object): + def __init__(self, ctx=None): + self.kwriter = get_keccak_writer(ctx=ctx) + self.ar = xmrserialize.Archive(self.kwriter, True) + + def ctx(self): + return self.kwriter.ctx() + + def refresh(self, ctx=None, xser=None): + if ctx is None: + ctx = self.kwriter.ctx() + if xser is None: + xser = xmrserialize + + self.kwriter = get_keccak_writer(ctx=ctx) + self.ar = xser.Archive(self.kwriter, True) + return self.ar + + +class HashWrapper(object): + def __init__(self, ctx): + self.ctx = ctx + + def update(self, buf): + if len(buf) == 0: + return + self.ctx.update(buf) + + def digest(self): + return self.ctx.digest() + + def hexdigest(self): + return self.ctx.hexdigest() + + +class AHashWriter: + def __init__(self, hasher, sub_writer=None): + self.hasher = hasher + self.sub_writer = sub_writer + + async def awrite(self, buf): + self.hasher.update(buf) + if self.sub_writer: + await self.sub_writer.awrite(buf) + return len(buf) + + def get_digest(self, *args) -> bytes: + return self.hasher.digest(*args) + + def ctx(self): + return self.hasher.ctx + + +def get_keccak_writer(sub_writer=None, ctx=None): + """ + Creates new fresh async Keccak writer + :param sub_writer: + :param ctx: + :return: + """ + return AHashWriter( + HashWrapper(crypto.get_keccak() if ctx is None else ctx), sub_writer=sub_writer + ) diff --git a/src/apps/monero/xmr/sub/mlsag_hasher.py b/src/apps/monero/xmr/sub/mlsag_hasher.py new file mode 100644 index 000000000..263a2146b --- /dev/null +++ b/src/apps/monero/xmr/sub/mlsag_hasher.py @@ -0,0 +1,139 @@ +from apps.monero.xmr import crypto +from apps.monero.xmr.serialize_messages.base import ECKey +from apps.monero.xmr.serialize_messages.ct_keys import KeyV +from apps.monero.xmr.serialize_messages.tx_ecdh import EcdhInfo +from apps.monero.xmr.serialize_messages.tx_full import RctSigBase + + +class PreMlsagHasher(object): + """ + Iterative construction of the pre_mlsag_hash + """ + + def __init__(self, state=None): + from apps.monero.xmr.sub.keccak_hasher import KeccakArchive, HashWrapper + + self.is_simple = state[0] if state else None + self.state = state[1] if state else 0 + self.kc_master = HashWrapper(state[2] if state else crypto.get_keccak()) + self.rsig_hasher = state[3] if state else crypto.get_keccak() + self.rtcsig_hasher = None + if state: + self.rtcsig_hasher = KeccakArchive(state[4]) if state[4] else None + else: + self.rtcsig_hasher = KeccakArchive() + + def state_save(self): + return ( + self.is_simple, + self.state, + self.kc_master.ctx, + self.rsig_hasher, + self.rtcsig_hasher.ctx() if self.rtcsig_hasher else None, + ) + + def state_load(self, x): + from apps.monero.xmr.sub.keccak_hasher import KeccakArchive, HashWrapper + + self.is_simple = x[0] + self.state = x[1] + self.kc_master = HashWrapper(x[2]) + self.rsig_hasher = x[3] + if x[4]: + self.rtcsig_hasher = KeccakArchive(x[4]) + else: + self.rtcsig_hasher = None + + def init(self, is_simple): + if self.state != 0: + raise ValueError("State error") + + self.state = 1 + self.is_simple = is_simple + + async def set_message(self, message): + self.kc_master.update(message) + + async def set_type_fee(self, rv_type, fee): + if self.state != 1: + raise ValueError("State error") + self.state = 2 + + rfields = RctSigBase.f_specs() + await self.rtcsig_hasher.ar.message_field( + None, field=rfields[0], fvalue=rv_type + ) + await self.rtcsig_hasher.ar.message_field(None, field=rfields[1], fvalue=fee) + + async def set_pseudo_out(self, out): + if self.state != 2 and self.state != 3: + raise ValueError("State error") + self.state = 3 + + await self.rtcsig_hasher.ar.field(out, KeyV.ELEM_TYPE) + + async def set_ecdh(self, ecdh): + if self.state != 2 and self.state != 3 and self.state != 4: + raise ValueError("State error") + self.state = 4 + + await self.rtcsig_hasher.ar.field(ecdh, EcdhInfo.ELEM_TYPE) + + async def set_out_pk(self, out_pk, mask=None): + if self.state != 4 and self.state != 5: + raise ValueError("State error") + self.state = 5 + + await self.rtcsig_hasher.ar.field(mask if mask else out_pk.mask, ECKey) + + async def rctsig_base_done(self): + if self.state != 5: + raise ValueError("State error") + self.state = 6 + + c_hash = self.rtcsig_hasher.kwriter.get_digest() + self.kc_master.update(c_hash) + self.rtcsig_hasher = None + + async def rsig_val(self, p, bulletproof, raw=False): + if self.state == 8: + raise ValueError("State error") + + if raw: + self.rsig_hasher.update(p) + return + + if bulletproof: + self.rsig_hasher.update(p.A) + self.rsig_hasher.update(p.S) + self.rsig_hasher.update(p.T1) + self.rsig_hasher.update(p.T2) + self.rsig_hasher.update(p.taux) + self.rsig_hasher.update(p.mu) + for i in range(len(p.L)): + self.rsig_hasher.update(p.L[i]) + for i in range(len(p.R)): + self.rsig_hasher.update(p.R[i]) + self.rsig_hasher.update(p.a) + self.rsig_hasher.update(p.b) + self.rsig_hasher.update(p.t) + + else: + for i in range(64): + self.rsig_hasher.update(p.asig.s0[i]) + for i in range(64): + self.rsig_hasher.update(p.asig.s1[i]) + self.rsig_hasher.update(p.asig.ee) + for i in range(64): + self.rsig_hasher.update(p.Ci[i]) + + async def get_digest(self): + if self.state != 6: + raise ValueError("State error") + self.state = 8 + + c_hash = self.rsig_hasher.digest() + self.rsig_hasher = None + + self.kc_master.update(c_hash) + return self.kc_master.digest() diff --git a/src/apps/monero/xmr/sub/recode.py b/src/apps/monero/xmr/sub/recode.py new file mode 100644 index 000000000..be6d351ff --- /dev/null +++ b/src/apps/monero/xmr/sub/recode.py @@ -0,0 +1,46 @@ +from apps.monero.xmr import crypto +from apps.monero.xmr.serialize_messages.tx_ecdh import EcdhTuple + + +def copy_ecdh(ecdh): + """ + Clones ECDH tuple + :param ecdh: + :return: + """ + return EcdhTuple(mask=ecdh.mask, amount=ecdh.amount) + + +def recode_ecdh(ecdh, encode=True): + """ + In-place ecdhtuple recoding + :param ecdh: + :param encode: if true encodes to byte representation, otherwise decodes from byte representation + :return: + """ + recode_int = crypto.encodeint if encode else crypto.decodeint + ecdh.mask = recode_int(ecdh.mask) + ecdh.amount = recode_int(ecdh.amount) + return ecdh + + +def recode_msg(mgs, encode=True): + """ + Recodes MGs signatures from raw forms to bytearrays so it works with serialization + :param rv: + :param encode: if true encodes to byte representation, otherwise decodes from byte representation + :return: + """ + recode_int = crypto.encodeint if encode else crypto.decodeint + recode_point = crypto.encodepoint if encode else crypto.decodepoint + + for idx in range(len(mgs)): + mgs[idx].cc = recode_int(mgs[idx].cc) + if hasattr(mgs[idx], "II") and mgs[idx].II: + for i in range(len(mgs[idx].II)): + mgs[idx].II[i] = recode_point(mgs[idx].II[i]) + + for i in range(len(mgs[idx].ss)): + for j in range(len(mgs[idx].ss[i])): + mgs[idx].ss[i][j] = recode_int(mgs[idx].ss[i][j]) + return mgs diff --git a/src/apps/monero/xmr/sub/tsx_helper.py b/src/apps/monero/xmr/sub/tsx_helper.py new file mode 100644 index 000000000..cafebe663 --- /dev/null +++ b/src/apps/monero/xmr/sub/tsx_helper.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Author: Dusan Klinec, ph4r05, 2018 +from apps.monero.xmr import crypto +from apps.monero.xmr.serialize import xmrserialize +from apps.monero.xmr.serialize.readwriter import MemoryReaderWriter +from apps.monero.xmr.serialize_messages.addr import AccountPublicAddress +from apps.monero.xmr.serialize_messages.tx_extra import ( + TxExtraAdditionalPubKeys, + TxExtraField, +) + + +async def parse_extra_fields(extra_buff): + """ + Parses extra buffer to the extra fields vector + :param extra_buff: + :return: + """ + extras = [] + rw = MemoryReaderWriter(extra_buff) + ar2 = xmrserialize.Archive(rw, False) + while len(rw.get_buffer()) > 0: + extras.append(await ar2.variant(elem_type=TxExtraField)) + return extras + + +def find_tx_extra_field_by_type(extra_fields, msg, idx=0): + """ + Finds given message type in the extra array, or returns None if not found + :param extra_fields: + :param msg: + :param idx: + :return: + """ + cur_idx = 0 + for x in extra_fields: + if isinstance(x, msg): + if cur_idx == idx: + return x + cur_idx += 1 + return None + + +def has_encrypted_payment_id(extra_nonce): + """ + Returns true if encrypted payment id is present + :param extra_nonce: + :return: + """ + return len(extra_nonce) == 9 and extra_nonce[0] == 1 + + +def has_payment_id(extra_nonce): + """ + Returns true if payment id is present + :param extra_nonce: + :return: + """ + return len(extra_nonce) == 33 and extra_nonce[0] == 0 + + +def get_payment_id_from_tx_extra_nonce(extra_nonce): + """ + Extracts encrypted payment id from extra + :param extra_nonce: + :return: + """ + if 33 != len(extra_nonce): + raise ValueError("Nonce size mismatch") + if 0x0 != extra_nonce[0]: + raise ValueError("Nonce payment type invalid") + return extra_nonce[1:] + + +def get_encrypted_payment_id_from_tx_extra_nonce(extra_nonce): + """ + Extracts encrypted payment id from extra + :param extra_nonce: + :return: + """ + if 9 != len(extra_nonce): + raise ValueError("Nonce size mismatch") + if 0x1 != extra_nonce[0]: + raise ValueError("Nonce payment type invalid") + return extra_nonce[1:] + + +def set_payment_id_to_tx_extra_nonce(payment_id): + """ + Sets payment ID to the extra + :param payment_id: + :return: + """ + return b"\x00" + payment_id + + +def absolute_output_offsets_to_relative(off): + """ + Relative offsets, prev + cur = next. + Helps with varint encoding size. + :param off: + :return: + """ + if len(off) == 0: + return off + res = sorted(off) + for i in range(len(off) - 1, 0, -1): + res[i] -= res[i - 1] + return res + + +def get_destination_view_key_pub(destinations, change_addr=None): + """ + Returns destination address public view key + :param destinations: + :type destinations: list[apps.monero.xmr.serialize_messages.tx_construct.TxDestinationEntry] + :param change_addr: + :return: + """ + from apps.monero.xmr.sub.addr import addr_eq + + addr = AccountPublicAddress( + m_spend_public_key=crypto.NULL_KEY_ENC, m_view_public_key=crypto.NULL_KEY_ENC + ) + count = 0 + for dest in destinations: + if dest.amount == 0: + continue + if change_addr and addr_eq(dest.addr, change_addr): + continue + if addr_eq(dest.addr, addr): + continue + if count > 0: + return crypto.NULL_KEY_ENC + addr = dest.addr + count += 1 + return addr.m_view_public_key + + +def encrypt_payment_id(payment_id, public_key, secret_key): + """ + Encrypts payment_id hex. + Used in the transaction extra. Only recipient is able to decrypt. + :param payment_id: + :param public_key: + :param secret_key: + :return: + """ + derivation_p = crypto.generate_key_derivation(public_key, secret_key) + derivation = bytearray(33) + derivation = crypto.encodepoint_into(derivation_p, derivation) + derivation[32] = 0x8b + hash = crypto.cn_fast_hash(derivation) + pm_copy = bytearray(payment_id) + for i in range(8): + pm_copy[i] ^= hash[i] + return pm_copy + + +def set_encrypted_payment_id_to_tx_extra_nonce(payment_id): + return b"\x01" + payment_id + + +async def remove_field_from_tx_extra(extra, mtype): + """ + Removes extra field of fiven type from the buffer + Reserializes with skipping the given mtype. + :param extra: + :param mtype: + :return: + """ + if len(extra) == 0: + return [] + + reader = MemoryReaderWriter(extra) + writer = MemoryReaderWriter() + ar_read = xmrserialize.Archive(reader, False) + ar_write = xmrserialize.Archive(writer, True) + while len(reader.get_buffer()) > 0: + c_extras = await ar_read.variant(elem_type=TxExtraField) + if not isinstance(c_extras, mtype): + await ar_write.variant(c_extras, elem_type=TxExtraField) + + return writer.get_buffer() + + +def add_extra_nonce_to_tx_extra(extra, extra_nonce): + """ + Appends nonce extra to the extra buffer + :param extra: + :param extra_nonce: + :return: + """ + if len(extra_nonce) > 255: + raise ValueError("Nonce could be 255 bytes max") + extra += b"\x02" + len(extra_nonce).to_bytes(1, "big") + extra_nonce + return extra + + +def add_tx_pub_key_to_extra(tx_extra, pub_key): + """ + Adds public key to the extra + :param tx_extra: + :param pub_key: + :return: + """ + to_add = bytearray(33) + to_add[0] = 1 + crypto.encodepoint_into(pub_key, memoryview(to_add)[1:]) # TX_EXTRA_TAG_PUBKEY + return tx_extra + to_add + + +async def add_additional_tx_pub_keys_to_extra( + tx_extra, additional_pub_keys=None, pub_enc=None +): + """ + Adds all pubkeys to the extra + :param tx_extra: + :param additional_pub_keys: + :param pub_enc: None + :return: + """ + pubs_msg = TxExtraAdditionalPubKeys( + data=pub_enc + if pub_enc + else [crypto.encodepoint(x) for x in additional_pub_keys] + ) + + rw = MemoryReaderWriter() + ar = xmrserialize.Archive(rw, True) + + # format: variant_tag (0x4) | array len varint | 32B | 32B | ... + await ar.variant(pubs_msg, TxExtraField) + tx_extra += bytes(rw.get_buffer()) + return tx_extra diff --git a/src/apps/monero/xmr/sub/xmr_net.py b/src/apps/monero/xmr/sub/xmr_net.py new file mode 100644 index 000000000..308215640 --- /dev/null +++ b/src/apps/monero/xmr/sub/xmr_net.py @@ -0,0 +1,46 @@ +class NetworkTypes(object): + MAINNET = 0 + TESTNET = 1 + STAGENET = 2 + FAKECHAIN = 3 + + +class MainNet(object): + PUBLIC_ADDRESS_BASE58_PREFIX = 18 + PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 19 + PUBLIC_SUBADDRESS_BASE58_PREFIX = 42 + + +class TestNet(object): + PUBLIC_ADDRESS_BASE58_PREFIX = 53 + PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 54 + PUBLIC_SUBADDRESS_BASE58_PREFIX = 63 + + +class StageNet(object): + PUBLIC_ADDRESS_BASE58_PREFIX = 24 + PUBLIC_INTEGRATED_ADDRESS_BASE58_PREFIX = 25 + PUBLIC_SUBADDRESS_BASE58_PREFIX = 36 + + +def net_version(network_type=NetworkTypes.MAINNET, is_subaddr=False): + """ + Network version bytes used for address construction + :return: + """ + c_net = None + if network_type is None or network_type == NetworkTypes.MAINNET: + c_net = MainNet + elif network_type == NetworkTypes.TESTNET: + c_net = TestNet + elif network_type == NetworkTypes.STAGENET: + c_net = StageNet + else: + raise ValueError("Unknown network type: %s" % network_type) + + prefix = ( + c_net.PUBLIC_ADDRESS_BASE58_PREFIX + if not is_subaddr + else c_net.PUBLIC_SUBADDRESS_BASE58_PREFIX + ) + return bytes([prefix]) diff --git a/src/apps/monero/xmr/tsx_data.py b/src/apps/monero/xmr/tsx_data.py new file mode 100644 index 000000000..38733d688 --- /dev/null +++ b/src/apps/monero/xmr/tsx_data.py @@ -0,0 +1,61 @@ +from apps.monero.xmr.serialize.base_types import BoolType, UVarintType +from apps.monero.xmr.serialize.message_types import BlobType, ContainerType, MessageType +from apps.monero.xmr.serialize_messages.base import SecretKey +from apps.monero.xmr.serialize_messages.tx_dest_entry import TxDestinationEntry + + +class TsxData(MessageType): + """ + TsxData, initial input to the transaction processing. + Serialization structure for easy hashing. + """ + + __slots__ = ( + "version", + "payment_id", + "unlock_time", + "outputs", + "change_dts", + "num_inputs", + "mixin", + "fee", + "account", + "minor_indices", + "is_multisig", + "exp_tx_prefix_hash", + "use_tx_keys", + "is_bulletproof", + ) + + @classmethod + def f_specs(cls): + return ( + ("version", UVarintType), + ("payment_id", BlobType), + ("unlock_time", UVarintType), + ("outputs", ContainerType, TxDestinationEntry), + ("change_dts", TxDestinationEntry), + ("num_inputs", UVarintType), + ("mixin", UVarintType), + ("fee", UVarintType), + ("account", UVarintType), + ("minor_indices", ContainerType, UVarintType), + ("is_multisig", BoolType), + ("exp_tx_prefix_hash", BlobType), # expected prefix hash, bail on error + ("use_tx_keys", ContainerType, SecretKey), # use this secret key, multisig + ("is_bulletproof", BoolType), + ) + + def __init__(self, payment_id=None, outputs=None, change_dts=None, **kwargs): + super().__init__(**kwargs) + + self.payment_id = payment_id + self.change_dts = change_dts + self.fee = 0 + self.account = 0 + self.minor_indices = [0] + self.outputs = outputs if outputs else [] # type: list[TxDestinationEntry] + self.is_multisig = False + self.is_bulletproof = False + self.exp_tx_prefix_hash = b"" + self.use_tx_keys = [] diff --git a/src/main.py b/src/main.py index 24af03958..9fd1dced1 100644 --- a/src/main.py +++ b/src/main.py @@ -14,6 +14,7 @@ import apps.wallet import apps.ethereum import apps.lisk +import apps.monero import apps.nem import apps.stellar import apps.ripple @@ -30,6 +31,7 @@ apps.wallet.boot() apps.ethereum.boot() apps.lisk.boot() +apps.monero.boot() apps.nem.boot() apps.stellar.boot() apps.ripple.boot() diff --git a/src/trezor/crypto/__init__.py b/src/trezor/crypto/__init__.py index 7eb67a3d9..3bdbcaf74 100644 --- a/src/trezor/crypto/__init__.py +++ b/src/trezor/crypto/__init__.py @@ -3,6 +3,7 @@ bip39, chacha20poly1305, crc, + monero, nem, pbkdf2, random,