bitcoinrb 1.4.0 → 1.6.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 +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 [![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
|
-
* [
|
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
|