1use 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
12const STORE_GPG_IDS_FILE: &str = ".gpg-id";
14
15const STORE_PUB_KEY_DIR: &str = ".public-keys/";
17
18pub fn store_gpg_ids_file(store: &Store) -> PathBuf {
20 store.root.join(STORE_GPG_IDS_FILE)
21}
22
23pub fn store_public_keys_dir(store: &Store) -> PathBuf {
25 store.root.join(STORE_PUB_KEY_DIR)
26}
27
28pub 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
38pub 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
48fn 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
60fn 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
73pub fn store_load_keys(store: &Store) -> Result<Vec<Key>> {
77 let mut keys = Vec::new();
78
79 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 Ok(keys)
96}
97
98pub fn store_load_recipients(store: &Store) -> Result<Recipients> {
102 Ok(Recipients::from(store_load_keys(store)?))
103}
104
105pub fn store_save_keys(store: &Store, keys: &[Key]) -> Result<()> {
109 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 store_sync_public_key_files(store, keys)?;
119
120 Ok(())
123}
124
125pub fn store_save_recipients(store: &Store, recipients: &Recipients) -> Result<()> {
129 store_save_keys(store, recipients.keys())
130}
131
132pub fn store_sync_public_key_files(store: &Store, keys: &[Key]) -> Result<()> {
141 let dir = store_public_keys_dir(store);
143 fs::create_dir_all(&dir).map_err(Err::SyncKeyFiles)?;
144
145 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 let store_gpg_fingerprints =
160 store_read_gpg_fingerprints(store).context("failed to read .gpg-id file")?;
161
162 for (path, _) in files.iter().filter(|(_, fp)| {
164 if !util::keys_contain_fingerprint(keys, fp) {
166 return false;
167 }
168
169 !store_gpg_fingerprints.contains(fp)
171 }) {
172 fs::remove_file(path).map_err(Err::SyncKeyFiles)?;
173 }
174
175 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 let proto = key.proto();
184 let config = Config::from(proto);
185 let context = contexts.get_mut(&config)?;
186
187 let path = dir.join(&fp);
189 context.export_key_file(key.clone(), &path)?;
190 }
191
192 Ok(())
195}
196
197pub fn import_missing_keys_from_store(
199 store: &Store,
200 confirm_callback: impl Fn(String) -> bool,
201) -> Result<Vec<ImportResult>> {
202 let dir = store_public_keys_dir(store);
204 if !dir.is_dir() {
205 return Ok(vec![]);
206 }
207
208 let mut contexts = ContextPool::empty();
210 let mut results = Vec::new();
211
212 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 Ok(results)
234}
235
236pub enum ImportResult {
238 Imported(String),
240
241 Unavailable(String),
243
244 Rejected(String),
246}
247
248pub trait StoreRecipients {
250 fn load(store: &Store) -> Result<Recipients>;
252
253 fn save(&self, store: &Store) -> Result<()>;
255}
256
257impl StoreRecipients for Recipients {
258 fn load(store: &Store) -> Result<Recipients> {
260 store_load_recipients(store)
261 }
262
263 fn save(&self, store: &Store) -> Result<()> {
265 store_save_recipients(store, self)
266 }
267}
268
269#[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}