dotlock-bin 0.1.1

Encrypted project-local environment variables manager
use base64::{Engine, engine::general_purpose};
use zeroize::{Zeroize, Zeroizing};

use crate::{
    crypto::{
        AccessMode,
        dek::{WrappedDek, unwrap_dek},
        integrity::verify_secrets_integrity,
        kdf::{KdfParams, derive_master_key},
        kek::derive_kek,
        prompt_unlock_password,
        share::unwrap_dek_with_private_key,
    },
    domain::{error::DotLockError, model::DotLockResult},
    storage::{
        cache::{invalidate_cache, read_cached_dek, write_cached_dek},
        identity::load_local_identity,
        project::SECRETS_FILE,
        vault_file::load_vault_metadata,
    },
};

fn unwrap_dek_with_passphrase(
    metadata: &crate::crypto::VaultKeyMetadata,
    passphrase: &str,
) -> DotLockResult<[u8; 32]> {
    let salt = general_purpose::STANDARD
        .decode(&metadata.salt_b64)
        .map_err(|_| DotLockError::LegacyVaultFormat)?;

    let params = KdfParams {
        memory_kib: metadata.memory_kib,
        iterations: metadata.iterations,
        parallelism: metadata.parallelism,
    };

    let mut master_key = derive_master_key(passphrase, &salt, params)
        .map_err(|e| DotLockError::Crypto(e.to_string()))?;

    let mut kek = derive_kek(
        &master_key,
        &metadata.project,
        &metadata.environment,
        metadata.kek_version,
    )
    .map_err(|e| DotLockError::Crypto(e.to_string()))?;

    master_key.zeroize();

    let nonce = general_purpose::STANDARD
        .decode(&metadata.wrapped_dek_nonce_b64)
        .map_err(|_| DotLockError::LegacyVaultFormat)?;

    let nonce: [u8; 24] = nonce
        .try_into()
        .map_err(|_| DotLockError::LegacyVaultFormat)?;

    let wrapped_dek = general_purpose::STANDARD
        .decode(&metadata.wrapped_dek_b64)
        .map_err(|_| DotLockError::LegacyVaultFormat)?;

    let wrapped = WrappedDek {
        nonce,
        ciphertext: wrapped_dek,
    };

    let dek = unwrap_dek(&kek, &wrapped, &metadata.project, &metadata.environment)
        .map_err(|_| DotLockError::InvalidMasterPassword)?;

    kek.zeroize();
    Ok(dek)
}

fn unlock_vault_with_dek(
    metadata: &crate::crypto::VaultKeyMetadata,
    dek: [u8; 32],
) -> DotLockResult<Zeroizing<[u8; 32]>> {
    verify_secrets_integrity(SECRETS_FILE, metadata, &dek)?;
    write_cached_dek(&dek)?;
    Ok(Zeroizing::new(dek))
}

pub fn unlock_vault_with_master_password(path: &str) -> DotLockResult<Zeroizing<[u8; 32]>> {
    let metadata = load_vault_metadata(path)?;
    let passphrase = prompt_unlock_password()?;
    let dek = unwrap_dek_with_passphrase(&metadata, &passphrase)?;
    unlock_vault_with_dek(&metadata, dek)
}

pub fn unlock_vault_with_master_password_and_passphrase(
    path: &str,
) -> DotLockResult<(Zeroizing<[u8; 32]>, String)> {
    let metadata = load_vault_metadata(path)?;
    let passphrase = prompt_unlock_password()?;
    let dek = unwrap_dek_with_passphrase(&metadata, &passphrase)?;
    let dek = unlock_vault_with_dek(&metadata, dek)?;
    Ok((dek, passphrase))
}

fn try_unlock_vault_with_local_identity(
    metadata: &crate::crypto::VaultKeyMetadata,
) -> DotLockResult<Zeroizing<[u8; 32]>> {
    let identity = load_local_identity()?;
    let recipient = metadata
        .recipients
        .iter()
        .find(|recipient| recipient.public_key_fingerprint == identity.fingerprint)
        .ok_or_else(|| DotLockError::RecipientNotFound {
            query: identity.fingerprint.clone(),
        })?;
    let dek = unwrap_dek_with_private_key(&recipient.wrapped_dek_b64, &identity.private_key_pem)?;
    unlock_vault_with_dek(metadata, dek)
}

pub fn unlock_vault(path: &str) -> DotLockResult<Zeroizing<[u8; 32]>> {
    let metadata = load_vault_metadata(path)?;

    if let Some(dek) = read_cached_dek() {
        match verify_secrets_integrity(SECRETS_FILE, &metadata, &dek) {
            Ok(()) => {
                write_cached_dek(&dek)?;
                return Ok(dek);
            }
            Err(DotLockError::TamperedSecretsFile) => {
                let _ = invalidate_cache();
                return Err(DotLockError::TamperedSecretsFile);
            }
            Err(_) => {
                let _ = invalidate_cache();
            }
        }
    }

    if metadata.access_mode == AccessMode::Shared {
        match try_unlock_vault_with_local_identity(&metadata) {
            Ok(dek) => return Ok(dek),
            Err(DotLockError::TamperedSecretsFile) => {
                return Err(DotLockError::TamperedSecretsFile);
            }
            Err(_) => {}
        }
    }

    unlock_vault_with_master_password(path)
}