money-tree-extended 0.11.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.
@@ -0,0 +1,265 @@
1
+ # encoding ascii-8bit
2
+
3
+ require 'openssl'
4
+
5
+ module MoneyTree
6
+ class Key
7
+ include OpenSSL
8
+ include Support
9
+ extend Support
10
+ class KeyInvalid < StandardError; end
11
+ class KeyGenerationFailure < StandardError; end
12
+ class KeyImportFailure < StandardError; end
13
+ class KeyFormatNotFound < StandardError; end
14
+ class InvalidWIFFormat < StandardError; end
15
+ class InvalidBase64Format < StandardError; end
16
+
17
+ attr_reader :options, :key, :raw_key
18
+ attr_accessor :ec_key
19
+
20
+ GROUP_NAME = 'secp256k1'
21
+ ORDER = "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141".to_i(16)
22
+
23
+ def valid?(eckey = nil)
24
+ eckey ||= ec_key
25
+ eckey.nil? ? false : eckey.check_key
26
+ end
27
+
28
+ def to_bytes
29
+ hex_to_bytes to_hex
30
+ end
31
+
32
+ def to_i
33
+ bytes_to_int to_bytes
34
+ end
35
+ end
36
+
37
+ class PrivateKey < Key
38
+
39
+ def initialize(opts = {})
40
+ @options = opts
41
+ @ec_key = PKey::EC.new GROUP_NAME
42
+ if @options[:key]
43
+ @raw_key = @options[:key]
44
+ @key = parse_raw_key
45
+ import
46
+ else
47
+ generate
48
+ @key = to_hex
49
+ end
50
+ end
51
+
52
+ def generate
53
+ ec_key.generate_key
54
+ end
55
+
56
+ def import
57
+ ec_key.private_key = BN.new(key, 16)
58
+ set_public_key
59
+ end
60
+
61
+ def calculate_public_key(opts = {})
62
+ opts[:compressed] = true unless opts[:compressed] == false
63
+ group = ec_key.group
64
+ group.point_conversion_form = opts[:compressed] ? :compressed : :uncompressed
65
+ point = group.generator.mul ec_key.private_key
66
+ end
67
+
68
+ def set_public_key(opts = {})
69
+ ec_key.public_key = calculate_public_key(opts)
70
+ end
71
+
72
+ def parse_raw_key
73
+ result = if raw_key.is_a?(Integer) then from_integer
74
+ elsif hex_format? then from_hex
75
+ elsif base64_format? then from_base64
76
+ elsif compressed_wif_format? then from_wif
77
+ elsif uncompressed_wif_format? then from_wif
78
+ else
79
+ raise KeyFormatNotFound
80
+ end
81
+ result.downcase
82
+ end
83
+
84
+ def from_integer(bignum = raw_key)
85
+ # TODO: does this need a byte size specification?
86
+ int_to_hex(bignum)
87
+ end
88
+
89
+ def from_hex(hex = raw_key)
90
+ hex
91
+ end
92
+
93
+ def from_wif(wif = raw_key)
94
+ compressed = wif.length == 52
95
+ validate_wif(wif)
96
+ hex = decode_base58(wif)
97
+ last_char = compressed ? -11 : -9
98
+ hex.slice(2..last_char)
99
+ end
100
+
101
+ def from_base64(base64_key = raw_key)
102
+ raise InvalidBase64Format unless base64_format?(base64_key)
103
+ decode_base64(base64_key)
104
+ end
105
+
106
+ def compressed_wif_format?
107
+ wif_format?(:compressed)
108
+ end
109
+
110
+ def uncompressed_wif_format?
111
+ wif_format?(:uncompressed)
112
+ end
113
+
114
+ def wif_format?(compression)
115
+ length = compression == :compressed ? 52 : 51
116
+ wif_prefixes = MoneyTree::NETWORKS.map {|k, v| v["#{compression}_wif_chars".to_sym]}.flatten
117
+ raw_key.length == length && wif_prefixes.include?(raw_key.slice(0))
118
+ end
119
+
120
+ def base64_format?(base64_key = raw_key)
121
+ base64_key.length == 44 && base64_key =~ /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
122
+ end
123
+
124
+ def hex_format?
125
+ raw_key.length == 64 && !raw_key[/\H/]
126
+ end
127
+
128
+ def to_hex
129
+ int_to_hex @ec_key.private_key, 64
130
+ end
131
+
132
+ def to_wif(compressed: true, network: :bitcoin)
133
+ source = NETWORKS[network][:privkey_version] + to_hex
134
+ source += NETWORKS[network][:privkey_compression_flag] if compressed
135
+ hash = sha256(source)
136
+ hash = sha256(hash)
137
+ checksum = hash.slice(0..7)
138
+ source_with_checksum = source + checksum
139
+ encode_base58(source_with_checksum)
140
+ end
141
+
142
+ def wif_valid?(wif)
143
+ hex = decode_base58(wif)
144
+ checksum = hex.chars.to_a.pop(8).join
145
+ source = hex.slice(0..-9)
146
+ hash = sha256(source)
147
+ hash = sha256(hash)
148
+ hash_checksum = hash.slice(0..7)
149
+ checksum == hash_checksum
150
+ end
151
+
152
+ def validate_wif(wif)
153
+ raise InvalidWIFFormat unless wif_valid?(wif)
154
+ end
155
+
156
+ def to_base64
157
+ encode_base64(to_hex)
158
+ end
159
+
160
+ def to_s(network: :bitcoin)
161
+ to_wif(network: network)
162
+ end
163
+
164
+ end
165
+
166
+ class PublicKey < Key
167
+ attr_reader :private_key, :point, :group, :key_int
168
+
169
+ def initialize(p_key, opts = {})
170
+ @options = opts
171
+ @options[:compressed] = true if @options[:compressed].nil?
172
+ if p_key.is_a?(PrivateKey)
173
+ @private_key = p_key
174
+ @point = @private_key.calculate_public_key(@options)
175
+ @group = @point.group
176
+ @key = @raw_key = to_hex
177
+ else
178
+ @raw_key = p_key
179
+ @group = PKey::EC::Group.new GROUP_NAME
180
+ @key = parse_raw_key
181
+ end
182
+
183
+ raise ArgumentError, "Must initialize with a MoneyTree::PrivateKey or a public key value" if @key.nil?
184
+ end
185
+
186
+ def compression
187
+ @group.point_conversion_form
188
+ end
189
+
190
+ def compression=(compression_type = :compressed)
191
+ @group.point_conversion_form = compression_type
192
+ end
193
+
194
+ def compressed
195
+ compressed_key = self.class.new raw_key, options # deep clone
196
+ compressed_key.set_point to_i, compressed: true
197
+ compressed_key
198
+ end
199
+
200
+ def uncompressed
201
+ uncompressed_key = self.class.new raw_key, options # deep clone
202
+ uncompressed_key.set_point to_i, compressed: false
203
+ uncompressed_key
204
+ end
205
+
206
+ def set_point(int = to_i, opts = {})
207
+ opts = options.merge(opts)
208
+ opts[:compressed] = true if opts[:compressed].nil?
209
+ self.compression = opts[:compressed] ? :compressed : :uncompressed
210
+ bn = BN.new int_to_hex(int), 16
211
+ @point = PKey::EC::Point.new group, bn
212
+ raise KeyInvalid, 'point is not on the curve' unless @point.on_curve?
213
+ end
214
+
215
+ def parse_raw_key
216
+ result = if raw_key.is_a?(Integer)
217
+ set_point raw_key
218
+ elsif hex_format?
219
+ set_point hex_to_int(raw_key), compressed: false
220
+ elsif compressed_hex_format?
221
+ set_point hex_to_int(raw_key), compressed: true
222
+ else
223
+ raise KeyFormatNotFound
224
+ end
225
+ to_hex
226
+ end
227
+
228
+ def hex_format?
229
+ raw_key.length == 130 && !raw_key[/\H/]
230
+ end
231
+
232
+ def compressed_hex_format?
233
+ raw_key.length == 66 && !raw_key[/\H/]
234
+ end
235
+
236
+ def to_hex
237
+ int_to_hex to_i, 66
238
+ end
239
+
240
+ def to_i
241
+ point.to_bn.to_i
242
+ end
243
+
244
+ def to_ripemd160
245
+ hash = sha256 to_hex
246
+ ripemd160 hash
247
+ end
248
+
249
+ def to_address(network: :bitcoin)
250
+ hash = to_ripemd160
251
+ address = NETWORKS[network][:address_version] + hash
252
+ to_serialized_base58 address
253
+ end
254
+ alias :to_s :to_address
255
+
256
+ def to_fingerprint
257
+ hash = to_ripemd160
258
+ hash.slice(0..7)
259
+ end
260
+
261
+ def to_bytes
262
+ int_to_bytes to_i
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,71 @@
1
+ module MoneyTree
2
+ NETWORKS =
3
+ begin
4
+ hsh = Hash.new do |_, key|
5
+ raise "#{key} is not a valid network!"
6
+ end.merge(
7
+ bitcoin: {
8
+ address_version: '00',
9
+ p2sh_version: '05',
10
+ p2sh_char: '3',
11
+ privkey_version: '80',
12
+ privkey_compression_flag: '01',
13
+ extended_privkey_version: "0488ade4",
14
+ extended_pubkey_version: "0488b21e",
15
+ compressed_wif_chars: %w(K L),
16
+ uncompressed_wif_chars: %w(5),
17
+ protocol_version: 70001
18
+ },
19
+ bitcoin_testnet: {
20
+ address_version: '6f',
21
+ p2sh_version: 'c4',
22
+ p2sh_char: '2',
23
+ privkey_version: 'ef',
24
+ privkey_compression_flag: '01',
25
+ extended_privkey_version: "04358394",
26
+ extended_pubkey_version: "043587cf",
27
+ compressed_wif_chars: %w(c),
28
+ uncompressed_wif_chars: %w(9),
29
+ protocol_version: 70001
30
+ },
31
+ zcoin: {
32
+ address_version: '6f',
33
+ p2sh_version: 'c4',
34
+ p2sh_char: 'a',
35
+ privkey_version: 'ef',
36
+ privkey_compression_flag: '01',
37
+ extended_privkey_version: "04358394",
38
+ extended_pubkey_version: "043587cf",
39
+ compressed_wif_chars: %w(K L),
40
+ uncompressed_wif_chars: %w(5),
41
+ protocol_version: 70001
42
+ },
43
+ litecoin: {
44
+ address_version: '30',
45
+ p2sh_version: '32',
46
+ p2sh_char: 'L',
47
+ privkey_version: '80',
48
+ privkey_compression_flag: '01',
49
+ extended_privkey_version: "0488ade4",
50
+ extended_pubkey_version: "0488b21e",
51
+ compressed_wif_chars: %w(K L),
52
+ uncompressed_wif_chars: %w(5),
53
+ protocol_version: 70001
54
+ },
55
+ bitcoin_cash: {
56
+ address_version: '00',
57
+ p2sh_version: '05',
58
+ p2sh_char: '3',
59
+ privkey_version: '80',
60
+ privkey_compression_flag: '01',
61
+ extended_privkey_version: "0488ade4",
62
+ extended_pubkey_version: "0488b21e",
63
+ compressed_wif_chars: %w(K L),
64
+ uncompressed_wif_chars: %w(5),
65
+ protocol_version: 70001
66
+ }
67
+ )
68
+ hsh[:testnet3] = hsh[:bitcoin_testnet]
69
+ hsh
70
+ end
71
+ end
@@ -0,0 +1,297 @@
1
+ module MoneyTree
2
+ class Node
3
+ include Support
4
+ extend Support
5
+ attr_reader :private_key, :public_key, :chain_code,
6
+ :is_private, :depth, :index, :parent
7
+
8
+ class PublicDerivationFailure < StandardError; end
9
+ class InvalidKeyForIndex < StandardError; end
10
+ class ImportError < StandardError; end
11
+ class PrivatePublicMismatch < StandardError; end
12
+
13
+ def initialize(opts = {})
14
+ opts.each { |k, v| instance_variable_set "@#{k}", v }
15
+ end
16
+
17
+ def self.from_bip32(address, has_version: true)
18
+ hex = from_serialized_base58 address
19
+ hex.slice!(0..7) if has_version
20
+ self.new({
21
+ depth: hex.slice!(0..1).to_i(16),
22
+ parent_fingerprint: hex.slice!(0..7),
23
+ index: hex.slice!(0..7).to_i(16),
24
+ chain_code: hex.slice!(0..63).to_i(16)
25
+ }.merge(parse_out_key(hex)))
26
+ end
27
+
28
+ def self.from_serialized_address(address)
29
+ puts 'Node.from_serialized_address is DEPRECATED. Please use .from_bip32 instead.'
30
+ from_bip32(address)
31
+ end
32
+
33
+ def self.parse_out_key(hex)
34
+ if hex.slice(0..1) == '00'
35
+ private_key = MoneyTree::PrivateKey.new(key: hex.slice(2..-1))
36
+ {
37
+ private_key: private_key,
38
+ public_key: MoneyTree::PublicKey.new(private_key)
39
+ }
40
+ elsif %w(02 03).include? hex.slice(0..1)
41
+ { public_key: MoneyTree::PublicKey.new(hex) }
42
+ else
43
+ raise ImportError, 'Public or private key data does not match version type'
44
+ end
45
+ end
46
+
47
+ def is_private?
48
+ index >= 0x80000000 || index < 0
49
+ end
50
+
51
+ def index_hex(i = index)
52
+ if i < 0
53
+ [i].pack('l>').unpack('H*').first
54
+ else
55
+ i.to_s(16).rjust(8, "0")
56
+ end
57
+ end
58
+
59
+ def depth_hex(depth)
60
+ depth.to_s(16).rjust(2, "0")
61
+ end
62
+
63
+ def private_derivation_message(i)
64
+ "\x00" + private_key.to_bytes + i_as_bytes(i)
65
+ end
66
+
67
+ def public_derivation_message(i)
68
+ public_key.to_bytes << i_as_bytes(i)
69
+ end
70
+
71
+ def i_as_bytes(i)
72
+ [i].pack('N')
73
+ end
74
+
75
+ def derive_private_key(i = 0)
76
+ message = i >= 0x80000000 || i < 0 ? private_derivation_message(i) : public_derivation_message(i)
77
+ hash = hmac_sha512 hex_to_bytes(chain_code_hex), message
78
+ left_int = left_from_hash(hash)
79
+ raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability
80
+ child_private_key = (left_int + private_key.to_i) % MoneyTree::Key::ORDER
81
+ raise InvalidKeyForIndex, 'equal to zero' if child_private_key == 0 # very low probability
82
+ child_chain_code = right_from_hash(hash)
83
+ return child_private_key, child_chain_code
84
+ end
85
+
86
+ def derive_public_key(i = 0)
87
+ raise PrivatePublicMismatch if i >= 0x80000000
88
+ message = public_derivation_message(i)
89
+ hash = hmac_sha512 hex_to_bytes(chain_code_hex), message
90
+ left_int = left_from_hash(hash)
91
+ raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability
92
+ factor = BN.new left_int.to_s
93
+ child_public_key = public_key.uncompressed.group.generator.mul(factor).add(public_key.uncompressed.point).to_bn.to_i
94
+ raise InvalidKeyForIndex, 'at infinity' if child_public_key == 1/0.0 # very low probability
95
+ child_chain_code = right_from_hash(hash)
96
+ return child_public_key, child_chain_code
97
+ end
98
+
99
+ def left_from_hash(hash)
100
+ bytes_to_int hash.bytes.to_a[0..31]
101
+ end
102
+
103
+ def right_from_hash(hash)
104
+ bytes_to_int hash.bytes.to_a[32..-1]
105
+ end
106
+
107
+ def to_serialized_hex(type = :public, network: :bitcoin)
108
+ raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
109
+ version_key = type.to_sym == :private ? :extended_privkey_version : :extended_pubkey_version
110
+ hex = NETWORKS[network][version_key] # version (4 bytes)
111
+ hex += depth_hex(depth) # depth (1 byte)
112
+ hex += parent_fingerprint # fingerprint of key (4 bytes)
113
+ hex += index_hex(index) # child number i (4 bytes)
114
+ hex += chain_code_hex
115
+ hex += type.to_sym == :private ? "00#{private_key.to_hex}" : public_key.compressed.to_hex
116
+ end
117
+
118
+ def to_bip32(type = :public, network: :bitcoin)
119
+ raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
120
+ to_serialized_base58 to_serialized_hex(type, network: network)
121
+ end
122
+
123
+ def to_serialized_address(type = :public, network: :bitcoin)
124
+ puts 'Node.to_serialized_address is DEPRECATED. Please use .to_bip32.'
125
+ to_bip32(type, network: network)
126
+ end
127
+
128
+ def to_identifier(compressed=true)
129
+ key = compressed ? public_key.compressed : public_key.uncompressed
130
+ key.to_ripemd160
131
+ end
132
+
133
+ def to_fingerprint
134
+ public_key.compressed.to_fingerprint
135
+ end
136
+
137
+ def parent_fingerprint
138
+ if @parent_fingerprint
139
+ @parent_fingerprint
140
+ else
141
+ depth.zero? ? '00000000' : parent.to_fingerprint
142
+ end
143
+ end
144
+
145
+ def to_address(compressed=true, network: :bitcoin)
146
+ address = NETWORKS[network][:address_version] + to_identifier(compressed)
147
+ to_serialized_base58 address
148
+ end
149
+
150
+ def subnode(i = 0, opts = {})
151
+ if private_key.nil?
152
+ child_public_key, child_chain_code = derive_public_key(i)
153
+ child_public_key = MoneyTree::PublicKey.new child_public_key
154
+ else
155
+ child_private_key, child_chain_code = derive_private_key(i)
156
+ child_private_key = MoneyTree::PrivateKey.new key: child_private_key
157
+ child_public_key = MoneyTree::PublicKey.new child_private_key
158
+ end
159
+
160
+ MoneyTree::Node.new( depth: depth+1,
161
+ index: i,
162
+ private_key: private_key.nil? ? nil : child_private_key,
163
+ public_key: child_public_key,
164
+ chain_code: child_chain_code,
165
+ parent: self)
166
+ end
167
+
168
+ # path: a path of subkeys denoted by numbers and slashes. Use
169
+ # p or i<0 for private key derivation. End with .pub to force
170
+ # the key public.
171
+ #
172
+ # Examples:
173
+ # 1p/-5/2/1 would call subkey(i=1, is_prime=True).subkey(i=-5).
174
+ # subkey(i=2).subkey(i=1) and then yield the private key
175
+ # 0/0/458.pub would call subkey(i=0).subkey(i=0).subkey(i=458) and
176
+ # then yield the public key
177
+ #
178
+ # You should choose either the p or the negative number convention for private key derivation.
179
+ def node_for_path(path)
180
+ force_public = path[-4..-1] == '.pub'
181
+ path = path[0..-5] if force_public
182
+ parts = path.split('/')
183
+ nodes = []
184
+ parts.each_with_index do |part, depth|
185
+ if part =~ /m/i
186
+ nodes << self
187
+ else
188
+ i = parse_index(part)
189
+ node = nodes.last || self
190
+ nodes << node.subnode(i)
191
+ end
192
+ end
193
+ if force_public or parts.first == 'M'
194
+ node = nodes.last
195
+ node.strip_private_info!
196
+ node
197
+ else
198
+ nodes.last
199
+ end
200
+ end
201
+
202
+ def parse_index(path_part)
203
+ is_prime = %w(p ').include? path_part[-1]
204
+ i = path_part.to_i
205
+
206
+ i = if i < 0
207
+ i
208
+ elsif is_prime
209
+ i | 0x80000000
210
+ else
211
+ i & 0x7fffffff
212
+ end
213
+ end
214
+
215
+ def strip_private_info!
216
+ @private_key = nil
217
+ end
218
+
219
+ def chain_code_hex
220
+ int_to_hex chain_code, 64
221
+ end
222
+ end
223
+
224
+ class Master < Node
225
+ module SeedGeneration
226
+ class Failure < Exception; end
227
+ class RNGFailure < Failure; end
228
+ class LengthFailure < Failure; end
229
+ class ValidityError < Failure; end
230
+ class ImportError < Failure; end
231
+ class TooManyAttempts < Failure; end
232
+ end
233
+
234
+ HD_WALLET_BASE_KEY = "Bitcoin seed"
235
+ RANDOM_SEED_SIZE = 32
236
+
237
+ attr_reader :seed, :seed_hash
238
+
239
+ def initialize(opts = {})
240
+ @depth = 0
241
+ @index = 0
242
+ opts[:seed] = [opts[:seed_hex]].pack("H*") if opts[:seed_hex]
243
+ if opts[:seed]
244
+ @seed = opts[:seed]
245
+ @seed_hash = generate_seed_hash(@seed)
246
+ raise SeedGeneration::ImportError unless seed_valid?(@seed_hash)
247
+ set_seeded_keys
248
+ elsif opts[:private_key] || opts[:public_key]
249
+ raise ImportError, 'chain code required' unless opts[:chain_code]
250
+ @chain_code = opts[:chain_code]
251
+ if opts[:private_key]
252
+ @private_key = opts[:private_key]
253
+ @public_key = MoneyTree::PublicKey.new @private_key
254
+ else opts[:public_key]
255
+ @public_key = if opts[:public_key].is_a?(MoneyTree::PublicKey)
256
+ opts[:public_key]
257
+ else
258
+ MoneyTree::PublicKey.new(opts[:public_key])
259
+ end
260
+ end
261
+ else
262
+ generate_seed
263
+ set_seeded_keys
264
+ end
265
+ end
266
+
267
+ def is_private?
268
+ true
269
+ end
270
+
271
+ def generate_seed
272
+ @seed = OpenSSL::Random.random_bytes(32)
273
+ @seed_hash = generate_seed_hash(@seed)
274
+ raise SeedGeneration::ValidityError unless seed_valid?(@seed_hash)
275
+ end
276
+
277
+ def generate_seed_hash(seed)
278
+ hmac_sha512 HD_WALLET_BASE_KEY, seed
279
+ end
280
+
281
+ def seed_valid?(seed_hash)
282
+ return false unless seed_hash.bytesize == 64
283
+ master_key = left_from_hash(seed_hash)
284
+ !master_key.zero? && master_key < MoneyTree::Key::ORDER
285
+ end
286
+
287
+ def set_seeded_keys
288
+ @private_key = MoneyTree::PrivateKey.new key: left_from_hash(seed_hash)
289
+ @chain_code = right_from_hash(seed_hash)
290
+ @public_key = MoneyTree::PublicKey.new @private_key
291
+ end
292
+
293
+ def seed_hex
294
+ bytes_to_hex(seed)
295
+ end
296
+ end
297
+ end