├── .gitignore ├── src ├── edit │ ├── args.rs │ └── mod.rs ├── remove │ ├── args.rs │ └── mod.rs ├── list │ ├── mod.rs │ └── args.rs ├── show │ ├── args.rs │ └── mod.rs ├── otp │ ├── args.rs │ ├── mod.rs │ ├── set.rs │ ├── show.rs │ └── generator.rs ├── clipboard.rs ├── init │ ├── args.rs │ └── mod.rs ├── pwgen │ ├── args.rs │ ├── mod.rs │ └── generator.rs ├── crypto │ ├── plain.rs │ ├── mod.rs │ └── gpg.rs ├── create │ ├── templates │ │ ├── 0-websites.toml │ │ └── 1-pin.toml │ └── mod.rs ├── main.rs ├── args.rs └── storage │ ├── fs.rs │ ├── mod.rs │ └── tree.rs ├── Cargo.toml ├── .github └── workflows │ └── release.yml ├── README.md ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/edit/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | pub struct Args { 5 | #[arg(index = 1)] 6 | pub item: String, 7 | } 8 | -------------------------------------------------------------------------------- /src/remove/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | pub struct Args { 5 | #[arg(index = 1)] 6 | pub name: String, 7 | } 8 | -------------------------------------------------------------------------------- /src/list/mod.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | 3 | pub use args::Args; 4 | 5 | pub fn main(args: Args) { 6 | crate::storage::get().tree(args.flat, args.no_color); 7 | } 8 | -------------------------------------------------------------------------------- /src/remove/mod.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | 3 | use crate::storage; 4 | pub use args::Args; 5 | 6 | pub fn main(args: Args) { 7 | storage::get().remove(args.name); 8 | } 9 | -------------------------------------------------------------------------------- /src/show/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | pub struct Args { 5 | #[arg(short, long, default_value = "false")] 6 | pub clip: bool, 7 | 8 | #[arg(index = 1)] 9 | pub item: String, 10 | } 11 | -------------------------------------------------------------------------------- /src/list/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | pub struct Args { 5 | #[arg(long, short, default_value = "false")] 6 | pub flat: bool, 7 | 8 | #[arg(long, short, default_value = "false")] 9 | pub no_color: bool, 10 | } 11 | -------------------------------------------------------------------------------- /src/otp/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser)] 4 | pub struct Args { 5 | #[command(subcommand)] 6 | pub command: Cmd, 7 | } 8 | 9 | #[derive(Subcommand)] 10 | pub enum Cmd { 11 | Set(super::set::Args), 12 | Show(super::show::Args), 13 | } 14 | -------------------------------------------------------------------------------- /src/otp/mod.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod generator; 3 | mod set; 4 | mod show; 5 | 6 | pub use args::{Args, Cmd}; 7 | 8 | pub fn main(args: Args) { 9 | match args.command { 10 | Cmd::Set(options) => set::main(options), 11 | Cmd::Show(options) => show::main(options), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/clipboard.rs: -------------------------------------------------------------------------------- 1 | use cli_clipboard::{ClipboardContext, ClipboardProvider}; 2 | 3 | /// Copy the given content to the clipboard 4 | /// 5 | /// * `content`: 6 | pub fn copy(content: &str) { 7 | ClipboardContext::new() 8 | .expect("Failed to create clipboard context") 9 | .set_contents(content.to_string()) 10 | .expect("Failed to set clipboard contents"); 11 | } 12 | -------------------------------------------------------------------------------- /src/init/args.rs: -------------------------------------------------------------------------------- 1 | use crate::crypto::Backend as CryptoBackend; 2 | use crate::storage::Backend as StorageBackend; 3 | use clap::Parser; 4 | 5 | #[derive(Parser)] 6 | pub struct Args { 7 | #[arg(short, long, value_enum, default_value = "gpg")] 8 | pub crypto: CryptoBackend, 9 | 10 | #[arg(short, long, value_enum, default_value = "fs")] 11 | pub storage: StorageBackend, 12 | } 13 | -------------------------------------------------------------------------------- /src/pwgen/args.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Parser)] 4 | pub struct Args { 5 | /// Desired length of the password 6 | #[arg(index = 1, default_value = "16")] 7 | pub length: usize, 8 | 9 | /// Include digits 10 | #[arg(short, long, default_value = "false")] 11 | pub digits: bool, 12 | 13 | /// Include special characters 14 | #[arg(short, long, default_value = "false")] 15 | pub symbols: bool, 16 | } 17 | -------------------------------------------------------------------------------- /src/crypto/plain.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | /// For plain, there is nothing extra to do. 4 | pub fn init() { 5 | let root = crate::args::root(); 6 | let crypto_config = root.join(".pass").join("crypto"); 7 | fs::write(crypto_config, "plain").unwrap(); 8 | } 9 | 10 | pub fn decrypt(content: Vec) -> String { 11 | String::from_utf8(content).unwrap() 12 | } 13 | 14 | pub fn encrypt(content: String) -> Vec { 15 | content.as_bytes().to_vec() 16 | } 17 | -------------------------------------------------------------------------------- /src/create/templates/0-websites.toml: -------------------------------------------------------------------------------- 1 | priority = 0 2 | name = "Website login" 3 | prefix = "websites" 4 | name_from = ["url", "username"] 5 | welcome = "Creating Website login" 6 | 7 | [[attributes]] 8 | name = "url" 9 | prompt = "Website URL" 10 | prompt_type = "string" 11 | 12 | [[attributes]] 13 | name = "username" 14 | prompt = "Username" 15 | prompt_type = "string" 16 | 17 | [[attributes]] 18 | name = "password" 19 | prompt = "Password for the Website" 20 | prompt_type = "password" 21 | -------------------------------------------------------------------------------- /src/create/templates/1-pin.toml: -------------------------------------------------------------------------------- 1 | priority = 1 2 | name = "PIN Code (numerical)" 3 | prefix = "pin" 4 | name_from = ["authority", "application"] 5 | welcome = "Creating numerical PIN" 6 | 7 | [[attributes]] 8 | name = "authority" 9 | prompt = "Authority" 10 | prompt_type = "string" 11 | 12 | [[attributes]] 13 | name = "application" 14 | prompt = "Entity" 15 | prompt_type = "string" 16 | 17 | [[attributes]] 18 | name = "password" 19 | prompt = "Pin" 20 | prompt_type = "password" 21 | 22 | [[attributes]] 23 | name = "comment" 24 | prompt = "Comment" 25 | prompt_type = "string" 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pass-rs" 3 | description = "Simple password manager, written in Rust" 4 | version = "0.2.0" 5 | edition = "2021" 6 | authors = ["Sabu Siyad", "Sabu Siyad "] 7 | 8 | [dependencies] 9 | base32 = "0.4.0" 10 | clap = { version = "4.5.3", features = ["derive", "cargo"] } 11 | cli-clipboard = "0.4.0" 12 | crossterm = "0.27.0" 13 | gpgme = "0.11.0" 14 | hmac = "0.12.1" 15 | ignore = "0.4.22" 16 | inquire = "0.7.2" 17 | rand = "0.8.5" 18 | serde = { version = "1.0.197", features = ["serde_derive"] } 19 | sha1 = "0.10.6" 20 | tempfile = "3.10.1" 21 | toml = "0.8.12" 22 | -------------------------------------------------------------------------------- /src/init/mod.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | 3 | pub use args::Args; 4 | use std::fs; 5 | 6 | pub fn main(args: Args) { 7 | // Get the root directory. 8 | let root = super::args::root(); 9 | 10 | // Create root directory if it doesn't exist. 11 | if !root.exists() { 12 | fs::create_dir(&root).unwrap(); 13 | } 14 | 15 | // Get config directory. 16 | let root_config = root.join(".pass"); 17 | 18 | // Create config directory if it doesn't exist. 19 | if !root_config.exists() { 20 | fs::create_dir(&root_config).unwrap(); 21 | } 22 | 23 | // Initialize the crypto backend 24 | args.crypto.init(); 25 | 26 | // Initialize the storage backend 27 | args.storage.init(); 28 | } 29 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod clipboard; 3 | mod create; 4 | mod crypto; 5 | mod edit; 6 | mod init; 7 | mod list; 8 | mod otp; 9 | mod pwgen; 10 | mod remove; 11 | mod show; 12 | mod storage; 13 | 14 | use args::{Args, Cmd}; 15 | use clap::Parser; 16 | 17 | pub fn main() { 18 | let args = Args::parse(); 19 | 20 | match args.command { 21 | Cmd::Create => create::main(), 22 | Cmd::Edit(options) => edit::main(options), 23 | Cmd::Init(options) => init::main(options), 24 | Cmd::List(options) => list::main(options), 25 | Cmd::Otp(options) => otp::main(options), 26 | Cmd::Pwgen(options) => pwgen::main(options), 27 | Cmd::Remove(options) => remove::main(options), 28 | Cmd::Show(options) => show::main(options), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/otp/set.rs: -------------------------------------------------------------------------------- 1 | use crate::crypto; 2 | use clap::Parser; 3 | use std::fs; 4 | 5 | #[derive(Parser)] 6 | pub struct Args { 7 | #[arg(index = 1)] 8 | name: String, 9 | 10 | #[arg(index = 2)] 11 | code: String, 12 | } 13 | 14 | pub fn main(args: Args) { 15 | let path = crate::args::root().join(args.name); 16 | let content_raw = fs::read(&path).expect("Failed to read file"); 17 | let crypto_backend = crypto::get(); 18 | let content = crypto_backend.decrypt(content_raw); 19 | let mut content_lines = content 20 | .lines() 21 | .filter(|x| !x.starts_with("totp: ")) 22 | .collect::>(); 23 | let totp_line = format!("totp: {}", args.code); 24 | content_lines.push(&totp_line); 25 | let content_updated = content_lines.join("\n"); 26 | let content_encrypted = crypto_backend.encrypt(content_updated); 27 | fs::write(path, content_encrypted).expect("Failed to write file"); 28 | } 29 | -------------------------------------------------------------------------------- /src/pwgen/mod.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod generator; 3 | 4 | pub use args::Args; 5 | use crossterm::terminal; 6 | pub use generator::Generator; 7 | 8 | pub fn main(args: Args) { 9 | let mut generator = Generator::new().with_lowercase().with_uppercase(); 10 | 11 | if args.digits { 12 | generator = generator.with_digits(); 13 | } 14 | 15 | if args.symbols { 16 | generator = generator.with_special(); 17 | } 18 | 19 | generator = generator.prepare(); 20 | 21 | let (terminal_width, _) = terminal::size().unwrap_or((args.length as u16, 0)); 22 | let per_line = terminal_width / (args.length as u16 + 1); 23 | let entries = per_line * 10; 24 | let mut counter = 0; 25 | 26 | for _ in 0..entries { 27 | print!("{} ", generator.generate(args.length)); 28 | counter += 1; 29 | 30 | if counter == per_line { 31 | println!(); 32 | counter = 0; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | release: 7 | name: release ${{ matrix.target }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - target: x86_64-unknown-linux-musl 14 | archive: tar.gz tar.xz tar.zst 15 | - target: x86_64-apple-darwin 16 | archive: zip 17 | steps: 18 | - uses: actions/checkout@master 19 | - name: Install dependencies 20 | run: | 21 | sudo apt-get -y update 22 | sudo apt-get install -y libgpgme-dev 23 | sudo apt-get install -y libgpg-error-dev 24 | - name: Compile and release 25 | uses: rust-build/rust-build.action@v1.4.5 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | with: 29 | STATIC_LINKING: false 30 | RUSTTARGET: ${{ matrix.target }} 31 | ARCHIVE_TYPES: ${{ matrix.archive }} 32 | -------------------------------------------------------------------------------- /src/show/mod.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | 3 | use crate::crypto; 4 | use crate::storage; 5 | pub use args::Args; 6 | 7 | pub fn main(args: Args) { 8 | let content_raw = storage::get().read(args.item); 9 | let content = crypto::get().decrypt(content_raw); 10 | 11 | if args.clip { 12 | let password = extract_password(&content); 13 | crate::clipboard::copy(password); 14 | } else { 15 | println!("{}", content); 16 | } 17 | } 18 | 19 | fn extract_password(content: &str) -> &str { 20 | let mut password = ""; 21 | for line in content.lines() { 22 | // If the line contains a space, it's not a password. This logic 23 | // might not be right. 24 | if !line.contains(' ') { 25 | password = line; 26 | break; 27 | } 28 | 29 | // If the line starts with `Password: `, then it's a password. 30 | if line.starts_with("password: ") { 31 | password = line.trim_start_matches("password: "); 32 | break; 33 | } 34 | } 35 | password 36 | } 37 | -------------------------------------------------------------------------------- /src/edit/mod.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | 3 | use crate::crypto; 4 | use crate::storage; 5 | pub use args::Args; 6 | use std::env; 7 | use std::error::Error; 8 | use std::fs::File; 9 | use std::io::prelude::*; 10 | use std::process::Command; 11 | use tempfile::NamedTempFile; 12 | 13 | pub fn main(args: Args) { 14 | let crypto_backend = crypto::get(); 15 | let storage_backend = storage::get(); 16 | let content_raw = storage_backend.read(args.item.clone()); 17 | let content = crypto_backend.decrypt(content_raw); 18 | let edited_content = editor(content).expect("Failed to edit"); 19 | let content_encrypted = crypto_backend.encrypt(edited_content); 20 | storage_backend.write(args.item, content_encrypted); 21 | } 22 | 23 | fn editor(content: String) -> Result> { 24 | let mut temp_file = NamedTempFile::new()?; 25 | temp_file.write_all(content.as_bytes())?; 26 | Command::new(get_editor()).arg(temp_file.path()).status()?; 27 | let mut edited_file = File::open(temp_file.path())?; 28 | let mut edited_output = String::new(); 29 | edited_file.read_to_string(&mut edited_output)?; 30 | Ok(edited_output) 31 | } 32 | 33 | fn get_editor() -> String { 34 | env::var("EDITOR").unwrap_or("vim".to_string()) 35 | } 36 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | use clap::{Parser, Subcommand}; 5 | 6 | #[derive(Parser)] 7 | #[command(about, version, arg_required_else_help(true))] 8 | pub struct Args { 9 | #[command(subcommand)] 10 | pub command: Cmd, 11 | } 12 | 13 | #[derive(Subcommand)] 14 | pub enum Cmd { 15 | /// Create a new password entry 16 | #[clap(alias = "new")] 17 | Create, 18 | 19 | /// Edit an existing password entry 20 | Edit(crate::edit::Args), 21 | 22 | /// Setup the password store 23 | Init(crate::init::Args), 24 | 25 | /// List all password entries 26 | #[clap(alias = "ls")] 27 | List(crate::list::Args), 28 | 29 | /// Manage time based one time passwords 30 | Otp(crate::otp::Args), 31 | 32 | /// Generate a new password 33 | #[clap(alias = "generate")] 34 | Pwgen(crate::pwgen::Args), 35 | 36 | /// Remove an existing password entry 37 | #[clap(alias = "rm")] 38 | Remove(crate::remove::Args), 39 | 40 | /// Show an existing password entry 41 | Show(crate::show::Args), 42 | } 43 | 44 | /// Returns the root directory for the password store. This will be 45 | /// `~/.pass-rs` by default. 46 | pub fn root() -> PathBuf { 47 | env::home_dir() 48 | .expect("Unable to locate home directory") 49 | .join(".pass-rs") 50 | } 51 | -------------------------------------------------------------------------------- /src/storage/fs.rs: -------------------------------------------------------------------------------- 1 | use super::tree::Node; 2 | use ignore::Walk; 3 | use std::{fs, path::PathBuf}; 4 | 5 | pub fn init() { 6 | let root = crate::args::root(); 7 | let crypto_config = root.join(".pass").join("storage"); 8 | fs::write(crypto_config, "fs").unwrap(); 9 | } 10 | 11 | fn path(name: String) -> PathBuf { 12 | crate::args::root().join(name) 13 | } 14 | 15 | pub fn read(name: String) -> Vec { 16 | fs::read(path(name)).unwrap() 17 | } 18 | 19 | pub fn write(name: String, content: Vec) { 20 | let effective_path = path(name); 21 | fs::create_dir_all(effective_path.parent().unwrap()).unwrap(); 22 | fs::write(effective_path, content).unwrap(); 23 | } 24 | 25 | pub fn remove(name: String) { 26 | fs::remove_file(path(name)).unwrap(); 27 | } 28 | 29 | pub fn tree(flat: bool, no_color: bool) { 30 | let root = crate::args::root(); 31 | let walker = Walk::new(&root); 32 | 33 | for n in walker.skip(1).filter_map(|x| x.ok()) { 34 | let path = n.path(); 35 | Node::new( 36 | path.strip_prefix(&root) 37 | .unwrap() 38 | .components() 39 | .map(|x| x.as_os_str().to_string_lossy().to_string()) 40 | .collect::>(), 41 | path.is_dir(), 42 | ) 43 | .print(flat, !no_color) 44 | .unwrap(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pass-rs` 2 | Simple password manager, written in Rust. 3 | 4 | --- 5 | ## Gallery 6 | [![asciicast](https://asciinema.org/a/6Q7uBJ0tVI6QCvQaGC2IMFtea.svg)](https://asciinema.org/a/6Q7uBJ0tVI6QCvQaGC2IMFtea) 7 | [![asciicast](https://asciinema.org/a/v1absRM8jZPAzTphShQYTpxKE.svg)](https://asciinema.org/a/v1absRM8jZPAzTphShQYTpxKE) 8 | [![asciicast](https://asciinema.org/a/TOn8nG0YJtO9FVDWVXuKIUUdY.svg)](https://asciinema.org/a/TOn8nG0YJtO9FVDWVXuKIUUdY) 9 | 10 | ## Build From Source 11 | ### Prerequisites 12 | For building `pass-rs` from source, you need to have these tools installed: 13 | 14 | * [Rust](https://rust-lang.org/tools/install) 15 | * Cargo (Automatically installed when installing Rust) 16 | * [GPG](https://gnupg.org/download/index.html) 17 | * [GPGME](https://gnupg.org/software/gpgme/index.html) 18 | 19 | ``` 20 | $ git clone https://github.com/ssiyad/pass-rs.git 21 | $ cd pass-rs/ 22 | $ cargo build --release 23 | ``` 24 | 25 | ## Contribution 26 | Ways to contribute: 27 | 28 | - Suggest a feature 29 | - Report a bug 30 | - Fix something and open a pull request 31 | - Help me document the code 32 | - Spread the word 33 | 34 | ## License 35 | Licensed under the GPLv3 License, see [LICENSE](/LICENSE) for more information. 36 | 37 | ## Liked the project? 38 | If you liked the project and found it useful, please give it a :star: and 39 | consider supporting the author! 40 | -------------------------------------------------------------------------------- /src/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | mod gpg; 2 | mod plain; 3 | 4 | use clap::ValueEnum; 5 | use std::fs; 6 | 7 | #[derive(Clone, ValueEnum)] 8 | pub enum Backend { 9 | Gpg, 10 | Plain, 11 | } 12 | 13 | impl Backend { 14 | /// Initialization steps for the crypto backend 15 | pub fn init(&self) { 16 | match self { 17 | Backend::Gpg => gpg::init(), 18 | Backend::Plain => plain::init(), 19 | } 20 | } 21 | 22 | /// Decrypt the content 23 | /// 24 | /// * `content`: 25 | pub fn decrypt(&self, content: Vec) -> String { 26 | match self { 27 | Backend::Gpg => gpg::decrypt(content), 28 | Backend::Plain => plain::decrypt(content), 29 | } 30 | } 31 | 32 | /// Encrypt the content 33 | /// 34 | /// * `content`: 35 | pub fn encrypt(&self, content: String) -> Vec { 36 | match self { 37 | Backend::Gpg => gpg::encrypt(content), 38 | Backend::Plain => plain::encrypt(content), 39 | } 40 | } 41 | } 42 | 43 | /// Get the crypto backend 44 | pub fn get() -> Backend { 45 | let root = crate::args::root(); 46 | let crypto_config = root.join(".pass").join("crypto"); 47 | let crypto_type = fs::read_to_string(crypto_config).unwrap().trim().to_owned(); 48 | 49 | match crypto_type.as_str() { 50 | "gpg" => Backend::Gpg, 51 | "plain" => Backend::Plain, 52 | _ => panic!("No crypto setup"), 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/pwgen/generator.rs: -------------------------------------------------------------------------------- 1 | use rand::distributions::Uniform; 2 | use rand::rngs::StdRng; 3 | use rand::{Rng, SeedableRng}; 4 | 5 | pub struct Generator { 6 | chars: Vec, 7 | distribution: Uniform, 8 | rng: StdRng, 9 | } 10 | 11 | impl Generator { 12 | pub fn new() -> Generator { 13 | Generator { 14 | chars: vec![], 15 | distribution: Uniform::new(0, 1), 16 | rng: StdRng::from_entropy(), 17 | } 18 | } 19 | 20 | pub fn with_lowercase(mut self) -> Self { 21 | for c in b'a'..=b'z' { 22 | self.chars.push(c as char); 23 | } 24 | self 25 | } 26 | 27 | pub fn with_uppercase(mut self) -> Self { 28 | for c in b'A'..=b'Z' { 29 | self.chars.push(c as char); 30 | } 31 | self 32 | } 33 | 34 | pub fn with_digits(mut self) -> Self { 35 | for c in b'0'..=b'9' { 36 | self.chars.push(c as char); 37 | } 38 | self 39 | } 40 | 41 | pub fn with_special(mut self) -> Self { 42 | for c in b'!'..=b'/' { 43 | self.chars.push(c as char); 44 | } 45 | self 46 | } 47 | 48 | pub fn prepare(mut self) -> Self { 49 | self.distribution = Uniform::new(0, self.chars.len()); 50 | self 51 | } 52 | 53 | pub fn generate(&mut self, length: usize) -> String { 54 | let mut res = String::new(); 55 | for _ in 0..length { 56 | let idx = self.rng.sample(self.distribution); 57 | res.push(self.chars[idx]); 58 | } 59 | res 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | mod fs; 2 | mod tree; 3 | 4 | use clap::ValueEnum; 5 | 6 | #[derive(Clone, ValueEnum)] 7 | pub enum Backend { 8 | Fs, 9 | } 10 | 11 | impl Backend { 12 | /// Initialization steps for the storage backend 13 | pub fn init(&self) { 14 | match self { 15 | Backend::Fs => fs::init(), 16 | } 17 | } 18 | 19 | /// Read the content 20 | /// 21 | /// * `name`: 22 | pub fn read(&self, name: String) -> Vec { 23 | match self { 24 | Backend::Fs => fs::read(name), 25 | } 26 | } 27 | 28 | /// Write the content 29 | /// 30 | /// * `name`: 31 | /// * `content`: 32 | pub fn write(&self, name: String, content: Vec) { 33 | match self { 34 | Backend::Fs => fs::write(name, content), 35 | } 36 | } 37 | 38 | /// Remove secret 39 | /// 40 | /// * `name`: 41 | pub fn remove(&self, name: String) { 42 | match self { 43 | Backend::Fs => fs::remove(name), 44 | } 45 | } 46 | 47 | /// List the secrets 48 | /// 49 | /// * `flat`: 50 | /// * `color`: 51 | pub fn tree(&self, flat: bool, no_color: bool) { 52 | match self { 53 | Backend::Fs => fs::tree(flat, no_color), 54 | } 55 | } 56 | } 57 | 58 | /// Get the crypto backend 59 | pub fn get() -> Backend { 60 | let root = crate::args::root(); 61 | let storage_config = root.join(".pass").join("storage"); 62 | let storage_type = std::fs::read_to_string(storage_config) 63 | .unwrap() 64 | .trim() 65 | .to_owned(); 66 | 67 | match storage_type.as_str() { 68 | "fs" => Backend::Fs, 69 | _ => panic!("No storage setup"), 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/otp/show.rs: -------------------------------------------------------------------------------- 1 | use super::generator::Generator; 2 | use crate::crypto; 3 | use clap::Parser; 4 | use crossterm::{ 5 | cursor::MoveUp, 6 | execute, 7 | style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor}, 8 | terminal::{Clear, ClearType}, 9 | }; 10 | use std::fs; 11 | use std::io; 12 | use std::thread; 13 | use std::time::Duration; 14 | 15 | #[derive(Parser)] 16 | pub struct Args { 17 | #[arg(index = 1)] 18 | pub name: String, 19 | } 20 | 21 | pub fn main(args: Args) { 22 | let path = crate::args::root().join(args.name); 23 | let content_raw = fs::read(path).expect("Could not read file"); 24 | let content = crypto::get().decrypt(content_raw); 25 | let token = content 26 | .lines() 27 | .find(|line| line.starts_with("totp: ")) 28 | .expect("No Otp line found") 29 | .trim_start_matches("totp: ") 30 | .to_string(); 31 | let otp_gen = Generator::new(token); 32 | let mut count = 0; 33 | 34 | loop { 35 | if count > 0 { 36 | execute!(io::stdout(), MoveUp(2), Clear(ClearType::FromCursorDown)).ok(); 37 | } 38 | 39 | let code = otp_gen.generate_current(); 40 | let refresh_in = otp_gen.refresh_current_in(); 41 | 42 | execute!( 43 | io::stdout(), 44 | SetForegroundColor(Color::Blue), 45 | SetAttribute(Attribute::Bold), 46 | Print("Refreshing in: "), 47 | SetAttribute(Attribute::Reset), 48 | SetForegroundColor(Color::Yellow), 49 | Print(refresh_in), 50 | Print('\n'), 51 | SetForegroundColor(Color::Blue), 52 | SetAttribute(Attribute::Bold), 53 | Print("Code: "), 54 | SetAttribute(Attribute::Reset), 55 | SetForegroundColor(Color::Green), 56 | Print(code), 57 | Print('\n'), 58 | SetAttribute(Attribute::Reset), 59 | ResetColor, 60 | ) 61 | .ok(); 62 | 63 | count += 1; 64 | thread::sleep(Duration::from_secs(1)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/crypto/gpg.rs: -------------------------------------------------------------------------------- 1 | use gpgme::{Context, Protocol}; 2 | use inquire::Select; 3 | use std::fs; 4 | 5 | /// Get the GPG context. 6 | fn get_context() -> Context { 7 | Context::from_protocol(Protocol::OpenPgp).unwrap() 8 | } 9 | 10 | /// Get secret keys. 11 | fn get_keys() -> Vec { 12 | let mut context = get_context(); 13 | let keys = context.secret_keys().unwrap(); 14 | keys.filter_map(|x| x.ok()) 15 | .map(|x| x.fingerprint().unwrap().to_owned()) 16 | .collect() 17 | } 18 | 19 | /// Get key used for recipients. 20 | fn get_key() -> String { 21 | let root = crate::args::root(); 22 | let path_key = root.join(".pass").join("gpg-id"); 23 | fs::read_to_string(path_key) 24 | .expect("Failed to read key from file") 25 | .trim() 26 | .to_owned() 27 | } 28 | 29 | pub fn init() { 30 | let root = crate::args::root(); 31 | let crypto_config = root.join(".pass").join("crypto"); 32 | fs::write(crypto_config, "gpg").unwrap(); 33 | let path_key = root.join(".pass").join("gpg-id"); 34 | let keys = get_keys(); 35 | 36 | // Create key info file if it doesn't exist. 37 | if !path_key.exists() { 38 | let key; 39 | 40 | // If no keys are present, ask to create one. 41 | if keys.is_empty() { 42 | panic!("No keys found. Please create one."); 43 | } 44 | // If only one key is present, use it. 45 | else if keys.len() == 1 { 46 | key = keys.clone().pop().unwrap(); 47 | } 48 | // If more than one key is present, ask for one. 49 | else { 50 | key = Select::new("Key", keys).prompt().unwrap(); 51 | } 52 | 53 | // Write the key to the file. 54 | fs::write(path_key, key).expect("Failed to write key to file"); 55 | } 56 | } 57 | 58 | pub fn decrypt(content: Vec) -> String { 59 | let mut output = Vec::new(); 60 | get_context().decrypt(content, &mut output).unwrap(); 61 | String::from_utf8(output).unwrap() 62 | } 63 | 64 | pub fn encrypt(content: String) -> Vec { 65 | let key_str = get_key(); 66 | let mut context = get_context(); 67 | let mut output = Vec::new(); 68 | let key = context.get_key(key_str).unwrap(); 69 | let recp = vec![&key]; 70 | context 71 | .encrypt(recp, content.as_bytes(), &mut output) 72 | .unwrap(); 73 | output 74 | } 75 | -------------------------------------------------------------------------------- /src/storage/tree.rs: -------------------------------------------------------------------------------- 1 | use crossterm::{ 2 | execute, 3 | style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor}, 4 | }; 5 | use std::io; 6 | 7 | /// A node in the tree. It contains the name of the file or directory, the depth of the node in the 8 | /// tree, and a flag indicating whether the node is a directory or not. The `print` method is used 9 | /// to print the node to the terminal. 10 | /// 11 | /// * `name`: 12 | /// * `depth`: 13 | /// * `is_dir`: 14 | pub struct Node { 15 | components: Vec, 16 | is_dir: bool, 17 | } 18 | 19 | impl Node { 20 | /// Create a new node with the given name, depth, and directory flag. 21 | /// 22 | /// * `name`: 23 | /// * `depth`: 24 | /// * `is_dir`: 25 | pub fn new(components: Vec, is_dir: bool) -> Node { 26 | Node { components, is_dir } 27 | } 28 | 29 | /// Print the node to the terminal. If `with_style` is `true`, the node will be printed with 30 | /// colors and styles. Otherwise, the node will be printed without any colors or styles. 31 | /// 32 | /// * `with_style`: 33 | pub fn print(&self, flat: bool, color: bool) -> Result<(), io::Error> { 34 | // Skip directories if flat is enabled 35 | if flat && self.is_dir { 36 | return Ok(()); 37 | } 38 | 39 | // Calculate the depth of the node 40 | let depth = self.components.len() - 1; 41 | 42 | // Print indentation 43 | if !flat { 44 | execute!(io::stdout(), Print(" ".repeat(depth)))?; 45 | } 46 | 47 | for (index, component) in self.components.iter().enumerate() { 48 | if !flat { 49 | if index < depth { 50 | continue; 51 | } 52 | 53 | if self.is_dir { 54 | execute!( 55 | io::stdout(), 56 | Print(match depth { 57 | 0 => "◆", 58 | 1 => "◈", 59 | 2 => "⟐", 60 | _ => "⟡", 61 | }), 62 | Print(" "), 63 | )?; 64 | } else { 65 | execute!(io::stdout(), Print(" "))?; 66 | } 67 | } 68 | 69 | if color && (self.is_dir || index < depth) { 70 | execute!( 71 | io::stdout(), 72 | SetAttribute(Attribute::Bold), 73 | SetForegroundColor(match index { 74 | 0 => Color::Green, 75 | 1 => Color::Cyan, 76 | _ => Color::Blue, 77 | }), 78 | )?; 79 | } 80 | 81 | execute!( 82 | io::stdout(), 83 | Print(component), 84 | ResetColor, 85 | SetAttribute(Attribute::Reset), 86 | )?; 87 | 88 | if flat && index < depth { 89 | execute!(io::stdout(), Print("/"))?; 90 | } 91 | } 92 | 93 | execute!(io::stdout(), Print("\n")) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/otp/generator.rs: -------------------------------------------------------------------------------- 1 | use hmac::{Hmac, Mac}; 2 | use sha1::Sha1; 3 | use std::time::SystemTime; 4 | 5 | pub struct Generator { 6 | secret: String, 7 | size: u8, 8 | step_size: u64, 9 | } 10 | 11 | impl Generator { 12 | /// Create a new Generator. Default size is 6 and step_size is 30. 13 | /// 14 | /// * `secret`: 15 | pub fn new(secret: String) -> Self { 16 | Self { 17 | secret, 18 | size: 6, 19 | step_size: 30, 20 | } 21 | } 22 | 23 | /// Set the size of the code. 24 | /// 25 | /// * `size`: 26 | fn with_size(mut self, size: u8) -> Self { 27 | self.size = size; 28 | self 29 | } 30 | 31 | /// Decode `secret` 32 | fn secret_base32(&self) -> Vec { 33 | base32::decode(base32::Alphabet::RFC4648 { padding: false }, &self.secret) 34 | .expect("Unable to decode secret") 35 | } 36 | 37 | /// Sign `secret` and `data` using HMAC-SHA1. 38 | /// 39 | /// * `data`: 40 | fn sign(&self, data: &[u8]) -> Vec { 41 | let secret = self.secret_base32(); 42 | let mut mac = Hmac::::new_from_slice(&secret).unwrap(); 43 | mac.update(data); 44 | mac.finalize().into_bytes().to_vec() 45 | } 46 | 47 | /// Get the time until the next refresh. 48 | /// 49 | /// * `time`: 50 | fn refresh_in(&self, time: u64) -> u64 { 51 | self.step_size - (time % self.step_size) 52 | } 53 | 54 | /// Get the time until the next refresh using the current time. 55 | pub fn refresh_current_in(&self) -> u64 { 56 | let time = SystemTime::now() 57 | .duration_since(SystemTime::UNIX_EPOCH) 58 | .unwrap() 59 | .as_secs(); 60 | self.refresh_in(time) 61 | } 62 | 63 | /// Generate a code using `time`. 64 | /// 65 | /// * `time`: 66 | fn generate(&self, time: u64) -> usize { 67 | let step = time / self.step_size; 68 | let signed = self.sign(&step.to_be_bytes()); 69 | let offset = (signed[19] & 0xf) as usize; 70 | let truncated = signed[offset..=(offset + 3)].to_vec(); 71 | let code_num = u32::from_be_bytes(truncated.try_into().unwrap()) & 0x7fffffff; 72 | let size_adjused = (code_num as u64) % 10_u64.pow(self.size as u32); 73 | size_adjused as usize 74 | } 75 | 76 | /// Generate a code using the current time. 77 | pub fn generate_current(&self) -> usize { 78 | let time = SystemTime::now() 79 | .duration_since(SystemTime::UNIX_EPOCH) 80 | .unwrap() 81 | .as_secs(); 82 | self.generate(time) 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | mod test { 88 | use super::*; 89 | 90 | #[test] 91 | fn known_value_1() { 92 | let otp_gen = Generator::new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ".to_string()).with_size(8); 93 | let code = otp_gen.generate(59); 94 | assert_eq!(code, 94287082); 95 | } 96 | 97 | #[test] 98 | fn known_value_2() { 99 | let otp_gen = Generator::new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ".to_string()).with_size(8); 100 | let code = otp_gen.generate(1111111109); 101 | assert_eq!(code, 7081804); 102 | } 103 | 104 | #[test] 105 | fn refresh_in_seconds_1() { 106 | let otp_gen = Generator::new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ".to_string()).with_size(8); 107 | let refresh = otp_gen.refresh_in(59); 108 | assert_eq!(refresh, 1); 109 | } 110 | 111 | #[test] 112 | fn refresh_in_seconds_2() { 113 | let otp_gen = Generator::new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ".to_string()).with_size(8); 114 | let refresh = otp_gen.refresh_in(12); 115 | assert_eq!(refresh, 18); 116 | } 117 | 118 | #[test] 119 | fn current_value() { 120 | let otp_gen = Generator::new("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ".to_string()).with_size(8); 121 | let time = SystemTime::now() 122 | .duration_since(SystemTime::UNIX_EPOCH) 123 | .unwrap() 124 | .as_secs(); 125 | let code = otp_gen.generate(time); 126 | let code_current = otp_gen.generate_current(); 127 | assert_eq!(code, code_current); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/create/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::crypto; 2 | use crate::pwgen::Generator as PwGen; 3 | use crate::storage; 4 | use crossterm::{ 5 | execute, 6 | style::{Color, Print, ResetColor, SetForegroundColor}, 7 | }; 8 | use inquire::{validator::Validation, Confirm, Password, Select, Text}; 9 | use serde::Deserialize; 10 | use std::collections::HashMap; 11 | use std::io; 12 | use std::process; 13 | 14 | #[derive(Debug, Deserialize)] 15 | struct Attribute { 16 | name: String, 17 | prompt: String, 18 | prompt_type: String, 19 | } 20 | 21 | impl Attribute { 22 | /// Prompt the user for the attribute. 23 | fn ask(&self) -> String { 24 | match self.prompt_type.as_str() { 25 | "string" => Text::new(&self.prompt).prompt().unwrap(), 26 | "password" => { 27 | if Confirm::new("Generate Password") 28 | .with_default(true) 29 | .prompt() 30 | .unwrap() 31 | { 32 | let mut generator = PwGen::new(); 33 | 34 | // Ask if the user wants to include uppercase. 35 | if Confirm::new("Uppercase") 36 | .with_default(true) 37 | .prompt() 38 | .unwrap() 39 | { 40 | generator = generator.with_uppercase(); 41 | } 42 | 43 | // Ask if the user want to include digits. 44 | if Confirm::new("Digits").with_default(true).prompt().unwrap() { 45 | generator = generator.with_digits(); 46 | } 47 | 48 | // Ask if the user wants to include special chars. 49 | if Confirm::new("Special chars") 50 | .with_default(false) 51 | .prompt() 52 | .unwrap() 53 | { 54 | generator = generator.with_special(); 55 | } 56 | 57 | // Ask for the length of the password. 58 | let length = Text::new("Length") 59 | .with_default("16") 60 | .with_validator(|input: &str| match input.parse::() { 61 | Ok(_) => Ok(Validation::Valid), 62 | Err(_) => Ok(Validation::Invalid("Invalid number".into())), 63 | }) 64 | .prompt() 65 | .unwrap() 66 | .parse::() 67 | .unwrap(); 68 | 69 | // Prepare generator. 70 | generator = generator.prepare(); 71 | 72 | // Generate the password and return it. 73 | generator.generate(length) 74 | } else { 75 | // Ask for password and return it. 76 | Password::new("Password").prompt().unwrap() 77 | } 78 | } 79 | _ => panic!("Unknown prompt type"), 80 | } 81 | } 82 | } 83 | 84 | #[derive(Debug, Deserialize)] 85 | struct Template { 86 | priority: u8, 87 | name: String, 88 | prefix: String, 89 | name_from: Vec, 90 | welcome: String, 91 | attributes: Vec, 92 | } 93 | 94 | pub fn main() { 95 | // Load templates. 96 | let mut templates = [ 97 | include_str!("templates/0-websites.toml"), 98 | include_str!("templates/1-pin.toml"), 99 | ] 100 | .iter() 101 | .map(|t| toml::from_str::