use serde::{Serialize, Deserialize};
use serde_json;
use std::{fs, path::{Path, PathBuf}};
use dirs::config_dir;
use chrono::{DateTime, Utc};
use std::{fmt, io::{self, Write}};
use rpassword::read_password;
use crate::securecrypto::{generate_rsa_keypair, encrypt_private_key, decrypt_private_key};
use crate::passgen::assess_password_strength;
#[derive(Debug, Serialize, Deserialize)]
pub struct VaultMetadata {
pub name: String,
pub path: String,
pub opt_private_key: Option<String>,
pub opt_public_key: Option<String>,
pub created_at: String,
pub last_modified: String,
}
impl VaultMetadata {
pub fn get_vaultmetadata(vaultpath: &str) -> Result<Self, ConfigError> {
let metadata_path = Path::new(vaultpath).join(".metadata.json");
let metadata_file = fs::File::open(&metadata_path)
.map_err(|e| format!("Failed to open metadata file: {}", e)).unwrap();
let metadata: Self = serde_json::from_reader(metadata_file)
.map_err(|e| format!("Failed to read metadata file: {}", e)).unwrap();
Ok(metadata)
}
pub fn from_vault(vault: &Vault) -> Self {
Self {
name: vault.name.clone(),
path: vault.path.clone(),
opt_private_key: None,
opt_public_key: None,
created_at: vault.created_at.clone(),
last_modified: vault.last_modified.clone(),
}
}
pub fn save_vaultmetadata(&self) -> Result<(), ConfigError> {
let metadata_path = Path::new(&self.path).join(".metadata.json");
let metadata_file = fs::File::create(&metadata_path)
.map_err(|e| format!("Failed to create metadata file: {}", e)).unwrap();
serde_json::to_writer_pretty(metadata_file, &self)
.map_err(|e| format!("Failed to write metadata file: {}", e)).unwrap();
Ok(())
}
pub fn vault_updated(&mut self) {
self.last_modified = Utc::now().to_rfc3339();
self.save_vaultmetadata().unwrap();
}
pub fn get_keypair(&mut self) -> (String, String) {
if self.check_keypair() {
let (prikey, pubkey) = get_opt_password(&self).unwrap();
let password = read_password_from_stdin("Enter new Authentication Password:").unwrap();
let comfirm = read_password_from_stdin("Confirm new Authentication Password:").unwrap();
if password != comfirm {
panic!("Passwords do not match");
}
let encrypted_prikey = encrypt_private_key(&prikey, &password).unwrap();
self.update_keypair(encrypted_prikey, pubkey).unwrap();
}
(self.opt_private_key.clone().unwrap(), self.opt_public_key.clone().unwrap())
}
fn check_keypair(&self) -> bool {
let last_modified = DateTime::parse_from_rfc3339(&self.last_modified).unwrap();
let now = Utc::now();
let diff = now.signed_duration_since(last_modified);
diff.num_days() >= 90
}
fn update_keypair(&mut self, prikey: String, pubkey: String) -> Result<(), ConfigError> {
self.opt_private_key = Some(prikey);
self.opt_public_key = Some(pubkey);
self.vault_updated();
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vault {
pub name: String,
pub path: String,
pub is_default: bool,
pub created_at: String,
pub last_modified: String,
}
impl Vault {
pub fn new(name: &str, path: &str, is_default: Option<bool>) -> Self {
let timenow = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
Self {
name: name.to_string(),
path: path.to_string(),
is_default: is_default.unwrap_or(false),
created_at: timenow.clone(),
last_modified: timenow.clone(),
}
}
pub fn set_default(&mut self, is_default: bool) {
self.is_default = is_default;
self.last_modified = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
}
pub fn update_vault(&mut self) {
let mut metadata = VaultMetadata::from_vault(self);
metadata.vault_updated();
self.last_modified = Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
}
}
#[derive(Debug)]
pub enum ConfigError {
IoError(std::io::Error),
JsonError(serde_json::Error),
ConfigDirError(String),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::IoError(e) => write!(f, "IO error: {}", e),
ConfigError::JsonError(e) => write!(f, "JSON error: {}", e),
ConfigError::ConfigDirError(msg) => write!(f, "Config directory error: {}", msg),
}
}
}
impl std::error::Error for ConfigError {}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigFile {
pub username: String,
pub encrypted_private_key: String,
pub public_key: String,
pub vaults: Vec<Vault>,
pub core_password_update_time: DateTime<Utc>,
}
impl ConfigFile {
pub fn new(username: &str, encrypted_private_key: &str, public_key: &str) -> Self {
Self {
username: username.to_string(),
encrypted_private_key: encrypted_private_key.to_string(),
public_key: public_key.to_string(),
vaults: Vec::new(),
core_password_update_time: Utc::now(),
}
}
pub fn add_vault(&mut self, vault: Vault) {
self.vaults.push(vault);
self.save_config().unwrap();
}
pub fn update_vault(&mut self, vault: Vault) {
for v in &mut self.vaults {
if v.path == vault.path {
*v = vault;
break;
}
}
self.save_config().unwrap();
}
pub fn save_config(&self) -> Result<(), ConfigError> {
let config_dir = get_config_dir().map_err(ConfigError::ConfigDirError)?;
let config_file_path = config_dir.join(format!("{}.json", self.username));
fs::create_dir_all(&config_dir)
.map_err(ConfigError::IoError)?;
let config_file = fs::File::create(&config_file_path)
.map_err(ConfigError::IoError)?;
serde_json::to_writer_pretty(config_file, &self)
.map_err(ConfigError::JsonError)?;
Ok(())
}
pub fn get_private_key(&self, password: &str) -> Result<String, String> {
let key = self.encrypted_private_key.clone();
let private_key = decrypt_private_key(&key, password)?;
Ok(private_key)
}
pub fn check_corepassword_valid(&mut self, password: &str) -> Result<String, String> {
let now = Utc::now();
let daygap = now.signed_duration_since(self.core_password_update_time).num_days();
let new_password = if daygap >= 90 { loop {
println!("⚠️ Core password has expired.");
let new_core_password = read_password_from_stdin("Enter new core password: ")?;
let confirm = read_password_from_stdin("Confirm new core password: ")?;
if new_core_password != confirm {
println!("New Core Passwords do not match. Please try again.");
continue;
}
let (rating, score, feedback) = assess_password_strength(&new_core_password)?;
if score < 3 {
println!("Warning: Weak core password. {}", feedback);
println!("New Core Password is {} ({}/4). Please try again.", rating, score);
continue;
}
let private_key = self.get_private_key(password)?;
self.encrypted_private_key = encrypt_private_key(&private_key,&new_core_password)?;
self.core_password_update_time = now;
self.save_config().unwrap();
break new_core_password;
}
} else {
password.to_string()
};
Ok(new_password)
}
}
pub fn prompt_input(prompt: &str) -> Result<String, String> {
print!("{}", prompt);
io::stdout().flush().map_err(|e| e.to_string())?;
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(|e| e.to_string())?;
Ok(input.trim().to_string())
}
pub fn read_password_from_stdin(prompt: &str) -> Result<String, String> {
print!("{}", prompt);
io::stdout().flush().map_err(|e| format!("Failed to flush output: {}", e))?;
read_password().map_err(|e| format!("Failed to read password: {}", e))
}
pub fn prompt_core_password(user: String) -> Result<String, String> {
let mut core_password = read_password_from_stdin("Enter core password: ")?;
let mut config = load_user_config(&user)?;
core_password = config.check_corepassword_valid(&core_password)?;
Ok(core_password)
}
pub fn get_config_dir() -> Result<PathBuf, String> {
match config_dir() {
Some(path) => Ok(path.join("rpawomaster")),
None => Err("Could not determine configuration directory".to_string()),
}
}
pub fn load_config(username: &str, password: &str) -> Result<ConfigFile, String> {
if check_user_exist(username)? {
let config_dir = get_config_dir()?;
let config_file_path = config_dir.join(format!("{}.json", username));
if !config_file_path.exists() {
return Err(format!("Configuration file for user '{}' not found. Please run 'init' first.", username));
}
let config_data = fs::read_to_string(&config_file_path)
.map_err(|e| format!("Failed to read config file: {}", e))?;
serde_json::from_str(&config_data)
.map_err(|e| format!("Failed to parse config file: {}", e))
} else {
println!("Generating RSA key pair...");
let (private_key, public_key) = generate_rsa_keypair()?;
println!("Encrypting private key...");
let encrypted_private_key = encrypt_private_key(&private_key, password)?;
Ok(ConfigFile::new(username, &encrypted_private_key, &public_key))
}
}
pub fn check_user_exist(username: &str) -> Result<bool, String> {
let config_dir = get_config_dir()?;
let config_file_path = config_dir.join(format!("{}.json", username));
Ok(config_file_path.exists())
}
pub fn get_username(user: Option<String>) -> Result<String, String> {
match user {
Some(u) => Ok(u),
None => {
print!("Enter username: ");
io::stdout().flush().map_err(|e| e.to_string())?;
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(|e| e.to_string())?;
let input = input.trim();
if input.is_empty() {
return Err("Username cannot be empty".to_string());
}
Ok(input.to_string())
}
}
}
pub fn load_user_config(username: &str) -> Result<ConfigFile, String> {
let config_dir = get_config_dir()?;
let config_file_path = config_dir.join(format!("{}.json", username));
if !config_file_path.exists() {
return Err(format!("No configuration found for user '{}'", username));
}
let config_data = fs::read_to_string(&config_file_path)
.map_err(|e| format!("Failed to read config file: {}", e))?;
let config: ConfigFile = serde_json::from_str(&config_data)
.map_err(|e| format!("Failed to parse config file: {}", e))?;
Ok(config)
}
pub fn select_vault(config: &ConfigFile, vault_arg: Option<String>) -> Result<Vault, String> {
match vault_arg {
Some(vault_name) => {
let existing_vaults: Vec<_> = config.vaults.iter().map(|v| &v.name).collect();
config.vaults.iter()
.find(|v| v.name == vault_name)
.cloned()
.ok_or(format!("Vault '{}' not found. Existing vaults: {:?}", vault_name, existing_vaults))
},
None => {
config.vaults.iter()
.find(|v| v.is_default)
.cloned()
.ok_or("No default vault found. Please specify a vault or set a default.".to_string())
}
}
}
pub fn input_password_check() -> Result<String, String> {
loop {
let password = read_password_from_stdin("Enter new password: ")?;
let confirm = read_password_from_stdin("Confirm new password: ")?;
if password != confirm {
println!("Passwords do not match. Please try again.");
continue;
}
let (rating, score,feedback) = assess_password_strength(&password)?;
if score < 2 {
println!("⚠️ 警告: 密码安全等级较低 - {}", feedback);
let retry = prompt_input("Do you want to try again? (Y/n): ")?;
if retry.trim().is_empty() {
continue;
} else {
if retry.trim().to_lowercase() != "n" {
continue;
} else {
println!("⭐ 密码安全等级: {} ({}/4)", rating, score);
break Ok(password);
}
}
}
break Ok(password);
}
}
pub fn get_opt_password(vaultmeta: &VaultMetadata) -> Result<(String, String), String> {
let corekey = match &vaultmeta.opt_private_key {
Some(_) => {
read_password_from_stdin("Enter Authentication Password: ")?
},
None => {
let password = read_password_from_stdin("Enter Authentication Password: ")?;
let confirm = read_password_from_stdin("Confirm Authentication Password: ")?;
if password != confirm {
return Err("Passwords do not match. Please try again.".to_string());
}
password
}
};
if vaultmeta.opt_private_key.is_none() {
let (prikey, pubkey) = generate_rsa_keypair()?;
Ok((prikey, pubkey))
} else {
let prikey = decrypt_private_key(&vaultmeta.opt_private_key.clone().unwrap(), &corekey)?;
Ok((prikey, vaultmeta.opt_public_key.clone().unwrap()))
}
}