diff --git a/wnfs/src/private/directory.rs b/wnfs/src/private/directory.rs index 926e5eb5..58bda558 100644 --- a/wnfs/src/private/directory.rs +++ b/wnfs/src/private/directory.rs @@ -1,7 +1,7 @@ use super::{ - encrypted::Encrypted, link::PrivateLink, PrivateDirectoryContentSerializable, PrivateFile, - PrivateForest, PrivateNode, PrivateNodeContentSerializable, PrivateNodeHeader, PrivateRef, - TemporalKey, + encrypted::Encrypted, link::PrivateLink, AesKey, PrivateDirectoryContentSerializable, + PrivateFile, PrivateForest, PrivateNode, PrivateNodeContentSerializable, PrivateNodeHeader, + PrivateRef, SnapshotKey, TemporalKey, KEY_BYTE_SIZE, }; use crate::{error::FsError, traits::Id, SearchResult, WNFS_VERSION}; use anyhow::{bail, ensure, Result}; @@ -1459,7 +1459,7 @@ impl PrivateDirectory { } /// Creates a new [`PrivateDirectory`] from a [`PrivateDirectoryContentSerializable`]. - pub(crate) async fn from_serializable( + pub(crate) async fn from_serializable_temporal( serializable: PrivateDirectoryContentSerializable, temporal_key: &TemporalKey, cid: Cid, @@ -1483,9 +1483,47 @@ impl PrivateDirectory { entries: entries_decrypted, }; - let header = PrivateNodeHeader::load(&serializable.header_cid, temporal_key, store).await?; + let header = + PrivateNodeHeader::load_temporal(&serializable.header_cid, temporal_key, store).await?; Ok(Self { header, content }) } + + #[allow(dead_code)] + /// Creates a new [`PrivateDirectory`] from a [`PrivateDirectoryContentSerializable`]. + pub(crate) async fn from_serializable_snapshot( + serializable: PrivateDirectoryContentSerializable, + snapshot_key: &SnapshotKey, + cid: Cid, + store: &impl BlockStore, + ) -> Result { + if serializable.version.major != 0 || serializable.version.minor != 2 { + bail!(FsError::UnexpectedVersion(serializable.version)); + } + + let mut entries_decrypted = BTreeMap::new(); + // let temporal_key = TemporalKey(snapshot_key.0.to_owned()); + for (name, private_ref_serializable) in serializable.entries { + let private_ref = PrivateRef { + saturated_name_hash: private_ref_serializable.saturated_name_hash, + // What are we supposed to do here in the absence of a parent key? This node is not decryptable + temporal_key: TemporalKey(AesKey::new([0u8; KEY_BYTE_SIZE])), + content_cid: private_ref_serializable.content_cid, + }; + entries_decrypted.insert(name, PrivateLink::from_ref(private_ref)); + } + + let content = PrivateDirectoryContent { + persisted_as: OnceCell::new_with(Some(cid)), + metadata: serializable.metadata, + previous: serializable.previous.into_iter().collect(), + entries: entries_decrypted, + }; + + let header = + PrivateNodeHeader::load_snapshot(&serializable.header_cid, snapshot_key, store).await?; + Ok(Self { header, content }) + } + /// Wraps the directory in a [`PrivateNode`]. pub fn as_node(self: &Rc) -> PrivateNode { PrivateNode::Dir(Rc::clone(self)) diff --git a/wnfs/src/private/file.rs b/wnfs/src/private/file.rs index 66bc2c02..ed1b9091 100644 --- a/wnfs/src/private/file.rs +++ b/wnfs/src/private/file.rs @@ -808,7 +808,31 @@ impl PrivateFile { content: serializable.content, }; - let header = PrivateNodeHeader::load(&serializable.header_cid, temporal_key, store).await?; + let header = + PrivateNodeHeader::load_temporal(&serializable.header_cid, temporal_key, store).await?; + Ok(Self { header, content }) + } + + /// Creates a new [`PrivateFile`] from a [`PrivateFileContentSerializable`] but only a Snapshot. + pub(crate) async fn from_serializable_snapshot( + serializable: PrivateFileContentSerializable, + snapshot_key: &SnapshotKey, + cid: Cid, + store: &impl BlockStore, + ) -> Result { + if serializable.version.major != 0 || serializable.version.minor != 2 { + bail!(FsError::UnexpectedVersion(serializable.version)); + } + + let content = PrivateFileContent { + persisted_as: OnceCell::new_with(Some(cid)), + previous: serializable.previous.into_iter().collect(), + metadata: serializable.metadata, + content: serializable.content, + }; + + let header = + PrivateNodeHeader::load_snapshot(&serializable.header_cid, snapshot_key, store).await?; Ok(Self { header, content }) } diff --git a/wnfs/src/private/node/header.rs b/wnfs/src/private/node/header.rs index 9e69c9d8..96aaed4a 100644 --- a/wnfs/src/private/node/header.rs +++ b/wnfs/src/private/node/header.rs @@ -1,12 +1,12 @@ -use super::TemporalKey; +use super::{SnapshotKey, TemporalKey}; use crate::private::RevisionRef; use anyhow::Result; -use libipld::{Cid, IpldCodec}; +use libipld::{Cid, Ipld, IpldCodec}; use rand_core::RngCore; use serde::{Deserialize, Serialize}; use sha3::Sha3_256; use skip_ratchet::Ratchet; -use std::fmt::Debug; +use std::{collections::BTreeMap, fmt::Debug}; use wnfs_common::{utils, BlockStore, HashOutput, HASH_BYTE_SIZE}; use wnfs_hamt::Hasher; use wnfs_namefilter::Namefilter; @@ -216,21 +216,103 @@ impl PrivateNodeHeader { /// BlockStore and returns its CID. pub async fn store(&self, store: &impl BlockStore) -> Result { let temporal_key = self.derive_temporal_key(); - let cbor_bytes = serde_ipld_dagcbor::to_vec(self)?; - let ciphertext = temporal_key.key_wrap_encrypt(&cbor_bytes)?; - store.put_block(ciphertext, IpldCodec::Raw).await + let snapshot_key = TemporalKey(temporal_key.derive_snapshot_key().0); + + let inumber_bytes = + snapshot_key.key_wrap_encrypt(&serde_ipld_dagcbor::to_vec(&self.inumber)?)?; + let ratchet_bytes = + temporal_key.key_wrap_encrypt(&serde_ipld_dagcbor::to_vec(&self.ratchet)?)?; + let bare_name_bytes = + snapshot_key.key_wrap_encrypt(&serde_ipld_dagcbor::to_vec(&self.bare_name)?)?; + + let inumber_cid = store.put_block(inumber_bytes, IpldCodec::Raw).await?; + let ratchet_cid = store.put_block(ratchet_bytes, IpldCodec::Raw).await?; + let bare_name_cid = store.put_block(bare_name_bytes, IpldCodec::Raw).await?; + + let mut map = >::new(); + map.insert("inumber".to_string(), Ipld::Link(inumber_cid)); + map.insert("ratchet".to_string(), Ipld::Link(ratchet_cid)); + map.insert("bare_name".to_string(), Ipld::Link(bare_name_cid)); + + let ipld_bytes = serde_ipld_dagcbor::to_vec(&Ipld::Map(map))?; + store.put_block(ipld_bytes, IpldCodec::Raw).await } + // async fn load_bytes(cid: &Cid, store: &impl BlockStore) -> Result<(Vec)> { + + // } + /// Loads a private node header from a given CID linking to the ciphertext block /// to be decrypted with given key. - pub(crate) async fn load( + pub(crate) async fn load_temporal( cid: &Cid, temporal_key: &TemporalKey, store: &impl BlockStore, ) -> Result { - let ciphertext = store.get_block(cid).await?; - let cbor_bytes = temporal_key.key_wrap_decrypt(&ciphertext)?; - Ok(serde_ipld_dagcbor::from_slice(&cbor_bytes)?) + let snapshot_key = temporal_key.derive_snapshot_key(); + + let ipld_bytes = store.get_block(cid).await?; + let Ipld::Map(map) = serde_ipld_dagcbor::from_slice(&ipld_bytes)? else { + return Err(anyhow::anyhow!("Unable to deserialize ipld map")); + }; + + let Some(Ipld::Link(inumber_cid)) = map.get("inumber") else { + return Err(anyhow::anyhow!("Missing inumber_cid")); + }; + let Some(Ipld::Link(ratchet_cid)) = map.get("ratchet") else { + return Err(anyhow::anyhow!("Missing ratchet_cid")); + }; + let Some(Ipld::Link(bare_name_cid)) = map.get("bare_name") else { + return Err(anyhow::anyhow!("Missing bare_name_cid")); + }; + + let inumber_bytes = TemporalKey(snapshot_key.0.to_owned()) + .key_wrap_decrypt(&store.get_block(inumber_cid).await?)?; + let ratchet_bytes = temporal_key.key_wrap_decrypt(&store.get_block(ratchet_cid).await?)?; + let bare_name_bytes = TemporalKey(snapshot_key.0.to_owned()) + .key_wrap_decrypt(&store.get_block(bare_name_cid).await?)?; + + let inumber: [u8; HASH_BYTE_SIZE] = serde_ipld_dagcbor::from_slice(&inumber_bytes)?; + let ratchet: Ratchet = serde_ipld_dagcbor::from_slice(&ratchet_bytes)?; + let bare_name: Namefilter = serde_ipld_dagcbor::from_slice(&bare_name_bytes)?; + + Ok(Self { + inumber, + ratchet, + bare_name, + }) + } + + pub(crate) async fn load_snapshot( + cid: &Cid, + snapshot_key: &SnapshotKey, + store: &impl BlockStore, + ) -> Result { + let ipld_bytes = store.get_block(cid).await?; + let Ipld::Map(map) = serde_ipld_dagcbor::from_slice(&ipld_bytes)? else { + return Err(anyhow::anyhow!("Unable to deserialize ipld map")); + }; + + let Some(Ipld::Link(inumber_cid)) = map.get("inumber") else { + return Err(anyhow::anyhow!("Missing inumber_cid")); + }; + let Some(Ipld::Link(bare_name_cid)) = map.get("bare_name") else { + return Err(anyhow::anyhow!("Missing bare_name_cid")); + }; + + let inumber_bytes = TemporalKey(snapshot_key.0.to_owned()) + .key_wrap_decrypt(&store.get_block(inumber_cid).await?)?; + let bare_name_bytes = TemporalKey(snapshot_key.0.to_owned()) + .key_wrap_decrypt(&store.get_block(bare_name_cid).await?)?; + + let inumber: [u8; HASH_BYTE_SIZE] = serde_ipld_dagcbor::from_slice(&inumber_bytes)?; + let bare_name: Namefilter = serde_ipld_dagcbor::from_slice(&bare_name_bytes)?; + + Ok(Self { + inumber, + ratchet: Ratchet::default(), + bare_name, + }) } } diff --git a/wnfs/src/private/node/node.rs b/wnfs/src/private/node/node.rs index 5732a9d8..781af341 100644 --- a/wnfs/src/private/node/node.rs +++ b/wnfs/src/private/node/node.rs @@ -1,13 +1,13 @@ -use super::{PrivateNodeHeader, TemporalKey}; +use super::{PrivateNodeHeader, SnapshotKey, TemporalKey}; use crate::{ error::FsError, private::{ - encrypted::Encrypted, link::PrivateLink, PrivateDirectory, PrivateFile, PrivateForest, - PrivateNodeContentSerializable, PrivateRef, + encrypted::Encrypted, link::PrivateLink, share::SnapshotSharePointer, PrivateDirectory, + PrivateFile, PrivateForest, PrivateNodeContentSerializable, PrivateRef, }, traits::Id, }; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use async_once_cell::OnceCell; use async_recursion::async_recursion; use chrono::{DateTime, Utc}; @@ -481,9 +481,46 @@ impl PrivateNode { _ => return Err(FsError::NotFound.into()), }; + // let snapshot_key = private_ref.temporal_key.derive_snapshot_key(); Self::from_cid(cid, &private_ref.temporal_key, store).await } + /// A version of the load function designed to work when only a SnapshotKey is available + pub async fn load_from_snapshot( + snapshot: SnapshotSharePointer, + forest: &PrivateForest, + store: &impl BlockStore, + ) -> Result { + let cid = match forest.get_encrypted(&snapshot.label, store).await? { + Some(cids) if cids.contains(&snapshot.content_cid) => snapshot.content_cid, + _ => return Err(FsError::NotFound.into()), + }; + + Self::from_cid_snapshot(cid, &snapshot.snapshot_key, store).await + } + + pub(crate) async fn from_cid_snapshot( + cid: Cid, + snapshot_key: &SnapshotKey, + store: &impl BlockStore, + ) -> Result { + let encrypted_bytes = store.get_block(&cid).await?; + let bytes = snapshot_key.decrypt(&encrypted_bytes)?; + let node: PrivateNodeContentSerializable = serde_ipld_dagcbor::from_slice(&bytes)?; + let node = match node { + PrivateNodeContentSerializable::File(file) => { + let file = + PrivateFile::from_serializable_snapshot(file, snapshot_key, cid, store).await?; + PrivateNode::File(Rc::new(file)) + } + PrivateNodeContentSerializable::Dir(_) => { + return Err(anyhow!("Not yet able to deserialize Dir from snapshot")); + } + }; + + Ok(node) + } + pub(crate) async fn from_cid( cid: Cid, temporal_key: &TemporalKey, @@ -500,7 +537,8 @@ impl PrivateNode { } PrivateNodeContentSerializable::Dir(dir) => { let dir = - PrivateDirectory::from_serializable(dir, temporal_key, cid, store).await?; + PrivateDirectory::from_serializable_temporal(dir, temporal_key, cid, store) + .await?; PrivateNode::Dir(Rc::new(dir)) } }; diff --git a/wnfs/src/private/share.rs b/wnfs/src/private/share.rs index 292a0cfe..d9b6646a 100644 --- a/wnfs/src/private/share.rs +++ b/wnfs/src/private/share.rs @@ -38,7 +38,7 @@ pub struct Share<'a, K: ExchangeKey, S: BlockStore> { pub struct Sharer<'a, S: BlockStore> { pub root_did: String, pub forest: &'a mut Rc, - pub store: &'a mut S, + pub store: &'a S, } #[derive(Debug)] @@ -308,7 +308,7 @@ pub mod recipient { error::ShareError, private::{PrivateForest, PrivateKey, PrivateNode, PrivateRef}, }; - use anyhow::{bail, Result}; + use anyhow::Result; use sha3::Sha3_256; use wnfs_common::BlockStore; use wnfs_hamt::Hasher; @@ -369,19 +369,21 @@ pub mod recipient { let payload: SharePayload = serde_ipld_dagcbor::from_slice(&recipient_key.decrypt(&encrypted_payload).await?)?; - let SharePayload::Temporal(TemporalSharePointer { - label, - content_cid, - temporal_key, - }) = payload - else { - // TODO(appcypher): We currently need both TemporalKey to decrypt a node. - bail!(ShareError::UnsupportedSnapshotShareReceipt); - }; - - // Use decrypted payload to get cid to encrypted node in sharer's forest. - let private_ref = PrivateRef::with_temporal_key(label, temporal_key, content_cid); - PrivateNode::load(&private_ref, sharer_forest, store).await + match payload { + SharePayload::Temporal(TemporalSharePointer { + label, + content_cid, + temporal_key, + }) => { + // Use decrypted payload to get cid to encrypted node in sharer's forest. + let private_ref = PrivateRef::with_temporal_key(label, temporal_key, content_cid); + PrivateNode::load(&private_ref, sharer_forest, store).await + } + SharePayload::Snapshot(snapshot) => { + // Load from Snapshot + PrivateNode::load_from_snapshot(snapshot, sharer_forest, store).await + } + } } } @@ -467,9 +469,9 @@ mod tests { } #[async_std::test] - async fn can_share_and_recieve_share() { + async fn can_share_and_recieve_temporal_share() { let recipient_store = &mut MemoryBlockStore::default(); - let sharer_store = &mut MemoryBlockStore::default(); + let sharer_store = &MemoryBlockStore::default(); let sharer_forest = &mut Rc::new(PrivateForest::new()); let rng = &mut TestRng::deterministic_rng(RngAlgorithm::ChaCha); @@ -532,6 +534,87 @@ mod tests { assert_eq!(node.as_dir().unwrap(), sharer_dir); } + #[async_std::test] + async fn can_share_and_recieve_snapshot_share() { + let recipient_store = &mut MemoryBlockStore::default(); + let sharer_store = &MemoryBlockStore::default(); + let sharer_forest = &mut Rc::new(PrivateForest::new()); + let rng = &mut TestRng::deterministic_rng(RngAlgorithm::ChaCha); + + let sharer_root_did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; + // Create directory to share. + + let sharer_dir = helper::create_sharer_dir(sharer_forest, sharer_store, rng) + .await + .unwrap(); + + let sharer_file = sharer_dir + .get_node(&["text.txt".into()], true, sharer_forest, sharer_store) + .await + .unwrap() + .unwrap() + .as_file() + .unwrap(); + sharer_file + .store(sharer_forest, sharer_store, rng) + .await + .unwrap(); + + // Establish recipient exchange root. + let (recipient_key, recipient_exchange_root) = + helper::create_recipient_exchange_root(recipient_store) + .await + .unwrap(); + + // Construct share payload from sharer's directory. + let sharer_payload = SharePayload::from_node( + &sharer_file.as_node(), + false, + sharer_forest, + sharer_store, + rng, + ) + .await + .unwrap(); + + // Share payload with recipient. + Share::::new(&sharer_payload, 0) + .by(Sharer { + root_did: sharer_root_did.into(), + store: sharer_store, + forest: sharer_forest, + }) + .to(Recipient { + exchange_root: PublicLink::new(PublicNode::Dir(recipient_exchange_root)), + store: recipient_store, + }) + .finish() + .await + .unwrap(); + + // Create share label. + let share_label = sharer::create_share_label( + 0, + sharer_root_did, + &recipient_key + .get_public_key() + .get_public_key_modulus() + .unwrap(), + ); + + // Grab node using share label. + let node = + recipient::receive_share(share_label, &recipient_key, sharer_forest, sharer_store) + .await + .unwrap(); + + let file = node.as_file().unwrap(); + + let content = file.get_content(sharer_forest, sharer_store).await.unwrap(); + let content_string = String::from_utf8(content).unwrap(); + assert_eq!(content_string, "Hello World!".to_string()); + } + #[async_std::test] async fn serialized_share_payload_can_be_deserialized() { let store = &mut MemoryBlockStore::default();