use crate::schnorr::random_scalar;
use aes_gcm::aead::{Aead, AeadCore, KeyInit};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use argon2::{password_hash::SaltString, Argon2};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use blstrs::Scalar;
use colored::Colorize;
use dirs::home_dir;
use ff::PrimeField;
use rand_core::OsRng;
use rpassword::read_password;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
#[derive(Serialize, Deserialize)]
struct Wallet {
private_key: String, }
#[derive(Serialize, Deserialize)]
struct EncryptedData {
salt: String,
nonce: String,
data: String,
}
pub fn check_and_prepare_seedelf() {
println!("{}", "Checking For Existing Seedelf Wallet".bright_blue());
let home: PathBuf = home_dir().expect("Failed to get home directory");
let seedelf_path: PathBuf = home.join(".seedelf");
if !seedelf_path.exists() {
fs::create_dir_all(&seedelf_path).expect("Failed to create .seedelf directory");
}
let contents: Vec<fs::DirEntry> = fs::read_dir(&seedelf_path)
.expect("Failed to read .seedelf directory")
.filter_map(|entry| entry.ok())
.collect::<Vec<_>>();
if contents.is_empty() {
let wallet_name = prompt_wallet_name();
let wallet_file_path = seedelf_path.join(format!("{}.wallet", wallet_name));
create_wallet(&wallet_file_path);
} else {
for entry in &contents {
if let Ok(file_name) = entry.file_name().into_string() {
println!("Found Wallet: {}", file_name.bright_cyan());
}
}
}
}
fn prompt_wallet_name() -> String {
let mut wallet_name = String::new();
println!("{}", "\nEnter A Wallet Name:".bright_purple());
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut wallet_name)
.expect("Failed to read wallet name");
let final_name: String = wallet_name.trim().to_string();
if final_name.is_empty() {
println!("{}", "Wallet Must Not Have Empty Name.".red());
return prompt_wallet_name();
}
final_name
}
fn create_wallet(wallet_path: &PathBuf) {
let sk: Scalar = random_scalar(); let private_key_bytes: [u8; 32] = sk.to_repr(); let private_key_hex: String = hex::encode(private_key_bytes);
let wallet: Wallet = Wallet {
private_key: private_key_hex,
};
let wallet_data: String =
serde_json::to_string_pretty(&wallet).expect("Failed to serialize wallet");
println!(
"{}",
"\nEnter A Password To Encrypt The Wallet:".bright_purple()
);
let password: String = read_password().expect("Failed to read password");
if !password_complexity_check(password.clone()) {
println!(
"{}",
"Passwords Must Contain The Following:\n
Minimum Length: At Least 14 Characters.
Uppercase Letter: Requires At Least One Uppercase Character.
Lowercase Letter: Requires At Least One Lowercase Character.
Number: Requires At Least One Digit.
Special Character: Requires At Least One Special Symbol.\n"
.red()
);
return create_wallet(wallet_path);
}
println!("{}", "Re-enter the password:".purple());
let password_copy: String = read_password().expect("Failed to read password");
if password != password_copy {
println!("{}", "Passwords Do Not Match; Try Again!".red());
return create_wallet(wallet_path);
}
let salt: SaltString = SaltString::generate(&mut OsRng);
let mut output_key_material: [u8; 32] = [0u8; 32];
let _ = Argon2::default().hash_password_into(
password.as_bytes(),
salt.to_string().as_bytes(),
&mut output_key_material,
);
let key = Key::<Aes256Gcm>::from_slice(&output_key_material);
let cipher = Aes256Gcm::new(key);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let encrypted_data = cipher
.encrypt(&nonce, wallet_data.as_bytes())
.expect("Encryption failed");
let output: EncryptedData = EncryptedData {
salt: salt.as_str().to_string(),
nonce: STANDARD.encode(nonce),
data: STANDARD.encode(encrypted_data),
};
let output_data: String =
serde_json::to_string_pretty(&output).expect("Failed to serialize wallet");
fs::write(wallet_path, output_data).expect("Failed to write wallet file");
println!(
"Wallet Created At: {}",
wallet_path.display().to_string().yellow()
);
}
pub fn load_wallet() -> Scalar {
let home: PathBuf = home_dir().expect("Failed to get home directory");
let seedelf_path: PathBuf = home.join(".seedelf");
let contents: Vec<fs::DirEntry> = fs::read_dir(&seedelf_path)
.expect("Failed to read .seedelf directory")
.filter_map(|entry| entry.ok())
.collect::<Vec<_>>();
if contents.is_empty() {
panic!("No wallet files found in .seedelf directory");
}
let first_file: &fs::DirEntry = &contents[0];
let wallet_path: PathBuf = first_file.path();
let wallet_data: String = fs::read_to_string(&wallet_path).expect("Failed to read wallet file");
let encrypted_wallet: EncryptedData =
serde_json::from_str(&wallet_data).expect("Failed to parse wallet JSON");
println!(
"{}",
"\nEnter The Password To Decrypt The Wallet:".bright_purple()
);
let password: String = read_password().expect("Failed to read password");
let salt: SaltString =
SaltString::from_b64(&encrypted_wallet.salt).expect("Invalid salt format");
let mut output_key_material: [u8; 32] = [0u8; 32];
let _ = Argon2::default().hash_password_into(
password.as_bytes(),
salt.to_string().as_bytes(),
&mut output_key_material,
);
let key = Key::<Aes256Gcm>::from_slice(&output_key_material);
let cipher = Aes256Gcm::new(key);
let nonce_bytes = STANDARD
.decode(&encrypted_wallet.nonce)
.expect("Failed to decode nonce");
let nonce = Nonce::from_slice(&nonce_bytes);
let encrypted_bytes = STANDARD
.decode(&encrypted_wallet.data)
.expect("Failed to decode encrypted data");
match cipher.decrypt(nonce, encrypted_bytes.as_ref()) {
Ok(decrypted_data) => {
let wallet: Wallet = serde_json::from_slice(&decrypted_data)
.expect("Failed to parse decrypted wallet JSON");
let private_key_bytes: Vec<u8> =
hex::decode(wallet.private_key).expect("Failed to decode private key hex");
Scalar::from_repr(private_key_bytes.try_into().expect("Invalid key length"))
.expect("Failed to reconstruct Scalar from bytes")
}
Err(_) => {
eprintln!("{}", "Failed To Decrypt; Try Again!".red());
load_wallet()
}
}
}
pub fn password_complexity_check(password: String) -> bool {
if password.len() < 14 {
return false;
}
if !password.chars().any(|c| c.is_uppercase()) {
return false;
}
if !password.chars().any(|c| c.is_lowercase()) {
return false;
}
if !password.chars().any(|c| c.is_ascii_digit()) {
return false;
}
if !password
.chars()
.any(|c| r#"~!@#$%^&*()_-+=<>?/|{}[]:;"'.,"#.contains(c))
{
return false;
}
true
}