skeleton_key 0.1.0 → 0.1.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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +30 -4
- data/lib/skeleton_key/chains/solana/account.rb +68 -7
- data/lib/skeleton_key/keyring.rb +3 -2
- data/lib/skeleton_key/seed.rb +7 -5
- data/lib/skeleton_key/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb63b4370d3a11f016dee71efc6df9516df5fcd436987dfeed4b07c348dd7277
|
|
4
|
+
data.tar.gz: e22d95c59f1b24ab1f8b2532bd55c9cb2407c555cd0e62324e8cd536945caec1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12b8bb96fd60252d45e46859ed505181f150c3c589cde6f8e5ae0120f610a3c204ae4893bd9534af6aafc2ddd3ac37e477d84d59235ce37de1f455e40263315c
|
|
7
|
+
data.tar.gz: bde4855fada160f437f17da16a04eaf6195a543be73fd10d1991ed2e831850f926a99060c23753d8ffaaa54633501327b603aab784eb63f27bb5aa953fd8eadc
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.1] - 2026-03-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added explicit BIP39 passphrase support to `SkeletonKey::Seed.import` and `SkeletonKey::Keyring.new` when recovering from mnemonic input.
|
|
15
|
+
- Added opt-in Solana `derivation_path: nil` support to match the default no-path behavior of `solana-keygen new`.
|
|
16
|
+
- Added Solana CLI-backed golden vectors and integration coverage for the no-path derivation mode.
|
|
17
|
+
- Added this changelog and included it in the packaged gem files.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Documented that BIP39 passphrases are separate from mnemonic words.
|
|
22
|
+
- Documented the Solana default path mode and the opt-in Solana CLI compatibility mode.
|
|
23
|
+
|
|
24
|
+
[Unreleased]: https://github.com/sebscholl/skeleton-key/compare/v0.1.1...HEAD
|
|
25
|
+
[0.1.1]: https://github.com/sebscholl/skeleton-key/releases/tag/v0.1.1
|
data/README.md
CHANGED
|
@@ -30,12 +30,13 @@
|
|
|
30
30
|
██████
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
[](https://badge.fury.io/rb/skeleton_key)
|
|
34
|
+
[](https://github.com/sebscholl/skeleton-key/actions/workflows/release.yml)
|
|
34
35
|
[](LICENSE)
|
|
35
36
|
[](https://www.ruby-lang.org )
|
|
36
37
|
[](SECURITY.md)
|
|
37
38
|
|
|
38
|
-
SkeletonKey is a Ruby library for deterministic wallet recovery and key derivation across Bitcoin, Ethereum, and Solana. It is designed around a strict boundary:
|
|
39
|
+
SkeletonKey is a zero-dependency Ruby library for deterministic wallet recovery and key derivation across Bitcoin, Ethereum, and Solana. It is designed around a strict boundary:
|
|
39
40
|
|
|
40
41
|
- recovery formats in the recovery layer
|
|
41
42
|
- shared seed and derivation primitives in the shared layer
|
|
@@ -91,6 +92,15 @@ Generate a random keyring:
|
|
|
91
92
|
keyring = SkeletonKey::Keyring.new
|
|
92
93
|
```
|
|
93
94
|
|
|
95
|
+
Initialize from a mnemonic with a BIP39 passphrase:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
keyring = SkeletonKey::Keyring.new(
|
|
99
|
+
seed: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
|
100
|
+
passphrase: "TREZOR"
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
94
104
|
Initialize from an existing hex seed:
|
|
95
105
|
|
|
96
106
|
```ruby
|
|
@@ -151,14 +161,18 @@ You can also pass a mnemonic directly to `Seed.import` or `Keyring.new`:
|
|
|
151
161
|
|
|
152
162
|
```ruby
|
|
153
163
|
seed = SkeletonKey::Seed.import(
|
|
154
|
-
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
164
|
+
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
|
165
|
+
passphrase: "TREZOR"
|
|
155
166
|
)
|
|
156
167
|
|
|
157
168
|
keyring = SkeletonKey::Keyring.new(
|
|
158
|
-
seed: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
|
169
|
+
seed: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
|
170
|
+
passphrase: "TREZOR"
|
|
159
171
|
)
|
|
160
172
|
```
|
|
161
173
|
|
|
174
|
+
The BIP39 passphrase is separate from the mnemonic. It is not an extra mnemonic word.
|
|
175
|
+
|
|
162
176
|
What BIP39 validation currently enforces:
|
|
163
177
|
|
|
164
178
|
- official BIP39 word counts only: `12`, `15`, `18`, `21`, `24`
|
|
@@ -436,6 +450,18 @@ Derive deeper hardened children:
|
|
|
436
450
|
node = account.address(change: 0, index: 15)
|
|
437
451
|
```
|
|
438
452
|
|
|
453
|
+
Match the default no-path behavior of `solana-keygen new`:
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
account = keyring.solana(derivation_path: nil)
|
|
457
|
+
node = account.address
|
|
458
|
+
|
|
459
|
+
node[:path] # nil
|
|
460
|
+
node[:private_key] # first 32 bytes of the canonical seed, hex
|
|
461
|
+
node[:public_key] # 32-byte Ed25519 public key, hex
|
|
462
|
+
node[:address] # Base58-encoded Solana address
|
|
463
|
+
```
|
|
464
|
+
|
|
439
465
|
Solana in SkeletonKey is hardened-only. There is no supported unhardened child derivation path.
|
|
440
466
|
|
|
441
467
|
## Architecture
|
|
@@ -26,18 +26,33 @@ module SkeletonKey
|
|
|
26
26
|
|
|
27
27
|
DEFAULT_PURPOSE = 44
|
|
28
28
|
SOLANA_COIN = 501
|
|
29
|
-
|
|
29
|
+
DEFAULT_DERIVATION_PATH = Object.new.freeze
|
|
30
|
+
UNSPECIFIED_CHANGE = Object.new.freeze
|
|
30
31
|
|
|
31
32
|
# @param seed [String] canonical seed bytes
|
|
32
33
|
# @param purpose [Integer] derivation purpose, fixed at 44
|
|
33
34
|
# @param coin_type [Integer] SLIP-0044 coin type, fixed at 501
|
|
34
35
|
# @param account_index [Integer] hardened account index
|
|
36
|
+
# @param derivation_path [nil] set to `nil` to match Solana CLI no-path mode
|
|
35
37
|
# @raise [Errors::UnsupportedPurposeError] if non-Solana path parameters are requested
|
|
36
|
-
def initialize(
|
|
38
|
+
def initialize(
|
|
39
|
+
seed:,
|
|
40
|
+
purpose: DEFAULT_PURPOSE,
|
|
41
|
+
coin_type: SOLANA_COIN,
|
|
42
|
+
account_index: 0,
|
|
43
|
+
derivation_path:
|
|
44
|
+
DEFAULT_DERIVATION_PATH
|
|
45
|
+
)
|
|
37
46
|
@purpose = purpose
|
|
38
47
|
@coin_type = coin_type
|
|
39
48
|
@account_index = account_index
|
|
40
|
-
@derived =
|
|
49
|
+
@derived = derive(
|
|
50
|
+
seed,
|
|
51
|
+
purpose: purpose,
|
|
52
|
+
coin_type: coin_type,
|
|
53
|
+
account: account_index,
|
|
54
|
+
derivation_path: derivation_path
|
|
55
|
+
)
|
|
41
56
|
end
|
|
42
57
|
|
|
43
58
|
# Returns the hardened account prefix for future child derivation.
|
|
@@ -55,9 +70,12 @@ module SkeletonKey
|
|
|
55
70
|
# @param change [Integer, nil] hardened child directly beneath the account
|
|
56
71
|
# @param index [Integer, nil] hardened child beneath the change node
|
|
57
72
|
# @return [Hash] path, private key, public key, address, and chain code
|
|
58
|
-
def address(change:
|
|
73
|
+
def address(change: UNSPECIFIED_CHANGE, index: nil)
|
|
74
|
+
return no_path_address(change: change, index: index) if path.nil?
|
|
75
|
+
|
|
59
76
|
key_seed, chain_code = derived[:key_seed], derived[:chain_code]
|
|
60
77
|
current_path = path
|
|
78
|
+
change = 0 if change.equal?(UNSPECIFIED_CHANGE)
|
|
61
79
|
|
|
62
80
|
unless change.nil?
|
|
63
81
|
current_path = "#{current_path}/#{change}'"
|
|
@@ -73,15 +91,25 @@ module SkeletonKey
|
|
|
73
91
|
|
|
74
92
|
{
|
|
75
93
|
path: current_path,
|
|
76
|
-
|
|
77
|
-
public_key: public_key.unpack1("H*"),
|
|
94
|
+
chain_code: chain_code,
|
|
78
95
|
address: to_address(public_key),
|
|
79
|
-
|
|
96
|
+
public_key: public_key.unpack1("H*"),
|
|
97
|
+
private_key: private_key.unpack1("H*"),
|
|
80
98
|
}
|
|
81
99
|
end
|
|
82
100
|
|
|
83
101
|
private
|
|
84
102
|
|
|
103
|
+
def derive(seed_bytes, purpose:, coin_type:, account:, derivation_path:)
|
|
104
|
+
return derive_without_path(seed_bytes, purpose: purpose, coin_type: coin_type, account: account) if derivation_path.nil?
|
|
105
|
+
|
|
106
|
+
unless derivation_path.equal?(DEFAULT_DERIVATION_PATH)
|
|
107
|
+
raise Errors::InvalidPathFormatError, "unsupported Solana derivation_path override"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
derive_from_seed(seed_bytes, purpose: purpose, coin_type: coin_type, account: account)
|
|
111
|
+
end
|
|
112
|
+
|
|
85
113
|
def derive_from_seed(seed_bytes, purpose:, coin_type:, account:)
|
|
86
114
|
raise Errors::UnsupportedPurposeError.new(purpose) unless purpose == 44
|
|
87
115
|
raise Errors::UnsupportedPurposeError.new(coin_type) unless coin_type == SOLANA_COIN
|
|
@@ -104,6 +132,39 @@ module SkeletonKey
|
|
|
104
132
|
}
|
|
105
133
|
end
|
|
106
134
|
|
|
135
|
+
def derive_without_path(seed_bytes, purpose:, coin_type:, account:)
|
|
136
|
+
unless purpose == DEFAULT_PURPOSE && coin_type == SOLANA_COIN && account.zero?
|
|
137
|
+
raise Errors::InvalidPathFormatError,
|
|
138
|
+
"solana derivation_path=nil is incompatible with purpose, coin_type, or account_index overrides"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
if seed_bytes.bytesize < Constants::PRIVATE_KEY_LENGTH
|
|
142
|
+
raise Errors::InvalidSeedError, "solana derivation_path=nil requires at least 32 seed bytes"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
{
|
|
146
|
+
path_prefix: nil,
|
|
147
|
+
key_seed: seed_bytes.byteslice(0, Constants::PRIVATE_KEY_LENGTH),
|
|
148
|
+
chain_code: nil
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def no_path_address(change:, index:)
|
|
153
|
+
unless (change.equal?(UNSPECIFIED_CHANGE) || change.nil?) && index.nil?
|
|
154
|
+
raise Errors::InvalidPathFormatError, "solana derivation_path=nil does not support child derivation"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private_key, public_key = keypair_from_seed(derived[:key_seed])
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
path: nil,
|
|
161
|
+
private_key: private_key.unpack1("H*"),
|
|
162
|
+
public_key: public_key.unpack1("H*"),
|
|
163
|
+
address: to_address(public_key),
|
|
164
|
+
chain_code: nil
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
107
168
|
# Encodes a child index as a hardened SLIP-0010 index.
|
|
108
169
|
#
|
|
109
170
|
# @param index [Integer]
|
data/lib/skeleton_key/keyring.rb
CHANGED
|
@@ -33,9 +33,10 @@ module SkeletonKey
|
|
|
33
33
|
# Initializes a new Keyring with an optional seed
|
|
34
34
|
#
|
|
35
35
|
# @param seed [String, Seed, Array<Integer>, nil] the seed to initialize the Keyring with (optional)
|
|
36
|
+
# @param passphrase [String] optional BIP39 passphrase when `seed` is a mnemonic
|
|
36
37
|
# @return [Keyring] the initialized Keyring
|
|
37
|
-
def initialize(seed: nil)
|
|
38
|
-
@seed = Seed.import(seed)
|
|
38
|
+
def initialize(seed: nil, passphrase: "")
|
|
39
|
+
@seed = Seed.import(seed, passphrase: passphrase)
|
|
39
40
|
end
|
|
40
41
|
|
|
41
42
|
# Access the Bitcoin account derived from the seed
|
data/lib/skeleton_key/seed.rb
CHANGED
|
@@ -52,15 +52,16 @@ module SkeletonKey
|
|
|
52
52
|
# - array of octets
|
|
53
53
|
#
|
|
54
54
|
# @param value [String, Seed, Recovery::Bip39, Array<Integer>, nil]
|
|
55
|
+
# @param passphrase [String] optional BIP39 passphrase for mnemonic input
|
|
55
56
|
# @return [Seed]
|
|
56
57
|
# @raise [Errors::InvalidSeedError] if the value cannot be normalized
|
|
57
|
-
def import(value)
|
|
58
|
+
def import(value, passphrase: "")
|
|
58
59
|
case
|
|
59
60
|
when value.nil? then generate
|
|
60
61
|
when value.is_a?(Seed) then import_from_seed(value)
|
|
61
|
-
when value.is_a?(Recovery::Bip39) then import_from_mnemonic(value)
|
|
62
|
+
when value.is_a?(Recovery::Bip39) then import_from_mnemonic(value, passphrase: passphrase)
|
|
62
63
|
when hex_string?(value) then import_from_hex(value)
|
|
63
|
-
when mnemonic_string?(value) then import_from_mnemonic(value)
|
|
64
|
+
when mnemonic_string?(value) then import_from_mnemonic(value, passphrase: passphrase)
|
|
64
65
|
when byte_string?(value) then import_from_bytes(value)
|
|
65
66
|
when octet_array?(value) then import_from_octets(value)
|
|
66
67
|
else
|
|
@@ -110,9 +111,10 @@ module SkeletonKey
|
|
|
110
111
|
# Recovers a seed from a BIP39 mnemonic phrase.
|
|
111
112
|
#
|
|
112
113
|
# @param mnemonic [Recovery::Bip39, String]
|
|
114
|
+
# @param passphrase [String] optional BIP39 passphrase
|
|
113
115
|
# @return [Seed]
|
|
114
|
-
def import_from_mnemonic(mnemonic)
|
|
115
|
-
Recovery::Bip39.import(mnemonic).seed
|
|
116
|
+
def import_from_mnemonic(mnemonic, passphrase: "")
|
|
117
|
+
Recovery::Bip39.import(mnemonic).seed(passphrase: passphrase)
|
|
116
118
|
end
|
|
117
119
|
|
|
118
120
|
private
|
data/lib/skeleton_key/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: skeleton_key
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Scholl
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '3.
|
|
18
|
+
version: '3.0'
|
|
19
19
|
type: :development
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '3.
|
|
25
|
+
version: '3.0'
|
|
26
26
|
description: SkeletonKey provides deterministic wallet recovery, seed normalization,
|
|
27
27
|
and key derivation for Bitcoin, Ethereum, and Solana.
|
|
28
28
|
email:
|
|
@@ -34,6 +34,7 @@ executables:
|
|
|
34
34
|
extensions: []
|
|
35
35
|
extra_rdoc_files: []
|
|
36
36
|
files:
|
|
37
|
+
- CHANGELOG.md
|
|
37
38
|
- README.md
|
|
38
39
|
- bin/console
|
|
39
40
|
- bin/lint
|