aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Mueller <deso@posteo.net>2017-03-27 20:45:25 -0700
committerDaniel Mueller <deso@posteo.net>2017-03-27 20:45:25 -0700
commit1e9627ad412f364f3c5f556c5bb2ca2bb076d06d (patch)
treead1e3191d72869234fa55d0ff993d1450866e372
parentde5ae8656387267bb4614bbab6b62784323f23c0 (diff)
downloadnitrocli-1e9627ad412f364f3c5f556c5bb2ca2bb076d06d.tar.gz
nitrocli-1e9627ad412f364f3c5f556c5bb2ca2bb076d06d.tar.bz2
Add pinentry module
We do not want to roll our own infrastructure for entering a password (or PIN) securely, as there are existing providers of such functionality. gpg-agent, which uses pinentry for this very purpose, is such a program and we can safely assume to be present because we use it with the smartcard part of the nitrokey. This change introduces a new module, pinentry.rs, that provides the means to invoke gpg-agent to ask the user for a PIN and to parse the result. Using gpg-agent like this has two advantages that other solutions do not necessarily provide: first, because we use gpg-agent anyway it's pinentry configuration is as the user desires it and, hence, the integration appears seamless. And second, the agent caches pass phrases which alleviates the need for repeated entry should the credential be required again.
-rw-r--r--nitrocli/src/error.rs20
-rw-r--r--nitrocli/src/pinentry.rs116
2 files changed, 136 insertions, 0 deletions
diff --git a/nitrocli/src/error.rs b/nitrocli/src/error.rs
index 65992f0..a88b5a7 100644
--- a/nitrocli/src/error.rs
+++ b/nitrocli/src/error.rs
@@ -19,11 +19,15 @@
use libhid;
use std::fmt;
+use std::io;
+use std::string;
#[derive(Debug)]
pub enum Error {
HidError(libhid::Error),
+ IoError(io::Error),
+ Utf8Error(string::FromUtf8Error),
Error(String),
}
@@ -35,10 +39,26 @@ impl From<libhid::Error> for Error {
}
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Error {
+ return Error::IoError(e);
+ }
+}
+
+
+impl From<string::FromUtf8Error> for Error {
+ fn from(e: string::FromUtf8Error) -> Error {
+ return Error::Utf8Error(e);
+ }
+}
+
+
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *&self {
&Error::HidError(ref e) => return write!(f, "hidapi error: {}", e),
+ &Error::Utf8Error(_) => return write!(f, "Encountered UTF-8 conversion error"),
+ &Error::IoError(ref e) => return write!(f, "IO error: {}", e.get_ref().unwrap()),
&Error::Error(ref e) => return write!(f, "{}", e),
}
}
diff --git a/nitrocli/src/pinentry.rs b/nitrocli/src/pinentry.rs
new file mode 100644
index 0000000..eabf598
--- /dev/null
+++ b/nitrocli/src/pinentry.rs
@@ -0,0 +1,116 @@
+// 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 <http://www.gnu.org/licenses/>. *
+// *************************************************************************
+
+use error::Error as Error;
+use std::process;
+
+
+const CACHE_ID: &'static str = "nitrokey";
+
+
+fn parse_pinentry_passphrase(response: Vec<u8>) -> Result<Vec<u8>, 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 <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().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<Vec<u8>, 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");
+ }
+ }
+}