module Lightning::Onion::Sphinx

Constants

ERROR_PACKET_LENGTH
HOP_LENGTH
LAST_PACKET
MAC_LENGTH
MAX_ERROR_PAYLOAD_LENGTH
MAX_HOPS
PACKET_LENGTH
PAYLOAD_LENGTH
PUBKEY_LENGTH
VERSION

Public Class Methods

check_mac(secret, payload) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 212
def self.check_mac(secret, payload)
  mac = payload[0...MAC_LENGTH]
  payload1 = payload[MAC_LENGTH..-1]
  um = generate_key('um', secret)
  mac == mac(um, payload1.unpack('C*'))
end
compute_blinding_factor(public_key, secret) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 142
def self.compute_blinding_factor(public_key, secret)
  Bitcoin.sha256(public_key.htb + secret.htb).bth
end
compute_keys_and_secrets(session_key, public_keys) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 82
def self.compute_keys_and_secrets(session_key, public_keys)
  point = ECDSA::Group::Secp256k1.generator
  generator_pubkey = ECDSA::Format::PointOctetString.encode(point, compression: true)
  ephemereal_public_key0 = make_blind(generator_pubkey.bth, session_key)
  secret0 = compute_shared_secret(public_keys[0], session_key)
  blinding_factor0 = compute_blinding_factor(ephemereal_public_key0, secret0)
  internal_compute_keys_and_secrets(
    session_key,
    public_keys[1..-1],
    [ephemereal_public_key0],
    [blinding_factor0],
    [secret0]
  )
end
compute_shared_secret(public_key, secret) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 135
def self.compute_shared_secret(public_key, secret)
  scalar = ECDSA::Format::IntegerOctetString.decode(secret.htb)
  point = Bitcoin::Key.new(pubkey: public_key).to_point.multiply_by_scalar(scalar)
  public_key = ECDSA::Format::PointOctetString.encode(point, compression: true)
  Bitcoin.sha256(public_key).bth
end
extract_failure_message(payload) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 219
def self.extract_failure_message(payload)
  raise "invalid length: #{payload.bytesize}" unless payload.bytesize == ERROR_PACKET_LENGTH
  _mac, len, rest = payload.unpack("a#{MAC_LENGTH}na*")
  FailureMessages.load(rest[0...len])
end
forward_error_packet(payload, shared_secret) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 190
def self.forward_error_packet(payload, shared_secret)
  key = generate_key('ammag', shared_secret)
  stream = generate_cipher_stream(key, ERROR_PACKET_LENGTH)
  xor(payload.unpack('C*'), stream.unpack('C*')).pack('C*')
end
generate_cipher_stream(key, length) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 166
def self.generate_cipher_stream(key, length)
  Lightning::Onion::ChaCha20.chacha20_encrypt(key, 0, "\x00" * 12, "\x00" * length)
end
generate_filler(key_type, shared_secrets, hop_size, max_number_of_hops = MAX_HOPS) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 146
def self.generate_filler(key_type, shared_secrets, hop_size, max_number_of_hops = MAX_HOPS)
  shared_secrets.inject([]) do |padding, secret|
    key = generate_key(key_type, secret)
    padding1 = padding + [0] * hop_size
    stream = generate_cipher_stream(key, hop_size * (max_number_of_hops + 1))
    stream = stream.reverse[0...padding1.size].reverse.unpack('c*')
    new_padding = xor(padding1, stream)
    new_padding
  end.pack('C*').bth
end
generate_key(key_type, secret) click to toggle source

Key generation

# File lib/lightning/onion/sphinx.rb, line 162
def self.generate_key(key_type, secret)
  hmac256(key_type, secret.htb)
end
hmac256(key, message) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 157
def self.hmac256(key, message)
  OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, key, message)
end
internal_compute_keys_and_secrets( session_key, public_keys, ephemereal_public_keys, blinding_factors, shared_secrets ) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 97
def self.internal_compute_keys_and_secrets(
  session_key,
  public_keys,
  ephemereal_public_keys,
  blinding_factors,
  shared_secrets
)
  if public_keys.empty?
    [ephemereal_public_keys, shared_secrets]
  else
    ephemereal_public_key = make_blind(ephemereal_public_keys.last, blinding_factors.last)
    secret = compute_shared_secret(make_blinds(public_keys.first, blinding_factors), session_key)
    blinding_factor = compute_blinding_factor(ephemereal_public_key, secret)
    ephemereal_public_keys << ephemereal_public_key
    blinding_factors << blinding_factor
    shared_secrets << secret
    internal_compute_keys_and_secrets(
      session_key,
      public_keys[1..-1],
      ephemereal_public_keys,
      blinding_factors,
      shared_secrets
    )
  end
end
internal_make_packet(hop_payloads, keys, shared_secrets, packet, associated_data) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 59
def self.internal_make_packet(hop_payloads, keys, shared_secrets, packet, associated_data)
  return packet if hop_payloads.empty?
  next_packet = make_next_packet(hop_payloads.last, associated_data, keys.last, shared_secrets.last, packet)
  internal_make_packet(hop_payloads[0...-1], keys[0...-1], shared_secrets[0...-1], next_packet, associated_data)
end
internal_parse_error(payload, node_shared_secrets) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 201
def self.internal_parse_error(payload, node_shared_secrets)
  raise RuntimeError unless node_shared_secrets
  node_shared_secret = node_shared_secrets.last
  next_payload = forward_error_packet(payload, node_shared_secret[0])
  if check_mac(node_shared_secret[0], next_payload)
    ErrorPacket.new(node_shared_secret[1], extract_failure_message(next_payload))
  else
    internal_parse_error(next_payload, node_shared_secrets[0...-1])
  end
end
mac(key, message) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 174
def self.mac(key, message)
  hmac256(key, message.pack('C*'))[0...MAC_LENGTH]
end
make_blind(public_key, blinding_factor) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 123
def self.make_blind(public_key, blinding_factor)
  point = Bitcoin::Key.new(pubkey: public_key).to_point
  scalar = ECDSA::Format::IntegerOctetString.decode(blinding_factor.htb)
  point = point.multiply_by_scalar(scalar)
  public_key = ECDSA::Format::PointOctetString.encode(point, compression: true)
  public_key.bth
end
make_blinds(public_key, blinding_factors) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 131
def self.make_blinds(public_key, blinding_factors)
  blinding_factors.inject(public_key) { |p, factor| make_blind(p, factor) }
end
make_error_packet(shared_secret, failure) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 178
def self.make_error_packet(shared_secret, failure)
  message = failure.to_payload
  um = generate_key('um', shared_secret)
  padlen = MAX_ERROR_PAYLOAD_LENGTH - message.length
  payload = +''
  payload << [message.length].pack('n')
  payload << message
  payload << [padlen].pack('n')
  payload << "\x00" * padlen
  forward_error_packet(mac(um, payload.unpack('C*')) + payload, shared_secret)
end
make_next_packet(payload, associated_data, ephemereal_public_key, shared_secret, packet, filler = '') click to toggle source
# File lib/lightning/onion/sphinx.rb, line 65
def self.make_next_packet(payload, associated_data, ephemereal_public_key, shared_secret, packet, filler = '')
  hops_data1 = payload.htb << packet.hmac.htb << packet.routing_info.htb[0...-HOP_LENGTH]
  rho_key = generate_key('rho', shared_secret)
  stream = generate_cipher_stream(rho_key, MAX_HOPS * HOP_LENGTH)
  hops_data2 = xor(hops_data1.unpack('C*'), stream.unpack('C*'))
  next_hops_data =
    if filler.empty?
      hops_data2
    else
      hops_data2[0...-filler.htb.unpack('C*').size] + filler.htb.unpack('C*')
    end
  mu_key = generate_key('mu', shared_secret)
  next_hmac = mac(mu_key, next_hops_data + associated_data.htb.unpack('C*'))
  routing_info = next_hops_data.pack('C*').bth
  Lightning::Onion::Packet.new(VERSION, ephemereal_public_key, routing_info, next_hmac.bth)
end
make_packet(session_key, public_keys, payloads, associated_data) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 18
def self.make_packet(session_key, public_keys, payloads, associated_data)
  ephemereal_public_keys, shared_secrets = compute_keys_and_secrets(session_key, public_keys)
  filler = generate_filler('rho', shared_secrets[0...-1], HOP_LENGTH, MAX_HOPS)
  last_packet = make_next_packet(
    payloads.last,
    associated_data,
    ephemereal_public_keys.last,
    shared_secrets.last,
    LAST_PACKET,
    filler
  )
  packet = internal_make_packet(
    payloads[0...-1],
    ephemereal_public_keys[0...-1],
    shared_secrets[0...-1],
    last_packet,
    associated_data
  )
  [packet, shared_secrets.zip(public_keys)]
end
parse(private_key, raw_packet) click to toggle source

@return payload 33bytes payload of the outermost layer of onions,which including realm @return packet @return Shared Secret

# File lib/lightning/onion/sphinx.rb, line 42
def self.parse(private_key, raw_packet)
  packet = Lightning::Onion::Packet.parse(raw_packet)
  shared_secret = compute_shared_secret(packet.public_key, private_key)
  rho = generate_key('rho', shared_secret)
  bin = xor(
    (packet.routing_info + '00' * HOP_LENGTH).htb.unpack('C*'),
    generate_cipher_stream(rho, HOP_LENGTH + MAX_HOPS * HOP_LENGTH).unpack('C*')
  )
  payload = bin[0...HOP_LENGTH].pack('C*')
  hmac = bin[PAYLOAD_LENGTH...HOP_LENGTH].pack('C*')
  next_hops_data = bin[HOP_LENGTH..-1]

  next_public_key = make_blind(packet.public_key, compute_blinding_factor(packet.public_key, shared_secret))
  routing_info = next_hops_data.pack('C*').bth
  [Lightning::Onion::HopData.parse(payload), Lightning::Onion::Packet.new(VERSION, next_public_key, routing_info, hmac.bth), shared_secret]
end
parse_error(payload, node_shared_secrets) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 196
def self.parse_error(payload, node_shared_secrets)
  raise "invalid length: #{payload.htb.bytesize}" unless payload.htb.bytesize == ERROR_PACKET_LENGTH
  internal_parse_error(payload.htb, node_shared_secrets)
end
xor(a, b) click to toggle source
# File lib/lightning/onion/sphinx.rb, line 170
def self.xor(a, b)
  a.zip(b).map { |x, y| ((x ^ y) & 0xff) }
end