Skip to content

Commit a2486f7

Browse files
authored
Implement extract subcommand and password insertion through stdin (#403)
Extract subcommand prints the code of the selected OTP code. Password insertion through standard input permits non interactive access to OTP codes.
2 parents 9e52247 + e765df0 commit a2486f7

File tree

6 files changed

+92
-19
lines changed

6 files changed

+92
-19
lines changed

src/args.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ use crate::{
1313
pub struct CotpArgs {
1414
#[command(subcommand)]
1515
command: Option<CotpSubcommands>,
16+
/// Fetch the password from standard input
17+
#[arg(long = "password-stdin", default_value_t = false)]
18+
pub password_from_stdin: bool,
1619
}
1720

1821
#[derive(Subcommand)]
@@ -25,6 +28,8 @@ enum CotpSubcommands {
2528
Import(ImportArgs),
2629
/// Export cotp database
2730
Export(ExportArgs),
31+
/// Copies the selected code into the clipboard
32+
Extract(ExtractArgs),
2833
/// Change database password
2934
Passwd,
3035
}
@@ -123,6 +128,25 @@ pub struct ImportArgs {
123128
pub path: PathBuf,
124129
}
125130

131+
#[derive(Args)]
132+
pub struct ExtractArgs {
133+
/// Code Index
134+
#[arg(short, long, required_unless_present_any=["issuer", "label"])]
135+
pub index: Option<usize>,
136+
137+
/// Code issuer
138+
#[arg(short = 's', long, required_unless_present_any=["index","label"])]
139+
pub issuer: Option<String>,
140+
141+
/// Code label
142+
#[arg(short, long, required_unless_present_any=["index", "issuer"])]
143+
pub label: Option<String>,
144+
145+
/// Copy the code to the clipboard
146+
#[arg(short, long = "copy-clipboard", default_value_t = false)]
147+
pub copy_to_clipboard: bool,
148+
}
149+
126150
#[derive(Args)]
127151
pub struct ExportArgs {
128152
/// Export file path
@@ -219,6 +243,7 @@ pub fn args_parser(matches: CotpArgs, read_result: OTPDatabase) -> color_eyre::R
219243
Some(CotpSubcommands::Edit(args)) => argument_functions::edit(args, read_result),
220244
Some(CotpSubcommands::Import(args)) => argument_functions::import(args, read_result),
221245
Some(CotpSubcommands::Export(args)) => argument_functions::export(args, read_result),
246+
Some(CotpSubcommands::Extract(args)) => argument_functions::extract(args, read_result),
222247
Some(CotpSubcommands::Passwd) => argument_functions::change_password(read_result),
223248
// no args, show dashboard
224249
None => dashboard(read_result).map_err(|e| eyre!("An error occurred: {e}")),

src/argument_functions.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::args::{AddArgs, EditArgs, ExportArgs, ImportArgs};
1+
use crate::args::{AddArgs, EditArgs, ExportArgs, ExtractArgs, ImportArgs};
22
use crate::exporters::do_export;
33
use crate::exporters::otp_uri::OtpUriList;
44
use crate::importers::aegis::AegisJson;
@@ -8,7 +8,7 @@ use crate::importers::converted::ConvertedJsonList;
88
use crate::importers::freeotp_plus::FreeOTPPlusJson;
99
use crate::importers::importer::import_from_path;
1010
use crate::otp::otp_element::{OTPDatabase, OTPElement};
11-
use crate::utils;
11+
use crate::{clipboard, utils};
1212
use color_eyre::eyre::{eyre, ErrReport};
1313
use zeroize::Zeroize;
1414

@@ -158,6 +158,27 @@ pub fn export(matches: ExportArgs, database: OTPDatabase) -> color_eyre::Result<
158158
.map_err(|e| eyre!("An error occurred while exporting database: {e}"))
159159
}
160160

161+
pub fn extract(args: ExtractArgs, database: OTPDatabase) -> color_eyre::Result<OTPDatabase> {
162+
let first_with_filters = database
163+
.elements
164+
.iter()
165+
.enumerate()
166+
.find(|(index, code)| filter_extract(&args, index, code))
167+
.map(|(_, code)| code);
168+
169+
if let Some(otp) = first_with_filters {
170+
let code = otp.get_otp_code()?;
171+
println!("{}", code);
172+
if args.copy_to_clipboard {
173+
let _ = clipboard::copy_string_to_clipboard(code.as_str())?;
174+
println!("Copied to clipboard");
175+
}
176+
Ok(database)
177+
} else {
178+
Err(eyre!("No such code found with these fields"))
179+
}
180+
}
181+
161182
pub fn change_password(mut database: OTPDatabase) -> color_eyre::Result<OTPDatabase> {
162183
let mut new_password = utils::verified_password("New password: ", 8);
163184
database
@@ -166,3 +187,17 @@ pub fn change_password(mut database: OTPDatabase) -> color_eyre::Result<OTPDatab
166187
new_password.zeroize();
167188
Ok(database)
168189
}
190+
191+
fn filter_extract(args: &ExtractArgs, index: &usize, code: &OTPElement) -> bool {
192+
let match_by_index = args.index.map_or(true, |i| i == *index);
193+
194+
let match_by_issuer = args.issuer.as_ref().map_or(true, |issuer| {
195+
code.issuer.to_lowercase() == issuer.to_lowercase()
196+
});
197+
198+
let match_by_label = args.label.as_ref().map_or(true, |label| {
199+
code.label.to_lowercase() == label.to_lowercase()
200+
});
201+
202+
match_by_index && match_by_issuer && match_by_label
203+
}

src/clipboard.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use base64::{engine::general_purpose, Engine as _};
2+
use color_eyre::eyre::eyre;
23
use copypasta_ext::prelude::*;
34
#[cfg(target_os = "linux")]
45
use copypasta_ext::wayland_bin::WaylandBinClipboardContext;
@@ -12,13 +13,13 @@ pub enum CopyType {
1213
OSC52,
1314
}
1415

15-
pub fn copy_string_to_clipboard(content: String) -> Result<CopyType, ()> {
16-
if ssh_clipboard(content.as_str()) {
16+
pub fn copy_string_to_clipboard(content: &str) -> color_eyre::Result<CopyType> {
17+
if ssh_clipboard(content) {
1718
Ok(CopyType::OSC52)
18-
} else if wayland_clipboard(content.as_str()) || other_platform_clipboard(content.as_str()) {
19+
} else if wayland_clipboard(content) || other_platform_clipboard(content) {
1920
Ok(CopyType::Native)
2021
} else {
21-
Err(())
22+
Err(eyre!("Cannot detect clipboard implementation"))
2223
}
2324
}
2425

src/interface/handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ fn copy_selected_code_to_clipboard(app: &mut App) -> String {
217217
Some(selected) => match app.table.items.get(selected) {
218218
Some(element) => match element.values.get(3) {
219219
Some(otp_code) => {
220-
if let Ok(result) = copy_string_to_clipboard(otp_code.to_owned()) {
220+
if let Ok(result) = copy_string_to_clipboard(otp_code) {
221221
match result {
222222
CopyType::Native => "Copied!".to_string(),
223223
CopyType::OSC52 => "Remote copied!".to_string(),

src/main.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use interface::ui::Tui;
99
use otp::otp_element::{OTPDatabase, CURRENT_DATABASE_VERSION};
1010
use ratatui::prelude::CrosstermBackend;
1111
use ratatui::Terminal;
12-
use reading::{get_elements, ReadResult};
12+
use reading::{get_elements_from_input, get_elements_from_stdin, ReadResult};
1313
use std::{io, vec};
1414
use zeroize::Zeroize;
1515

@@ -25,7 +25,7 @@ mod path;
2525
mod reading;
2626
mod utils;
2727

28-
fn init() -> color_eyre::Result<ReadResult> {
28+
fn init(read_password_from_stdin: bool) -> color_eyre::Result<ReadResult> {
2929
match utils::init_app() {
3030
Ok(first_run) => {
3131
if first_run {
@@ -39,8 +39,10 @@ fn init() -> color_eyre::Result<ReadResult> {
3939
let save_result = database.save_with_pw(&pw);
4040
pw.zeroize();
4141
save_result.map(|(key, salt)| (database, key, salt.to_vec()))
42+
} else if read_password_from_stdin {
43+
get_elements_from_stdin()
4244
} else {
43-
get_elements()
45+
get_elements_from_input()
4446
}
4547
}
4648
Err(()) => Err(eyre!("An error occurred during database creation")),
@@ -50,8 +52,8 @@ fn init() -> color_eyre::Result<ReadResult> {
5052
fn main() -> AppResult<()> {
5153
color_eyre::install()?;
5254

53-
let cotp_args = CotpArgs::parse();
54-
let (database, mut key, salt) = match init() {
55+
let cotp_args: CotpArgs = CotpArgs::parse();
56+
let (database, mut key, salt) = match init(cotp_args.password_from_stdin) {
5557
Ok(v) => v,
5658
Err(e) => {
5759
println!("{e}");
@@ -71,7 +73,7 @@ fn main() -> AppResult<()> {
7173
let error_code = if reowned_database.is_modified() {
7274
match reowned_database.save(&key, &salt) {
7375
Ok(_) => {
74-
println!("Success");
76+
println!("Modifications has been persisted");
7577
0
7678
}
7779
Err(_) => {
@@ -80,7 +82,6 @@ fn main() -> AppResult<()> {
8082
}
8183
}
8284
} else {
83-
println!("Success");
8485
0
8586
};
8687
key.zeroize();

src/reading.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,26 @@ use crate::path::get_db_path;
44
use crate::utils;
55
use color_eyre::eyre::{eyre, ErrReport};
66
use std::fs::read_to_string;
7-
use std::io;
7+
use std::io::{self, BufRead};
88
use zeroize::Zeroize;
99

1010
pub type ReadResult = (OTPDatabase, Vec<u8>, Vec<u8>);
1111

12-
pub fn get_elements() -> color_eyre::Result<ReadResult> {
13-
let mut pw = utils::password("Password: ", 8);
14-
let (elements, key, salt) = read_from_file(&pw)?;
15-
pw.zeroize();
12+
pub fn get_elements_from_input() -> color_eyre::Result<ReadResult> {
13+
let pw = utils::password("Password: ", 8);
14+
get_elements_with_password(pw)
15+
}
16+
17+
pub fn get_elements_from_stdin() -> color_eyre::Result<ReadResult> {
18+
if let Some(password) = io::stdin().lock().lines().next() {
19+
return get_elements_with_password(password?);
20+
}
21+
Err(eyre!("Failure during stdin reading"))
22+
}
23+
24+
fn get_elements_with_password(mut password: String) -> color_eyre::Result<ReadResult> {
25+
let (elements, key, salt) = read_from_file(&password)?;
26+
password.zeroize();
1627
Ok((elements, key, salt))
1728
}
1829

0 commit comments

Comments
 (0)