dotlock-bin 1.1.0

Encrypted project-local environment variables manager
use base64::{Engine as _, engine::general_purpose};
use chacha20poly1305::{
    XChaCha20Poly1305, XNonce,
    aead::{Aead, KeyInit, Payload},
};

use crate::domain::{
    error::DotLockError,
    keys::{ProjectKey, SecretKey},
    model::DotLockResult,
};

const SDK_LEN: usize = 32;
const NONCE_LEN: usize = 24;
const SDK_AAD: &[u8] = b"dotlock:v3:sdk-wrap";

pub fn generate_sdk() -> DotLockResult<SecretKey> {
    let mut sdk = [0u8; SDK_LEN];
    getrandom::fill(&mut sdk).map_err(|err| DotLockError::Crypto(err.to_string()))?;
    Ok(SecretKey::new(sdk))
}

pub fn wrap_sdk_for_project_key(
    sdk: &SecretKey,
    project_key: &ProjectKey,
) -> DotLockResult<String> {
    let mut nonce = [0u8; NONCE_LEN];
    getrandom::fill(&mut nonce).map_err(|err| DotLockError::Crypto(err.to_string()))?;

    let cipher = XChaCha20Poly1305::new(project_key.as_bytes().into());
    let ciphertext = cipher
        .encrypt(
            XNonce::from_slice(&nonce),
            Payload {
                msg: sdk.as_bytes().as_slice(),
                aad: SDK_AAD,
            },
        )
        .map_err(|_| DotLockError::Crypto("failed to wrap secret key".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 unwrap_sdk_with_project_key(
    wrapped_sdk_b64: &str,
    project_key: &ProjectKey,
) -> DotLockResult<SecretKey> {
    let data = general_purpose::STANDARD
        .decode(wrapped_sdk_b64)
        .map_err(|err| {
            DotLockError::Crypto(format!("failed to decode wrapped secret key: {err}"))
        })?;
    if data.len() < NONCE_LEN {
        return Err(DotLockError::Crypto(
            "invalid wrapped secret key".to_string(),
        ));
    }

    let cipher = XChaCha20Poly1305::new(project_key.as_bytes().into());
    let plaintext = cipher
        .decrypt(
            XNonce::from_slice(&data[..NONCE_LEN]),
            Payload {
                msg: &data[NONCE_LEN..],
                aad: SDK_AAD,
            },
        )
        .map_err(|_| DotLockError::Crypto("failed to unwrap secret key".to_string()))?;

    let sdk: [u8; SDK_LEN] = plaintext
        .try_into()
        .map_err(|_| DotLockError::Crypto("invalid secret key size".to_string()))?;
    Ok(SecretKey::new(sdk))
}