Skip to content

Commit

Permalink
Implement Bitcoin::BIP324::Cipher
Browse files Browse the repository at this point in the history
  • Loading branch information
azuchi committed Jan 15, 2024
1 parent fde143d commit 599dedd
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 1 deletion.
3 changes: 3 additions & 0 deletions lib/bitcoin/bip324.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ module Bitcoin
# https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki
module BIP324
autoload :EllSwiftPubkey, 'bitcoin/bip324/ell_swift_pubkey'
autoload :Cipher, 'bitcoin/bip324/cipher'

FIELD_SIZE = 2**256 - 2**32 - 977
FIELD = ECDSA::PrimeField.new(FIELD_SIZE)

REKEY_INTERVAL = 224 # packets

module_function

def sqrt(n)
Expand Down
54 changes: 54 additions & 0 deletions lib/bitcoin/bip324/cipher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Bitcoin
module BIP324
# The BIP324 packet cipher, encapsulating its key derivation, stream cipher, and AEAD.
class Cipher
include Bitcoin::Util

attr_reader :key
attr_reader :our_pubkey

attr_accessor :session_id
attr_accessor :send_garbage_terminator
attr_accessor :recv_garbage_terminator
attr_accessor :send_l_cipher
attr_accessor :send_p_cipher
attr_accessor :recv_l_cipher
attr_accessor :recv_p_cipher

# Constructor
# @param [Bitcoin::Key] key Private key.
# @param [Bitcoin::BIP324::EllSwiftPubkey] our_pubkey Ellswift public key for testing.
def initialize(key, our_pubkey = nil)
raise ArgumentError, "key must be Bitcoin::Key" unless key.is_a?(Bitcoin::Key)
raise ArgumentError, "our_pubkey must be Bitcoin::BIP324::EllSwiftPubkey" if our_pubkey && !our_pubkey.is_a?(Bitcoin::BIP324::EllSwiftPubkey)
@our_pubkey = our_pubkey ? our_pubkey : key.create_ell_pubkey
@key = key
end

# Setup when the other side's public key is received.
# @param [Bitcoin::BIP324::EllSwiftPubkey] their_pubkey
# @param [Boolean] initiator Set true if we are the initiator establishing the v2 P2P connection.
def setup(their_pubkey, initiator)
salt = 'bitcoin_v2_shared_secret' + Bitcoin.chain_params.magic_head.htb
ecdh_secret = BIP324.v2_ecdh(key.priv_key, their_pubkey, our_pubkey, initiator).htb
terminator = hkdf_sha256(ecdh_secret, salt, 'garbage_terminators')
if initiator
self.send_l_cipher = hkdf_sha256(ecdh_secret, salt, 'initiator_L').bth
self.send_p_cipher = hkdf_sha256(ecdh_secret, salt, 'initiator_P').bth
self.recv_l_cipher = hkdf_sha256(ecdh_secret, salt, 'responder_L').bth
self.recv_p_cipher = hkdf_sha256(ecdh_secret, salt, 'responder_P').bth
self.send_garbage_terminator = terminator[0...16].bth
self.recv_garbage_terminator = terminator[16..-1].bth
else
self.recv_l_cipher = hkdf_sha256(ecdh_secret, salt, 'initiator_L').bth
self.recv_p_cipher = hkdf_sha256(ecdh_secret, salt, 'initiator_P').bth
self.send_l_cipher = hkdf_sha256(ecdh_secret, salt, 'responder_L').bth
self.send_p_cipher = hkdf_sha256(ecdh_secret, salt, 'responder_P').bth
self.recv_garbage_terminator = terminator[0...16].bth
self.send_garbage_terminator = terminator[16..-1].bth
end
self.session_id = hkdf_sha256(ecdh_secret, salt, 'session_id').bth
end
end
end
end
1 change: 1 addition & 0 deletions lib/bitcoin/key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ def fully_valid_pubkey?(allow_hybrid = false)
# @return [Bitcoin::BIP324::EllSwiftPubkey]
# @raise ArgumentError If ent32 does not 32 bytes.
def create_ell_pubkey
raise ArgumentError, "private_key required." unless priv_key
if secp256k1_module.is_a?(Bitcoin::Secp256k1::Native)
Bitcoin::BIP324::EllSwiftPubkey.new(secp256k1_module.ellswift_create(priv_key))
else
Expand Down
10 changes: 10 additions & 0 deletions lib/bitcoin/util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ def hmac_sha256(key, data)
Bitcoin.hmac_sha256(key, data)
end

# Run HKDF.
# @param [String] ikm The input keying material with binary format.
# @param [String] salt The salt with binary format.
# @param [String] info The context and application specific information with binary format.
# @param [Integer] length The output length in octets.
# @return [String] The result of HKDF with binary format.
def hkdf_sha256(ikm, salt, info, length = 32)
OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: "SHA256")
end

# check whether +addr+ is valid address.
# @param [String] addr an address
# @return [Boolean] if valid address return true, otherwise false.
Expand Down
18 changes: 17 additions & 1 deletion spec/bitcoin/bip324_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_vectors
end
end

describe "ellswift_xdh" do
describe "ellswift_xdh", network: :mainnet do
context "native", use_secp256k1: true do
it { test_ellswift_xdh }
end
Expand All @@ -57,8 +57,24 @@ def test_ellswift_xdh
expect(our_ell.decode.xonly_pubkey).to eq(v['mid_x_ours'])
their_ell = Bitcoin::BIP324::EllSwiftPubkey.new(v['in_ellswift_theirs'])
expect(their_ell.decode.xonly_pubkey).to eq(v['mid_x_theirs'])
cipher = Bitcoin::BIP324::Cipher.new(our_priv, our_ell)
cipher.setup(their_ell, initiating)
shared_x = described_class.v2_ecdh(our_priv.priv_key, their_ell, our_ell, initiating)
expect(shared_x).to eq(v['mid_shared_secret'])
if initiating
expect(cipher.send_l_cipher).to eq(v['mid_initiator_l'])
expect(cipher.send_p_cipher).to eq(v['mid_initiator_p'])
expect(cipher.recv_l_cipher).to eq(v['mid_responder_l'])
expect(cipher.recv_p_cipher).to eq(v['mid_responder_p'])
else
expect(cipher.recv_l_cipher).to eq(v['mid_initiator_l'])
expect(cipher.recv_p_cipher).to eq(v['mid_initiator_p'])
expect(cipher.send_l_cipher).to eq(v['mid_responder_l'])
expect(cipher.send_p_cipher).to eq(v['mid_responder_p'])
end
expect(cipher.send_garbage_terminator).to eq(v['mid_send_garbage_terminator'])
expect(cipher.recv_garbage_terminator).to eq(v['mid_recv_garbage_terminator'])
expect(cipher.session_id).to eq(v['out_session_id'])
end
end
end
Expand Down

0 comments on commit 599dedd

Please sign in to comment.