use super::data_storage::DataStorage;
use aes::Aes256;
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::{BlockModeDecrypt, KeyIvInit};
use anyhow::{Context, Result};
use base64::prelude::*;
use dialoguer::{Password, theme::ColorfulTheme};
use keyring::Entry;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
include!(concat!(env!("OUT_DIR"), "/app_metadata.rs"));
type Aes256CbcDec = cbc::Decryptor<Aes256>;
const KEYRING_SERVICE: &str = "lacodda.kasl";
#[derive(Clone, Debug)]
pub struct Secret {
account: String,
prompt: String,
legacy_file_path: PathBuf,
}
impl Secret {
pub fn new(secret_name: &str, prompt: &str) -> Self {
let account = secret_name.trim_start_matches('.').trim_end_matches("_secret").to_string();
let legacy_file_path = DataStorage::new().get_path(secret_name).unwrap_or_else(|_| PathBuf::from(secret_name));
Self {
account,
prompt: prompt.to_owned(),
legacy_file_path,
}
}
fn entry(&self) -> Result<Entry> {
Entry::new(KEYRING_SERVICE, &self.account).with_context(|| format!("cannot open keyring entry for '{}'", self.account))
}
pub fn get_or_prompt(&self) -> Result<String> {
if let Some(password) = self.try_get_cached() {
return Ok(password);
}
self.prompt()
}
pub fn try_get_cached(&self) -> Option<String> {
if let Ok(entry) = self.entry()
&& let Ok(password) = entry.get_password()
{
return Some(password);
}
self.migrate_legacy_file()
}
pub fn prompt(&self) -> Result<String> {
crate::libs::prompt::ensure_interactive(&format!(
"{} - but there is no terminal to ask; run `kasl init` interactively first",
self.prompt
))?;
let password = Password::with_theme(&ColorfulTheme::default()).with_prompt(&self.prompt).interact()?;
self.store(&password)?;
Ok(password)
}
pub fn store(&self, password: &str) -> Result<()> {
self.entry()?
.set_password(password)
.with_context(|| format!("cannot store credential '{}' in the OS keyring", self.account))
}
pub fn delete(&self) -> Result<()> {
match self.entry()?.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(e).with_context(|| format!("cannot remove credential '{}' from the OS keyring", self.account)),
}
}
fn migrate_legacy_file(&self) -> Option<String> {
let password = self.decrypt_legacy_file().ok()?;
if self.store(&password).is_err() {
return Some(password);
}
let _ = fs::remove_file(&self.legacy_file_path);
Some(password)
}
fn decrypt_legacy_file(&self) -> Result<String> {
let mut file = fs::File::open(&self.legacy_file_path)?;
let mut encoded = String::new();
file.read_to_string(&mut encoded)?;
let ciphertext = BASE64_STANDARD.decode(encoded.trim())?;
let cipher = Aes256CbcDec::new_from_slices(APP_METADATA_ENCRYPTION_KEY, APP_METADATA_ENCRYPTION_IV)?;
let plaintext = cipher
.decrypt_padded_vec::<Pkcs7>(&ciphertext)
.map_err(|e| anyhow::anyhow!("failed to decrypt stored secret: {e}"))?;
Ok(String::from_utf8(plaintext)?)
}
}