Implement Bip32 for seed-phrase/passphrase signing (#16942)
* Add Keypair helpers for bip32 derivation * Plumb bip32 for SignerSourceKind::Ask * Support full-path querystring * Use as_ref * Add public wrappers for from_uri cases * Support master root derivations (and fix too-deep print * Add ask:// HD documentation * Update ASK elsewhere in docs
This commit is contained in:
parent
6318705607
commit
694c674aa6
|
@ -853,6 +853,16 @@ dependencies = [
|
||||||
"subtle 2.2.2",
|
"subtle 2.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-mac"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array 0.14.3",
|
||||||
|
"subtle 2.2.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-mac"
|
name = "crypto-mac"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
@ -1094,6 +1104,19 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-dalek-bip32"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "057f328f31294b5ab432e6c39642f54afd1531677d6d4ba2905932844cc242f3"
|
||||||
|
dependencies = [
|
||||||
|
"derivation-path",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"failure",
|
||||||
|
"hmac 0.9.0",
|
||||||
|
"sha2 0.9.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "educe"
|
name = "educe"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
|
@ -1666,6 +1689,16 @@ dependencies = [
|
||||||
"digest 0.9.0",
|
"digest 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-mac 0.9.1",
|
||||||
|
"digest 0.9.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
@ -5092,6 +5125,7 @@ dependencies = [
|
||||||
"derivation-path",
|
"derivation-path",
|
||||||
"digest 0.9.0",
|
"digest 0.9.0",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
"ed25519-dalek-bip32",
|
||||||
"generic-array 0.14.3",
|
"generic-array 0.14.3",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac 0.10.1",
|
"hmac 0.10.1",
|
||||||
|
|
|
@ -57,7 +57,7 @@ pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option<Keypair> {
|
||||||
if let Some(value) = matches.value_of(name) {
|
if let Some(value) = matches.value_of(name) {
|
||||||
if value == ASK_KEYWORD {
|
if value == ASK_KEYWORD {
|
||||||
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
||||||
keypair_from_seed_phrase(name, skip_validation, true).ok()
|
keypair_from_seed_phrase(name, skip_validation, true, None).ok()
|
||||||
} else {
|
} else {
|
||||||
read_keypair_file(value).ok()
|
read_keypair_file(value).ok()
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ pub fn keypairs_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<Keypair>>
|
||||||
.filter_map(|value| {
|
.filter_map(|value| {
|
||||||
if value == ASK_KEYWORD {
|
if value == ASK_KEYWORD {
|
||||||
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
||||||
keypair_from_seed_phrase(name, skip_validation, true).ok()
|
keypair_from_seed_phrase(name, skip_validation, true, None).ok()
|
||||||
} else {
|
} else {
|
||||||
read_keypair_file(value).ok()
|
read_keypair_file(value).ok()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@ use {
|
||||||
message::Message,
|
message::Message,
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::{
|
signature::{
|
||||||
keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair,
|
generate_seed_from_seed_phrase_and_passphrase, keypair_from_seed_and_derivation_path,
|
||||||
read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer,
|
read_keypair, read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
std::{
|
std::{
|
||||||
|
@ -181,14 +181,17 @@ pub(crate) fn parse_signer_source<S: AsRef<str>>(
|
||||||
if let Some(scheme) = uri.scheme() {
|
if let Some(scheme) = uri.scheme() {
|
||||||
let scheme = scheme.as_str().to_ascii_lowercase();
|
let scheme = scheme.as_str().to_ascii_lowercase();
|
||||||
match scheme.as_str() {
|
match scheme.as_str() {
|
||||||
"ask" => Ok(SignerSource::new(SignerSourceKind::Ask)),
|
"ask" => Ok(SignerSource {
|
||||||
|
kind: SignerSourceKind::Ask,
|
||||||
|
derivation_path: DerivationPath::from_uri_any_query(&uri)?,
|
||||||
|
}),
|
||||||
"file" => Ok(SignerSource::new(SignerSourceKind::Filepath(
|
"file" => Ok(SignerSource::new(SignerSourceKind::Filepath(
|
||||||
uri.path().to_string(),
|
uri.path().to_string(),
|
||||||
))),
|
))),
|
||||||
"stdin" => Ok(SignerSource::new(SignerSourceKind::Stdin)),
|
"stdin" => Ok(SignerSource::new(SignerSourceKind::Stdin)),
|
||||||
"usb" => Ok(SignerSource {
|
"usb" => Ok(SignerSource {
|
||||||
kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?),
|
kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?),
|
||||||
derivation_path: DerivationPath::from_uri(&uri)?,
|
derivation_path: DerivationPath::from_uri_key_query(&uri)?,
|
||||||
}),
|
}),
|
||||||
_ => Err(SignerSourceError::UnrecognizedSource),
|
_ => Err(SignerSourceError::UnrecognizedSource),
|
||||||
}
|
}
|
||||||
|
@ -264,6 +267,7 @@ pub fn signer_from_path_with_config(
|
||||||
keypair_name,
|
keypair_name,
|
||||||
skip_validation,
|
skip_validation,
|
||||||
false,
|
false,
|
||||||
|
derivation_path,
|
||||||
)?))
|
)?))
|
||||||
}
|
}
|
||||||
SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
|
SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
|
||||||
|
@ -341,7 +345,7 @@ pub fn resolve_signer_from_path(
|
||||||
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
||||||
// This method validates the seed phrase, but returns `None` because there is no path
|
// This method validates the seed phrase, but returns `None` because there is no path
|
||||||
// on disk or to a device
|
// on disk or to a device
|
||||||
keypair_from_seed_phrase(keypair_name, skip_validation, false).map(|_| None)
|
keypair_from_seed_phrase(keypair_name, skip_validation, false, derivation_path).map(|_| None)
|
||||||
}
|
}
|
||||||
SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
|
SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
|
||||||
Err(e) => Err(std::io::Error::new(
|
Err(e) => Err(std::io::Error::new(
|
||||||
|
@ -407,6 +411,7 @@ pub fn keypair_from_seed_phrase(
|
||||||
keypair_name: &str,
|
keypair_name: &str,
|
||||||
skip_validation: bool,
|
skip_validation: bool,
|
||||||
confirm_pubkey: bool,
|
confirm_pubkey: bool,
|
||||||
|
derivation_path: Option<DerivationPath>,
|
||||||
) -> Result<Keypair, Box<dyn error::Error>> {
|
) -> Result<Keypair, Box<dyn error::Error>> {
|
||||||
let seed_phrase = prompt_password_stderr(&format!("[{}] seed phrase: ", keypair_name))?;
|
let seed_phrase = prompt_password_stderr(&format!("[{}] seed phrase: ", keypair_name))?;
|
||||||
let seed_phrase = seed_phrase.trim();
|
let seed_phrase = seed_phrase.trim();
|
||||||
|
@ -417,7 +422,8 @@ pub fn keypair_from_seed_phrase(
|
||||||
|
|
||||||
let keypair = if skip_validation {
|
let keypair = if skip_validation {
|
||||||
let passphrase = prompt_passphrase(&passphrase_prompt)?;
|
let passphrase = prompt_passphrase(&passphrase_prompt)?;
|
||||||
keypair_from_seed_phrase_and_passphrase(&seed_phrase, &passphrase)?
|
let seed = generate_seed_from_seed_phrase_and_passphrase(&seed_phrase, &passphrase);
|
||||||
|
keypair_from_seed_and_derivation_path(&seed, derivation_path)?
|
||||||
} else {
|
} else {
|
||||||
let sanitized = sanitize_seed_phrase(seed_phrase);
|
let sanitized = sanitize_seed_phrase(seed_phrase);
|
||||||
let parse_language_fn = || {
|
let parse_language_fn = || {
|
||||||
|
@ -440,7 +446,7 @@ pub fn keypair_from_seed_phrase(
|
||||||
let mnemonic = parse_language_fn()?;
|
let mnemonic = parse_language_fn()?;
|
||||||
let passphrase = prompt_passphrase(&passphrase_prompt)?;
|
let passphrase = prompt_passphrase(&passphrase_prompt)?;
|
||||||
let seed = Seed::new(&mnemonic, &passphrase);
|
let seed = Seed::new(&mnemonic, &passphrase);
|
||||||
keypair_from_seed(seed.as_bytes())?
|
keypair_from_seed_and_derivation_path(&seed.as_bytes(), derivation_path)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if confirm_pubkey {
|
if confirm_pubkey {
|
||||||
|
|
|
@ -46,15 +46,15 @@ on your wallet type.
|
||||||
#### Paper Wallet
|
#### Paper Wallet
|
||||||
|
|
||||||
In a paper wallet, the keypair is securely derived from the seed words and
|
In a paper wallet, the keypair is securely derived from the seed words and
|
||||||
optional passphrase you entered when the wallet was create. To use a paper wallet
|
optional passphrase you entered when the wallet was create. To use a paper
|
||||||
keypair anywhere the `<KEYPAIR>` text is shown in examples or help documents,
|
wallet keypair anywhere the `<KEYPAIR>` text is shown in examples or help
|
||||||
enter the word `ASK` and the program will prompt you to enter your seed words
|
documents, enter the uri scheme `ask://` and the program will prompt you to
|
||||||
when you run the command.
|
enter your seed words when you run the command.
|
||||||
|
|
||||||
To display the wallet address of a Paper Wallet:
|
To display the wallet address of a Paper Wallet:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
solana-keygen pubkey ASK
|
solana-keygen pubkey ask://
|
||||||
```
|
```
|
||||||
|
|
||||||
#### File System Wallet
|
#### File System Wallet
|
||||||
|
|
|
@ -155,7 +155,7 @@ solana-keygen new --no-outfile
|
||||||
The corresponding identity public key can now be viewed by running:
|
The corresponding identity public key can now be viewed by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
solana-keygen pubkey ASK
|
solana-keygen pubkey ask://
|
||||||
```
|
```
|
||||||
|
|
||||||
and then entering your seed phrase.
|
and then entering your seed phrase.
|
||||||
|
@ -294,8 +294,8 @@ The ledger will be placed in the `ledger/` directory by default, use the
|
||||||
> [paper wallet seed phrase](../wallet-guide/paper-wallet.md)
|
> [paper wallet seed phrase](../wallet-guide/paper-wallet.md)
|
||||||
> for your `--identity` and/or
|
> for your `--identity` and/or
|
||||||
> `--authorized-voter` keypairs. To use these, pass the respective argument as
|
> `--authorized-voter` keypairs. To use these, pass the respective argument as
|
||||||
> `solana-validator --identity ASK ... --authorized-voter ASK ...` and you will be
|
> `solana-validator --identity ask:// ... --authorized-voter ask:// ...`
|
||||||
> prompted to enter your seed phrases and optional passphrase.
|
> and you will be prompted to enter your seed phrases and optional passphrase.
|
||||||
|
|
||||||
Confirm your validator connected to the network by opening a new terminal and
|
Confirm your validator connected to the network by opening a new terminal and
|
||||||
running:
|
running:
|
||||||
|
|
|
@ -85,12 +85,13 @@ solana-keygen new --help
|
||||||
### Public Key Derivation
|
### Public Key Derivation
|
||||||
|
|
||||||
Public keys can be derived from a seed phrase and a passphrase if you choose to
|
Public keys can be derived from a seed phrase and a passphrase if you choose to
|
||||||
use one. This is useful for using an offline-generated seed phrase to
|
use one. This is useful for using an offline-generated seed phrase to derive a
|
||||||
derive a valid public key. The `solana-keygen pubkey` command will walk you
|
valid public key. The `solana-keygen pubkey` command will walk you through how
|
||||||
through entering your seed phrase and a passphrase if you chose to use one.
|
to use your seed phrase (and a passphrase if you chose to use one) as a signer
|
||||||
|
with the solana command-line tools using the `ask` uri scheme.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
solana-keygen pubkey ASK
|
solana-keygen pubkey ask://
|
||||||
```
|
```
|
||||||
|
|
||||||
> Note that you could potentially use different passphrases for the same seed phrase. Each unique passphrase will yield a different keypair.
|
> Note that you could potentially use different passphrases for the same seed phrase. Each unique passphrase will yield a different keypair.
|
||||||
|
@ -102,11 +103,11 @@ will need to pass the `--skip-seed-phrase-validation` argument and forego this
|
||||||
validation.
|
validation.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
solana-keygen pubkey ASK --skip-seed-phrase-validation
|
solana-keygen pubkey ask:// --skip-seed-phrase-validation
|
||||||
```
|
```
|
||||||
|
|
||||||
After entering your seed phrase with `solana-keygen pubkey ASK` the console
|
After entering your seed phrase with `solana-keygen pubkey ask://` the console
|
||||||
will display a string of base-58 character. This is the _wallet address_
|
will display a string of base-58 character. This is the base _wallet address_
|
||||||
associated with your seed phrase.
|
associated with your seed phrase.
|
||||||
|
|
||||||
> Copy the derived address to a USB stick for easy usage on networked computers
|
> Copy the derived address to a USB stick for easy usage on networked computers
|
||||||
|
@ -119,20 +120,48 @@ For full usage details run:
|
||||||
solana-keygen pubkey --help
|
solana-keygen pubkey --help
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Hierarchical Derivation
|
||||||
|
|
||||||
|
The solana-cli supports
|
||||||
|
[BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) and
|
||||||
|
[BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki)
|
||||||
|
hierarchical derivation of private keys from your seed phrase and passphrase by
|
||||||
|
adding either the `?key=` query string or the `?full-path=` query string.
|
||||||
|
|
||||||
|
To use solana's BIP44 derivation path `m/44'/501'`, supply the `?key=m` query
|
||||||
|
string, or `?key=<ACCOUNT>/<CHANGE>`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
solana-keygen pubkey ask://?key=0/1
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a derivation path other than solana's standard BIP44, you can supply `?full-path=m/<PURPOSE>/<COIN_TYPE>/<ACCOUNT>/<CHANGE>`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
solana-keygen pubkey ask://?full-path=m/44/2017/0/1
|
||||||
|
```
|
||||||
|
|
||||||
|
Because Solana uses Ed25519 keypairs, as per
|
||||||
|
[SLIP-0010](https://github.com/satoshilabs/slips/blob/master/slip-0010.md) all
|
||||||
|
derivation-path indexes will be promoted to hardened indexes -- eg.
|
||||||
|
`?key=0'/0'`, `?full-path=m/44'/2017'/0'/1'` -- regardless of whether ticks are
|
||||||
|
included in the query-string input.
|
||||||
|
|
||||||
## Verifying the Keypair
|
## Verifying the Keypair
|
||||||
|
|
||||||
To verify you control the private key of a paper wallet address, use
|
To verify you control the private key of a paper wallet address, use
|
||||||
`solana-keygen verify`:
|
`solana-keygen verify`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
solana-keygen verify <PUBKEY> ASK
|
solana-keygen verify <PUBKEY> ask://
|
||||||
```
|
```
|
||||||
|
|
||||||
where `<PUBKEY>` is replaced with the wallet address and they keyword `ASK` tells the
|
where `<PUBKEY>` is replaced with the wallet address and they keyword `ask://`
|
||||||
command to prompt you for the keypair's seed phrase. Note that for security
|
tells the command to prompt you for the keypair's seed phrase; `key` and
|
||||||
reasons, your seed phrase will not be displayed as you type. After entering your
|
`full-path` query-strings accepted. Note that for security reasons, your seed
|
||||||
seed phrase, the command will output "Success" if the given public key matches the
|
phrase will not be displayed as you type. After entering your seed phrase, the
|
||||||
keypair generated from your seed phrase, and "Failed" otherwise.
|
command will output "Success" if the given public key matches the keypair
|
||||||
|
generated from your seed phrase, and "Failed" otherwise.
|
||||||
|
|
||||||
## Checking Account Balance
|
## Checking Account Balance
|
||||||
|
|
||||||
|
|
|
@ -589,7 +589,7 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
||||||
let keypair = keypair_from_seed_phrase("recover", skip_validation, true)?;
|
let keypair = keypair_from_seed_phrase("recover", skip_validation, true, None)?;
|
||||||
output_keypair(&keypair, &outfile, "recovered")?;
|
output_keypair(&keypair, &outfile, "recovered")?;
|
||||||
}
|
}
|
||||||
("grind", Some(matches)) => {
|
("grind", Some(matches)) => {
|
||||||
|
|
|
@ -629,6 +629,16 @@ dependencies = [
|
||||||
"subtle 2.2.2",
|
"subtle 2.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-mac"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "58bcd97a54c7ca5ce2f6eb16f6bede5b0ab5f0055fedc17d2f0b4466e21671ca"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array 0.14.3",
|
||||||
|
"subtle 2.2.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-mac"
|
name = "crypto-mac"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
@ -786,6 +796,19 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ed25519-dalek-bip32"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "057f328f31294b5ab432e6c39642f54afd1531677d6d4ba2905932844cc242f3"
|
||||||
|
dependencies = [
|
||||||
|
"derivation-path",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"failure",
|
||||||
|
"hmac 0.9.0",
|
||||||
|
"sha2 0.9.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "educe"
|
name = "educe"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
@ -860,6 +883,7 @@ version = "0.1.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
|
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
"failure_derive",
|
"failure_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1217,6 +1241,16 @@ dependencies = [
|
||||||
"digest 0.9.0",
|
"digest 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-mac 0.9.1",
|
||||||
|
"digest 0.9.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
@ -3450,6 +3484,7 @@ dependencies = [
|
||||||
"derivation-path",
|
"derivation-path",
|
||||||
"digest 0.9.0",
|
"digest 0.9.0",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
"ed25519-dalek-bip32",
|
||||||
"generic-array 0.14.3",
|
"generic-array 0.14.3",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac 0.10.1",
|
"hmac 0.10.1",
|
||||||
|
|
|
@ -28,6 +28,7 @@ full = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
"ed25519-dalek-bip32",
|
||||||
"solana-logger",
|
"solana-logger",
|
||||||
"solana-crate-features",
|
"solana-crate-features",
|
||||||
"libsecp256k1",
|
"libsecp256k1",
|
||||||
|
@ -46,6 +47,7 @@ curve25519-dalek = { version = "2.1.0", optional = true }
|
||||||
derivation-path = { version = "0.1.3", default-features = false }
|
derivation-path = { version = "0.1.3", default-features = false }
|
||||||
digest = { version = "0.9.0", optional = true }
|
digest = { version = "0.9.0", optional = true }
|
||||||
ed25519-dalek = { version = "=1.0.1", optional = true }
|
ed25519-dalek = { version = "=1.0.1", optional = true }
|
||||||
|
ed25519-dalek-bip32 = { version = "0.1.1", optional = true }
|
||||||
generic-array = { version = "0.14.3", default-features = false, features = ["serde", "more_lengths"], optional = true }
|
generic-array = { version = "0.14.3", default-features = false, features = ["serde", "more_lengths"], optional = true }
|
||||||
hex = "0.4.2"
|
hex = "0.4.2"
|
||||||
hmac = "0.10.1"
|
hmac = "0.10.1"
|
||||||
|
|
|
@ -44,6 +44,12 @@ impl TryFrom<&str> for DerivationPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<[ChildIndex]> for DerivationPath {
|
||||||
|
fn as_ref(&self) -> &[ChildIndex] {
|
||||||
|
&self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DerivationPath {
|
impl DerivationPath {
|
||||||
fn new<P: Into<Box<[ChildIndex]>>>(path: P) -> Self {
|
fn new<P: Into<Box<[ChildIndex]>>>(path: P) -> Self {
|
||||||
Self(DerivationPathInner::new(path))
|
Self(DerivationPathInner::new(path))
|
||||||
|
@ -54,8 +60,12 @@ impl DerivationPath {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_key_str_with_coin<T: Bip44>(path: &str, coin: T) -> Result<Self, DerivationPathError> {
|
fn from_key_str_with_coin<T: Bip44>(path: &str, coin: T) -> Result<Self, DerivationPathError> {
|
||||||
let path = format!("m/{}", path);
|
let master_path = if path == "m" {
|
||||||
let extend = DerivationPathInner::from_str(&path)
|
path.to_string()
|
||||||
|
} else {
|
||||||
|
format!("m/{}", path)
|
||||||
|
};
|
||||||
|
let extend = DerivationPathInner::from_str(&master_path)
|
||||||
.map_err(|err| DerivationPathError::InvalidDerivationPath(err.to_string()))?;
|
.map_err(|err| DerivationPathError::InvalidDerivationPath(err.to_string()))?;
|
||||||
let mut extend = extend.into_iter();
|
let mut extend = extend.into_iter();
|
||||||
let account = extend.next().map(|index| index.to_u32());
|
let account = extend.next().map(|index| index.to_u32());
|
||||||
|
@ -69,7 +79,7 @@ impl DerivationPath {
|
||||||
Ok(Self::new_bip44_with_coin(coin, account, change))
|
Ok(Self::new_bip44_with_coin(coin, account, change))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _from_absolute_path_str(path: &str) -> Result<Self, DerivationPathError> {
|
fn from_absolute_path_str(path: &str) -> Result<Self, DerivationPathError> {
|
||||||
let inner = DerivationPath::_from_absolute_path_insecure_str(path)?
|
let inner = DerivationPath::_from_absolute_path_insecure_str(path)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|c| ChildIndex::Hardened(c.to_u32()))
|
.map(|c| ChildIndex::Hardened(c.to_u32()))
|
||||||
|
@ -123,8 +133,18 @@ impl DerivationPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only accepts single query string pair of type `key`
|
pub fn from_uri_key_query(uri: &URIReference<'_>) -> Result<Option<Self>, DerivationPathError> {
|
||||||
pub fn from_uri(uri: &URIReference<'_>) -> Result<Option<Self>, DerivationPathError> {
|
Self::from_uri(uri, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_uri_any_query(uri: &URIReference<'_>) -> Result<Option<Self>, DerivationPathError> {
|
||||||
|
Self::from_uri(uri, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_uri(
|
||||||
|
uri: &URIReference<'_>,
|
||||||
|
key_only: bool,
|
||||||
|
) -> Result<Option<Self>, DerivationPathError> {
|
||||||
if let Some(query) = uri.query() {
|
if let Some(query) = uri.query() {
|
||||||
let query_str = query.as_str();
|
let query_str = query.as_str();
|
||||||
if query_str.is_empty() {
|
if query_str.is_empty() {
|
||||||
|
@ -136,16 +156,26 @@ impl DerivationPath {
|
||||||
"invalid query string, extra fields not supported".to_string(),
|
"invalid query string, extra fields not supported".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let key = query.get("key");
|
let key = query.get(QueryKey::Key.as_ref());
|
||||||
if key.is_none() {
|
if let Some(key) = key {
|
||||||
|
// Use from_key_str instead of TryInto here to make it more explicit that this
|
||||||
|
// generates a Solana bip44 DerivationPath
|
||||||
|
return Self::from_key_str(key).map(Some);
|
||||||
|
}
|
||||||
|
if key_only {
|
||||||
return Err(DerivationPathError::InvalidDerivationPath(format!(
|
return Err(DerivationPathError::InvalidDerivationPath(format!(
|
||||||
"invalid query string `{}`, only `key` supported",
|
"invalid query string `{}`, only `key` supported",
|
||||||
query_str,
|
query_str,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
// Use from_key_str instead of TryInto here to make it a little more explicit that this
|
let full_path = query.get(QueryKey::FullPath.as_ref());
|
||||||
// generates a Solana bip44 DerivationPath
|
if let Some(full_path) = full_path {
|
||||||
key.map(Self::from_key_str).transpose()
|
return Self::from_absolute_path_str(full_path).map(Some);
|
||||||
|
}
|
||||||
|
Err(DerivationPathError::InvalidDerivationPath(format!(
|
||||||
|
"invalid query string `{}`, only `key` and `full-path` supported",
|
||||||
|
query_str,
|
||||||
|
)))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
@ -170,6 +200,46 @@ impl<'a> IntoIterator for &'a DerivationPath {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QUERY_KEY_FULL_PATH: &str = "full-path";
|
||||||
|
const QUERY_KEY_KEY: &str = "key";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error, PartialEq)]
|
||||||
|
#[error("invalid query key `{0}`")]
|
||||||
|
struct QueryKeyError(String);
|
||||||
|
|
||||||
|
enum QueryKey {
|
||||||
|
FullPath,
|
||||||
|
Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for QueryKey {
|
||||||
|
type Err = QueryKeyError;
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let lowercase = s.to_ascii_lowercase();
|
||||||
|
match lowercase.as_str() {
|
||||||
|
QUERY_KEY_FULL_PATH => Ok(Self::FullPath),
|
||||||
|
QUERY_KEY_KEY => Ok(Self::Key),
|
||||||
|
_ => Err(QueryKeyError(s.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for QueryKey {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::FullPath => QUERY_KEY_FULL_PATH,
|
||||||
|
Self::Key => QUERY_KEY_KEY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for QueryKey {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
let s: &str = self.as_ref();
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trait Bip44 {
|
trait Bip44 {
|
||||||
const PURPOSE: u32 = 44;
|
const PURPOSE: u32 = 44;
|
||||||
const COIN: u32;
|
const COIN: u32;
|
||||||
|
@ -240,41 +310,41 @@ mod tests {
|
||||||
fn test_from_absolute_path_str() {
|
fn test_from_absolute_path_str() {
|
||||||
let s = "m/44/501";
|
let s = "m/44/501";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::_from_absolute_path_str(s).unwrap(),
|
DerivationPath::from_absolute_path_str(s).unwrap(),
|
||||||
DerivationPath::default()
|
DerivationPath::default()
|
||||||
);
|
);
|
||||||
let s = "m/44'/501'";
|
let s = "m/44'/501'";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::_from_absolute_path_str(s).unwrap(),
|
DerivationPath::from_absolute_path_str(s).unwrap(),
|
||||||
DerivationPath::default()
|
DerivationPath::default()
|
||||||
);
|
);
|
||||||
let s = "m/44'/501'/1/2";
|
let s = "m/44'/501'/1/2";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::_from_absolute_path_str(s).unwrap(),
|
DerivationPath::from_absolute_path_str(s).unwrap(),
|
||||||
DerivationPath::new_bip44(Some(1), Some(2))
|
DerivationPath::new_bip44(Some(1), Some(2))
|
||||||
);
|
);
|
||||||
let s = "m/44'/501'/1'/2'";
|
let s = "m/44'/501'/1'/2'";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::_from_absolute_path_str(s).unwrap(),
|
DerivationPath::from_absolute_path_str(s).unwrap(),
|
||||||
DerivationPath::new_bip44(Some(1), Some(2))
|
DerivationPath::new_bip44(Some(1), Some(2))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test non-Solana Bip44
|
// Test non-Solana Bip44
|
||||||
let s = "m/44'/999'/1/2";
|
let s = "m/44'/999'/1/2";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::_from_absolute_path_str(s).unwrap(),
|
DerivationPath::from_absolute_path_str(s).unwrap(),
|
||||||
DerivationPath::new_bip44_with_coin(TestCoin, Some(1), Some(2))
|
DerivationPath::new_bip44_with_coin(TestCoin, Some(1), Some(2))
|
||||||
);
|
);
|
||||||
let s = "m/44'/999'/1'/2'";
|
let s = "m/44'/999'/1'/2'";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::_from_absolute_path_str(s).unwrap(),
|
DerivationPath::from_absolute_path_str(s).unwrap(),
|
||||||
DerivationPath::new_bip44_with_coin(TestCoin, Some(1), Some(2))
|
DerivationPath::new_bip44_with_coin(TestCoin, Some(1), Some(2))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test non-bip44 paths
|
// Test non-bip44 paths
|
||||||
let s = "m/501'/0'/0/0";
|
let s = "m/501'/0'/0/0";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::_from_absolute_path_str(s).unwrap(),
|
DerivationPath::from_absolute_path_str(s).unwrap(),
|
||||||
DerivationPath::new(vec![
|
DerivationPath::new(vec![
|
||||||
ChildIndex::Hardened(501),
|
ChildIndex::Hardened(501),
|
||||||
ChildIndex::Hardened(0),
|
ChildIndex::Hardened(0),
|
||||||
|
@ -284,7 +354,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
let s = "m/501'/0'/0'/0'";
|
let s = "m/501'/0'/0'/0'";
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::_from_absolute_path_str(s).unwrap(),
|
DerivationPath::from_absolute_path_str(s).unwrap(),
|
||||||
DerivationPath::new(vec![
|
DerivationPath::new(vec![
|
||||||
ChildIndex::Hardened(501),
|
ChildIndex::Hardened(501),
|
||||||
ChildIndex::Hardened(0),
|
ChildIndex::Hardened(0),
|
||||||
|
@ -295,7 +365,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_from_uri() {
|
fn test_from_uri() {
|
||||||
let derivation_path = DerivationPath::new_bip44(Some(0), Some(0));
|
let derivation_path = DerivationPath::new_bip44(Some(0), Some(0));
|
||||||
|
|
||||||
// test://path?key=0/0
|
// test://path?key=0/0
|
||||||
|
@ -311,7 +381,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::from_uri(&uri).unwrap(),
|
DerivationPath::from_uri(&uri, true).unwrap(),
|
||||||
Some(derivation_path.clone())
|
Some(derivation_path.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -328,7 +398,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::from_uri(&uri).unwrap(),
|
DerivationPath::from_uri(&uri, true).unwrap(),
|
||||||
Some(derivation_path.clone())
|
Some(derivation_path.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -345,10 +415,27 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DerivationPath::from_uri(&uri).unwrap(),
|
DerivationPath::from_uri(&uri, true).unwrap(),
|
||||||
Some(derivation_path)
|
Some(derivation_path)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// test://path?key=m
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("key=m"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
DerivationPath::from_uri(&uri, true).unwrap(),
|
||||||
|
Some(DerivationPath::new_bip44(None, None))
|
||||||
|
);
|
||||||
|
|
||||||
// test://path
|
// test://path
|
||||||
let mut builder = URIReferenceBuilder::new();
|
let mut builder = URIReferenceBuilder::new();
|
||||||
builder
|
builder
|
||||||
|
@ -359,7 +446,7 @@ mod tests {
|
||||||
.try_path("")
|
.try_path("")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert_eq!(DerivationPath::from_uri(&uri).unwrap(), None);
|
assert_eq!(DerivationPath::from_uri(&uri, true).unwrap(), None);
|
||||||
|
|
||||||
// test://path?
|
// test://path?
|
||||||
let mut builder = URIReferenceBuilder::new();
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
@ -373,7 +460,7 @@ mod tests {
|
||||||
.try_query(Some(""))
|
.try_query(Some(""))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert_eq!(DerivationPath::from_uri(&uri).unwrap(), None);
|
assert_eq!(DerivationPath::from_uri(&uri, true).unwrap(), None);
|
||||||
|
|
||||||
// test://path?key=0/0/0
|
// test://path?key=0/0/0
|
||||||
let mut builder = URIReferenceBuilder::new();
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
@ -388,7 +475,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
DerivationPath::from_uri(&uri),
|
DerivationPath::from_uri(&uri, true),
|
||||||
Err(DerivationPathError::InvalidDerivationPath(_))
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -405,7 +492,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
DerivationPath::from_uri(&uri),
|
DerivationPath::from_uri(&uri, true),
|
||||||
Err(DerivationPathError::InvalidDerivationPath(_))
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -422,7 +509,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
DerivationPath::from_uri(&uri),
|
DerivationPath::from_uri(&uri, true),
|
||||||
Err(DerivationPathError::InvalidDerivationPath(_))
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -439,7 +526,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
DerivationPath::from_uri(&uri),
|
DerivationPath::from_uri(&uri, true),
|
||||||
Err(DerivationPathError::InvalidDerivationPath(_))
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -456,7 +543,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
DerivationPath::from_uri(&uri),
|
DerivationPath::from_uri(&uri, true),
|
||||||
Err(DerivationPathError::InvalidDerivationPath(_))
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -473,7 +560,182 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let uri = builder.build().unwrap();
|
let uri = builder.build().unwrap();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
DerivationPath::from_uri(&uri),
|
DerivationPath::from_uri(&uri, true),
|
||||||
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_from_uri_full_path() {
|
||||||
|
let derivation_path = DerivationPath::from_absolute_path_str("m/44'/999'/1'").unwrap();
|
||||||
|
|
||||||
|
// test://path?full-path=m/44/999/1
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path=m/44/999/1"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
DerivationPath::from_uri(&uri, false).unwrap(),
|
||||||
|
Some(derivation_path.clone())
|
||||||
|
);
|
||||||
|
|
||||||
|
// test://path?full-path=m/44'/999'/1'
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path=m/44'/999'/1'"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
DerivationPath::from_uri(&uri, false).unwrap(),
|
||||||
|
Some(derivation_path.clone())
|
||||||
|
);
|
||||||
|
|
||||||
|
// test://path?full-path=m/44\'/999\'/1\'
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path=m/44\'/999\'/1\'"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
DerivationPath::from_uri(&uri, false).unwrap(),
|
||||||
|
Some(derivation_path)
|
||||||
|
);
|
||||||
|
|
||||||
|
// test://path?full-path=m
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path=m"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
DerivationPath::from_uri(&uri, false).unwrap(),
|
||||||
|
Some(DerivationPath(DerivationPathInner::from_str("m").unwrap()))
|
||||||
|
);
|
||||||
|
|
||||||
|
// test://path?full-path=m/44/999/1, only `key` supported
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path=m/44/999/1"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
DerivationPath::from_uri(&uri, true),
|
||||||
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
// test://path?key=0/0&full-path=m/44/999/1
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("key=0/0&full-path=m/44/999/1"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
DerivationPath::from_uri(&uri, false),
|
||||||
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
// test://path?full-path=m/44/999/1&bad-key=0/0
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path=m/44/999/1&bad-key=0/0"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
DerivationPath::from_uri(&uri, false),
|
||||||
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
// test://path?full-path=bad-value
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path=bad-value"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
DerivationPath::from_uri(&uri, false),
|
||||||
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
// test://path?full-path=
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path="))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
DerivationPath::from_uri(&uri, false),
|
||||||
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
// test://path?full-path
|
||||||
|
let mut builder = URIReferenceBuilder::new();
|
||||||
|
builder
|
||||||
|
.try_scheme(Some("test"))
|
||||||
|
.unwrap()
|
||||||
|
.try_authority(Some("path"))
|
||||||
|
.unwrap()
|
||||||
|
.try_path("")
|
||||||
|
.unwrap()
|
||||||
|
.try_query(Some("full-path"))
|
||||||
|
.unwrap();
|
||||||
|
let uri = builder.build().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
DerivationPath::from_uri(&uri, false),
|
||||||
Err(DerivationPathError::InvalidDerivationPath(_))
|
Err(DerivationPathError::InvalidDerivationPath(_))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
//! The `signature` module provides functionality for public, and private keys.
|
//! The `signature` module provides functionality for public, and private keys.
|
||||||
#![cfg(feature = "full")]
|
#![cfg(feature = "full")]
|
||||||
|
|
||||||
use crate::{pubkey::Pubkey, transaction::TransactionError};
|
use crate::{derivation_path::DerivationPath, pubkey::Pubkey, transaction::TransactionError};
|
||||||
use ed25519_dalek::Signer as DalekSigner;
|
use ed25519_dalek::Signer as DalekSigner;
|
||||||
|
use ed25519_dalek_bip32::Error as Bip32Error;
|
||||||
use generic_array::{typenum::U64, GenericArray};
|
use generic_array::{typenum::U64, GenericArray};
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -396,10 +397,37 @@ pub fn keypair_from_seed(seed: &[u8]) -> Result<Keypair, Box<dyn error::Error>>
|
||||||
Ok(Keypair(dalek_keypair))
|
Ok(Keypair(dalek_keypair))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keypair_from_seed_phrase_and_passphrase(
|
/// Generates a Keypair using Bip32 Hierarchical Derivation if derivation-path is provided;
|
||||||
|
/// otherwise builds standard Keypair using the seed as SecretKey
|
||||||
|
pub fn keypair_from_seed_and_derivation_path(
|
||||||
|
seed: &[u8],
|
||||||
|
derivation_path: Option<DerivationPath>,
|
||||||
|
) -> Result<Keypair, Box<dyn error::Error>> {
|
||||||
|
if let Some(derivation_path) = derivation_path {
|
||||||
|
bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into())
|
||||||
|
} else {
|
||||||
|
keypair_from_seed(seed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a Keypair using Bip32 Hierarchical Derivation
|
||||||
|
fn bip32_derived_keypair(
|
||||||
|
seed: &[u8],
|
||||||
|
derivation_path: DerivationPath,
|
||||||
|
) -> Result<Keypair, Bip32Error> {
|
||||||
|
let extended = ed25519_dalek_bip32::ExtendedSecretKey::from_seed(seed)
|
||||||
|
.and_then(|extended| extended.derive(&derivation_path))?;
|
||||||
|
let extended_public_key = extended.public_key();
|
||||||
|
Ok(Keypair(ed25519_dalek::Keypair {
|
||||||
|
secret: extended.secret_key,
|
||||||
|
public: extended_public_key,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_seed_from_seed_phrase_and_passphrase(
|
||||||
seed_phrase: &str,
|
seed_phrase: &str,
|
||||||
passphrase: &str,
|
passphrase: &str,
|
||||||
) -> Result<Keypair, Box<dyn error::Error>> {
|
) -> Vec<u8> {
|
||||||
const PBKDF2_ROUNDS: u32 = 2048;
|
const PBKDF2_ROUNDS: u32 = 2048;
|
||||||
const PBKDF2_BYTES: usize = 64;
|
const PBKDF2_BYTES: usize = 64;
|
||||||
|
|
||||||
|
@ -412,7 +440,17 @@ pub fn keypair_from_seed_phrase_and_passphrase(
|
||||||
PBKDF2_ROUNDS,
|
PBKDF2_ROUNDS,
|
||||||
&mut seed,
|
&mut seed,
|
||||||
);
|
);
|
||||||
keypair_from_seed(&seed[..])
|
seed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keypair_from_seed_phrase_and_passphrase(
|
||||||
|
seed_phrase: &str,
|
||||||
|
passphrase: &str,
|
||||||
|
) -> Result<Keypair, Box<dyn error::Error>> {
|
||||||
|
keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase(
|
||||||
|
seed_phrase,
|
||||||
|
passphrase,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
Loading…
Reference in New Issue