Compare commits
2 Commits
da09b95bae
...
5424e66aed
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 5424e66aed | |
Ryan Heywood | e850c75879 |
|
@ -41,6 +41,18 @@ dependencies = [
|
|||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
|
@ -67,12 +79,6 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90064b8dee6815a6470d60bad07bbbaee885c0e12d04177138fa3291a01b7bc4"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
|
@ -115,6 +121,12 @@ dependencies = [
|
|||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.9"
|
||||
|
@ -124,6 +136,18 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
|
@ -134,6 +158,16 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
@ -142,27 +176,38 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
name = "ecdsa"
|
||||
version = "0.16.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
"der",
|
||||
"elliptic-curve",
|
||||
"signature",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.48.0",
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"pkcs8",
|
||||
"rand_core",
|
||||
"sec1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -171,6 +216,16 @@ version = "0.3.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.28"
|
||||
|
@ -203,6 +258,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -222,6 +278,17 @@ version = "0.28.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.2"
|
||||
|
@ -234,6 +301,21 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-literal"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.31.0"
|
||||
|
@ -253,11 +335,30 @@ version = "1.0.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyfork-derive-util"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hex-literal",
|
||||
"hmac",
|
||||
"k256",
|
||||
"ripemd",
|
||||
"serde",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -293,7 +394,6 @@ name = "keyforkd"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"dirs",
|
||||
"keyfork-derive-util",
|
||||
"keyfork-frame",
|
||||
"keyfork-mnemonic-util",
|
||||
|
@ -400,12 +500,6 @@ version = "1.18.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
|
@ -444,6 +538,16 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.66"
|
||||
|
@ -463,23 +567,12 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"redox_syscall",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -526,6 +619,15 @@ version = "0.7.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
|
||||
|
||||
[[package]]
|
||||
name = "ripemd"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
|
@ -538,6 +640,20 @@ version = "1.0.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.186"
|
||||
|
@ -598,6 +714,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "similar"
|
||||
version = "2.2.1"
|
||||
|
@ -620,6 +745,22 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.29"
|
||||
|
@ -1008,3 +1149,9 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
|||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[workspace]
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"keyfork-mnemonic-generate",
|
||||
"keyfork-mnemonic-util",
|
||||
|
|
|
@ -5,5 +5,25 @@ edition = "2021"
|
|||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = ["secp256k1"]
|
||||
secp256k1 = ["k256"]
|
||||
|
||||
[dependencies]
|
||||
# Included in Rust
|
||||
digest = "0.10.7"
|
||||
sha2 = "0.10.7"
|
||||
|
||||
# Rust-Crypto ecosystem, not personally audited
|
||||
ripemd = "0.1.3"
|
||||
hmac = { version = "0.12.1", features = ["std"] }
|
||||
|
||||
# Personally audited
|
||||
serde = { version = "1.0.186", features = ["derive"] }
|
||||
thiserror = "1.0.47"
|
||||
|
||||
# Optional, not personally audited
|
||||
k256 = { version = "0.13.1", default-features = false, features = ["std", "arithmetic"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.4.1"
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Index is too large, must be less than 0x80000000: {0}")]
|
||||
IndexTooLarge(u32),
|
||||
|
||||
#[error("Unable to parse integer for index")]
|
||||
IntParseError(#[from] std::num::ParseIntError),
|
||||
|
||||
#[error("Unable to parse path due to bad path prefix")]
|
||||
UnknownPathPrefix,
|
||||
|
||||
#[error("Seed length in bits must be divisible by 32")]
|
||||
BadSeedLength(usize),
|
||||
|
||||
/// This should never happen. HMAC keys should be able to take any size input.
|
||||
#[error("Invalid length for HMAC key while generating master key (report me!)")]
|
||||
HmacInvalidLength(#[from] hmac::digest::InvalidLength),
|
||||
|
||||
/// There's a 1 in 2^256 chance this will happen. If it does, I'm sorry. Pick a new mnemonic.
|
||||
#[error("Seed hash generated 32 bytes of zero, pick a new seed")]
|
||||
HashedSeedIsZero,
|
||||
}
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
|
@ -0,0 +1,2 @@
|
|||
pub mod private_key;
|
||||
pub mod public_key;
|
|
@ -0,0 +1,150 @@
|
|||
use crate::{DerivationIndex, DerivationPath, PrivateKey, PublicKey};
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::Sha512;
|
||||
use thiserror::Error;
|
||||
|
||||
const KEY_SIZE: usize = 256;
|
||||
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Seed had an unsuitable length: {0}")]
|
||||
BadSeedLength(usize),
|
||||
|
||||
#[error("Reached maximum depth for key derivation")]
|
||||
Depth,
|
||||
|
||||
/// This should never happen. HMAC keys should be able to take any size input.
|
||||
#[error("Invalid length for HMAC key while generating master key (report me!)")]
|
||||
HmacInvalidLength(#[from] hmac::digest::InvalidLength),
|
||||
|
||||
#[error("Unknown error while deriving child key")]
|
||||
Derivation,
|
||||
}
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
pub type ChainCode = [u8; 32];
|
||||
type HmacSha512 = Hmac<Sha512>;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ExtendedPrivateKey<K: PrivateKey + Clone> {
|
||||
pub private_key: K,
|
||||
depth: u8,
|
||||
pub(crate) chain_code: ChainCode,
|
||||
}
|
||||
|
||||
impl<K: PrivateKey + Clone> std::fmt::Debug for ExtendedPrivateKey<K> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ExtendedPrivateKey")
|
||||
.field("private_key", &"obscured")
|
||||
.field("depth", &self.depth)
|
||||
.field("chain_code", &self.chain_code)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> ExtendedPrivateKey<K>
|
||||
where
|
||||
K: PrivateKey + Clone,
|
||||
{
|
||||
/// Generate a new [`ExtendedPublicKey`] from a seed, ideally from a 12-word or 24-word
|
||||
/// mnemonic, but may take 16-byte seeds.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
/// * The given seed had an incorrect length.
|
||||
/// * A `HmacSha512` can't be constructed - this should be impossible.
|
||||
pub fn new(seed: impl AsRef<[u8]>) -> Result<Self> {
|
||||
Self::new_internal(seed.as_ref())
|
||||
}
|
||||
|
||||
fn new_internal(seed: &[u8]) -> Result<Self> {
|
||||
let len = seed.len();
|
||||
if ![16, 32, 64].contains(&len) {
|
||||
return Err(Error::BadSeedLength(len));
|
||||
}
|
||||
|
||||
let hash = HmacSha512::new_from_slice(&K::key().bytes().collect::<Vec<_>>())?
|
||||
.chain_update(seed)
|
||||
.finalize()
|
||||
.into_bytes();
|
||||
let (private_key, chain_code) = hash.split_at(KEY_SIZE / 8);
|
||||
|
||||
Ok(Self {
|
||||
private_key: K::from_bytes(private_key.try_into().expect("Invalid key length")),
|
||||
depth: 0,
|
||||
chain_code: chain_code.try_into().expect("Invalid chain code length"),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> K::PublicKey {
|
||||
self.private_key.public_key()
|
||||
}
|
||||
|
||||
/// Derive a child using the given [`DerivationPath`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned under the same circumstances as
|
||||
/// [`ExtendedPrivateKey::derive_child`].
|
||||
pub fn derive_path(&self, path: &DerivationPath) -> Result<Self> {
|
||||
if path.path.is_empty() {
|
||||
Ok(self.clone())
|
||||
} else {
|
||||
path.iter()
|
||||
.try_fold(self.clone(), |key, index| key.derive_child(index))
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a child with a given [`DerivationIndex`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The method performs unchecked `try_into()` operations on constant-sized slice.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
///
|
||||
/// * The depth exceeds the maximum depth [`u8::MAX`].
|
||||
/// * A `HmacSha512` can't be constructed - this should be impossible.
|
||||
/// * Deriving a child key fails. Check the documentation for your [`PrivateKey`].
|
||||
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
|
||||
let depth = self.depth.checked_add(1).ok_or(Error::Depth)?;
|
||||
|
||||
let mut hmac =
|
||||
HmacSha512::new_from_slice(&self.chain_code).map_err(Error::HmacInvalidLength)?;
|
||||
if index.is_hardened() {
|
||||
hmac.update(&[0]);
|
||||
hmac.update(&self.private_key.to_bytes());
|
||||
} else {
|
||||
hmac.update(&self.private_key.public_key().to_bytes());
|
||||
}
|
||||
hmac.update(&index.to_bytes());
|
||||
let result = hmac.finalize().into_bytes();
|
||||
let (private_key, chain_code) = result.split_at(KEY_SIZE / 8);
|
||||
|
||||
let private_key = self
|
||||
.private_key
|
||||
.derive_child(
|
||||
&private_key
|
||||
.try_into()
|
||||
.expect("Invalid length for private key"),
|
||||
)
|
||||
.map_err(|_| Error::Derivation)?;
|
||||
|
||||
Ok(Self {
|
||||
private_key,
|
||||
depth,
|
||||
chain_code: chain_code
|
||||
.try_into()
|
||||
.expect("Invalid length for chain code"),
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
use crate::{DerivationIndex, PublicKey};
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha512;
|
||||
use thiserror::Error;
|
||||
|
||||
const KEY_SIZE: usize = 256;
|
||||
|
||||
#[derive(Error, Clone, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Public keys may not be derived when hardened")]
|
||||
HardenedIndex,
|
||||
|
||||
#[error("Reached maximum depth for key derivation")]
|
||||
Depth,
|
||||
|
||||
/// This should never happen. HMAC keys should be able to take any size input.
|
||||
#[error("Invalid length for HMAC key while generating master key (report me!)")]
|
||||
HmacInvalidLength(#[from] hmac::digest::InvalidLength),
|
||||
|
||||
#[error("Unknown error while deriving child key")]
|
||||
Derivation,
|
||||
}
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
pub type ChainCode = [u8; 32];
|
||||
type HmacSha512 = Hmac<Sha512>;
|
||||
|
||||
pub struct ExtendedPublicKey<K: PublicKey> {
|
||||
public_key: K,
|
||||
depth: u8,
|
||||
chain_code: ChainCode,
|
||||
}
|
||||
|
||||
impl<K> ExtendedPublicKey<K>
|
||||
where
|
||||
K: PublicKey,
|
||||
{
|
||||
pub fn new(public_key: K, chain_code: ChainCode) -> Self {
|
||||
Self {
|
||||
public_key,
|
||||
depth: 0,
|
||||
chain_code,
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a child with a given [`DerivationIndex`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The method performs unchecked `try_into()` operations on a constant-sized slice.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
///
|
||||
/// * The depth exceeds the maximum depth [`u8::MAX`].
|
||||
/// * A `HmacSha512` can't be constructed - this should be impossible.
|
||||
/// * Deriving a child key fails. Check the documentation for your [`PublicKey`].
|
||||
pub fn derive_child(&self, index: &DerivationIndex) -> Result<Self> {
|
||||
if index.is_hardened() {
|
||||
return Err(Error::HardenedIndex);
|
||||
}
|
||||
|
||||
let depth = self.depth.checked_add(1).ok_or(Error::Depth)?;
|
||||
|
||||
let hmac = HmacSha512::new_from_slice(&self.chain_code)
|
||||
.map_err(Error::from)?
|
||||
.chain_update(self.public_key.to_bytes())
|
||||
.chain_update(index.to_bytes())
|
||||
.finalize()
|
||||
.into_bytes();
|
||||
|
||||
let (child_key, chain_code) = hmac.split_at(KEY_SIZE / 8);
|
||||
let derived_key = self
|
||||
.public_key
|
||||
.derive_child(child_key.try_into().expect("Invalid key length"))
|
||||
.map_err(|_| Error::Derivation)?;
|
||||
let chain_code = chain_code.try_into().expect("Invalid chain code length");
|
||||
|
||||
Ok(Self {
|
||||
public_key: derived_key,
|
||||
depth,
|
||||
chain_code,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
use crate::error::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DerivationIndex(pub(crate) u32);
|
||||
|
||||
impl DerivationIndex {
|
||||
/// Creates a new [`DerivationIndex`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the index is larger than the hardened flag.
|
||||
pub fn new(index: u32, hardened: bool) -> Result<Self> {
|
||||
if index & (0b1 << 31) > 0 {
|
||||
return Err(Error::IndexTooLarge(index));
|
||||
}
|
||||
Ok(Self(index | (u32::from(hardened) << 31)))
|
||||
}
|
||||
|
||||
/*
|
||||
* Probably never used.
|
||||
pub(crate) fn from_bytes(bytes: [u8; 4]) -> Self {
|
||||
Self(u32::from_be_bytes(bytes))
|
||||
}
|
||||
*/
|
||||
|
||||
pub(crate) fn to_bytes(&self) -> [u8; 4] {
|
||||
self.0.to_be_bytes()
|
||||
}
|
||||
|
||||
pub fn is_hardened(&self) -> bool {
|
||||
self.0 & (0b1 << 31) != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for DerivationIndex {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
// Returns &str without suffix if suffix is found
|
||||
let (s, is_hardened) = match s.strip_suffix('\'') {
|
||||
Some(subslice) => (subslice, true),
|
||||
None => (s, false),
|
||||
};
|
||||
let index: u32 = s.parse()?;
|
||||
Self::new(index, is_hardened)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DerivationIndex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0 & (u32::MAX >> 1))?;
|
||||
if self.0 & (0b1 << 31) != 0 {
|
||||
write!(f, "'")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn fails_on_high_index() {
|
||||
DerivationIndex::new(0x80000001, false).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_hardened_bit() {
|
||||
assert_eq!(DerivationIndex::new(0x0, true).unwrap().0, 0b1 << 31);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn misc_values() -> Result<()> {
|
||||
assert_eq!(DerivationIndex::new(0x80000000 - 1, true)?.0, u32::MAX);
|
||||
assert_eq!(DerivationIndex::new(0x2, false)?.0, 2);
|
||||
assert_eq!(DerivationIndex::new(0x00ABCDEF, true)?.0, 0x80ABCDEF);
|
||||
assert_eq!(DerivationIndex::new(0x00ABCDEF, false)?.0, 0x00ABCDEF);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() -> Result<()> {
|
||||
assert_eq!(DerivationIndex::from_str("100000")?.0, 100000);
|
||||
assert_eq!(
|
||||
DerivationIndex::from_str("100000'")?.0,
|
||||
(0b1 << 31) + 100000
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() -> Result<()> {
|
||||
assert_eq!(&DerivationIndex::new(3232, false)?.to_string(), "3232");
|
||||
assert_eq!(&DerivationIndex::new(3232, true)?.to_string(), "3232'");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equivalency() -> Result<()> {
|
||||
let values = ["123456'", "123456", "1726562", "0'", "0"];
|
||||
for value in values {
|
||||
assert_eq!(value, DerivationIndex::from_str(value)?.to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn from_str_fails_on_high_index() {
|
||||
DerivationIndex::from_str(&0x80000001u32.to_string()).unwrap();
|
||||
}
|
||||
}
|
|
@ -1,23 +1,21 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct DerivablePath {
|
||||
pub(crate) path: Vec<u32>,
|
||||
}
|
||||
pub mod error;
|
||||
pub mod extended_key;
|
||||
pub mod index;
|
||||
pub mod master_key;
|
||||
pub mod path;
|
||||
pub mod private_key;
|
||||
pub mod public_key;
|
||||
|
||||
// TODO: move DerivablePath into a models crate for clients to produce?
|
||||
/*
|
||||
impl DerivablePath {
|
||||
pub fn new(input: &[&[u8]]) -> DerivablePath {
|
||||
DerivablePath {
|
||||
path: input
|
||||
.iter()
|
||||
.map(|&word| {
|
||||
// perform path validation
|
||||
word.to_vec()
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use crate::{
|
||||
error::{Error, Result},
|
||||
extended_key::{private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey},
|
||||
index::DerivationIndex,
|
||||
path::DerivationPath,
|
||||
private_key::PrivateKey,
|
||||
public_key::PublicKey,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
use crate::error::{Error, Result};
|
||||
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha512;
|
||||
|
||||
pub trait MasterKey<'a> {
|
||||
/// Return the textual content used to derive the master key, as specified in BIP 0032 and SLIP
|
||||
/// 0010. For example, a secp256k1 master key would use the textual content "Bitcoin seed", to
|
||||
/// ensure compatibility with BIP 0032, despite the key being used for more functionality than
|
||||
/// purely Bitcoin.
|
||||
fn key() -> &'static str;
|
||||
|
||||
/// Some key algorithms, such as Ed25519, allow 0 as a valid private key. Those algorithhms
|
||||
/// should override this method to indicate as such.
|
||||
fn is_zero_valid_private_key() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Return the seed used to derive the Master Key, with a size between 128 to 512 bits. Most
|
||||
/// seeds should be 256 bits, the largest size available from a BIP-0039 mnemonic.
|
||||
fn seed(&self) -> &'a [u8];
|
||||
}
|
||||
|
||||
type HmacSha512 = Hmac<Sha512>;
|
||||
|
||||
/// Generate a Master Secret Key and Chain Code (what is this used for?).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
/// * The `HmacSha512` key returned by `T::key()` is invalid. This should never happen.
|
||||
/// * The generated master key is all zeroes. This has a cosmically small chance of happening.
|
||||
pub fn generate<'a, T: MasterKey<'a>>(generator: &T) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
let seed = generator.seed();
|
||||
let len = seed.len();
|
||||
if len * 8 % 32 != 0 {
|
||||
return Err(Error::BadSeedLength(len));
|
||||
}
|
||||
|
||||
let mut hmac = HmacSha512::new_from_slice(&T::key().bytes().collect::<Vec<_>>())?;
|
||||
hmac.update(seed);
|
||||
let result = hmac.finalize().into_bytes();
|
||||
let left = &result[..32];
|
||||
let right = &result[32..];
|
||||
if left.iter().all(|n| n == &0) {
|
||||
// Wow. Impressive.
|
||||
// NOTE: SLIP-0010 says to retry if this happens, but uses some weird terminology to do so.
|
||||
// I do not trust it. BIP-0032 says this key is "invalid", with no instructions to retry.
|
||||
// This is a low enough chance I am fine with it being freak-of-nature error.
|
||||
return Err(Error::HashedSeedIsZero);
|
||||
}
|
||||
|
||||
Ok((left.to_vec(), right.to_vec()))
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
use crate::error::{Error, Result};
|
||||
use crate::index::DerivationIndex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const PREFIX: &str = "m";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
pub struct DerivationPath {
|
||||
pub(crate) path: Vec<DerivationIndex>,
|
||||
}
|
||||
|
||||
impl DerivationPath {
|
||||
pub fn iter(&self) -> impl Iterator<Item = &DerivationIndex> {
|
||||
self.path.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for DerivationPath {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut iter = s.split('/');
|
||||
if iter.next() != Some(PREFIX) {
|
||||
return Err(Error::UnknownPathPrefix);
|
||||
}
|
||||
Ok(Self {
|
||||
path: iter.map(DerivationIndex::from_str).collect::<Result<_>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DerivationPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{PREFIX}")?;
|
||||
for index in self.iter() {
|
||||
write!(f, "/{index}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn requires_master_path() {
|
||||
DerivationPath::from_str("1234/5678'").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equivalency() -> Result<()> {
|
||||
let paths = ["m/1234'/5678", "m/44'/0'/0'", "m"];
|
||||
for path in paths {
|
||||
assert_eq!(&DerivationPath::from_str(path)?.to_string(), path);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
use crate::PublicKey;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub type PrivateKeyBytes = [u8; 32];
|
||||
|
||||
pub trait PrivateKey: Sized {
|
||||
type PublicKey: PublicKey;
|
||||
type Err: std::error::Error;
|
||||
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Self;
|
||||
fn to_bytes(&self) -> PrivateKeyBytes;
|
||||
|
||||
fn is_zero_valid_public_key() -> bool {
|
||||
false
|
||||
}
|
||||
fn key() -> &'static str;
|
||||
|
||||
fn public_key(&self) -> Self::PublicKey;
|
||||
|
||||
/// Derive a child [`PrivateKey`] with given `PrivateKeyBytes`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
/// * A nonzero `other` is provided.
|
||||
/// * An error specific to the given algorithm was encountered.
|
||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum PrivateKeyError {
|
||||
#[error("The provided private key must be nonzero, but is not")]
|
||||
NonZero,
|
||||
|
||||
#[error("Unable to convert point to key")]
|
||||
PointToKey(#[from] k256::elliptic_curve::Error),
|
||||
}
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
use k256::NonZeroScalar;
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
impl PrivateKey for k256::SecretKey {
|
||||
type Err = PrivateKeyError;
|
||||
type PublicKey = k256::PublicKey;
|
||||
|
||||
fn key() -> &'static str {
|
||||
"Bitcoin seed"
|
||||
}
|
||||
|
||||
fn from_bytes(b: &PrivateKeyBytes) -> Self {
|
||||
Self::from_slice(b).expect("Invalid private key bytes")
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> PrivateKeyBytes {
|
||||
// Note: Safety assured by type returned from EncodedPoint
|
||||
self.to_bytes().into()
|
||||
}
|
||||
|
||||
fn derive_child(&self, other: &PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
if other.iter().all(|n| n == &0) {
|
||||
return Err(PrivateKeyError::NonZero);
|
||||
}
|
||||
let other = *other;
|
||||
// Checked: See above nonzero check
|
||||
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
|
||||
.expect("Should have been able to get a NonZeroScalar");
|
||||
|
||||
let derived_scalar = self.to_nonzero_scalar().as_ref() + scalar.as_ref();
|
||||
Ok(
|
||||
Option::<NonZeroScalar>::from(NonZeroScalar::new(derived_scalar))
|
||||
.map(Into::into)
|
||||
.expect("Should be able to make Key"),
|
||||
)
|
||||
}
|
||||
|
||||
fn public_key(&self) -> Self::PublicKey {
|
||||
self.public_key()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
use crate::private_key::PrivateKeyBytes;
|
||||
|
||||
use digest::Digest;
|
||||
use ripemd::Ripemd160;
|
||||
use sha2::Sha256;
|
||||
use thiserror::Error;
|
||||
|
||||
pub type PublicKeyBytes = [u8; 33];
|
||||
|
||||
pub trait PublicKey: Sized {
|
||||
type Err: std::error::Error;
|
||||
|
||||
fn from_bytes(b: &PublicKeyBytes) -> Self;
|
||||
fn to_bytes(&self) -> PublicKeyBytes;
|
||||
|
||||
/// Derive a child [`PublicKey`] with given `PrivateKeyBytes`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error may be returned if:
|
||||
/// * A nonzero `other` is provided.
|
||||
/// * An error specific to the given algorithm was encountered.
|
||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err>;
|
||||
|
||||
fn fingerprint(&self) -> [u8; 4] {
|
||||
let hash = Sha256::new().chain_update(self.to_bytes()).finalize();
|
||||
let hash = Ripemd160::new().chain_update(hash).finalize();
|
||||
// Note: Safety assured by type returned from Ripemd160
|
||||
hash[..4].try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum PublicKeyError {
|
||||
#[error("The provided public key must be nonzero, but is not")]
|
||||
NonZero,
|
||||
|
||||
#[error("Unable to convert point to key")]
|
||||
PointToKey(#[from] k256::elliptic_curve::Error),
|
||||
}
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
use k256::{
|
||||
elliptic_curve::{group::prime::PrimeCurveAffine, sec1::ToEncodedPoint},
|
||||
AffinePoint, NonZeroScalar,
|
||||
};
|
||||
|
||||
#[cfg(feature = "secp256k1")]
|
||||
impl PublicKey for k256::PublicKey {
|
||||
type Err = PublicKeyError;
|
||||
|
||||
fn from_bytes(b: &PublicKeyBytes) -> Self {
|
||||
Self::from_sec1_bytes(b).expect("Invalid public key bytes")
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> PublicKeyBytes {
|
||||
// Note: Safety assured by type returned from EncodedPoint
|
||||
self.to_encoded_point(true).as_bytes().try_into().unwrap()
|
||||
}
|
||||
|
||||
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
|
||||
if other.iter().all(|n| n == &0) {
|
||||
return Err(PublicKeyError::NonZero);
|
||||
}
|
||||
// Checked: See above
|
||||
let scalar = Option::<NonZeroScalar>::from(NonZeroScalar::from_repr(other.into()))
|
||||
.expect("Should have been able to get a NonZeroScalar");
|
||||
|
||||
let point = self.to_projective() + (AffinePoint::generator() * *scalar);
|
||||
Self::from_affine(point.into()).map_err(From::from)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
use crate::*;
|
||||
use hex_literal::hex;
|
||||
use k256::SecretKey;
|
||||
use std::str::FromStr;
|
||||
|
||||
// Pulled from: https://github.com/satoshilabs/slips/blob/master/slip-0010.md
|
||||
|
||||
#[test]
|
||||
fn example() {
|
||||
// seed, chain, chain code, private, public
|
||||
let tests = [(
|
||||
&hex!("000102030405060708090a0b0c0d0e0f")[..],
|
||||
DerivationPath::from_str("m").unwrap(),
|
||||
hex!("873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508"),
|
||||
hex!("e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"),
|
||||
hex!("0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2"),
|
||||
), (
|
||||
&hex!("000102030405060708090a0b0c0d0e0f")[..],
|
||||
DerivationPath::from_str("m/0'").unwrap(),
|
||||
hex!("47fdacbd0f1097043b78c63c20c34ef4ed9a111d980047ad16282c7ae6236141"),
|
||||
hex!("edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea"),
|
||||
hex!("035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56"),
|
||||
), (
|
||||
&hex!("000102030405060708090a0b0c0d0e0f")[..],
|
||||
DerivationPath::from_str("m/0'/1").unwrap(),
|
||||
hex!("2a7857631386ba23dacac34180dd1983734e444fdbf774041578e9b6adb37c19"),
|
||||
hex!("3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368"),
|
||||
hex!("03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c"),
|
||||
), (
|
||||
&hex!("000102030405060708090a0b0c0d0e0f")[..],
|
||||
DerivationPath::from_str("m/0'/1/2'").unwrap(),
|
||||
hex!("04466b9cc8e161e966409ca52986c584f07e9dc81f735db683c3ff6ec7b1503f"),
|
||||
hex!("cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca"),
|
||||
hex!("0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2"),
|
||||
), (
|
||||
&hex!("000102030405060708090a0b0c0d0e0f")[..],
|
||||
DerivationPath::from_str("m/0'/1/2'/2").unwrap(),
|
||||
hex!("cfb71883f01676f587d023cc53a35bc7f88f724b1f8c2892ac1275ac822a3edd"),
|
||||
hex!("0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4"),
|
||||
hex!("02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29"),
|
||||
), (
|
||||
&hex!("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542")[..],
|
||||
DerivationPath::from_str("m").unwrap(),
|
||||
hex!("60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689"),
|
||||
hex!("4b03d6fc340455b363f51020ad3ecca4f0850280cf436c70c727923f6db46c3e"),
|
||||
hex!("03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7"),
|
||||
), (
|
||||
&hex!("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542")[..],
|
||||
DerivationPath::from_str("m/0").unwrap(),
|
||||
hex!("f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c"),
|
||||
hex!("abe74a98f6c7eabee0428f53798f0ab8aa1bd37873999041703c742f15ac7e1e"),
|
||||
hex!("02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea"),
|
||||
), (
|
||||
&hex!("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542")[..],
|
||||
DerivationPath::from_str("m/0/2147483647'/1/2147483646'/2").unwrap(),
|
||||
hex!("9452b549be8cea3ecb7a84bec10dcfd94afe4d129ebfd3b3cb58eedf394ed271"),
|
||||
hex!("bb7d39bdb83ecf58f2fd82b6d918341cbef428661ef01ab97c28a4842125ac23"),
|
||||
hex!("024d902e1a2fc7a8755ab5b694c575fce742c48d9ff192e63df5193e4c7afe1f9c"),
|
||||
)];
|
||||
for (seed, chain, chain_code, private_key, public_key) in tests {
|
||||
let xkey = ExtendedPrivateKey::<SecretKey>::new(seed).unwrap();
|
||||
let derived_key = xkey.derive_path(&chain).unwrap();
|
||||
assert_eq!(derived_key.chain_code, chain_code);
|
||||
assert_eq!(derived_key.private_key.to_bytes().as_slice(), private_key);
|
||||
assert_eq!(derived_key.public_key().to_bytes(), public_key);
|
||||
}
|
||||
}
|
|
@ -10,9 +10,14 @@ default = ["async"]
|
|||
async = ["dep:tokio"]
|
||||
|
||||
[dependencies]
|
||||
hex = "0.4.3"
|
||||
# Included in Rust
|
||||
sha2 = "0.10.7"
|
||||
|
||||
# Personally audited
|
||||
thiserror = "1.0.47"
|
||||
hex = "0.4.3"
|
||||
|
||||
# Optional, not personally audited
|
||||
tokio = { version = "1.32.0", optional = true, features = ["io-util"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# Included in rust
|
||||
sha2 = "0.10.7"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -11,15 +11,20 @@ tracing = ["tower/tracing", "tokio/tracing", "dep:tracing", "dep:tracing-subscri
|
|||
multithread = ["tokio/rt-multi-thread"]
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.3.3"
|
||||
dirs = "5.0.1"
|
||||
keyfork-derive-util = { version = "0.1.0", path = "../keyfork-derive-util" }
|
||||
keyfork-frame = { version = "0.1.0", path = "../keyfork-frame" }
|
||||
keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" }
|
||||
serde = { version = "1.0.186", features = ["derive"] }
|
||||
thiserror = "1.0.47"
|
||||
|
||||
# Not personally audited
|
||||
bincode = "1.3.3"
|
||||
|
||||
# Ecosystem trust, not personally audited
|
||||
tokio = { version = "1.32.0", features = ["io-util", "macros", "rt", "io-std", "net", "fs", "signal"] }
|
||||
tower = { version = "0.4.13", features = ["tokio", "util"] }
|
||||
tracing = { version = "0.1.37", optional = true }
|
||||
tracing-error = { version = "0.2.0", optional = true }
|
||||
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
|
||||
tower = { version = "0.4.13", features = ["tokio", "util"] }
|
||||
|
||||
# Personally audited
|
||||
thiserror = "1.0.47"
|
||||
serde = { version = "1.0.186", features = ["derive"] }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub(crate) enum KeycloakdError {
|
||||
#[error("No runtime directory found from dirs::runtime_dir()")]
|
||||
NoRuntimeDir,
|
||||
pub(crate) enum KeyforkdError {
|
||||
#[error("Neither KEYFORKD_SOCKET_PATH nor XDG_RUNTIME_DIR were set, nowhere to mount socket")]
|
||||
NoSocketPath,
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use keyfork_mnemonic_util::Mnemonic;
|
||||
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
|
@ -15,7 +17,7 @@ use tracing_subscriber::{
|
|||
mod error;
|
||||
mod server;
|
||||
mod service;
|
||||
use error::KeycloakdError;
|
||||
use error::KeyforkdError;
|
||||
use server::UnixServer;
|
||||
use service::Keyforkd;
|
||||
|
||||
|
@ -52,21 +54,51 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
let service = Keyforkd::new(mnemonic);
|
||||
|
||||
let mut runtime_dir = dirs::runtime_dir().ok_or(KeycloakdError::NoRuntimeDir)?;
|
||||
runtime_dir.push("keyforkd");
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!("ensuring directory exists: {}", runtime_dir.display());
|
||||
if !runtime_dir.is_dir() {
|
||||
tokio::fs::create_dir(&runtime_dir).await?;
|
||||
let runtime_vars = std::env::vars()
|
||||
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
|
||||
.collect::<HashMap<String, String>>();
|
||||
let mut runtime_path: PathBuf;
|
||||
#[allow(clippy::single_match_else)]
|
||||
match runtime_vars.get("KEYFORKD_SOCKET_PATH") {
|
||||
Some(occupied) => {
|
||||
runtime_path = PathBuf::from(occupied);
|
||||
}
|
||||
None => {
|
||||
runtime_path = PathBuf::from(
|
||||
runtime_vars
|
||||
.get("XDG_RUNTIME_DIR")
|
||||
.ok_or(KeyforkdError::NoSocketPath)?,
|
||||
);
|
||||
runtime_path.push("keyforkd");
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!("ensuring directory exists: {}", runtime_path.display());
|
||||
if !runtime_path.is_dir() {
|
||||
tokio::fs::create_dir(&runtime_path).await?;
|
||||
}
|
||||
runtime_path.push("keyforkd.sock");
|
||||
}
|
||||
}
|
||||
runtime_dir.push("keyforkd.sock");
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(
|
||||
"binding UNIX socket in runtime dir: {}",
|
||||
runtime_dir.display()
|
||||
runtime_path.display()
|
||||
);
|
||||
|
||||
let mut server = UnixServer::bind(&runtime_dir)?;
|
||||
let _ = server.run(service).await;
|
||||
let mut server = match UnixServer::bind(&runtime_path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Encountered error attempting to bind socket: {}", runtime_path.display());
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
match server.run(service).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Encountered error while running");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::service::{DerivationError, Keyforkd};
|
||||
use keyfork_derive_util::DerivablePath;
|
||||
use keyfork_derive_util::DerivationPath;
|
||||
use keyfork_frame::asyncext::{try_decode_from, try_encode_to};
|
||||
use std::{
|
||||
io::Error,
|
||||
|
@ -13,13 +13,13 @@ use tracing::debug;
|
|||
|
||||
async fn read_path_from_socket(
|
||||
socket: &mut UnixStream,
|
||||
) -> Result<DerivablePath, Box<dyn std::error::Error + Send>> {
|
||||
) -> Result<DerivationPath, Box<dyn std::error::Error + Send>> {
|
||||
let data = try_decode_from(socket).await.unwrap();
|
||||
let path: DerivablePath = bincode::deserialize(&data[..]).unwrap();
|
||||
let path: DerivationPath = bincode::deserialize(&data[..]).unwrap();
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
async fn wait_and_run(app: &mut Keyforkd, path: DerivablePath) -> Result<Vec<u8>, DerivationError> {
|
||||
async fn wait_and_run(app: &mut Keyforkd, path: DerivationPath) -> Result<Vec<u8>, DerivationError> {
|
||||
app.ready().await?.call(path).await
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,8 @@ impl UnixServer {
|
|||
let mut path = PathBuf::new();
|
||||
path.extend(address.as_ref().components());
|
||||
tokio::spawn(async move {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!("Binding tokio ctrl-c handler");
|
||||
let result = tokio::signal::ctrl_c().await;
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(
|
||||
|
@ -53,6 +55,8 @@ impl UnixServer {
|
|||
}
|
||||
|
||||
pub async fn run(&mut self, app: Keyforkd) -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!("Listening for clients");
|
||||
loop {
|
||||
let mut app = app.clone();
|
||||
let (mut socket, _) = self.listener.accept().await?;
|
||||
|
@ -63,7 +67,7 @@ impl UnixServer {
|
|||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Error reading DerivablePath from socket");
|
||||
debug!(%e, "Error reading DerivationPath from socket");
|
||||
let content = e.to_string().bytes().collect::<Vec<_>>();
|
||||
let result = try_encode_to(&content[..], &mut socket).await;
|
||||
#[cfg(feature = "tracing")]
|
||||
|
@ -78,7 +82,7 @@ impl UnixServer {
|
|||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
debug!(%e, "Error reading DerivablePath from socket");
|
||||
debug!(%e, "Error reading DerivationPath from socket");
|
||||
let content = e.to_string().bytes().collect::<Vec<_>>();
|
||||
let result = try_encode_to(&content[..], &mut socket).await;
|
||||
#[cfg(feature = "tracing")]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{future::Future, pin::Pin, sync::Arc, task::Poll};
|
||||
|
||||
use keyfork_derive_util::DerivablePath;
|
||||
use keyfork_derive_util::DerivationPath;
|
||||
use keyfork_mnemonic_util::Mnemonic;
|
||||
use thiserror::Error;
|
||||
use tower::Service;
|
||||
|
@ -22,7 +22,7 @@ impl Keyforkd {
|
|||
}
|
||||
}
|
||||
|
||||
impl Service<DerivablePath> for Keyforkd {
|
||||
impl Service<DerivationPath> for Keyforkd {
|
||||
type Response = Vec<u8>;
|
||||
|
||||
type Error = DerivationError;
|
||||
|
@ -37,7 +37,7 @@ impl Service<DerivablePath> for Keyforkd {
|
|||
}
|
||||
|
||||
#[cfg_attr(feature = "tracing", tracing::instrument(skip(self)))]
|
||||
fn call(&mut self, req: DerivablePath) -> Self::Future {
|
||||
fn call(&mut self, req: DerivationPath) -> Self::Future {
|
||||
dbg!(&req, &self.mnemonic);
|
||||
Box::pin(async { Ok(vec![]) })
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue