Module: Lightning::Onion::Sphinx

Defined in:
lib/lightning/onion/sphinx.rb

Constant Summary collapse

VERSION =
"\x00"
PUBKEY_LENGTH =
33
PAYLOAD_LENGTH =
33
MAC_LENGTH =
32
MAX_HOPS =
20
HOP_LENGTH =
PAYLOAD_LENGTH + MAC_LENGTH
MAX_ERROR_PAYLOAD_LENGTH =
256
ERROR_PACKET_LENGTH =
MAC_LENGTH + MAX_ERROR_PAYLOAD_LENGTH + 2 + 2
PACKET_LENGTH =
VERSION.bytesize + PUBKEY_LENGTH + MAX_HOPS * HOP_LENGTH + MAC_LENGTH
LAST_PACKET =
Lightning::Onion::Packet.new(VERSION, "\x00" * PUBKEY_LENGTH, '00' * MAX_HOPS * HOP_LENGTH, '00' * MAC_LENGTH)

Class Method Summary collapse

Class Method Details

.check_mac(secret, payload) ⇒ Object



212
213
214
215
216
217
# 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) ⇒ Object



142
143
144
# 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) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 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) ⇒ Object



135
136
137
138
139
140
# 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) ⇒ Object



219
220
221
222
223
# 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) ⇒ Object



190
191
192
193
194
# 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) ⇒ Object



166
167
168
# 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) ⇒ Object



146
147
148
149
150
151
152
153
154
155
# 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) ⇒ Object

Key generation



162
163
164
# File 'lib/lightning/onion/sphinx.rb', line 162

def self.generate_key(key_type, secret)
  hmac256(key_type, secret.htb)
end

.hmac256(key, message) ⇒ Object



157
158
159
# 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) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# 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) ⇒ Object



59
60
61
62
63
# 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) ⇒ Object

Raises:

  • (RuntimeError)


201
202
203
204
205
206
207
208
209
210
# 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) ⇒ Object



174
175
176
# 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) ⇒ Object



123
124
125
126
127
128
129
# 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) ⇒ Object



131
132
133
# 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) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
# 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 = '') ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# 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) ⇒ Object



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 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) ⇒ Object

Returns:

  • payload 33bytes payload of the outermost layer of onions,which including realm

  • packet

  • Shared Secret



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# 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) ⇒ Object



196
197
198
199
# 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) ⇒ Object



170
171
172
# File 'lib/lightning/onion/sphinx.rb', line 170

def self.xor(a, b)
  a.zip(b).map { |x, y| ((x ^ y) & 0xff) }
end