use crate::{
storage::AppPaths,
vfs::{self, File},
Error, Result,
};
use age::Encryptor;
use futures::io::AsyncReadExt;
use secrecy::SecretString;
use sha2::{Digest, Sha256};
use std::path::{Path, PathBuf};
use tokio_util::compat::TokioAsyncReadCompatExt;
use super::EncryptedFile;
pub struct FileStorage;
impl FileStorage {
pub async fn encrypt_file_passphrase<S: AsRef<Path>, T: AsRef<Path>>(
input: S,
target: T,
passphrase: SecretString,
) -> Result<(Vec<u8>, u64)> {
let file = File::open(input.as_ref()).await?;
let encryptor = Encryptor::with_user_passphrase(passphrase);
let mut encrypted = Vec::new();
let mut writer = encryptor.wrap_async_output(&mut encrypted).await?;
futures::io::copy(&mut file.compat(), &mut writer).await?;
writer.finish()?;
let mut hasher = Sha256::new();
hasher.update(&encrypted);
let digest = hasher.finalize();
let file_name = hex::encode(digest);
let dest = PathBuf::from(target.as_ref()).join(file_name);
let size = encrypted.len() as u64;
vfs::write(dest, encrypted).await?;
Ok((digest.to_vec(), size))
}
pub async fn decrypt_file_passphrase<P: AsRef<Path>>(
input: P,
passphrase: &SecretString,
) -> Result<Vec<u8>> {
let mut file = File::open(input.as_ref()).await?.compat();
let decryptor = match age::Decryptor::new_async(&mut file).await? {
age::Decryptor::Passphrase(d) => d,
_ => return Err(Error::NotPassphraseEncryption),
};
let mut decrypted = vec![];
let mut reader = decryptor.decrypt_async(passphrase, None)?;
reader.read_to_end(&mut decrypted).await?;
Ok(decrypted)
}
pub async fn encrypt_file_storage<
P: AsRef<Path>,
A: AsRef<Path>,
V: AsRef<Path>,
S: AsRef<Path>,
>(
password: SecretString,
path: P,
address: A,
vault_id: V,
secret_id: S,
) -> Result<EncryptedFile> {
let target =
AppPaths::files_dir(address)?.join(vault_id).join(secret_id);
if !vfs::try_exists(&target).await? {
vfs::create_dir_all(&target).await?;
}
let (digest, size) =
Self::encrypt_file_passphrase(path, target, password).await?;
Ok(EncryptedFile { digest, size })
}
pub async fn decrypt_file_storage<
A: AsRef<Path>,
V: AsRef<Path>,
S: AsRef<Path>,
F: AsRef<Path>,
>(
password: &SecretString,
address: A,
vault_id: V,
secret_id: S,
file_name: F,
) -> Result<Vec<u8>> {
let path =
AppPaths::file_location(address, vault_id, secret_id, file_name)?;
Self::decrypt_file_passphrase(path, password).await
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{passwd::diceware::generate_passphrase, vfs};
use anyhow::Result;
#[tokio::test]
async fn file_encrypt_decrypt() -> Result<()> {
let (passphrase, _) = generate_passphrase()?;
let input = "fixtures/sample.heic";
let output = "target/file-encrypt-decrypt";
if let Ok(true) = vfs::try_exists(output).await {
vfs::remove_dir_all(output).await?;
}
vfs::create_dir_all(output).await?;
let encrypted = FileStorage::encrypt_file_passphrase(
input,
output,
passphrase.clone(),
)
.await?;
let target = PathBuf::from(output).join(hex::encode(encrypted.0));
let decrypted =
FileStorage::decrypt_file_passphrase(target, &passphrase).await?;
let contents = vfs::read(input).await?;
assert_eq!(contents, decrypted);
Ok(())
}
}