aldabra: --bootstrap-from-xprv power-user import path
Adds RootKey::from_root_xsk_bech32() / from_xprv_bytes() / to_root_xsk_bech32() so RootKey can ingest + emit the same bech32 root extended secret key shape that cardano-cli + cardano-address + the IOG node priv/wallet/<name>/root.prv file already use. HRP is strictly root_xsk — refuses acct_xsk/addr_xsk to keep the import scoped to the actual HD root. New CLI flag --bootstrap-from-xprv runs an interactive import: paste root_xsk1... bech32, prompt passphrase, encrypt, persist as root-xprv.age (parallel to mnemonic.age). Refuses to overwrite either existing key file (per Sulkta's no-delete-crypto-keys rule — caller has to move aside, not delete). Startup path now checks for either mnemonic.age OR root-xprv.age; refuses if both exist (ambiguous). Same RootKey downstream — derivation tree, signing, all of it works identically whether the key came in via mnemonic or xprv import. Test root_xprv_round_trip proves the imported xprv derives to the same address as the mnemonic-imported equivalent.
This commit is contained in:
parent
c57316a4fb
commit
2ff007ccaf
3 changed files with 275 additions and 45 deletions
|
|
@ -190,6 +190,78 @@ impl RootKey {
|
|||
pub(crate) fn xprv(&self) -> &XPrv {
|
||||
&self.xprv
|
||||
}
|
||||
|
||||
/// Construct from raw 96-byte XPrv (64-byte extended secret +
|
||||
/// 32-byte chain code). Power-user import path — bypasses the
|
||||
/// BIP-39 mnemonic flow entirely. Used when ingesting a key
|
||||
/// generated by `cardano-cli address key-gen` or extracted from
|
||||
/// a different Cardano wallet (the cnode `root.prv` file is the
|
||||
/// canonical example).
|
||||
///
|
||||
/// Validates the byte count; the key itself is treated as
|
||||
/// trusted input — this isn't a place to enforce semantic
|
||||
/// correctness because any 96-byte sequence is a valid XPrv
|
||||
/// from the type's perspective.
|
||||
pub fn from_xprv_bytes(bytes: &[u8]) -> Result<Self, WalletError> {
|
||||
if bytes.len() != XPRV_SIZE {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"xprv must be {XPRV_SIZE} bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
let mut buf = [0u8; XPRV_SIZE];
|
||||
buf.copy_from_slice(bytes);
|
||||
let xprv = XPrv::from_bytes_verified(buf)
|
||||
.map_err(|e| WalletError::Derivation(format!("xprv verify: {e:?}")))?;
|
||||
Ok(RootKey { xprv })
|
||||
}
|
||||
|
||||
/// Construct from a bech32-encoded extended secret key —
|
||||
/// specifically the `root_xsk1...` shape that Cardano CLI's
|
||||
/// HD wallet tooling (cardano-address, cardano-hw-cli, the
|
||||
/// IOG node `priv/wallet/<name>/root.prv` file) emits.
|
||||
///
|
||||
/// HRP must be exactly `root_xsk` — we refuse other extended-
|
||||
/// key flavours (`acct_xsk`, `addr_xsk`, etc) so callers don't
|
||||
/// accidentally import a derived child as if it were the root.
|
||||
/// If you actually want to import an account-level or address-
|
||||
/// level key, you'd be locking yourself out of CIP-1852
|
||||
/// derivation; we'd rather force the explicit conversation.
|
||||
pub fn from_root_xsk_bech32(s: &str) -> Result<Self, WalletError> {
|
||||
let trimmed = s.trim();
|
||||
let (hrp, data, variant) = bech32::decode(trimmed)
|
||||
.map_err(|e| WalletError::Derivation(format!("bech32 decode: {e}")))?;
|
||||
if hrp != "root_xsk" {
|
||||
return Err(WalletError::Derivation(format!(
|
||||
"expected root_xsk bech32, got {hrp:?}"
|
||||
)));
|
||||
}
|
||||
if variant != bech32::Variant::Bech32 {
|
||||
return Err(WalletError::Derivation(
|
||||
"expected Bech32 (not Bech32m) for root_xsk".into(),
|
||||
));
|
||||
}
|
||||
use bech32::FromBase32;
|
||||
let bytes = Vec::<u8>::from_base32(&data)
|
||||
.map_err(|e| WalletError::Derivation(format!("bech32 base32: {e}")))?;
|
||||
Self::from_xprv_bytes(&bytes)
|
||||
}
|
||||
|
||||
/// Encode the underlying XPrv bytes as `root_xsk1...` bech32.
|
||||
/// Symmetric counterpart to [`from_root_xsk_bech32`]. Useful for
|
||||
/// emitting the same shape that cardano-cli / cardano-address /
|
||||
/// the IOG node's `priv/wallet/<name>/root.prv` files store.
|
||||
/// **Sensitive output** — anyone with this string can spend the
|
||||
/// wallet's funds.
|
||||
pub fn to_root_xsk_bech32(&self) -> Result<String, WalletError> {
|
||||
use bech32::ToBase32;
|
||||
bech32::encode(
|
||||
"root_xsk",
|
||||
self.xprv.as_ref().to_base32(),
|
||||
bech32::Variant::Bech32,
|
||||
)
|
||||
.map_err(|e| WalletError::Derivation(format!("bech32 encode: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
/// Network parameter — bech32 prefix + protocol magic.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue