bitcoinrb 0.2.9 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +2 -2
- data/lib/bitcoin.rb +22 -0
- data/lib/bitcoin/descriptor.rb +147 -0
- data/lib/bitcoin/key.rb +6 -2
- data/lib/bitcoin/message/network_addr.rb +31 -12
- data/lib/bitcoin/message/version.rb +7 -22
- data/lib/bitcoin/network/peer.rb +3 -7
- data/lib/bitcoin/out_point.rb +7 -7
- data/lib/bitcoin/psbt.rb +3 -1
- data/lib/bitcoin/psbt/input.rb +2 -2
- data/lib/bitcoin/psbt/tx.rb +11 -0
- data/lib/bitcoin/rpc/request_handler.rb +1 -1
- data/lib/bitcoin/script/script.rb +2 -1
- data/lib/bitcoin/slip39.rb +93 -0
- data/lib/bitcoin/slip39/share.rb +122 -0
- data/lib/bitcoin/slip39/sss.rb +242 -0
- data/lib/bitcoin/slip39/wordlist/english.txt +1024 -0
- data/lib/bitcoin/tx_in.rb +1 -1
- data/lib/bitcoin/util.rb +1 -1
- data/lib/bitcoin/version.rb +1 -1
- metadata +7 -2
data/lib/bitcoin/psbt/input.rb
CHANGED
@@ -156,7 +156,7 @@ module Bitcoin
|
|
156
156
|
# @param [String] pubkey a public key with hex format.
|
157
157
|
# @param [String] sig a signature.
|
158
158
|
def add_sig(pubkey, sig)
|
159
|
-
raise ArgumentError, 'The sighash in signature is invalid.'
|
159
|
+
raise ArgumentError, 'The sighash in signature is invalid.' if sighash_type && sig.unpack('C*')[-1] != sighash_type
|
160
160
|
raise ArgumentError, 'Duplicate Key, input partial signature for pubkey already provided.' if partial_sigs[pubkey]
|
161
161
|
partial_sigs[pubkey] = sig
|
162
162
|
end
|
@@ -168,7 +168,7 @@ module Bitcoin
|
|
168
168
|
raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Input.' unless psbi.is_a?(Bitcoin::PSBT::Input)
|
169
169
|
raise ArgumentError, 'The Partially Signed Input\'s non_witness_utxo are different.' unless non_witness_utxo == psbi.non_witness_utxo
|
170
170
|
raise ArgumentError, 'The Partially Signed Input\'s witness_utxo are different.' unless witness_utxo == psbi.witness_utxo
|
171
|
-
raise ArgumentError, 'The Partially Signed Input\'s sighash_type are different.'
|
171
|
+
raise ArgumentError, 'The Partially Signed Input\'s sighash_type are different.' if sighash_type && psbi.sighash_type && sighash_type != psbi.sighash_type
|
172
172
|
raise ArgumentError, 'The Partially Signed Input\'s redeem_script are different.' unless redeem_script == psbi.redeem_script
|
173
173
|
raise ArgumentError, 'The Partially Signed Input\'s witness_script are different.' unless witness_script == psbi.witness_script
|
174
174
|
combined = Bitcoin::PSBT::Input.new(non_witness_utxo: non_witness_utxo, witness_utxo: witness_utxo)
|
data/lib/bitcoin/psbt/tx.rb
CHANGED
@@ -30,6 +30,7 @@ module Bitcoin
|
|
30
30
|
attr_reader :inputs
|
31
31
|
attr_reader :outputs
|
32
32
|
attr_accessor :unknowns
|
33
|
+
attr_accessor :version_number
|
33
34
|
|
34
35
|
def initialize(tx = nil)
|
35
36
|
@tx = tx
|
@@ -82,6 +83,9 @@ module Bitcoin
|
|
82
83
|
info = Bitcoin::PSBT::KeyOriginInfo.parse_from_payload(value)
|
83
84
|
raise ArgumentError, "global xpub's depth and the number of indexes not matched." unless xpub.depth == info.key_paths.size
|
84
85
|
partial_tx.xpubs << Bitcoin::PSBT::GlobalXpub.new(xpub, info)
|
86
|
+
when PSBT_GLOBAL_TYPES[:ver]
|
87
|
+
partial_tx.version_number = value.unpack('V').first
|
88
|
+
raise ArgumentError, "An unsupported version was detected." if SUPPORT_VERSION < partial_tx.version_number
|
85
89
|
else
|
86
90
|
raise ArgumentError, 'Duplicate Key, key for unknown value already provided.' if partial_tx.unknowns[key]
|
87
91
|
partial_tx.unknowns[([key_type].pack('C') + key).bth] = value
|
@@ -120,6 +124,12 @@ module Bitcoin
|
|
120
124
|
partial_tx
|
121
125
|
end
|
122
126
|
|
127
|
+
# get PSBT version
|
128
|
+
# @return [Integer] PSBT version number
|
129
|
+
def version
|
130
|
+
version_number ? version_number : 0
|
131
|
+
end
|
132
|
+
|
123
133
|
# Finds the UTXO for a given input index
|
124
134
|
# @param [Integer] index input_index Index of the input to retrieve the UTXO of
|
125
135
|
# @return [Bitcoin::TxOut] The UTXO of the input if found.
|
@@ -138,6 +148,7 @@ module Bitcoin
|
|
138
148
|
|
139
149
|
payload << PSBT.serialize_to_vector(PSBT_GLOBAL_TYPES[:unsigned_tx], value: tx.to_payload)
|
140
150
|
payload << xpubs.map(&:to_payload).join
|
151
|
+
payload << PSBT.serialize_to_vector(PSBT_GLOBAL_TYPES[:ver], value: [version_number].pack('V')) if version_number
|
141
152
|
|
142
153
|
payload << unknowns.map {|k,v|Bitcoin.pack_var_int(k.htb.bytesize) << k.htb << Bitcoin.pack_var_int(v.bytesize) << v}.join
|
143
154
|
|
@@ -49,7 +49,7 @@ module Bitcoin
|
|
49
49
|
# Returns connected peer information.
|
50
50
|
def getpeerinfo
|
51
51
|
node.pool.peers.map do |peer|
|
52
|
-
local_addr = peer.remote_version.remote_addr
|
52
|
+
local_addr = "#{peer.remote_version.remote_addr.ip}:18333"
|
53
53
|
{
|
54
54
|
id: peer.id,
|
55
55
|
addr: "#{peer.host}:#{peer.port}",
|
@@ -54,7 +54,8 @@ module Bitcoin
|
|
54
54
|
# @param [String] m the number of signatures required for multisig
|
55
55
|
# @param [Array] pubkeys array of public keys that compose multisig
|
56
56
|
# @return [Script] multisig script.
|
57
|
-
def self.to_multisig_script(m, pubkeys)
|
57
|
+
def self.to_multisig_script(m, pubkeys, sort: false)
|
58
|
+
pubkeys = pubkeys.sort if sort
|
58
59
|
new << m << pubkeys << pubkeys.size << OP_CHECKMULTISIG
|
59
60
|
end
|
60
61
|
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module SLIP39
|
3
|
+
|
4
|
+
WORDS = File.readlines("#{__dir__}/slip39/wordlist/english.txt").map(&:strip)
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def bits_to_bytes(n)
|
9
|
+
(n + 7) / 8
|
10
|
+
end
|
11
|
+
|
12
|
+
def bits_to_words(n)
|
13
|
+
(n + RADIX_BITS - 1) / RADIX_BITS
|
14
|
+
end
|
15
|
+
|
16
|
+
# The length of the radix in bits.
|
17
|
+
RADIX_BITS = 10
|
18
|
+
# The number of words in the wordlist.
|
19
|
+
RADIX = 2 ** RADIX_BITS
|
20
|
+
# The length of the random identifier in bits.
|
21
|
+
ID_LENGTH_BITS = 15
|
22
|
+
# The length of the iteration exponent in bits.
|
23
|
+
ITERATION_EXP_LENGTH_BITS = 5
|
24
|
+
# The length of the random identifier and iteration exponent in words.
|
25
|
+
ID_EXP_LENGTH_WORDS = bits_to_words(ID_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS)
|
26
|
+
# The maximum number of shares that can be created.
|
27
|
+
MAX_SHARE_COUNT = 16
|
28
|
+
# The length of the RS1024 checksum in words.
|
29
|
+
CHECKSUM_LENGTH_WORDS = 3
|
30
|
+
# The length of the digest of the shared secret in bytes.
|
31
|
+
DIGEST_LENGTH_BYTES = 4
|
32
|
+
# The customization string used in the RS1024 checksum and in the PBKDF2 salt.
|
33
|
+
CUSTOMIZATION_STRING = 'shamir'.bytes
|
34
|
+
# The length of the mnemonic in words without the share value.
|
35
|
+
METADATA_LENGTH_WORDS = ID_EXP_LENGTH_WORDS + 2 + CHECKSUM_LENGTH_WORDS
|
36
|
+
# The minimum allowed entropy of the master secret.
|
37
|
+
MIN_STRENGTH_BITS = 128
|
38
|
+
# The minimum allowed length of the mnemonic in words.
|
39
|
+
MIN_MNEMONIC_LENGTH_WORDS = METADATA_LENGTH_WORDS + bits_to_words(MIN_STRENGTH_BITS)
|
40
|
+
# The minimum number of iterations to use in PBKDF2.
|
41
|
+
BASE_ITERATION_COUNT = 10000
|
42
|
+
# The number of rounds to use in the Feistel cipher.
|
43
|
+
ROUND_COUNT = 4
|
44
|
+
# The index of the share containing the shared secret.
|
45
|
+
SECRET_INDEX = 255
|
46
|
+
# The index of the share containing the digest of the shared secret.
|
47
|
+
DIGEST_INDEX = 254
|
48
|
+
|
49
|
+
EXP_TABLE = [
|
50
|
+
1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19,
|
51
|
+
53, 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34,
|
52
|
+
102, 170, 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144,
|
53
|
+
171, 230, 49, 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184,
|
54
|
+
211, 110, 178, 205, 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241,
|
55
|
+
8, 24, 40, 120, 136, 131, 158, 185, 208, 107, 189, 220, 127, 129, 152,
|
56
|
+
179, 206, 73, 219, 118, 154, 181, 196, 87, 249, 16, 48, 80, 240, 11,
|
57
|
+
29, 39, 105, 187, 214, 97, 163, 254, 25, 43, 125, 135, 146, 173, 236,
|
58
|
+
47, 113, 147, 174, 233, 32, 96, 160, 251, 22, 58, 78, 210, 109, 183,
|
59
|
+
194, 93, 231, 50, 86, 250, 21, 63, 65, 195, 94, 226, 61, 71, 201,
|
60
|
+
64, 192, 91, 237, 44, 116, 156, 191, 218, 117, 159, 186, 213, 100, 172,
|
61
|
+
239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128, 155, 182, 193, 88,
|
62
|
+
232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84, 252, 31, 33,
|
63
|
+
99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202, 69, 207,
|
64
|
+
74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14, 18,
|
65
|
+
54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23,
|
66
|
+
57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246
|
67
|
+
]
|
68
|
+
|
69
|
+
LOG_TABLE = [
|
70
|
+
0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3,
|
71
|
+
100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28,
|
72
|
+
193, 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201,
|
73
|
+
9, 120, 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53,
|
74
|
+
147, 218, 142, 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241,
|
75
|
+
64, 70, 131, 56, 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226,
|
76
|
+
152, 34, 136, 145, 16, 126, 110, 72, 195, 163, 182, 30, 66, 58, 107,
|
77
|
+
40, 84, 250, 133, 61, 186, 43, 121, 10, 21, 155, 159, 94, 202, 78,
|
78
|
+
212, 172, 229, 243, 115, 167, 87, 175, 88, 168, 80, 244, 234, 214, 116,
|
79
|
+
79, 174, 233, 213, 231, 230, 173, 232, 44, 215, 117, 122, 235, 22, 11,
|
80
|
+
245, 89, 203, 95, 176, 156, 169, 81, 160, 127, 12, 246, 111, 23, 196,
|
81
|
+
73, 236, 216, 67, 31, 45, 164, 118, 123, 183, 204, 187, 62, 90, 251,
|
82
|
+
96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157, 151, 178, 135, 144,
|
83
|
+
97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209, 83, 57, 132,
|
84
|
+
60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171, 68, 17, 146,
|
85
|
+
217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165, 103, 74, 237,
|
86
|
+
222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7
|
87
|
+
]
|
88
|
+
|
89
|
+
autoload :SSS, 'bitcoin/slip39/sss'
|
90
|
+
autoload :Share, 'bitcoin/slip39/share'
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Bitcoin
|
2
|
+
module SLIP39
|
3
|
+
|
4
|
+
# Share of Shamir's Secret Sharing Scheme
|
5
|
+
class Share
|
6
|
+
|
7
|
+
attr_accessor :id # 15 bits, Integer
|
8
|
+
attr_accessor :iteration_exp # 5 bits, Integer
|
9
|
+
attr_accessor :group_index # 4 bits, Integer
|
10
|
+
attr_accessor :group_threshold # 4 bits, Integer
|
11
|
+
attr_accessor :group_count # 4 bits, Integer
|
12
|
+
attr_accessor :member_index # 4 bits, Integer
|
13
|
+
attr_accessor :member_threshold # 4 bits, Integer
|
14
|
+
attr_accessor :value # 8n bits, hex string.
|
15
|
+
attr_accessor :checksum # 30 bits, Integer
|
16
|
+
|
17
|
+
# Recover Share from the mnemonic words
|
18
|
+
# @param [Array{String}] words the mnemonic words
|
19
|
+
# @return [Bitcoin::SLIP39::Share] a share
|
20
|
+
def self.from_words(words)
|
21
|
+
raise ArgumentError, 'Mnemonics should be an array of strings' unless words.is_a?(Array)
|
22
|
+
indices = words.map do |word|
|
23
|
+
index = Bitcoin::SLIP39::WORDS.index(word.downcase)
|
24
|
+
raise IndexError, 'word not found in words list.' unless index
|
25
|
+
index
|
26
|
+
end
|
27
|
+
|
28
|
+
raise ArgumentError, 'Invalid mnemonic length.' if indices.size < MIN_MNEMONIC_LENGTH_WORDS
|
29
|
+
raise ArgumentError, 'Invalid mnemonic checksum.' unless verify_rs1024_checksum(indices)
|
30
|
+
|
31
|
+
padding_length = (RADIX_BITS * (indices.size - METADATA_LENGTH_WORDS)) % 16
|
32
|
+
raise ArgumentError, 'Invalid mnemonic length.' if padding_length > 8
|
33
|
+
data = indices.map{|i|i.to_s(2).rjust(10, '0')}.join
|
34
|
+
|
35
|
+
s = self.new
|
36
|
+
s.id = data[0...ID_LENGTH_BITS].to_i(2)
|
37
|
+
s.iteration_exp = data[ID_LENGTH_BITS...(ID_LENGTH_BITS + ITERATION_EXP_LENGTH_BITS)].to_i(2)
|
38
|
+
s.group_index = data[20...24].to_i(2)
|
39
|
+
s.group_threshold = data[24...28].to_i(2) + 1
|
40
|
+
s.group_count = data[28...32].to_i(2) + 1
|
41
|
+
raise ArgumentError, "Invalid mnemonic. Group threshold(#{s.group_threshold}) cannot be greater than group count(#{s.group_count})." if s.group_threshold > s.group_count
|
42
|
+
s.member_index = data[32...36].to_i(2)
|
43
|
+
s.member_threshold = data[36...40].to_i(2) + 1
|
44
|
+
value_length = data.length - 70
|
45
|
+
start_index = 40 + padding_length
|
46
|
+
end_index = start_index + value_length - padding_length
|
47
|
+
padding_value = data[40...(40 + padding_length)]
|
48
|
+
raise ArgumentError, "Invalid mnemonic. padding must only zero." unless padding_value.to_i(2) == 0
|
49
|
+
s.value = data[start_index...end_index].to_i(2).to_even_length_hex
|
50
|
+
s.checksum = data[(40 + value_length)..-1].to_i(2)
|
51
|
+
s
|
52
|
+
end
|
53
|
+
|
54
|
+
# Generate mnemonic words
|
55
|
+
# @return [Array[String]] array of mnemonic word.
|
56
|
+
def to_words
|
57
|
+
indices = build_word_indices
|
58
|
+
indices.map{|index| Bitcoin::SLIP39::WORDS[index]}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Calculate checksum using current fields
|
62
|
+
# @return [Integer] checksum
|
63
|
+
def calculate_checksum
|
64
|
+
indices = build_word_indices(false)
|
65
|
+
create_rs1024_checksum(indices).map{|i|i.to_bits(10)}.join.to_i(2)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.rs1024_polymod(values)
|
69
|
+
gen = [0xe0e040, 0x1c1c080, 0x3838100, 0x7070200, 0xe0e0009, 0x1c0c2412, 0x38086c24, 0x3090fc48, 0x21b1f890, 0x3f3f120]
|
70
|
+
chk = 1
|
71
|
+
values.each do |v|
|
72
|
+
b = (chk >> 20)
|
73
|
+
chk = (chk & 0xfffff) << 10 ^ v
|
74
|
+
10.times do |i|
|
75
|
+
chk ^= (((b >> i) & 1 == 1) ? gen[i] : 0)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
chk
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Create word indices from this share.
|
84
|
+
# @param [Boolean] include_checksum whether include checksum when creating indices.
|
85
|
+
# @param [Array[Integer]] the array of index
|
86
|
+
def build_word_indices(include_checksum = true)
|
87
|
+
s = id.to_bits(ID_LENGTH_BITS)
|
88
|
+
s << iteration_exp.to_bits(ITERATION_EXP_LENGTH_BITS)
|
89
|
+
s << group_index.to_bits(4)
|
90
|
+
s << (group_threshold - 1).to_bits(4)
|
91
|
+
s << (group_count - 1).to_bits(4)
|
92
|
+
raise StandardError, "Group threshold(#{group_threshold}) cannot be greater than group count(#{group_count})." if group_threshold > group_count
|
93
|
+
s << member_index.to_bits(4)
|
94
|
+
s << (member_threshold - 1).to_bits(4)
|
95
|
+
value_length = value.to_i(16).bit_length
|
96
|
+
padding_length = RADIX_BITS - (value_length % RADIX_BITS)
|
97
|
+
s << value.to_i(16).to_bits(value_length + padding_length)
|
98
|
+
s << checksum.to_bits(30) if include_checksum
|
99
|
+
s.chars.each_slice(10).map{|index| index.join.to_i(2)}
|
100
|
+
end
|
101
|
+
|
102
|
+
# Verify RS1024 checksum
|
103
|
+
# @param [Array[Integer] data the array of mnemonic word index
|
104
|
+
# @return [Boolean] verify result
|
105
|
+
def self.verify_rs1024_checksum(data)
|
106
|
+
rs1024_polymod(CUSTOMIZATION_STRING + data) == 1
|
107
|
+
end
|
108
|
+
|
109
|
+
# Create RS1024 checksum
|
110
|
+
# @param [Array[Integer] data the array of mnemonic word index without checksum
|
111
|
+
# @return [Array[Integer]] the array of checksum integer
|
112
|
+
def create_rs1024_checksum(data)
|
113
|
+
values = CUSTOMIZATION_STRING + data + Array.new(CHECKSUM_LENGTH_WORDS, 0)
|
114
|
+
polymod = Bitcoin::SLIP39::Share.rs1024_polymod(values) ^ 1
|
115
|
+
CHECKSUM_LENGTH_WORDS.times.to_a.reverse.map {|i|(polymod >> (10 * i)) & 1023 }
|
116
|
+
end
|
117
|
+
|
118
|
+
private_class_method :verify_rs1024_checksum
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module Bitcoin
|
4
|
+
module SLIP39
|
5
|
+
|
6
|
+
# Shamir's Secret Sharing
|
7
|
+
class SSS
|
8
|
+
|
9
|
+
# Create SSS shares.
|
10
|
+
#
|
11
|
+
# [Usage]
|
12
|
+
# 4 groups shares.
|
13
|
+
# = two for Alice
|
14
|
+
# = one for friends(required 3 of her 5 friends) and
|
15
|
+
# = one for family members(required 2 of her 6 family)
|
16
|
+
#
|
17
|
+
# Two of these group shares are required to reconstruct the master secret.
|
18
|
+
# groups = [1, 1], [1, 1], [3, 5], [2, 6]
|
19
|
+
#
|
20
|
+
# group_shares = Bitcoin::SLIP39::SSS.setup_shares(group_threshold: 2, groups: groups, secret: 'secret with hex format', passphrase: 'xxx')
|
21
|
+
# return 4 group array of Bitcoin::SLIP39::Share
|
22
|
+
#
|
23
|
+
# Get each share word
|
24
|
+
# groups[0][1].to_words
|
25
|
+
# => ["shadow", "pistol", "academic", "always", "adequate", "wildlife", "fancy", "gross", "oasis", "cylinder", "mustang", "wrist", "rescue", "view", "short", "owner", "flip", "making", "coding", "armed"]
|
26
|
+
#
|
27
|
+
# @param [Array[Array[Integer, Integer]]] groups
|
28
|
+
# @param [Integer] group_threshold threshold number of group shares required to reconstruct the master secret.
|
29
|
+
# @param [Integer] exp Iteration exponent. default is 0.
|
30
|
+
# @param [String] secret master secret with hex format.
|
31
|
+
# @param [String] passphrase the passphrase used for encryption/decryption.
|
32
|
+
# @return [Array[Array[Bitcoin::SLIP39::Share]]] array of group shares.
|
33
|
+
def self.setup_shares(groups: [], group_threshold: nil, exp: 0, secret: nil, passphrase: '')
|
34
|
+
raise ArgumentError, 'Groups is empty.' if groups.empty?
|
35
|
+
raise ArgumentError, 'Group threshold must be greater than 0.' if group_threshold.nil? || group_threshold < 1
|
36
|
+
raise ArgumentError, 'Master secret does not specified.' unless secret
|
37
|
+
raise ArgumentError, "The length of the master secret (#{secret.htb.bytesize} bytes) must be at least #{MIN_STRENGTH_BITS / 8} bytes." if (secret.htb.bytesize * 8) < MIN_STRENGTH_BITS
|
38
|
+
raise ArgumentError, 'The length of the master secret in bytes must be an even number.' unless secret.bytesize.even?
|
39
|
+
raise ArgumentError, 'The passphrase must contain only printable ASCII characters (code points 32-126).' unless passphrase.ascii_only?
|
40
|
+
raise ArgumentError, "The requested group threshold (#{group_threshold}) must not exceed the number of groups (#{groups.length})." if group_threshold > groups.length
|
41
|
+
groups.each do |threshold, count|
|
42
|
+
raise ArgumentError, 'Group threshold must be greater than 0.' if threshold.nil? || threshold < 1
|
43
|
+
raise ArgumentError, "The requested member threshold (#{threshold}) must not exceed the number of share (#{count})." if threshold > count
|
44
|
+
raise ArgumentError, "Creating multiple member shares with member threshold 1 is not allowed. Use 1-of-1 member sharing instead." if threshold == 1 && count > 1
|
45
|
+
end
|
46
|
+
|
47
|
+
id = SecureRandom.random_number(32767) # 32767 is max number for 15 bits.
|
48
|
+
ems = encrypt(secret, passphrase, exp, id)
|
49
|
+
|
50
|
+
group_shares = split_secret(group_threshold, groups.length, ems)
|
51
|
+
|
52
|
+
shares = group_shares.map.with_index do |s, i|
|
53
|
+
group_index, group_share = s[0], s[1]
|
54
|
+
member_threshold, member_count = groups[i][0], groups[i][1]
|
55
|
+
shares = split_secret(member_threshold, member_count, group_share)
|
56
|
+
shares.map do |member_index, member_share|
|
57
|
+
share = Bitcoin::SLIP39::Share.new
|
58
|
+
share.id = id
|
59
|
+
share.iteration_exp = exp
|
60
|
+
share.group_index = group_index
|
61
|
+
share.group_threshold = group_threshold
|
62
|
+
share.group_count = groups.length
|
63
|
+
share.member_index = member_index
|
64
|
+
share.member_threshold = member_threshold
|
65
|
+
share.value = member_share
|
66
|
+
share.checksum = share.calculate_checksum
|
67
|
+
share
|
68
|
+
end
|
69
|
+
end
|
70
|
+
shares
|
71
|
+
end
|
72
|
+
|
73
|
+
# recovery master secret form shares.
|
74
|
+
#
|
75
|
+
# [Usage]
|
76
|
+
# shares: An array of shares required for recovery.
|
77
|
+
# master_secret = Bitcoin::SLIP39::SSS.recover_secret(shares, passphrase: 'xxx')
|
78
|
+
#
|
79
|
+
# @param [Array[Bitcoin::SLIP30::Share]] shares an array of shares.
|
80
|
+
# @param [String] passphrase the passphrase using decrypt master secret.
|
81
|
+
# @return [String] a master secret.
|
82
|
+
def self.recover_secret(shares, passphrase: '')
|
83
|
+
raise ArgumentError, 'share is empty.' if shares.nil? || shares.empty?
|
84
|
+
groups = {}
|
85
|
+
id = shares[0].id
|
86
|
+
exp = shares[0].iteration_exp
|
87
|
+
group_threshold = shares.first.group_threshold
|
88
|
+
group_count = shares.first.group_count
|
89
|
+
|
90
|
+
shares.each do |share|
|
91
|
+
raise ArgumentError, 'Invalid set of shares. All shares must have the same id.' unless id == share.id
|
92
|
+
raise ArgumentError, 'Invalid set of shares. All shares must have the same group threshold.' unless group_threshold == share.group_threshold
|
93
|
+
raise ArgumentError, 'Invalid set of shares. All shares must have the same group count.' unless group_count == share.group_count
|
94
|
+
raise ArgumentError, 'Invalid set of shares. All Shares must have the same iteration exponent.' unless exp == share.iteration_exp
|
95
|
+
groups[share.group_index] ||= []
|
96
|
+
groups[share.group_index] << share
|
97
|
+
end
|
98
|
+
|
99
|
+
group_shares = {}
|
100
|
+
groups.each do |group_index, shares|
|
101
|
+
member_threshold = shares.first.member_threshold
|
102
|
+
raise ArgumentError, "Wrong number of mnemonics. Threshold is #{member_threshold}, but share count is #{shares.length}" if shares.length < member_threshold
|
103
|
+
if shares.length == 1 && member_threshold == 1
|
104
|
+
group_shares[group_index] = shares.first.value
|
105
|
+
else
|
106
|
+
value_length = shares.first.value.length
|
107
|
+
x_coordinates = []
|
108
|
+
shares.each do |share|
|
109
|
+
raise ArgumentError, 'Invalid set of shares. All shares in a group must have the same member threshold.' unless member_threshold == share.member_threshold
|
110
|
+
raise ArgumentError, 'Invalid set of shares. All share values must have the same length.' unless value_length == share.value.length
|
111
|
+
x_coordinates << share.member_index
|
112
|
+
end
|
113
|
+
x_coordinates.uniq!
|
114
|
+
raise ArgumentError, 'Invalid set of shares. Share indices must be unique.' unless x_coordinates.size == shares.size
|
115
|
+
interpolate_shares = shares.map{|s|[s.member_index, s.value]}
|
116
|
+
|
117
|
+
secret = interpolate(interpolate_shares, SECRET_INDEX)
|
118
|
+
digest_value = interpolate(interpolate_shares, DIGEST_INDEX).htb
|
119
|
+
digest, random_value = digest_value[0...DIGEST_LENGTH_BYTES].bth, digest_value[DIGEST_LENGTH_BYTES..-1].bth
|
120
|
+
recover_digest = create_digest(secret, random_value)
|
121
|
+
raise ArgumentError, 'Invalid digest of the shared secret.' unless digest == recover_digest
|
122
|
+
|
123
|
+
group_shares[group_index] = secret
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
return decrypt(group_shares.values.first, passphrase, exp, id) if group_threshold == 1
|
128
|
+
|
129
|
+
raise ArgumentError, "Wrong number of mnemonics. Group threshold is #{group_threshold}, but share count is #{group_shares.length}" if group_shares.length < group_threshold
|
130
|
+
|
131
|
+
interpolate_shares = group_shares.map{|k, v|[k, v]}
|
132
|
+
secret = interpolate(interpolate_shares, SECRET_INDEX)
|
133
|
+
digest_value = interpolate(interpolate_shares, DIGEST_INDEX).htb
|
134
|
+
digest, random_value = digest_value[0...DIGEST_LENGTH_BYTES].bth, digest_value[DIGEST_LENGTH_BYTES..-1].bth
|
135
|
+
recover_digest = create_digest(secret, random_value)
|
136
|
+
raise ArgumentError, 'Invalid digest of the shared secret.' unless digest == recover_digest
|
137
|
+
|
138
|
+
decrypt(secret, passphrase, exp, id)
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
# Calculate f(x) from given shamir shares.
|
144
|
+
# @param [Array[index, value]] shares the array of shamir shares.
|
145
|
+
# @param [Integer] x the x coordinate of the result.
|
146
|
+
# @return [String] f(x) value with hex format.
|
147
|
+
def self.interpolate(shares, x)
|
148
|
+
s = shares.find{|s|s[0] == x}
|
149
|
+
return s[1] if s
|
150
|
+
|
151
|
+
log_prod = shares.sum{|s|LOG_TABLE[s[0] ^ x]}
|
152
|
+
|
153
|
+
result = ('00' * shares.first[1].length).htb
|
154
|
+
shares.each do |share|
|
155
|
+
log_basis_eval = (log_prod - LOG_TABLE[share[0] ^ x] - shares.sum{|s|LOG_TABLE[share[0] ^ s[0]]}) % 255
|
156
|
+
result = share[1].htb.bytes.each.map.with_index do |v, i|
|
157
|
+
(result[i].bti ^ (v == 0 ? 0 : (EXP_TABLE[(LOG_TABLE[v] + log_basis_eval) % 255]))).itb
|
158
|
+
end.join
|
159
|
+
end
|
160
|
+
result.bth
|
161
|
+
end
|
162
|
+
|
163
|
+
# Decrypt encrypted master secret using passphrase.
|
164
|
+
# @param [String] ems an encrypted master secret with hex format.
|
165
|
+
# @param [String] passphrase the passphrase when using encrypt master secret with binary format.
|
166
|
+
# @param [Integer] exp iteration exponent
|
167
|
+
# @param [Integer] id identifier
|
168
|
+
def self.decrypt(ems, passphrase, exp, id)
|
169
|
+
l, r = ems[0...(ems.length / 2)].htb, ems[(ems.length / 2)..-1].htb
|
170
|
+
salt = get_salt(id)
|
171
|
+
e = (Bitcoin::SLIP39::BASE_ITERATION_COUNT << exp) / Bitcoin::SLIP39::ROUND_COUNT
|
172
|
+
Bitcoin::SLIP39::ROUND_COUNT.times.to_a.reverse.each do |i|
|
173
|
+
f = OpenSSL::PKCS5.pbkdf2_hmac((i.itb + passphrase), salt + r, e, r.bytesize, 'sha256')
|
174
|
+
l, r = r, (l.bti ^ f.bti).itb
|
175
|
+
end
|
176
|
+
(r + l).bth
|
177
|
+
end
|
178
|
+
|
179
|
+
# Encrypt master secret using passphrase
|
180
|
+
# @param [String] secret master secret with hex format.
|
181
|
+
# @param [String] passphrase the passphrase when using encrypt master secret with binary format.
|
182
|
+
# @param [Integer] exp iteration exponent
|
183
|
+
# @param [Integer] id identifier
|
184
|
+
# @return [String] encrypted master secret with hex format.
|
185
|
+
def self.encrypt(secret, passphrase, exp, id)
|
186
|
+
s = secret.htb
|
187
|
+
l, r = s[0...(s.bytesize / 2)], s[(s.bytesize / 2)..-1]
|
188
|
+
salt = get_salt(id)
|
189
|
+
e = (Bitcoin::SLIP39::BASE_ITERATION_COUNT << exp) / Bitcoin::SLIP39::ROUND_COUNT
|
190
|
+
Bitcoin::SLIP39::ROUND_COUNT.times.to_a.each do |i|
|
191
|
+
f = OpenSSL::PKCS5.pbkdf2_hmac((i.itb + passphrase), salt + r, e, r.bytesize, 'sha256')
|
192
|
+
l, r = r, (l.bti ^ f.bti).itb
|
193
|
+
end
|
194
|
+
(r + l).bth
|
195
|
+
end
|
196
|
+
|
197
|
+
# Create digest of the shared secret.
|
198
|
+
# @param [String] secret the shared secret with hex format.
|
199
|
+
# @param [String] random value (n-4 bytes) with hex format.
|
200
|
+
# @return [String] digest value(4 bytes) with hex format.
|
201
|
+
def self.create_digest(secret, random)
|
202
|
+
h = Bitcoin.hmac_sha256(random.htb, secret.htb)
|
203
|
+
h[0...4].bth
|
204
|
+
end
|
205
|
+
|
206
|
+
# get salt using encryption/decryption form id.
|
207
|
+
# @param [Integer] id id
|
208
|
+
# @return [String] salt with binary format.
|
209
|
+
def self.get_salt(id)
|
210
|
+
(Bitcoin::SLIP39::CUSTOMIZATION_STRING.pack('c*') + id.itb)
|
211
|
+
end
|
212
|
+
|
213
|
+
# Split the share into +count+ with threshold +threshold+.
|
214
|
+
# @param [Integer] threshold the threshold.
|
215
|
+
# @param [Integer] count split count.
|
216
|
+
# @param [Integer] secret the secret to be split.
|
217
|
+
# @return [Array[Integer, String]] the array of split secret.
|
218
|
+
def self.split_secret(threshold, count, secret)
|
219
|
+
raise ArgumentError, "The requested threshold (#{threshold}) must be a positive integer." if threshold < 1
|
220
|
+
raise ArgumentError, "The requested threshold (#{threshold}) must not exceed the number of shares (#{count})." if threshold > count
|
221
|
+
raise ArgumentError, "The requested number of shares (#{count}) must not exceed #{MAX_SHARE_COUNT}." if count > MAX_SHARE_COUNT
|
222
|
+
|
223
|
+
return count.times.map{|i|[i, secret]} if threshold == 1 # if the threshold is 1, digest of the share is not used.
|
224
|
+
|
225
|
+
random_share_count = threshold - 2
|
226
|
+
|
227
|
+
shares = random_share_count.times.map{|i|[i, SecureRandom.hex(secret.htb.bytesize)]}
|
228
|
+
random_part = SecureRandom.hex(secret.htb.bytesize - DIGEST_LENGTH_BYTES)
|
229
|
+
digest = create_digest(secret, random_part)
|
230
|
+
|
231
|
+
base_shares = shares + [[DIGEST_INDEX, digest + random_part], [SECRET_INDEX, secret]]
|
232
|
+
|
233
|
+
(random_share_count...count).each { |i| shares << [i, interpolate(base_shares, i)]}
|
234
|
+
|
235
|
+
shares
|
236
|
+
end
|
237
|
+
|
238
|
+
private_class_method :split_secret, :get_salt, :interpolate, :encrypt, :decrypt, :create_digest
|
239
|
+
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|