seedelf_cli/
setup.rs

1use crate::schnorr::random_scalar;
2use aes_gcm::aead::{Aead, AeadCore, KeyInit};
3use aes_gcm::{Aes256Gcm, Key, Nonce};
4use argon2::{Argon2, password_hash::SaltString};
5use base64::Engine;
6use base64::engine::general_purpose::STANDARD;
7use blstrs::Scalar;
8use colored::Colorize;
9use dirs::home_dir;
10use ff::PrimeField;
11use rand_core::OsRng;
12use rpassword::read_password;
13use serde::{Deserialize, Serialize};
14use std::fs;
15use std::io::{self, Write};
16use std::path::PathBuf;
17
18/// Data structure for storing wallet information
19#[derive(Serialize, Deserialize)]
20struct Wallet {
21    private_key: String, // Store the scalar as a hex string
22}
23
24/// Data structure for storing wallet information
25#[derive(Serialize, Deserialize)]
26struct EncryptedData {
27    salt: String,
28    nonce: String,
29    data: String,
30}
31
32/// Check if `.seedelf` exists, create it if it doesn't, and handle file logic
33pub fn check_and_prepare_seedelf() {
34    println!("{}", "Checking For Existing Seedelf Wallet".bright_blue());
35
36    let home: PathBuf = home_dir().expect("Failed to get home directory");
37    let seedelf_path: PathBuf = home.join(".seedelf");
38
39    // Check if `.seedelf` exists
40    if !seedelf_path.exists() {
41        fs::create_dir_all(&seedelf_path).expect("Failed to create .seedelf directory");
42    }
43
44    // Check if there are any files in `.seedelf`
45    let contents: Vec<fs::DirEntry> = fs::read_dir(&seedelf_path)
46        .expect("Failed to read .seedelf directory")
47        .filter_map(|entry| entry.ok())
48        .collect::<Vec<_>>();
49
50    if contents.is_empty() {
51        // Prompt the user for a wallet name
52        let wallet_name = prompt_wallet_name();
53        let wallet_file_path = seedelf_path.join(format!("{}.wallet", wallet_name));
54        create_wallet(&wallet_file_path);
55    } else {
56        for entry in &contents {
57            if let Ok(file_name) = entry.file_name().into_string() {
58                println!("Found Wallet: {}", file_name.bright_cyan());
59            }
60        }
61    }
62}
63
64/// Prompt the user to enter a wallet name
65fn prompt_wallet_name() -> String {
66    let mut wallet_name = String::new();
67    println!("{}", "\nEnter A Wallet Name:".bright_purple());
68    io::stdout().flush().unwrap();
69    io::stdin()
70        .read_line(&mut wallet_name)
71        .expect("Failed to read wallet name");
72    let final_name: String = wallet_name.trim().to_string();
73    if final_name.is_empty() {
74        println!("{}", "Wallet Must Not Have Empty Name.".red());
75        return prompt_wallet_name();
76    }
77    final_name
78}
79
80/// Create a wallet file and save a random private key
81fn create_wallet(wallet_path: &PathBuf) {
82    // Generate a random private key
83    let sk: Scalar = random_scalar(); // Requires `Field` trait in scope
84    let private_key_bytes: [u8; 32] = sk.to_repr(); // Use `to_repr()` to get canonical bytes
85    let private_key_hex: String = hex::encode(private_key_bytes);
86
87    // Serialize the wallet
88    let wallet: Wallet = Wallet {
89        private_key: private_key_hex,
90    };
91    let wallet_data: String =
92        serde_json::to_string_pretty(&wallet).expect("Failed to serialize wallet");
93
94    // Prompt user for an encryption password
95    println!(
96        "{}",
97        "\nEnter A Password To Encrypt The Wallet:".bright_purple()
98    );
99    let password: String = read_password().expect("Failed to read password");
100
101    // check for basic password complexity
102    if !password_complexity_check(password.clone()) {
103        println!(
104            "{}",
105            "Passwords Must Contain The Following:\n
106                  Minimum Length: At Least 14 Characters.
107                  Uppercase Letter: Requires At Least One Uppercase Character.
108                  Lowercase Letter: Requires At Least One Lowercase Character.
109                  Number: Requires At Least One Digit.
110                  Special Character: Requires At Least One Special Symbol.\n"
111                .red()
112        );
113        return create_wallet(wallet_path);
114    }
115
116    println!("{}", "Re-enter the password:".purple());
117    let password_copy: String = read_password().expect("Failed to read password");
118    // this is just a simple way to check if the user typed it in correctly
119    // if they do it twice then they probably mean it
120    if password != password_copy {
121        println!("{}", "Passwords Do Not Match; Try Again!".red());
122        return create_wallet(wallet_path);
123    }
124
125    let salt: SaltString = SaltString::generate(&mut OsRng);
126    let mut output_key_material: [u8; 32] = [0u8; 32];
127    let _ = Argon2::default().hash_password_into(
128        password.as_bytes(),
129        salt.to_string().as_bytes(),
130        &mut output_key_material,
131    );
132
133    // let key: &Key<Aes256Gcm> = output_key_material.into();
134    // let key = Key::from_slice(&output_key_material);
135    let key = Key::<Aes256Gcm>::from_slice(&output_key_material);
136    let cipher = Aes256Gcm::new(key);
137    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
138
139    // let nonce = Nonce::from_slice();
140    let encrypted_data = cipher
141        .encrypt(&nonce, wallet_data.as_bytes())
142        .expect("Encryption failed");
143
144    // Save encrypted data, salt, and nonce as JSON
145    let output: EncryptedData = EncryptedData {
146        salt: salt.as_str().to_string(),
147        nonce: STANDARD.encode(nonce),
148        data: STANDARD.encode(encrypted_data),
149    };
150    let output_data: String =
151        serde_json::to_string_pretty(&output).expect("Failed to serialize wallet");
152
153    // Save to file
154    fs::write(wallet_path, output_data).expect("Failed to write wallet file");
155    println!(
156        "Wallet Created At: {}",
157        wallet_path.display().to_string().yellow()
158    );
159}
160
161/// Load the wallet file and deserialize the private key into a Scalar
162pub fn load_wallet() -> Scalar {
163    let home: PathBuf = home_dir().expect("Failed to get home directory");
164    let seedelf_path: PathBuf = home.join(".seedelf");
165
166    // Get the list of files in `.seedelf`
167    let contents: Vec<fs::DirEntry> = fs::read_dir(&seedelf_path)
168        .expect("Failed to read .seedelf directory")
169        .filter_map(|entry| entry.ok())
170        .collect::<Vec<_>>();
171
172    if contents.is_empty() {
173        panic!("No wallet files found in .seedelf directory");
174    }
175
176    // Use the first file in the directory to build the wallet path
177    let first_file: &fs::DirEntry = &contents[0];
178    let wallet_path: PathBuf = first_file.path();
179
180    // Read the wallet file
181    let wallet_data: String = fs::read_to_string(&wallet_path).expect("Failed to read wallet file");
182
183    // Deserialize the wallet JSON
184    let encrypted_wallet: EncryptedData =
185        serde_json::from_str(&wallet_data).expect("Failed to parse wallet JSON");
186
187    // Prompt user for the decryption password
188    println!(
189        "{}",
190        "\nEnter The Password To Decrypt The Wallet:".bright_purple()
191    );
192    let password: String = read_password().expect("Failed to read password");
193
194    // Derive the decryption key using the provided salt
195    let salt: SaltString =
196        SaltString::from_b64(&encrypted_wallet.salt).expect("Invalid salt format");
197    let mut output_key_material: [u8; 32] = [0u8; 32];
198    let _ = Argon2::default().hash_password_into(
199        password.as_bytes(),
200        salt.to_string().as_bytes(),
201        &mut output_key_material,
202    );
203
204    let key = Key::<Aes256Gcm>::from_slice(&output_key_material);
205    let cipher = Aes256Gcm::new(key);
206
207    // Decode the nonce and encrypted data from base64
208    let nonce_bytes = STANDARD
209        .decode(&encrypted_wallet.nonce)
210        .expect("Failed to decode nonce");
211    let nonce = Nonce::from_slice(&nonce_bytes);
212
213    let encrypted_bytes = STANDARD
214        .decode(&encrypted_wallet.data)
215        .expect("Failed to decode encrypted data");
216
217    // Decrypt the wallet data
218    match cipher.decrypt(nonce, encrypted_bytes.as_ref()) {
219        Ok(decrypted_data) => {
220            // Deserialize the decrypted wallet JSON
221            let wallet: Wallet = serde_json::from_slice(&decrypted_data)
222                .expect("Failed to parse decrypted wallet JSON");
223
224            // Decode the hex string back into bytes
225            let private_key_bytes: Vec<u8> =
226                hex::decode(wallet.private_key).expect("Failed to decode private key hex");
227
228            // Convert bytes to Scalar
229            Scalar::from_repr(private_key_bytes.try_into().expect("Invalid key length"))
230                .expect("Failed to reconstruct Scalar from bytes")
231        }
232        Err(_) => {
233            eprintln!("{}", "Failed To Decrypt; Try Again!".red());
234            load_wallet()
235        }
236    }
237}
238
239pub fn password_complexity_check(password: String) -> bool {
240    // length check, 14 for now
241    if password.len() < 14 {
242        return false;
243    }
244
245    // must contain uppercase
246    if !password.chars().any(|c| c.is_uppercase()) {
247        return false;
248    }
249
250    // must contain lowercase
251    if !password.chars().any(|c| c.is_lowercase()) {
252        return false;
253    }
254
255    // must contain number
256    if !password.chars().any(|c| c.is_ascii_digit()) {
257        return false;
258    }
259
260    // must contain special character
261    if !password
262        .chars()
263        .any(|c| r#"~!@#$%^&*()_-+=<>?/|{}[]:;"'.,"#.contains(c))
264    {
265        return false;
266    }
267    true
268}