Skip to content

Commit c422dab

Browse files
committed
chore(workspace): Bump version to 0.8.2 and add byte accessor methods to DerivedAccount/DerivedAddress
- Bump workspace version from 0.8.1 to 0.8.2 - Remove SECURITY.md file - Add `private_key_bytes()` and `public_key_bytes()` methods to `kobe_primitives::DerivedAccount` for hex-to-bytes conversion with zeroization - Add `private_key_bytes()` and `public_key_bytes()` methods to `kobe_btc::DerivedAddress` and `kobe_svm::DerivedAddress` - Add `DeriveError::InvalidHex` variant for malformed hex dec
1 parent 7547700 commit c422dab

8 files changed

Lines changed: 212 additions & 71 deletions

File tree

Cargo.lock

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ default-members = ["crates/kobe"]
44
resolver = "3"
55

66
[workspace.package]
7-
version = "0.8.1"
7+
version = "0.8.2"
88
edition = "2024"
99
license = "MIT OR Apache-2.0"
1010
repository = "https://github.com/qntx/kobe"

SECURITY.md

Lines changed: 0 additions & 52 deletions
This file was deleted.

crates/kobe-btc/src/deriver.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,37 @@ pub struct DerivedAddress {
5151
pub address_type: AddressType,
5252
}
5353

54+
impl DerivedAddress {
55+
/// Decode the hex-encoded private key into raw 32-byte material.
56+
///
57+
/// Returned buffer is zeroized on drop.
58+
///
59+
/// # Errors
60+
///
61+
/// Returns an error if the stored hex is malformed or not exactly 32
62+
/// bytes. Never produced by this workspace's derivers under normal use.
63+
pub fn private_key_bytes(&self) -> Result<Zeroizing<[u8; 32]>, DeriveError> {
64+
let mut buf = Zeroizing::new([0u8; 32]);
65+
hex::decode_to_slice(self.private_key_hex.as_str(), buf.as_mut_slice()).map_err(|e| {
66+
kobe_primitives::DeriveError::InvalidHex(alloc::format!("private_key_hex: {e}"))
67+
})?;
68+
Ok(buf)
69+
}
70+
71+
/// Decode the hex-encoded compressed public key (33 bytes).
72+
///
73+
/// # Errors
74+
///
75+
/// Returns an error if the stored hex is malformed.
76+
pub fn public_key_bytes(&self) -> Result<Vec<u8>, DeriveError> {
77+
hex::decode(&self.public_key_hex)
78+
.map_err(|e| {
79+
kobe_primitives::DeriveError::InvalidHex(alloc::format!("public_key_hex: {e}"))
80+
})
81+
.map_err(Into::into)
82+
}
83+
}
84+
5485
impl<'a> Deriver<'a> {
5586
/// Create a new Bitcoin deriver from a wallet.
5687
///
@@ -317,4 +348,19 @@ mod tests {
317348
let d2 = Deriver::new(&wallet2, Network::Mainnet).unwrap();
318349
assert_ne!(d1.derive(0).unwrap().address, d2.derive(0).unwrap().address);
319350
}
351+
352+
#[test]
353+
fn bytes_accessors_roundtrip() {
354+
let wallet = test_wallet();
355+
let deriver = Deriver::new(&wallet, Network::Mainnet).unwrap();
356+
let da = deriver.derive_with(AddressType::P2wpkh, 0).unwrap();
357+
358+
let sk = da.private_key_bytes().unwrap();
359+
assert_eq!(sk.len(), 32);
360+
assert_eq!(hex::encode(*sk), da.private_key_hex.as_str());
361+
362+
let pk = da.public_key_bytes().unwrap();
363+
assert_eq!(pk.len(), 33);
364+
assert_eq!(hex::encode(&pk), da.public_key_hex);
365+
}
320366
}

crates/kobe-primitives/Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ categories = ["cryptography", "no-std"]
1212
[features]
1313
default = ["std"]
1414
std = ["alloc", "bip39/std", "zeroize/std"]
15-
alloc = ["bip39/alloc", "zeroize/alloc"]
15+
alloc = ["bip39/alloc", "zeroize/alloc", "dep:hex"]
1616
rand = ["bip39/rand"]
1717
rand_core = ["bip39/rand_core"]
1818
camouflage = ["dep:hmac", "dep:sha2", "alloc"]
19-
slip10 = ["dep:ed25519-dalek", "dep:hmac", "dep:sha2", "dep:hex", "alloc"]
20-
bip32 = ["dep:bip32-crate", "dep:k256", "dep:hex", "alloc"]
19+
slip10 = ["dep:ed25519-dalek", "dep:hmac", "dep:sha2", "alloc"]
20+
bip32 = ["dep:bip32-crate", "dep:k256", "alloc"]
2121

2222
[dependencies]
2323
bip39.workspace = true

crates/kobe-primitives/src/derive.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,38 @@ impl DerivedAccount {
4444
address,
4545
}
4646
}
47+
48+
/// Decode the hex-encoded private key into raw 32-byte material.
49+
///
50+
/// Every chain deriver in this workspace produces a 32-byte scalar
51+
/// (secp256k1 for EVM/BTC/Cosmos/Tron/Spark/Filecoin/XRPL/Nostr,
52+
/// Ed25519 for SVM/SUI/TON/Aptos), so the output is fixed-length.
53+
/// The returned buffer is zeroized on drop.
54+
///
55+
/// # Errors
56+
///
57+
/// Returns an error if the stored hex is malformed or not exactly
58+
/// 32 bytes. Derivers in this workspace never produce malformed data,
59+
/// so this error is unexpected in normal use.
60+
pub fn private_key_bytes(&self) -> Result<Zeroizing<[u8; 32]>, DeriveError> {
61+
let mut buf = Zeroizing::new([0u8; 32]);
62+
hex::decode_to_slice(self.private_key.as_str(), buf.as_mut_slice())
63+
.map_err(|e| DeriveError::InvalidHex(alloc::format!("private_key: {e}")))?;
64+
Ok(buf)
65+
}
66+
67+
/// Decode the hex-encoded public key into raw bytes.
68+
///
69+
/// Length is chain-specific: 33 for compressed secp256k1, 65 for
70+
/// uncompressed, 32 for Ed25519 / x-only secp256k1.
71+
///
72+
/// # Errors
73+
///
74+
/// Returns an error if the stored hex is malformed.
75+
pub fn public_key_bytes(&self) -> Result<Vec<u8>, DeriveError> {
76+
hex::decode(&self.public_key)
77+
.map_err(|e| DeriveError::InvalidHex(alloc::format!("public_key: {e}")))
78+
}
4779
}
4880

4981
/// Unified derivation trait implemented by all chain derivers.
@@ -98,3 +130,63 @@ pub trait DeriveExt: Derive {
98130
}
99131

100132
impl<T: Derive> DeriveExt for T {}
133+
134+
#[cfg(test)]
135+
mod tests {
136+
use super::*;
137+
138+
fn sample_account() -> DerivedAccount {
139+
DerivedAccount::new(
140+
String::from("m/44'/60'/0'/0/0"),
141+
Zeroizing::new(String::from(
142+
"1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727",
143+
)),
144+
String::from("0237b0bb7a8288d38ed49a524b5dc98cff3eb5ca824c9f9dc0dfdb3d9cd600f299"),
145+
String::from("0x9858EfFD232B4033E47d90003D41EC34EcaEda94"),
146+
)
147+
}
148+
149+
#[test]
150+
fn private_key_bytes_roundtrip() {
151+
let acct = sample_account();
152+
let bytes = acct.private_key_bytes().unwrap();
153+
assert_eq!(bytes.len(), 32);
154+
assert_eq!(hex::encode(*bytes), acct.private_key.as_str());
155+
}
156+
157+
#[test]
158+
fn public_key_bytes_roundtrip() {
159+
let acct = sample_account();
160+
let bytes = acct.public_key_bytes().unwrap();
161+
assert_eq!(bytes.len(), 33);
162+
assert_eq!(hex::encode(&bytes), acct.public_key);
163+
}
164+
165+
#[test]
166+
fn private_key_bytes_rejects_short_hex() {
167+
let bad = DerivedAccount::new(
168+
String::from("m/0"),
169+
Zeroizing::new(String::from("deadbeef")),
170+
String::new(),
171+
String::new(),
172+
);
173+
assert!(matches!(
174+
bad.private_key_bytes(),
175+
Err(DeriveError::InvalidHex(_))
176+
));
177+
}
178+
179+
#[test]
180+
fn public_key_bytes_rejects_non_hex() {
181+
let bad = DerivedAccount::new(
182+
String::from("m/0"),
183+
Zeroizing::new(String::new()),
184+
String::from("not-hex!"),
185+
String::new(),
186+
);
187+
assert!(matches!(
188+
bad.public_key_bytes(),
189+
Err(DeriveError::InvalidHex(_))
190+
));
191+
}
192+
}

crates/kobe-primitives/src/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ pub enum DeriveError {
6868
#[cfg(feature = "bip32")]
6969
#[error("BIP-32: {0}")]
7070
Bip32Derivation(String),
71+
72+
/// Stored hex material is malformed.
73+
///
74+
/// Produced by the byte-level accessors on `DerivedAccount` /
75+
/// `DerivedAddress` when decoding their hex fields. Derivers in this
76+
/// workspace never produce malformed hex, so this error indicates
77+
/// externally-constructed data.
78+
#[cfg(feature = "alloc")]
79+
#[error("invalid hex: {0}")]
80+
InvalidHex(String),
7181
}
7282

7383
#[cfg(not(feature = "std"))]

0 commit comments

Comments
 (0)