bitcoinrb 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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