~cyborg/crypt

ref: d0313ab66246d36a90c883dfa994ccba340ddfb3 crypt/src/main.rs -rw-r--r-- 9.5 KiB
d0313ab6christian-cleberg update dependencies and clean up code 4 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
use clap::{Arg, App};
use cli_table::{print_stdout, Cell, Style, Table};
use rand::{thread_rng, Rng};
use rusqlite::{Connection, Result};
use std::{fs, str};
use uuid::Uuid;

pub const SQLITE_DB: &str = "vault.sqlite";
pub const KEY_FILE: &str = "vault.key";
pub const UPPERCASE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
pub const LOWERCASE: &str = "abcdefghijklmnopqrstuvwxyz";
pub const NUMBERS: &str = "0123456789";
pub const SYMBOLS: &str = ")(*&^%$#@!~";
static DEFAULT_WORDLIST: &'static str = include_str!("wordlist.txt");

#[derive(Debug)]
struct Account {
    uuid: String,
    title: String,
    username: String,
    password: String,
    url: String,
}

// Read user input as a string
fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    let cleaned_input = input.trim().to_string();
    cleaned_input
}

// Read user input as a 32-bit unsigned integer
fn read_integer() -> u32 {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    let cleaned_input: u32 = input.trim().parse().expect("Error");
    cleaned_input
}

// Generate a random password string
fn generate_password(n: u32) -> String {
    // Get a random list of characters
    let mut charset = String::from(UPPERCASE);
    charset.push_str(LOWERCASE);
    charset.push_str(SYMBOLS);
    charset.push_str(NUMBERS);
    let char_vec: Vec<char> = charset.chars().collect();

    // Map random characters to a password
    let mut rng = rand::thread_rng();
    let password: String = (0..n)
        .map(|_| {
            let idx = rng.gen_range(0..char_vec.len());
            char_vec[idx] as char
        })
        .collect();
    password
}

// Generate a random passphrase string
fn generate_passphrase(n: u32, passphrase_symbol: String) -> String {
    // Load the words from file
    let words: Vec<&str> = DEFAULT_WORDLIST.lines()
        .collect();

    // Get random words
    let len = words.len();
    let mut rng = thread_rng();
    let password_words: Vec<&str> = (0..n)
        .map(|_| words[(rng.gen::<usize>() % len) - 1])
        .collect();

    // Join passphrase together with a symbol
    let passphrase = password_words.join(&*passphrase_symbol);
    passphrase
}

// Create the database table, if it doesn't exist
fn create_db() -> Result<()> {
    let conn = Connection::open(SQLITE_DB)?;
    conn.execute(
        "create table if not exists accounts (
             uuid text,
             application text,
             username text,
             password text,
             url text
         )",
        [],
    )?;
    Ok(())
}

// Insert data into the database
fn insert_db(uuid: String, application: String, username: String, password: String, url: String) -> Result<()> {
    let conn = Connection::open(SQLITE_DB)?;
    conn.execute(
        "INSERT INTO accounts (uuid, application, username, password, url) values (?1, ?2, ?3, ?4, ?5)",
        [uuid, application, username, password, url],
    )?;

    Ok(())
}

// Read all records from the database and print
fn read_db() -> Result<()> {
    // Connect to the database and select all accounts
    let conn = Connection::open(SQLITE_DB)?;
    let mut stmt = conn.prepare(
        "SELECT * from accounts",
    )?;

    // Map each account returned from SQLite to an Account struct
    let accounts = stmt.query_map([], |row| {
        Ok(Account {
            uuid: row.get(0)?,
            title: row.get(1)?,
            username: row.get(2)?,
            password: row.get(3)?,
            url: row.get(4)?,
        })
    })?;

    // Loop through saved accounts and collect them in a vec
    let mut tmp_table = vec![];
    for account in accounts {
        let tmp_account = account.unwrap();
        tmp_table.push(
            vec![
                decrypt(tmp_account.uuid).cell(),
                decrypt(tmp_account.title).cell(),
                decrypt(tmp_account.username).cell(),
                decrypt(tmp_account.password).cell(),
                decrypt(tmp_account.url).cell(),
            ]
        );
    }

    // Create a new, non-mutable vec to display
    let table = tmp_table
        .table()
        .title(vec![
            "UUID".cell().bold(true),
            "Title".cell().bold(true),
            "Username".cell().bold(true),
            "Password".cell().bold(true),
            "URL".cell().bold(true),
        ])
        .bold(true);

    assert!(print_stdout(table).is_ok());
    Ok(())
}

// Generate a new account
fn new() {
    // Generate UUID
    let uuid = Uuid::new_v4();
    println!("UUID: {}", uuid);

    // Gather input
    println!("Enter a title for this account:");
    let title = read_string();

    println!("Enter your username:");
    let username = read_string();

    println!("(Optional) Enter a URL for this account:");
    let url = read_string();

    let password: String = loop {
        println!("Do you want an XKCD-style passphrase [1] or a random password [2]? (1/2)");
        let password_choice = read_integer();
        if password_choice == 1 {
            let passphrase_words = loop {
                println!("Please enter number of words to include (min. 4):");
                let passphrase_words = read_integer();
                if passphrase_words >= 3 {
                    break passphrase_words;
                }
                println!("Invalid length. Please enter a number >= 3.");
            };
            println!("Please enter your desired separator symbol (_, -, ~, etc.:");
            let passphrase_symbol = read_string();
            let password = generate_passphrase(passphrase_words, passphrase_symbol);
            break password;
        } else if password_choice == 2 {
            let password_length = loop {
                println!("Please enter desired password length (min. 8):");
                let password_length = read_integer();
                if password_length >= 8 {
                    break password_length;
                }
                println!("Invalid length. Please enter a number >= 8.");
            };
            let password = generate_password(password_length);
            break password;
        }
        println!("Invalid response. Please respond with 1 or 2.");
    };

    // Generate an Account struct
    let account = Account {
        uuid: encrypt(uuid.to_string()),
        title: encrypt(title),
        username: encrypt(username),
        password: encrypt(password),
        url: encrypt(url),
    };

    // Create the database, if necessary, and insert data
    create_db();
    insert_db(account.uuid, account.title, account.username, account.password, account.url);
    println!("Account saved to the vault. Use `crypt --list` to see all saved accounts.");
}

// List all saved accounts
fn list() -> Result<()> {
    read_db();
    Ok(())
}

// TODO: Edit a saved account
fn edit() {
    println!();
}

// TODO: Delete a saved account
fn delete() {
    println!();
}

// TODO: Delete all saved accounts and delete the vault file
fn purge() {
    println!();
}

// Encrypt plaintext using a generated key file
fn encrypt(plaintext: String) -> String {
    let key_exists: bool = std::path::Path::new(KEY_FILE).exists();
    let mut key = String::from("");
    if key_exists {
        key = fs::read_to_string(KEY_FILE).expect("Unable to read saved key file.");
    } else {
        key = fernet::Fernet::generate_key();
        fs::write(KEY_FILE, &key).expect("Unable to save key to file.");
        println!("Key file has been written to: {}. DO NOT DELETE OR MODIFY THIS FILE.", KEY_FILE);
    }
    let fernet = fernet::Fernet::new(&key).unwrap();
    let ciphertext = fernet.encrypt(plaintext.as_ref());
    ciphertext
}

// Decrypt ciphertext using a saved key file
fn decrypt(ciphertext: String) -> String {
    let key = fs::read_to_string(KEY_FILE).expect("Unable to read saved key file.");
    let fernet = fernet::Fernet::new(&key).unwrap();
    let decrypted_plaintext = fernet.decrypt(&ciphertext).expect("Error decrypting data - the key file may have been modified or deleted.");
    let plaintext = String::from_utf8(decrypted_plaintext).unwrap();
    plaintext
}

// Interpret user commands
fn main() {
    let matches = App::new("Crypt")
        .version("1.0")
        .author("Christian Cleberg <hello@cleberg.io>")
        .about("A safe and convenient command-line password vault.")
        .arg(Arg::with_name("new")
            .short("n")
            .long("new")
            .help("Create a new account")
            .takes_value(false))
        .arg(Arg::with_name("list")
            .short("l")
            .long("list")
            .help("List all saved accounts")
            .takes_value(false))
        .arg(Arg::with_name("edit")
            .short("e")
            .long("edit")
            .help("Edit a saved account")
            .value_name("uuid")
            .value_name("field")
            .takes_value(true))
        .arg(Arg::with_name("delete")
            .short("d")
            .long("delete")
            .help("Delete a saved account")
            .value_name("uuid")
            .takes_value(true))
        .arg(Arg::with_name("purge")
            .short("p")
            .long("purge")
            .help("Purge all saved accounts")
            .takes_value(false))
        .get_matches();

    if matches.is_present("new") {
        new();
    } else if matches.is_present("list") {
        list();
    } else if matches.is_present("edit") {
        edit();
    } else if matches.is_present("delete") {
        delete();
    } else if matches.is_present("purge") {
        purge();
    }
}