use colored::Colorize;
use crate::{
audit::record_ratchet,
cli::present::print_ratchet_summary,
crypto::{
VaultKeyMetadata, dek::generate_dek, integrity::seal_vault_metadata,
update_master_password_metadata,
},
domain::{keys::ProjectKey, model::DotLockResult},
storage::{
cache::invalidate_cache,
project::{ensure_project_initialized, secrets_file, vault_file},
unlock_file::{
UnlockAccess, prepare_vault_access, unlock_vault_prepared,
unlock_vault_with_master_password_prepared,
},
vault_file::{
RatchetSummary, rotate_project_key_wrapping, should_auto_ratchet_for_next_write,
},
vault_txn::{VaultPairWrite, commit_vault_pair},
},
};
pub struct VaultContext {
pub metadata: VaultKeyMetadata,
pub access: UnlockAccess,
}
impl VaultContext {
pub fn unlock() -> DotLockResult<Self> {
ensure_project_initialized()?;
let metadata = prepare_vault_access(&vault_file())?;
let access = unlock_vault_prepared(&metadata)?;
Ok(Self { metadata, access })
}
pub fn unlock_with_master_password() -> DotLockResult<(Self, zeroize::Zeroizing<String>)> {
ensure_project_initialized()?;
let metadata = prepare_vault_access(&vault_file())?;
let (dek, passphrase) = unlock_vault_with_master_password_prepared(&metadata)?;
Ok((
Self {
metadata,
access: UnlockAccess::Full(dek),
},
passphrase,
))
}
pub fn into_read(self) -> (VaultKeyMetadata, ProjectKey) {
(self.metadata, self.access.into_read_key())
}
pub fn into_write(self) -> DotLockResult<(VaultKeyMetadata, ProjectKey)> {
let Self {
mut metadata,
access,
} = self;
let current_dek = access.require_full()?;
if !should_auto_ratchet_for_next_write(&metadata) {
return Ok((metadata, current_dek));
}
let (verified_dek, passphrase) = unlock_vault_with_master_password_prepared(&metadata)?;
let (new_dek, summary) = rotate_project_key(&mut metadata, &verified_dek, &passphrase)?;
print_ratchet_summary(&summary);
Ok((metadata, new_dek))
}
}
pub fn rotate_project_key(
metadata: &mut VaultKeyMetadata,
current_dek: &ProjectKey,
passphrase: &str,
) -> DotLockResult<(ProjectKey, RatchetSummary)> {
let new_dek = generate_dek()?;
let summary = rotate_project_key_wrapping(metadata, current_dek, &new_dek)?;
update_master_password_metadata(metadata, &new_dek, passphrase)?;
seal_vault_metadata(metadata, &new_dek)?;
commit_vault_pair(
std::path::Path::new(&vault_file()),
std::path::Path::new(&secrets_file()),
VaultPairWrite {
metadata,
secrets_lock_bytes: None,
},
)?;
record_ratchet_best_effort(&summary);
invalidate_cache()?;
Ok((new_dek, summary))
}
pub fn record_ratchet_best_effort(summary: &RatchetSummary) {
if let Err(err) = record_ratchet(
summary.old_kek_version,
summary.new_kek_version,
summary.secrets_rewrapped,
summary.recipients_rewrapped,
) {
eprintln!(
"{} audit log write failed: {}",
"warn:".yellow().bold(),
err
);
}
}