rpgpie-certificate-store 0.0.2

Certificate store for rpgpie, based on openpgp-cert-d
Documentation
// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Access layer for access to a
//! [Shared OpenPGP Certificate Directory](https://crates.io/crates/openpgp-cert-d) with
//! [rpgpie](https://crates.io/crates/rpgpie) 🦀️🔐🥧.
//! This crate offers various modes of certificate lookup, including search by subkey fingerprint,
//! as well as by user id.
//!
//! The index for this crate is stored in a crate-specific sqlite database.

use std::path::{Path, PathBuf};

use diesel::{ExpressionMethods, query_dsl, QueryDsl, RunQueryDsl, TextExpressionMethods};
use openpgp_cert_d::{CertD, MergeResult};
use rpgpie::key::Certificate;

use crate::schema::{certs, emails, keyids, subkeys, userids};

#[rustfmt::skip]
mod schema;

mod db;
mod model;
mod util;

/// Parse all certificates during `Store::new` (to populate the lookup tables right away)
///
/// FIXME: consider lazier loading methods? (e.g.: update lookup cache on "search")
const PRELOAD: bool = true;

const DB_FILENAME: &str = "_rpgpie.sqlite";

/// Access to an OpenPGP Certificate Directory, including efficient lookup.
/// Returns `rpgpie` Certificates.
pub struct Store {
    certd: CertD,
    db_path: PathBuf,
}

impl Store {
    /// A view of the default `openpgp-cert-d` instance.
    pub fn new() -> openpgp_cert_d::Result<Self> {
        Self::with_base_dir(CertD::user_configured_store_path()?)
    }

    /// A view of the custom `openpgp-cert-d` instance.
    pub fn with_base_dir(base: impl AsRef<Path>) -> openpgp_cert_d::Result<Self> {
        let certd = CertD::with_base_dir(base)?;

        let mut db_path = certd.base_dir().to_path_buf();
        db_path.push(DB_FILENAME);

        log::debug!("Store::new with db path {:?}", db_path);

        let store = Self { certd, db_path };

        // Initialize `certs` table with cache metadata.
        //
        // Also parse and load all (new/updated) certificate information into our lookup tables.
        // FIXME: don't pre-parse eagerly? (at least not the preloading?)
        store.init(PRELOAD);

        Ok(store)
    }

    /// Insert a [Certificate] into the store.
    ///
    /// FIXME: this currently fails if a version of `certificate` is already present in the store.
    pub fn insert(&self, certificate: &Certificate) -> openpgp_cert_d::Result<()> {
        let serialized: Vec<u8> = certificate.try_into().expect("fixme");

        let fp = certificate.fingerprint();

        if let Ok((tag, _)) = self
            .certd
            .insert(&hex::encode(fp), serialized, false, |this, old| {
                if old.is_none() {
                    Ok(MergeResult::Data(this))
                } else {
                    panic!("merging/updating is not yet implemented");
                }
            })
        {
            let mut conn = self.conn();

            Self::cache_update(certificate, tag, &mut conn);
        } else {
            todo!();
        }

        Ok(())
    }

    /// Get [Certificate] by (exact) primary fingerprint
    pub fn get_by_primary_fingerprint(
        &self,
        fingerprint: &str,
    ) -> openpgp_cert_d::Result<Option<Certificate>> {
        let mut conn = self.conn();

        self.get_by_primary_with_conn(fingerprint, &mut conn)
    }

    /// Get [Certificate]s by exact primary or subkey fingerprint
    pub fn search_by_fingerprint(
        &self,
        fingerprint: &str,
    ) -> openpgp_cert_d::Result<Vec<Certificate>> {
        let mut res = vec![];

        let mut conn = self.conn();

        if let Ok(Some(primary)) = self.get_by_primary_with_conn(fingerprint, &mut conn) {
            res.push(primary);
        }

        let matches = subkeys::table
            .inner_join(certs::table)
            .filter(subkeys::fp.eq(fingerprint.to_ascii_lowercase()))
            .select(certs::fp)
            .load::<String>(&mut conn)
            .expect("foo");

        matches
            .iter()
            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
            .flatten()
            .for_each(|c| res.push(c));

        Ok(res)
    }

    /// Get [Certificate]s by exact primary or subkey key id
    pub fn search_by_key_id(&self, key_id: &str) -> openpgp_cert_d::Result<Vec<Certificate>> {
        let mut conn = self.conn();

        let matches = keyids::table
            .inner_join(certs::table)
            .filter(keyids::keyid.eq(key_id.to_ascii_lowercase()))
            .select(certs::fp)
            .load::<String>(&mut conn)
            .expect("foo");

        let res = matches
            .iter()
            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
            .flatten()
            .collect();

        Ok(res)
    }

    /// Get [Certificate]s by exact Email string
    ///
    /// (Returns only exact matches for the email, e.g. `alice@example.org`)
    pub fn search_by_email(&self, email: &str) -> openpgp_cert_d::Result<Vec<Certificate>> {
        let mut conn = self.conn();

        let matches = emails::table
            .inner_join(certs::table)
            .filter(emails::email.eq(email))
            .select(certs::fp)
            .load::<String>(&mut conn)
            .expect("foo");

        let res = matches
            .iter()
            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
            .flatten()
            .collect();

        Ok(res)
    }

    /// Get [Certificate]s by exact User ID string
    ///
    /// (Returns only exact matches for the full User ID string, e.g. `Alice <alice@example.org>`)
    pub fn search_exact_user_id(&self, user_id: &str) -> openpgp_cert_d::Result<Vec<Certificate>> {
        let mut conn = self.conn();

        let matches = userids::table
            .inner_join(certs::table)
            .filter(userids::userid.eq(user_id))
            .select(certs::fp)
            .load::<String>(&mut conn)
            .expect("foo");

        let res = matches
            .iter()
            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
            .flatten()
            .collect();

        Ok(res)
    }

    /// Get [Certificate]s by SQL match on the User ID string
    ///
    /// The lookup mechanism compares using SQLite's "LIKE" mechanism.
    /// So the `match` string must contain `%` or similar, for partial matches.
    pub fn search_like_user_id(&self, like: &str) -> openpgp_cert_d::Result<Vec<Certificate>> {
        let mut conn = self.conn();

        let matches = query_dsl::methods::GroupByDsl::group_by(
            userids::table
                .inner_join(certs::table)
                .filter(userids::userid.like(like))
                .select(certs::fp),
            certs::id,
        )
        .load::<String>(&mut conn)
        .expect("foo");

        let res = matches
            .iter()
            .flat_map(|fp| self.get_by_primary_with_conn(fp, &mut conn))
            .flatten()
            .collect();

        Ok(res)
    }
}