bitcoinrb 0.2.9 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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