From 904896a7bfceb7373530778a94ab15559c00b2d1 Mon Sep 17 00:00:00 2001 From: optout <13562139+optout21@users.noreply.github.com> Date: Sun, 4 May 2025 14:14:32 +0200 Subject: [PATCH] Use Network type for network --- README.md | 2 +- seedstore/Cargo.toml | 2 +- seedstore/examples/create_seedstore.rs | 3 +- seedstore/src/keystore.rs | 5 +- seedstore/src/lib.rs | 7 +-- seedstore/src/seedstore.rs | 75 +++++++++++++------------- seedstore/src/test_seedstore.rs | 46 +++++++--------- seedstore/src/tool.rs | 6 ++- 8 files changed, 73 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index b975779..2221d51 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Writing out seed secret: ``` use seedstore::SeedStoreCreator; - let seedstore = SeedStoreCreator::new_from_data(&entropy_bytes, 0, None)?; + let seedstore = SeedStoreCreator::new_from_data(&entropy_bytes, None, None)?; SeedStoreCreator::write_to_file(&seedstore, "./sample.secret", "PasswordVEWFVFDHHEBNJS3")?; } ``` diff --git a/seedstore/Cargo.toml b/seedstore/Cargo.toml index b6c86b0..3ca2fe2 100644 --- a/seedstore/Cargo.toml +++ b/seedstore/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "seedstore" -version = "0.9.1" +version = "0.9.2" description = "Store bitcoin secret material (BIP39 mnemonic entropy, or similar) in an encrypted file" license = "MIT" edition = "2021" diff --git a/seedstore/examples/create_seedstore.rs b/seedstore/examples/create_seedstore.rs index b3486d4..aef8aac 100644 --- a/seedstore/examples/create_seedstore.rs +++ b/seedstore/examples/create_seedstore.rs @@ -5,14 +5,13 @@ use zeroize::Zeroize; fn main() -> Result<(), String> { // Create a new secret file with provided secret data - let network = 0; // Entropy corresponding to a 12-word BIP39 mnemonic, dummy data let dummy_entropy = [42; 16].to_vec(); let user_password = "SecretStrongPasswordVDSVEWFVFDHHEBNJS36DFH"; let path_for_secret_file = format!("{}/sample.secret", temp_dir().to_str().unwrap()); - let mut seedstore = SeedStoreCreator::new_from_data(&dummy_entropy, network, None)?; + let mut seedstore = SeedStoreCreator::new_from_data(&dummy_entropy, None, None)?; let _res = SeedStoreCreator::write_to_file(&seedstore, &path_for_secret_file, user_password)?; println!("Secret entropy written to file {}", path_for_secret_file); diff --git a/seedstore/src/keystore.rs b/seedstore/src/keystore.rs index 9f39c16..db0a8d4 100644 --- a/seedstore/src/keystore.rs +++ b/seedstore/src/keystore.rs @@ -21,9 +21,10 @@ const NONSECRET_DATA_LEN: usize = 4; /// Store a single bitcoin-style ECDSA 32-byte private key in an encrypted file. /// The secret can be loaded from an encrypted file. -/// Additionally store as non-secret 4 bytes reserved for later use. +/// Additionally store 4 bytes of non-secret data, reserved for later use. +/// /// The secret is stored in memory scrambled (using an ephemeral scrambling key). -/// See also [`KeyStoreCreator`], [`SeedStore`]. +/// See also [`KeyStoreCreator`], [`super::SeedStore`]. pub struct KeyStore { secretstore: SecretStore, public_key: PublicKey, diff --git a/seedstore/src/lib.rs b/seedstore/src/lib.rs index 91d2f56..d60bd31 100644 --- a/seedstore/src/lib.rs +++ b/seedstore/src/lib.rs @@ -5,11 +5,12 @@ // . // You may not use this file except in accordance with the license. -//! [`SeedStore`] is a solution for storing a BIP32-style master secret. +//! [`SeedStore`] is a solution for storing a BIP32-style master secret //! in a password-protected encrypted file. -//! SeedStore is built on [`SecretStore`]. +//! SeedStore is built on [`secretstore::SecretStore`]. //! A typical example is a wallet storing the secret seed. -//! If only a single key is used, it it possible to use a child key, or use [`KeyStore`] for single key. +//! +//! If only a single key is needed, it it possible to use a single child key, or use [`KeyStore`] for a single key. mod keystore; mod seedstore; diff --git a/seedstore/src/seedstore.rs b/seedstore/src/seedstore.rs index fa5d879..38c3f04 100644 --- a/seedstore/src/seedstore.rs +++ b/seedstore/src/seedstore.rs @@ -17,13 +17,15 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; const NONSECRET_DATA_LEN: usize = 4; -/// Store a secret BIP32-style entropy in an encrypted file +/// Store a secret BIP32-style entropy in an encrypted file. /// Can be loaded from an encrypted file. /// Additionally store a network type byte, and 3 bytes reserved for later use. +/// /// The secret is stored in memory scrambled (using an ephemeral scrambling key). -/// An seed passphrase can be used optionally, but it is not stored in the encrypted file, -/// but has to be provided when the file is read. Internally it is stored scrambled. -/// See also [`SeedStoreCreator`], [`KeyStore`]. +/// A seed passphrase can be used optionally, but it is not stored in the encrypted file, +/// it has to be provided when the file is read. Internally it is stored scrambled. +/// +/// See also [`SeedStoreCreator`], [`super::KeyStore`]. pub struct SeedStore { secretstore: SecretStore, /// Optional seed passphrase, stored as scrambled bytes (normalized UTF). @@ -36,7 +38,7 @@ pub struct SeedStore { /// See also [`SeedStore`]. pub struct SeedStoreCreator {} -/// Various ways to specify a child, e.g. by index or derivation path. +/// Various ways to specify a child key, e.g. by index or derivation path. pub enum ChildSpecifier { /// Specify by the 4th (last, 'address_index') index (non-hardened) of the BIP84 derivation path; /// corresponds to "m/84'/'/0'/0/" @@ -120,16 +122,21 @@ impl SeedStore { ) } - /// Accessor for network byte. - pub fn network(&self) -> u8 { + /// Accessor for network. + pub fn network(&self) -> Network { + Self::network_byte_to_enum(self.network_as_u8()) + } + + /// Accessor for network, as byte. + pub fn network_as_u8(&self) -> u8 { let nonsecret_data = self.secretstore.nonsecret_data(); debug_assert_eq!(nonsecret_data.len(), NONSECRET_DATA_LEN); nonsecret_data[0] } /// Convert network byte to [`bitcoin::network::Network`]. - pub fn network_byte_as_enum(&self) -> Network { - match self.network() { + pub fn network_byte_to_enum(network: u8) -> Network { + match network { 0 => Network::Bitcoin, 1 => Network::Testnet, 2 => Network::Testnet4, @@ -246,11 +253,8 @@ impl SeedStore { /// Caution: secret material is taken, processed and returned fn xpriv3_from_entropy(&self, entropy: &Vec) -> Result { let mut seed = self.seed_from_entropy(entropy)?; - let xpriv = Xpriv::new_master( - >::into(self.network_byte_as_enum()), - &seed, - ) - .map_err(|e| format!("Internal XPriv derivation error {}", e))?; + let xpriv = Xpriv::new_master(>::into(self.network()), &seed) + .map_err(|e| format!("Internal XPriv derivation error {}", e))?; let derivation = ChildSpecifier::default_account_derivation_path3(self.network()); let derivation_path_3 = DerivationPath::from_str(&derivation) .map_err(|e| format!("Internal derivation conversion error {}", e))?; @@ -275,11 +279,8 @@ impl SeedStore { derivation: &DerivationPath, ) -> Result { let mut seed = self.seed_from_entropy(entropy)?; - let xpriv = Xpriv::new_master( - >::into(self.network_byte_as_enum()), - &seed, - ) - .map_err(|e| format!("Internal XPriv derivation error {}", e))?; + let xpriv = Xpriv::new_master(>::into(self.network()), &seed) + .map_err(|e| format!("Internal XPriv derivation error {}", e))?; let child_xpriv = xpriv .derive_priv(&self.secp, &derivation) .map_err(|e| format!("Internal XPriv derivation error {}", e))?; @@ -295,10 +296,7 @@ impl SeedStore { derivation: &DerivationPath, ) -> Result { let public_key = self.get_child_public_key_intern(entropy, derivation)?; - let address = Address::p2wpkh( - &CompressedPublicKey(public_key), - self.network_byte_as_enum(), - ); + let address = Address::p2wpkh(&CompressedPublicKey(public_key), self.network()); Ok(address.to_string()) } @@ -381,17 +379,19 @@ impl SeedStoreCreator { /// Caution: unencrypted secret data is taken. /// `entropy`: the BIP39-style entropy bytes, with one of these lengths: /// 16 (12 BIP39 mnemonic words), 20 (15 words), 24 (18 words), 28 bytes (21 words), or 32 bytes (24 words). + /// - network: Optionally a different bitcoin network can be specified, default is Mainnet/0 (also for None). /// `seed_passphrase`: Optional seed passphrase, needed to get the correct seed from the entropy (if it was used). pub fn new_from_data( entropy: &Vec, - network: u8, + network: Option, seed_passphrase: Option<&str>, ) -> Result { // verify entropy length let _res = Self::verify_entropy_length(entropy)?; // Non-secret data: network byte, and 3 reserved bytes (reserved for later use) - let nonsecret_data = vec![network, 42, 43, 44]; + let network_byte = SeedStore::network_enum_as_byte(network.unwrap_or(Network::Bitcoin)); + let nonsecret_data = vec![network_byte, 42, 43, 44]; debug_assert_eq!(nonsecret_data.len(), NONSECRET_DATA_LEN); let secretstore = SecretStoreCreator::new_from_data(nonsecret_data, entropy)?; @@ -422,7 +422,7 @@ impl SeedStoreCreator { } impl ChildSpecifier { - pub fn derivation_path(&self, network: u8) -> Result { + pub fn derivation_path(&self, network: Network) -> Result { let derivation_str = match &self { Self::Derivation(derivation_str) => derivation_str.clone(), Self::ChangeAndIndex34(i3, i4) => format!( @@ -447,9 +447,9 @@ impl ChildSpecifier { Ok(derivation) } - fn default_account_derivation_path3(network: u8) -> String { + fn default_account_derivation_path3(network: Network) -> String { match network { - 0 => "m/84'/0'/0'".to_string(), + Network::Bitcoin => "m/84'/0'/0'".to_string(), _ => "m/84'/1'/0'".to_string(), } } @@ -474,21 +474,22 @@ mod tests { use std::str::FromStr; use bitcoin::bip32::DerivationPath; + use bitcoin::Network; use super::ChildSpecifier; #[test] fn test_default_path() { assert_eq!( - ChildSpecifier::default_account_derivation_path3(0), + ChildSpecifier::default_account_derivation_path3(Network::Bitcoin), "m/84'/0'/0'" ); assert_eq!( - ChildSpecifier::default_account_derivation_path3(1), + ChildSpecifier::default_account_derivation_path3(Network::Signet), "m/84'/1'/0'" ); assert_eq!( - ChildSpecifier::default_account_derivation_path3(2), + ChildSpecifier::default_account_derivation_path3(Network::Testnet4), "m/84'/1'/0'" ); } @@ -497,24 +498,26 @@ mod tests { fn test_derivation() { assert_eq!( ChildSpecifier::Derivation("m/49'/1'/2'/3/4".to_owned()) - .derivation_path(0) + .derivation_path(Network::Bitcoin) .unwrap(), DerivationPath::from_str("m/49'/1'/2'/3/4").unwrap() ); assert_eq!( ChildSpecifier::ChangeAndIndex34(1, 4) - .derivation_path(0) + .derivation_path(Network::Bitcoin) .unwrap(), DerivationPath::from_str("m/84'/0'/0'/1/4").unwrap() ); assert_eq!( ChildSpecifier::ChangeAndIndex34(1, 4) - .derivation_path(1) + .derivation_path(Network::Testnet) .unwrap(), DerivationPath::from_str("m/84'/1'/0'/1/4").unwrap() ); assert_eq!( - ChildSpecifier::Index4(66).derivation_path(0).unwrap(), + ChildSpecifier::Index4(66) + .derivation_path(Network::Bitcoin) + .unwrap(), DerivationPath::from_str("m/84'/0'/0'/0/66").unwrap() ); } @@ -523,7 +526,7 @@ mod tests { fn neg_test_invalid_derivation() { assert_eq!( ChildSpecifier::Derivation("what//deriv/j9".to_owned()) - .derivation_path(0) + .derivation_path(Network::Bitcoin) .err() .unwrap(), "Derivation parsing error what//deriv/j9 invalid child number format" diff --git a/seedstore/src/test_seedstore.rs b/seedstore/src/test_seedstore.rs index 0a2d5bf..30eb2c8 100644 --- a/seedstore/src/test_seedstore.rs +++ b/seedstore/src/test_seedstore.rs @@ -1,4 +1,5 @@ use crate::{ChildSpecifier, SeedStore, SeedStoreCreator}; +use bitcoin::Network; use hex_conservative::{DisplayHex, FromHex}; use rand::Rng; use std::env::temp_dir; @@ -22,11 +23,10 @@ const PASSPHRASE1: &str = "this_is_a_secret_passphrase"; #[test] fn create_from_data() { - let network = 0u8; let entropy = Vec::from_hex(ENTROPY_OIL12).unwrap(); - let mut store = SeedStoreCreator::new_from_data(&entropy, network, None).unwrap(); + let mut store = SeedStoreCreator::new_from_data(&entropy, None, None).unwrap(); - assert_eq!(store.network(), 0); + assert_eq!(store.network(), Network::Bitcoin); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB1); assert_eq!( store @@ -64,9 +64,8 @@ fn create_from_data() { #[cfg(feature = "accesssecret")] #[test] fn create_get_secret() { - let network = 0u8; let entropy = Vec::from_hex(ENTROPY_OIL12).unwrap(); - let mut store = SeedStoreCreator::new_from_data(&entropy, network, None).unwrap(); + let mut store = SeedStoreCreator::new_from_data(&entropy, None, None).unwrap(); assert_eq!( store @@ -85,12 +84,12 @@ fn create_get_secret() { } #[test] -fn create_from_data_net_3() { - let network = 3u8; +fn create_from_data_net_signet() { + let network = Network::Signet; let entropy = Vec::from_hex(ENTROPY_OIL12).unwrap(); - let store = SeedStoreCreator::new_from_data(&entropy, network, None).unwrap(); + let store = SeedStoreCreator::new_from_data(&entropy, Some(network), None).unwrap(); - assert_eq!(store.network(), 3); + assert_eq!(store.network(), network); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB2); assert_eq!( store @@ -107,7 +106,7 @@ fn create_from_payload_const_scrypt() { let mut store = SeedStore::new_from_payload(&payload, &password, None).unwrap(); - assert_eq!(store.network(), 0); + assert_eq!(store.network(), Network::Bitcoin); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB1); assert_eq!( store @@ -126,7 +125,7 @@ fn create_from_payload_const_chacha() { let mut store = SeedStore::new_from_payload(&payload, &password, None).unwrap(); - assert_eq!(store.network(), 0); + assert_eq!(store.network(), Network::Bitcoin); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB1); assert_eq!( store @@ -145,7 +144,7 @@ fn create_from_payload_const_xor() { let mut store = SeedStore::new_from_payload(&payload, &password, None).unwrap(); - assert_eq!(store.network(), 0); + assert_eq!(store.network(), Network::Bitcoin); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB1); assert_eq!( store @@ -167,9 +166,8 @@ fn get_temp_file_name() -> String { #[test] fn write_to_file() { - let network = 0u8; let entropy = Vec::from_hex(ENTROPY_OIL12).unwrap(); - let store = SeedStoreCreator::new_from_data(&entropy, network, None).unwrap(); + let store = SeedStoreCreator::new_from_data(&entropy, None, None).unwrap(); let temp_file = get_temp_file_name(); let password = PASSWORD1.to_owned(); @@ -198,7 +196,7 @@ fn read_from_file() { let password = PASSWORD1.to_owned(); let store = SeedStore::new_from_encrypted_file(&temp_file, &password, None).unwrap(); - assert_eq!(store.network(), 0); + assert_eq!(store.network(), Network::Bitcoin); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB1); let _res = fs::remove_file(&temp_file); @@ -236,20 +234,18 @@ fn neg_rcreate_from_payload_xor_wrong_pw_wrong_result() { #[test] fn neg_create_from_data_invalid_entropy_len() { - let network = 0u8; // 18-byte entropy is not valid let entropy = [42u8; 18].to_vec(); - let store_res = SeedStoreCreator::new_from_data(&entropy, network, None); + let store_res = SeedStoreCreator::new_from_data(&entropy, None, None); assert_eq!(store_res.err().unwrap(), "Invalid entropy length 18"); } #[test] fn test_signature() { - let network = 0u8; let entropy = Vec::from_hex(ENTROPY_OIL12).unwrap(); - let store = SeedStoreCreator::new_from_data(&entropy, network, None).unwrap(); + let store = SeedStoreCreator::new_from_data(&entropy, None, None).unwrap(); - assert_eq!(store.network(), 0); + assert_eq!(store.network(), Network::Bitcoin); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB1); let child_specifier = ChildSpecifier::Index4(7); @@ -273,11 +269,10 @@ fn test_signature() { #[test] fn neg_test_signature_wrong_signer_key() { - let network = 0u8; let entropy = Vec::from_hex(ENTROPY_OIL12).unwrap(); - let store = SeedStoreCreator::new_from_data(&entropy, network, None).unwrap(); + let store = SeedStoreCreator::new_from_data(&entropy, None, None).unwrap(); - assert_eq!(store.network(), 0); + assert_eq!(store.network(), Network::Bitcoin); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB1); let hash_to_be_signed = [42; 32]; @@ -328,19 +323,18 @@ fn passphrase_create_from_payload() { #[test] fn passphrase_create_from_data() { - let network = 0u8; let entropy = Vec::from_hex(ENTROPY_OIL12).unwrap(); { // with no passphrase let passphrase = None; - let store = SeedStoreCreator::new_from_data(&entropy, network, passphrase).unwrap(); + let store = SeedStoreCreator::new_from_data(&entropy, None, passphrase).unwrap(); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB1); } { // with a passphrase let passphrase = Some(PASSPHRASE1); - let store = SeedStoreCreator::new_from_data(&entropy, network, passphrase).unwrap(); + let store = SeedStoreCreator::new_from_data(&entropy, None, passphrase).unwrap(); assert_eq!(store.get_xpub().unwrap().to_string(), XPUB3); } } diff --git a/seedstore/src/tool.rs b/seedstore/src/tool.rs index 2ce2c76..4c34f33 100644 --- a/seedstore/src/tool.rs +++ b/seedstore/src/tool.rs @@ -201,7 +201,9 @@ impl SeedStoreTool { let seedstore = SeedStoreCreator::new_from_data( &entropy, - self.config.network.unwrap_or_default(), + self.config + .network + .map(|nb| SeedStore::network_byte_to_enum(nb)), Some(&passphrase), ) .map_err(|e| format!("Could not encrypt secret, {}", e))?; @@ -425,7 +427,7 @@ mod tests { { let store = SeedStore::new_from_encrypted_file(&temp_file, PASSWORD1, None).unwrap(); - assert_eq!(store.network(), network); + assert_eq!(store.network_as_u8(), network); assert_eq!(store.get_xpub().unwrap().to_string(), "tpubDCRo9GmRAvEWANJ5iSfMEqPoq3uYvjBPAAjrDj5iQMxAq7DCs5orw7m9xJes8hWYAwKuH3T63WrKfzzw7g9ucbjq4LUu5cgCLUPMN7gUkrL"); drop(store); }