tmove bolt-04 onion stuff to its own module - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 5a05a92b3d631277318cb015b56f98ee36a084ee DIR parent 9247da5203f2e6f6c9dff85db1a6acaf83f4249a HTML Author: SomberNight <somber.night@protonmail.com> Date: Wed, 25 Jul 2018 18:43:20 +0200 move bolt-04 onion stuff to its own module Diffstat: M electrum/lnbase.py | 2 +- A electrum/lnonion.py | 299 +++++++++++++++++++++++++++++++ M electrum/lnrouter.py | 285 +------------------------------ M electrum/tests/test_lnrouter.py | 7 ++++--- 4 files changed, 305 insertions(+), 288 deletions(-) --- DIR diff --git a/electrum/lnbase.py b/electrum/lnbase.py t@@ -31,7 +31,7 @@ from . import constants from . import transaction from .util import PrintError, bh2u, print_error, bfh, profiler, xor_bytes from .transaction import opcodes, Transaction -from .lnrouter import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error +from .lnonion import new_onion_packet, OnionHopsDataSingle, OnionPerHop, decode_onion_error from .lnaddr import lndecode from .lnhtlc import UpdateAddHtlc, HTLCStateMachine, RevokeAndAck, SettleHtlc DIR diff --git a/electrum/lnonion.py b/electrum/lnonion.py t@@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2018 The Electrum developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import hashlib +import hmac +from collections import namedtuple +from typing import Sequence + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms +from cryptography.hazmat.backends import default_backend + +from . import ecc +from .crypto import sha256 +from .util import bh2u, profiler, xor_bytes, bfh +from .lnutil import get_ecdh + + +NUM_MAX_HOPS_IN_PATH = 20 +HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 +PER_HOP_FULL_SIZE = 65 # HOPS_DATA_SIZE / 20 +NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE +PER_HOP_HMAC_SIZE = 32 + + +class UnsupportedOnionPacketVersion(Exception): pass +class InvalidOnionMac(Exception): pass + + +class OnionPerHop: + + def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes): + self.short_channel_id = short_channel_id + self.amt_to_forward = amt_to_forward + self.outgoing_cltv_value = outgoing_cltv_value + + def to_bytes(self) -> bytes: + ret = self.short_channel_id + ret += self.amt_to_forward + ret += self.outgoing_cltv_value + ret += bytes(12) # padding + if len(ret) != 32: + raise Exception('unexpected length {}'.format(len(ret))) + return ret + + @classmethod + def from_bytes(cls, b: bytes): + if len(b) != 32: + raise Exception('unexpected length {}'.format(len(b))) + return OnionPerHop( + short_channel_id=b[:8], + amt_to_forward=b[8:16], + outgoing_cltv_value=b[16:20] + ) + + +class OnionHopsDataSingle: # called HopData in lnd + + def __init__(self, per_hop: OnionPerHop = None): + self.realm = 0 + self.per_hop = per_hop + self.hmac = None + + def to_bytes(self) -> bytes: + ret = bytes([self.realm]) + ret += self.per_hop.to_bytes() + ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE) + if len(ret) != PER_HOP_FULL_SIZE: + raise Exception('unexpected length {}'.format(len(ret))) + return ret + + @classmethod + def from_bytes(cls, b: bytes): + if len(b) != PER_HOP_FULL_SIZE: + raise Exception('unexpected length {}'.format(len(b))) + ret = OnionHopsDataSingle() + ret.realm = b[0] + if ret.realm != 0: + raise Exception('only realm 0 is supported') + ret.per_hop = OnionPerHop.from_bytes(b[1:33]) + ret.hmac = b[33:] + return ret + + +class OnionPacket: + + def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes): + self.version = 0 + self.public_key = public_key + self.hops_data = hops_data # also called RoutingInfo in bolt-04 + self.hmac = hmac + + def to_bytes(self) -> bytes: + ret = bytes([self.version]) + ret += self.public_key + ret += self.hops_data + ret += self.hmac + if len(ret) != 1366: + raise Exception('unexpected length {}'.format(len(ret))) + return ret + + @classmethod + def from_bytes(cls, b: bytes): + if len(b) != 1366: + raise Exception('unexpected length {}'.format(len(b))) + version = b[0] + if version != 0: + raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version)) + return OnionPacket( + public_key=b[1:34], + hops_data=b[34:1334], + hmac=b[1334:] + ) + + +def get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes: + if key_type not in (b'rho', b'mu', b'um', b'ammag'): + raise Exception('invalid key_type {}'.format(key_type)) + key = hmac.new(key_type, msg=secret, digestmod=hashlib.sha256).digest() + return key + + +def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes], + session_key: bytes) -> Sequence[bytes]: + num_hops = len(payment_path_pubkeys) + hop_shared_secrets = num_hops * [b''] + ephemeral_key = session_key + # compute shared key for each hop + for i in range(0, num_hops): + hop_shared_secrets[i] = get_ecdh(ephemeral_key, payment_path_pubkeys[i]) + ephemeral_pubkey = ecc.ECPrivkey(ephemeral_key).get_public_key_bytes() + blinding_factor = sha256(ephemeral_pubkey + hop_shared_secrets[i]) + blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big") + ephemeral_key_int = int.from_bytes(ephemeral_key, byteorder="big") + ephemeral_key_int = ephemeral_key_int * blinding_factor_int % ecc.CURVE_ORDER + ephemeral_key = ephemeral_key_int.to_bytes(32, byteorder="big") + return hop_shared_secrets + + +def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes, + hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket: + num_hops = len(payment_path_pubkeys) + hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key) + + filler = generate_filler(b'rho', num_hops, PER_HOP_FULL_SIZE, hop_shared_secrets) + mix_header = bytes(HOPS_DATA_SIZE) + next_hmac = bytes(PER_HOP_HMAC_SIZE) + + # compute routing info and MAC for each hop + for i in range(num_hops-1, -1, -1): + rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i]) + mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i]) + hops_data[i].hmac = next_hmac + stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) + mix_header = mix_header[:-PER_HOP_FULL_SIZE] + mix_header = hops_data[i].to_bytes() + mix_header + mix_header = xor_bytes(mix_header, stream_bytes) + if i == num_hops - 1 and len(filler) != 0: + mix_header = mix_header[:-len(filler)] + filler + packet = mix_header + associated_data + next_hmac = hmac.new(mu_key, msg=packet, digestmod=hashlib.sha256).digest() + + return OnionPacket( + public_key=ecc.ECPrivkey(session_key).get_public_key_bytes(), + hops_data=mix_header, + hmac=next_hmac) + + +def generate_filler(key_type: bytes, num_hops: int, hop_size: int, + shared_secrets: Sequence[bytes]) -> bytes: + filler_size = (NUM_MAX_HOPS_IN_PATH + 1) * hop_size + filler = bytearray(filler_size) + + for i in range(0, num_hops-1): # -1, as last hop does not obfuscate + filler = filler[hop_size:] + filler += bytearray(hop_size) + stream_key = get_bolt04_onion_key(key_type, shared_secrets[i]) + stream_bytes = generate_cipher_stream(stream_key, filler_size) + filler = xor_bytes(filler, stream_bytes) + + return filler[(NUM_MAX_HOPS_IN_PATH-num_hops+2)*hop_size:] + + +def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes: + algo = algorithms.ChaCha20(stream_key, nonce=bytes(16)) + cipher = Cipher(algo, mode=None, backend=default_backend()) + encryptor = cipher.encryptor() + return encryptor.update(bytes(num_bytes)) + + +ProcessedOnionPacket = namedtuple("ProcessedOnionPacket", ["are_we_final", "hop_data", "next_packet"]) + + +# TODO replay protection +def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes, + our_onion_private_key: bytes) -> ProcessedOnionPacket: + shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key) + + # check message integrity + mu_key = get_bolt04_onion_key(b'mu', shared_secret) + calculated_mac = hmac.new(mu_key, msg=onion_packet.hops_data+associated_data, + digestmod=hashlib.sha256).digest() + if onion_packet.hmac != calculated_mac: + raise InvalidOnionMac() + + # peel an onion layer off + rho_key = get_bolt04_onion_key(b'rho', shared_secret) + stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) + padded_header = onion_packet.hops_data + bytes(PER_HOP_FULL_SIZE) + next_hops_data = xor_bytes(padded_header, stream_bytes) + + # calc next ephemeral key + blinding_factor = sha256(onion_packet.public_key + shared_secret) + blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big") + next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int + next_public_key = next_public_key_int.get_public_key_bytes() + + hop_data = OnionHopsDataSingle.from_bytes(next_hops_data[:PER_HOP_FULL_SIZE]) + next_onion_packet = OnionPacket( + public_key=next_public_key, + hops_data=next_hops_data[PER_HOP_FULL_SIZE:], + hmac=hop_data.hmac + ) + if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE): + # we are the destination / exit node + are_we_final = True + else: + # we are an intermediate node; forwarding + are_we_final = False + return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet) + + +class FailedToDecodeOnionError(Exception): pass + + +class OnionRoutingFailureMessage: + + def __init__(self, code: int, data: bytes): + self.code = code + self.data = data + + def __repr__(self): + return repr((self.code, self.data)) + + +def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes], + session_key: bytes) -> (bytes, int): + """Returns the decoded error bytes, and the index of the sender of the error.""" + num_hops = len(payment_path_pubkeys) + hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key) + for i in range(num_hops): + ammag_key = get_bolt04_onion_key(b'ammag', hop_shared_secrets[i]) + um_key = get_bolt04_onion_key(b'um', hop_shared_secrets[i]) + stream_bytes = generate_cipher_stream(ammag_key, len(error_packet)) + error_packet = xor_bytes(error_packet, stream_bytes) + hmac_computed = hmac.new(um_key, msg=error_packet[32:], digestmod=hashlib.sha256).digest() + hmac_found = error_packet[:32] + if hmac_computed == hmac_found: + return error_packet, i + raise FailedToDecodeOnionError() + + +def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes], + session_key: bytes) -> (OnionRoutingFailureMessage, int): + """Returns the failure message, and the index of the sender of the error.""" + decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key) + failure_msg = get_failure_msg_from_onion_error(decrypted_error) + return failure_msg, sender_index + + +def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailureMessage: + # get failure_msg bytes from error packet + failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big') + failure_msg = decrypted_error_packet[34:34+failure_len] + # create failure message object + failure_code = int.from_bytes(failure_msg[:2], byteorder='big') + failure_data = failure_msg[2:] + return OnionRoutingFailureMessage(failure_code, failure_data) DIR diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py t@@ -23,29 +23,16 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. - import queue -import traceback -import sys -import binascii -import hashlib -import hmac import os import json import threading from collections import namedtuple, defaultdict from typing import Sequence, Union, Tuple, Optional -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms -from cryptography.hazmat.backends import default_backend -from . import bitcoin -from . import ecc -from . import crypto from . import constants -from .crypto import sha256 -from .util import PrintError, bh2u, profiler, xor_bytes, get_headers_dir, bfh -from .lnutil import get_ecdh +from .util import PrintError, bh2u, profiler, get_headers_dir, bfh from .storage import JsonDB from .lnchanannverifier import LNChanAnnVerifier, verify_sig_for_channel_update t@@ -424,273 +411,3 @@ class LNPathFinder(PrintError): route.append(RouteEdge(node_id, short_channel_id, channel_policy)) prev_node_id = node_id return route - - -# bolt 04, "onion" -----> - -NUM_MAX_HOPS_IN_PATH = 20 -HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04 -PER_HOP_FULL_SIZE = 65 # HOPS_DATA_SIZE / 20 -NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE -PER_HOP_HMAC_SIZE = 32 - - -class UnsupportedOnionPacketVersion(Exception): pass -class InvalidOnionMac(Exception): pass - - -class OnionPerHop: - - def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes): - self.short_channel_id = short_channel_id - self.amt_to_forward = amt_to_forward - self.outgoing_cltv_value = outgoing_cltv_value - - def to_bytes(self) -> bytes: - ret = self.short_channel_id - ret += self.amt_to_forward - ret += self.outgoing_cltv_value - ret += bytes(12) # padding - if len(ret) != 32: - raise Exception('unexpected length {}'.format(len(ret))) - return ret - - @classmethod - def from_bytes(cls, b: bytes): - if len(b) != 32: - raise Exception('unexpected length {}'.format(len(b))) - return OnionPerHop( - short_channel_id=b[:8], - amt_to_forward=b[8:16], - outgoing_cltv_value=b[16:20] - ) - - -class OnionHopsDataSingle: # called HopData in lnd - - def __init__(self, per_hop: OnionPerHop = None): - self.realm = 0 - self.per_hop = per_hop - self.hmac = None - - def to_bytes(self) -> bytes: - ret = bytes([self.realm]) - ret += self.per_hop.to_bytes() - ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE) - if len(ret) != PER_HOP_FULL_SIZE: - raise Exception('unexpected length {}'.format(len(ret))) - return ret - - @classmethod - def from_bytes(cls, b: bytes): - if len(b) != PER_HOP_FULL_SIZE: - raise Exception('unexpected length {}'.format(len(b))) - ret = OnionHopsDataSingle() - ret.realm = b[0] - if ret.realm != 0: - raise Exception('only realm 0 is supported') - ret.per_hop = OnionPerHop.from_bytes(b[1:33]) - ret.hmac = b[33:] - return ret - - -class OnionPacket: - - def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes): - self.version = 0 - self.public_key = public_key - self.hops_data = hops_data # also called RoutingInfo in bolt-04 - self.hmac = hmac - - def to_bytes(self) -> bytes: - ret = bytes([self.version]) - ret += self.public_key - ret += self.hops_data - ret += self.hmac - if len(ret) != 1366: - raise Exception('unexpected length {}'.format(len(ret))) - return ret - - @classmethod - def from_bytes(cls, b: bytes): - if len(b) != 1366: - raise Exception('unexpected length {}'.format(len(b))) - version = b[0] - if version != 0: - raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version)) - return OnionPacket( - public_key=b[1:34], - hops_data=b[34:1334], - hmac=b[1334:] - ) - - -def get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes: - if key_type not in (b'rho', b'mu', b'um', b'ammag'): - raise Exception('invalid key_type {}'.format(key_type)) - key = hmac.new(key_type, msg=secret, digestmod=hashlib.sha256).digest() - return key - - -def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes], - session_key: bytes) -> Sequence[bytes]: - num_hops = len(payment_path_pubkeys) - hop_shared_secrets = num_hops * [b''] - ephemeral_key = session_key - # compute shared key for each hop - for i in range(0, num_hops): - hop_shared_secrets[i] = get_ecdh(ephemeral_key, payment_path_pubkeys[i]) - ephemeral_pubkey = ecc.ECPrivkey(ephemeral_key).get_public_key_bytes() - blinding_factor = sha256(ephemeral_pubkey + hop_shared_secrets[i]) - blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big") - ephemeral_key_int = int.from_bytes(ephemeral_key, byteorder="big") - ephemeral_key_int = ephemeral_key_int * blinding_factor_int % ecc.CURVE_ORDER - ephemeral_key = ephemeral_key_int.to_bytes(32, byteorder="big") - return hop_shared_secrets - - -def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes, - hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket: - num_hops = len(payment_path_pubkeys) - hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key) - - filler = generate_filler(b'rho', num_hops, PER_HOP_FULL_SIZE, hop_shared_secrets) - mix_header = bytes(HOPS_DATA_SIZE) - next_hmac = bytes(PER_HOP_HMAC_SIZE) - - # compute routing info and MAC for each hop - for i in range(num_hops-1, -1, -1): - rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i]) - mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i]) - hops_data[i].hmac = next_hmac - stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) - mix_header = mix_header[:-PER_HOP_FULL_SIZE] - mix_header = hops_data[i].to_bytes() + mix_header - mix_header = xor_bytes(mix_header, stream_bytes) - if i == num_hops - 1 and len(filler) != 0: - mix_header = mix_header[:-len(filler)] + filler - packet = mix_header + associated_data - next_hmac = hmac.new(mu_key, msg=packet, digestmod=hashlib.sha256).digest() - - return OnionPacket( - public_key=ecc.ECPrivkey(session_key).get_public_key_bytes(), - hops_data=mix_header, - hmac=next_hmac) - - -def generate_filler(key_type: bytes, num_hops: int, hop_size: int, - shared_secrets: Sequence[bytes]) -> bytes: - filler_size = (NUM_MAX_HOPS_IN_PATH + 1) * hop_size - filler = bytearray(filler_size) - - for i in range(0, num_hops-1): # -1, as last hop does not obfuscate - filler = filler[hop_size:] - filler += bytearray(hop_size) - stream_key = get_bolt04_onion_key(key_type, shared_secrets[i]) - stream_bytes = generate_cipher_stream(stream_key, filler_size) - filler = xor_bytes(filler, stream_bytes) - - return filler[(NUM_MAX_HOPS_IN_PATH-num_hops+2)*hop_size:] - - -def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes: - algo = algorithms.ChaCha20(stream_key, nonce=bytes(16)) - cipher = Cipher(algo, mode=None, backend=default_backend()) - encryptor = cipher.encryptor() - return encryptor.update(bytes(num_bytes)) - - -ProcessedOnionPacket = namedtuple("ProcessedOnionPacket", ["are_we_final", "hop_data", "next_packet"]) - - -# TODO replay protection -def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes, - our_onion_private_key: bytes) -> ProcessedOnionPacket: - shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key) - - # check message integrity - mu_key = get_bolt04_onion_key(b'mu', shared_secret) - calculated_mac = hmac.new(mu_key, msg=onion_packet.hops_data+associated_data, - digestmod=hashlib.sha256).digest() - if onion_packet.hmac != calculated_mac: - raise InvalidOnionMac() - - # peel an onion layer off - rho_key = get_bolt04_onion_key(b'rho', shared_secret) - stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES) - padded_header = onion_packet.hops_data + bytes(PER_HOP_FULL_SIZE) - next_hops_data = xor_bytes(padded_header, stream_bytes) - - # calc next ephemeral key - blinding_factor = sha256(onion_packet.public_key + shared_secret) - blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big") - next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int - next_public_key = next_public_key_int.get_public_key_bytes() - - hop_data = OnionHopsDataSingle.from_bytes(next_hops_data[:PER_HOP_FULL_SIZE]) - next_onion_packet = OnionPacket( - public_key=next_public_key, - hops_data=next_hops_data[PER_HOP_FULL_SIZE:], - hmac=hop_data.hmac - ) - if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE): - # we are the destination / exit node - are_we_final = True - else: - # we are an intermediate node; forwarding - are_we_final = False - return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet) - - -class FailedToDecodeOnionError(Exception): pass - - -class OnionRoutingFailureMessage: - - def __init__(self, code: int, data: bytes): - self.code = code - self.data = data - - def __repr__(self): - return repr((self.code, self.data)) - - -def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes], - session_key: bytes) -> (bytes, int): - """Returns the decoded error bytes, and the index of the sender of the error.""" - num_hops = len(payment_path_pubkeys) - hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key) - for i in range(num_hops): - ammag_key = get_bolt04_onion_key(b'ammag', hop_shared_secrets[i]) - um_key = get_bolt04_onion_key(b'um', hop_shared_secrets[i]) - stream_bytes = generate_cipher_stream(ammag_key, len(error_packet)) - error_packet = xor_bytes(error_packet, stream_bytes) - hmac_computed = hmac.new(um_key, msg=error_packet[32:], digestmod=hashlib.sha256).digest() - hmac_found = error_packet[:32] - if hmac_computed == hmac_found: - return error_packet, i - raise FailedToDecodeOnionError() - - -def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes], - session_key: bytes) -> (OnionRoutingFailureMessage, int): - """Returns the failure message, and the index of the sender of the error.""" - decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key) - failure_msg = get_failure_msg_from_onion_error(decrypted_error) - return failure_msg, sender_index - - -def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailureMessage: - # get failure_msg bytes from error packet - failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big') - failure_msg = decrypted_error_packet[34:34+failure_len] - # create failure message object - failure_code = int.from_bytes(failure_msg[:2], byteorder='big') - failure_data = failure_msg[2:] - return OnionRoutingFailureMessage(failure_code, failure_data) - - - - -# <----- bolt 04, "onion" - DIR diff --git a/electrum/tests/test_lnrouter.py b/electrum/tests/test_lnrouter.py t@@ -4,7 +4,8 @@ import shutil from electrum.util import bh2u, bfh from electrum.lnbase import Peer -from electrum.lnrouter import OnionHopsDataSingle, new_onion_packet, OnionPerHop +from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet, OnionPerHop, + process_onion_packet, _decode_onion_error) from electrum import bitcoin, lnrouter from electrum.simple_config import SimpleConfig from electrum import lnchanannverifier t@@ -164,7 +165,7 @@ class Test_LNRouter(TestCaseForTestnet): parazyd.org:70 /git/electrum/commit/5a05a92b3d631277318cb015b56f98ee36a084ee.gph:652: line too long