// pinentry.rs // ************************************************************************* // * Copyright (C) 2017 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 . * // ************************************************************************* use error::Error as Error; use std::process; const CACHE_ID: &'static str = "nitrokey"; fn parse_pinentry_passphrase(response: Vec) -> Result, Error> { let string = String::from_utf8(response)?; 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 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().into_bytes()); } // Check if we are dealing with a special "ERR " line and report that // specially. if lines.len() >= 1 && lines[0].starts_with("ERR ") { let (_, error) = lines[0].split_at(4); return Err(Error::Error(error.to_string())); } return Err(Error::Error("Unexpected response: ".to_string() + &string)); } pub fn inquire_passphrase() -> Result, Error> { const PINENTRY_DESCR: &'static str = "+"; const PINENTRY_TITLE: &'static str = "Please+enter+user+PIN"; const PINENTRY_PASSWD: &'static str = "PIN"; let args = vec![CACHE_ID, PINENTRY_DESCR, PINENTRY_PASSWD, PINENTRY_TITLE].join(" "); let command = "GET_PASSPHRASE --data ".to_string() + &args; // We could also use the --data parameter here to have a more direct // representation of the passphrase but the resulting response was // considered more difficult to parse overall. It appears 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()?; return parse_pinentry_passphrase(output.stdout); } #[cfg(test)] mod tests { use super::*; #[test] fn parse_pinentry_passphrase_good() { let response = "D passphrase\nOK\n".to_string().into_bytes(); let expected = "passphrase".to_string().into_bytes(); assert_eq!(parse_pinentry_passphrase(response).unwrap(), expected) } #[test] fn parse_pinentry_passphrase_error() { let error = "83886179 Operation cancelled"; let response = "ERR ".to_string() + error + "\n"; let expected = error; let error = parse_pinentry_passphrase(response.to_string().into_bytes()); if let Error::Error(ref e) = error.err().unwrap() { assert_eq!(e, &expected); } else { panic!("Unexpected result"); } } #[test] fn parse_pinentry_passphrase_unexpected() { let response = "foobar\n"; let expected = "Unexpected response: ".to_string() + response; let error = parse_pinentry_passphrase(response.to_string().into_bytes()); if let Error::Error(ref e) = error.err().unwrap() { assert_eq!(e, &expected); } else { panic!("Unexpected result"); } } }