def do_keys(self, line): print( "Spend key priv: 0x%s" % binascii.hexlify(crypto.encodeint(self.creds.spend_key_private)).decode( "ascii" ) ) print( "View key priv: 0x%s" % binascii.hexlify(crypto.encodeint(self.creds.view_key_private)).decode( "ascii" ) ) print("") print( "Spend key pub: 0x%s" % binascii.hexlify(crypto.encodepoint(self.creds.spend_key_public)).decode( "ascii" ) ) print( "View key pub: 0x%s" % binascii.hexlify(crypto.encodepoint(self.creds.view_key_public)).decode( "ascii" ) )
async def final_msg(self, *args, **kwargs): """ Final step after transaction signing. :param args: :param kwargs: :return: """ from monero_glue.messages.MoneroTransactionFinalAck import ( MoneroTransactionFinalAck ) from monero_glue.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 )
async def sync(self, ctx, tds: MoneroKeyImageSyncStepRequest): self.ctx = ctx if self.blocked: raise ValueError("Blocked") if len(tds.tdis) == 0: raise ValueError("Empty") resp = [] for td in tds.tdis: self.c_idx += 1 if self.c_idx >= self.num: raise ValueError("Too many outputs") hash = key_image.compute_hash(td) self.hasher.update(hash) ki, sig = await key_image.export_key_image(self.creds, self.subaddresses, td) buff = crypto.encodepoint(ki) buff += crypto.encodeint(sig[0][0]) buff += crypto.encodeint(sig[0][1]) nonce, ciph, tag = chacha_poly.encrypt(self.enc_key, buff) eki = MoneroExportedKeyImage(iv=nonce, tag=tag, blob=ciph) resp.append(eki) return MoneroKeyImageSyncStepAck(kis=resp)
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: """ 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) # tx_key = crypto.pbkdf2(passwd, salt, count=100) return tx_key, salt, rand_mult
async def unblind(self): AKout = self._fetch_decrypt(32) k = self._fetch(32) v = self._fetch(32) v, k, AKout = self.unblind_int(v, k, AKout) self._insert(crypto.encodeint(v)) self._insert(crypto.encodeint(k)) return SW_OK
async def chacha8_prekey(self): abt = bytearray(65) memcpy(abt, 0, crypto.encodeint(self.a), 0, 32) memcpy(abt, 32, crypto.encodeint(self.b), 0, 32) abt[64] = 0x8C pre = crypto.keccak_hash(abt) # gibberish expansion to 200 bytes, different from Ledger as it uses 200B of keccak state for i in range(6): self._insert(pre) self._insert(pre[:8]) return SW_OK
async def mlsag_prehash_update(self): is_subaddress = 0 Aout = None Bout = None C = None v = None k = None changed = 0 is_subaddress = bool(self._fetch_u8()) Aout = crypto.decodepoint(self._fetch()) Bout = crypto.decodepoint(self._fetch()) if self.sig_mode == TRANSACTION_CREATE_REAL: if crypto.point_eq(Aout, self.A) and crypto.point_eq(Bout, self.B): changed = 1 AKout = self._fetch_decrypt() C = self._fetch() k = self._fetch() v = self._fetch() self.ctx_h.update(k) self.ctx_h.update(v) self.ctx_amount.update(AKout) v, k, AKout = self.unblind_int(v, k, AKout) self.ctx_amount.update(crypto.encodeint(k)) self.ctx_amount.update(crypto.encodeint(v)) vH = crypto.scalarmult_h(v) kG = crypto.scalarmult_base(k) k = crypto.point_add(kG, vH) if not crypto.point_eq(k, crypto.decodepoint(C)): raise ValueError(SW_SECURITY_COMMITMENT_CONTROL) self.ctx_commitment.update(C) if self.options & IN_OPTION_MORE_COMMAND == 0: k = self.ctx_amount.digest() if not common.ct_equal(k, self.KV): raise ValueError(SW_SECURITY_AMOUNT_CHAIN_CONTROL) self.C = self.ctx_commitment.digest() self.ctx_commitment = sha256() if self.sig_mode == TRANSACTION_CREATE_REAL: if not changed: await self._req_dst(Aout, Bout, v, is_subaddress) return SW_OK
def test_encrypt_base(self): for i in range(10): key = crypto.cn_fast_hash(crypto.encodeint(crypto.random_scalar())) data = crypto.cn_fast_hash(crypto.encodeint( crypto.random_scalar())) * (i + 1) ciphertext = chacha.encrypt(key, data) plaintext = chacha.decrypt(key, ciphertext) self.assertEqual(plaintext, data) plaintext2 = chacha.decrypt( key, bytes(int(ciphertext[0]) ^ 0xff) + ciphertext[1:]) self.assertNotEqual(plaintext2, data)
async def mlsag_prepare(self): Hi = None xin = None options = 0 if len(self.c_msg) > 1: options = 1 Hi = crypto.decodepoint(self._fetch()) if self.options & 0x40: xin = crypto.decodeint(self._fetch()) else: xin = crypto.decodeint(self._fetch_decrypt()) alpha = crypto.random_scalar() self._insert_encrypt(crypto.encodeint(alpha)) # ai.G self._insert(crypto.encodepoint(crypto.scalarmult_base(alpha))) if options: # ai * Hi self._insert(crypto.encodepoint(crypto.scalarmult(Hi, alpha))) # xin * Hi self._insert(crypto.encodepoint(crypto.scalarmult(Hi, xin))) return SW_OK
async def derive_secret_key(self): derivation = crypto.decodepoint(self._fetch_decrypt()) output_index = self._fetch_u32() sec = self._fetch_decrypt_key() drvsec = crypto.derive_secret_key(derivation, output_index, sec) self._insert_encrypt(crypto.encodeint(drvsec)) return SW_OK
def set_seed(self, seed, path=None, slip0010=False): """ Sets master secret for BIP44 derivation :param seed: :param path: :param slip0010: :return: """ self.master_seed = seed self.is_slip0010 = slip0010 if path is None: self.path = DEFAULT_BIP44_PATH if not slip0010 else DEFAULT_SLIP0010_PATH else: self.path = path wl = bip32.Wallet.from_master_secret(seed, use_ed25519=slip0010) # Generate private keys based on the gen mechanism. Bip44 path + Monero backward compatible data = wl.get_child_for_path(self.path) self.pre_hash = binascii.unhexlify(data.private_key.get_key()) if slip0010: self.monero_master = crypto.encodeint( crypto.decodeint(self.pre_hash)) else: # Ledger way = words -> bip39 pbkdf -> master seed -> bip32 normal with # "Bitcoin seed" seed, get private key node -> cn_fast_hash -> monero master secret self.monero_master = crypto.cn_fast_hash(self.pre_hash) self.set_monero_seed(self.monero_master)
async def save_account(self, file): """ Stores account data :param file: :return: """ if self.wallet_salt is None: self.wallet_salt = crypto.random_bytes(32) # Wallet view key encryption wallet_enc_key = misc.wallet_enc_key(self.wallet_salt, self.wallet_password) ciphertext = chacha_poly.encrypt_pack(wallet_enc_key, crypto.encodeint(self.priv_view)) with open(file, "w") as fh: data = { "view_key_enc": binascii.hexlify(ciphertext).decode("ascii"), "address": self.address.decode("ascii"), "network_type": self.network_type, "wallet_salt": binascii.hexlify(self.wallet_salt).decode("ascii"), "rpc_addr": self.rpc_addr, "wallet_file": self.wallet_file, "monero_bin": self.monero_bin, } json.dump(data, fh, indent=2)
def ctest_multiexp(self): scalars = [0, 1, 2, 3, 4, 99] point_base = [0, 2, 4, 7, 12, 18] scalar_sc = [crypto.sc_init(x) for x in scalars] points = [ crypto.scalarmult_base(crypto.sc_init(x)) for x in point_base ] muex = bp.MultiExp( scalars=[crypto.encodeint(x) for x in scalar_sc], point_fnc=lambda i, d: crypto.encodepoint(points[i])) self.assertEqual(len(muex), len(scalars)) res = bp.multiexp(None, muex) res2 = bp.vector_exponent_custom( A=bp.KeyVEval( 3, lambda i, d: crypto.encodepoint_into( crypto.scalarmult_base(crypto.sc_init(point_base[i])), d)), B=bp.KeyVEval( 3, lambda i, d: crypto.encodepoint_into( crypto.scalarmult_base(crypto.sc_init(point_base[3 + i])), d)), a=bp.KeyVEval( 3, lambda i, d: crypto.encodeint_into(crypto.sc_init(scalars[i]), d), ), b=bp.KeyVEval( 3, lambda i, d: crypto.encodeint_into( crypto.sc_init(scalars[i + 3]), d)), ) self.assertEqual(res, res2)
def back_encrypt(self, authenticated=True): for i in range(5): priv_key = crypto.random_scalar() data = crypto.cn_fast_hash(crypto.encodeint( crypto.random_scalar())) * (i + 1) blob = chacha.encrypt_xmr(priv_key, data, authenticated=authenticated) plaintext = chacha.decrypt_xmr(priv_key, blob, authenticated=authenticated) self.assertEqual(data, plaintext) try: plaintext2 = chacha.decrypt_xmr( crypto.sc_add(priv_key, crypto.sc_init(1)), blob, authenticated=authenticated, ) if authenticated: self.fail("Signature error expected") else: self.assertNotEqual(data, plaintext2) except: if not authenticated: raise
def poc2(self): print('[+] PoC Ledger-app-Monero 1.4.2 spend key extraction, v2') self.reset() self.set_mode() self.open_tx() # 1. get A, find x, s.t.: [8*a*x*G]_pt == [8*a*x*G]_sc A = self.scalarmult(self.fake_a) Apt = crypto.decodepoint(A) x, A8x = self.find_confusion(Apt) Gx = crypto.encodepoint(crypto.scalarmult_base(crypto.sc_init(x))) print(' 1. Confusion found, x: %d' % (x, )) print(' 8xA: %s' % binascii.hexlify(A8x).decode('ascii')) print(' A: %s' % binascii.hexlify(A).decode('ascii')) print(' xG: %s' % binascii.hexlify(Gx).decode('ascii')) # 2. gen_deriv (8*a*x*G) = enc(8x*A) = enc(P); we know {P, enc(P)}; # It holds that P=8xA is also a valid scalar value, from the step above. P = self.gen_derivation(Gx, self.fake_a) print(' 2. P: %s' % (self.fmtkey(P).decode('ascii'), )) # 3. get_secret_key: s1 = Hs(P||0) + s sp = self.derive_secret_key(P, 0, self.fake_b) print(' 3. sp: %s' % (self.fmtkey(sp).decode('ascii'), )) # 4. mlsag_hash(p2=1, opt=0x80) = c c = self.mlsag_hash() print(' 4. c: %s' % (binascii.hexlify(c).decode('ascii'), )) # 5. mlsag_sign(s1, enc(P)), r1 = enc(s1 - Pc) = enc(Hs(P||0) + s - Pc); # We have R = Hs(P||0) + s - Pc -> R - Hs(P||0) + Pc = s r = self.mlsag_sign_s(P, sp) print(' 5. r: %s' % (binascii.hexlify(r[0]).decode('ascii'), )) # Extract the spend key hs0 = crypto.hash_to_scalar(bytearray(A8x) + bytearray(1)) rsc = crypto.decodeint(r[0]) rsc = crypto.sc_sub(rsc, hs0) bsc = crypto.sc_add( rsc, crypto.sc_mul(crypto.decodeint(c), crypto.decodeint(A8x))) b = crypto.encodeint(bsc) print(' 5. b: %s' % binascii.hexlify(b).decode('ascii')) B = crypto.scalarmult_base(bsc) print(' 5. B: %s' % binascii.hexlify(crypto.encodepoint(B)).decode('ascii')) # 6. Verify BB = self.scalarmult(self.fake_b) print(' 6. bG: %s\n' % binascii.hexlify(BB).decode('ascii')) if BB == crypto.encodepoint(B): print('[+] PoC successful') else: print('[-] PoC not working') print('\nCommands: ') for x in self.commands: print(' %s' % x)
def extra_poc(self, zero, hs0, B): # --- Extra --- # Extract view key and address reconstruction. # Not needed for the PoC print('\nExtracting view-key...') encr = self.derive_secret_key(zero, 0, self.fake_a) r = self.mlsag_sign_s(zero, encr) rsc = crypto.decodeint(r[0]) asc = crypto.sc_sub(rsc, hs0) a = crypto.encodeint(asc) print(' a: %s' % binascii.hexlify(a).decode('ascii')) A = crypto.scalarmult_base(asc) print(' A: %s' % binascii.hexlify(crypto.encodepoint(A)).decode('ascii')) AA = self.scalarmult(self.fake_a) print(' aG: %s' % binascii.hexlify(AA).decode('ascii')) main_addr = addr.encode_addr( xmr_net.net_version(xmr_net.NetworkTypes.MAINNET), crypto.encodepoint(B), crypto.encodepoint(A), ) test_addr = addr.encode_addr( xmr_net.net_version(xmr_net.NetworkTypes.TESTNET), crypto.encodepoint(B), crypto.encodepoint(A), ) print('Mainnet address: %s' % main_addr.decode('ascii')) print('Testnet address: %s' % test_addr.decode('ascii'))
async def on_watch_only(self, request=None): """ Exports watch only credentials :param request: :return: """ if not self.creds: logger.warning( "Agent asks for watch-only credentials, Trezor not initialized" ) return abort(406) if self.watch_only_waiter.in_confirmation: logger.warning( "Agent asks for watch-only credentials concurrently") return abort(406) # Prompt user to confirm. self.on_watchonly() confirmed = self.watch_only_waiter.wait_confirmation() if not confirmed: logger.warning("Watch only rejected") return abort(403) logger.info("Returning watch only credentials...") res = MoneroWatchKey( watch_key=crypto.encodeint(self.creds.view_key_private), address=self.creds.address, ) return jsonify({"result": True, "payload": await self.proto_res(res)})
def ecdh_encdec(masked, receiver_sk=None, derivation=None, v2=False, enc=True, dest=None): """ Elliptic Curve Diffie-Helman: encodes and decodes the amount b and mask a where C= aG + bH """ rv = xmrtypes.EcdhTuple() if dest is None else dest if derivation is None: derivation = crypto.scalarmult(masked.senderPk, receiver_sk) if v2: amnt = masked.amount rv.mask = monero.commitment_mask(derivation) rv.amount = bytearray(32) crypto.encodeint_into(rv.amount, amnt) crypto.xor8(rv.amount, monero.ecdh_hash(derivation)) rv.amount = crypto.decodeint(rv.amount) return rv else: amount_key_hash_single = crypto.hash_to_scalar(derivation) amount_key_hash_double = crypto.hash_to_scalar( crypto.encodeint(amount_key_hash_single) ) sc_fnc = crypto.sc_add if enc else crypto.sc_sub rv.mask = sc_fnc(masked.mask, amount_key_hash_single) rv.amount = sc_fnc(masked.amount, amount_key_hash_double) return rv
async def test_get_watch(self): if self.test_only_tsx: self.skipTest("Get watch skipped") res = await self.agent.get_watch_only() self.assertIsNotNone(res) self.assertEqual(res.watch_key, crypto.encodeint(self.creds.view_key_private)) self.assertEqual(res.address, self.creds.address)
def unblind_int(self, v, k, AKout): AKout = crypto.hash_to_scalar(AKout) k = crypto.sc_sub(crypto.decodeint(k), AKout) AKout = crypto.hash_to_scalar(crypto.encodeint(AKout)) v = crypto.sc_sub(crypto.decodeint(v), AKout) return v, k, AKout
async def ensure_watch_only(self): """ Ensures watch only wallet for monero exists :return: """ if self.wallet_file is None: return key_file = "%s.keys" % self.wallet_file if os.path.exists(key_file): logger.debug("Watch only wallet key file exists: %s" % key_file) match, addr = False, None try: addr, match = await self.check_existing_wallet_file(key_file) except Exception as e: logger.error("Wallet key file processing exception: %s" % e) if not match: logger.error("Key file address is not correct: %s" % addr) print( "Please, move the file so Agent can create correct key file" ) sys.exit(2) return account_keys = xmrtypes.AccountKeys() key_data = wallet.WalletKeyData() wallet_data = wallet.WalletKeyFile() wallet_data.key_data = key_data wallet_data.watch_only = 1 wallet_data.testnet = self.network_type == monero.NetworkTypes.TESTNET key_data.m_creation_timestamp = int(time.time()) key_data.m_keys = account_keys account_keys.m_account_address = xmrtypes.AccountPublicAddress( m_spend_public_key=crypto.encodepoint(self.pub_spend), m_view_public_key=crypto.encodepoint(self.pub_view), ) account_keys.m_spend_secret_key = crypto.encodeint(crypto.sc_0()) account_keys.m_view_secret_key = crypto.encodeint(self.priv_view) await wallet.save_keys_file(key_file, self.wallet_password, wallet_data) logger.debug("Watch-only wallet keys generated: %s" % key_file)
def main(): """ Entry point :return: """ parser = argparse.ArgumentParser( description="Generate non-deterministic wallet credentials") parser.add_argument( "--testnet", dest="testnet", default=False, action="store_const", const=True, help="Testnet", ) parser.add_argument( "--stagenet", dest="stagenet", default=False, action="store_const", const=True, help="Testnet", ) args = parser.parse_args() network_type = monero.NetworkTypes.MAINNET if args.testnet: network_type = monero.NetworkTypes.TESTNET elif args.stagenet: network_type = monero.NetworkTypes.STAGENET priv_view = crypto.random_scalar() priv_spend = crypto.random_scalar() w = monero.AccountCreds.new_wallet(priv_view, priv_spend, network_type=network_type) print("Address: %s" % w.address.decode("utf8")) print("Private view key: %s" % binascii.hexlify(crypto.encodeint(priv_view)).decode("utf8")) print("Private spend key: %s" % binascii.hexlify(crypto.encodeint(priv_spend)).decode("utf8"))
def test_derivation_to_scalar(self): derivation = unhexlify( b"e720a09f2e3a0bbf4e4ba7ad93653bb296885510121f806acb2a5f9168fafa01" ) scalar = unhexlify( b"25d08763414c379aa9cf989cdcb3cadd36bd5193b500107d6bf5f921f18e470e" ) sc_int = crypto.derivation_to_scalar(crypto.decodepoint(derivation), 0) self.assertEqual(scalar, crypto.encodeint(sc_int))
async def compute_sec_keys(self, tsx_data, tsx_ctr): """ Generate master key H(TsxData || r || c_tsx) :return: """ from monero_glue.xmr.sub.keccak_hasher import get_keccak_writer from monero_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)
async def get_subaddress_secret_key(self): sec = crypto.decodeint(self._fetch_decrypt()) index = self._fetch(8) major, minor = self._idx_parse(index) sub_sec = monero.get_subaddress_secret_key(sec, major=major, minor=minor) self._insert_encrypt(crypto.encodeint(sub_sec)) return SW_OK
async def blind(self): AKout = self._fetch_decrypt(32) k = self._fetch(32) v = self._fetch(32) self.ctx_amount.update(AKout) self.ctx_amount.update(k) self.ctx_amount.update(v) AKout = crypto.hash_to_scalar(AKout) k = crypto.sc_add(crypto.decodeint(k), AKout) AKout = crypto.hash_to_scalar(crypto.encodeint(AKout)) v = crypto.sc_add(crypto.decodeint(v), AKout) self._insert(crypto.encodeint(v)) self._insert(crypto.encodeint(k)) return SW_OK
def find_confusion(self, A, N=10000): """find x, s.t.: [8*x*A]_pt == [8*x*A]_sc""" for i in range(1, N): Ac = crypto.scalarmult(A, crypto.sc_init(i * 8)) Ab = crypto.encodepoint(Ac) red = crypto.encodeint(crypto.decodeint(Ab)) if red == Ab: return i, Ab raise ValueError('Could not find a confusion parameter!')
def encrypt_xmr(priv_key, plaintext, authenticated=True): """ Monero-like authenticated encryption with Chacha20 and EC signature :param priv_key: :param plaintext: :param authenticated: :return: """ key = generate_key(crypto.encodeint(priv_key)) ciphertext = encrypt(key, plaintext) if not authenticated: return ciphertext hash = crypto.cn_fast_hash(ciphertext) c, r, pub = crypto.generate_signature(hash, priv_key) signature = crypto.encodeint(c) + crypto.encodeint(r) return ciphertext + signature
async def open_tx(self): self.ctx_amount = sha256() self.account = self._fetch_u32() self.r = crypto.random_scalar() self.R = crypto.scalarmult_base(self.r) self._insert(crypto.encodepoint(self.R)) self._insert_encrypt(crypto.encodeint(self.r)) return SW_OK
def ecdh_encode_into(dst, unmasked, derivation=None): """ Elliptic Curve Diffie-Helman: encodes and decodes the amount b and mask a where C= aG + bH """ sec1 = crypto.hash_to_scalar(derivation) sec2 = crypto.hash_to_scalar(crypto.encodeint(sec1)) dst.mask = crypto.sc_add(unmasked.mask, sec1) dst.amount = crypto.sc_add(unmasked.amount, sec2) return dst