diff --git a/sn_cli/src/acc_packet.rs b/sn_cli/src/acc_packet.rs index 892d171fa6..bc024d600a 100644 --- a/sn_cli/src/acc_packet.rs +++ b/sn_cli/src/acc_packet.rs @@ -8,21 +8,25 @@ mod change_tracking; +use change_tracking::*; + use super::{ files::{download_file, FilesUploader}, ChunkManager, }; -use bls::PublicKey; -use change_tracking::*; -use color_eyre::{ - eyre::{bail, eyre}, - Result, -}; + use sn_client::{ protocol::storage::{Chunk, RegisterAddress, RetryStrategy}, registers::EntryHash, + transfers::{DerivationIndex, MainSecretKey}, Client, FilesApi, FolderEntry, FoldersApi, Metadata, UploadCfg, }; + +use bls::PublicKey; +use color_eyre::{ + eyre::{bail, eyre}, + Result, +}; use std::{ collections::{ btree_map::{Entry, OccupiedEntry}, @@ -38,6 +42,18 @@ use tracing::trace; use walkdir::{DirEntry, WalkDir}; use xor_name::XorName; +/// Derivation index used to obtain the account packet root folder xorname +// TODO: use eip2333 path for deriving keys +const ACC_PACKET_ADDR_DERIVATION_INDEX: DerivationIndex = DerivationIndex([0x0; 32]); + +/// Derivation index used to obtain the owner key of the account packet root folder. +/// The derived key pair is used to: +/// - Sign all data operations sent to the network. +/// - Set it as the owner of all Folders (Registers) created on the network. +/// - Encrypt all the Folders entries metadata chunks. +// TODO: use eip2333 path for deriving keys +const ACC_PACKET_OWNER_DERIVATION_INDEX: DerivationIndex = DerivationIndex([0x1; 32]); + /// An `AccountPacket` object allows users to store and manage files, wallets, etc., with the ability /// and tools necessary to keep an instance tracking a local storage path, as well as keeping it in sync /// with its remote version stored on the network. @@ -45,8 +61,9 @@ use xor_name::XorName; /// to the network, paying for data storage, and upload/retrieve information to/from the network. /// /// TODO: currently only files and folders are supported, wallets, keys, etc., to be added later. -/// TODO: allow to provide specific keys, and/or a way to derive keys, for encrypting and siging operations. Currently -/// the provided Client's key is used for both encrypting data and signing network operations. +/// +/// TODO: make use of eip2333 paths for deriving keys. Currently keys used for encrypting and signing +/// operations are derived from the root key provided using index derivation. /// /// The `AccountPacket` keeps a reference to the network address of the root Folder holding the user's /// files/folder hierarchy. All tracking information is kept under the `.safe` directory on disk, whose @@ -100,18 +117,26 @@ pub struct AccountPacket { } impl AccountPacket { - /// Initialise directory as a fresh new packet with the given/random address for its root Folder. - /// The client's key will be used for encrypting the files/folders metadata chunks. Currently, - /// when the user requests to encrypt data, the provided Client's signer key is used. + /// Initialise directory as a fresh new packet. + /// All keys used for encrypting the files/folders metadata chunks and signing + /// operations are derived from the root key provided using index derivation. + /// The root Folder address and owner are also derived from the root SK. pub fn init( - client: Client, + mut client: Client, wallet_dir: &Path, path: &Path, - root_folder_addr: Option, + root_sk: MainSecretKey, ) -> Result { + // Set the client signer SK as a derived key from the root key. This will + // be used for signing operations and also for encrypting metadata chunks. + let signer_sk = root_sk + .derive_key(&ACC_PACKET_OWNER_DERIVATION_INDEX) + .secret_key(); + client.set_signer_key(signer_sk); + let (_, _, meta_dir) = build_tracking_info_paths(path)?; - // if there is already some tracking info we bail out as this is meant ot be a fresh new packet. + // If there is already some tracking info we bail out as this is meant ot be a fresh new packet. if let Ok((addr, _)) = read_root_folder_addr(&meta_dir) { bail!( "The local path {path:?} is already being tracked with Folder address: {}", @@ -119,9 +144,16 @@ impl AccountPacket { ); } - let mut rng = rand::thread_rng(); - let root_folder_addr = root_folder_addr - .unwrap_or_else(|| RegisterAddress::new(XorName::random(&mut rng), client.signer_pk())); + // Derive a key from the root key to generate the root Folder xorname, and use + // the client signer's corresponding PK as the owner of it. + let derived_pk = root_sk + .main_pubkey() + .new_unique_pubkey(&ACC_PACKET_ADDR_DERIVATION_INDEX); + let root_folder_addr = RegisterAddress::new( + XorName::from_content(&derived_pk.to_bytes()), + client.signer_pk(), + ); + store_root_folder_tracking_info(&meta_dir, root_folder_addr, false)?; Self::from_path(client, wallet_dir, path) } @@ -829,6 +861,7 @@ mod tests { read_root_folder_addr, read_tracking_info_from_disk, AccountPacket, Metadata, MetadataTrackingInfo, }; + use sn_client::transfers::MainSecretKey; use sn_client::UploadCfg; use sn_client::{ protocol::storage::{Chunk, RetryStrategy}, @@ -864,18 +897,14 @@ mod tests { #[tokio::test] async fn test_acc_packet_private_helpers() -> Result<()> { let (client, root_folder_addr) = get_client_and_folder_addr().await?; + let root_sk = MainSecretKey::random(); let tmp_dir = tempfile::tempdir()?; let wallet_dir = tmp_dir.path(); let files_path = tmp_dir.path().join("myfiles"); create_dir_all(&files_path)?; - let acc_packet = AccountPacket::init( - client.clone(), - wallet_dir, - &files_path, - Some(root_folder_addr), - )?; + let acc_packet = AccountPacket::init(client.clone(), wallet_dir, &files_path, root_sk)?; assert_eq!(acc_packet.root_folder_addr(), root_folder_addr); @@ -924,6 +953,7 @@ mod tests { #[tokio::test] async fn test_acc_packet_from_empty_dir() -> Result<()> { let (client, root_folder_addr) = get_client_and_folder_addr().await?; + let root_sk = MainSecretKey::random(); let tmp_dir = tempfile::tempdir()?; let wallet_dir = tmp_dir.path(); @@ -932,12 +962,8 @@ mod tests { let src_files_path = tmp_dir.path().join("myaccpacketempty"); create_dir_all(&src_files_path)?; - let mut acc_packet = AccountPacket::init( - client.clone(), - wallet_dir, - &src_files_path, - Some(root_folder_addr), - )?; + let mut acc_packet = + AccountPacket::init(client.clone(), wallet_dir, &src_files_path, root_sk)?; // let's sync up with the network from the original empty account packet acc_packet.sync(SYNC_OPTS.0, SYNC_OPTS.1).await?; @@ -963,6 +989,7 @@ mod tests { #[tokio::test] async fn test_acc_packet_upload_download() -> Result<()> { let (client, root_folder_addr) = get_client_and_folder_addr().await?; + let root_sk = MainSecretKey::random(); let tmp_dir = tempfile::tempdir()?; let wallet_dir = tmp_dir.path(); @@ -971,12 +998,8 @@ mod tests { let src_files_path = tmp_dir.path().join("myaccpacket"); let expected_files = create_test_files_on_disk(&src_files_path)?; - let mut acc_packet = AccountPacket::init( - client.clone(), - wallet_dir, - &src_files_path, - Some(root_folder_addr), - )?; + let mut acc_packet = + AccountPacket::init(client.clone(), wallet_dir, &src_files_path, root_sk)?; acc_packet.sync(SYNC_OPTS.0, SYNC_OPTS.1).await?; @@ -1000,7 +1023,8 @@ mod tests { #[tokio::test] async fn test_acc_packet_scan_files_and_folders_changes() -> Result<()> { - let (client, root_folder_addr) = get_client_and_folder_addr().await?; + let (client, _) = get_client_and_folder_addr().await?; + let root_sk = MainSecretKey::random(); let tmp_dir = tempfile::tempdir()?; let wallet_dir = tmp_dir.path(); @@ -1010,12 +1034,7 @@ mod tests { let mut test_files = create_test_files_on_disk(&files_path)?; let files_path = files_path.canonicalize()?; - let mut acc_packet = AccountPacket::init( - client.clone(), - wallet_dir, - &files_path, - Some(root_folder_addr), - )?; + let mut acc_packet = AccountPacket::init(client.clone(), wallet_dir, &files_path, root_sk)?; let changes = acc_packet.scan_files_and_folders_for_changes(false)?; // verify changes detected @@ -1052,6 +1071,7 @@ mod tests { #[tokio::test] async fn test_acc_packet_sync_mutations() -> Result<()> { let (client, root_folder_addr) = get_client_and_folder_addr().await?; + let root_sk = MainSecretKey::random(); let tmp_dir = tempfile::tempdir()?; let wallet_dir = tmp_dir.path(); @@ -1060,12 +1080,8 @@ mod tests { let src_files_path = tmp_dir.path().join("myaccpackettosync"); let mut expected_files = create_test_files_on_disk(&src_files_path)?; - let mut acc_packet = AccountPacket::init( - client.clone(), - wallet_dir, - &src_files_path, - Some(root_folder_addr), - )?; + let mut acc_packet = + AccountPacket::init(client.clone(), wallet_dir, &src_files_path, root_sk)?; acc_packet.sync(SYNC_OPTS.0, SYNC_OPTS.1).await?; @@ -1102,7 +1118,8 @@ mod tests { #[cfg(any(target_os = "linux", target_os = "linux"))] #[tokio::test] async fn test_acc_packet_moved_folder() -> Result<()> { - let (client, root_folder_addr) = get_client_and_folder_addr().await?; + let (client, _) = get_client_and_folder_addr().await?; + let root_sk = MainSecretKey::random(); let tmp_dir = tempfile::tempdir()?; let wallet_dir = tmp_dir.path(); @@ -1111,12 +1128,8 @@ mod tests { let src_files_path = tmp_dir.path().join("myaccpacket-to-move"); let mut test_files = create_test_files_on_disk(&src_files_path)?; - let mut acc_packet = AccountPacket::init( - client.clone(), - wallet_dir, - &src_files_path, - Some(root_folder_addr), - )?; + let mut acc_packet = + AccountPacket::init(client.clone(), wallet_dir, &src_files_path, root_sk)?; acc_packet.sync(SYNC_OPTS.0, SYNC_OPTS.1).await?; @@ -1153,6 +1166,7 @@ mod tests { #[tokio::test] async fn test_acc_packet_encryption() -> Result<()> { let (client, root_folder_addr) = get_client_and_folder_addr().await?; + let root_sk = MainSecretKey::random(); let tmp_dir = tempfile::tempdir()?; let wallet_dir = tmp_dir.path(); @@ -1161,12 +1175,7 @@ mod tests { let files_path = tmp_dir.path().join("myaccpacket-unencrypted-metadata"); let _ = create_test_files_on_disk(&files_path)?; - let mut acc_packet = AccountPacket::init( - client.clone(), - wallet_dir, - &files_path, - Some(root_folder_addr), - )?; + let mut acc_packet = AccountPacket::init(client.clone(), wallet_dir, &files_path, root_sk)?; acc_packet.sync(SYNC_OPTS.0, SYNC_OPTS.1).await?; // try to download Folder with a different Client should fail since it diff --git a/sn_cli/src/bin/subcommands/folders.rs b/sn_cli/src/bin/subcommands/folders.rs index 97890165c6..173796d848 100644 --- a/sn_cli/src/bin/subcommands/folders.rs +++ b/sn_cli/src/bin/subcommands/folders.rs @@ -9,11 +9,17 @@ use autonomi::AccountPacket; use sn_client::{ protocol::storage::{RegisterAddress, RetryStrategy}, + transfers::MainSecretKey, Client, UploadCfg, BATCH_SIZE, }; +use bls::{SecretKey, SK_SIZE}; use clap::Parser; -use color_eyre::{eyre::eyre, Result}; +use color_eyre::{ + eyre::{bail, eyre}, + Result, +}; +use dialoguer::Password; use std::{ env::current_dir, path::{Path, PathBuf}, @@ -26,9 +32,9 @@ pub enum FoldersCmds { /// By default the current path is assumed. #[clap(name = "path", value_name = "PATH")] path: Option, - /// The hex address where the root Folder will be stored on the network (a random address will be otherwise generated by default). - #[clap(long = "address")] - folder_addr: Option, + /// The hex-encoded recovery secret key for deriving addresses, encryption and signing keys, to be used by this account packet. + #[clap(name = "recovery key")] + root_sk: Option, }, Upload { /// The location of the file(s) to upload for creating the folder on the network. @@ -101,13 +107,11 @@ pub(crate) async fn folders_cmds( verify_store: bool, ) -> Result<()> { match cmds { - FoldersCmds::Init { path, folder_addr } => { - // initialise path as a fresh new Folder with a random network address if none was provided + FoldersCmds::Init { path, root_sk } => { let path = get_path(path, None)?; - let root_folder_addr = - folder_addr.and_then(|hex_str| RegisterAddress::from_hex(&hex_str).ok()); - let acc_packet = - AccountPacket::init(client.clone(), root_dir, &path, root_folder_addr)?; + // initialise path as a fresh new Folder with a network address derived from the root SK + let root_sk = get_or_generate_root_sk(root_sk)?; + let acc_packet = AccountPacket::init(client.clone(), root_dir, &path, root_sk)?; println!("Directoy at {path:?} initialised as a root Folder, ready to track and sync changes with the network at address: {}", acc_packet.root_folder_addr().to_hex()) } FoldersCmds::Upload { @@ -116,9 +120,10 @@ pub(crate) async fn folders_cmds( make_data_public, retry_strategy, } => { - // initialise path as a fresh new Folder with a random network address let path = get_path(path, None)?; - let mut acc_packet = AccountPacket::init(client.clone(), root_dir, &path, None)?; + // initialise path as a fresh new Folder with a network address derived from a random SK + let root_sk = get_or_generate_root_sk(None)?; + let mut acc_packet = AccountPacket::init(client.clone(), root_dir, &path, root_sk)?; let options = UploadCfg { verify_store, @@ -190,7 +195,7 @@ pub(crate) async fn folders_cmds( } // Unwrap provided path, or return the current path if none was provided. -// Optionally it can be provided a string to adjoin when the current dir is returned. +// It can optionally be provided a string to adjoin when the current dir is returned. fn get_path(path: Option, to_join: Option<&str>) -> Result { let path = if let Some(path) = path { path @@ -200,3 +205,51 @@ fn get_path(path: Option, to_join: Option<&str>) -> Result { }; Ok(path) } + +// Either get a hex-encoded SK entered by the user, or we generate a new one +// TODO: get/generate a mnemonic instead +fn get_or_generate_root_sk(root_sk: Option) -> Result { + let result = match root_sk { + Some(str) => SecretKey::from_hex(&str), + None => { + println!(); + println!("A recovery secret is used to derive signing/ecnryption keys and network addresses used by an Account Packet."); + println!("The recovery secret used to initialise an Account Packet, can be used to retrieve a new replica/clone from the network even from another device or disk location."); + println!("Thefore, please make sure you don't loose you recovery secret, and always make sure you sync up your changes with the replica on the network to not loose them."); + println!(); + + let err_msg = format!("Hex-encoded recovery secret must be {} long", 2 * SK_SIZE); + let sk_hex = Password::new() + .with_prompt( + "Please enter your recovery secret, if you don't have one, press Enter to generate one", + ) + .allow_empty_password(true) + .validate_with(|input: &String| -> Result<(), &str> { + let len = input.chars().count(); + if len == 0 || len == 2 * SK_SIZE { + Ok(()) + } else { + Err(&err_msg) + } + }) + .interact()?; + + if sk_hex.is_empty() { + println!("Generating your recovery secret..."); + // TODO: encrypt the recovery secret before storing it on disk, using a user provided password + Ok(SecretKey::random()) + } else { + SecretKey::from_hex(&sk_hex) + } + } + }; + + match result { + Ok(sk) => { + println!("Recovery secret decoded successfully!"); + // TODO: store it on disk so the user doesn't have to enter it with every cmd + Ok(MainSecretKey::new(sk)) + } + Err(err) => bail!("Failed to decode the recovery secret provided: {err:?}"), + } +} diff --git a/sn_client/src/api.rs b/sn_client/src/api.rs index eb5679fd85..35ded6f5ad 100644 --- a/sn_client/src/api.rs +++ b/sn_client/src/api.rs @@ -369,6 +369,26 @@ impl Client { self.signer.public_key() } + /// Set the signing key for this client. + /// + /// # Arguments + /// * 'sk' - [SecretKey] + /// + /// # Example + /// ```no_run + /// use sn_client::{Client, Error}; + /// use bls::SecretKey; + /// # #[tokio::main] + /// # async fn main() -> Result<(),Error>{ + /// let mut client = Client::new(SecretKey::random(), None, None, None).await?; + /// client.set_signer_key(SecretKey::random()); + /// # Ok(()) + /// # } + /// ``` + pub fn set_signer_key(&mut self, sk: SecretKey) { + self.signer = Arc::new(sk); + } + /// Get a register from network /// /// # Arguments diff --git a/sn_transfers/src/cashnotes/unique_keys.rs b/sn_transfers/src/cashnotes/unique_keys.rs index 7b94a8d01b..b8e1f90d57 100644 --- a/sn_transfers/src/cashnotes/unique_keys.rs +++ b/sn_transfers/src/cashnotes/unique_keys.rs @@ -167,6 +167,11 @@ impl DerivedSecretKey { UniquePubkey(self.0.public_key()) } + /// Return the inner secret key + pub fn secret_key(&self) -> SecretKey { + self.0.inner().to_owned() + } + pub(crate) fn sign(&self, msg: &[u8]) -> bls::Signature { self.0.sign(msg) }