password_store/
lib.rs

1/*
2 * Copyright (c) 2016-2020 Boucher, Antoni <bouanto@zoho.com>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy of
5 * this software and associated documentation files (the "Software"), to deal in
6 * the Software without restriction, including without limitation the rights to
7 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 * the Software, and to permit persons to whom the Software is furnished to do so,
9 * subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in all
12 * copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 */
21
22#[macro_use]
23extern crate json;
24
25mod chomp;
26
27use std::error;
28use std::ffi::OsStr;
29use std::fmt::{self, Display, Formatter};
30use std::io::{self, Write};
31use std::process::{Command, Stdio};
32use std::str::{self, Utf8Error};
33use std::string;
34
35use json::JsonValue;
36
37use Error::*;
38use chomp::Chomp;
39
40macro_rules! validate_path {
41    ($path:expr) => {
42        if $path.trim().is_empty() {
43            return Err(InvalidInput);
44        }
45    };
46}
47
48const MSG_SIZE: usize = 4;
49
50/// `Error` type that can be returned by the `PasswordStore` methods.
51#[derive(Debug)]
52pub enum Error {
53    FromUtf8(string::FromUtf8Error),
54    Json(json::Error),
55    Io(io::Error),
56    InvalidInput,
57    InvalidOutput,
58    Pass(String),
59    Utf8(Utf8Error),
60}
61
62impl From<json::Error> for Error {
63    fn from(error: json::Error) -> Self {
64        Json(error)
65    }
66}
67
68impl From<io::Error> for Error {
69    fn from(error: io::Error) -> Self {
70        Io(error)
71    }
72}
73
74impl From<Utf8Error> for Error {
75    fn from(error: Utf8Error) -> Self {
76        Utf8(error)
77    }
78}
79
80impl From<string::FromUtf8Error> for Error {
81    fn from(error: string::FromUtf8Error) -> Self {
82        FromUtf8(error)
83    }
84}
85
86impl Display for Error {
87    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
88        let string =
89            match *self {
90                FromUtf8(ref error) => error.to_string(),
91                Json(ref error) => error.to_string(),
92                Io(ref error) => error.to_string(),
93                InvalidInput => "invalid input".to_string(),
94                InvalidOutput => "invalid output".to_string(),
95                Pass(ref error) => error.clone(),
96                Utf8(ref error) => error.to_string(),
97            };
98        write!(formatter, "{}", string)
99    }
100}
101
102impl error::Error for Error {
103    fn description(&self) -> &str {
104        match *self {
105            FromUtf8(ref error) => error.description(),
106            Json(ref error) => error.description(),
107            Io(ref error) => error.description(),
108            InvalidInput => "invalid input",
109            InvalidOutput => "invalid output",
110            Pass(ref error) => error,
111            Utf8(ref error) => error.description(),
112        }
113    }
114}
115
116/// `Result` type returned by the `PasswordStore` methods.
117pub type Result<T> = std::result::Result<T, Error>;
118
119/// `Pass` process runner.
120pub struct PasswordStore;
121
122impl PasswordStore {
123    /// Get the username and password a the specified `path`.
124    pub fn get(path: &str) -> Result<(String, String)> {
125        validate_path!(path);
126        let mut response = gopass_ipc(object! {
127            "type" => "getLogin",
128            "entry" => path
129        })?;
130        if let (Some(mut username), Some(password)) = (response["username"].take_string(),
131            response["password"].take_string())
132        {
133            if username.is_empty() {
134                username = path.to_string();
135            }
136            Ok((username, password))
137        }
138        else {
139            Err(InvalidOutput)
140        }
141    }
142
143    /// Get the list of usernames at the specified `path`.
144    pub fn get_usernames(path: &str) -> Result<Vec<String>> {
145        validate_path!(path);
146        let response = gopass_ipc(object! {
147            "type" => "query",
148            "query" => path
149        })?;
150        let mut result = vec![];
151        match response {
152            JsonValue::Array(usernames) => {
153                for username in usernames {
154                    let username =
155                        match username.as_str() {
156                            Some(username) => username,
157                            None => return Err(InvalidOutput),
158                        };
159                    let index = username.rfind('/').map(|index| index + 1).unwrap_or(0);
160                    result.push(username[index..].to_string());
161                }
162            },
163            _ => return Err(InvalidOutput),
164        }
165        Ok(result)
166    }
167
168    /// Generate a password in the store.
169    pub fn generate(path: &str, use_symbols: bool, length: i32) -> Result<()> {
170        validate_path!(path);
171        let response = gopass_ipc(object! {
172            "type" => "create",
173            "entry_name" => path,
174            "password" => "",
175            "generate" => true,
176            "length" => length,
177            "use_symbols" => use_symbols
178        })?;
179        if response["username"].as_str().is_none() {
180            return Err(InvalidOutput);
181        }
182        Ok(())
183    }
184
185    /// Insert a password in the store.
186    pub fn insert(path: &str, password: &str) -> Result<()> {
187        validate_path!(path);
188        let response = gopass_ipc(object! {
189            "type" => "create",
190            "entry_name" => path,
191            "password" => password
192        })?;
193        if let Some(inserted_password) = response["password"].as_str() {
194            if password != inserted_password {
195                return Err(InvalidOutput);
196            }
197        }
198        Ok(())
199    }
200
201    /// Remove a password from the store.
202    pub fn remove(path: &str) -> Result<()> {
203        validate_path!(path);
204        exec_pass("rm", &["-f", path])?;
205        Ok(())
206    }
207}
208
209/// Exec the `gopass` process with the specified `command` and `args`.
210fn exec_pass<S: AsRef<OsStr>>(command: &str, args: &[S]) -> Result<String> {
211    let mut process = Command::new("gopass");
212    if !command.trim().is_empty() {
213        process.arg(command);
214    }
215    let child = process.args(args)
216        .stderr(Stdio::piped())
217        .stdin(Stdio::piped())
218        .stdout(Stdio::piped())
219        .spawn()?;
220    let output = child.wait_with_output()?;
221    let mut stderr = String::from_utf8(output.stderr)?;
222    if !stderr.is_empty() {
223        stderr.chomp();
224        Err(Pass(stderr))
225    }
226    else {
227        Ok(String::from_utf8(output.stdout)?)
228    }
229}
230
231/// Query the `gopass` process with a `json_query`.
232fn gopass_ipc(json_query: JsonValue) -> Result<JsonValue> {
233    let mut process = Command::new("gopass-jsonapi");
234    let mut child = process.args(&["listen"])
235        .stderr(Stdio::piped())
236        .stdin(Stdio::piped())
237        .stdout(Stdio::piped())
238        .spawn()?;
239    if let Some(stdin) = child.stdin.as_mut() {
240        let json_string = json_query.dump();
241        stdin.write_all(&i32_to_bytes(json_string.len() as i32))?;
242        write!(stdin, "{}", json_string)?;
243    }
244    let output = child.wait_with_output()?;
245    let mut stderr = String::from_utf8(output.stderr)?;
246    if !stderr.is_empty() {
247        stderr.chomp();
248        Err(Pass(stderr))
249    }
250    else {
251        json::parse(str::from_utf8(&output.stdout[MSG_SIZE..])?) // Skip the size of the json message.
252            .map_err(Into::into)
253    }
254}
255
256fn i32_to_bytes(num: i32) -> Vec<u8> {
257    vec![
258        (num & 0xFF) as u8,
259        ((num >> 8) & 0xFF) as u8,
260        ((num >> 16) & 0xFF) as u8,
261        ((num >> 24) & 0xFF) as u8,
262    ]
263}