Build a password manager with Rust - Part 2
Tags: rust, learning
Published: 2024-06-01 16:53:44


## Introduction

After our [first article](https://damiencosset.dev/posts/build-password-manager-rust-part-1/), we have a simple command line application that can display the contents of a file where we store our passwords. We can also add a new password to that file through the command line.

In this article, we'll add some functionalities:
- Possibility to generate a new password automatically
- Add a password by getting the user's clipboard content
- Keep the possibility to type the password in
- Alert the user if the password is considered too weak

Let's go!

## First step: Improving the command line arguments

In order to handle different ways to add a password, we need to change the command line arguments. We are going to configure some flags for our application:
- one for the password generation
- one to get the password from the clipboard
- one to allow the user to write a password

We still keep the service and username arguments. The new command line arguments configuration can look like this:

```rust
#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    cmd: Commands,
}

#[derive(Subcommand)]
enum Commands {
    List,
    Add {
        #[arg(short, long)]
        service: String,
        #[arg(short, long)]
        username: String,
        #[arg(short, long, default_missing_value("true"))]
        clipboard: bool, // copy from clipboard
        #[arg(short, long, default_missing_value("true"))]
        generate: bool, // generate new password
        #[arg(short, long, default_missing_value("true"))]
        write: bool, // type new password
    },
}
```

Now, we can take three arguments: clipboard, generate and write. Each of them take default_missing_value as true so we do not have to add anything behind the flag.

Let's update our *main* function to test it out:
```rust
fn main() -> std::io::Result<()> {
    let args = Cli::parse();
    match args.cmd {
        Commands::List => display_passwords()?,
        Commands::Add {
            service,
            username,
            clipboard,
            generate,
            write,
        } => {
            println!(
                "{}, {}, clipboard: {}, generate: {}, write: {}",
                service, username, clipboard, generate, write
            )
        }
    }
    Ok(())
}
```

Now let's run it. Thanks to the advice from Vladimir Ignatev, I discovered that you can run your application through the build output in *target/...* folder, it makes it a little bit cleaner :wink:. For me, it's in the target/debug folder. So after running `cargo build`, we can run our application with:
`./target/debug/password_manager add -u toto -s serv -c`

*Note: password_manager is the name of my application, yours might be different.*

This command above gives us:

![First clipboard run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rvspiuv6njmwj0jfrorn.png)

It works! Now let's test out our other 2 flags to make sure everything is in order:

![First generate run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eh9d8o718n7jnx0smuaw.png)

![First write run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zluelmukktx8o2dkc4r9.png)

Perfect! 

## Second step: Generate a password for the user

Now that we have different options to save a new password. We need to implement the logic for each. We'll start by generating a new password. Now, there are several ways to do this. You could use a existing crate that do it for you or build something yourself. I chose to use the [*rand*](https://crates.io/crates/rand) crate. This is crate is a general-purpose random number generator. It will give us the foundation to randomly generate a password. I'd like to keep some freedom to custom the functionality later on, instead of relying on an existing crate. But feel free to use an existing crate :wink:

First, let's add our crate with `cargo add rand`
Then, let's add the following code to generate a password:

```rust
// All our imports
use clap::{Parser, Subcommand};
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
use std::io::Write;
use std::{
    fs::{self, OpenOptions},
    iter,
    path::Path,
};

// More code...

fn main() -> std::io::Result<()> {
    let args = Cli::parse();
    match args.cmd {
        Commands::List => display_passwords()?,
        Commands::Add {
            service,
            username,
            clipboard,
            generate,
            write,
        } => {
            println!(
                "{}, {}, clipboard: {}, generate: {}, write: {}",
                service, username, clipboard, generate, write
            );
            if generate {
                let password = generate_password();
                println!("{}", password)
            }
        }
    }
    Ok(())
}

fn generate_password() -> String {
    let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
                           abcdefghijklmnopqrstuvwxyz\
                           0123456789\
                           !@#$%^&*()_-+=[{]}\\;:'\",<.>/?";
    let mut rng = thread_rng();
    let password: String = iter::repeat_with(|| {
        let idx = rng.sample(Uniform::from(0..charset.len()));
        charset[idx] as char
    })
    .take(16)
    .collect();
    password
}

// rest of the code...
```

The idea is that we create a charset variable that holds all the possible characters our password can contain. The list here is a completely arbitrary choice. You could choose to use the Alphanumerical distribution provided by the *rand* crate for example if you wanted. After that, we use the *thread_rng* function provided by the *rand* crate. With an iterator and *repeat_with*, we randomly select one character from the charset. The *.take(16)* indicates that we want this function to run 16 times. Again, this is an arbitrary choice I'm making for now. After that, we collect the result of the iterator into a collection ( so a collection of chars is a String ) and return our password.

Let's test it! Let's run `cargo build` and generate our password:

![First password generation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ts19njl4w6plyf3kluf.png)


![Second password generation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ybmle0lf0o6zd1e9udbt.png)


Awesome! Now we can successfully generate random strong passwords of 16 characters.

## Third step: Getting a password from the clipboard

Let's move on to our second use-case: getting the clipboard's content to serve as our password. It's not uncommon to fetch a password from somewhere and copying it to save it. To have access to the clipboard, we'll use the [*copypasta*](https://crates.io/crates/copypasta) crate to make sure we have something that works accross all operating systems. Let's add the crate with `cargo add copypasta` and add the following code:

```rust
// Import
use copypasta::{ClipboardContext, ClipboardProvider};

fn main() -> std::io::Result<()> {
    let args = Cli::parse();
    match args.cmd {
        Commands::List => display_passwords()?,
        Commands::Add {
            service,
            username,
            clipboard,
            generate,
            write,
        } => {
            println!(
                "{}, {}, clipboard: {}, generate: {}, write: {}",
                service, username, clipboard, generate, write
            );
            if generate {
                let password = generate_password();
                println!("{}", password)
            }

            if clipboard {
                let password = get_clipboard_password();
                println!("{}", password)
            }
        }
    }
    Ok(())
}

fn get_clipboard_password() -> String {
    let mut ctx = ClipboardContext::new().unwrap();
    return ctx.get_contents().unwrap();
}
```

There, in two lines of code, we're able to get the clipboard content! Let's try it out. I'll just copy that last line.

![Get clipboard contents and print it](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uau016fgmhjnxrtj915n.png)

Nice!

## Fourth step: Making the user write the password

Finally, let's implement the last use case. We need to handle our user's input.

```rust
fn main() -> std::io::Result<()> {
    let args = Cli::parse();
    match args.cmd {
        Commands::List => display_passwords()?,
        Commands::Add {
            service,
            username,
            clipboard,
            generate,
            write,
        } => {
            println!(
                "{}, {}, clipboard: {}, generate: {}, write: {}",
                service, username, clipboard, generate, write
            );
            if generate {
                let password = generate_password();
                println!("{}", password)
            }

            if clipboard {
                let password = get_clipboard_password();
                println!("{}", password)
            }

            if write {
                let password = get_user_input();
                println!("You typed: {}", password.unwrap())
            }
        }
    }
    Ok(())
}

fn get_user_input() -> io::Result<String> {
    println!("Write it down the press Enter!");
    let mut input = String::new();
    match io::stdin().read_line(&mut input) {
        Ok(n) => return Ok(input.trim().to_string()),
        Err(error) => Err(error),
    }
}
```

We're using the *read_line* function from the *Stdin* struct. This means that the user can type what he wants until Enter is pressed. Let's run the app after `cargo build`:

![Write the password and print it](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jbnhv1blf34ltdn6b1te.png)

Awesome! We now have our 3 ways to create a new password. 

## Fifth step: Scoring passwords

For our last functionality in this article, we are going to alert the user if a password is considered too weak ( when we get it from the clipboard or the user types it in ).

To give a password a score, we are going to use the *passwords* crate. Let's add it: `cargo add passwords`.

The process will look like this:
- we get a password from the user's input or the clipboard
- we use the passwords crate to check that password score
- if the score is too low ( weak password ), we print out an alert to the user asking him to confirm that she's ok with the weak password.

I'm going to anticipate a "problem" we are going to encounter. For my use case, I would like the user to press Q or Enter to confirm or not that she wants to save a weak password. From what I understood, it's not straightforward to do this with the same io::stdin we used before because of the line-buffered nature of most terminals. Meaning, we have to wait for the user to press Enter to read the input. So, we are going to use the [*crossterm*](https://crates.io/crates/crossterm) crate that allow us to read single chars from user's input. We can add it with: `cargo add crossterm`


Let's write some code:

```rust
use crossterm::event::{read, Event, KeyCode};
use crossterm::execute;
use crossterm::style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use passwords::analyzer;
use passwords::scorer;

#[derive(PartialEq)]
enum WeakPasswordChoice {
    ABORT,
    CONTINUE,
}


fn should_save_password(password: &str) -> bool {
    if is_password_weak(password) {
        print_alert(password);
        if let Ok(choice) = read_next_char() {
            return choice == WeakPasswordChoice::CONTINUE;
        } else {
            return false;
        }
    } else {
        return true;
    }
}

fn is_password_weak(password: &str) -> bool {
    let score = scorer::score(&analyzer::analyze(password));
    return score < 80.0;
}

fn print_alert(password: &str) {
    let alert = format!(
        "{} is a weak password. Press Enter to continue anyway. Press Q to abort and try again!",
        password
    );
    execute!(
        io::stdout(),
        SetForegroundColor(Color::Red),
        SetAttribute(Attribute::Bold),
        Print(alert),
        ResetColor,
        SetAttribute(Attribute::Reset)
    )
    .unwrap();
}


fn read_next_char() -> io::Result<WeakPasswordChoice> {
    enable_raw_mode()?;
    let result = loop {
        match read()? {
            Event::Key(event) => {
                match event.code {
                    KeyCode::Char('q') | KeyCode::Char('Q') => {
                        break Ok(WeakPasswordChoice::ABORT);
                    }
                    KeyCode::Enter => {
                        break Ok(WeakPasswordChoice::CONTINUE);
                    }
                    _ => {}
                }
            }
            _ => {}
        }
    };

    disable_raw_mode()?;
    result
}
```

We added a lot of things here:
- The *should_save_password* function takes the password and returns a boolean. It tells us whether or not the password can be saved in our file.
- To achieve this, we first create a *is_password_weak* function. This function uses the analyzer and scorer from the *passwords* crate. By getting a score for our password, we are able to determine if it is weak or not. I chose to consider 80 or less as a weak score. To get more information, you can check the [crate's docs](https://docs.rs/passwords/latest/passwords/).
- If the password is considered weak. We print an alert to the user. We use the *crossterm* crate to style the output in the *print_alert* function.
- The *read_next_char* function checks the next character the user will enter. To do this, we use *crossterm*'s functionalities to make it easier. If it's a Q or q, we will abort. If it's enter, we will continue the process of saving a password.
- To express those two choices, I created an enum *WeakPasswordChoice*. Notice that nothing happens if the user presses any other keys.

Note: The `#[derive(PartialEq)]` on the enum allows us to do comparison like this: `choice == WeakPasswordChoice::CONTINUE;`

Let's test it out by adding our *should_save_password* function in our clipboard and write use case:

```rust
  if clipboard {
        let password = get_clipboard_password();
        println!("{}", password);
        should_save_password(&password);
    }

    if write {
        let password = get_user_input().unwrap();
        println!("You typed: {}", password);
        should_save_password(&password);
    }
```

Let's run `cargo build && ./target/debug/password_manager add -u myUsername -s aService -w`:

![Alert for weak password typed in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hr0itqumjlz3fw2j76zc.png)

Or: `./target/debug/password_manager add -u myUsername -s aService -c`

![Alert for weak password with clipboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/phpz46w2djstak2remp6.png)

Amazing! Now for the final step! We just need to connect the existing functionality of saving password to the code we just wrote.


Here's the full code for the end of our part 2:

```rust
use clap::{Parser, Subcommand};
use copypasta::{ClipboardContext, ClipboardProvider};
use rand::distributions::Uniform;
use rand::{thread_rng, Rng};
use std::io;
use std::io::Write;
use std::{
    fs::{self, OpenOptions},
    iter,
    path::Path,
};

use crossterm::event::{read, Event, KeyCode};
use crossterm::execute;
use crossterm::style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor};

use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use passwords::analyzer;
use passwords::scorer;

#[derive(PartialEq)]
enum WeakPasswordChoice {
    ABORT,
    CONTINUE,
}

#[derive(Parser)]
struct Cli {
    #[command(subcommand)]
    cmd: Commands,
}

#[derive(Subcommand)]
enum Commands {
    List,
    Add {
        #[arg(short, long)]
        service: String,
        #[arg(short, long)]
        username: String,
        #[arg(short, long, default_missing_value("true"))]
        clipboard: bool, // copy from clipboard
        #[arg(short, long, default_missing_value("true"))]
        generate: bool, // generate new password
        #[arg(short, long, default_missing_value("true"))]
        write: bool, // type new password
    },
}

fn main() -> std::io::Result<()> {
    let args = Cli::parse();
    match args.cmd {
        Commands::List => display_passwords()?,
        Commands::Add {
            service,
            username,
            clipboard,
            generate,
            write,
        } => {
            if generate {
                let password = generate_password();
                println!("{}", password);
                let _ = add_new_password(&service, &username, &password);
            }

            if clipboard {
                let password = get_clipboard_password();
                println!("{}", password);
                if should_save_password(&password) {
                    let _ = add_new_password(&service, &username, &password);
                }
            }

            if write {
                let password = get_user_input().unwrap();
                if should_save_password(&password) {
                    let _ = add_new_password(&service, &username, &password);
                }
            }
        }
    }
    Ok(())
}

fn get_user_input() -> io::Result<String> {
    println!("Write it down the press Enter!");
    let mut input = String::new();
    match io::stdin().read_line(&mut input) {
        Ok(n) => return Ok(input.trim().to_string()),
        Err(error) => Err(error),
    }
}

fn get_clipboard_password() -> String {
    let mut ctx = ClipboardContext::new().unwrap();
    return ctx.get_contents().unwrap();
}

fn generate_password() -> String {
    let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
                           abcdefghijklmnopqrstuvwxyz\
                           0123456789\
                           !@#$%^&*()_-+=[{]}\\;:'\",<.>/?";
    let mut rng = thread_rng();
    let password: String = iter::repeat_with(|| {
        let idx = rng.sample(Uniform::from(0..charset.len()));
        charset[idx] as char
    })
    .take(16)
    .collect();
    password
}

fn should_save_password(password: &str) -> bool {
    if is_password_weak(password) {
        print_alert(password);
        if let Ok(choice) = read_next_char() {
            return choice == WeakPasswordChoice::CONTINUE;
        } else {
            return false;
        }
    } else {
        return true;
    }
}

fn print_alert(password: &str) {
    let alert = format!(
        "{} is a weak password. Press Enter to continue anyway. Press Q to abort and try again!",
        password
    );
    execute!(
        io::stdout(),
        SetForegroundColor(Color::Red),
        SetAttribute(Attribute::Bold),
        Print(alert),
        ResetColor,
        SetAttribute(Attribute::Reset)
    )
    .unwrap();
}

fn is_password_weak(password: &str) -> bool {
    let score = scorer::score(&analyzer::analyze(password));
    return score < 80.0;
}

fn read_next_char() -> io::Result<WeakPasswordChoice> {
    enable_raw_mode()?;
    let result = loop {
        match read()? {
            Event::Key(event) => match event.code {
                KeyCode::Char('q') | KeyCode::Char('Q') => {
                    break Ok(WeakPasswordChoice::ABORT);
                }
                KeyCode::Enter => {
                    break Ok(WeakPasswordChoice::CONTINUE);
                }
                _ => {}
            },
            _ => {}
        }
    };

    disable_raw_mode()?;
    result
}

fn display_passwords() -> std::io::Result<()> {
    let path = Path::new("./passwords.txt");
    let contents = fs::read_to_string(path).expect("Could not read the passwords file");

    println!("{}", contents);
    Ok(())
}

fn add_new_password(service: &str, username: &str, password: &str) -> std::io::Result<()> {
    let path = Path::new("./passwords.txt");
    let password_infos = format!("{}|{}|{}\n", service, username, password);

    let mut file = OpenOptions::new().append(true).open(path)?;

    file.write_all(password_infos.as_bytes())?;

    Ok(())
}

```


Let's make sure everything works as it should:

![Add Password Generate](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h2la2yiamozmtz6y774g.png)


![Add Password Clipboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i8kxe5cstu50zokmquwl.png)


![Add Password Write In](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ut53h1ox6qn1ac4h4tk4.png)

And check the list in our file:

![Password list display](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d9jzf8lj3mtsyi4b7tvc.png)

Yayyy! 

## Conclusion

In this article, we added quite a lot of functionality for our little application. We can now add passwords in three different ways and alert our user if their password is too weak. As always, feel free to tell me if some of this code could be improved, or if you would like to see some functionalities for this password manager. The full code can be found on [github](https://github.com/Tagada85/rust_password_manager)

Have fun :heart:!