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}