bitcoinrb 1.4.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +2 -2
  3. data/.ruby-version +1 -1
  4. data/README.md +16 -5
  5. data/bitcoinrb.gemspec +2 -2
  6. data/lib/bitcoin/bip324/cipher.rb +113 -0
  7. data/lib/bitcoin/bip324/ell_swift_pubkey.rb +42 -0
  8. data/lib/bitcoin/bip324/fs_chacha20.rb +132 -0
  9. data/lib/bitcoin/bip324/fs_chacha_poly1305.rb +129 -0
  10. data/lib/bitcoin/bip324.rb +144 -0
  11. data/lib/bitcoin/descriptor/addr.rb +31 -0
  12. data/lib/bitcoin/descriptor/checksum.rb +74 -0
  13. data/lib/bitcoin/descriptor/combo.rb +30 -0
  14. data/lib/bitcoin/descriptor/expression.rb +122 -0
  15. data/lib/bitcoin/descriptor/key_expression.rb +23 -0
  16. data/lib/bitcoin/descriptor/multi.rb +49 -0
  17. data/lib/bitcoin/descriptor/multi_a.rb +43 -0
  18. data/lib/bitcoin/descriptor/pk.rb +27 -0
  19. data/lib/bitcoin/descriptor/pkh.rb +15 -0
  20. data/lib/bitcoin/descriptor/raw.rb +32 -0
  21. data/lib/bitcoin/descriptor/script_expression.rb +24 -0
  22. data/lib/bitcoin/descriptor/sh.rb +31 -0
  23. data/lib/bitcoin/descriptor/sorted_multi.rb +15 -0
  24. data/lib/bitcoin/descriptor/sorted_multi_a.rb +15 -0
  25. data/lib/bitcoin/descriptor/tr.rb +91 -0
  26. data/lib/bitcoin/descriptor/wpkh.rb +19 -0
  27. data/lib/bitcoin/descriptor/wsh.rb +30 -0
  28. data/lib/bitcoin/descriptor.rb +176 -100
  29. data/lib/bitcoin/ext/ecdsa.rb +0 -6
  30. data/lib/bitcoin/key.rb +16 -4
  31. data/lib/bitcoin/message_sign.rb +13 -8
  32. data/lib/bitcoin/script/script.rb +8 -3
  33. data/lib/bitcoin/secp256k1/native.rb +62 -6
  34. data/lib/bitcoin/secp256k1/ruby.rb +21 -4
  35. data/lib/bitcoin/taproot/custom_depth_builder.rb +64 -0
  36. data/lib/bitcoin/taproot/simple_builder.rb +1 -6
  37. data/lib/bitcoin/taproot.rb +1 -0
  38. data/lib/bitcoin/tx.rb +1 -1
  39. data/lib/bitcoin/util.rb +11 -3
  40. data/lib/bitcoin/version.rb +1 -1
  41. data/lib/bitcoin.rb +1 -0
  42. metadata +30 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 544382f715d87eb79e185e310c827b2349c0af75c1bd89887a653510e226cd1f
4
- data.tar.gz: c807d0965da7c06f71b248d5a543ccfff2375d702a6ab4db70698c4be245720d
3
+ metadata.gz: f2697b9fbcca175453c80fc67d70466845c7141e6b9e9f193b5825d34bd7ffa3
4
+ data.tar.gz: 61a5c26a7cfaf7abcc7c692386ff08091ea0249c9e8ea9ccb4f3231049319f47
5
5
  SHA512:
6
- metadata.gz: 22ea36535fb045c35d28809f87c60f4451e2d37801e7a231772068c268d6e58cdeb086456ffc649091a5a28a82815af77f6d35ef286a7a101e53fa3f97e55a9a
7
- data.tar.gz: 7ca7b637b77bdde57d2af3faf056184397b1d87fd98dd5c46933dfb40e716d5bf8ef56890691771b97d8e66dde1c9684cb1d03e9f74f99d4f8b5bbaeb6a37e2c
6
+ metadata.gz: 40574931606fc1ff074f638bda7042f86c0225a5661f37622971e0b9502e039c5010d31663c34846c80f0bff2d25a02122eb4773e30a6fadad72bb170260b114
7
+ data.tar.gz: aaefa2672459d1d133191aecf8cbe0e8391210647e109c6e5205e349e264ad2303b25c36d4812daf5f5f0e88fb15cc4b0bfc54653ba0a2b4a8bac9fd5fd9b5d6
@@ -19,10 +19,10 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
21
21
  matrix:
22
- ruby-version: ['3.0', '3.1', '3.2']
22
+ ruby-version: ['3.0', '3.1', '3.2', '3.3']
23
23
 
24
24
  steps:
25
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v4
26
26
  - name: Install leveldb
27
27
  run: sudo apt-get install libleveldb-dev
28
28
  - name: Set up Ruby
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.2.0
1
+ ruby-3.3.0
data/README.md CHANGED
@@ -1,6 +1,5 @@
1
1
  # Bitcoinrb [![Build Status](https://github.com/chaintope/bitcoinrb/actions/workflows/ruby.yml/badge.svg?branch=master)](https://github.com/chaintope/bitcoinrb/actions/workflows/ruby.yml) [![Gem Version](https://badge.fury.io/rb/bitcoinrb.svg)](https://badge.fury.io/rb/bitcoinrb) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) <img src="http://segwit.co/static/public/images/logo.png" width="100">
2
2
 
3
-
4
3
  Bitcoinrb is a Ruby implementation of Bitcoin Protocol.
5
4
 
6
5
  NOTE: Bitcoinrb work in progress, and there is a possibility of incompatible change.
@@ -18,10 +17,9 @@ Bitcoinrb supports following feature:
18
17
  * bech32([BIP-173](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki)) and bech32m([BIP-350](https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki)) address support
19
18
  * [BIP-174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki) PSBT(Partially Signed Bitcoin Transaction) support
20
19
  * [BIP-85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki) Deterministic Entropy From BIP32 Keychains support by `Bitcoin::BIP85Entropy` class.
21
- * Schnorr signature([BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki))
22
- * Taproot consensus([BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) and [BIP-342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki))
23
- * [WIP] SPV node
24
- * [WIP] 0ff-chain protocol
20
+ * Schnorr signature([BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki))
21
+ * Taproot consensus([BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) and [BIP-342](https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki))
22
+ * [Output script descriptor](https://github.com/chaintope/bitcoinrb/wiki/Output-Script-Descriptor) ([BIP-380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki), [BIP-381](https://github.com/bitcoin/bips/blob/master/bip-0381.mediawiki), [BIP-382](https://github.com/bitcoin/bips/blob/master/bip-0382.mediawiki), [BIP-383](https://github.com/bitcoin/bips/blob/master/bip-0383.mediawiki), [BIP-384](https://github.com/bitcoin/bips/blob/master/bip-0384.mediawiki), [BIP-385](https://github.com/bitcoin/bips/blob/master/bip-0385.mediawiki), [BIP-386](https://github.com/bitcoin/bips/blob/master/bip-0386.mediawiki), [BIP-387](https://github.com/bitcoin/bips/blob/master/bip-0387.mediawiki))
25
23
 
26
24
  ## Requirements
27
25
 
@@ -105,6 +103,19 @@ Bitcoin.chain_params = :signet
105
103
 
106
104
  This parameter is described in https://github.com/chaintope/bitcoinrb/blob/master/lib/bitcoin/chainparams/signet.yml.
107
105
 
106
+ ## Test
107
+
108
+ This library can use the [libsecp256k1](https://github.com/bitcoin-core/secp256k1/) dynamic library.
109
+ Therefore, some tests require this library. In a Linux environment, `spec/lib/libsecp256k1.so` is already located,
110
+ so there is no need to do anything. If you want to test in another environment,
111
+ please set the library path in the environment variable `TEST_LIBSECP256K1_PATH`.
112
+
113
+ In case the supplied linux `spec/lib/libsecp256k1.so` is not working (architecture), you might have to compile it yourself.
114
+ Since if available in the repository, it might not be compiled using the `./configure --enable-module-recovery` option.
115
+ Then `TEST_LIBSECP256K1_PATH=/path/to/secp256k1/.libs/libsecp256k1.so rspec` can be used.
116
+
117
+ The libsecp256k1 library currently tested for operation with this library is `v0.4.0`.
118
+
108
119
  ## Contributing
109
120
 
110
121
  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.0'
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.5.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