const EXCHANGE_KEY_NAME: &str = "v1.exchange_key";
pub mod sharer {
use super::EXCHANGE_KEY_NAME;
use crate::{
private::{AccessKey, ExchangeKey, PublicKeyModulus, forest::traits::PrivateForest},
public::PublicLink,
};
use anyhow::Result;
use async_stream::try_stream;
use futures::{Stream, TryStreamExt};
use wnfs_common::{BlockStore, CODEC_RAW};
use wnfs_nameaccumulator::{Name, NameSegment};
#[allow(clippy::too_many_arguments)]
pub async fn share<K: ExchangeKey>(
access_key: &AccessKey,
share_count: u64,
sharer_root_did: &str,
recipient_exchange_root: PublicLink,
forest: &mut impl PrivateForest,
store: &impl BlockStore,
) -> Result<()> {
let mut exchange_keys = fetch_exchange_keys(recipient_exchange_root, store).await;
let encoded_key = &serde_ipld_dagcbor::to_vec(access_key)?;
while let Some(public_key_modulus) = exchange_keys.try_next().await? {
let exchange_key = K::from_modulus(public_key_modulus.as_ref()).await?;
let encrypted_key = exchange_key.encrypt(encoded_key).await?;
let share_label =
create_share_name(share_count, sharer_root_did, &public_key_modulus, forest);
let access_key_cid = store.put_block(encrypted_key, CODEC_RAW).await?;
forest
.put_encrypted(&share_label, Some(access_key_cid), store)
.await?;
}
Ok(())
}
pub async fn fetch_exchange_keys(
recipient_exchange_root: PublicLink,
store: &impl BlockStore,
) -> impl Stream<Item = Result<PublicKeyModulus>> + '_ {
Box::pin(try_stream! {
let root_dir = recipient_exchange_root
.resolve_value(store)
.await?
.as_dir()?;
let devices = root_dir.ls(&[], store).await?;
for (device, _) in devices {
let value = root_dir.ls(std::slice::from_ref(&device), store).await?;
for (name, _) in value {
if name == EXCHANGE_KEY_NAME {
yield root_dir.read(&[device, name], store).await?;
break
}
}
}
})
}
pub fn create_share_name(
share_count: u64,
sharer_root_did: &str,
recipient_exchange_key: &[u8],
forest: &impl PrivateForest,
) -> Name {
forest.empty_name().with_segments_added([
NameSegment::new_hashed("Testing", sharer_root_did.as_bytes()),
NameSegment::new_hashed("Testing", recipient_exchange_key),
NameSegment::new_hashed("Testing", share_count.to_le_bytes()),
])
}
}
pub mod recipient {
use super::sharer;
use crate::{
error::ShareError,
private::{AccessKey, PrivateKey, PrivateNode, forest::traits::PrivateForest},
};
use anyhow::Result;
use wnfs_common::BlockStore;
use wnfs_hamt::Hasher;
use wnfs_nameaccumulator::Name;
pub async fn find_latest_share_counter(
share_count_start: u64,
limit: u64,
recipient_exchange_key: &[u8],
sharer_root_did: &str,
forest: &impl PrivateForest,
store: &impl BlockStore,
) -> Result<Option<u64>> {
for share_count in share_count_start..share_count_start + limit {
let share_label = sharer::create_share_name(
share_count,
sharer_root_did,
recipient_exchange_key,
forest,
);
if !forest.has(&share_label, store).await? {
if share_count == share_count_start {
return Ok(None);
} else {
return Ok(Some(share_count - 1));
}
}
}
Ok(Some(share_count_start + limit - 1))
}
pub async fn receive_share(
share_label: &Name,
recipient_key: &impl PrivateKey,
forest: &impl PrivateForest,
store: &impl BlockStore,
) -> Result<PrivateNode> {
let access_key_cid = forest
.get_encrypted_by_hash(
&blake3::Hasher::hash(&forest.get_accumulated_name(share_label)),
store,
)
.await?
.ok_or(ShareError::AccessKeyNotFound)?
.first()
.ok_or(ShareError::AccessKeyNotFound)?;
let encrypted_access_key = store.get_block(access_key_cid).await?.to_vec();
let access_key: AccessKey =
serde_ipld_dagcbor::from_slice(&recipient_key.decrypt(&encrypted_access_key).await?)?;
PrivateNode::from_private_ref(&access_key.derive_private_ref()?, forest, store, None).await
}
}
#[cfg(test)]
mod tests {
use super::{
EXCHANGE_KEY_NAME,
recipient::{self, find_latest_share_counter},
sharer,
};
use crate::{
private::{
AccessKey, PrivateDirectory, RsaPublicKey,
forest::{hamt::HamtForest, traits::PrivateForest},
},
public::PublicLink,
};
use chrono::Utc;
use rand_chacha::ChaCha12Rng;
use rand_core::SeedableRng;
use wnfs_common::{MemoryBlockStore, utils::Arc};
mod helper {
use crate::{
private::{
PrivateDirectory, RsaPrivateKey, forest::traits::PrivateForest,
share::EXCHANGE_KEY_NAME,
},
public::PublicDirectory,
};
use anyhow::Result;
use chrono::Utc;
use rand_core::CryptoRngCore;
use wnfs_common::{
BlockStore,
utils::{Arc, CondSend},
};
pub(super) async fn create_sharer_dir(
forest: &mut impl PrivateForest,
store: &impl BlockStore,
rng: &mut (impl CryptoRngCore + CondSend),
) -> Result<Arc<PrivateDirectory>> {
let mut dir = PrivateDirectory::new_and_store(
&forest.empty_name(),
Utc::now(),
forest,
store,
rng,
)
.await?;
dir.write(
&["text.txt".into()],
true,
Utc::now(),
b"Hello World!".to_vec(),
forest,
store,
rng,
)
.await?;
Ok(dir)
}
pub(super) async fn create_recipient_exchange_root(
store: &impl BlockStore,
) -> Result<(RsaPrivateKey, Arc<PublicDirectory>)> {
let key = RsaPrivateKey::new()?;
let exchange_key = key.get_public_key().get_public_key_modulus()?;
let mut root_dir = PublicDirectory::new_rc(Utc::now());
root_dir
.write(
&["device1".into(), EXCHANGE_KEY_NAME.into()],
exchange_key,
Utc::now(),
store,
)
.await?;
Ok((key, root_dir))
}
}
#[async_std::test]
async fn can_share_and_recieve_share() {
let rng = &mut ChaCha12Rng::seed_from_u64(0);
let store = &MemoryBlockStore::new();
let forest = &mut HamtForest::new_rsa_2048_rc(rng);
let sharer_root_did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
let sharer_dir = helper::create_sharer_dir(forest, store, rng).await.unwrap();
let (recipient_key, recipient_exchange_root) =
helper::create_recipient_exchange_root(store).await.unwrap();
let access_key = sharer_dir
.as_node()
.store(forest, store, rng)
.await
.unwrap();
sharer::share::<RsaPublicKey>(
&access_key,
0,
sharer_root_did,
PublicLink::with_rc_dir(recipient_exchange_root),
forest,
store,
)
.await
.unwrap();
let share_label = sharer::create_share_name(
0,
sharer_root_did,
&recipient_key
.get_public_key()
.get_public_key_modulus()
.unwrap(),
forest,
);
let node = recipient::receive_share(&share_label, &recipient_key, forest, store)
.await
.unwrap();
assert_eq!(node.as_dir().unwrap(), sharer_dir);
}
#[async_std::test]
async fn serialized_share_payload_can_be_deserialized() {
let rng = &mut ChaCha12Rng::seed_from_u64(0);
let store = &MemoryBlockStore::new();
let forest = &mut HamtForest::new_rsa_2048_rc(rng);
let dir =
PrivateDirectory::new_and_store(&forest.empty_name(), Utc::now(), forest, store, rng)
.await
.unwrap();
let access_key = dir.as_node().store(forest, store, rng).await.unwrap();
let serialized = serde_ipld_dagcbor::to_vec(&access_key).unwrap();
assert!(serialized.len() <= 190);
let deserialized: AccessKey = serde_ipld_dagcbor::from_slice(&serialized).unwrap();
assert_eq!(access_key, deserialized);
}
#[async_std::test]
async fn find_latest_share_counter_finds_highest_count() {
let rng = &mut ChaCha12Rng::seed_from_u64(0);
let store = &MemoryBlockStore::new();
let forest = &mut HamtForest::new_rsa_2048_rc(rng);
let sharer_root_did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
let (_, recipient_exchange_root) =
helper::create_recipient_exchange_root(store).await.unwrap();
let recipient_exchange_key = recipient_exchange_root
.read(&["device1".into(), EXCHANGE_KEY_NAME.into()], store)
.await
.unwrap();
let max_share_count_before = find_latest_share_counter(
0,
100,
recipient_exchange_key.as_ref(),
sharer_root_did,
forest,
store,
)
.await
.unwrap();
assert_eq!(max_share_count_before, None);
let dir =
PrivateDirectory::new_and_store(&forest.empty_name(), Utc::now(), forest, store, rng)
.await
.unwrap();
let access_key = dir.as_node().store(forest, store, rng).await.unwrap();
let expected_max_share_count = 5;
for i in 0..=expected_max_share_count {
sharer::share::<RsaPublicKey>(
&access_key,
i,
sharer_root_did,
PublicLink::with_rc_dir(Arc::clone(&recipient_exchange_root)),
forest,
store,
)
.await
.unwrap();
}
let max_share_count = find_latest_share_counter(
0,
100,
recipient_exchange_key.as_ref(),
sharer_root_did,
forest,
store,
)
.await
.unwrap();
assert_eq!(max_share_count, Some(expected_max_share_count));
}
}