diff --git a/autonomi/Cargo.toml b/autonomi/Cargo.toml index c7ecf07338..12dbf13cf9 100644 --- a/autonomi/Cargo.toml +++ b/autonomi/Cargo.toml @@ -13,10 +13,10 @@ repository = "https://github.com/maidsafe/safe_network" crate-type = ["cdylib", "rlib"] [features] -default = ["data"] +default = ["data", "vault"] full = ["data", "registers", "vault"] data = [] -vault = ["data"] +vault = ["data", "registers"] fs = ["tokio/fs", "data"] local = ["sn_networking/local", "test_utils/local", "sn_evm/local"] registers = ["data"] diff --git a/autonomi/src/client/archive.rs b/autonomi/src/client/archive.rs index 2e4b1b7e4a..17055e0682 100644 --- a/autonomi/src/client/archive.rs +++ b/autonomi/src/client/archive.rs @@ -14,13 +14,12 @@ use std::{ use sn_networking::target_arch::{Duration, SystemTime, UNIX_EPOCH}; use super::{ - data::DataAddr, - data::{GetError, PutError}, + data::{CostError, DataAddr, GetError, PutError}, Client, }; use bytes::Bytes; use serde::{Deserialize, Serialize}; -use sn_evm::EvmWallet; +use sn_evm::{AttoTokens, EvmWallet}; use xor_name::XorName; /// The address of an archive on the network. Points to an [`Archive`]. @@ -36,13 +35,13 @@ pub enum RenameError { /// An archive of files that containing file paths, their metadata and the files data addresses /// Using archives is useful for uploading entire directories to the network, only needing to keep track of a single address. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct Archive { map: HashMap, } /// Metadata for a file in an archive -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Metadata { pub created: u64, pub modified: u64, @@ -147,12 +146,6 @@ impl Archive { } } -impl Default for Archive { - fn default() -> Self { - Self::new() - } -} - impl Client { /// Fetch an archive from the network pub async fn archive_get(&self, addr: ArchiveAddr) -> Result { @@ -171,4 +164,12 @@ impl Client { .map_err(|e| PutError::Serialization(format!("Failed to serialize archive: {e:?}")))?; self.data_put(bytes, wallet).await } + + /// Get the cost to upload an archive + pub async fn archive_cost(&self, archive: Archive) -> Result { + let bytes = archive + .into_bytes() + .map_err(|e| CostError::Serialization(format!("Failed to serialize archive: {e:?}")))?; + self.data_cost(bytes).await + } } diff --git a/autonomi/src/client/data.rs b/autonomi/src/client/data.rs index 366ad643be..d417978b81 100644 --- a/autonomi/src/client/data.rs +++ b/autonomi/src/client/data.rs @@ -39,7 +39,7 @@ pub enum PutError { Network(#[from] NetworkError), #[error("Error occurred during payment.")] PayError(#[from] PayError), - #[error("Failed to serialize {0}")] + #[error("Serialization error: {0}")] Serialization(String), #[error("A wallet error occurred.")] Wallet(#[from] sn_evm::EvmError), @@ -82,6 +82,8 @@ pub enum CostError { CouldNotGetStoreQuote(XorName), #[error("Could not get store costs: {0:?}")] CouldNotGetStoreCosts(NetworkError), + #[error("Failed to serialize {0}")] + Serialization(String), } impl Client { diff --git a/autonomi/src/client/mod.rs b/autonomi/src/client/mod.rs index df5dab4ec0..2205d51cd5 100644 --- a/autonomi/src/client/mod.rs +++ b/autonomi/src/client/mod.rs @@ -18,6 +18,8 @@ pub mod fs; pub mod registers; #[cfg(feature = "vault")] pub mod vault; +#[cfg(feature = "vault")] +pub mod vault_user_data; #[cfg(target_arch = "wasm32")] pub mod wasm; diff --git a/autonomi/src/client/vault.rs b/autonomi/src/client/vault.rs index 02eda1f4a6..4004a3d530 100644 --- a/autonomi/src/client/vault.rs +++ b/autonomi/src/client/vault.rs @@ -7,17 +7,18 @@ // permissions and limitations relating to use of the SAFE Network Software. use std::collections::HashSet; +use std::hash::{DefaultHasher, Hash, Hasher}; use crate::client::data::PutError; use crate::client::Client; use bls::SecretKey; -use bytes::Bytes; use libp2p::kad::{Quorum, Record}; use sn_evm::EvmWallet; use sn_networking::{GetRecordCfg, NetworkError, PutRecordCfg, VerificationKind}; use sn_protocol::storage::{ try_serialize_record, RecordKind, RetryStrategy, Scratchpad, ScratchpadAddress, }; +use sn_protocol::Bytes; use sn_protocol::{storage::try_deserialize_record, NetworkAddress}; use tracing::info; @@ -33,16 +34,32 @@ pub enum VaultError { Network(#[from] NetworkError), } +/// The content type of the vault data +/// The number is used to determine the type of the contents of the bytes contained in a vault +/// Custom apps can use this to store their own custom types of data in vaults +/// It is recommended to use the hash of the app name or an unique identifier as the content type using [`app_name_to_vault_content_type`] +/// The value 0 is reserved for tests +pub type VaultContentType = u64; + +/// For custom apps using Scratchpad, this function converts an app identifier or name to a [`VaultContentType`] +pub fn app_name_to_vault_content_type(s: T) -> VaultContentType { + let mut hasher = DefaultHasher::new(); + s.hash(&mut hasher); + hasher.finish() +} + impl Client { /// Retrieves and returns a decrypted vault if one exists. + /// Returns the content type of the bytes in the vault pub async fn fetch_and_decrypt_vault( &self, secret_key: &SecretKey, - ) -> Result, VaultError> { + ) -> Result<(Bytes, VaultContentType), VaultError> { info!("Fetching and decrypting vault"); let pad = self.get_vault_from_network(secret_key).await?; - Ok(pad.decrypt_data(secret_key)?) + let data = pad.decrypt_data(secret_key)?; + Ok((data, pad.data_encoding())) } /// Gets the vault Scratchpad from a provided client public key @@ -81,14 +98,16 @@ impl Client { /// Put data into the client's VaultPacket /// - /// Pays for a new VaultPacket if none yet created for the client. Returns the current version - /// of the data on success. + /// Pays for a new VaultPacket if none yet created for the client. + /// Provide the bytes to be written to the vault and the content type of those bytes. + /// It is recommended to use the hash of the app name or unique identifier as the content type. pub async fn write_bytes_to_vault( &self, data: Bytes, wallet: &EvmWallet, secret_key: &SecretKey, - ) -> Result { + content_type: VaultContentType, + ) -> Result<(), PutError> { let client_pk = secret_key.public_key(); let pad_res = self.get_vault_from_network(secret_key).await; @@ -106,10 +125,10 @@ impl Client { existing_data } else { trace!("new scratchpad creation"); - Scratchpad::new(client_pk) + Scratchpad::new(client_pk, content_type) }; - let next_count = scratch.update_and_sign(data, secret_key); + let _next_count = scratch.update_and_sign(data, secret_key); let scratch_address = scratch.network_address(); let scratch_key = scratch_address.to_record_key(); @@ -181,6 +200,6 @@ impl Client { ) })?; - Ok(next_count) + Ok(()) } } diff --git a/autonomi/src/client/vault_user_data.rs b/autonomi/src/client/vault_user_data.rs new file mode 100644 index 0000000000..779cf023d9 --- /dev/null +++ b/autonomi/src/client/vault_user_data.rs @@ -0,0 +1,123 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use std::collections::HashMap; +use std::collections::HashSet; + +use super::archive::ArchiveAddr; +use super::data::GetError; +use super::data::PutError; +use super::registers::RegisterAddress; +use super::vault::VaultError; +use super::Client; +use crate::client::vault::{app_name_to_vault_content_type, VaultContentType}; +use bls::SecretKey; +use serde::{Deserialize, Serialize}; +use sn_evm::EvmWallet; +use sn_protocol::Bytes; + +use std::sync::LazyLock; + +/// Vault content type for UserDataVault +pub static USER_DATA_VAULT_CONTENT_IDENTIFIER: LazyLock = + LazyLock::new(|| app_name_to_vault_content_type("UserData")); + +/// UserData is stored in Vaults and contains most of a user's private data: +/// It allows users to keep track of only the key to their User Data Vault +/// while having the rest kept on the Network encrypted in a Vault for them +/// Using User Data Vault is optional, one can decide to keep all their data locally instead. +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +pub struct UserData { + /// The register secret key hex encoded + pub register_sk: Option, + /// Owned register addresses + pub registers: HashSet, + /// Owned file archive addresses + pub file_archives: HashSet, + + /// Owner register names, providing it is optional + pub register_names: HashMap, + /// Owned file archive addresses along with a name for that archive providing it is optional + pub file_archive_names: HashMap, +} + +/// Errors that can occur during the get operation. +#[derive(Debug, thiserror::Error)] +pub enum UserDataVaultGetError { + #[error("Vault error: {0}")] + Vault(#[from] VaultError), + #[error("Unsupported vault content type: {0}")] + UnsupportedVaultContentType(VaultContentType), + #[error("Serialization error: {0}")] + Serialization(String), + #[error("Get error: {0}")] + GetError(#[from] GetError), +} + +impl UserData { + /// Create a new empty UserData + pub fn new() -> Self { + Self::default() + } + + /// To bytes + pub fn to_bytes(&self) -> Result { + let bytes = rmp_serde::to_vec(&self)?; + Ok(Bytes::from(bytes)) + } + + /// From bytes + pub fn from_bytes(bytes: Bytes) -> Result { + let vault_content = rmp_serde::from_slice(&bytes)?; + Ok(vault_content) + } +} + +impl Client { + /// Get the user data from the vault + pub async fn get_user_data_from_vault( + &self, + secret_key: &SecretKey, + ) -> Result { + let (bytes, content_type) = self.fetch_and_decrypt_vault(secret_key).await?; + + if content_type != *USER_DATA_VAULT_CONTENT_IDENTIFIER { + return Err(UserDataVaultGetError::UnsupportedVaultContentType( + content_type, + )); + } + + let vault = UserData::from_bytes(bytes).map_err(|e| { + UserDataVaultGetError::Serialization(format!( + "Failed to deserialize vault content: {e}" + )) + })?; + + Ok(vault) + } + + /// Put the user data to the vault + pub async fn put_user_data_to_vault( + &self, + secret_key: &SecretKey, + wallet: &EvmWallet, + user_data: UserData, + ) -> Result<(), PutError> { + let bytes = user_data + .to_bytes() + .map_err(|e| PutError::Serialization(format!("Failed to serialize user data: {e}")))?; + self.write_bytes_to_vault( + bytes, + wallet, + secret_key, + *USER_DATA_VAULT_CONTENT_IDENTIFIER, + ) + .await?; + Ok(()) + } +} diff --git a/autonomi/src/client/wasm.rs b/autonomi/src/client/wasm.rs index 34400356f2..3bc2504636 100644 --- a/autonomi/src/client/wasm.rs +++ b/autonomi/src/client/wasm.rs @@ -2,6 +2,10 @@ use libp2p::Multiaddr; use wasm_bindgen::prelude::*; use super::address::{addr_to_str, str_to_addr}; +use super::vault_user_data::UserData; + +#[wasm_bindgen(js_name = UserData)] +pub struct JsUserData(UserData); #[wasm_bindgen(js_name = Client)] pub struct JsClient(super::Client); @@ -115,33 +119,31 @@ mod vault { #[wasm_bindgen(js_class = Client)] impl JsClient { - #[wasm_bindgen(js_name = fetchAndDecryptVault)] - pub async fn fetch_and_decrypt_vault( + #[wasm_bindgen(js_name = getUserDataFromVault)] + pub async fn get_user_data_from_vault( &self, secret_key: Vec, - ) -> Result>, JsError> { + ) -> Result { let secret_key: [u8; 32] = secret_key[..].try_into()?; let secret_key = SecretKey::from_bytes(secret_key)?; - let vault = self.0.fetch_and_decrypt_vault(&secret_key).await?; - let vault = vault.map(|v| v.to_vec()); + let user_data = self.0.get_user_data_from_vault(&secret_key).await?; - Ok(vault) + Ok(JsUserData(user_data)) } - #[wasm_bindgen(js_name = writeBytesToVault)] - pub async fn write_bytes_to_vault( + #[wasm_bindgen(js_name = putUserDataToVault)] + pub async fn put_user_data_to_vault( &self, - vault: Vec, + user_data: JsUserData, wallet: &mut JsWallet, secret_key: Vec, ) -> Result<(), JsError> { let secret_key: [u8; 32] = secret_key[..].try_into()?; let secret_key = SecretKey::from_bytes(secret_key)?; - let vault = bytes::Bytes::from(vault); self.0 - .write_bytes_to_vault(vault, &mut wallet.0, &secret_key) + .put_user_data_to_vault(&secret_key, &wallet.0, user_data.0) .await?; Ok(()) diff --git a/autonomi/tests/fs.rs b/autonomi/tests/fs.rs index 9c53fd26b8..b952852bc2 100644 --- a/autonomi/tests/fs.rs +++ b/autonomi/tests/fs.rs @@ -91,24 +91,22 @@ async fn file_into_vault() -> Result<()> { sleep(Duration::from_secs(2)).await; let archive = client.archive_get(addr).await?; + let set_version = 0; client - .write_bytes_to_vault(archive.into_bytes()?, &wallet, &client_sk) + .write_bytes_to_vault(archive.into_bytes()?, &wallet, &client_sk, set_version) .await?; // now assert over the stored account packet let new_client = Client::connect(&[]).await?; - if let Some(ap) = new_client.fetch_and_decrypt_vault(&client_sk).await? { - let ap_archive_fetched = autonomi::client::archive::Archive::from_bytes(ap)?; + let (ap, got_version) = new_client.fetch_and_decrypt_vault(&client_sk).await?; + assert_eq!(set_version, got_version); + let ap_archive_fetched = autonomi::client::archive::Archive::from_bytes(ap)?; - assert_eq!( - archive.iter().count(), - ap_archive_fetched.iter().count(), - "archive fetched should match archive put" - ); - } else { - eyre::bail!("No account packet found"); - } + assert_eq!( + archive, ap_archive_fetched, + "archive fetched should match archive put" + ); Ok(()) } diff --git a/sn_networking/src/record_store.rs b/sn_networking/src/record_store.rs index 35b1cdec59..4ac9170e85 100644 --- a/sn_networking/src/record_store.rs +++ b/sn_networking/src/record_store.rs @@ -1238,7 +1238,7 @@ mod tests { let owner_sk = SecretKey::random(); let owner_pk = owner_sk.public_key(); - let mut scratchpad = Scratchpad::new(owner_pk); + let mut scratchpad = Scratchpad::new(owner_pk, 0); let _next_version = scratchpad.update_and_sign(unencrypted_scratchpad_data.clone(), &owner_sk); @@ -1283,8 +1283,7 @@ mod tests { let decrypted_data = scratchpad.decrypt_data(&owner_sk)?; assert_eq!( - decrypted_data, - Some(unencrypted_scratchpad_data), + decrypted_data, unencrypted_scratchpad_data, "Stored scratchpad data should match original" ); } diff --git a/sn_protocol/src/error.rs b/sn_protocol/src/error.rs index 8462ff85f3..2d24feb0d9 100644 --- a/sn_protocol/src/error.rs +++ b/sn_protocol/src/error.rs @@ -51,6 +51,9 @@ pub enum Error { /// The provided SecretyKey failed to decrypt the data #[error("Failed to derive CipherText from encrypted_data")] ScratchpadCipherTextFailed, + /// The provided cypher text is invalid + #[error("Provided cypher text is invalid")] + ScratchpadCipherTextInvalid, // ---------- payment errors #[error("There was an error getting the storecost from kademlia store")] diff --git a/sn_protocol/src/lib.rs b/sn_protocol/src/lib.rs index 4d3b92628d..f397173ca1 100644 --- a/sn_protocol/src/lib.rs +++ b/sn_protocol/src/lib.rs @@ -32,7 +32,10 @@ pub use error::Error; use storage::ScratchpadAddress; use self::storage::{ChunkAddress, RegisterAddress, SpendAddress}; -use bytes::Bytes; + +/// Re-export of Bytes used throughout the protocol +pub use bytes::Bytes; + use libp2p::{ kad::{KBucketDistance as Distance, KBucketKey as Key, RecordKey}, multiaddr::Protocol, diff --git a/sn_protocol/src/storage/scratchpad.rs b/sn_protocol/src/storage/scratchpad.rs index ea38d2e686..5c99cbdcac 100644 --- a/sn_protocol/src/storage/scratchpad.rs +++ b/sn_protocol/src/storage/scratchpad.rs @@ -8,9 +8,9 @@ use super::ScratchpadAddress; use crate::error::{Error, Result}; +use crate::Bytes; use crate::NetworkAddress; use bls::{Ciphertext, PublicKey, SecretKey, Signature}; -use bytes::Bytes; use serde::{Deserialize, Serialize}; use xor_name::XorName; @@ -23,6 +23,8 @@ pub struct Scratchpad { /// Network address. Omitted when serialising and /// calculated from the `encrypted_data` when deserialising. address: ScratchpadAddress, + /// Data encoding: custom apps using scratchpad should use this so they can identify the type of data they are storing + data_encoding: u64, /// Contained data. This should be encrypted #[debug(skip)] encrypted_data: Bytes, @@ -35,10 +37,11 @@ pub struct Scratchpad { impl Scratchpad { /// Creates a new instance of `Scratchpad`. - pub fn new(owner: PublicKey) -> Self { + pub fn new(owner: PublicKey, data_encoding: u64) -> Self { Self { address: ScratchpadAddress::new(owner), encrypted_data: Bytes::new(), + data_encoding, counter: 0, signature: None, } @@ -49,6 +52,11 @@ impl Scratchpad { self.counter } + /// Return the current data encoding + pub fn data_encoding(&self) -> u64 { + self.data_encoding + } + /// Increments the counter value. pub fn increment(&mut self) -> u64 { self.counter += 1; @@ -94,13 +102,13 @@ impl Scratchpad { } /// Returns the encrypted_data, decrypted via the passed SecretKey - pub fn decrypt_data(&self, sk: &SecretKey) -> Result> { - Ok(sk - .decrypt( - &Ciphertext::from_bytes(&self.encrypted_data) - .map_err(|_| Error::ScratchpadCipherTextFailed)?, - ) - .map(Bytes::from)) + pub fn decrypt_data(&self, sk: &SecretKey) -> Result { + let cipher = Ciphertext::from_bytes(&self.encrypted_data) + .map_err(|_| Error::ScratchpadCipherTextFailed)?; + let bytes = sk + .decrypt(&cipher) + .ok_or(Error::ScratchpadCipherTextInvalid)?; + Ok(Bytes::from(bytes)) } /// Returns the encrypted_data hash