vyn-core 0.1.4

Core library for vyn. Crypto, keychain, manifest, storage, and diff engine
Documentation
use std::collections::HashMap;
use std::sync::Arc;

use secrecy::ExposeSecret;
use tokio::sync::RwLock;

use crate::crypto::{SecretBytes, decrypt, encrypt, secret_bytes};
use crate::manifest::Manifest;

#[derive(Debug, thiserror::Error)]
pub enum StorageError {
    #[error("object not found")]
    NotFound,
    #[error("serialization failed: {0}")]
    Serialize(#[from] serde_json::Error),
    #[error("crypto operation failed: {0}")]
    Crypto(#[from] crate::crypto::CryptoError),
    #[error("transport error: {0}")]
    Transport(String),
}

pub type StorageResult<T> = Result<T, StorageError>;

#[allow(async_fn_in_trait)]
pub trait StorageProvider: Send + Sync {
    async fn get_manifest(&self, project_id: &str) -> StorageResult<Option<Vec<u8>>>;
    async fn put_manifest(&self, project_id: &str, manifest_payload: &[u8]) -> StorageResult<()>;
    async fn upload_blob(&self, hash: &str, data: Vec<u8>) -> StorageResult<()>;
    async fn download_blob(&self, hash: &str) -> StorageResult<Option<Vec<u8>>>;
    async fn create_invite(
        &self,
        user_id: &str,
        vault_id: &str,
        payload: Vec<u8>,
    ) -> StorageResult<()>;
    async fn get_invites(&self, user_id: &str, vault_id: &str) -> StorageResult<Vec<Vec<u8>>>;
}

#[derive(Clone, Default)]
pub struct InMemoryStorageProvider {
    manifests: Arc<RwLock<HashMap<String, Vec<u8>>>>,
    blobs: Arc<RwLock<HashMap<String, Vec<u8>>>>,
    invites: Arc<RwLock<HashMap<String, Vec<Vec<u8>>>>>,
}

impl InMemoryStorageProvider {
    pub fn new() -> Self {
        Self::default()
    }
}

impl StorageProvider for InMemoryStorageProvider {
    async fn get_manifest(&self, project_id: &str) -> StorageResult<Option<Vec<u8>>> {
        Ok(self.manifests.read().await.get(project_id).cloned())
    }

    async fn put_manifest(&self, project_id: &str, manifest_payload: &[u8]) -> StorageResult<()> {
        self.manifests
            .write()
            .await
            .insert(project_id.to_string(), manifest_payload.to_vec());
        Ok(())
    }

    async fn upload_blob(&self, hash: &str, data: Vec<u8>) -> StorageResult<()> {
        self.blobs.write().await.insert(hash.to_string(), data);
        Ok(())
    }

    async fn download_blob(&self, hash: &str) -> StorageResult<Option<Vec<u8>>> {
        Ok(self.blobs.read().await.get(hash).cloned())
    }

    async fn create_invite(
        &self,
        user_id: &str,
        vault_id: &str,
        payload: Vec<u8>,
    ) -> StorageResult<()> {
        let key = format!("{user_id}:{vault_id}");
        let mut invites = self.invites.write().await;
        invites.entry(key).or_default().push(payload);
        Ok(())
    }

    async fn get_invites(&self, user_id: &str, vault_id: &str) -> StorageResult<Vec<Vec<u8>>> {
        let key = format!("{user_id}:{vault_id}");
        Ok(self
            .invites
            .read()
            .await
            .get(&key)
            .cloned()
            .unwrap_or_default())
    }
}

pub fn encrypt_manifest(manifest: &Manifest, key: &SecretBytes) -> StorageResult<Vec<u8>> {
    let bytes = serde_json::to_vec(manifest)?;
    let encrypted = encrypt(key, &secret_bytes(bytes))?;

    let mut payload = Vec::with_capacity(encrypted.nonce.len() + encrypted.ciphertext.len());
    payload.extend_from_slice(&encrypted.nonce);
    payload.extend_from_slice(&encrypted.ciphertext);
    Ok(payload)
}

pub fn decrypt_manifest(payload: &[u8], key: &SecretBytes) -> StorageResult<Manifest> {
    const NONCE_LEN: usize = 12;
    if payload.len() < NONCE_LEN {
        return Err(StorageError::NotFound);
    }

    let mut nonce = [0u8; NONCE_LEN];
    nonce.copy_from_slice(&payload[..NONCE_LEN]);
    let encrypted = crate::crypto::EncryptedData {
        nonce,
        ciphertext: payload[NONCE_LEN..].to_vec(),
    };

    let plaintext = decrypt(key, &encrypted)?;
    Ok(serde_json::from_slice(plaintext.expose_secret())?)
}

#[cfg(test)]
mod tests {
    use super::{InMemoryStorageProvider, StorageProvider, decrypt_manifest, encrypt_manifest};
    use crate::crypto::secret_bytes;
    use crate::manifest::{FileEntry, Manifest};

    #[test]
    fn manifest_encryption_roundtrip() {
        let manifest = Manifest {
            version: 1,
            files: vec![FileEntry {
                path: ".env".to_string(),
                sha256: "abc".to_string(),
                size: 42,
                mode: 0o644,
            }],
        };
        let key = secret_bytes(vec![7u8; 32]);

        let encrypted =
            encrypt_manifest(&manifest, &key).expect("manifest encryption should succeed");
        let restored =
            decrypt_manifest(&encrypted, &key).expect("manifest decryption should succeed");

        assert_eq!(manifest, restored);
    }

    #[tokio::test]
    async fn push_pull_roundtrip() {
        let provider = InMemoryStorageProvider::new();
        let key = secret_bytes(vec![9u8; 32]);
        let project = "vault-123";

        let manifest = Manifest {
            version: 1,
            files: vec![FileEntry {
                path: ".env".to_string(),
                sha256: "hash-1".to_string(),
                size: 5,
                mode: 0o644,
            }],
        };

        let manifest_payload = encrypt_manifest(&manifest, &key).expect("manifest should encrypt");
        provider
            .put_manifest(project, &manifest_payload)
            .await
            .expect("manifest should upload");
        provider
            .upload_blob("hash-1", b"hello".to_vec())
            .await
            .expect("blob should upload");

        let pulled_manifest_payload = provider
            .get_manifest(project)
            .await
            .expect("manifest should download")
            .expect("manifest should exist");
        let pulled_manifest =
            decrypt_manifest(&pulled_manifest_payload, &key).expect("manifest should decrypt");
        let blob = provider
            .download_blob("hash-1")
            .await
            .expect("blob should download")
            .expect("blob should exist");

        assert_eq!(pulled_manifest, manifest);
        assert_eq!(blob, b"hello");
    }
}