prs_lib/crypto/
store.rs

1//! Helpers to use recipients with password store.
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use thiserror::Error;
8
9use super::{Config, ContextPool, Key, Proto, prelude::*, recipients::Recipients, util};
10use crate::Store;
11
12/// Password store GPG IDs file.
13const STORE_GPG_IDS_FILE: &str = ".gpg-id";
14
15/// Password store public key directory.
16const STORE_PUB_KEY_DIR: &str = ".public-keys/";
17
18/// Get the GPG IDs file for a store.
19pub fn store_gpg_ids_file(store: &Store) -> PathBuf {
20    store.root.join(STORE_GPG_IDS_FILE)
21}
22
23/// Get the public keys directory for a store.
24pub fn store_public_keys_dir(store: &Store) -> PathBuf {
25    store.root.join(STORE_PUB_KEY_DIR)
26}
27
28/// Read GPG fingerprints from store.
29pub fn store_read_gpg_fingerprints(store: &Store) -> Result<Vec<String>> {
30    let path = store_gpg_ids_file(store);
31    if path.is_file() {
32        read_fingerprints(path)
33    } else {
34        Ok(vec![])
35    }
36}
37
38/// Write GPG fingerprints to a store.
39///
40/// Overwrites any existing file.
41pub fn store_write_gpg_fingerprints<S: AsRef<str>>(
42    store: &Store,
43    fingerprints: &[S],
44) -> Result<()> {
45    write_fingerprints(store_gpg_ids_file(store), fingerprints)
46}
47
48/// Read fingerprints from the given file.
49///
50/// Normalizes each fingerprint, see [`normalize_fingerprint`].
51fn read_fingerprints<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
52    Ok(fs::read_to_string(path)
53        .map_err(Err::ReadFile)?
54        .lines()
55        .map(util::normalize_fingerprint)
56        .filter(|fp| !fp.is_empty())
57        .collect())
58}
59
60/// Write fingerprints to the given file.
61fn write_fingerprints<P: AsRef<Path>, S: AsRef<str>>(path: P, fingerprints: &[S]) -> Result<()> {
62    fs::write(
63        path,
64        fingerprints
65            .iter()
66            .map(|k| k.as_ref())
67            .collect::<Vec<_>>()
68            .join("\n"),
69    )
70    .map_err(|err| Err::WriteFile(err).into())
71}
72
73/// Load the keys for the given store.
74///
75/// This will try to load the keys for all configured protocols, and errors if it fails.
76pub fn store_load_keys(store: &Store) -> Result<Vec<Key>> {
77    let mut keys = Vec::new();
78
79    // TODO: what to do if ids file does not exist?
80    // TODO: what to do if recipients is empty?
81    // TODO: what to do if key listed in file is not found, attempt to install?
82
83    // Load GPG keys
84    // TODO: do not crash here if GPG ids file is not found!
85    let fingerprints = store_read_gpg_fingerprints(store)?;
86
87    if !fingerprints.is_empty() {
88        let mut context = super::context(&crate::CONFIG)?;
89        let fingerprints: Vec<_> = fingerprints.iter().map(|fp| fp.as_str()).collect();
90        keys.extend(context.find_public_keys(&fingerprints)?);
91    }
92
93    // NEWPROTO: if a new proto is added, keys for a store should be loaded here
94
95    Ok(keys)
96}
97
98/// Load the recipients for the given store.
99///
100/// This will try to load the recipient keys for all configured protocols, and errors if it fails.
101pub fn store_load_recipients(store: &Store) -> Result<Recipients> {
102    Ok(Recipients::from(store_load_keys(store)?))
103}
104
105/// Save the keys for the given store.
106///
107/// This overwrites any existing recipient keys.
108pub fn store_save_keys(store: &Store, keys: &[Key]) -> Result<()> {
109    // Save GPG keys
110    let gpg_fingerprints: Vec<_> = keys
111        .iter()
112        .filter(|key| key.proto() == Proto::Gpg)
113        .map(|key| key.fingerprint(false))
114        .collect();
115    store_write_gpg_fingerprints(store, &gpg_fingerprints)?;
116
117    // Sync public keys for all proto's
118    store_sync_public_key_files(store, keys)?;
119
120    // TODO: import missing keys to system?
121
122    Ok(())
123}
124
125/// Save the keys for the given store.
126///
127/// This overwrites any existing recipient keys.
128pub fn store_save_recipients(store: &Store, recipients: &Recipients) -> Result<()> {
129    store_save_keys(store, recipients.keys())
130}
131
132/// Sync public key files in store with selected recipients.
133///
134/// - Removes obsolete keys that are not a selected recipient
135/// - Adds missing keys that are a recipient
136///
137/// This syncs public key files for all protocols. This is because the public key files themselves
138/// don't specify what protocol they use. All public key files and keys must therefore be taken
139/// into consideration all at once.
140pub fn store_sync_public_key_files(store: &Store, keys: &[Key]) -> Result<()> {
141    // Get public keys directory, ensure it exists
142    let dir = store_public_keys_dir(store);
143    fs::create_dir_all(&dir).map_err(Err::SyncKeyFiles)?;
144
145    // List key files in keys directory
146    let files: Vec<(PathBuf, String)> = dir
147        .read_dir()
148        .map_err(Err::SyncKeyFiles)?
149        .filter_map(|e| e.ok())
150        .filter(|e| e.file_type().map(|f| f.is_file()).unwrap_or(false))
151        .filter_map(|e| {
152            e.file_name()
153                .to_str()
154                .map(|fp| (e.path(), util::format_fingerprint(fp)))
155        })
156        .collect();
157
158    // List finger prints in .gpg-id file
159    let store_gpg_fingerprints =
160        store_read_gpg_fingerprints(store).context("failed to read .gpg-id file")?;
161
162    // Remove unused keys
163    for (path, _) in files.iter().filter(|(_, fp)| {
164        // Don't delete if key is in keychain
165        if !util::keys_contain_fingerprint(keys, fp) {
166            return false;
167        }
168
169        // Don't delete if key is in store fingerprints file
170        !store_gpg_fingerprints.contains(fp)
171    }) {
172        fs::remove_file(path).map_err(Err::SyncKeyFiles)?;
173    }
174
175    // Add missing keys
176    let mut contexts = ContextPool::empty();
177    for (key, fp) in keys
178        .iter()
179        .map(|k| (k, k.fingerprint(false)))
180        .filter(|(_, fp)| !files.iter().any(|(_, other)| fp == other))
181    {
182        // Lazy load compatible context
183        let proto = key.proto();
184        let config = Config::from(proto);
185        let context = contexts.get_mut(&config)?;
186
187        // Export public key to disk
188        let path = dir.join(&fp);
189        context.export_key_file(key.clone(), &path)?;
190    }
191
192    // NEWPROTO: if a new proto is added, public keys should be synced here
193
194    Ok(())
195}
196
197/// Import keys from store that are missing in the keychain.
198pub fn import_missing_keys_from_store(
199    store: &Store,
200    confirm_callback: impl Fn(String) -> bool,
201) -> Result<Vec<ImportResult>> {
202    // Get public keys directory, ensure it exists
203    let dir = store_public_keys_dir(store);
204    if !dir.is_dir() {
205        return Ok(vec![]);
206    }
207
208    // Cache protocol contexts
209    let mut contexts = ContextPool::empty();
210    let mut results = Vec::new();
211
212    // Check for missing GPG keys based on fingerprint, import them
213    let gpg_fingerprints = store_read_gpg_fingerprints(store)?;
214    for fingerprint in gpg_fingerprints {
215        let context = contexts.get_mut(&crate::CONFIG)?;
216        if context.get_public_key(&fingerprint).is_err() {
217            let path = &store_public_keys_dir(store).join(&fingerprint);
218            if path.is_file() {
219                if confirm_callback(fingerprint.clone()) {
220                    context.import_key_file(path)?;
221                    results.push(ImportResult::Imported(fingerprint));
222                } else {
223                    results.push(ImportResult::Rejected(fingerprint));
224                }
225            } else {
226                results.push(ImportResult::Unavailable(fingerprint));
227            }
228        }
229    }
230
231    // NEWPROTO: if a new proto is added, import missing keys here
232
233    Ok(results)
234}
235
236/// Missing key import results.
237pub enum ImportResult {
238    /// Key with given fingerprint was imported into keychain.
239    Imported(String),
240
241    /// Key with given fingerprint was not found and was not imported in keychain.
242    Unavailable(String),
243
244    /// Key with given fingerprint was rejected by the user.
245    Rejected(String),
246}
247
248/// Recipients extension for store functionality.
249pub trait StoreRecipients {
250    /// Load recipients from given store.
251    fn load(store: &Store) -> Result<Recipients>;
252
253    /// Save recipients to given store.
254    fn save(&self, store: &Store) -> Result<()>;
255}
256
257impl StoreRecipients for Recipients {
258    /// Load recipients from given store.
259    fn load(store: &Store) -> Result<Recipients> {
260        store_load_recipients(store)
261    }
262
263    /// Save recipients to given store.
264    fn save(&self, store: &Store) -> Result<()> {
265        store_save_recipients(store, self)
266    }
267}
268
269/// Store crypto error.
270#[derive(Debug, Error)]
271pub enum Err {
272    #[error("failed to write to file")]
273    WriteFile(#[source] std::io::Error),
274
275    #[error("failed to read from file")]
276    ReadFile(#[source] std::io::Error),
277
278    #[error("failed to sync public key files")]
279    SyncKeyFiles(#[source] std::io::Error),
280}