libpass/
store_entry.rs

1//! Type definitions and interaction logic for entries in a password store
2
3use crate::file_io::{CipherFile, RoPlainFile, RwPlainFile};
4use crate::{utils, PassError, Result};
5use std::collections::hash_set::Iter as HashSetIter;
6use std::collections::HashSet;
7use std::fs::File;
8use std::hash::{Hash, Hasher};
9use std::io::{BufRead, BufReader};
10use std::path::{Path, PathBuf};
11
12/// An entry in the password store
13#[derive(Debug, Eq, PartialEq, Clone, Hash)]
14pub enum StoreEntry {
15    /// A reference to a directory which contains other entries
16    Directory(StoreDirectoryRef),
17    /// A reference to a file that holds the actual content of a store
18    File(StoreFileRef),
19}
20
21impl StoreEntry {
22    /// Retrieve the name of the store entry
23    ///
24    /// The name is represented as a relative path from the store root and can be used to retrieve this
25    /// entry using [`retrieve`](crate::retrieve).
26    pub fn name(&self) -> Result<String> {
27        match self {
28            Self::Directory(dir) => dir.name(),
29            Self::File(file) => file.name(),
30        }
31    }
32
33    /// Verify that this store entry matches what is actually present on the filesystem
34    pub(crate) fn verify(&self) -> Result<()> {
35        match self {
36            Self::Directory(dir) => dir.verify(),
37            Self::File(file) => file.verify(),
38        }
39    }
40}
41
42/// A reference to a directory in the password store
43#[derive(Debug, Eq, Clone)]
44pub struct StoreDirectoryRef {
45    /// Absolute path to the referenced directory
46    pub path: PathBuf,
47    /// Other entries that are contained in this directory
48    pub content: HashSet<StoreEntry>,
49}
50
51impl StoreDirectoryRef {
52    /// Retrieve the name of the store entry
53    ///
54    /// The name is represented as a relative path from the store root and can be used to retrieve this
55    /// entry using [`retrieve`](crate::retrieve).
56    pub fn name(&self) -> Result<String> {
57        Ok(utils::path2str(utils::abspath2relpath(&self.path)?)?.to_string())
58    }
59
60    /// Verify that *self* references an existing directory
61    pub(crate) fn verify(&self) -> Result<()> {
62        if self.path.exists() && self.path.is_dir() {
63            Ok(())
64        } else {
65            Err(PassError::InvalidStoreFormat(
66                self.path.to_owned(),
67                "Path either does not exist or is not a directory".to_string(),
68            ))
69        }
70    }
71
72    /// iterate over all the entries contained in the storage hierarchy below this directory
73    ///
74    /// **Note:** The iterator iterates over all entries even if they are in a subdirectory further down the
75    /// storage hierarchy thus flattening it. If you want to iterate only over the entries contained directly
76    /// in this directory, use the [`content`](StoreDirectoryRef::content) field instead.
77    pub fn iter(&self) -> StoreDirectoryIter {
78        StoreDirectoryIter {
79            entries: self.content.iter(),
80            current_dir: None,
81        }
82    }
83}
84
85impl Hash for StoreDirectoryRef {
86    fn hash<H: Hasher>(&self, state: &mut H) {
87        self.path.hash(state);
88    }
89}
90
91impl PartialEq for StoreDirectoryRef {
92    fn eq(&self, other: &Self) -> bool {
93        self.path == other.path
94    }
95}
96
97impl<'a> IntoIterator for &'a StoreDirectoryRef {
98    type Item = &'a StoreEntry;
99    type IntoIter = StoreDirectoryIter<'a>;
100
101    fn into_iter(self) -> Self::IntoIter {
102        self.iter()
103    }
104}
105
106/// An iterator that iterates over [`&StoreEntries`](StoreEntry) contained in a directory and its
107/// subdirectories
108#[derive(Debug)]
109pub struct StoreDirectoryIter<'a> {
110    entries: HashSetIter<'a, StoreEntry>,
111    current_dir: Option<Box<StoreDirectoryIter<'a>>>,
112}
113
114impl<'a> Iterator for StoreDirectoryIter<'a> {
115    type Item = &'a StoreEntry;
116
117    fn next(&mut self) -> Option<Self::Item> {
118        match self.current_dir {
119            Some(ref mut entry) => match entry.next() {
120                Some(next_entry) => Some(next_entry),
121                None => {
122                    self.current_dir = None;
123                    self.next()
124                }
125            },
126            None => match self.entries.next() {
127                Some(next_entry) => match next_entry {
128                    StoreEntry::File(_) => Some(next_entry),
129                    StoreEntry::Directory(dir) => {
130                        self.current_dir = Some(Box::new(dir.iter()));
131                        self.next()
132                    }
133                },
134                None => None,
135            },
136        }
137    }
138}
139
140/// A reference to a file in the password store
141#[derive(Debug, Eq, PartialEq, Clone, Hash)]
142pub struct StoreFileRef {
143    /// Absolute path to the referenced directory
144    pub path: PathBuf,
145}
146
147impl StoreFileRef {
148    /// Retrieve the name of the store entry
149    ///
150    /// The name is represented as a relative path from the store root and can be used to retrieve this
151    /// entry using [`retrieve`](crate::retrieve).
152    pub fn name(&self) -> Result<String> {
153        let relative_path = utils::path2str(utils::abspath2relpath(&self.path)?)?;
154
155        Ok(relative_path
156            .strip_suffix(".gpg")
157            .ok_or_else(|| {
158                PassError::InvalidStoreFormat(
159                    self.path.to_owned(),
160                    "File does not end with .gpg extension".to_string(),
161                )
162            })?
163            .to_string())
164    }
165
166    /// Retrieve the encryption keys that are used to encrypt this file
167    ///
168    /// This is a collection of gpg keys which are used as gpg recipients during encryption operations.
169    /// They are taken from a `.gpg-id` file that is automatically searched for adjecent to this file and
170    /// further up in the directory hierarchy.
171    ///
172    /// ## Example
173    /// If you already have a [`StoreFileRef`], you can use this method like so:
174    ///
175    /// ```
176    /// # use std::io::{Read, Seek, SeekFrom, Write};
177    /// # use libpass::{StoreEntry};
178    /// # use libpass::file_io::CipherFile;
179    /// # std::env::set_var("PASSWORD_STORE_DIR", std::env::current_dir().unwrap().join("tests/simple"));
180    /// # let store_file_ref = match libpass::retrieve("secret-a").unwrap() {
181    /// #     StoreEntry::File(f) => f,
182    /// #     StoreEntry::Directory(_) => panic!()
183    /// # };
184    /// assert_eq!(
185    ///     store_file_ref.encryption_keys().unwrap()[0].id().unwrap(),
186    ///     "8497251104B6F45F"
187    /// )
188    /// ```
189    pub fn encryption_keys(&self) -> Result<Vec<gpgme::Key>> {
190        log::warn!(
191            "Looking for encryption keys for entry at {}",
192            self.path.display()
193        );
194
195        /// look for a .gpg-id file starting from the given directory path
196        fn look_for_keys_file_from_dir(path: &Path) -> Result<PathBuf> {
197            log::trace!("Looking for .gpg-id file in directory {}", path.display());
198
199            let gpg_id_path = path.join(".gpg-id");
200            if gpg_id_path.exists() {
201                if gpg_id_path.is_file() {
202                    Ok(gpg_id_path)
203                } else {
204                    Err(PassError::InvalidStoreFormat(
205                        gpg_id_path,
206                        "Path is a directory but should be a file containing encryption key ids"
207                            .to_string(),
208                    ))
209                }
210            } else {
211                // recursion into parent directory
212                look_for_keys_file_from_dir(path.parent().ok_or_else(|| {
213                    PassError::InvalidStoreFormat(
214                        path.to_owned(),
215                        "Path does not hava a parent but a .gpg-id file has not yet been found"
216                            .to_string(),
217                    )
218                })?)
219            }
220        }
221
222        // start search in directory that this file contains
223        let keys_path = look_for_keys_file_from_dir(self.path.parent().ok_or_else(|| {
224            PassError::InvalidStoreFormat(
225                self.path.to_owned(),
226                "File does not have a parent which means it is not contained in a password store"
227                    .to_string(),
228            )
229        })?)?;
230
231        // extract keys from the file
232        log::trace!(
233            "Found .gpg-id file at {}, inspecting gpg keys from it",
234            keys_path.display()
235        );
236        let mut gpg_ctx = utils::create_gpg_context()?;
237        let file = File::open(keys_path)?;
238        let buffered_reader = BufReader::new(file);
239        buffered_reader
240            .lines()
241            .map(|maybe_line| match maybe_line {
242                Err(e) => Err(PassError::from(e)),
243                Ok(line) => {
244                    log::trace!("Loading key {}", line);
245                    Ok(gpg_ctx
246                        .get_key(&line)
247                        .map_err(|_| PassError::GpgKeyNotFoundError(line))?)
248                }
249            })
250            .collect()
251    }
252
253    /// Get an IO handle to the encrypted content of this file
254    pub fn cipher_io(&self) -> Result<CipherFile> {
255        CipherFile::new(&self.path)
256    }
257
258    /// Get a read-write IO handle to the plaintext content of this file
259    pub fn plain_io_rw(&self) -> Result<RwPlainFile> {
260        RwPlainFile::new(&self.path, self.encryption_keys()?)
261    }
262
263    /// Get a read-only IO handle to the plaintext of this file
264    pub fn plain_io_ro(&self) -> Result<RoPlainFile> {
265        RoPlainFile::new(&self.path)
266    }
267
268    /// Verify that *self* references an existing file with the expected file extension
269    pub(crate) fn verify(&self) -> Result<()> {
270        if self.path.exists()
271            && self.path.is_file()
272            && match self.path.extension() {
273                None => false,
274                Some(extension) => extension == "gpg",
275            }
276        {
277            Ok(())
278        } else {
279            Err(PassError::InvalidStoreFormat(self.path.to_owned(), "Path either does not exist, is not a regular file or does not have a .gpg extension".to_string()))
280        }
281    }
282}