dotlock-bin 0.1.3

Encrypted project-local environment variables manager
use anyhow::{Result, anyhow};
use chacha20poly1305::{
    XChaCha20Poly1305, XNonce,
    aead::{Aead, KeyInit, Payload},
};

const KEY_LEN: usize = 32;
const NONCE_LEN: usize = 24;

pub struct WrappedDek {
    pub nonce: [u8; NONCE_LEN],
    pub ciphertext: Vec<u8>,
}

pub fn generate_dek() -> Result<[u8; KEY_LEN]> {
    let mut dek = [0u8; KEY_LEN];

    getrandom::fill(&mut dek).map_err(|e| anyhow!("failed to generate DEK: {e}"))?;

    Ok(dek)
}

pub fn wrap_dek(
    kek: &[u8; KEY_LEN],
    dek: &[u8; KEY_LEN],
    project: &str,
    environment: &str,
) -> Result<WrappedDek> {
    let mut nonce = [0u8; NONCE_LEN];

    getrandom::fill(&mut nonce).map_err(|e| anyhow!("failed to generate nonce: {e}"))?;

    let aad = format!("dotlock:v1:wrapped-dek:project={project}:env={environment}");

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

    let ciphertext = cipher
        .encrypt(
            XNonce::from_slice(&nonce),
            Payload {
                msg: dek,
                aad: aad.as_bytes(),
            },
        )
        .map_err(|_| anyhow!("failed to wrap DEK"))?;

    Ok(WrappedDek { nonce, ciphertext })
}

pub fn unwrap_dek(
    kek: &[u8; KEY_LEN],
    wrapped: &WrappedDek,
    project: &str,
    environment: &str,
) -> Result<[u8; KEY_LEN]> {
    let aad = format!("dotlock:v1:wrapped-dek:project={project}:env={environment}");

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

    let plaintext = cipher
        .decrypt(
            XNonce::from_slice(&wrapped.nonce),
            Payload {
                msg: wrapped.ciphertext.as_ref(),
                aad: aad.as_bytes(),
            },
        )
        .map_err(|_| anyhow!("failed to unwrap DEK"))?;

    let dek: [u8; KEY_LEN] = plaintext
        .try_into()
        .map_err(|_| anyhow!("invalid DEK length"))?;

    Ok(dek)
}