rpgpie_certificate_store/
lib.rs

1// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Access layer for access to a
5//! [Shared OpenPGP Certificate Directory](https://crates.io/crates/openpgp-cert-d) with
6//! [rpgpie](https://crates.io/crates/rpgpie) 🦀️🔐🥧.
7//! This crate offers various modes of certificate lookup, including search by subkey fingerprint,
8//! as well as by user id.
9//!
10//! The index for this crate is stored in a crate-specific sqlite database.
11
12#![cfg_attr(docsrs, feature(doc_auto_cfg))]
13
14use std::path::{Path, PathBuf};
15
16use diesel::{query_dsl, ExpressionMethods, QueryDsl, RunQueryDsl, TextExpressionMethods};
17use openpgp_cert_d::{CertD, MergeResult};
18use rpgpie::certificate::Certificate;
19
20use crate::schema::{certs, emails, keyids, subkeys, userids};
21
22#[rustfmt::skip]
23mod schema;
24
25mod db;
26#[cfg(feature = "lookup")]
27mod lookup;
28mod model;
29mod util;
30
31/// Parse all certificates during `Store::new` (to populate the lookup tables right away)
32///
33/// FIXME: consider lazier loading methods? (e.g.: update lookup cache on "search")
34const PRELOAD: bool = true;
35
36const DB_FILENAME: &str = "_rpgpie.sqlite";
37
38/// Access to an OpenPGP Certificate Directory, including efficient lookup.
39/// Returns `rpgpie` Certificates.
40pub struct Store {
41    certd: CertD,
42    db_path: PathBuf,
43}
44
45impl Store {
46    /// A view of the default `openpgp-cert-d` instance.
47    ///
48    /// Creates the cert-d directory if it doesn't exist yet.
49    pub fn new() -> Result<Self, Error> {
50        Self::with_base_dir(CertD::user_configured_store_path()?)
51    }
52
53    /// A view of the custom `openpgp-cert-d` instance.
54    ///
55    /// Creates the cert-d directory if it doesn't exist yet.
56    pub fn with_base_dir(base: impl AsRef<Path>) -> Result<Self, Error> {
57        // create cert-d directory if it doesn't exist yet
58        let p: &Path = base.as_ref();
59        if !p.exists() {
60            std::fs::create_dir_all(p).ok();
61        }
62
63        let certd = CertD::with_base_dir(base)?;
64
65        let mut db_path = certd.base_dir().to_path_buf();
66        db_path.push(DB_FILENAME);
67
68        log::debug!("Store::new with db path {:?}", db_path);
69
70        let store = Self { certd, db_path };
71
72        // Initialize `certs` table with cache metadata.
73        //
74        // Also parse and load all (new/updated) certificate information into our lookup tables.
75        // FIXME: don't pre-parse eagerly? (at least not the preloading?)
76        store.init(PRELOAD)?;
77
78        Ok(store)
79    }
80
81    /// Insert a [Certificate] into the store.
82    ///
83    /// FIXME: this currently fails if a version of `certificate` is already present in the store.
84    pub fn insert(&self, certificate: &Certificate) -> Result<(), Error> {
85        let serialized: Vec<u8> = certificate.try_into()?;
86
87        let fp = certificate.fingerprint();
88
89        match self.certd.insert(
90            &hex::encode(fp.as_bytes()),
91            serialized,
92            false,
93            |this, old| {
94                if old.is_none() {
95                    Ok(MergeResult::Data(this))
96                } else {
97                    // FIXME
98                    log::warn!("Merging/updating is not yet implemented");
99
100                    Err(openpgp_cert_d::Error::Other(
101                        "Merging/updating is not yet implemented".into(),
102                    ))
103                }
104            },
105        ) {
106            Ok((tag, _)) => {
107                let mut conn = self.conn()?;
108                Self::cache_update(certificate, tag, &mut conn)
109            }
110            Err(e) => Err(Error::Message(format!("Database insert failed {:?}", e))),
111        }
112    }
113
114    /// Get [Certificate] by (exact) primary fingerprint
115    pub fn get_by_primary_fingerprint(
116        &self,
117        fingerprint: &str,
118    ) -> Result<Option<Certificate>, Error> {
119        let mut conn = self.conn()?;
120
121        self.get_by_primary_with_conn(fingerprint, &mut conn)
122    }
123
124    /// Get [Certificate]s by exact primary or subkey fingerprint
125    pub fn search_by_fingerprint(&self, fingerprint: &str) -> Result<Vec<Certificate>, Error> {
126        let mut res = vec![];
127
128        let mut conn = self.conn()?;
129
130        if let Ok(Some(primary)) = self.get_by_primary_with_conn(fingerprint, &mut conn) {
131            res.push(primary);
132        }
133
134        let matches = subkeys::table
135            .inner_join(certs::table)
136            .filter(subkeys::fp.eq(fingerprint.to_ascii_lowercase()))
137            .select(certs::fp)
138            .load::<String>(&mut conn)?;
139
140        matches
141            .iter()
142            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
143            .flatten()
144            .for_each(|c| res.push(c));
145
146        Ok(res)
147    }
148
149    /// Get [Certificate]s by exact primary or subkey key id
150    pub fn search_by_key_id(&self, key_id: &str) -> Result<Vec<Certificate>, Error> {
151        let mut conn = self.conn()?;
152
153        let matches = keyids::table
154            .inner_join(certs::table)
155            .filter(keyids::keyid.eq(key_id.to_ascii_lowercase()))
156            .select(certs::fp)
157            .load::<String>(&mut conn)?;
158
159        let res = matches
160            .iter()
161            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
162            .flatten()
163            .collect();
164
165        Ok(res)
166    }
167
168    /// Get [Certificate]s by exact Email string
169    ///
170    /// (Returns only exact matches for the email, e.g. `alice@example.org`)
171    pub fn search_by_email(&self, email: &str) -> Result<Vec<Certificate>, Error> {
172        let mut conn = self.conn()?;
173
174        let matches = emails::table
175            .inner_join(certs::table)
176            .filter(emails::email.eq(email))
177            .select(certs::fp)
178            .load::<String>(&mut conn)?;
179
180        let res = matches
181            .iter()
182            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
183            .flatten()
184            .collect();
185
186        Ok(res)
187    }
188
189    /// Get [Certificate]s by exact User ID string
190    ///
191    /// (Returns only exact matches for the full User ID string, e.g. `Alice <alice@example.org>`)
192    pub fn search_exact_user_id(&self, user_id: &str) -> Result<Vec<Certificate>, Error> {
193        let mut conn = self.conn()?;
194
195        let matches = userids::table
196            .inner_join(certs::table)
197            .filter(userids::userid.eq(user_id))
198            .select(certs::fp)
199            .load::<String>(&mut conn)?;
200
201        let res = matches
202            .iter()
203            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
204            .flatten()
205            .collect();
206
207        Ok(res)
208    }
209
210    /// Get [Certificate]s by SQL match on the User ID string
211    ///
212    /// The lookup mechanism compares using SQLite's "LIKE" mechanism.
213    /// So the `match` string must contain `%` or similar, for partial matches.
214    pub fn search_like_user_id(&self, like: &str) -> Result<Vec<Certificate>, Error> {
215        let mut conn = self.conn()?;
216
217        let matches = query_dsl::methods::GroupByDsl::group_by(
218            userids::table
219                .inner_join(certs::table)
220                .filter(userids::userid.like(like))
221                .select(certs::fp),
222            certs::id,
223        )
224        .load::<String>(&mut conn)?;
225
226        let res = matches
227            .iter()
228            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
229            .flatten()
230            .collect();
231
232        Ok(res)
233    }
234
235    /// Lookup certificates from network (keyservers).
236    ///
237    /// Network lookups are implicitly rate limited to "once per day".
238    ///
239    /// FIXME: combine with db lookup (this clarifies the type of ident!)
240    #[cfg(feature = "lookup")]
241    pub fn poll(&self, idents: &[String]) -> Result<Vec<Certificate>, Error> {
242        let mut conn = self.conn()?;
243
244        Ok(lookup::get_keyservers(idents, &mut conn))
245    }
246}
247
248/// Enum wrapper for the error types of this crate
249#[derive(thiserror::Error, Debug)]
250#[non_exhaustive]
251pub enum Error {
252    #[error("rpgpie error: {0}")]
253    Rpgpie(rpgpie::Error),
254
255    #[error("Database error")]
256    Database(diesel::result::Error),
257
258    #[error("cert-d error: {0}")]
259    Certd(openpgp_cert_d::Error),
260
261    #[error("Internal error: {0}")]
262    Message(String),
263}
264
265impl From<rpgpie::Error> for Error {
266    fn from(value: rpgpie::Error) -> Self {
267        Error::Rpgpie(value)
268    }
269}
270
271impl From<diesel::result::Error> for Error {
272    fn from(value: diesel::result::Error) -> Self {
273        Error::Database(value)
274    }
275}
276
277impl From<openpgp_cert_d::Error> for Error {
278    fn from(value: openpgp_cert_d::Error) -> Self {
279        Error::Certd(value)
280    }
281}