bitcoinrb 0.2.9 → 0.3.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.
@@ -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.' unless sig.unpack('C*')[-1] == sighash_type
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.' unless sighash_type == psbi.sighash_type
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)
@@ -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[0..peer.remote_version.remote_addr.rindex(':')] + '18333'
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