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

use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl, SqliteConnection};
use openpgp_cert_d::Tag;
use rpgpie::key::Certificate;
use rpgpie::key::checked::CheckedCertificate;
use rpgpie::key::component::ComponentKeyPub;

use crate::model::{Cert, NewCert, NewEmail, NewKeyid, NewSubkey, NewUserid};
use crate::schema::*;
use crate::Store;

pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations =
    diesel_migrations::embed_migrations!();

impl Store {
    pub(crate) fn run_migration(conn: &mut SqliteConnection) {
        use diesel_migrations::MigrationHarness;

        conn.run_pending_migrations(MIGRATIONS).unwrap();
    }

    pub(crate) fn conn(&self) -> SqliteConnection {
        SqliteConnection::establish(self.db_path.to_str().unwrap())
            .unwrap_or_else(|_| panic!("Error connecting to {:?}", self.db_path))
    }

    /// Initialize the `certs` table with an entry for each certificate in the cert-d.
    ///
    /// These entries reflect the existence of a primary fingerprint in the cert-d, and the tag of
    /// the latest version of that certificate that has been cached in the lookup tables for subkey
    /// fingerprints and user ids.
    ///
    /// If `parse` is true, each new/updated cert will get parsed, evaluated and stored in the
    /// lookup tables.
    ///
    /// Otherwise, only the `certs` table is initialized, and reflects which certificates still
    /// need to be parsed into the lookup tables.
    pub(crate) fn init(&self, parse: bool) {
        let mut conn = self.conn();

        // initialize/update database schema
        Self::run_migration(&mut conn);

        let mut inserted = 0;

        // iterate over all certificates that the cert-d knows about
        for (fp, tag, cert) in self.certd.iter().flatten() {
            let stored_tag = match certs::table
                .filter(certs::fp.eq(&fp))
                .first::<Cert>(&mut conn)
            {
                Ok(cert) => {
                    if Some(tag.0.to_string()) != cert.cached_tag {
                        let _changed = diesel::update(certs::table)
                            .filter(certs::id.eq(cert.id))
                            .set(certs::needs_cache_update.eq(true))
                            .execute(&mut conn)
                            .expect("update");
                    }

                    cert.cached_tag
                }
                Err(_e) => {
                    let c = NewCert {
                        fp: &fp,
                        cached_tag: None,
                        needs_cache_update: true,
                    };

                    // create new rows for fingerprints that aren't yet in the DB
                    let i = diesel::insert_into(certs::table)
                        .values(&c)
                        .execute(&mut conn)
                        .expect("insert");

                    inserted += i;

                    None
                }
            };

            if parse && stored_tag != Some(tag.0.to_string()) {
                log::debug!("preload {}", &fp);

                let cert = Certificate::try_from(cert.as_slice()).expect("foo");
                Self::cache_update(&cert, tag, &mut conn);
            }
        }

        log::debug!("inserted {inserted} new rows into cache status table");
    }

    // Store information for a certificate in the lookup tables (subkey fingerprints, key ids,
    // user ids, emails), and update the entry for `cert` in the certs table.
    pub(crate) fn cache_update(cert: &Certificate, tag: Tag, conn: &mut SqliteConnection) {
        // get current cache status entry
        let pri = hex::encode(cert.fingerprint());
        log::debug!("put cache cert: {}", pri);

        let mut cache = certs::table
            .filter(certs::fp.eq(&pri))
            .load::<Cert>(conn)
            .expect("foo");

        if cache.len() == 1 {
            let cache = &mut cache[0];

            // FIXME: compare tag?
            if cache.needs_cache_update {
                let ccert: CheckedCertificate = cert.into();

                // insert primary key id
                let keyid = &pri[24..40]; // FIXME: get key_id calculated by rpgpie
                let keyid = NewKeyid {
                    keyid,
                    cert_id: cache.id,
                };
                let _ = diesel::insert_into(keyids::table)
                    .values(keyid)
                    .on_conflict_do_nothing()
                    .execute(conn)
                    .expect("insert");

                // insert subkey fingerprints for lookup
                for subkey in ccert.subkeys() {
                    let fp = ComponentKeyPub::from(subkey).fingerprint();
                    let fp = hex::encode(fp);

                    let c = NewSubkey {
                        fp: &fp,
                        cert_id: cache.id,
                    };

                    let _ = diesel::insert_into(subkeys::table)
                        .values(&c)
                        .on_conflict_do_nothing()
                        .execute(conn)
                        .expect("insert");

                    // insert subkey key id
                    let keyid = &fp[24..40]; // FIXME: get key_id calculated by rpgpie
                    let keyid = NewKeyid {
                        keyid,
                        cert_id: cache.id,
                    };
                    let _ = diesel::insert_into(keyids::table)
                        .values(keyid)
                        .on_conflict_do_nothing()
                        .execute(conn)
                        .expect("insert");
                }

                for userid in ccert.user_ids() {
                    let uid = userid.id.id().to_string();

                    let u = NewUserid {
                        userid: &uid,
                        cert_id: cache.id,
                    };

                    let _ = diesel::insert_into(userids::table)
                        .values(&u)
                        .on_conflict_do_nothing()
                        .execute(conn)
                        .expect("insert");

                    if let Some(e) = crate::util::email_for_userid(&uid) {
                        let e = NewEmail {
                            email: &e,
                            cert_id: cache.id,
                        };

                        let _ = diesel::insert_into(emails::table)
                            .values(&e)
                            .on_conflict_do_nothing()
                            .execute(conn)
                            .expect("insert");
                    }
                }

                // update cache entry, set tag + cache=ok
                cache.cached_tag = Some(tag.0.to_string());
                cache.needs_cache_update = false;

                diesel::update(&*cache)
                    .set(&*cache)
                    .execute(conn)
                    .expect("fixme");
            }
        } else {
            log::debug!("skipping {}", pri);
        }
    }

    // Unpack cert-d representation into a Certificate, and update our lookup cache, if required
    fn unpack_cert_d_repr(&self, c: &[u8], tag: Tag, conn: &mut SqliteConnection) -> Certificate {
        let cert = Certificate::try_from(c).expect("FIXME");

        Self::cache_update(&cert, tag, conn);

        cert
    }

    pub(crate) fn get_by_primary_with_conn(
        &self,
        fingerprint: &str,
        conn: &mut SqliteConnection,
    ) -> openpgp_cert_d::Result<Option<Certificate>> {
        let Some((tag, c)) = self.certd.get(fingerprint)? else {
            return Ok(None);
        };

        // while we're handling this certificate, make sure it's up-to-date in our lookup cache
        let cert = self.unpack_cert_d_repr(&c, tag, conn);

        Ok(Some(cert))
    }
}