diff options
author | Daniel Mueller <deso@posteo.net> | 2020-04-04 15:32:14 -0700 |
---|---|---|
committer | Daniel Mueller <deso@posteo.net> | 2020-04-04 15:32:14 -0700 |
commit | 681cc8882f7995407c33eb48730daaa901074460 (patch) | |
tree | c865f6c4a34e11af685889a09d95f3225e54a16c /nitrocli/src/pinentry.rs | |
parent | d0d9683df8398696147e7ee1fcffb2e4e957008c (diff) | |
download | nitrocli-681cc8882f7995407c33eb48730daaa901074460.tar.gz nitrocli-681cc8882f7995407c33eb48730daaa901074460.tar.bz2 |
Move nitrocli source code into repository root
Now that all vendored dependencies have been removed, this change moves
the program's source code from the nitrocli/ directory into the root of
the repository.
Diffstat (limited to 'nitrocli/src/pinentry.rs')
-rw-r--r-- | nitrocli/src/pinentry.rs | 404 |
1 files changed, 0 insertions, 404 deletions
diff --git a/nitrocli/src/pinentry.rs b/nitrocli/src/pinentry.rs deleted file mode 100644 index fd47657..0000000 --- a/nitrocli/src/pinentry.rs +++ /dev/null @@ -1,404 +0,0 @@ -// pinentry.rs - -// ************************************************************************* -// * Copyright (C) 2017-2020 Daniel Mueller (deso@posteo.net) * -// * * -// * This program is free software: you can redistribute it and/or modify * -// * it under the terms of the GNU General Public License as published by * -// * the Free Software Foundation, either version 3 of the License, or * -// * (at your option) any later version. * -// * * -// * This program is distributed in the hope that it will be useful, * -// * but WITHOUT ANY WARRANTY; without even the implied warranty of * -// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * -// * GNU General Public License for more details. * -// * * -// * You should have received a copy of the GNU General Public License * -// * along with this program. If not, see <http://www.gnu.org/licenses/>. * -// ************************************************************************* - -use std::borrow; -use std::fmt; -use std::io; -use std::process; -use std::str; - -use crate::args; -use crate::error::Error; - -type CowStr = borrow::Cow<'static, str>; - -/// PIN type requested from pinentry. -/// -/// The available PIN types correspond to the PIN types used by the Nitrokey devices: user and -/// admin. -#[allow(unused_doc_comments)] -Enum! {PinType, [ - Admin => "admin", - User => "user", -]} - -/// A trait representing a secret to be entered by the user. -pub trait SecretEntry: fmt::Debug { - /// The cache ID to use for this secret. - fn cache_id(&self) -> Option<CowStr>; - /// The prompt to display when asking for the secret. - fn prompt(&self) -> CowStr; - /// The description to display when asking for the secret. - fn description(&self, mode: Mode) -> CowStr; - /// The minimum number of characters the secret needs to have. - fn min_len(&self) -> u8; -} - -#[derive(Debug)] -pub struct PinEntry { - pin_type: PinType, - model: nitrokey::Model, - serial: String, -} - -impl PinEntry { - pub fn from<'mgr, D>(pin_type: PinType, device: &D) -> crate::Result<Self> - where - D: nitrokey::Device<'mgr>, - { - let model = device.get_model(); - let serial = device.get_serial_number()?; - Ok(Self { - pin_type, - model, - serial, - }) - } - - pub fn pin_type(&self) -> PinType { - self.pin_type - } -} - -impl SecretEntry for PinEntry { - fn cache_id(&self) -> Option<CowStr> { - let model = self.model.to_string().to_lowercase(); - let suffix = format!("{}:{}", model, self.serial); - let cache_id = match self.pin_type { - PinType::Admin => format!("nitrocli:admin:{}", suffix), - PinType::User => format!("nitrocli:user:{}", suffix), - }; - Some(cache_id.into()) - } - - fn prompt(&self) -> CowStr { - match self.pin_type { - PinType::Admin => "Admin PIN", - PinType::User => "User PIN", - } - .into() - } - - fn description(&self, mode: Mode) -> CowStr { - format!( - "{} for\rNitrokey {} {}", - match self.pin_type { - PinType::Admin => match mode { - Mode::Choose => "Please enter a new admin PIN", - Mode::Confirm => "Please confirm the new admin PIN", - Mode::Query => "Please enter the admin PIN", - }, - PinType::User => match mode { - Mode::Choose => "Please enter a new user PIN", - Mode::Confirm => "Please confirm the new user PIN", - Mode::Query => "Please enter the user PIN", - }, - }, - self.model, - self.serial, - ) - .into() - } - - fn min_len(&self) -> u8 { - match self.pin_type { - PinType::Admin => 8, - PinType::User => 6, - } - } -} - -#[derive(Debug)] -pub struct PwdEntry { - model: nitrokey::Model, - serial: String, -} - -impl PwdEntry { - pub fn from<'mgr, D>(device: &D) -> crate::Result<Self> - where - D: nitrokey::Device<'mgr>, - { - let model = device.get_model(); - let serial = device.get_serial_number()?; - Ok(Self { model, serial }) - } -} - -impl SecretEntry for PwdEntry { - fn cache_id(&self) -> Option<CowStr> { - None - } - - fn prompt(&self) -> CowStr { - "Password".into() - } - - fn description(&self, mode: Mode) -> CowStr { - format!( - "{} for\rNitrokey {} {}", - match mode { - Mode::Choose => "Please enter a new hidden volume password", - Mode::Confirm => "Please confirm the new hidden volume password", - Mode::Query => "Please enter a hidden volume password", - }, - self.model, - self.serial, - ) - .into() - } - - fn min_len(&self) -> u8 { - // More or less arbitrary minimum length based on the fact that the - // manual mentions six letter passwords in examples. Users - // *probably* should go longer than that, but we don't want to be - // too opinionated. - 6 - } -} - -/// Secret entry mode for pinentry. -/// -/// This enum describes the context of the pinentry query, for example -/// prompting for the current secret or requesting a new one. The mode -/// may affect the pinentry description and whether a quality bar is -/// shown. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Mode { - /// Let the user choose a new secret. - Choose, - /// Let the user confirm the previously chosen secret. - Confirm, - /// Query an existing secret. - Query, -} - -impl Mode { - fn show_quality_bar(self) -> bool { - self == Mode::Choose - } -} - -fn parse_pinentry_pin<R>(response: R) -> crate::Result<String> -where - R: AsRef<str>, -{ - let string = response.as_ref(); - let lines: Vec<&str> = string.lines().collect(); - - // We expect the response to be of the form: - // > D passphrase - // > OK - // or potentially: - // > ERR 83886179 Operation cancelled <Pinentry> - if lines.len() == 2 && lines[1] == "OK" && lines[0].starts_with("D ") { - // We got the only valid answer we accept. - let (_, pass) = lines[0].split_at(2); - return Ok(pass.to_string()); - } - - // Check if we are dealing with a special "ERR " line and report that - // specially. - if !lines.is_empty() && lines[0].starts_with("ERR ") { - let (_, error) = lines[0].split_at(4); - return Err(Error::from(error)); - } - Err(Error::Error(format!("Unexpected response: {}", string))) -} - -/// Inquire a secret from the user. -/// -/// This function inquires a secret from the user or returns a cached -/// entry, if available (and if caching is not disabled for the given -/// execution context). If an error message is set, it is displayed in -/// the entry dialog. The mode describes the context of the pinentry -/// dialog. It is used to choose an appropriate description and to -/// decide whether a quality bar is shown in the dialog. -pub fn inquire<E>( - ctx: &mut args::ExecCtx<'_>, - entry: &E, - mode: Mode, - error_msg: Option<&str>, -) -> crate::Result<String> -where - E: SecretEntry, -{ - let cache_id = entry - .cache_id() - .and_then(|id| if ctx.no_cache { None } else { Some(id) }) - // "X" is a sentinel value indicating that no caching is desired. - .unwrap_or_else(|| "X".into()) - .into(); - - let error_msg = error_msg - .map(|msg| msg.replace(" ", "+")) - .unwrap_or_else(|| String::from("+")); - let prompt = entry.prompt().replace(" ", "+"); - let description = entry.description(mode).replace(" ", "+"); - - let args = vec![cache_id, error_msg, prompt, description].join(" "); - let mut command = "GET_PASSPHRASE --data ".to_string(); - if mode.show_quality_bar() { - command += "--qualitybar "; - } - command += &args; - // An error reported for the GET_PASSPHRASE command does not actually - // cause gpg-connect-agent to exit with a non-zero error code, we have - // to evaluate the output to determine success/failure. - let output = process::Command::new("gpg-connect-agent") - .arg(command) - .arg("/bye") - .output() - .map_err(|err| match err.kind() { - io::ErrorKind::NotFound => { - io::Error::new(io::ErrorKind::NotFound, "gpg-connect-agent not found") - } - _ => err, - })?; - parse_pinentry_pin(str::from_utf8(&output.stdout)?) -} - -fn check<E>(entry: &E, secret: &str) -> crate::Result<()> -where - E: SecretEntry, -{ - if secret.len() < usize::from(entry.min_len()) { - Err(Error::Error(format!( - "The secret must be at least {} characters long", - entry.min_len() - ))) - } else { - Ok(()) - } -} - -pub fn choose<E>(ctx: &mut args::ExecCtx<'_>, entry: &E) -> crate::Result<String> -where - E: SecretEntry, -{ - clear(entry)?; - let chosen = inquire(ctx, entry, Mode::Choose, None)?; - clear(entry)?; - check(entry, &chosen)?; - - let confirmed = inquire(ctx, entry, Mode::Confirm, None)?; - clear(entry)?; - - if chosen != confirmed { - Err(Error::from("Entered secrets do not match")) - } else { - Ok(chosen) - } -} - -fn parse_pinentry_response<R>(response: R) -> crate::Result<()> -where - R: AsRef<str>, -{ - let string = response.as_ref(); - let lines = string.lines().collect::<Vec<_>>(); - - if lines.len() == 1 && lines[0] == "OK" { - // We got the only valid answer we accept. - return Ok(()); - } - Err(Error::Error(format!("Unexpected response: {}", string))) -} - -/// Clear the cached secret represented by the given entry. -pub fn clear<E>(entry: &E) -> crate::Result<()> -where - E: SecretEntry, -{ - if let Some(cache_id) = entry.cache_id() { - let command = format!("CLEAR_PASSPHRASE {}", cache_id); - let output = process::Command::new("gpg-connect-agent") - .arg(command) - .arg("/bye") - .output()?; - - parse_pinentry_response(str::from_utf8(&output.stdout)?) - } else { - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_pinentry_pin_good() { - let response = "D passphrase\nOK\n"; - let expected = "passphrase"; - - assert_eq!(parse_pinentry_pin(response).unwrap(), expected) - } - - #[test] - fn parse_pinentry_pin_error() { - let error = "83886179 Operation cancelled"; - let response = "ERR ".to_string() + error + "\n"; - let expected = error; - - let error = parse_pinentry_pin(response); - - if let Error::Error(ref e) = error.err().unwrap() { - assert_eq!(e, &expected); - } else { - panic!("Unexpected result"); - } - } - - #[test] - fn parse_pinentry_pin_unexpected() { - let response = "foobar\n"; - let expected = format!("Unexpected response: {}", response); - let error = parse_pinentry_pin(response); - - if let Error::Error(ref e) = error.err().unwrap() { - assert_eq!(e, &expected); - } else { - panic!("Unexpected result"); - } - } - - #[test] - fn parse_pinentry_response_ok() { - assert!(parse_pinentry_response("OK\n").is_ok()) - } - - #[test] - fn parse_pinentry_response_ok_no_newline() { - assert!(parse_pinentry_response("OK").is_ok()) - } - - #[test] - fn parse_pinentry_response_unexpected() { - let response = "ERR 42"; - let expected = format!("Unexpected response: {}", response); - let error = parse_pinentry_response(response); - - if let Error::Error(ref e) = error.err().unwrap() { - assert_eq!(e, &expected); - } else { - panic!("Unexpected result"); - } - } -} |