derivator 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/Rakefile +6 -0
- data/bin/console +12 -0
- data/derivator.gemspec +27 -0
- data/exe/derivator_key_from_parent +17 -0
- data/exe/derivator_key_from_seed +15 -0
- data/exe/derivator_mnemonic +12 -0
- data/exe/derivator_public_from_private +15 -0
- data/exe/derivator_seed_from_mnemonic +14 -0
- data/lib/derivator/key.rb +300 -0
- data/lib/derivator/mnemonic.rb +51 -0
- data/lib/derivator/refinements.rb +37 -0
- data/lib/derivator/word_lists/english.txt +2048 -0
- data/lib/derivator.rb +10 -0
- metadata +98 -0
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
data/.rspec
ADDED
data/Gemfile
ADDED
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
data/bin/console
ADDED
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,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
|