aboutsummaryrefslogtreecommitdiff
path: root/nitrocli/src/pinentry.rs
blob: eabf59833e798428bf91f5e2215985c7d58739f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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");
    }
  }
}