derivator 0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6fb64625fb015d83f804ddd0323ebc07825ea569dd1a1f2c739d7e8e143b7e65
4
+ data.tar.gz: 5f3519ad198b4ca049be879525df4e0d987a37f4a56b4969871188b007b132c3
5
+ SHA512:
6
+ metadata.gz: e3e67c14744d3c3173178f17c6a6003694abc095b3a10a78a40c42f16f78fd1e8d25e937df05ebd793b4d4d6bd01013a5e923ad3508e23e0eae5106e7e25507e
7
+ data.tar.gz: 1abba063b0c1dfbbeb38e7f0db23d87da0179d021f25e0003dee1df0ffd9255504fbdfef34245eb4b9809d83cd8c9591b6f71ea0030a2ae38d158a22baeef011
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in derivator.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~> 13.0'
7
+
8
+ gem 'rspec', '~> 3.0'
data/Gemfile.lock ADDED
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ derivator (0.1)
5
+ openssl (>= 3.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.5.0)
11
+ openssl (3.0.0)
12
+ rake (13.0.6)
13
+ rspec (3.11.0)
14
+ rspec-core (~> 3.11.0)
15
+ rspec-expectations (~> 3.11.0)
16
+ rspec-mocks (~> 3.11.0)
17
+ rspec-core (3.11.0)
18
+ rspec-support (~> 3.11.0)
19
+ rspec-expectations (3.11.0)
20
+ diff-lcs (>= 1.2.0, < 2.0)
21
+ rspec-support (~> 3.11.0)
22
+ rspec-mocks (3.11.1)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.11.0)
25
+ rspec-support (3.11.0)
26
+ webrick (1.7.0)
27
+ yard (0.9.28)
28
+ webrick (~> 1.7.0)
29
+
30
+ PLATFORMS
31
+ ruby
32
+ x86_64-linux
33
+
34
+ DEPENDENCIES
35
+ derivator!
36
+ rake (~> 13.0)
37
+ rspec (~> 3.0)
38
+ yard
39
+
40
+ BUNDLED WITH
41
+ 2.2.22
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 WAGMI LTD.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # Derivator
2
+
3
+ Ruby implementation of EC HD key derivation ([SLIP10](https://github.com/satoshilabs/slips/blob/master/slip-0010.md), [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)) and mnemonic sentence interpretation ([BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)).
4
+
5
+ Supports secp256k1 (Bitcoin), nist256p1 (P-256) and ed25519 (Edwards 25519) elliptic curves.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'derivator'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install derivator
22
+
23
+ ## Usage
24
+
25
+ ### Generate mnemonic
26
+
27
+ ```bash
28
+ # bash
29
+ derivator_mnemonic
30
+ ```
31
+
32
+ ```ruby
33
+ # ruby
34
+ require 'derivator'
35
+
36
+ puts Derivator::Mnemonic.generate
37
+ ```
38
+
39
+ ### Generate seed from mnemonic
40
+
41
+ ```bash
42
+ # bash
43
+ echo "finish merry file canoe cruel meadow spoil sunset pigeon depend brush step" | \
44
+ derivator_seed_from_mnemonic
45
+
46
+ echo "spike kit woman maze culture uncle way tobacco saddle silly sunset certain" | \
47
+ derivator_seed_from_mnemonic "my_mnemonic_password"
48
+ ```
49
+
50
+ ```ruby
51
+ # ruby
52
+ require 'derivator'
53
+
54
+ mnemonic = 'finish merry file canoe cruel meadow spoil sunset pigeon depend brush step'
55
+ puts Derivator::Mnemonic.seed(mnemonic)
56
+
57
+ mnemonic = 'spike kit woman maze culture uncle way tobacco saddle silly sunset certain'
58
+ puts Derivator::Mnemonic.seed(mnemonic, 'my_mnemonic_password')
59
+ ```
60
+
61
+ ### Generate master key and chain code from seed
62
+
63
+ ```bash
64
+ # bash
65
+ echo 000102030405060708090a0b0c0d0e0f | derivator_key_from_seed ed25519
66
+ ```
67
+
68
+ ```ruby
69
+ # ruby
70
+ require 'derivator'
71
+
72
+ seed = '000102030405060708090a0b0c0d0e0f'
73
+ key = Derivator::Key.from_seed(seed, :ed25519)
74
+ puts "#{key.private_key_hex} #{key.chain_code_hex}"
75
+ ```
76
+
77
+ ### Derive child key from master key and chain code
78
+
79
+ ```bash
80
+ # bash
81
+ echo 2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7 90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb \
82
+ derivator_key_from_parent "m/0'/1'/2'/2'/1000000000'" ed25519
83
+ ```
84
+
85
+ ```ruby
86
+ # ruby
87
+ require 'derivator'
88
+
89
+ master_private_key_hex = '2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7'
90
+ master_chain_code_hex = '90046a93de5380a72b5e45010748567d5ea02bbf6522f979e05c0d8d8ca9fffb'
91
+ key = Derivator::Key.from_hex(master_private_key_hex, master_chain_code_hex, :ed25519)
92
+ derived_key = key.derive("m/0'/1'/2'/2'/1000000000'")
93
+ puts "#{derived_key.private_key_hex} #{derived_key.chain_code_hex}"
94
+ ```
95
+
96
+ ### Derive public key from private key
97
+
98
+ ```bash
99
+ # bash
100
+ echo 2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7 | \
101
+ derivator_public_from_private ed25519
102
+ ```
103
+
104
+ ```ruby
105
+ # ruby
106
+ require 'derivator'
107
+
108
+ private_key_hex = '2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7'
109
+ key = Derivator::Key.from_hex(private_key_hex, '', :ed25519)
110
+ puts key.public_key_hex
111
+ ```
112
+
113
+ ## License
114
+
115
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'derivator'
5
+ require 'derivator/refinements'
6
+
7
+ include Derivator
8
+
9
+ using Refinements
10
+
11
+ require 'irb'
12
+ IRB.start(__FILE__)
data/derivator.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'derivator'
3
+ spec.version = '0.1'
4
+ spec.authors = ['WAGMI LTD.']
5
+ spec.email = ['debifi@debifi.com']
6
+
7
+ spec.summary = 'BIP-0039, BIP-0032, SLIP-0010 elliptic curve (P-256, ED25519, SECP256K1) keys derivator'
8
+ spec.homepage = 'https://gitlab.com/debifi-public/derivator'
9
+ spec.license = 'MIT'
10
+ spec.required_ruby_version = '>= 2.4.0'
11
+
12
+ spec.metadata['homepage_uri'] = spec.homepage
13
+ spec.metadata['source_code_uri'] = spec.homepage
14
+
15
+ # Specify which files should be added to the gem when it is released.
16
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
17
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'yard'
25
+
26
+ spec.add_dependency 'openssl', '>= 3.0'
27
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ path = ARGV.first
13
+ curve = ARGV[1] || 'secp256k1'
14
+ private_key_hex, chain_code_hex = STDIN.gets.split
15
+ key = Derivator::Key.from_hex(private_key_hex, chain_code_hex, curve.to_sym)
16
+ derived_key = key.derive(path)
17
+ puts "#{derived_key.private_key_hex} #{derived_key.chain_code_hex}"
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ curve = ARGV.first || 'secp256k1'
13
+ seed_hex = STDIN.gets.chomp
14
+ key = Derivator::Key.from_seed(seed_hex, curve.to_sym)
15
+ puts "#{key.private_key_hex} #{key.chain_code_hex}"
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ puts Derivator::Mnemonic.generate
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ private_key = STDIN.gets.split.first
13
+ curve = ARGV.first || 'secp256k1'
14
+ key = Derivator::Key.from_hex(private_key, '', curve.to_sym)
15
+ puts key.public_key_hex
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ git_path = File.expand_path("../.git", __dir__)
4
+
5
+ if File.exist?(git_path)
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $:.unshift(lib_path)
8
+ end
9
+
10
+ require 'derivator'
11
+
12
+ mnemonic = STDIN.gets
13
+ password = ARGV.first
14
+ puts Derivator::Mnemonic.seed(mnemonic, password)
@@ -0,0 +1,300 @@
1
+ require_relative 'refinements'
2
+
3
+ module Derivator
4
+ # {https://github.com/satoshilabs/slips/blob/master/slip-0010.md SLIP10} key derivation.
5
+ class Key
6
+ # secp256k1 EC private key binary prefix when DER-encoded
7
+ SECP256K1_DER_PRIVATE_PREFIX = '303e020100301006072a8648ce3d020106052b8104000a042730250201010420'
8
+ # nist256p1 EC private key binary prefix when DER-encoded
9
+ NIST256P1_DER_PRIVATE_PREFIX = '3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420'
10
+ # ed25519 EC private key binary prefix when DER-encoded
11
+ ED25519_DER_PRIVATE_PREFIX = '302e020100300506032b657004220420'
12
+ # ed25519 EC public key binary prefix when DER-encoded
13
+ ED25519_DER_PUBLIC_PREFIX = '302a300506032b65700321'
14
+
15
+ # secp256k1 BIP32/SLIP10 seed key
16
+ SECP256K1_SEED_KEY = 'Bitcoin seed'
17
+ # nist256p1 SLIP10 seed key
18
+ NIST256P1_SEED_KEY = 'Nist256p1 seed'
19
+ # ed25519 SLIP10 seed key
20
+ ED25519_SEED_KEY = 'ed25519 seed'
21
+
22
+ # secp256k1 largest valid private key
23
+ SECP256K1_LARGEST_KEY = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140
24
+ # nist256p1 largest valid private key
25
+ NIST256P1_LARGEST_KEY = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
26
+ # ed25519 largest valid private key (unlimited)
27
+ ED25519_LARGEST_KEY = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
28
+
29
+ # @return [Symbol] EC curve used for the key (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>).
30
+ attr_reader :curve
31
+
32
+ # @return [String] private key (in binary format).
33
+ attr_reader :private_key
34
+
35
+ # @return [String] chain code (in binary format).
36
+ attr_reader :chain_code
37
+
38
+ using Refinements
39
+
40
+ # Creates new key from private key and chain code hex strings.
41
+ #
42
+ # @param private_key_hex [String] private key hex string
43
+ # @param chain_code_hex [String] chain code hex string
44
+ # @param curve [Symbol] curve (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>)
45
+ # @return [Key] new key
46
+ def self.from_hex(private_key_hex, chain_code_hex, curve = :secp256k1)
47
+ new(private_key_hex.from_hex, chain_code_hex.from_hex, curve)
48
+ end
49
+
50
+ # Creates new key from seed hex string.
51
+ #
52
+ # @param seed_hex [String] seed hex string
53
+ # @param curve [Symbol] curve (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>)
54
+ # @return [Key] new key
55
+ def self.from_seed(seed_hex, curve = :secp256k1)
56
+ seed_bytes = seed_hex.from_hex
57
+ seed_key =
58
+ case curve
59
+ when :secp256k1
60
+ SECP256K1_SEED_KEY
61
+ when :nist256p1
62
+ NIST256P1_SEED_KEY
63
+ when :ed25519
64
+ ED25519_SEED_KEY
65
+ end
66
+
67
+ hmac =
68
+ OpenSSL::HMAC.hexdigest(
69
+ "SHA512",
70
+ seed_key,
71
+ seed_bytes
72
+ )
73
+
74
+ private_key_hex = hmac[0..63]
75
+ chain_code_hex = hmac[64..-1]
76
+ if valid_private_key?(private_key_hex, curve)
77
+ from_hex(private_key_hex, chain_code_hex, curve)
78
+ else
79
+ from_seed(hmac, curve)
80
+ end
81
+ end
82
+
83
+ # Checks private key for a particular curve (used internally).
84
+ # Primarily checks whether key value is less than order of the curve.
85
+ #
86
+ # @param private_key_hex [String] private key hex string
87
+ # @param curve [Symbol] curve (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>)
88
+ # @return [true, false] whether the key is valid
89
+ def self.valid_private_key?(private_key_hex, curve = :secp256k1)
90
+ return false unless private_key_hex =~ /\A[a-f0-9]{64}\z/
91
+
92
+ largest_key =
93
+ case curve
94
+ when :secp256k1
95
+ SECP256K1_LARGEST_KEY
96
+ when :nist256p1
97
+ NIST256P1_LARGEST_KEY
98
+ when :ed25519
99
+ ED25519_LARGEST_KEY
100
+ end
101
+ private_key = private_key_hex.to_i(16)
102
+ private_key <= largest_key && (private_key > 0 || curve == :ed25519)
103
+ end
104
+
105
+ # Creates private key from binary data.
106
+ # Use {.from_hex} or {.from_seed} instead for convenience.
107
+ #
108
+ # @param private_key [String] private key (in binary format)
109
+ # @param chain_code [String] chain_code (in binary format)
110
+ # @param curve [Symbol] curve (<code>:secp256k1</code>, <code>:nist256p1</code> or <code>:ed25519</code>)
111
+ def initialize(private_key, chain_code, curve = :secp256k1)
112
+ @private_key = private_key.dup.freeze
113
+ @chain_code = chain_code.dup.freeze
114
+
115
+ unless %i[secp256k1 nist256p1 ed25519].include?(curve)
116
+ raise ArgumentError.new('curve must be :secp256k1, :nist256p1 or :ed25519')
117
+ end
118
+
119
+ @curve = curve
120
+ end
121
+
122
+ # Compares with another {Key}.
123
+ #
124
+ # @param other [Key] subject of comparison
125
+ # @return [true, false] whether {private_key}, {chain_code} and {curve} of
126
+ # <code>other</code> matches <code>self</code>
127
+ def ==(other)
128
+ return false unless %i[private_key chain_code curve].all? { |m| other.respond_to?(m) }
129
+
130
+ private_key == other.private_key &&
131
+ chain_code == other.chain_code &&
132
+ curve == other.curve
133
+ end
134
+
135
+ # Chain code as hex string.
136
+ #
137
+ # @return [String]
138
+ def chain_code_hex
139
+ @chain_code_hex ||= @chain_code.to_hex.freeze
140
+ end
141
+
142
+ # {https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#key-identifiers BIP32 fingerprint}
143
+ # (first 4 bytes of HASH160 of public key) as hex string.
144
+ #
145
+ # @return [String]
146
+ def fingerprint
147
+ @fingerprint ||= public_key.hash160[0..3].to_hex
148
+ end
149
+
150
+ # Private key as hex string.
151
+ #
152
+ # @return [String]
153
+ def private_key_hex
154
+ @private_key_hex ||= @private_key.to_hex.freeze
155
+ end
156
+
157
+ # Public key (in binary format).
158
+ # Use {public_key_hex} for convenience.
159
+ #
160
+ # @return [String]
161
+ def public_key
162
+ @public_key ||= begin
163
+ case curve
164
+ when :ed25519
165
+ openssl_pkey.
166
+ public_to_der.
167
+ to_hex.
168
+ delete_prefix(ED25519_DER_PUBLIC_PREFIX).
169
+ from_hex.freeze
170
+ else
171
+ openssl_pkey.
172
+ public_key.
173
+ to_octet_string(:compressed).freeze
174
+ end
175
+ end
176
+ end
177
+
178
+ # Public key as hex string.
179
+ #
180
+ # @return [String]
181
+ def public_key_hex
182
+ @public_key_hex ||= public_key.to_hex.freeze
183
+ end
184
+
185
+ # Derive child key.
186
+ #
187
+ # @param path [String] derivation path, e.g. <code>'m/0/1'</code>,
188
+ # <code>'5'</code>, <code>'0/3'</code>, <code>"m/0/3'/5'/1/2"</code>.
189
+ # Use <code>'</code> for hardened keys.
190
+ def derive(path)
191
+ return self if path == 'm' || path.empty?
192
+ path = path.delete_prefix('m').delete_prefix('/')
193
+ path = path.split('/')
194
+
195
+ i_string = path.first
196
+ unless i_string =~ /\A[0-9]+'?\z/
197
+ raise ArgumentError.new("Wrong derivation segment: #{i_string.inspect}")
198
+ end
199
+ i = i_string.to_i
200
+ i += 2**31 if i_string[-1] == "'"
201
+
202
+ if curve == :ed25519 && i < 2**31
203
+ raise ArgumentError.new("Only hardened derivation supported with ED25519, got #{i_string} instead")
204
+ end
205
+
206
+ if curve != :ed25519
207
+ generator = openssl_group.generator
208
+ end
209
+
210
+ data =
211
+ if i >= 2**31
212
+ # Data for HMAC-SHA512(Key = cpar, Data = 0x00 || ser256(kpar) || ser32(i))
213
+ "00" + private_key_hex + ("%08x" % i)
214
+ else
215
+ # Data for HMAC-SHA512(Key = cpar, Data = serP(point(kpar)) || ser32(i))
216
+ generator.mul(private_key_hex.to_i(16)).to_octet_string(:compressed).to_hex + ("%08x" % i)
217
+ end
218
+ data = data.from_hex
219
+
220
+ hmac =
221
+ OpenSSL::HMAC.hexdigest(
222
+ "SHA512",
223
+ chain_code,
224
+ data
225
+ )
226
+ derived_private_key_hex = hmac[0..63]
227
+ derived_chain_code_hex = hmac[64..-1]
228
+
229
+ if curve != :ed25519
230
+ derived_private_key_hex, derived_chain_code_hex =
231
+ finish_derivation(derived_private_key_hex, derived_chain_code_hex, i)
232
+ end
233
+
234
+ new_key = self.class.from_hex(derived_private_key_hex, derived_chain_code_hex, curve)
235
+ new_key.derive(path[1..-1].join('/'))
236
+ end
237
+
238
+ # Exports key to PEM format.
239
+ #
240
+ # @return [String]
241
+ def to_pem
242
+ private_prefix =
243
+ case curve
244
+ when :secp256k1
245
+ SECP256K1_DER_PRIVATE_PREFIX
246
+ when :nist256p1
247
+ NIST256P1_DER_PRIVATE_PREFIX
248
+ when :ed25519
249
+ ED25519_DER_PRIVATE_PREFIX
250
+ end
251
+
252
+ der = (private_prefix + private_key_hex).from_hex
253
+ pem = <<~END
254
+ -----BEGIN PRIVATE KEY-----
255
+ #{der.to_base64.strip}
256
+ -----END PRIVATE KEY-----
257
+ END
258
+ end
259
+
260
+ # Creates {OpenSSL::PKey}[https://ruby-doc.org/stdlib-2.7.4/libdoc/openssl/rdoc/OpenSSL/PKey/EC/Point.html] instance.
261
+ #
262
+ # @return [OpenSSL::PKey::EC::Point]
263
+ def openssl_pkey
264
+ OpenSSL::PKey.read(to_pem)
265
+ end
266
+
267
+ # Creates {OpenSSL::PKey::EC::Group}[https://ruby-doc.org/stdlib-2.7.4/libdoc/openssl/rdoc/OpenSSL/PKey/EC/Group.html] instance for the key's {curve}.
268
+ #
269
+ # @return [OpenSSL::PKey::EC::Group]
270
+ def openssl_group
271
+ case curve
272
+ when :secp256k1
273
+ OpenSSL::PKey::EC::Group.new('secp256k1')
274
+ when :nist256p1
275
+ OpenSSL::PKey::EC::Group.new('prime256v1')
276
+ end
277
+ end
278
+
279
+ private
280
+
281
+ def finish_derivation(i_l_hex, i_r_hex, i)
282
+ group = openssl_group
283
+ i_l = i_l_hex.to_i(16)
284
+ new_key_int = (i_l + private_key_hex.to_i(16)) % group.order
285
+ if i_l >= group.order || new_key_int == 0
286
+ # let I = HMAC-SHA512(Key = cpar, Data = 0x01 || IR || ser32(i) and restart at step 2.
287
+ data = ("01" + i_r_hex + ("%08x" % i)).from_hex
288
+ hmac =
289
+ OpenSSL::HMAC.hexdigest(
290
+ "SHA512",
291
+ chain_code,
292
+ data
293
+ )
294
+ finish_derivation(hmac[0..63], hmac[64..-1], i)
295
+ else
296
+ ["%064x" % new_key_int, i_r_hex]
297
+ end
298
+ end
299
+ end
300
+ end