proofmode 0.9.0

Capture, share, and preserve verifiable photos and videos
Documentation
use crate::generate_error::{ProofModeError, Result};

#[cfg(feature = "sequoia-openpgp")]
mod sequoia_impl {
    use super::*;
    use openpgp::crypto::Password;
    use openpgp::parse::{stream::*, Parse};
    use openpgp::policy::StandardPolicy;
    use openpgp::serialize::stream::{Armorer, Message, Signer};
    use openpgp::serialize::Serialize;
    use openpgp::types::HashAlgorithm;
    use openpgp::Cert;
    use sequoia_openpgp as openpgp;
    use std::io::Write;

    pub struct PgpUtils {
        cert: Option<Cert>,
    }

    impl Default for PgpUtils {
        fn default() -> Self {
            Self::new()
        }
    }

    impl PgpUtils {
        pub fn new() -> Self {
            Self { cert: None }
        }

        pub fn generate_keys(&mut self, email: &str, passphrase: &str) -> Result<()> {
            let (cert, _) = openpgp::cert::CertBuilder::new()
                .add_userid(email)
                .set_creation_time(std::time::SystemTime::now())
                .add_signing_subkey()
                .set_password(Some(passphrase.into()))
                .generate()
                .map_err(|e| ProofModeError::Pgp(e.to_string()))?;

            self.cert = Some(cert);

            Ok(())
        }

        pub fn get_public_key_string(&self) -> Result<String> {
            match &self.cert {
                Some(cert) => {
                    let mut buf = Vec::new();
                    cert.as_tsk()
                        .armored()
                        .serialize(&mut buf)
                        .map_err(|e| ProofModeError::Pgp(e.to_string()))?;

                    Ok(String::from_utf8_lossy(&buf).to_string())
                }
                None => Err(ProofModeError::Crypto(
                    "No certificate available".to_string(),
                )),
            }
        }

        pub fn sign_data(&self, data: &[u8], passphrase: &str) -> Result<Vec<u8>> {
            match &self.cert {
                Some(cert) => {
                    let mut output = Vec::new();
                    let policy = StandardPolicy::new();

                    let signing_keypair = cert
                        .keys()
                        .secret()
                        .with_policy(&policy, None)
                        .supported()
                        .for_signing()
                        .alive()
                        .revoked(false)
                        .next()
                        .ok_or_else(|| {
                            ProofModeError::Crypto("No signing key available".to_string())
                        })?;

                    let key = signing_keypair.key();
                    let keypair = key
                        .clone()
                        .decrypt_secret(&Password::from(passphrase))
                        .map_err(|e| ProofModeError::Pgp(e.to_string()))?
                        .into_keypair()
                        .map_err(|e| ProofModeError::Pgp(e.to_string()))?;

                    {
                        let message = Message::new(&mut output);
                        let message = Armorer::new(message)
                            .kind(openpgp::armor::Kind::Signature)
                            .build()
                            .map_err(|e| ProofModeError::Pgp(e.to_string()))?;

                        let mut signer = Signer::new(message, keypair)
                            .map_err(|e: anyhow::Error| ProofModeError::Pgp(e.to_string()))?
                            .detached()
                            .hash_algo(HashAlgorithm::SHA256)
                            .map_err(|e: anyhow::Error| ProofModeError::Pgp(e.to_string()))?
                            .build()
                            .map_err(|e: anyhow::Error| ProofModeError::Pgp(e.to_string()))?;

                        signer
                            .write_all(data)
                            .map_err(|e: std::io::Error| ProofModeError::Io(e.to_string()))?;

                        signer
                            .finalize()
                            .map_err(|e: anyhow::Error| ProofModeError::Pgp(e.to_string()))?;
                    }

                    Ok(output)
                }
                None => Err(ProofModeError::Crypto(
                    "No certificate available".to_string(),
                )),
            }
        }

        pub fn verify_signature(&self, data: &[u8], signature: &[u8]) -> Result<bool> {
            match &self.cert {
                Some(cert) => {
                    use std::io::Cursor;

                    struct Helper<'a> {
                        cert: &'a Cert,
                    }

                    impl VerificationHelper for Helper<'_> {
                        fn get_certs(
                            &mut self,
                            _ids: &[openpgp::KeyHandle],
                        ) -> openpgp::Result<Vec<Cert>> {
                            Ok(vec![self.cert.clone()])
                        }

                        fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> {
                            use openpgp::parse::stream::MessageLayer;
                            for layer in structure.iter() {
                                if let MessageLayer::SignatureGroup { results } = layer {
                                    for result in results {
                                        if result.is_ok() {
                                            return Ok(());
                                        }
                                    }
                                }
                            }
                            Err(anyhow::anyhow!("No valid signatures found"))
                        }
                    }

                    let policy = StandardPolicy::new();
                    let helper = Helper { cert };

                    let signature_reader = Cursor::new(signature);
                    let data_reader = Cursor::new(data);

                    openpgp::parse::stream::DetachedVerifierBuilder::from_reader(signature_reader)
                        .map_err(|e| ProofModeError::Pgp(e.to_string()))?
                        .with_policy(&policy, None, helper)
                        .map_err(|e| ProofModeError::Pgp(e.to_string()))?
                        .verify_reader(data_reader)
                        .map_err(|e| ProofModeError::Pgp(e.to_string()))?;

                    Ok(true)
                }
                None => Err(ProofModeError::Crypto(
                    "No certificate available".to_string(),
                )),
            }
        }

        pub fn load_keys(&mut self, secret_key_armor: &str, _passphrase: &str) -> Result<()> {
            let cert = Cert::from_reader(secret_key_armor.as_bytes())
                .map_err(|e| ProofModeError::Pgp(e.to_string()))?;

            self.cert = Some(cert);

            Ok(())
        }
    }
}

#[cfg(feature = "sequoia-openpgp")]
pub use sequoia_impl::PgpUtils;

#[cfg(not(feature = "sequoia-openpgp"))]
pub struct PgpUtils;

#[cfg(not(feature = "sequoia-openpgp"))]
impl PgpUtils {
    pub fn new() -> Self {
        Self
    }

    pub fn generate_keys(&mut self, _email: &str, _passphrase: &str) -> Result<()> {
        Err(ProofModeError::Crypto(
            "PGP functionality not available in WASM builds".to_string(),
        ))
    }

    pub fn get_public_key_string(&self) -> Result<String> {
        Err(ProofModeError::Crypto(
            "PGP functionality not available in WASM builds".to_string(),
        ))
    }

    pub fn sign_data(&self, _data: &[u8], _passphrase: &str) -> Result<Vec<u8>> {
        Err(ProofModeError::Crypto(
            "PGP functionality not available in WASM builds".to_string(),
        ))
    }

    pub fn verify_signature(&self, _data: &[u8], _signature: &[u8]) -> Result<bool> {
        Err(ProofModeError::Crypto(
            "PGP functionality not available in WASM builds".to_string(),
        ))
    }

    pub fn load_keys(&mut self, _secret_key_armor: &str, _passphrase: &str) -> Result<()> {
        Err(ProofModeError::Crypto(
            "PGP functionality not available in WASM builds".to_string(),
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pgp_utils_new() {
        let pgp = PgpUtils::new();
        // Should not panic and create instance
        assert!(true);
    }

    #[cfg(feature = "sequoia-openpgp")]
    #[test]
    fn test_generate_keys() {
        let mut pgp = PgpUtils::new();
        let result = pgp.generate_keys("test@example.com", "testpassword");
        assert!(result.is_ok());
    }

    #[cfg(feature = "sequoia-openpgp")]
    #[test]
    fn test_get_public_key_string_without_keys() {
        let pgp = PgpUtils::new();
        let result = pgp.get_public_key_string();
        assert!(result.is_err());
    }

    #[cfg(feature = "sequoia-openpgp")]
    #[test]
    fn test_sign_data_without_keys() {
        let pgp = PgpUtils::new();
        let data = b"test data";
        let result = pgp.sign_data(data, "password");
        assert!(result.is_err());
    }

    #[cfg(feature = "sequoia-openpgp")]
    #[test]
    fn test_full_pgp_workflow() {
        let mut pgp = PgpUtils::new();

        // Generate keys
        pgp.generate_keys("test@example.com", "testpassword")
            .unwrap();

        // Get public key
        let public_key = pgp.get_public_key_string();
        assert!(public_key.is_ok());
        let public_key = public_key.unwrap();
        assert!(public_key.contains("-----BEGIN PGP"));

        // Sign data
        let data = b"test message to sign";
        let signature = pgp.sign_data(data, "testpassword");
        assert!(signature.is_ok());
        let signature = signature.unwrap();
        assert!(!signature.is_empty());

        // Verify signature
        let verification = pgp.verify_signature(data, &signature);
        assert!(verification.is_ok());
        assert!(verification.unwrap());
    }

    #[cfg(not(feature = "sequoia-openpgp"))]
    #[test]
    fn test_wasm_pgp_fallback() {
        let mut pgp = PgpUtils::new();

        // All operations should return errors in WASM builds
        assert!(pgp.generate_keys("test@example.com", "password").is_err());
        assert!(pgp.get_public_key_string().is_err());
        assert!(pgp.sign_data(b"data", "password").is_err());
        assert!(pgp.verify_signature(b"data", b"signature").is_err());
        assert!(pgp.load_keys("key", "password").is_err());
    }
}