├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── docker-compose.yml ├── src ├── config.rs ├── crud.rs ├── keybase.rs └── main.rs └── test ├── Dockerfile ├── bin └── keybase └── tests.sh /.gitignore: -------------------------------------------------------------------------------- 1 | target/* 2 | !Cargo.lock 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.4.0 4 | hooks: 5 | - id: check-merge-conflict 6 | - id: check-toml 7 | - id: end-of-file-fixer 8 | - id: trailing-whitespace 9 | - repo: https://github.com/doublify/pre-commit-rust 10 | rev: 55f347186aec7f2a4ec13c31effdb4b26512f2bc 11 | hooks: 12 | - id: cargo-check 13 | - id: clippy 14 | - id: fmt 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: rust 4 | rust: 5 | - stable 6 | - beta 7 | matrix: 8 | allow_failures: 9 | - rust: beta 10 | 11 | cache: cargo 12 | 13 | env: 14 | - KEYBASE_USER=passbase_test 15 | 16 | install: 17 | - cargo build 18 | 19 | before_script: 20 | - mv target/debug/passbase test/bin 21 | 22 | script: 23 | - ./test/tests.sh 24 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "atty" 25 | version = "0.2.14" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 28 | dependencies = [ 29 | "hermit-abi", 30 | "libc", 31 | "winapi", 32 | ] 33 | 34 | [[package]] 35 | name = "autocfg" 36 | version = "1.0.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 39 | 40 | [[package]] 41 | name = "bitflags" 42 | version = "1.2.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 45 | 46 | [[package]] 47 | name = "clap" 48 | version = "2.33.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 51 | dependencies = [ 52 | "ansi_term", 53 | "atty", 54 | "bitflags", 55 | "strsim", 56 | "textwrap", 57 | "unicode-width", 58 | "vec_map", 59 | ] 60 | 61 | [[package]] 62 | name = "dtoa" 63 | version = "0.2.2" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "0dd841b58510c9618291ffa448da2e4e0f699d984d436122372f446dae62263d" 66 | 67 | [[package]] 68 | name = "fuchsia-cprng" 69 | version = "0.1.1" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 72 | 73 | [[package]] 74 | name = "hermit-abi" 75 | version = "0.1.6" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" 78 | dependencies = [ 79 | "libc", 80 | ] 81 | 82 | [[package]] 83 | name = "itoa" 84 | version = "0.1.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "ae3088ea4baeceb0284ee9eea42f591226e6beaecf65373e41b38d95a1b8e7a1" 87 | 88 | [[package]] 89 | name = "lazy_static" 90 | version = "0.2.11" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" 93 | 94 | [[package]] 95 | name = "libc" 96 | version = "0.2.66" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 99 | 100 | [[package]] 101 | name = "memchr" 102 | version = "2.5.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 105 | 106 | [[package]] 107 | name = "num-traits" 108 | version = "0.1.43" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" 111 | dependencies = [ 112 | "num-traits 0.2.11", 113 | ] 114 | 115 | [[package]] 116 | name = "num-traits" 117 | version = "0.2.11" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 120 | dependencies = [ 121 | "autocfg", 122 | ] 123 | 124 | [[package]] 125 | name = "os_type" 126 | version = "2.2.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "7edc011af0ae98b7f88cf7e4a83b70a54a75d2b8cb013d6efd02e5956207e9eb" 129 | dependencies = [ 130 | "regex", 131 | ] 132 | 133 | [[package]] 134 | name = "passbase" 135 | version = "0.1.0" 136 | dependencies = [ 137 | "clap", 138 | "lazy_static", 139 | "os_type", 140 | "rand 0.3.23", 141 | "serde", 142 | "serde_derive", 143 | "serde_json", 144 | ] 145 | 146 | [[package]] 147 | name = "quote" 148 | version = "0.3.15" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 151 | 152 | [[package]] 153 | name = "rand" 154 | version = "0.3.23" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" 157 | dependencies = [ 158 | "libc", 159 | "rand 0.4.6", 160 | ] 161 | 162 | [[package]] 163 | name = "rand" 164 | version = "0.4.6" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 167 | dependencies = [ 168 | "fuchsia-cprng", 169 | "libc", 170 | "rand_core 0.3.1", 171 | "rdrand", 172 | "winapi", 173 | ] 174 | 175 | [[package]] 176 | name = "rand_core" 177 | version = "0.3.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 180 | dependencies = [ 181 | "rand_core 0.4.2", 182 | ] 183 | 184 | [[package]] 185 | name = "rand_core" 186 | version = "0.4.2" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 189 | 190 | [[package]] 191 | name = "rdrand" 192 | version = "0.4.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 195 | dependencies = [ 196 | "rand_core 0.3.1", 197 | ] 198 | 199 | [[package]] 200 | name = "regex" 201 | version = "1.5.6" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" 204 | dependencies = [ 205 | "aho-corasick", 206 | "memchr", 207 | "regex-syntax", 208 | ] 209 | 210 | [[package]] 211 | name = "regex-syntax" 212 | version = "0.6.26" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" 215 | 216 | [[package]] 217 | name = "serde" 218 | version = "0.8.23" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" 221 | 222 | [[package]] 223 | name = "serde_codegen" 224 | version = "0.8.23" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "a4c5d8a33087d8984f9535daa62a6498a08f6476050b00ab9339dd847e4c25cc" 227 | dependencies = [ 228 | "quote", 229 | "serde_codegen_internals", 230 | "syn", 231 | ] 232 | 233 | [[package]] 234 | name = "serde_codegen_internals" 235 | version = "0.11.3" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "afad7924a009f859f380e4a2e3a509a845c2ac66435fcead74a4d983b21ae806" 238 | dependencies = [ 239 | "syn", 240 | ] 241 | 242 | [[package]] 243 | name = "serde_derive" 244 | version = "0.8.23" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "ce44e5f4264b39e9d29c875357b7cc3ebdfb967bb9e22bfb5e44ffa400af5306" 247 | dependencies = [ 248 | "serde_codegen", 249 | ] 250 | 251 | [[package]] 252 | name = "serde_json" 253 | version = "0.8.6" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "67f7d2e9edc3523a9c8ec8cd6ec481b3a27810aafee3e625d311febd3e656b4c" 256 | dependencies = [ 257 | "dtoa", 258 | "itoa", 259 | "num-traits 0.1.43", 260 | "serde", 261 | ] 262 | 263 | [[package]] 264 | name = "strsim" 265 | version = "0.8.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 268 | 269 | [[package]] 270 | name = "syn" 271 | version = "0.10.8" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "58fd09df59565db3399efbba34ba8a2fec1307511ebd245d0061ff9d42691673" 274 | dependencies = [ 275 | "quote", 276 | "unicode-xid", 277 | ] 278 | 279 | [[package]] 280 | name = "textwrap" 281 | version = "0.11.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 284 | dependencies = [ 285 | "unicode-width", 286 | ] 287 | 288 | [[package]] 289 | name = "unicode-width" 290 | version = "0.1.7" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 293 | 294 | [[package]] 295 | name = "unicode-xid" 296 | version = "0.0.4" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 299 | 300 | [[package]] 301 | name = "vec_map" 302 | version = "0.8.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 305 | 306 | [[package]] 307 | name = "winapi" 308 | version = "0.3.8" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 311 | dependencies = [ 312 | "winapi-i686-pc-windows-gnu", 313 | "winapi-x86_64-pc-windows-gnu", 314 | ] 315 | 316 | [[package]] 317 | name = "winapi-i686-pc-windows-gnu" 318 | version = "0.4.0" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 321 | 322 | [[package]] 323 | name = "winapi-x86_64-pc-windows-gnu" 324 | version = "0.4.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 327 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passbase" 3 | version = "0.1.0" 4 | authors = ["dev@ojford.com"] 5 | 6 | [dependencies] 7 | clap = "^2.14" 8 | lazy_static = "^0.2" 9 | os_type = "^2.2" 10 | rand = "^0.3" 11 | serde = "^0.8" 12 | serde_derive = "^0.8" 13 | serde_json = "^0.8" 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Passbase 2 | ======== 3 | 4 | Passbase is a free password manager built on [Keybase](https://keybase.io). 5 | 6 | Generate long, random, secure passwords and store encrypted copies of them on your Keybase File System. 7 | 8 | [Soon, you'll also be able to share passwords with other Keybase users](#upcoming-features), at your discretion. 9 | 10 | Usage 11 | ----- 12 | 13 | **Whenever you create/change/read a password, it will open in `less`: this is to ensure it does not pollute your history, or remain visible on screen; once you have copied the password to your clipboard, press `q` to quit.** 14 | 15 | Create a new password for cool-new-website: 16 | ``` 17 | passbase create cool-new-website 18 | ``` 19 | 20 | (Realise cool-new-website requires passwords to be at most 25 characters, and only contain the special characters `!` and `@`): 21 | ``` 22 | passbase change cool-new-website -n25 -S!@ 23 | ``` 24 | 25 | Some time later, when you want to login to cool-new-website again: 26 | ``` 27 | passbase read cool-new-website 28 | ``` 29 | 30 | Get bored of cool-new-website: 31 | ``` 32 | passbase delete cool-new-website 33 | ``` 34 | 35 | What sites do we have passwords for again? 36 | ``` 37 | passbase list 38 | ``` 39 | 40 | I'd suggest creating an alias to help find and read your passwords, such as: 41 | ``` 42 | passbase read "$(passbase ls | fzf --no-preview --no-multi)" | pbcopy 43 | ``` 44 | 45 | which will fuzzy-find a password name, and then its value to the clipboard. (Example assumes POSIX shell on macOS.) 46 | 47 | ### Aliases 48 | 49 | You can also use `ls` and `rm` as aliases for the obvious. 50 | 51 | `--length` and `-n` are synonyms; as are `--specials` and `-S`. 52 | 53 | You can specify not to use any special characters at all with `--no-specials` or `-x`. (cf. [Defaults](#defaults)) 54 | 55 | ### Defaults 56 | 57 | The default password length is `128` characters. This is not for any particular reason - if you're copy-pasting the length is rather immaterial, and it's rarely caused me any issues. That said, I'm definitely open to feedback; if you're frequently running into lower limits, please open an issue (if one doesn't exist). 58 | 59 | Special characters included by default are ``~`!@£&*_+-=\,./|?`` - specified characters may only be a subset of these. They were arrived at by being those on my keyboard that do not break 'double-tap copy'. Again, open to suggestions for changes. 60 | 61 | Upcoming Features 62 | ----------------- 63 | 64 | Keybase allows not only keeping things private, but also mutual privacy. The main goal of v0.2 will be to facilitate *shared passwords* - like the supermarket or pizza place you order from with roommates, or the video on demand subscription you share with family. 65 | 66 | Installation 67 | ------------ 68 | 69 | macOS users can use [Homebrew](http://brew.sh/): 70 | ``` 71 | brew install OJFord/formulae/passbase 72 | ``` 73 | 74 | or download [the latest release](https://github.com/OJFord/passbase/releases). 75 | 76 | Development 77 | ----------- 78 | [![CI Status](https://travis-ci.com/OJFord/passbase.svg?token=SxsettpUmvjPeVFxsTig&branch=master)](https://travis-ci.com/OJFord/passbase) 79 | 80 | ### Tests 81 | 82 | The (integration) tests run inside a Docker container, primarily for ease of mocking the Keybase CLI tool and root directory. 83 | 84 | ``` 85 | docker-compose run tests 86 | ``` 87 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | tests: 2 | build: test 3 | environment: 4 | KEYBASE_USER: passbase_test 5 | volumes: 6 | - ./:/var/run 7 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | extern crate serde_json; 2 | 3 | use std::default::Default; 4 | use std::env; 5 | use std::fs; 6 | use std::fs::File; 7 | use std::path::PathBuf; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | struct Config { 11 | #[serde(rename = "User")] 12 | user: Option, 13 | } 14 | 15 | impl Default for Config { 16 | fn default() -> Config { 17 | Config { 18 | user: Default::default(), 19 | } 20 | } 21 | } 22 | 23 | pub const KBFS_DATA_DIR: &'static str = ".passbase"; 24 | 25 | fn config_file() -> Result { 26 | let path = env::home_dir() 27 | .expect("Failed to determine $HOME dir!") 28 | .join(&KBFS_DATA_DIR); 29 | if path.exists() { 30 | assert!(path.is_file()); 31 | } else { 32 | let _ = File::create(&path) 33 | .map(|mut buf| serde_json::to_writer(&mut buf, &Config::default())) 34 | .expect("Failed to create file."); 35 | } 36 | Ok(path) 37 | } 38 | 39 | fn set_config(config: &Config) { 40 | let _ = fs::OpenOptions::new() 41 | .write(true) 42 | .open(config_file().unwrap()) 43 | .map(|mut buf| serde_json::to_writer(&mut buf, config)) 44 | .expect("Failed to write to config file."); 45 | } 46 | 47 | fn get_config() -> Result { 48 | serde_json::from_reader(File::open(config_file()?)?) 49 | } 50 | 51 | pub fn get_user() -> Result { 52 | get_config() 53 | .map_err(|err| err.to_string())? 54 | .user 55 | .ok_or("User not set.".to_owned()) 56 | } 57 | 58 | pub fn set_user(user: &String) { 59 | let mut config = get_config().unwrap(); 60 | config.user = Some(user.clone()); 61 | set_config(&config); 62 | } 63 | -------------------------------------------------------------------------------- /src/crud.rs: -------------------------------------------------------------------------------- 1 | extern crate rand; 2 | 3 | use self::rand::Rng; 4 | use std::error::Error; 5 | use std::fs; 6 | use std::io; 7 | use std::io::prelude::*; 8 | use std::path::Path; 9 | use std::process::Command; 10 | 11 | fn gen(len: u16, specials: &str) -> String { 12 | let mut pswd = String::new(); 13 | 14 | let mut rng_a = rand::thread_rng(); 15 | let mut rng_b = rand::thread_rng(); 16 | 17 | let mut alphanums = rng_a.gen_ascii_chars(); 18 | 19 | if specials.is_empty() { 20 | return alphanums.take(len as usize).collect(); 21 | } 22 | 23 | let specials: Vec = specials.chars().map(|c| c.clone()).collect(); 24 | 25 | for _ in 0..len { 26 | if rng_b.gen_weighted_bool(8) { 27 | let special: char = *rng_b.choose(specials.as_slice()).unwrap(); 28 | pswd.push(special); 29 | } else { 30 | pswd.push(alphanums.next().unwrap()); 31 | } 32 | } 33 | pswd 34 | } 35 | 36 | pub fn create(passbase_dir: &Path, tag: &str, len: u16, specials: &str) { 37 | let file = passbase_dir.join(tag); 38 | assert!( 39 | !file.is_file(), 40 | format!("Password for {} already exists!", tag) 41 | ); 42 | let mut fp = fs::File::create(&file).expect("Failed to create file"); 43 | 44 | match fp.write_all(gen(len, specials).as_bytes()) { 45 | Err(why) => { 46 | fs::remove_file(&file).expect("Failed to remove created file"); 47 | panic!("Failed: {}", why.description()); 48 | } 49 | Ok(_) => { 50 | read(passbase_dir, tag); 51 | } 52 | } 53 | } 54 | 55 | pub fn list(passbase_dir: &Path) { 56 | let mut tags: Vec<_> = fs::read_dir(&passbase_dir) 57 | .expect("Failed to read directory") 58 | .map(|tag| tag.unwrap()) 59 | .collect(); 60 | tags.sort_by_key(|tag| tag.path()); 61 | 62 | for tag in tags { 63 | if !tag.path().is_dir() { 64 | println!("{}", tag.file_name().into_string().unwrap()); 65 | } 66 | } 67 | } 68 | 69 | pub fn read(passbase_dir: &Path, tag: &str) { 70 | let file = passbase_dir.join(tag); 71 | assert!(file.is_file(), format!("No password exists for {}", tag)); 72 | 73 | let ro_file = "/tmp/passbase-read"; 74 | fs::copy(file, ro_file).expect("Failed to access the filesystem"); 75 | 76 | let less = Command::new("less") 77 | .arg(ro_file) 78 | .spawn() 79 | .expect("Failed to spawn less"); 80 | 81 | let exit = less 82 | .wait_with_output() 83 | .expect("Failed to wait on less") 84 | .status; 85 | 86 | assert!(exit.success()); 87 | } 88 | 89 | pub fn change(passbase_dir: &Path, tag: &str, len: u16, specials: &str) { 90 | let file = passbase_dir.join(tag); 91 | assert!(file.is_file(), format!("No password exists for {}", tag)); 92 | 93 | let mut old_file = file.clone(); 94 | old_file.set_extension("old"); 95 | 96 | match fs::rename(file, old_file) { 97 | Ok(_) => create(passbase_dir, tag, len, specials), 98 | Err(e) => panic!("Failed to rename old file: {}", e), 99 | } 100 | } 101 | 102 | pub fn recover(passbase_dir: &Path, tag: &str) { 103 | read(passbase_dir, format!("{}.old", tag).as_str()); 104 | } 105 | 106 | pub fn remove(passbase_dir: &Path, tag: &str) { 107 | let file = passbase_dir.join(tag); 108 | assert!(file.is_file(), format!("No password exists for {}", tag)); 109 | println!("Are you sure, remove password for {tag} [y/N]? ", tag = tag); 110 | 111 | let mut answer = String::new(); 112 | io::stdin() 113 | .read_line(&mut answer) 114 | .expect("Failed to read from stdin"); 115 | match answer.trim().as_ref() { 116 | "y" | "Y" => { 117 | fs::remove_file(&file).expect("Failed to remove file"); 118 | } 119 | _ => { 120 | println!("Not removing password for {tag}", tag = tag); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/keybase.rs: -------------------------------------------------------------------------------- 1 | extern crate serde_json; 2 | 3 | use self::serde_json::{Map, Value}; 4 | use std; 5 | 6 | pub fn get_user() -> String { 7 | std::process::Command::new("keybase") 8 | .arg("login") 9 | .spawn() 10 | .expect("Keybase auth failed"); 11 | let output = std::process::Command::new("keybase") 12 | .arg("status") 13 | .arg("-j") 14 | .output() 15 | .unwrap(); 16 | let status: Map = 17 | serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).unwrap(); 18 | 19 | return String::from(status.get("Username").unwrap().as_str().unwrap()); 20 | } 21 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate clap; 2 | extern crate os_type; 3 | #[macro_use] 4 | extern crate serde_derive; 5 | #[macro_use] 6 | extern crate lazy_static; 7 | 8 | mod config; 9 | mod crud; 10 | mod keybase; 11 | 12 | use self::clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; 13 | use crud::*; 14 | use std::collections::HashSet; 15 | use std::fs; 16 | use std::iter::FromIterator; 17 | use std::path::Path; 18 | 19 | // These are the easily accessible special characters on a UK keyboard; 20 | // which, in my testing, do not break 'double-tap select' (to copy easily). 21 | const ACCEPTED_SPECIAL_CHARS: &'static str = "~`!@£&*_+-=\\,./|?"; 22 | 23 | lazy_static! { 24 | static ref ACCEPTED_SPECIALS_HASH: HashSet = { ACCEPTED_SPECIAL_CHARS.chars().collect() }; 25 | } 26 | 27 | fn main() { 28 | let tag_arg = Arg::with_name("tag") 29 | .index(1) 30 | .required(true) 31 | .takes_value(true) 32 | .value_name("NAME"); 33 | 34 | let len_arg = Arg::with_name("length") 35 | .help("Sets the number of characters in password") 36 | .short("n") 37 | .long("length") 38 | .takes_value(true) 39 | .default_value("128") 40 | .validator(validate_number); 41 | 42 | let no_sym_arg = Arg::with_name("no-specials") 43 | .help("Sets a strictly alphanumeric password") 44 | .short("X") 45 | .long("no-specials"); 46 | 47 | let sym_arg = Arg::with_name("specials") 48 | .help("Provides a set of special chars to use") 49 | // Conflicts with option is incompatible with a default 50 | //.conflicts_with(no_sym_arg.name) 51 | .short("s") 52 | .long("specials") 53 | .takes_value(true) 54 | .default_value(ACCEPTED_SPECIAL_CHARS) 55 | .validator(validate_special_chars); 56 | 57 | let app_matches = App::new("Passbase") 58 | .version("0.1") 59 | .author("Oliver Ford ") 60 | .about("Password generation & management integrated with Keybase") 61 | .arg(tag_arg.clone()) 62 | .setting(AppSettings::SubcommandsNegateReqs) 63 | .subcommand(SubCommand::with_name("list").visible_alias("ls")) 64 | .subcommand( 65 | SubCommand::with_name("read") 66 | .visible_alias("cat") 67 | .arg(tag_arg.clone()), 68 | ) 69 | .subcommand( 70 | SubCommand::with_name("create") 71 | .visible_alias("touch") 72 | .arg(no_sym_arg.clone()) 73 | .arg(sym_arg.clone()) 74 | .arg(len_arg.clone()) 75 | .arg(tag_arg.clone()), 76 | ) 77 | .subcommand( 78 | SubCommand::with_name("change") 79 | .help("Changes a password. Previous can be `recover`ed.") 80 | .arg(no_sym_arg.clone()) 81 | .arg(sym_arg.clone()) 82 | .arg(len_arg.clone()) 83 | .arg(tag_arg.clone()), 84 | ) 85 | .subcommand( 86 | SubCommand::with_name("recover") 87 | .help("Recovers the previous version of a `change`d password") 88 | .arg(tag_arg.clone()), 89 | ) 90 | .subcommand( 91 | SubCommand::with_name("remove") 92 | .help("DELETES given password FOREVER. Cannot be `recover`ed") 93 | .visible_alias("rm") 94 | .arg(tag_arg.clone()), 95 | ) 96 | .get_matches(); 97 | 98 | let user: String; 99 | if let Ok(config_user) = config::get_user() { 100 | user = config_user; 101 | } else { 102 | user = keybase::get_user(); 103 | config::set_user(&user); 104 | } 105 | 106 | let passbase_dir = match os_type::current_platform().os_type { 107 | os_type::OSType::OSX => { 108 | if os_type::current_platform().version > "10.15.0".into() { 109 | Path::new("/Volumes/Keybase/private") 110 | } else { 111 | Path::new("/Keybase/private") 112 | } 113 | } 114 | _ => Path::new("/Keybase/private"), 115 | } 116 | .join(user) 117 | .join(".passbase"); 118 | 119 | match passbase_dir.exists() { 120 | true => assert!( 121 | passbase_dir.is_dir(), 122 | format!("A file {} already exists in KBFS!", config::KBFS_DATA_DIR) 123 | ), 124 | false => { 125 | println!("Passbase directory does not exist in KBFS, creating..."); 126 | fs::create_dir(&passbase_dir).expect("Failed to create Passbase directory"); 127 | } 128 | } 129 | 130 | fn tag<'a>(args: &'a ArgMatches) -> &'a str { 131 | return args.value_of("tag").unwrap(); 132 | } 133 | 134 | fn len<'a>(args: &'a ArgMatches) -> u16 { 135 | return args.value_of("length").unwrap().parse::().unwrap(); 136 | } 137 | 138 | fn specials<'a>(args: &'a ArgMatches) -> &'a str { 139 | if args.is_present("no-specials") { 140 | return ""; 141 | } else { 142 | return args.value_of("specials").unwrap(); 143 | } 144 | } 145 | 146 | match app_matches.subcommand() { 147 | ("list", _) => list(&passbase_dir), 148 | ("create", Some(args)) => create(&passbase_dir, tag(args), len(args), specials(args)), 149 | ("change", Some(args)) => change(&passbase_dir, tag(args), len(args), specials(args)), 150 | ("recover", Some(args)) => recover(&passbase_dir, tag(args)), 151 | ("remove", Some(args)) => remove(&passbase_dir, tag(args)), 152 | ("read", Some(args)) => read(&passbase_dir, tag(args)), 153 | _ => read(&passbase_dir, tag(&app_matches)), 154 | } 155 | } 156 | 157 | fn validate_special_chars(v: String) -> Result<(), String> { 158 | let given: HashSet = v.chars().collect(); 159 | 160 | if given.is_subset(&ACCEPTED_SPECIALS_HASH) { 161 | Ok(()) 162 | } else { 163 | let intersection_of_requirements: HashSet = given 164 | .intersection(&ACCEPTED_SPECIALS_HASH) 165 | .cloned() 166 | .collect(); 167 | 168 | Err(format!( 169 | "must be a subset of {} -- use: {}", 170 | ACCEPTED_SPECIAL_CHARS, 171 | String::from_iter(intersection_of_requirements.iter().map(|c| *c)) 172 | )) 173 | } 174 | } 175 | 176 | fn validate_number(v: String) -> Result<(), String> { 177 | match v.parse::() { 178 | Ok(_) => Ok(()), 179 | Err(_) => Err(String::from("must be an integer")), 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN mkdir -p /keybase/private /var/run 6 | RUN apt-get update \ 7 | && apt-get install \ 8 | ca-certificates \ 9 | curl \ 10 | file \ 11 | gcc \ 12 | libc6-dev \ 13 | sudo \ 14 | -qqy \ 15 | --no-install-recommends \ 16 | && rm -rf /var/lib/apt/lists/* 17 | RUN curl -sSf https://static.rust-lang.org/rustup.sh \ 18 | | sh -s -- --channel=nightly 19 | 20 | ADD bin/keybase /usr/local/bin/keybase 21 | ADD tests.sh /usr/local/bin/runtests 22 | 23 | WORKDIR /var/run 24 | CMD cargo build && mv target/debug/passbase /usr/local/bin && runtests 25 | -------------------------------------------------------------------------------- /test/bin/keybase: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo {\"Username\": \"$KEYBASE_USER\"} 3 | -------------------------------------------------------------------------------- /test/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | red="\033[31m" 5 | green="\033[32m" 6 | default="\033[39m" 7 | outcome_pos="\033[74G" 8 | 9 | tests_run=0 10 | failures=0 11 | 12 | failure() { 13 | printf $outcome_pos 14 | printf $red 15 | printf "FAILED\n" 16 | printf $default 17 | failures=$((failures+1)) 18 | cat /tmp/test_err >> /tmp/test_failures 19 | rm /tmp/test_err 20 | } 21 | 22 | success() { 23 | printf $outcome_pos 24 | printf $green 25 | printf "OK\n" 26 | printf $default 27 | rm -f /tmp/test_err 28 | } 29 | 30 | should_pass() { 31 | $@ 1>/dev/null 2>>/tmp/test_err || touch /tmp/test_failed 32 | printf '.' 33 | } 34 | 35 | should_fail() { 36 | $@ 1>/dev/null 2>>/tmp/test_err && touch /tmp/test_failed 37 | printf '.' 38 | } 39 | 40 | describe() { 41 | printf "Testing $1.." 42 | } 43 | 44 | finish() { 45 | tests_run=$((tests_run+1)) 46 | test -f /tmp/test_failed && failure || success 47 | rm -f /tmp/test_failed 48 | } 49 | 50 | 51 | # Setup 52 | test_dir=$(dirname $(readlink -f "$0")) 53 | export PATH="$test_dir/bin:$PATH" 54 | 55 | keybase_dir="/keybase/private/$KEYBASE_USER" 56 | sudo mkdir -p $keybase_dir 57 | user=$(whoami) 58 | sudo chown -R $user $keybase_dir 59 | 60 | passbase_dir="$keybase_dir/.passbase" 61 | config_file="$HOME/.passbase" 62 | 63 | keybase_loc=$(which keybase) 64 | hide_keybase() { 65 | sudo mv $keybase_loc /tmp/keybase 66 | } 67 | unhide_keybase() { 68 | sudo mv /tmp/keybase $keybase_loc 69 | } 70 | # 71 | 72 | describe "list succeeds with no tags" 73 | should_pass passbase list 74 | finish 75 | 76 | describe "failure with no config or Keybase" 77 | rm $config_file 78 | hide_keybase 79 | should_fail passbase list 80 | unhide_keybase 81 | finish 82 | 83 | describe "with .passbase config" 84 | printf "{\"User\":\"$KEYBASE_USER\"}" > $config_file 85 | hide_keybase 86 | should_pass passbase list 87 | unhide_keybase 88 | finish 89 | 90 | describe "update to .passbase config" 91 | rm $config_file 92 | should_pass passbase list 93 | should_pass test -f $config_file 94 | finish 95 | 96 | describe "failure to access non-existent tag" 97 | should_fail passbase read foo 98 | should_fail passbase change foo 99 | yes | should_fail passbase remove foo 100 | finish 101 | 102 | describe "creation of tag" 103 | should_pass passbase create foo 104 | should_pass test -f $passbase_dir/foo 105 | should_pass passbase read foo 106 | finish 107 | 108 | describe "change to existing tag" 109 | touch $passbase_dir/foo /tmp/foo_old 110 | should_pass passbase change foo 111 | should_fail cmp -s /tmp/foo_old $passbase_dir/foo 112 | finish 113 | 114 | describe "removal of existing tag" 115 | touch $passbase_dir/foo 116 | yes | should_pass passbase remove foo 117 | should_fail test -f $passbase_dir/foo 118 | finish 119 | 120 | describe "'are you sure' prompt for removal" 121 | touch $passbase_dir/bar 122 | yes 'n' | should_pass passbase remove bar 123 | should_pass test -f $passbase_dir/bar 124 | finish 125 | 126 | describe "'are you sure' removal prompt defaults to 'no'" 127 | touch $passbase_dir/rab 128 | yes '' | should_pass passbase remove rab 129 | should_pass test -f $passbase_dir/rab 130 | finish 131 | 132 | describe "aliases" 133 | should_pass passbase ls 134 | should_pass passbase touch foobar 135 | should_pass passbase cat foobar 136 | yes | should_pass passbase rm foobar 137 | finish 138 | 139 | describe "custom length" 140 | should_pass passbase create -n10 short 141 | len=$(wc -c < $passbase_dir/short) 142 | should_pass test $len -eq 10 143 | should_pass passbase change --length 5 short 144 | len=$(wc -c < $passbase_dir/short) 145 | should_pass test $len -eq 5 146 | finish 147 | 148 | describe "no special chars" 149 | should_pass passbase create -X -n1000 nospecials 150 | len=$(cat $passbase_dir/nospecials | sed 's/[^a-z0-9A-Z]//g' | awk '{ print length }') 151 | should_pass test $len -eq 1000 152 | should_pass passbase change --no-specials -n1000 nospecials 153 | len=$(cat $passbase_dir/nospecials | sed 's/[^a-z0-9A-Z]//g' | awk '{ print length }') 154 | should_pass test $len -eq 1000 155 | finish 156 | 157 | describe "custom special chars" 158 | should_pass passbase create -s@! -n1000 somespecials 159 | len=$(cat $passbase_dir/somespecials | sed 's/[^!@a-z0-9A-Z]//g' | awk '{ print length }') 160 | should_pass test $len -eq 1000 161 | len=$(cat $passbase_dir/somespecials | sed 's/[^a-z0-9A-Z]//g' | awk '{ print length }') 162 | should_fail test $len -eq 1000 163 | should_pass passbase change --specials @! -n1000 somespecials 164 | len=$(cat $passbase_dir/somespecials | sed 's/[^!@a-z0-9A-Z]//g' | awk '{ print length }') 165 | should_pass test $len -eq 1000 166 | len=$(cat $passbase_dir/somespecials | sed 's/[^a-z0-9A-Z]//g' | awk '{ print length }') 167 | should_fail test $len -eq 1000 168 | finish 169 | 170 | # Teardown 171 | sudo rm -r /keybase/private/passbase_test 172 | rm $config_file 173 | echo "Ran $tests_run tests; $failures of which failed." 174 | test $failures -eq 0 && exit 0 || exit 1 175 | # 176 | --------------------------------------------------------------------------------