#[cfg(test)]
mod tests {
use aes::Aes256;
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::{BlockModeEncrypt, KeyIvInit};
use base64::prelude::*;
use kasl::libs::data_storage::DataStorage;
use kasl::libs::secret::Secret;
use serial_test::serial;
use std::fs;
use tempfile::TempDir;
use test_context::{TestContext, test_context};
type Aes256CbcEnc = cbc::Encryptor<Aes256>;
const DEFAULT_KEY: &[u8; 32] = b"kasl_default_encryption_key_32b!";
const DEFAULT_IV: &[u8; 16] = b"kasl_iv_16b!!!!!";
struct SecretTestContext {
_temp_dir: TempDir,
}
impl TestContext for SecretTestContext {
fn setup() -> Self {
let temp_dir = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var("HOME", temp_dir.path());
std::env::set_var("LOCALAPPDATA", temp_dir.path());
}
SecretTestContext { _temp_dir: temp_dir }
}
}
fn write_legacy_file(secret_name: &str, password: &str) -> std::path::PathBuf {
let cipher = Aes256CbcEnc::new_from_slices(DEFAULT_KEY, DEFAULT_IV).unwrap();
let ciphertext = cipher.encrypt_padded_vec::<Pkcs7>(password.as_bytes());
let encoded = BASE64_STANDARD.encode(&ciphertext);
let path = DataStorage::new().get_path(secret_name).unwrap();
fs::write(&path, encoded).unwrap();
path
}
fn keyring_available() -> bool {
let probe = Secret::new(".keyring_probe_secret", "probe prompt");
let usable = probe.store("probe").is_ok();
if usable {
let _ = probe.delete();
}
usable
}
fn built_with_default_keys() -> bool {
let name = ".keycheck_secret";
write_legacy_file(name, "probe");
let recovered = Secret::new(name, "probe prompt").try_get_cached();
let matched = recovered.as_deref() == Some("probe");
if matched {
let _ = Secret::new(name, "probe prompt").delete();
}
let _ = fs::remove_file(DataStorage::new().get_path(name).unwrap());
matched
}
#[test_context(SecretTestContext)]
#[serial]
#[test]
fn legacy_file_is_migrated_into_the_keyring(_ctx: &mut SecretTestContext) {
if !keyring_available() || !built_with_default_keys() {
return;
}
let path = write_legacy_file(".migrate_secret", "s3cret-from-0.10");
let secret = Secret::new(".migrate_secret", "Enter password");
assert_eq!(secret.try_get_cached().as_deref(), Some("s3cret-from-0.10"));
assert!(!path.exists(), "legacy file should be removed after migration");
assert_eq!(secret.try_get_cached().as_deref(), Some("s3cret-from-0.10"));
secret.delete().unwrap();
}
#[test_context(SecretTestContext)]
#[serial]
#[test]
fn undecryptable_legacy_file_is_left_alone(_ctx: &mut SecretTestContext) {
let path = DataStorage::new().get_path(".corrupt_secret").unwrap();
fs::write(&path, "not base64 at all !@#$").unwrap();
let secret = Secret::new(".corrupt_secret", "Enter password");
assert_eq!(secret.try_get_cached(), None);
assert!(path.exists(), "unreadable legacy file must not be deleted");
}
#[test_context(SecretTestContext)]
#[serial]
#[test]
fn missing_credential_reports_absence_without_prompting(_ctx: &mut SecretTestContext) {
let secret = Secret::new(".absent_secret", "Enter password");
assert_eq!(secret.try_get_cached(), None);
}
#[test_context(SecretTestContext)]
#[serial]
#[test]
fn stored_credential_round_trips(_ctx: &mut SecretTestContext) {
if !keyring_available() {
return;
}
let secret = Secret::new(".roundtrip_secret", "Enter password");
secret.store("hunter2").unwrap();
assert_eq!(secret.try_get_cached().as_deref(), Some("hunter2"));
secret.delete().unwrap();
assert_eq!(secret.try_get_cached(), None);
}
#[test_context(SecretTestContext)]
#[serial]
#[test]
fn deleting_an_absent_credential_succeeds(_ctx: &mut SecretTestContext) {
if !keyring_available() {
return;
}
let secret = Secret::new(".never_stored_secret", "Enter password");
assert!(secret.delete().is_ok());
}
}