DEV Community

Cover image for Build a password manager with Rust - Part 1
Damien Cosset
Damien Cosset

Posted on • Originally published at damiencosset.dev

Build a password manager with Rust - Part 1

Introduction

To learn a new language, I find it quite interesting to build something. Having simple features at first to get the hang of the language, then adding more stuff to it, making it more complex, faster, cleaner...

So this is what we'll try to do together in this series. We will build a password manager in Rust. The first article will start with simple features. By the end, we'll have a command line application with:

  • a way to display our passwords
  • a way to save a new password

No encryption, password generations, user interfaces... These will come later. Ok, let's go!

First step: The command line

For this project, we will use the Clap crate to interact easily with the command line. We already used it in this article.

So, let's recap what we want from our command line arguments:

  • an argument to list our existing passwords
  • an argument to save a new password

Here's how we can do it:

use clap::{Parser, Subcommand};

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

#[derive(Subcommand)]
enum Commands {
    List,
    Add {
        service: String,
        username: String,
        password: String,
    },
}
Enter fullscreen mode Exit fullscreen mode

What's going on here? Using Clap, we define a Cli structure with a cmd. This can take two values, List or Add (in our enum Commands). Note that the #[derive(...)] syntax tells Rust that we want our enum or struc to implement certains traits. In our case, we want our enum Commands to implement the Subcommand trait from Clap.

Our List subcommand doesn't take any arguments. But our Add subcommand will take three, the name of the service, a username and a password.

Great! Let's add some code in our main function to see how we interact with all of this:

use clap::{Parser, Subcommand};

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

#[derive(Subcommand)]
enum Commands {
    List,
    Add {
        service: String,
        username: String,
        password: String,
    },
}

fn main() -> std::io::Result<()> {
    let args = Cli::parse();
    match args.cmd {
        Commands::List => display_passwords(),
        Commands::Add {
            service,
            username,
            password,
        } => add_new_password(service, username, password),
    }
    Ok(())
}

fn display_passwords() {
    println!("Will display passwords")
}

fn add_new_password(service: String, username: String, password: String) {
    println!("Will add new password.");
    println!("Service:{}", service);
    println!("Username:{}", username);
    println!("Password:{}", password);
}
Enter fullscreen mode Exit fullscreen mode

We add a match in the main function to handle the different commands we can handle. We add two functions that will only print out something for now. Let's run it!

Cargo run list:
Cargo run list displaying our text

Cargo run add command:
Cargo run add with three arguments displaying all three arguments properly

What happens if we run the add command without the correct number of arguments?

Cargo run add without all arguments displaying an error

It's another nice thing about Clap, it gives us helpful information about the arguments we can use. It also gives us the help command by default:

Cargo run help displaying the informations about the command line application

Step 2: Having a way to store our passwords

So, this works great, but our functions don't do anything yet. We need to decide how we are going to store our passwords. For now, we are simply going to have a .txt file. So, at the root of our project, we are going to add a passwords.txt file. We'll define some rules for our very simple implementation:

  • each password will have its own line in the file
  • on each line, we'll separate the service, username and password with a special character

This is obviously far from being perfect, but it will do the job in our quest to learn the language. Let's do it!

Our passwords.txt will have some entries already for testing purposes:

dev.to|Damien|my dev password
twitter|Bobby|123Awesome456

Enter fullscreen mode Exit fullscreen mode

Our List command will read from this file and display its contents. Let's update our display_passwords function:

//All our imports
use clap::{Parser, Subcommand};
use std::{
    fs::{self},
    path::Path,
};

// Rest of the code ...

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

    println!("{}", contents)
}
Enter fullscreen mode Exit fullscreen mode

We create a new Path with our relative file path and we pass it to the read_to_string function provided by the fs module.
Note: If you put the txt file in the src folder, your relative path will be ./src/passwords.txt.

The .expect() will print out the message we provided if we encounter an error. Let's check the results:

Cargo run list with the passwords.txt file contents displayed

Nice! On to the next step.

Step 3: Adding a new password to our file

We're almost there! Now, we need to implement our add_new_password function.

Step 4:

  • Writing to the new file
fn add_new_password(service: String, username: String, password: String) -> 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(())
}
Enter fullscreen mode Exit fullscreen mode

After creating our Path, we use format! to format our String we want to append to our passwords.txt file. Then, we use OpenOptions. Provided by the fs module, it defines how we want to open a file. In our case, we want to append data to the contents, so we use append(true). Notice that we use the mut keyword, to tell Rust that this variable is mutable.

Then, we use the write_all function to append our password informations. Careful, the write_all function expects a u8 primitive type for its argument. That's why we use as_bytes().

Finally, we return Ok(()).

And we should be done! Our full code looks like this:

use clap::{Parser, Subcommand};
use std::io::Write;
use std::{
    fs::{self, OpenOptions},
    path::Path,
};

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

#[derive(Subcommand)]
enum Commands {
    List,
    Add {
        service: String,
        username: String,
        password: String,
    },
}

fn main() -> std::io::Result<()> {
    let args = Cli::parse();
    match args.cmd {
        Commands::List => display_passwords(),
        Commands::Add {
            service,
            username,
            password,
        } => add_new_password(service, username, password)?,
    }
    Ok(())
}

fn display_passwords() {
    let path = Path::new(".passwords.txt");
    let contents = fs::read_to_string(path).expect("Could not read the passwords file");

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

fn add_new_password(service: String, username: String, password: String) -> 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(())
}
Enter fullscreen mode Exit fullscreen mode

Time to run this and test it out!

Adding a new password:

Cargo run add with three arguments, with the username having a space in the middle

Listing our passwords list:

Cargo run list with our new password we just created

Yay! It works! Congratulations!

In this article, we explored a bit deeper the Clap crate to create subcommands. Then, we learned how to interact with files thanks to the fs module. In the next articles, we'll keep making this little project better. You can find the code here

Have fun ❤️

Top comments (2)

Collapse
 
vladignatyev profile image
Vladimir Ignatev • Edited

Congrats on your progress in Rust!

By the way, running your program via cargo isn't much fun. Why not just run a compiled version directly from CLI? It's in /target/release or similar folder and have the name of your project from .toml file.

Also, I would recommend to use serialization using the serde instead of parsing file manually. By the way, you use CSV file format with | as a delimiter. Rust has an awesome package called csv for saving and loading data from this file format.

I'm thrilling to read your next post on Rust! Keep up doing!

Collapse
 
damcosset profile image
Damien Cosset

Thank you for the tips and the kind words! I'll make sure to use your suggestions in the next articles to keep improving the code. ❤️