krz/rust-pass
A CLI password manager.
clone: git clone https://gitbay.org/krz/rust-pass.git
1use clap::{Arg, App};
2use cli_table::{print_stdout, Cell, Style, Table};
3use rand::{thread_rng, Rng};
4use rusqlite::{Connection, Result};
5use std::{fs, str};
6use uuid::Uuid;
7
8pub const SQLITE_DB: &str = "vault.sqlite";
9pub const KEY_FILE: &str = "vault.key";
10pub const UPPERCASE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
11pub const LOWERCASE: &str = "abcdefghijklmnopqrstuvwxyz";
12pub const NUMBERS: &str = "0123456789";
13pub const SYMBOLS: &str = ")(*&^%$#@!~";
14static DEFAULT_WORDLIST: &'static str = include_str!("wordlist.txt");
15
16#[derive(Debug)]
17struct Account {
18 uuid: String,
19 title: String,
20 username: String,
21 password: String,
22 url: String,
23}
24
25// Read user input as a string
26fn read_string() -> String {
27 let mut input = String::new();
28 std::io::stdin()
29 .read_line(&mut input)
30 .expect("can not read user input");
31 let cleaned_input = input.trim().to_string();
32 cleaned_input
33}
34
35// Read user input as a 32-bit unsigned integer
36fn read_integer() -> u32 {
37 let mut input = String::new();
38 std::io::stdin()
39 .read_line(&mut input)
40 .expect("can not read user input");
41 let cleaned_input: u32 = input.trim().parse().expect("Error");
42 cleaned_input
43}
44
45// Generate a random password string
46fn generate_password(n: u32) -> String {
47 // Get a random list of characters
48 let mut charset = String::from(UPPERCASE);
49 charset.push_str(LOWERCASE);
50 charset.push_str(SYMBOLS);
51 charset.push_str(NUMBERS);
52 let char_vec: Vec<char> = charset.chars().collect();
53
54 // Map random characters to a password
55 let mut rng = rand::thread_rng();
56 let password: String = (0..n)
57 .map(|_| {
58 let idx = rng.gen_range(0..char_vec.len());
59 char_vec[idx] as char
60 })
61 .collect();
62 password
63}
64
65// Generate a random passphrase string
66fn generate_passphrase(n: u32, passphrase_symbol: String) -> String {
67 // Load the words from file
68 let words: Vec<&str> = DEFAULT_WORDLIST.lines()
69 .collect();
70
71 // Get random words
72 let len = words.len();
73 let mut rng = thread_rng();
74 let password_words: Vec<&str> = (0..n)
75 .map(|_| words[(rng.gen::<usize>() % len) - 1])
76 .collect();
77
78 // Join passphrase together with a symbol
79 let passphrase = password_words.join(&*passphrase_symbol);
80 passphrase
81}
82
83// Create the database table, if it doesn't exist
84fn create_db() -> Result<()> {
85 let conn = Connection::open(SQLITE_DB)?;
86 conn.execute(
87 "create table if not exists accounts (
88 uuid text,
89 application text,
90 username text,
91 password text,
92 url text
93 )",
94 [],
95 )?;
96 Ok(())
97}
98
99// Insert data into the database
100fn insert_account(uuid: String, application: String, username: String, password: String, url: String) -> Result<()> {
101 let conn = Connection::open(SQLITE_DB)?;
102 conn.execute(
103 "INSERT INTO accounts (uuid, application, username, password, url) values (?1, ?2, ?3, ?4, ?5)",
104 [uuid, application, username, password, url],
105 )?;
106 Ok(())
107}
108
109// Delete data from the database
110fn update_account(uuid: String, field_string: String, new_value: String) -> Result<()> {
111 let mut field: usize = usize::MAX;
112 if (field_string == "title") | (field_string == "Title") {
113 field = 0;
114 } else if (field_string == "username") | (field_string == "Username") {
115 field = 1;
116 } else if (field_string == "password") | (field_string == "Password") {
117 field = 2;
118 } else if (field_string == "url") | (field_string == "URL") {
119 field = 3;
120 } else {
121 eprintln!("Error: Provided field to edit does not match a field in the database.");
122 }
123 println!("Field: {}: {}", field_string, field);
124 println!("New Value/UUID: {}: {}", new_value, uuid);
125 let queries = vec![
126 "UPDATE accounts SET application = ?1 WHERE uuid = ?2",
127 "UPDATE accounts SET username = ?1 WHERE uuid = ?2",
128 "UPDATE accounts SET password = ?1 WHERE uuid = ?2",
129 "UPDATE accounts SET url = ?1 WHERE uuid = ?2",
130 ];
131 println!("Query: {}", queries[field]);
132 let conn = Connection::open(SQLITE_DB)?;
133 conn.execute(
134 queries[field],
135 [new_value, uuid],
136 )?;
137 Ok(())
138}
139
140// Delete data from the database
141fn delete_account(uuid: String) -> Result<()> {
142 let conn = Connection::open(SQLITE_DB)?;
143 conn.execute(
144 "DELETE FROM accounts WHERE uuid = ?1",
145 [uuid],
146 )?;
147 Ok(())
148}
149
150// Read all records from the database and print
151fn read_db() -> Result<()> {
152 // Connect to the database and select all accounts
153 let conn = Connection::open(SQLITE_DB)?;
154 let mut stmt = conn.prepare(
155 "SELECT * from accounts",
156 )?;
157
158 // Map each account returned from SQLite to an Account struct
159 let accounts = stmt.query_map([], |row| {
160 Ok(Account {
161 uuid: row.get(0)?,
162 title: row.get(1)?,
163 username: row.get(2)?,
164 password: row.get(3)?,
165 url: row.get(4)?,
166 })
167 })?;
168
169 // Loop through saved accounts and collect them in a vec
170 let mut tmp_table = vec![];
171 for account in accounts {
172 let tmp_account = account.unwrap();
173 tmp_table.push(
174 vec![
175 decrypt(tmp_account.uuid).cell(),
176 decrypt(tmp_account.title).cell(),
177 decrypt(tmp_account.username).cell(),
178 decrypt(tmp_account.password).cell(),
179 decrypt(tmp_account.url).cell(),
180 ]
181 );
182 }
183
184 // Create a new, non-mutable vec to display
185 let table = tmp_table
186 .table()
187 .title(vec![
188 "UUID".cell().bold(true),
189 "Title".cell().bold(true),
190 "Username".cell().bold(true),
191 "Password".cell().bold(true),
192 "URL".cell().bold(true),
193 ])
194 .bold(true);
195
196 assert!(print_stdout(table).is_ok());
197 Ok(())
198}
199
200// Generate a new account
201fn new() {
202 // Generate UUID
203 let uuid = Uuid::new_v4();
204 println!("UUID: {}", uuid);
205
206 // Gather input
207 println!("Enter a title for this account:");
208 let title = read_string();
209
210 println!("Enter your username:");
211 let username = read_string();
212
213 println!("(Optional) Enter a URL for this account:");
214 let url = read_string();
215
216 let password: String = loop {
217 println!("Do you want an XKCD-style passphrase [1] or a random password [2]? (1/2)");
218 let password_choice = read_integer();
219 if password_choice == 1 {
220 let passphrase_words = loop {
221 println!("Please enter number of words to include (min. 4):");
222 let passphrase_words = read_integer();
223 if passphrase_words >= 3 {
224 break passphrase_words;
225 }
226 println!("Invalid length. Please enter a number >= 3.");
227 };
228 println!("Please enter your desired separator symbol (_, -, ~, etc.:");
229 let passphrase_symbol = read_string();
230 let password = generate_passphrase(passphrase_words, passphrase_symbol);
231 break password;
232 } else if password_choice == 2 {
233 let password_length = loop {
234 println!("Please enter desired password length (min. 8):");
235 let password_length = read_integer();
236 if password_length >= 8 {
237 break password_length;
238 }
239 println!("Invalid length. Please enter a number >= 8.");
240 };
241 let password = generate_password(password_length);
242 break password;
243 }
244 println!("Invalid response. Please respond with 1 or 2.");
245 };
246
247 // Generate an Account struct
248 let account = Account {
249 uuid: encrypt(uuid.to_string()),
250 title: encrypt(title),
251 username: encrypt(username),
252 password: encrypt(password),
253 url: encrypt(url),
254 };
255
256 // Create the database, if necessary, and insert data
257 create_db();
258 insert_account(account.uuid, account.title, account.username, account.password, account.url);
259 println!("Account saved to the vault. Use `rpass --list` to see all saved accounts.");
260}
261
262// List all saved accounts
263fn list() -> Result<()> {
264 read_db();
265 Ok(())
266}
267
268// TODO: Edit a saved account
269// WARNING: This process does not currently work as expected; /
270// I think the encrypted UUID differs from the encrypted UUID in the database
271fn edit(uuid: String, field_name: String, new_value: String) {
272 update_account(encrypt(uuid), field_name, encrypt(new_value));
273}
274
275// TODO: Delete a saved account
276// WARNING: This process does not currently work as expected; /
277// I think the encrypted UUID differs from the encrypted UUID in the database
278fn delete(uuid: String) {
279 delete_account(uuid);
280}
281
282// TODO: Delete all saved accounts and delete the vault file
283fn purge() {
284 println!();
285}
286
287// Encrypt plaintext using a generated key file
288fn encrypt(plaintext: String) -> String {
289 let key_exists: bool = std::path::Path::new(KEY_FILE).exists();
290 let mut key = String::from("");
291 if key_exists {
292 key = fs::read_to_string(KEY_FILE).expect("Unable to read saved key file.");
293 } else {
294 key = fernet::Fernet::generate_key();
295 fs::write(KEY_FILE, &key).expect("Unable to save key to file.");
296 println!("Key file has been written to: {}. DO NOT DELETE OR MODIFY THIS FILE.", KEY_FILE);
297 }
298 let fernet = fernet::Fernet::new(&key).unwrap();
299 let ciphertext = fernet.encrypt(plaintext.as_ref());
300 ciphertext
301}
302
303// Decrypt ciphertext using a saved key file
304fn decrypt(ciphertext: String) -> String {
305 let key = fs::read_to_string(KEY_FILE).expect("Unable to read saved key file.");
306 let fernet = fernet::Fernet::new(&key).unwrap();
307 let decrypted_plaintext = fernet.decrypt(&ciphertext).expect("Error decrypting data - the key file may have been modified or deleted.");
308 let plaintext = String::from_utf8(decrypted_plaintext).unwrap();
309 plaintext
310}
311
312// Interpret user commands
313fn main() {
314 let matches = App::new("rpass")
315 .version("1.1")
316 .author("Christian Cleberg <hello@cmc.pub>")
317 .about("A safe and convenient command-line password vault.")
318 .arg(Arg::with_name("new")
319 .short("n")
320 .long("new")
321 .help("Create a new account")
322 .takes_value(false))
323 .arg(Arg::with_name("list")
324 .short("l")
325 .long("list")
326 .help("List all saved accounts")
327 .takes_value(false))
328 .arg(Arg::with_name("edit")
329 .short("e")
330 .long("edit")
331 .help("Edit a saved account")
332 .value_names(&["uuid", "field_name", "new_value"])
333 .takes_value(true))
334 .arg(Arg::with_name("delete")
335 .short("d")
336 .long("delete")
337 .help("Delete a saved account")
338 .value_name("uuid")
339 .takes_value(true))
340 .arg(Arg::with_name("purge")
341 .short("p")
342 .long("purge")
343 .help("Purge all saved accounts")
344 .takes_value(false))
345 .get_matches();
346
347 if matches.is_present("new") {
348 new();
349 } else if matches.is_present("list") {
350 list();
351 } else if matches.is_present("edit") {
352 let values: Vec<_> = matches.values_of("edit").unwrap().collect();
353 edit(
354 String::from(values[0]),
355 String::from(values[1]),
356 String::from(values[2]),
357 );
358 } else if matches.is_present("delete") {
359 let values: Vec<_> = matches.values_of("delete").unwrap().collect();
360 delete(String::from(values[0]));
361 } else if matches.is_present("purge") {
362 purge();
363 }
364}