dotlock-bin 0.1.1

Encrypted project-local environment variables manager
use std::process::Command;

use base64::{Engine, engine::general_purpose};
use chacha20poly1305::{
    XChaCha20Poly1305,
    aead::{Aead, AeadCore, KeyInit, OsRng},
};

use crate::{
    domain::{
        error::DotLockError,
        model::{Alg, DataEncrypted, DotLockResult},
    },
    storage::{project::SECRETS_FILE, secrets_lock::load_secrets_file},
    utils::parse_alg,
};

pub fn encryption_process<'a>(
    name: String,
    value: String,
    alg: Alg,
    dek: &[u8; 32],
) -> DotLockResult<DataEncrypted<'a>> {
    match alg {
        Alg::XChaCha20Poly1305 => {
            let encrypted = encrypt_xchacha20poly1305(value, dek)?;
            Ok(DataEncrypted {
                alg: "xchacha20-poly1305",
                name,
                data: encrypted.into_bytes(),
            })
        }
    }
}

fn encrypt_xchacha20poly1305(plaintext: String, key: &[u8; 32]) -> DotLockResult<String> {
    let cipher = XChaCha20Poly1305::new(key.into());
    let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);

    let ciphertext = cipher
        .encrypt(&nonce, plaintext.as_bytes())
        .map_err(|err| DotLockError::Crypto(err.to_string()))?;

    let mut output = Vec::with_capacity(nonce.len() + ciphertext.len());
    output.extend_from_slice(&nonce);
    output.extend_from_slice(&ciphertext);

    Ok(general_purpose::STANDARD.encode(output))
}

pub fn decryption_process(
    encrypted_data: String,
    alg: Alg,
    dek: &[u8; 32],
) -> DotLockResult<String> {
    match alg {
        Alg::XChaCha20Poly1305 => decrypt_xchacha20poly1305(encrypted_data, dek),
    }
}

fn decrypt_xchacha20poly1305(encrypted_data: String, key: &[u8; 32]) -> DotLockResult<String> {
    let data = general_purpose::STANDARD
        .decode(encrypted_data)
        .map_err(|err| DotLockError::Crypto(err.to_string()))?;

    if data.len() < 24 {
        return Err(DotLockError::Crypto("invalid encrypted data".to_string()));
    }

    let nonce = &data[..24];
    let ciphertext = &data[24..];

    let cipher = XChaCha20Poly1305::new(key.into());

    let plaintext = cipher
        .decrypt(nonce.into(), ciphertext)
        .map_err(|err| DotLockError::Crypto(err.to_string()))?;

    let value =
        String::from_utf8(plaintext).map_err(|err| DotLockError::Crypto(err.to_string()))?;
    Ok(value)
}

pub fn run_with_secrets(command: Vec<String>, dek: &[u8; 32]) -> DotLockResult<()> {
    if command.is_empty() {
        return Err(DotLockError::MissingCommand);
    }

    let file = load_secrets_file(SECRETS_FILE)?;

    let mut envs = Vec::new();

    for secret in file.secrets {
        let alg = parse_alg(&secret.alg)?;
        let value = decryption_process(secret.data, alg, dek)?;
        envs.push((secret.name, value));
    }

    let program = &command[0];
    let args = &command[1..];

    let status = Command::new(program)
        .args(args)
        .envs(envs)
        .status()
        .map_err(|e| DotLockError::Io(e.to_string()))?;

    if !status.success() {
        return Err(DotLockError::CommandFailed {
            status: status.to_string(),
        });
    }

    Ok(())
}