libpass/
lib.rs

1//! Library for interacting with [pass](https://www.passwordstore.org/) managed data
2//!
3//! # Examples
4//!
5//! **Note:** These examples assume that the environment variable `PASSWORD_STORE_DIR` is set to point
6//! to the `tests/simple/` folder of this repository.
7//!
8//! - Retrieve a specific entry from the store
9//!
10//!   ```
11//!     # std::env::set_var("PASSWORD_STORE_DIR", std::env::current_dir().unwrap().join("tests/simple"));
12//!     let entry = libpass::retrieve("folder/subsecret-a").unwrap();
13//!     assert_eq!(entry.name().unwrap(), "folder/subsecret-a");
14//!   ```
15//!
16//! - Retrieve and decrypt a specific entry from the store
17//!
18//!   ```
19//!     use libpass::StoreEntry;
20//!     # std::env::set_var("PASSWORD_STORE_DIR", std::env::current_dir().unwrap().join("tests/simple"));
21//!
22//!     match libpass::retrieve("folder/subsecret-a").unwrap() {
23//!         StoreEntry::File(entry) => {
24//!             assert_eq!(entry.plain_io_ro().unwrap().as_ref(), "foobar123\n".as_bytes())
25//!         },
26//!         _ => panic!()
27//!     }
28//!   ```
29//!
30
31#![deny(unsafe_code)]
32#![warn(
33    clippy::unwrap_used,
34    missing_copy_implementations,
35    missing_debug_implementations,
36    missing_docs,
37    trivial_casts,
38    trivial_numeric_casts,
39    unreachable_pub,
40    unused_lifetimes,
41    unused_qualifications
42)]
43
44extern crate core;
45
46pub use crate::errors::PassError;
47pub use crate::store_entry::{StoreDirectoryIter, StoreDirectoryRef, StoreEntry, StoreFileRef};
48use std::collections::HashSet;
49use std::ffi::{OsStr, OsString};
50use std::path::{Path, PathBuf};
51use std::{env, fs};
52
53mod errors;
54pub mod file_io;
55mod store_entry;
56#[cfg(test)]
57mod tests;
58mod utils;
59
60/// Custom Result that is equivalent to `Result<T, PassError>`.
61pub type Result<T, E = PassError> = core::result::Result<T, E>;
62
63/// Environment variable that is interpreted when evaluating [`password_store_dir()`]
64pub const PASSWORD_STORE_DIR_ENV: &str = "PASSWORD_STORE_DIR";
65
66/// The default password store directory.
67///
68/// This is usually *~/.password-store* but can be overwritten by the environment variable defined in
69/// [`PASSWORD_STORE_DIR_ENV`].
70///
71/// ## Errors
72/// This function can produce an error during path canonicalization.
73/// This means that paths which begin with `~` are resolved to the current users home directory which can
74/// produce io errors.
75pub fn password_store_dir() -> Result<PathBuf> {
76    let path = match env::var(PASSWORD_STORE_DIR_ENV) {
77        Ok(env_var) => Path::new(&env_var).to_path_buf(),
78        Err(_) => Path::new("~/.password-store").to_path_buf(),
79    };
80    Ok(utils::canonicalize_path(&path)?)
81}
82
83/// List all passwords in the password store in a flat data structure
84///
85/// For detailed information that preserves the tree structure of the store use [`retrieve("/")`](retrieve)
86/// instead.
87pub fn list() -> Result<HashSet<StoreEntry>> {
88    match retrieve("/")? {
89        StoreEntry::File(file) => Err(PassError::InvalidStoreFormat(
90            file.path,
91            "Store root is not a directory but a file".to_string(),
92        )),
93        StoreEntry::Directory(dir) => Ok(HashSet::from_iter(dir.iter().cloned())),
94    }
95}
96
97/// Inspect the folder at *path* and recursively map it and its content to a [`StoreEntry`]
98fn inspect_folder(path: impl AsRef<Path>) -> Result<HashSet<StoreEntry>> {
99    fs::read_dir(path)?
100        // retrieve additional information about each file from filesystem
101        .map(|file| match file {
102            Err(e) => Err(e),
103            Ok(file) => Ok((
104                file.path(),
105                file.path().extension().unwrap_or_else(|| OsStr::new("")).to_os_string(),
106                file.file_type()?,
107            )),
108        })
109        // rule out that any errors occurred during information retrieval
110        .collect::<Result<Vec<_>, _>>()?
111        .iter()
112        // filter out files without .gpg extension
113        .filter(|(_, file_extension, file_type)| (file_type.is_file() && file_extension == &OsString::from("gpg") || !file_type.is_file()))
114        // map to correct StoreEntry representation and recurse into subdirectories
115        .map(|(path, _, file_type)|
116            if file_type.is_file() {
117                Ok(StoreEntry::File(StoreFileRef {
118                    path: path.clone()
119                }))
120            } else if file_type.is_dir() {
121                Ok(StoreEntry::Directory(StoreDirectoryRef{
122                    content: inspect_folder(&path)?,
123                    path: path.clone(),
124                }))
125            } else {
126                Err(PassError::InvalidStoreFormat(
127                    path.clone(),
128                    "File is neither a string nor directory but pass stores can only contain those types of files".to_string())
129                )
130            })
131        .collect()
132}
133
134/// Retrieve the stored entry identified by *pass_name*
135///
136/// `pass_name` is a path to a password file or directory relative to the store root
137pub fn retrieve(pass_name: &str) -> Result<StoreEntry> {
138    // strip leading / characters if there is one
139    let pass_name = match pass_name.strip_prefix('/') {
140        Some(result) => result,
141        None => pass_name,
142    };
143
144    // resolve paths that could possibly be meant by pass_name
145    let dir_path = password_store_dir()?.join(pass_name);
146    let file_path = password_store_dir()?.join(pass_name.to_string() + ".gpg");
147
148    // check if there is a file or directory with that name and return the correct result after
149    // additional verification
150    match (dir_path.exists(), file_path.exists()) {
151        (true, true) => Err(PassError::AmbiguousPassName(pass_name.to_string())),
152        (false, false) => Err(PassError::EntryNotFound(pass_name.to_string())),
153        (true, false) => Ok(StoreEntry::Directory(StoreDirectoryRef {
154            content: inspect_folder(&dir_path)?,
155            path: dir_path,
156        })),
157        (false, true) => Ok(StoreEntry::File(StoreFileRef { path: file_path })),
158    }
159    .and_then(|store_entry| {
160        store_entry.verify()?;
161        Ok(store_entry)
162    })
163}