money-tree-extended 0.11.0

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