bitcoinrb 1.4.0 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.ruby-version +1 -1
- data/README.md +9 -0
- data/bitcoinrb.gemspec +2 -2
- data/lib/bitcoin/bip324/cipher.rb +113 -0
- data/lib/bitcoin/bip324/ell_swift_pubkey.rb +42 -0
- data/lib/bitcoin/bip324/fs_chacha20.rb +132 -0
- data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +129 -0
- data/lib/bitcoin/bip324.rb +144 -0
- data/lib/bitcoin/ext/ecdsa.rb +0 -6
- data/lib/bitcoin/key.rb +15 -3
- data/lib/bitcoin/message_sign.rb +13 -8
- data/lib/bitcoin/secp256k1/native.rb +61 -5
- data/lib/bitcoin/secp256k1/ruby.rb +21 -4
- data/lib/bitcoin/util.rb +11 -3
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin.rb +1 -0
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 900806837ee6cd6b011a50d6a58c37ea233079e8b86a594c6a5031c338f9a490
|
4
|
+
data.tar.gz: 34b437fee384d630d9b7f1a47faa8175692d9ebe21408d4720828140461b152c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 87bcfdb59403593475f90b49f9f67299bee3bfd8cfae7089080e95ab8e1ffddcdc21ebbb72f8ca49f1caea9d8df2ea5ca613eb97b698ddab3fb3d3c617611165
|
7
|
+
data.tar.gz: 55dcbb9ce3f10a5dd822e98605eb00fa74f3034a8852cdcd714b4dff5cd347cbdc55bffd49ae2f74cc4aeaad2bf86d01d635e7146207ccb6557f55ae0849ff0e
|
data/.github/workflows/ruby.yml
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
ruby-3.
|
1
|
+
ruby-3.3.0
|
data/README.md
CHANGED
@@ -105,6 +105,15 @@ Bitcoin.chain_params = :signet
|
|
105
105
|
|
106
106
|
This parameter is described in https://github.com/chaintope/bitcoinrb/blob/master/lib/bitcoin/chainparams/signet.yml.
|
107
107
|
|
108
|
+
## Test
|
109
|
+
|
110
|
+
This library can use the [libsecp256k1](https://github.com/bitcoin-core/secp256k1/) dynamic library.
|
111
|
+
Therefore, some tests require this library. In a Linux environment, `spec/lib/libsecp256k1.so` is already located,
|
112
|
+
so there is no need to do anything. If you want to test in another environment,
|
113
|
+
please set the library path in the environment variable `TEST_LIBSECP256K1_PATH`.
|
114
|
+
|
115
|
+
The libsecp256k1 library currently tested for operation with this library is `v0.4.0`.
|
116
|
+
|
108
117
|
## Contributing
|
109
118
|
|
110
119
|
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/bitcoinrb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
data/bitcoinrb.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
-
spec.add_runtime_dependency 'ecdsa_ext', '~> 0.5.
|
23
|
+
spec.add_runtime_dependency 'ecdsa_ext', '~> 0.5.1'
|
24
24
|
spec.add_runtime_dependency 'eventmachine'
|
25
25
|
spec.add_runtime_dependency 'murmurhash3', '~> 0.1.7'
|
26
26
|
spec.add_runtime_dependency 'bech32', '>= 1.3.0'
|
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.add_runtime_dependency 'iniparse'
|
33
33
|
spec.add_runtime_dependency 'siphash'
|
34
34
|
spec.add_runtime_dependency 'json_pure', '>= 2.3.1'
|
35
|
-
spec.add_runtime_dependency 'bip-schnorr', '>= 0.
|
35
|
+
spec.add_runtime_dependency 'bip-schnorr', '>= 0.7.0'
|
36
36
|
spec.add_runtime_dependency 'base32', '>= 0.3.4'
|
37
37
|
|
38
38
|
# for options
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module BIP324
|
3
|
+
# The BIP324 packet cipher, encapsulating its key derivation, stream cipher, and AEAD.
|
4
|
+
class Cipher
|
5
|
+
include Bitcoin::Util
|
6
|
+
|
7
|
+
HEADER = [1 << 7].pack('C')
|
8
|
+
HEADER_LEN = 1
|
9
|
+
LENGTH_LEN = 3
|
10
|
+
EXPANSION = LENGTH_LEN + HEADER_LEN + 16
|
11
|
+
|
12
|
+
attr_reader :key
|
13
|
+
attr_reader :our_pubkey
|
14
|
+
|
15
|
+
attr_accessor :session_id
|
16
|
+
attr_accessor :send_garbage_terminator
|
17
|
+
attr_accessor :recv_garbage_terminator
|
18
|
+
attr_accessor :send_l_cipher
|
19
|
+
attr_accessor :send_p_cipher
|
20
|
+
attr_accessor :recv_l_cipher
|
21
|
+
attr_accessor :recv_p_cipher
|
22
|
+
|
23
|
+
# Constructor
|
24
|
+
# @param [Bitcoin::Key] key Private key.
|
25
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] our_pubkey Ellswift public key for testing.
|
26
|
+
# @raise ArgumentError
|
27
|
+
def initialize(key, our_pubkey = nil)
|
28
|
+
raise ArgumentError, "key must be Bitcoin::Key" unless key.is_a?(Bitcoin::Key)
|
29
|
+
raise ArgumentError, "our_pubkey must be Bitcoin::BIP324::EllSwiftPubkey" if our_pubkey && !our_pubkey.is_a?(Bitcoin::BIP324::EllSwiftPubkey)
|
30
|
+
@our_pubkey = our_pubkey ? our_pubkey : key.create_ell_pubkey
|
31
|
+
@key = key
|
32
|
+
end
|
33
|
+
|
34
|
+
# Setup when the other side's public key is received.
|
35
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] their_pubkey
|
36
|
+
# @param [Boolean] initiator Set true if we are the initiator establishing the v2 P2P connection.
|
37
|
+
# @param [Boolean] self_decrypt only for testing, and swaps encryption/decryption keys, so that encryption
|
38
|
+
# and decryption can be tested without knowing the other side's private key.
|
39
|
+
def setup(their_pubkey, initiator, self_decrypt = false)
|
40
|
+
salt = 'bitcoin_v2_shared_secret' + Bitcoin.chain_params.magic_head.htb
|
41
|
+
ecdh_secret = BIP324.v2_ecdh(key.priv_key, their_pubkey, our_pubkey, initiator).htb
|
42
|
+
terminator = hkdf_sha256(ecdh_secret, salt, 'garbage_terminators')
|
43
|
+
side = initiator != self_decrypt
|
44
|
+
if side
|
45
|
+
self.send_l_cipher = FSChaCha20.new(hkdf_sha256(ecdh_secret, salt, 'initiator_L'))
|
46
|
+
self.send_p_cipher = FSChaCha20Poly1305.new(hkdf_sha256(ecdh_secret, salt, 'initiator_P'))
|
47
|
+
self.recv_l_cipher = FSChaCha20.new(hkdf_sha256(ecdh_secret, salt, 'responder_L'))
|
48
|
+
self.recv_p_cipher = FSChaCha20Poly1305.new(hkdf_sha256(ecdh_secret, salt, 'responder_P'))
|
49
|
+
else
|
50
|
+
self.recv_l_cipher = FSChaCha20.new(hkdf_sha256(ecdh_secret, salt, 'initiator_L'))
|
51
|
+
self.recv_p_cipher = FSChaCha20Poly1305.new(hkdf_sha256(ecdh_secret, salt, 'initiator_P'))
|
52
|
+
self.send_l_cipher = FSChaCha20.new(hkdf_sha256(ecdh_secret, salt, 'responder_L'))
|
53
|
+
self.send_p_cipher = FSChaCha20Poly1305.new(hkdf_sha256(ecdh_secret, salt, 'responder_P'))
|
54
|
+
end
|
55
|
+
if initiator
|
56
|
+
self.send_garbage_terminator = terminator[0...16].bth
|
57
|
+
self.recv_garbage_terminator = terminator[16..-1].bth
|
58
|
+
else
|
59
|
+
self.recv_garbage_terminator = terminator[0...16].bth
|
60
|
+
self.send_garbage_terminator = terminator[16..-1].bth
|
61
|
+
end
|
62
|
+
self.session_id = hkdf_sha256(ecdh_secret, salt, 'session_id').bth
|
63
|
+
end
|
64
|
+
|
65
|
+
# Encrypt a packet. Only after setup.
|
66
|
+
# @param [String] contents Packet with binary format.
|
67
|
+
# @param [String] aad AAD
|
68
|
+
# @param [Boolean] ignore Whether contains ignore bit or not.
|
69
|
+
# @raise Bitcoin::BIP324::TooLargeContent
|
70
|
+
def encrypt(contents, aad: '', ignore: false)
|
71
|
+
raise Bitcoin::BIP324::TooLargeContent unless contents.bytesize <= (2**24 - 1)
|
72
|
+
|
73
|
+
# encrypt length
|
74
|
+
len = Array.new(3)
|
75
|
+
len[0] = contents.bytesize & 0xff
|
76
|
+
len[1] = (contents.bytesize >> 8) & 0xff
|
77
|
+
len[2] = (contents.bytesize >> 16) & 0xff
|
78
|
+
enc_plaintext_len = send_l_cipher.encrypt(len.pack('C*'))
|
79
|
+
|
80
|
+
# encrypt contents
|
81
|
+
header = ignore ? HEADER : "00".htb
|
82
|
+
plaintext = header + contents
|
83
|
+
aead_ciphertext = send_p_cipher.encrypt(aad, plaintext)
|
84
|
+
enc_plaintext_len + aead_ciphertext
|
85
|
+
end
|
86
|
+
|
87
|
+
# Decrypt a packet. Only after setup.
|
88
|
+
# @param [String] input Packet to be decrypt.
|
89
|
+
# @param [String] aad AAD
|
90
|
+
# @param [Boolean] ignore Whether contains ignore bit or not.
|
91
|
+
# @return [String] Plaintext
|
92
|
+
# @raise Bitcoin::BIP324::InvalidPaketLength
|
93
|
+
def decrypt(input, aad: '', ignore: false)
|
94
|
+
len = decrypt_length(input[0...Bitcoin::BIP324::Cipher::LENGTH_LEN])
|
95
|
+
raise Bitcoin::BIP324::InvalidPaketLength unless input.bytesize == len + EXPANSION
|
96
|
+
recv_p_cipher.decrypt(aad, input[Bitcoin::BIP324::Cipher::LENGTH_LEN..-1])
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Decrypt the length of a packet. Only after setup.
|
102
|
+
# @param [String] input Length packet with binary format.
|
103
|
+
# @return [Integer] length
|
104
|
+
# @raise Bitcoin::BIP324::InvalidPaketLength
|
105
|
+
def decrypt_length(input)
|
106
|
+
raise Bitcoin::BIP324::InvalidPaketLength unless input.bytesize == LENGTH_LEN
|
107
|
+
ret = recv_l_cipher.decrypt(input)
|
108
|
+
b0, b1, b2 = ret.unpack('CCC')
|
109
|
+
b0 + (b1 << 8) + (b2 << 16)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module BIP324
|
3
|
+
# An ElligatorSwift-encoded public key.
|
4
|
+
class EllSwiftPubkey
|
5
|
+
include Schnorr::Util
|
6
|
+
|
7
|
+
SIZE = 64
|
8
|
+
|
9
|
+
attr_reader :key
|
10
|
+
|
11
|
+
# Constructor
|
12
|
+
# @param [String] key 64 bytes of key data.
|
13
|
+
# @raise Bitcoin::BIP324::InvalidEllSwiftKey If key is invalid.
|
14
|
+
def initialize(key)
|
15
|
+
@key = hex2bin(key)
|
16
|
+
raise Bitcoin::BIP324::InvalidEllSwiftKey, 'key must be 64 bytes.' unless @key.bytesize == SIZE
|
17
|
+
end
|
18
|
+
|
19
|
+
# Decode to public key.
|
20
|
+
# @return [Bitcoin::Key] Decoded public key.
|
21
|
+
def decode
|
22
|
+
if Bitcoin.secp_impl.is_a?(Bitcoin::Secp256k1::Native)
|
23
|
+
pubkey = Bitcoin.secp_impl.ellswift_decode(key)
|
24
|
+
Bitcoin::Key.new(pubkey: pubkey, key_type: Bitcoin::Key::TYPES[:compressed])
|
25
|
+
else
|
26
|
+
u = key[0...32].bth
|
27
|
+
t = key[32..-1].bth
|
28
|
+
x = BIP324.xswiftec(u, t)
|
29
|
+
Bitcoin::Key.new(pubkey: "03#{x}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check whether same public key or not?
|
34
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] other
|
35
|
+
# @return [Boolean]
|
36
|
+
def ==(other)
|
37
|
+
return false unless other.is_a?(EllSwiftPubkey)
|
38
|
+
key == other.key
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module BIP324
|
3
|
+
|
4
|
+
module ChaCha20
|
5
|
+
module_function
|
6
|
+
|
7
|
+
INDICES = [
|
8
|
+
[0, 4, 8, 12], [1, 5, 9, 13], [2, 6, 10, 14], [3, 7, 11, 15],
|
9
|
+
[0, 5, 10, 15], [1, 6, 11, 12], [2, 7, 8, 13], [3, 4, 9, 14]
|
10
|
+
]
|
11
|
+
|
12
|
+
CONSTANTS = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
|
13
|
+
|
14
|
+
# Rotate the 32-bit value v left by bits bits.
|
15
|
+
# @param [Integer] v
|
16
|
+
# @param [Integer] bits
|
17
|
+
# @return [Integer]
|
18
|
+
def rotl32(v, bits)
|
19
|
+
raise Bitcoin::BIP324::Error, "v must be integer" unless v.is_a?(Integer)
|
20
|
+
raise Bitcoin::BIP324::Error, "bits must be integer" unless bits.is_a?(Integer)
|
21
|
+
((v << bits) & 0xffffffff) | (v >> (32 - bits))
|
22
|
+
end
|
23
|
+
|
24
|
+
# Apply a ChaCha20 double round to 16-element state array +s+.
|
25
|
+
# @param [Array[Integer]] s
|
26
|
+
# @return
|
27
|
+
def double_round(s)
|
28
|
+
raise Bitcoin::BIP324::Error, "s must be Array" unless s.is_a?(Array)
|
29
|
+
INDICES.each do |a, b, c, d|
|
30
|
+
s[a] = (s[a] + s[b]) & 0xffffffff
|
31
|
+
s[d] = rotl32(s[d] ^ s[a], 16)
|
32
|
+
s[c] = (s[c] + s[d]) & 0xffffffff
|
33
|
+
s[b] = rotl32(s[b] ^ s[c], 12)
|
34
|
+
s[a] = (s[a] + s[b]) & 0xffffffff
|
35
|
+
s[d] = rotl32(s[d] ^ s[a], 8)
|
36
|
+
s[c] = (s[c] + s[d]) & 0xffffffff
|
37
|
+
s[b] = rotl32(s[b] ^ s[c], 7)
|
38
|
+
end
|
39
|
+
s
|
40
|
+
end
|
41
|
+
|
42
|
+
# Compute the 64-byte output of the ChaCha20 block function.
|
43
|
+
# @param [String] key 32-bytes key with binary format.
|
44
|
+
# @param [String] nonce 12-byte nonce with binary format.
|
45
|
+
# @param [Integer] count 32-bit integer counter.
|
46
|
+
# @return [String] 64-byte output.
|
47
|
+
def block(key, nonce, count)
|
48
|
+
raise Bitcoin::BIP324::Error, "key must be 32 byte string" if !key.is_a?(String) || key.bytesize != 32
|
49
|
+
raise Bitcoin::BIP324::Error, "nonce must be 12 byte string" if !nonce.is_a?(String) || nonce.bytesize != 12
|
50
|
+
raise Bitcoin::BIP324::Error, "count must be integer" unless count.is_a?(Integer)
|
51
|
+
# Initialize state
|
52
|
+
init = Array.new(16, 0)
|
53
|
+
4.times {|i| init[i] = CONSTANTS[i]}
|
54
|
+
key = key.unpack("V*")
|
55
|
+
8.times {|i| init[4 + i] = key[i]}
|
56
|
+
init[12] = count
|
57
|
+
nonce = nonce.unpack("V*")
|
58
|
+
3.times {|i| init[13 + i] = nonce[i]}
|
59
|
+
# Perform 20 rounds
|
60
|
+
state = init.dup
|
61
|
+
10.times do
|
62
|
+
state = double_round(state)
|
63
|
+
end
|
64
|
+
# Add initial values back into state.
|
65
|
+
16.times do |i|
|
66
|
+
state[i] = (state[i] + init[i]) & 0xffffffff
|
67
|
+
end
|
68
|
+
state.pack("V16")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Rekeying wrapper stream cipher around ChaCha20.
|
73
|
+
class FSChaCha20
|
74
|
+
attr_accessor :key
|
75
|
+
attr_reader :rekey_interval
|
76
|
+
attr_accessor :chunk_counter
|
77
|
+
attr_accessor :block_counter
|
78
|
+
attr_accessor :key_stream
|
79
|
+
|
80
|
+
def initialize(initial_key, rekey_interval = BIP324::REKEY_INTERVAL)
|
81
|
+
@block_counter = 0
|
82
|
+
@chunk_counter = 0
|
83
|
+
@key = initial_key
|
84
|
+
@rekey_interval = rekey_interval
|
85
|
+
@key_stream = ''
|
86
|
+
end
|
87
|
+
|
88
|
+
# Encrypt a chunk
|
89
|
+
# @param [String] chunk Chunk data with binary format.
|
90
|
+
# @return [String] Encrypted data with binary format.
|
91
|
+
def encrypt(chunk)
|
92
|
+
crypt(chunk)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Decrypt a chunk
|
96
|
+
# @param [String] chunk Chunk data with binary format.
|
97
|
+
# @return [String] Decrypted data with binary format.
|
98
|
+
def decrypt(chunk)
|
99
|
+
crypt(chunk)
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def key_stream_bytes(n_bytes)
|
105
|
+
while key_stream.bytesize < n_bytes
|
106
|
+
nonce = [0, (chunk_counter / REKEY_INTERVAL)].pack("VQ<")
|
107
|
+
self.key_stream << ChaCha20.block(key, nonce, block_counter)
|
108
|
+
self.block_counter += 1
|
109
|
+
end
|
110
|
+
ret = self.key_stream[0...n_bytes]
|
111
|
+
self.key_stream = self.key_stream[n_bytes..-1]
|
112
|
+
ret
|
113
|
+
end
|
114
|
+
|
115
|
+
# Encrypt or decrypt a chunk.
|
116
|
+
# @param [String] chunk Chunk data with binary format.
|
117
|
+
# @return [String]
|
118
|
+
def crypt(chunk)
|
119
|
+
ks = key_stream_bytes(chunk.bytesize)
|
120
|
+
ret = chunk.unpack("C*").zip(ks.unpack("C*")).map do |c, k|
|
121
|
+
c ^ k
|
122
|
+
end.pack("C*")
|
123
|
+
if (self.chunk_counter + 1) % rekey_interval == 0
|
124
|
+
self.key = key_stream_bytes(32)
|
125
|
+
self.block_counter = 0
|
126
|
+
end
|
127
|
+
self.chunk_counter += 1
|
128
|
+
ret
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module BIP324
|
3
|
+
# Class representing a running poly1305 computation.
|
4
|
+
class Poly1305
|
5
|
+
|
6
|
+
MODULUS = 2**130 - 5
|
7
|
+
TAG_LEN = 16
|
8
|
+
|
9
|
+
attr_reader :r
|
10
|
+
attr_reader :s
|
11
|
+
attr_accessor :acc
|
12
|
+
|
13
|
+
# Constructor
|
14
|
+
#
|
15
|
+
def initialize(key)
|
16
|
+
@r = key[0...16].reverse.bti & 0xffffffc0ffffffc0ffffffc0fffffff
|
17
|
+
@s = key[16..-1].reverse.bti
|
18
|
+
@acc = 0
|
19
|
+
end
|
20
|
+
|
21
|
+
# Add a message of any length. Input so far must be a multiple of 16 bytes.
|
22
|
+
# @param [String] msg A message with binary format.
|
23
|
+
# @return [Poly1305] self
|
24
|
+
def add(msg, length: nil, padding: false)
|
25
|
+
len = length ? length : msg.bytesize
|
26
|
+
((len + 15) / 16).times do |i|
|
27
|
+
chunk = msg[(i * 16)...(i * 16 + [16, len - i * 16].min)]
|
28
|
+
val = chunk.reverse.bti + 256**(padding ? 16 : chunk.bytesize)
|
29
|
+
self.acc = r * (acc + val) % MODULUS
|
30
|
+
end
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Compute the poly1305 tag.
|
35
|
+
# @return Poly1305 tag wit binary format.
|
36
|
+
def tag
|
37
|
+
ECDSA::Format::IntegerOctetString.encode((acc + s) & 0xffffffffffffffffffffffffffffffff, TAG_LEN).reverse
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Forward-secure wrapper around AEADChaCha20Poly1305.
|
42
|
+
class FSChaCha20Poly1305
|
43
|
+
attr_accessor :aead
|
44
|
+
attr_reader :rekey_interval
|
45
|
+
attr_accessor :packet_counter
|
46
|
+
attr_accessor :key
|
47
|
+
|
48
|
+
def initialize(initial_key, rekey_interval = REKEY_INTERVAL)
|
49
|
+
@packet_counter = 0
|
50
|
+
@rekey_interval = rekey_interval
|
51
|
+
@key = initial_key
|
52
|
+
end
|
53
|
+
|
54
|
+
# Encrypt a +plaintext+ with a specified +aad+.
|
55
|
+
# @param [String] aad AAD
|
56
|
+
# @param [String] plaintext Data to be encrypted with binary format.
|
57
|
+
# @return [String] Ciphertext
|
58
|
+
def encrypt(aad, plaintext)
|
59
|
+
crypt(aad, plaintext, false)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Decrypt a *ciphertext* with a specified +aad+.
|
63
|
+
# @param [String] aad AAD
|
64
|
+
# @param [String] ciphertext Data to be decrypted with binary format.
|
65
|
+
# @return [Array] [header, plaintext]
|
66
|
+
def decrypt(aad, ciphertext)
|
67
|
+
contents = crypt(aad, ciphertext, true)
|
68
|
+
[contents[0], contents[1..-1]]
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Encrypt or decrypt the specified (plain/cipher)text.
|
74
|
+
def crypt(aad, text, is_decrypt)
|
75
|
+
nonce = [packet_counter % rekey_interval, packet_counter / rekey_interval].pack("VQ<")
|
76
|
+
ret = if is_decrypt
|
77
|
+
chacha20_poly1305_decrypt(key, nonce, aad, text)
|
78
|
+
else
|
79
|
+
chacha20_poly1305_encrypt(key, nonce, aad, text)
|
80
|
+
end
|
81
|
+
if (packet_counter + 1) % rekey_interval == 0
|
82
|
+
rekey_nonce = "ffffffff".htb + nonce[4..-1]
|
83
|
+
newkey1 = chacha20_poly1305_encrypt(key, rekey_nonce, "", "00".htb * 32)[0...32]
|
84
|
+
newkey2 = ChaCha20.block(key, rekey_nonce, 1)[0...32]
|
85
|
+
raise Bitcoin::BIP324::Error, "newkey1 != newkey2" unless newkey1 == newkey2
|
86
|
+
self.key = newkey1
|
87
|
+
end
|
88
|
+
self.packet_counter += 1
|
89
|
+
ret
|
90
|
+
end
|
91
|
+
|
92
|
+
# Encrypt a plaintext using ChaCha20Poly1305.
|
93
|
+
def chacha20_poly1305_encrypt(key, nonce, aad, plaintext)
|
94
|
+
msg_len = plaintext.bytesize
|
95
|
+
ret = ((msg_len + 63) / 64).times.map do |i|
|
96
|
+
now = [64, msg_len - 64 * i].min
|
97
|
+
keystream = ChaCha20.block(key, nonce, i + 1)
|
98
|
+
now.times.map do |j|
|
99
|
+
plaintext[j + 64 * i].unpack1('C') ^ keystream[j].unpack1('C')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
ret = ret.flatten.pack('C*')
|
103
|
+
poly1305 = Poly1305.new(ChaCha20.block(key, nonce, 0)[0...32])
|
104
|
+
poly1305.add(aad, padding: true).add(ret, padding: true)
|
105
|
+
poly1305.add([aad.bytesize, msg_len].pack("Q<Q<"))
|
106
|
+
ret + poly1305.tag
|
107
|
+
end
|
108
|
+
|
109
|
+
# Decrypt a ChaCha20Poly1305 ciphertext.
|
110
|
+
def chacha20_poly1305_decrypt(key, nonce, aad, ciphertext)
|
111
|
+
return nil if ciphertext.bytesize < 16
|
112
|
+
msg_len = ciphertext.bytesize - 16
|
113
|
+
poly1305 = Poly1305.new(ChaCha20.block(key, nonce, 0)[0...32])
|
114
|
+
poly1305.add(aad, padding: true)
|
115
|
+
poly1305.add(ciphertext, length: msg_len, padding: true)
|
116
|
+
poly1305.add([aad.bytesize, msg_len].pack("Q<Q<"))
|
117
|
+
return nil unless ciphertext[-16..-1] == poly1305.tag
|
118
|
+
ret = ((msg_len + 63) / 64).times.map do |i|
|
119
|
+
now = [64, msg_len - 64 * i].min
|
120
|
+
keystream = ChaCha20.block(key, nonce, i + 1)
|
121
|
+
now.times.map do |j|
|
122
|
+
ciphertext[j + 64 * i].unpack1('C') ^ keystream[j].unpack1('C')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
ret.flatten.pack('C*')
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
# BIP 324 module
|
3
|
+
# https://github.com/bitcoin/bips/blob/master/bip-0324.mediawiki
|
4
|
+
module BIP324
|
5
|
+
|
6
|
+
class Error < StandardError; end
|
7
|
+
class InvalidPaketLength < Error; end
|
8
|
+
class TooLargeContent < Error; end
|
9
|
+
class InvalidEllSwiftKey < Error; end
|
10
|
+
|
11
|
+
autoload :EllSwiftPubkey, 'bitcoin/bip324/ell_swift_pubkey'
|
12
|
+
autoload :Cipher, 'bitcoin/bip324/cipher'
|
13
|
+
autoload :FSChaCha20, 'bitcoin/bip324/fs_chacha20'
|
14
|
+
autoload :FSChaCha20Poly1305, 'bitcoin/bip324/fs_chacha_poly1305'
|
15
|
+
|
16
|
+
FIELD_SIZE = 2**256 - 2**32 - 977
|
17
|
+
FIELD = ECDSA::PrimeField.new(FIELD_SIZE)
|
18
|
+
|
19
|
+
REKEY_INTERVAL = 224 # packets
|
20
|
+
|
21
|
+
module_function
|
22
|
+
|
23
|
+
def sqrt(n)
|
24
|
+
candidate = FIELD.power(n, (FIELD.prime + 1) / 4)
|
25
|
+
return nil unless FIELD.square(candidate) == n
|
26
|
+
candidate
|
27
|
+
end
|
28
|
+
|
29
|
+
MINUS_3_SQRT = sqrt(FIELD.mod(-3))
|
30
|
+
|
31
|
+
# Decode field elements (u, t) to an X coordinate on the curve.
|
32
|
+
# @param [String] u u of ElligatorSwift encoding with hex format.
|
33
|
+
# @param [String] t t of ElligatorSwift encoding with hex format.
|
34
|
+
# @return [String] x coordinate with hex format.
|
35
|
+
def xswiftec(u, t)
|
36
|
+
u = FIELD.mod(u.hex)
|
37
|
+
t = FIELD.mod(t.hex)
|
38
|
+
u = 1 if u == 0
|
39
|
+
t = 1 if t == 0
|
40
|
+
t = FIELD.mod(2 * t) if FIELD.mod(FIELD.power(u, 3) + FIELD.power(t, 2) + 7) == 0
|
41
|
+
x = FIELD.mod(FIELD.mod(FIELD.power(u, 3) + 7 - FIELD.power(t, 2)) * FIELD.inverse(2 * t))
|
42
|
+
y = FIELD.mod((x + t) * FIELD.inverse(MINUS_3_SQRT * u))
|
43
|
+
x1 = FIELD.mod(u + 4 * FIELD.power(y, 2))
|
44
|
+
x2 = FIELD.mod(FIELD.mod(FIELD.mod(-x) * FIELD.inverse(y) - u) * FIELD.inverse(2))
|
45
|
+
x3 = FIELD.mod(FIELD.mod(x * FIELD.inverse(y) - u) * FIELD.inverse(2))
|
46
|
+
[x1, x2, x3].each do |x|
|
47
|
+
unless ECDSA::Group::Secp256k1.solve_for_y(x).empty?
|
48
|
+
return ECDSA::Format::IntegerOctetString.encode(x, 32).bth
|
49
|
+
end
|
50
|
+
end
|
51
|
+
raise ArgumentError, 'Decode failed.'
|
52
|
+
end
|
53
|
+
|
54
|
+
# Inverse map for ElligatorSwift. Given x and u, find t such that xswiftec(u, t) = x, or return nil.
|
55
|
+
# @param [String] x x coordinate with hex format
|
56
|
+
# @param [String] u u of ElligatorSwift encoding with hex format
|
57
|
+
# @param [Integer] c Case selects which of the up to 8 results to return.
|
58
|
+
# @return [String] Inverse of xswiftec(u, t) with hex format or nil.
|
59
|
+
def xswiftec_inv(x, u, c)
|
60
|
+
x = FIELD.mod(x.hex)
|
61
|
+
u = FIELD.mod(u.hex)
|
62
|
+
if c & 2 == 0
|
63
|
+
return nil unless ECDSA::Group::Secp256k1.solve_for_y(FIELD.mod(-x - u)).empty?
|
64
|
+
v = x
|
65
|
+
s = FIELD.mod(
|
66
|
+
-FIELD.mod(FIELD.power(u, 3) + 7) *
|
67
|
+
FIELD.inverse(FIELD.mod(FIELD.power(u, 2) + u * v + FIELD.power(v, 2))))
|
68
|
+
else
|
69
|
+
s = FIELD.mod(x - u)
|
70
|
+
return nil if s == 0
|
71
|
+
r = sqrt(FIELD.mod(-s * (4 * (FIELD.power(u, 3) + 7) + 3 * s * FIELD.power(u, 2))))
|
72
|
+
return nil if r.nil?
|
73
|
+
return nil if c & 1 == 1 && r == 0
|
74
|
+
v = FIELD.mod(FIELD.mod(-u + r * FIELD.inverse(s)) * FIELD.inverse(2))
|
75
|
+
end
|
76
|
+
w = sqrt(s)
|
77
|
+
return nil if w.nil?
|
78
|
+
result = if c & 5 == 0
|
79
|
+
FIELD.mod(-w * FIELD.mod(u * (1 - MINUS_3_SQRT) * FIELD.inverse(2) + v))
|
80
|
+
elsif c & 5 == 1
|
81
|
+
FIELD.mod(w * FIELD.mod(u * (1 + MINUS_3_SQRT) * FIELD.inverse(2) + v))
|
82
|
+
elsif c & 5 == 4
|
83
|
+
FIELD.mod(w * FIELD.mod(u * (1 - MINUS_3_SQRT) * FIELD.inverse(2) + v))
|
84
|
+
elsif c & 5 == 5
|
85
|
+
FIELD.mod(-w * FIELD.mod(u * (1 + MINUS_3_SQRT) * FIELD.inverse(2) + v))
|
86
|
+
else
|
87
|
+
return nil
|
88
|
+
end
|
89
|
+
ECDSA::Format::IntegerOctetString.encode(result, 32).bth
|
90
|
+
end
|
91
|
+
|
92
|
+
# Given a field element X on the curve, find (u, t) that encode them.
|
93
|
+
# @param [String] x coordinate with hex format.
|
94
|
+
# @return [String] ElligatorSwift public key with hex format.
|
95
|
+
def xelligatorswift(x)
|
96
|
+
loop do
|
97
|
+
u = SecureRandom.random_number(1..ECDSA::Group::Secp256k1.order).to_s(16)
|
98
|
+
c = Random.rand(0..8)
|
99
|
+
t = xswiftec_inv(x, u, c)
|
100
|
+
unless t.nil?
|
101
|
+
return (ECDSA::Format::IntegerOctetString.encode(u.hex, 32) +
|
102
|
+
ECDSA::Format::IntegerOctetString.encode(t.hex, 32)).bth
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Compute x coordinate of shared ECDH point between +ellswift_theirs+ and +priv_key+.
|
108
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] ellswift_theirs Their EllSwift public key.
|
109
|
+
# @param [String] priv_key Private key with hex format.
|
110
|
+
# @return [String] x coordinate of shared ECDH point with hex format.
|
111
|
+
# @raise ArgumentError
|
112
|
+
def ellswift_ecdh_xonly(ellswift_theirs, priv_key)
|
113
|
+
raise ArgumentError, "ellswift_theirs must be a Bitcoin::BIP324::EllSwiftPubkey" unless ellswift_theirs.is_a?(Bitcoin::BIP324::EllSwiftPubkey)
|
114
|
+
d = priv_key.hex
|
115
|
+
x = ellswift_theirs.decode.to_point.x
|
116
|
+
field = BIP324::FIELD
|
117
|
+
y = BIP324.sqrt(field.mod(field.power(x, 3) + 7))
|
118
|
+
return nil unless y
|
119
|
+
point = ECDSA::Point.new(ECDSA::Group::Secp256k1, x, y) * d
|
120
|
+
ECDSA::Format::FieldElementOctetString.encode(point.x, point.group.field).bth
|
121
|
+
end
|
122
|
+
|
123
|
+
# Compute BIP324 shared secret.
|
124
|
+
# @param [String] priv_key Private key with hex format.
|
125
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] ellswift_theirs Their EllSwift public key.
|
126
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] ellswift_ours Our EllSwift public key.
|
127
|
+
# @param [Boolean] initiating Whether your initiator or not.
|
128
|
+
# @return [String] Shared secret with hex format.
|
129
|
+
# @raise ArgumentError
|
130
|
+
def v2_ecdh(priv_key, ellswift_theirs, ellswift_ours, initiating)
|
131
|
+
raise ArgumentError, "ellswift_theirs must be a Bitcoin::BIP324::EllSwiftPubkey" unless ellswift_theirs.is_a?(Bitcoin::BIP324::EllSwiftPubkey)
|
132
|
+
raise ArgumentError, "ellswift_ours must be a Bitcoin::BIP324::EllSwiftPubkey" unless ellswift_ours.is_a?(Bitcoin::BIP324::EllSwiftPubkey)
|
133
|
+
|
134
|
+
if Bitcoin.secp_impl.is_a?(Bitcoin::Secp256k1::Native)
|
135
|
+
Bitcoin::Secp256k1::Native.ellswift_ecdh_xonly(ellswift_theirs, ellswift_ours, priv_key, initiating)
|
136
|
+
else
|
137
|
+
ecdh_point_x32 = ellswift_ecdh_xonly(ellswift_theirs, priv_key).htb
|
138
|
+
content = initiating ? ellswift_ours.key + ellswift_theirs.key + ecdh_point_x32 :
|
139
|
+
ellswift_theirs.key + ellswift_ours.key + ecdh_point_x32
|
140
|
+
Bitcoin.tagged_hash('bip324_ellswift_xonly_ecdh', content).bth
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/bitcoin/ext/ecdsa.rb
CHANGED
@@ -10,12 +10,6 @@ class ::ECDSA::Signature
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
class ::ECDSA::Point
|
14
|
-
def to_hex(compression = true)
|
15
|
-
ECDSA::Format::PointOctetString.encode(self, compression: compression).bth
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
13
|
module ::ECDSA::Format::PointOctetString
|
20
14
|
|
21
15
|
class << self
|
data/lib/bitcoin/key.rb
CHANGED
@@ -146,9 +146,9 @@ module Bitcoin
|
|
146
146
|
# @return [Bitcoin::Key] Recovered public key.
|
147
147
|
def self.recover_compact(data, signature)
|
148
148
|
rec_id = signature.unpack1('C')
|
149
|
-
rec = rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE
|
150
|
-
raise ArgumentError, 'Invalid signature parameter' if rec < 0 || rec >
|
151
|
-
|
149
|
+
rec = (rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE) & 3
|
150
|
+
raise ArgumentError, 'Invalid signature parameter' if rec < 0 || rec > 3
|
151
|
+
|
152
152
|
compressed = (rec_id - Bitcoin::Key::COMPACT_SIG_HEADER_BYTE) & 4 != 0
|
153
153
|
Bitcoin.secp_impl.recover_compact(data, signature, rec, compressed)
|
154
154
|
end
|
@@ -344,6 +344,18 @@ module Bitcoin
|
|
344
344
|
valid_pubkey? && secp256k1_module.parse_ec_pubkey?(pubkey, allow_hybrid)
|
345
345
|
end
|
346
346
|
|
347
|
+
# Create an ellswift-encoded public key for this key, with specified entropy.
|
348
|
+
# @return [Bitcoin::BIP324::EllSwiftPubkey]
|
349
|
+
# @raise ArgumentError If ent32 does not 32 bytes.
|
350
|
+
def create_ell_pubkey
|
351
|
+
raise ArgumentError, "private_key required." unless priv_key
|
352
|
+
if secp256k1_module.is_a?(Bitcoin::Secp256k1::Native)
|
353
|
+
Bitcoin::BIP324::EllSwiftPubkey.new(secp256k1_module.ellswift_create(priv_key))
|
354
|
+
else
|
355
|
+
Bitcoin::BIP324::EllSwiftPubkey.new(Bitcoin::BIP324.xelligatorswift(xonly_pubkey))
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
347
359
|
private
|
348
360
|
|
349
361
|
def self.compare_big_endian(c1, c2)
|
data/lib/bitcoin/message_sign.rb
CHANGED
@@ -50,18 +50,22 @@ module Bitcoin
|
|
50
50
|
# @param [String] message The message that was signed.
|
51
51
|
# @return [Boolean] Verification result.
|
52
52
|
def verify_message(address, signature, message, prefix: Bitcoin.chain_params.message_magic)
|
53
|
-
|
53
|
+
addr_script = Bitcoin::Script.parse_from_addr(address)
|
54
54
|
begin
|
55
55
|
sig = Base64.strict_decode64(signature)
|
56
56
|
rescue ArgumentError
|
57
57
|
raise ArgumentError, 'Invalid signature'
|
58
58
|
end
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
59
|
+
if addr_script.p2pkh?
|
60
|
+
begin
|
61
|
+
# Legacy verification
|
62
|
+
pubkey = Bitcoin::Key.recover_compact(message_hash(message, prefix: prefix, legacy: true), sig)
|
63
|
+
return false unless pubkey
|
64
|
+
pubkey.to_p2pkh == address
|
65
|
+
rescue RuntimeError
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
elsif addr_script.witness_program?
|
65
69
|
# BIP322 verification
|
66
70
|
tx = to_sign_tx(message_hash(message, prefix: prefix, legacy: false), address)
|
67
71
|
tx.in[0].script_witness = Bitcoin::ScriptWitness.parse_from_payload(sig)
|
@@ -69,6 +73,8 @@ module Bitcoin
|
|
69
73
|
tx_out = Bitcoin::TxOut.new(script_pubkey: script_pubkey)
|
70
74
|
interpreter = Bitcoin::ScriptInterpreter.new(checker: Bitcoin::TxChecker.new(tx: tx, input_index: 0, prevouts: [tx_out]))
|
71
75
|
interpreter.verify_script(Bitcoin::Script.new, script_pubkey, tx.in[0].script_witness)
|
76
|
+
else
|
77
|
+
raise ArgumentError, "This address unsupported."
|
72
78
|
end
|
73
79
|
end
|
74
80
|
|
@@ -82,7 +88,6 @@ module Bitcoin
|
|
82
88
|
end
|
83
89
|
|
84
90
|
def validate_address!(address)
|
85
|
-
raise ArgumentError, 'Invalid address' unless Bitcoin.valid_address?(address)
|
86
91
|
script = Bitcoin::Script.parse_from_addr(address)
|
87
92
|
raise ArgumentError, 'This address unsupported' if script.p2sh? || script.p2wsh?
|
88
93
|
end
|
@@ -5,7 +5,7 @@ module Bitcoin
|
|
5
5
|
module Secp256k1
|
6
6
|
|
7
7
|
# binding for secp256k1 (https://github.com/bitcoin-core/secp256k1/)
|
8
|
-
#
|
8
|
+
# tag: v0.4.0
|
9
9
|
# this is not included by default, to enable set shared object path to ENV['SECP256K1_LIB_PATH']
|
10
10
|
# for linux, ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so'
|
11
11
|
# for mac,
|
@@ -50,14 +50,20 @@ module Bitcoin
|
|
50
50
|
attach_function(:secp256k1_ecdsa_signature_parse_der, [:pointer, :pointer, :pointer, :size_t], :int)
|
51
51
|
attach_function(:secp256k1_ecdsa_signature_normalize, [:pointer, :pointer, :pointer], :int)
|
52
52
|
attach_function(:secp256k1_ecdsa_verify, [:pointer, :pointer, :pointer, :pointer], :int)
|
53
|
-
attach_function(:
|
54
|
-
attach_function(:secp256k1_schnorrsig_verify, [:pointer, :pointer, :pointer, :pointer], :int)
|
53
|
+
attach_function(:secp256k1_schnorrsig_sign32, [:pointer, :pointer, :pointer, :pointer, :pointer], :int)
|
54
|
+
attach_function(:secp256k1_schnorrsig_verify, [:pointer, :pointer, :pointer, :size_t, :pointer], :int)
|
55
55
|
attach_function(:secp256k1_keypair_create, [:pointer, :pointer, :pointer], :int)
|
56
56
|
attach_function(:secp256k1_xonly_pubkey_parse, [:pointer, :pointer, :pointer], :int)
|
57
57
|
attach_function(:secp256k1_ecdsa_sign_recoverable, [:pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int)
|
58
58
|
attach_function(:secp256k1_ecdsa_recoverable_signature_serialize_compact, [:pointer, :pointer, :pointer, :pointer], :int)
|
59
59
|
attach_function(:secp256k1_ecdsa_recover, [:pointer, :pointer, :pointer, :pointer], :int)
|
60
60
|
attach_function(:secp256k1_ecdsa_recoverable_signature_parse_compact, [:pointer, :pointer, :pointer, :int], :int)
|
61
|
+
attach_function(:secp256k1_ellswift_decode, [:pointer, :pointer, :pointer], :int)
|
62
|
+
attach_function(:secp256k1_ellswift_create, [:pointer, :pointer, :pointer, :pointer], :int)
|
63
|
+
# Define function pointer
|
64
|
+
callback(:secp256k1_ellswift_xdh_hash_function, [:pointer, :pointer, :pointer, :pointer, :pointer], :int)
|
65
|
+
attach_variable(:secp256k1_ellswift_xdh_hash_function_bip324, :secp256k1_ellswift_xdh_hash_function)
|
66
|
+
attach_function(:secp256k1_ellswift_xdh, [:pointer, :pointer, :pointer, :pointer, :pointer, :int, :pointer, :pointer], :int)
|
61
67
|
end
|
62
68
|
|
63
69
|
def with_context(flags: (SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN))
|
@@ -224,6 +230,56 @@ module Bitcoin
|
|
224
230
|
true
|
225
231
|
end
|
226
232
|
|
233
|
+
# Decode ellswift public key.
|
234
|
+
# @param [String] ell_key ElligatorSwift key with binary format.
|
235
|
+
# @return [String] Decoded public key with hex format
|
236
|
+
def ellswift_decode(ell_key)
|
237
|
+
with_context do |context|
|
238
|
+
ell64 = FFI::MemoryPointer.new(:uchar, ell_key.bytesize).put_bytes(0, ell_key)
|
239
|
+
internal = FFI::MemoryPointer.new(:uchar, 64)
|
240
|
+
result = secp256k1_ellswift_decode(context, internal, ell64)
|
241
|
+
raise ArgumentError, 'Decode failed.' unless result == 1
|
242
|
+
serialize_pubkey_internal(context, internal, true)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Compute an ElligatorSwift public key for a secret key.
|
247
|
+
# @param [String] priv_key private key with hex format
|
248
|
+
# @return [String] ElligatorSwift public key with hex format.
|
249
|
+
def ellswift_create(priv_key)
|
250
|
+
with_context(flags: SECP256K1_CONTEXT_SIGN) do |context|
|
251
|
+
ell64 = FFI::MemoryPointer.new(:uchar, 64)
|
252
|
+
seckey32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, priv_key.htb)
|
253
|
+
result = secp256k1_ellswift_create(context, ell64, seckey32, nil)
|
254
|
+
raise ArgumentError, 'Failed to create ElligatorSwift public key.' unless result == 1
|
255
|
+
ell64.read_string(64).bth
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Compute X coordinate of shared ECDH point between elswift pubkey and privkey.
|
260
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] their_ell_pubkey Their EllSwift public key.
|
261
|
+
# @param [Bitcoin::BIP324::EllSwiftPubkey] our_ell_pubkey Our EllSwift public key.
|
262
|
+
# @param [String] priv_key private key with hex format.
|
263
|
+
# @param [Boolean] initiating Whether your initiator or not.
|
264
|
+
# @return [String] x coordinate with hex format.
|
265
|
+
def ellswift_ecdh_xonly(their_ell_pubkey, our_ell_pubkey, priv_key, initiating)
|
266
|
+
with_context(flags: SECP256K1_CONTEXT_SIGN) do |context|
|
267
|
+
output = FFI::MemoryPointer.new(:uchar, 32)
|
268
|
+
our_ell_ptr = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, our_ell_pubkey.key)
|
269
|
+
their_ell_ptr = FFI::MemoryPointer.new(:uchar, 64).put_bytes(0, their_ell_pubkey.key)
|
270
|
+
seckey32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, priv_key.htb)
|
271
|
+
hashfp = secp256k1_ellswift_xdh_hash_function_bip324
|
272
|
+
result = secp256k1_ellswift_xdh(context, output,
|
273
|
+
initiating ? our_ell_ptr : their_ell_ptr,
|
274
|
+
initiating ? their_ell_ptr : our_ell_ptr,
|
275
|
+
seckey32,
|
276
|
+
initiating ? 0 : 1,
|
277
|
+
hashfp, nil)
|
278
|
+
raise ArgumentError, "secret was invalid or hashfp returned 0" unless result == 1
|
279
|
+
output.read_string(32).bth
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
227
283
|
private
|
228
284
|
|
229
285
|
# Calculate full public key(64 bytes) from public key(32 bytes).
|
@@ -280,7 +336,7 @@ module Bitcoin
|
|
280
336
|
signature = FFI::MemoryPointer.new(:uchar, 64)
|
281
337
|
msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
|
282
338
|
aux_rand = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, aux_rand) if aux_rand
|
283
|
-
raise 'Failed to generate schnorr signature.' unless
|
339
|
+
raise 'Failed to generate schnorr signature.' unless secp256k1_schnorrsig_sign32(context, signature, msg32, keypair, aux_rand) == 1
|
284
340
|
signature.read_string(64)
|
285
341
|
end
|
286
342
|
end
|
@@ -316,7 +372,7 @@ module Bitcoin
|
|
316
372
|
xonly_pubkey = FFI::MemoryPointer.new(:uchar, pubkey.bytesize).put_bytes(0, pubkey)
|
317
373
|
signature = FFI::MemoryPointer.new(:uchar, sig.bytesize).put_bytes(0, sig)
|
318
374
|
msg32 = FFI::MemoryPointer.new(:uchar, 32).put_bytes(0, data)
|
319
|
-
result = secp256k1_schnorrsig_verify(context, signature, msg32, xonly_pubkey)
|
375
|
+
result = secp256k1_schnorrsig_verify(context, signature, msg32, 32, xonly_pubkey)
|
320
376
|
result == 1
|
321
377
|
end
|
322
378
|
end
|
@@ -77,13 +77,30 @@ module Bitcoin
|
|
77
77
|
# @param [Boolean] compressed whether compressed public key or not.
|
78
78
|
# @return [Bitcoin::Key] Recovered public key.
|
79
79
|
def recover_compact(data, signature, rec, compressed)
|
80
|
+
group = Bitcoin::Secp256k1::GROUP
|
80
81
|
r = ECDSA::Format::IntegerOctetString.decode(signature[1...33])
|
81
82
|
s = ECDSA::Format::IntegerOctetString.decode(signature[33..-1])
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
return nil if r.zero?
|
84
|
+
return nil if s.zero?
|
85
|
+
|
86
|
+
digest = ECDSA.normalize_digest(data, group.bit_length)
|
87
|
+
field = ECDSA::PrimeField.new(group.order)
|
88
|
+
|
89
|
+
unless rec & 2 == 0
|
90
|
+
r = field.mod(r + group.order)
|
86
91
|
end
|
92
|
+
|
93
|
+
is_odd = (rec & 1 == 1)
|
94
|
+
y_coordinate = group.solve_for_y(r).find{|y| is_odd ? y.odd? : y.even?}
|
95
|
+
|
96
|
+
p = group.new_point([r, y_coordinate])
|
97
|
+
|
98
|
+
inv_r = field.inverse(r)
|
99
|
+
u1 = field.mod(inv_r * digest)
|
100
|
+
u2 = field.mod(inv_r * s)
|
101
|
+
q = p * u2 + (group.new_point(u1)).negate
|
102
|
+
return nil if q.infinity?
|
103
|
+
Bitcoin::Key.from_point(q, compressed: compressed)
|
87
104
|
end
|
88
105
|
|
89
106
|
# verify signature using public key
|
data/lib/bitcoin/util.rb
CHANGED
@@ -131,10 +131,18 @@ module Bitcoin
|
|
131
131
|
double_sha256(hex.htb).bth[0..7]
|
132
132
|
end
|
133
133
|
|
134
|
-
DIGEST_NAME_SHA256 = 'sha256'
|
135
|
-
|
136
134
|
def hmac_sha256(key, data)
|
137
|
-
|
135
|
+
Bitcoin.hmac_sha256(key, data)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Run HKDF.
|
139
|
+
# @param [String] ikm The input keying material with binary format.
|
140
|
+
# @param [String] salt The salt with binary format.
|
141
|
+
# @param [String] info The context and application specific information with binary format.
|
142
|
+
# @param [Integer] length The output length in octets.
|
143
|
+
# @return [String] The result of HKDF with binary format.
|
144
|
+
def hkdf_sha256(ikm, salt, info, length = 32)
|
145
|
+
OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: length, hash: "SHA256")
|
138
146
|
end
|
139
147
|
|
140
148
|
# check whether +addr+ is valid address.
|
data/lib/bitcoin/version.rb
CHANGED
data/lib/bitcoin.rb
CHANGED
@@ -61,6 +61,7 @@ module Bitcoin
|
|
61
61
|
autoload :SigHashGenerator, 'bitcoin/sighash_generator'
|
62
62
|
autoload :MessageSign, 'bitcoin/message_sign'
|
63
63
|
autoload :Taproot, 'bitcoin/taproot'
|
64
|
+
autoload :BIP324, 'bitcoin/bip324'
|
64
65
|
|
65
66
|
require_relative 'bitcoin/constants'
|
66
67
|
require_relative 'bitcoin/ext/ecdsa'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bitcoinrb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- azuchi
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ecdsa_ext
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.5.
|
19
|
+
version: 0.5.1
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.5.
|
26
|
+
version: 0.5.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: eventmachine
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -184,14 +184,14 @@ dependencies:
|
|
184
184
|
requirements:
|
185
185
|
- - ">="
|
186
186
|
- !ruby/object:Gem::Version
|
187
|
-
version: 0.
|
187
|
+
version: 0.7.0
|
188
188
|
type: :runtime
|
189
189
|
prerelease: false
|
190
190
|
version_requirements: !ruby/object:Gem::Requirement
|
191
191
|
requirements:
|
192
192
|
- - ">="
|
193
193
|
- !ruby/object:Gem::Version
|
194
|
-
version: 0.
|
194
|
+
version: 0.7.0
|
195
195
|
- !ruby/object:Gem::Dependency
|
196
196
|
name: base32
|
197
197
|
requirement: !ruby/object:Gem::Requirement
|
@@ -332,6 +332,11 @@ files:
|
|
332
332
|
- exe/bitcoinrbd
|
333
333
|
- lib/bitcoin.rb
|
334
334
|
- lib/bitcoin/base58.rb
|
335
|
+
- lib/bitcoin/bip324.rb
|
336
|
+
- lib/bitcoin/bip324/cipher.rb
|
337
|
+
- lib/bitcoin/bip324/ell_swift_pubkey.rb
|
338
|
+
- lib/bitcoin/bip324/fs_chacha20.rb
|
339
|
+
- lib/bitcoin/bip324/fs_chacha_poly1305.rb
|
335
340
|
- lib/bitcoin/bip85_entropy.rb
|
336
341
|
- lib/bitcoin/bit_stream.rb
|
337
342
|
- lib/bitcoin/block.rb
|
@@ -496,7 +501,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
496
501
|
- !ruby/object:Gem::Version
|
497
502
|
version: '0'
|
498
503
|
requirements: []
|
499
|
-
rubygems_version: 3.
|
504
|
+
rubygems_version: 3.5.3
|
500
505
|
signing_key:
|
501
506
|
specification_version: 4
|
502
507
|
summary: The implementation of Bitcoin Protocol for Ruby.
|