use crate::Result;
use async_zip::{tokio::write::ZipFileWriter, Compression, ZipEntryBuilder};
use secrecy::{ExposeSecret, SecretBox};
use serde::{Deserialize, Serialize};
use sos_backend::AccessPoint;
use sos_core::{SecretId, VaultId};
use sos_vault::{
secret::{FileContent, Secret, SecretMeta},
SecretAccess, Summary, VaultMeta,
};
use std::collections::HashMap;
use tokio::io::AsyncWrite;
use tokio_util::compat::Compat;
pub struct PublicExport<W: AsyncWrite + Unpin> {
writer: ZipFileWriter<W>,
vault_ids: Vec<VaultId>,
}
impl<W: AsyncWrite + Unpin> PublicExport<W> {
pub fn new(inner: W) -> Self {
Self {
writer: ZipFileWriter::with_tokio(inner),
vault_ids: Vec::new(),
}
}
async fn append_file_buffer(
&mut self,
path: &str,
buffer: &[u8],
) -> Result<()> {
let entry = ZipEntryBuilder::new(path.into(), Compression::Deflate);
self.writer.write_entry_whole(entry, buffer).await?;
Ok(())
}
pub async fn add(&mut self, access: &AccessPoint) -> Result<()> {
let meta = access.vault_meta().await?;
let vault_id = access.summary().id();
let base_path = format!("vaults/{}", vault_id);
let file_path = format!("{}/files", base_path);
let store = PublicVaultInfo {
meta,
summary: access.summary().clone(),
secrets: access.vault().keys().copied().collect(),
};
let store_path = format!("{}/meta.json", base_path);
let buffer = serde_json::to_vec_pretty(&store)?;
self.append_file_buffer(&store_path, buffer.as_slice())
.await?;
for id in access.vault().keys() {
if let Some((meta, mut secret, _)) =
access.read_secret(id).await?
{
self.move_file_buffer(&file_path, &mut secret).await?;
for field in secret.user_data_mut().fields_mut() {
self.move_file_buffer(&file_path, field.secret_mut())
.await?;
}
let path = format!("{}/{}.json", base_path, id);
let public_secret = PublicSecret {
id: *id,
meta,
secret,
};
let buffer = serde_json::to_vec_pretty(&public_secret)?;
self.append_file_buffer(&path, buffer.as_slice()).await?;
}
}
self.vault_ids.push(*vault_id);
Ok(())
}
async fn move_file_buffer(
&mut self,
file_path: &str,
secret: &mut Secret,
) -> Result<()> {
if let Secret::File { content, .. } = secret {
if let FileContent::Embedded {
buffer, checksum, ..
} = content
{
let path = format!("{}/{}", file_path, hex::encode(checksum));
self.append_file_buffer(
&path,
buffer.expose_secret().as_slice(),
)
.await?;
*buffer = SecretBox::new(vec![].into());
}
}
Ok(())
}
pub async fn append_files(
&mut self,
files: HashMap<&str, &[u8]>,
) -> Result<()> {
for (path, buffer) in files {
self.append_file_buffer(path, buffer).await?;
}
Ok(())
}
pub async fn finish(mut self) -> Result<Compat<W>> {
let path = "vaults.json";
let buffer = serde_json::to_vec_pretty(&self.vault_ids)?;
self.append_file_buffer(path, buffer.as_slice()).await?;
Ok(self.writer.close().await?)
}
}
#[derive(Default, Serialize, Deserialize)]
pub struct PublicVaultInfo {
summary: Summary,
meta: VaultMeta,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
secrets: Vec<SecretId>,
}
#[derive(Default, Serialize, Deserialize)]
pub struct PublicSecret {
id: SecretId,
meta: SecretMeta,
secret: Secret,
}