krz/rust-pass

A CLI password manager.

clone: git clone https://gitbay.org/krz/rust-pass.git

main: src/main.rs · raw

  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}