use crate::core::api::ApiError;
use crate::core::auth::{AuthProfile, OAuth2Token};
use aes_gcm::{
aead::{Aead, AeadCore, KeyInit, OsRng},
Aes256Gcm, Key, Nonce,
};
use anyhow::{Context, Result};
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHasher};
use chrono::Utc;
use std::fs;
use std::path::PathBuf;
fn get_auth_dir() -> Result<PathBuf> {
let home = dirs::home_dir().context("Could not determine home directory")?;
let auth_dir = home.join(".mrapids").join("auth");
fs::create_dir_all(&auth_dir)?;
Ok(auth_dir)
}
fn get_tokens_dir() -> Result<PathBuf> {
let auth_dir = get_auth_dir()?;
let tokens_dir = auth_dir.join("tokens");
fs::create_dir_all(&tokens_dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&tokens_dir)?.permissions();
perms.set_mode(0o700); fs::set_permissions(&tokens_dir, perms)?;
}
Ok(tokens_dir)
}
fn derive_encryption_key(profile: &str) -> Result<[u8; 32]> {
let machine_id = match fs::read_to_string("/etc/machine-id") {
Ok(id) => id.trim().to_string(),
Err(_) => {
hostname::get()?.to_string_lossy().to_string()
}
};
let salt = format!("mrapids-{}-{}", machine_id, profile);
let salt = SaltString::encode_b64(salt.as_bytes())
.map_err(|e| ApiError::InternalError(format!("Failed to create salt: {}", e)))?;
let argon2 = Argon2::default();
let password = format!("{}-{}", machine_id, profile);
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| ApiError::InternalError(format!("Failed to derive key: {}", e)))?;
let hash = password_hash.hash.unwrap();
let mut key = [0u8; 32];
key.copy_from_slice(&hash.as_bytes()[..32]);
Ok(key)
}
fn encrypt_data(data: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let ciphertext = cipher
.encrypt(&nonce, data)
.map_err(|e| ApiError::InternalError(format!("Encryption failed: {}", e)))?;
let mut result = nonce.to_vec();
result.extend_from_slice(&ciphertext);
Ok(result)
}
fn decrypt_data(encrypted: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
if encrypted.len() < 12 {
return Err(ApiError::ValidationError("Invalid encrypted data".to_string()).into());
}
let (nonce_bytes, ciphertext) = encrypted.split_at(12);
let nonce = Nonce::from_slice(nonce_bytes);
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
let plaintext = cipher
.decrypt(nonce, ciphertext)
.map_err(|e| ApiError::InternalError(format!("Decryption failed: {}", e)))?;
Ok(plaintext)
}
pub fn store_tokens(profile: &str, tokens: &OAuth2Token) -> Result<()> {
let tokens_dir = get_tokens_dir()?;
let token_path = tokens_dir.join(format!("{}.enc", profile));
let token_data = serde_json::to_vec(tokens)?;
let key = derive_encryption_key(profile)?;
let encrypted = encrypt_data(&token_data, &key)?;
fs::write(&token_path, encrypted)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&token_path)?.permissions();
perms.set_mode(0o600); fs::set_permissions(&token_path, perms)?;
}
Ok(())
}
pub fn load_tokens(profile: &str) -> Result<OAuth2Token> {
let tokens_dir = get_tokens_dir()?;
let token_path = tokens_dir.join(format!("{}.enc", profile));
if !token_path.exists() {
return Err(
ApiError::AuthError(format!("No tokens found for profile '{}'", profile)).into(),
);
}
let encrypted = fs::read(&token_path)?;
let key = derive_encryption_key(profile)?;
let decrypted = decrypt_data(&encrypted, &key)?;
let tokens: OAuth2Token = serde_json::from_slice(&decrypted)?;
if let Ok(mut auth_profile) = load_profile(profile) {
auth_profile.last_used = Some(Utc::now());
let _ = store_profile(&auth_profile);
}
Ok(tokens)
}
pub fn store_profile(profile: &AuthProfile) -> Result<()> {
let auth_dir = get_auth_dir()?;
let profiles_path = auth_dir.join("profiles.json");
let mut profiles: Vec<AuthProfile> = if profiles_path.exists() {
let data = fs::read_to_string(&profiles_path)?;
serde_json::from_str(&data).unwrap_or_default()
} else {
Vec::new()
};
if let Some(pos) = profiles.iter().position(|p| p.name == profile.name) {
profiles[pos] = profile.clone();
} else {
profiles.push(profile.clone());
}
let data = serde_json::to_string_pretty(&profiles)?;
fs::write(profiles_path, data)?;
Ok(())
}
pub fn load_profile(name: &str) -> Result<AuthProfile> {
let auth_dir = get_auth_dir()?;
let profiles_path = auth_dir.join("profiles.json");
if !profiles_path.exists() {
return Err(ApiError::AuthError("No profiles found".to_string()).into());
}
let data = fs::read_to_string(&profiles_path)?;
let profiles: Vec<AuthProfile> = serde_json::from_str(&data)?;
profiles
.into_iter()
.find(|p| p.name == name)
.context(format!("Profile '{}' not found", name))
}
pub fn list_profiles() -> Result<Vec<AuthProfile>> {
let auth_dir = get_auth_dir()?;
let profiles_path = auth_dir.join("profiles.json");
if !profiles_path.exists() {
return Ok(Vec::new());
}
let data = fs::read_to_string(&profiles_path)?;
let profiles: Vec<AuthProfile> = serde_json::from_str(&data)?;
Ok(profiles)
}
pub fn delete_profile(name: &str) -> Result<()> {
let tokens_dir = get_tokens_dir()?;
let token_path = tokens_dir.join(format!("{}.enc", name));
if token_path.exists() {
fs::remove_file(token_path)?;
}
let auth_dir = get_auth_dir()?;
let provider_path = auth_dir.join("providers").join(format!("{}.json", name));
if provider_path.exists() {
fs::remove_file(provider_path)?;
}
let profiles_path = auth_dir.join("profiles.json");
if profiles_path.exists() {
let data = fs::read_to_string(&profiles_path)?;
let mut profiles: Vec<AuthProfile> = serde_json::from_str(&data)?;
profiles.retain(|p| p.name != name);
let data = serde_json::to_string_pretty(&profiles)?;
fs::write(profiles_path, data)?;
}
Ok(())
}
pub fn profile_exists(name: &str) -> bool {
if let Ok(profiles) = list_profiles() {
profiles.iter().any(|p| p.name == name)
} else {
false
}
}