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 +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
|