Compare commits

...

8 Commits

Author SHA1 Message Date
German Lashevich 38a560681a
Obey clippy 5 years ago
German Lashevich 4a6a528885
Create rust.yml 5 years ago
German Lashevich 43422db885
Further update some deps 5 years ago
German Lashevich 18d0e79368
Remove deprecations 5 years ago
German Lashevich 6bfef70c18
Update deps 5 years ago
German Lashevich 6c1a52b7f1 Update dependencies 5 years ago
German Lashevich 4507e078f7
Update README.md 6 years ago
German Lashevich 64ab6f441b
🎨 Remove some deprecations 6 years ago

@ -0,0 +1,15 @@
name: Rust
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

1036
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -9,19 +9,20 @@ license = "MIT"
name = "rustotpony"
readme = "README.md"
repository = "https://github.com/zebradil/rustotpony"
version = "0.2.3"
version = "0.2.5"
[dependencies]
base32 = "0.3.1"
base32 = "0.4.0"
clap = "^2.29.0"
keyring = "0.6.0"
keyring = "0.7.1"
oath = "0.10.2"
rand = "0.4.1"
rpassword = "2.0.0"
rand = "0.7"
rpassword = "4.0"
rust-crypto = "0.2.36"
serde = "1.0.24"
serde_derive = "1.0.24"
serde_json = "1.0.8"
dirs = "2.0.2"
[dependencies.ctrlc]
features = ["termination"]

@ -4,23 +4,35 @@
[![Build status](https://ci.appveyor.com/api/projects/status/rx68dv1kjepslelh/branch/master?svg=true)](https://ci.appveyor.com/project/Zebradil/rustotpony/branch/master)
CLI manager of one-time password generators aka Google Authenticator.
Actually, it's simple in-file database which stores TOTP secret keys
without any encryption (will be added in the future).
CLI manager of [time-based one-time password](https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm) generators.
It is a desktop alternative for Google Authenticator.
## Installation
Make sure you have `$HOME/.cargo/bin` in your `$PATH`.
### From crates.io
```sh
$ cargo install rustotpony
```
### From source
1. Clone this repo
1. Run `cargo install` from the inside of the repo directory
1. Keep calm and wait for compilation
Probably, you need `gcc` (Linux) or `clang` (Mac OS) to compile dependencies.
Probably, you will need `gcc` (Linux) or `clang` (Mac OS) to compile dependencies.
## Usage
```text
$ totp help
🐴 RusTOTPony 0.2.3
German Lashevich <german.lashevich@gmail.com>
CLI manager of one-time password generators aka Google Authenticator
USAGE:
totp [SUBCOMMAND]
@ -40,11 +52,55 @@ SUBCOMMANDS:
Try `totp help [SUBCOMMAND]` to see help for the given subcommand
```
Steps:
1. Retrieve your secret key from TOTP provider (it must be base32 encoded)
1. Add new generator with `totp add GENNAME` (you will be asked for your secret)
1. Check new generator by `totp list` or just display dashboard with one-time passwords with `totp dash`
### Choose your password wisely
At the very first run `totp` asks for a password for a new database. It's located at `$HOME/.rustotpony/db.json` (don't be confused by `json` extension, actually, it's a binary file). If you forget the password or want to change it, you have to remove `$HOME/.rustotpony` directory. It's not convenient, but I'm going to improve usablity and an option for changing password.
### Basic scenario
1. Retrieve a secret key from your TOTP provider (it must be encoded with base32, for example: `GEZDGMZSGE2TKCQ=`)
```sh
$ # Creating a fake secret key for demo purposes
$ echo 123321555 | base32
GEZDGMZSGE2TKNIK
```
1. Add new generator with `totp add <NAME>` (you will be asked for a secret and a password)
```sh
$ # Adding a new TOTP generator
$ totp add demo
Enter your secret code:
Enter your database pass:
New application created: demo
```
If it's not the first run, you'll be asked for password twice: for opening database and for saving it.
1. Use `totp list` to check your secrets
```sh
$ # Listing all secrets in the database
$ totp list
Enter your database pass:
+------+------------------+----------+
| name | key | username |
+------+------------------+----------+
| demo | GEZDGMZSGE2TKNIK | |
+------+------------------+----------+
```
1. Use `totp dash` or just `totp` for realtime dashboard
```sh
$ # Display real-time dashboard with all generators
$ totp
Enter your database pass:
Welcome to RusTOTPony realtime dashboard! Press ^C to quit.
[============================================= ]
009216 demo
```
1. After hitting ^C it'll cleanup the dashboard
```sh
$ totp
Enter your database pass:
I won't tell anyone about this 🤫
```
## TODO
@ -58,4 +114,4 @@ Steps:
Licensed under [the MIT License][MIT License].
[MIT License]: https://github.com/zebradil/rustotpony/blob/master/LICENSE
[MIT License]: https://github.com/zebradil/rustotpony/blob/master/LICENSE

@ -1,5 +1,6 @@
extern crate clap;
extern crate ctrlc;
extern crate dirs;
extern crate rpassword;
extern crate rustotpony;
@ -25,7 +26,7 @@ impl Cli {
}
fn get_secret() -> String {
return rpassword::prompt_password_stdout("Enter your database pass: ").unwrap();
rpassword::prompt_password_stdout("Enter your database pass: ").unwrap()
}
// fn get_secret_from_storage() -> String { }
@ -119,7 +120,7 @@ impl Cli {
}
fn get_database_path() -> PathBuf {
let home = std::env::home_dir().unwrap_or(PathBuf::from("."));
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
home.join(Path::new(CONFIG_PATH))
}
@ -133,9 +134,10 @@ impl Cli {
print!("\x1B[{}A\x1B[0G\x1B[0J", lines_count + 1);
println!("I won't tell anyone about this 🤫");
std::process::exit(0);
}).expect("Error setting Ctrl-C handler");
})
.expect("Error setting Ctrl-C handler");
// Prepare sorted keys for displaying apps in order
let mut keys: Vec<String> = apps.keys().map(|key| key.clone()).collect();
let mut keys: Vec<String> = apps.keys().cloned().collect();
keys.sort();
loop {
if is_first_iteration {
@ -146,7 +148,7 @@ impl Cli {
Self::print_progress_bar();
for key in keys.iter() {
let app = &apps[key];
println!{"{:06} {}", app.get_code(), app.get_name()};
println! {"{:06} {}", app.get_code(), app.get_name()};
}
thread::sleep(Duration::from_millis(100));
}
@ -178,19 +180,19 @@ impl Cli {
return;
}
};
for (_, application) in apps {
for application in apps.values() {
applications_count += 1;
output_table
.entry("name")
.or_insert(Vec::new())
.or_insert_with(Vec::new)
.push(application.get_name());
output_table
.entry("key")
.or_insert(Vec::new())
.or_insert_with(Vec::new)
.push(application.get_secret());
output_table
.entry("username")
.or_insert(Vec::new())
.or_insert_with(Vec::new)
.push(application.get_username());
}
let name_max_length = output_table["name"]

@ -1,7 +1,6 @@
#![feature(fs_read_write)]
extern crate base32;
extern crate crypto;
extern crate dirs;
extern crate oath;
extern crate rand;
extern crate serde_json;
@ -14,7 +13,7 @@ use crypto::digest::Digest;
use crypto::sha2::Sha256;
use crypto::{aes, blockmodes, buffer, symmetriccipher};
use rand::{OsRng, Rng};
use rand::prelude::*;
use std::collections::HashMap;
use std::fs::{create_dir_all, File, OpenOptions};
@ -48,16 +47,16 @@ impl<DB: Database> RusTOTPony<DB> {
if self.applications.contains_key(name) {
Err(format!("Application with name '{}' already exists!", name))
} else {
&self.applications.insert(String::from(name), new_app);
self.applications.insert(String::from(name), new_app);
Ok(())
}
} else {
return Err(String::from("Couldn't decode secret key"));
Err(String::from("Couldn't decode secret key"))
}
}
pub fn delete_application(&mut self, name: &str) -> Result<(), String> {
if let Some(_) = self.applications.remove(name) {
if self.applications.remove(name).is_some() {
Ok(())
} else {
Err(format!(
@ -77,7 +76,7 @@ impl<DB: Database> RusTOTPony<DB> {
}
pub fn get_applications(&self) -> Result<&HashMap<String, GenApp>, String> {
if self.applications.len() == 0 {
if self.applications.is_empty() {
Err(String::from("There are no applications"))
} else {
Ok(&self.applications)
@ -97,7 +96,7 @@ impl<DB: Database> RusTOTPony<DB> {
}
pub fn flush(&self) {
&self.database.save_applications(&self.applications);
self.database.save_applications(&self.applications);
}
}
@ -132,16 +131,16 @@ struct DatabaseContentSchema {
pub struct JsonDatabase {
file_path: PathBuf,
secret_fn: &'static Fn() -> String,
secret_fn: &'static dyn Fn() -> String,
}
const IV_SIZE: usize = 16;
const KEY_SIZE: usize = 32;
impl JsonDatabase {
pub fn new(path: PathBuf, secret_fn: &'static Fn() -> String) -> JsonDatabase {
pub fn new(path: PathBuf, secret_fn: &'static dyn Fn() -> String) -> JsonDatabase {
JsonDatabase {
file_path: path,
secret_fn: secret_fn,
secret_fn,
}
}
@ -150,7 +149,7 @@ impl JsonDatabase {
sha.input_str(input);
let mut res: [u8; KEY_SIZE] = [0; KEY_SIZE];
sha.result(&mut res);
return res;
res
}
fn read_database_file(&self) -> JsonDatabaseSchema {
@ -181,7 +180,7 @@ impl JsonDatabase {
fn create_iv() -> Vec<u8> {
let mut iv = vec![0; IV_SIZE];
let mut rng = OsRng::new().ok().unwrap();
let mut rng = rand::thread_rng();
rng.fill_bytes(&mut iv);
iv
}
@ -189,7 +188,8 @@ impl JsonDatabase {
fn save_database_file(&self, content: JsonDatabaseSchema) {
let mut file = match self.open_database_file_for_write() {
Ok(f) => f,
Err(ref err) if err.kind() == ErrorKind::NotFound => self.create_database_file()
Err(ref err) if err.kind() == ErrorKind::NotFound => self
.create_database_file()
.expect("Couldn't create database file"),
Err(err) => panic!("Couldn't open database file: {:?}", err),
};
@ -243,7 +243,7 @@ impl JsonDatabase {
// us that it stopped processing data due to not having any more data in the
// input buffer.
loop {
let result = try!(encryptor.encrypt(&mut read_buffer, &mut write_buffer, true));
let result = encryptor.encrypt(&mut read_buffer, &mut write_buffer, true)?;
// "write_buffer.take_read_buffer().take_remaining()" means:
// from the writable buffer, create a new readable buffer which
@ -254,7 +254,7 @@ impl JsonDatabase {
.take_read_buffer()
.take_remaining()
.iter()
.map(|&i| i),
.copied(),
);
match result {
@ -282,13 +282,13 @@ impl JsonDatabase {
let mut write_buffer = buffer::RefWriteBuffer::new(&mut buffer);
loop {
let result = try!(decryptor.decrypt(&mut read_buffer, &mut write_buffer, true));
let result = decryptor.decrypt(&mut read_buffer, &mut write_buffer, true)?;
final_result.extend(
write_buffer
.take_read_buffer()
.take_remaining()
.iter()
.map(|&i| i),
.copied(),
);
match result {
BufferResult::BufferUnderflow => break,
@ -300,7 +300,7 @@ impl JsonDatabase {
}
fn create_database_file(&self) -> Result<File, std::io::Error> {
let dir = std::env::home_dir().unwrap_or(PathBuf::from("."));
let dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
if let Some(parent_dir) = Path::new(&self.file_path).parent() {
let dir = dir.join(parent_dir);
create_dir_all(dir)?;
@ -340,7 +340,7 @@ impl GenApp {
name: String::from(name),
secret: String::from(secret),
username: String::from(username),
secret_bytes: secret_bytes,
secret_bytes,
}
}

Loading…
Cancel
Save