use crate::errors::{LicenseError, LicenseResult};
use crate::hardware::get_hardware_id;
use std::io::ErrorKind;
use std::path::PathBuf;
use tokio::fs;
const LICENSE_FILE: &str = "talos_license.enc";
const CACHE_FILE: &str = "talos_cache.enc";
const KEYRING_SERVICE: &str = "talos";
#[derive(Debug, Clone, Copy)]
pub enum StorageKey {
License,
Cache,
}
impl StorageKey {
fn keyring_name(&self) -> String {
let hw_id = get_hardware_id();
match self {
StorageKey::License => format!("license:{}", hw_id),
StorageKey::Cache => format!("cache:{}", hw_id),
}
}
fn filename(&self) -> &'static str {
match self {
StorageKey::License => LICENSE_FILE,
StorageKey::Cache => CACHE_FILE,
}
}
}
fn get_app_data_dir() -> Option<PathBuf> {
dirs::data_dir().map(|p| p.join("talos"))
}
fn get_storage_path(key: StorageKey) -> Option<PathBuf> {
get_app_data_dir().map(|dir| dir.join(key.filename()))
}
fn get_legacy_path(key: StorageKey) -> PathBuf {
PathBuf::from(key.filename())
}
fn save_to_keyring(key: StorageKey, data: &str) -> Result<(), keyring::Error> {
let entry = keyring::Entry::new(KEYRING_SERVICE, &key.keyring_name())?;
entry.set_password(data)
}
fn load_from_keyring(key: StorageKey) -> Result<String, keyring::Error> {
let entry = keyring::Entry::new(KEYRING_SERVICE, &key.keyring_name())?;
entry.get_password()
}
fn clear_from_keyring(key: StorageKey) -> Result<(), keyring::Error> {
let entry = keyring::Entry::new(KEYRING_SERVICE, &key.keyring_name())?;
entry.delete_credential()
}
async fn save_to_file(key: StorageKey, data: &str) -> LicenseResult<()> {
let dir = get_app_data_dir().ok_or_else(|| {
LicenseError::StorageError(std::io::Error::new(
ErrorKind::NotFound,
"Could not determine app data directory",
))
})?;
fs::create_dir_all(&dir).await?;
let path = dir.join(key.filename());
fs::write(&path, data).await?;
Ok(())
}
async fn load_from_file(key: StorageKey) -> LicenseResult<String> {
let path = get_storage_path(key).ok_or_else(|| {
LicenseError::StorageError(std::io::Error::new(
ErrorKind::NotFound,
"Could not determine app data directory",
))
})?;
match fs::read_to_string(&path).await {
Ok(data) => Ok(data),
Err(e) if e.kind() == ErrorKind::NotFound => Err(LicenseError::InvalidLicense(
"No stored data found.".to_string(),
)),
Err(e) => Err(LicenseError::StorageError(e)),
}
}
async fn clear_from_file(key: StorageKey) -> LicenseResult<()> {
if let Some(path) = get_storage_path(key) {
match fs::remove_file(&path).await {
Ok(_) => Ok(()),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
Err(e) => Err(LicenseError::StorageError(e)),
}
} else {
Ok(())
}
}
async fn load_from_legacy(key: StorageKey) -> Option<String> {
let path = get_legacy_path(key);
fs::read_to_string(&path).await.ok()
}
async fn clear_legacy_file(key: StorageKey) {
let path = get_legacy_path(key);
let _ = fs::remove_file(&path).await;
}
pub async fn save_to_storage(key: StorageKey, data: &str) -> LicenseResult<()> {
match save_to_keyring(key, data) {
Ok(()) => {
log::debug!("Saved {:?} to keyring", key);
if load_from_keyring(key).is_ok() {
return Ok(());
}
log::debug!(
"Keyring save verification failed for {:?}, falling back to file",
key
);
}
Err(e) => {
log::debug!(
"Keyring save failed for {:?}: {}, falling back to file",
key,
e
);
}
}
save_to_file(key, data).await?;
log::debug!("Saved {:?} to app data directory", key);
Ok(())
}
pub async fn load_from_storage(key: StorageKey) -> LicenseResult<String> {
match load_from_keyring(key) {
Ok(data) => {
log::debug!("Loaded {:?} from keyring", key);
return Ok(data);
}
Err(e) => {
log::debug!("Keyring load failed for {:?}: {}", key, e);
}
}
match load_from_file(key).await {
Ok(data) => {
log::debug!("Loaded {:?} from app data directory", key);
if save_to_keyring(key, &data).is_ok() {
log::debug!("Migrated {:?} from app data to keyring", key);
}
return Ok(data);
}
Err(LicenseError::InvalidLicense(_)) => {
}
Err(e) => {
log::debug!("App data file load failed for {:?}: {}", key, e);
}
}
if let Some(data) = load_from_legacy(key).await {
log::info!("Found legacy {:?} file in CWD, migrating...", key);
if let Err(e) = save_to_storage(key, &data).await {
log::warn!("Failed to migrate {:?} to new storage: {}", key, e);
} else {
clear_legacy_file(key).await;
log::info!("Successfully migrated {:?} and cleaned up legacy file", key);
}
return Ok(data);
}
Err(LicenseError::InvalidLicense(
"No stored data found.".to_string(),
))
}
pub async fn clear_from_storage(key: StorageKey) -> LicenseResult<()> {
let mut last_error: Option<LicenseError> = None;
if let Err(e) = clear_from_keyring(key) {
match e {
keyring::Error::NoEntry => {}
_ => log::debug!("Failed to clear {:?} from keyring: {}", key, e),
}
}
if let Err(e) = clear_from_file(key).await {
last_error = Some(e);
}
clear_legacy_file(key).await;
match last_error {
Some(e) => Err(e),
None => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use tokio::test as tokio_test;
#[tokio_test]
#[serial]
async fn test_file_storage_roundtrip() {
let test_data = "test_encrypted_data_12345";
save_to_file(StorageKey::License, test_data)
.await
.expect("save should succeed");
let loaded = load_from_file(StorageKey::License)
.await
.expect("load should succeed");
assert_eq!(loaded, test_data);
clear_from_file(StorageKey::License)
.await
.expect("clear should succeed");
}
#[tokio_test]
#[serial]
async fn test_missing_file_returns_invalid_license() {
let _ = clear_from_file(StorageKey::Cache).await;
let result = load_from_file(StorageKey::Cache).await;
assert!(matches!(result, Err(LicenseError::InvalidLicense(_))));
}
#[tokio_test]
#[serial]
async fn test_storage_api_roundtrip() {
let test_data = "storage_api_test_data";
let _ = clear_from_storage(StorageKey::License).await;
save_to_storage(StorageKey::License, test_data)
.await
.expect("save should succeed");
let loaded = load_from_storage(StorageKey::License)
.await
.expect("load should succeed");
assert_eq!(loaded, test_data);
clear_from_storage(StorageKey::License)
.await
.expect("clear should succeed");
}
#[tokio_test]
#[serial]
async fn test_legacy_migration() {
let test_data = "legacy_migration_test_data";
let legacy_path = get_legacy_path(StorageKey::Cache);
let _ = clear_from_storage(StorageKey::Cache).await;
fs::write(&legacy_path, test_data)
.await
.expect("creating legacy file should succeed");
let loaded = load_from_storage(StorageKey::Cache)
.await
.expect("load should succeed");
assert_eq!(loaded, test_data);
assert!(
!legacy_path.exists(),
"legacy file should be deleted after migration"
);
let loaded_again = load_from_storage(StorageKey::Cache)
.await
.expect("load should still succeed after migration");
assert_eq!(loaded_again, test_data);
let _ = clear_from_storage(StorageKey::Cache).await;
}
#[test]
fn test_storage_key_names() {
let license_name = StorageKey::License.keyring_name();
let cache_name = StorageKey::Cache.keyring_name();
assert!(license_name.starts_with("license:"));
assert!(cache_name.starts_with("cache:"));
assert_eq!(StorageKey::License.filename(), "talos_license.enc");
assert_eq!(StorageKey::Cache.filename(), "talos_cache.enc");
}
#[test]
fn test_app_data_dir_exists() {
let dir = get_app_data_dir();
assert!(dir.is_some(), "app data directory should be determinable");
}
}