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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +2 -2
- data/.ruby-version +1 -1
- data/README.md +16 -5
- 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/descriptor/addr.rb +31 -0
- data/lib/bitcoin/descriptor/checksum.rb +74 -0
- data/lib/bitcoin/descriptor/combo.rb +30 -0
- data/lib/bitcoin/descriptor/expression.rb +122 -0
- data/lib/bitcoin/descriptor/key_expression.rb +23 -0
- data/lib/bitcoin/descriptor/multi.rb +49 -0
- data/lib/bitcoin/descriptor/multi_a.rb +43 -0
- data/lib/bitcoin/descriptor/pk.rb +27 -0
- data/lib/bitcoin/descriptor/pkh.rb +15 -0
- data/lib/bitcoin/descriptor/raw.rb +32 -0
- data/lib/bitcoin/descriptor/script_expression.rb +24 -0
- data/lib/bitcoin/descriptor/sh.rb +31 -0
- data/lib/bitcoin/descriptor/sorted_multi.rb +15 -0
- data/lib/bitcoin/descriptor/sorted_multi_a.rb +15 -0
- data/lib/bitcoin/descriptor/tr.rb +91 -0
- data/lib/bitcoin/descriptor/wpkh.rb +19 -0
- data/lib/bitcoin/descriptor/wsh.rb +30 -0
- data/lib/bitcoin/descriptor.rb +176 -100
- data/lib/bitcoin/ext/ecdsa.rb +0 -6
- data/lib/bitcoin/key.rb +16 -4
- data/lib/bitcoin/message_sign.rb +13 -8
- data/lib/bitcoin/script/script.rb +8 -3
- data/lib/bitcoin/secp256k1/native.rb +62 -6
- data/lib/bitcoin/secp256k1/ruby.rb +21 -4
- data/lib/bitcoin/taproot/custom_depth_builder.rb +64 -0
- data/lib/bitcoin/taproot/simple_builder.rb +1 -6
- data/lib/bitcoin/taproot.rb +1 -0
- data/lib/bitcoin/tx.rb +1 -1
- data/lib/bitcoin/util.rb +11 -3
- data/lib/bitcoin/version.rb +1 -1
- data/lib/bitcoin.rb +1 -0
- metadata +30 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2697b9fbcca175453c80fc67d70466845c7141e6b9e9f193b5825d34bd7ffa3
|
4
|
+
data.tar.gz: 61a5c26a7cfaf7abcc7c692386ff08091ea0249c9e8ea9ccb4f3231049319f47
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40574931606fc1ff074f638bda7042f86c0225a5661f37622971e0b9502e039c5010d31663c34846c80f0bff2d25a02122eb4773e30a6fadad72bb170260b114
|
7
|
+
data.tar.gz: aaefa2672459d1d133191aecf8cbe0e8391210647e109c6e5205e349e264ad2303b25c36d4812daf5f5f0e88fb15cc4b0bfc54653ba0a2b4a8bac9fd5fd9b5d6
|
data/.github/workflows/ruby.yml
CHANGED
@@ -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@
|
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.
|
1
|
+
ruby-3.3.0
|
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# Bitcoinrb [](https://github.com/chaintope/bitcoinrb/actions/workflows/ruby.yml) [](https://badge.fury.io/rb/bitcoinrb) [](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
|
-
* [
|
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.
|
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
|