proofmode 0.9.0

Capture, share, and preserve verifiable photos and videos
Documentation
use crate::check::preprocess::{InputProofFile, PGPKey, ProofCheckFile};
#[cfg(feature = "c2pa")]
use c2pa::Reader;
use opentimestamps::DetachedTimestampFile;
use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey};
use pgp::types::KeyDetails;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::io::Cursor;

#[cfg(feature = "c2pa")]
fn is_supported_c2pa_mime_type(mime_type: String) -> bool {
    vec![
        "video/msvideo".to_string(),                     // avi
        "video/avi".to_string(),                         // avi
        "application-msvideo".to_string(),               // avi
        "image/avif".to_string(),                        // avif
        "application/x-c2pa-manifest-store".to_string(), // c2pa
        "image/x-adobe-dng".to_string(),                 // dng
        "image/heic".to_string(),                        // heic
        "image/heif".to_string(),                        // heif
        "image/jpeg".to_string(),                        // jpg, jpeg
        "audio/mp4".to_string(),                         // m4a
        "audio/mpeg".to_string(),                        // mp3
        "video/mp4".to_string(),                         // mp4
        "application/mp4".to_string(),                   // mp4
        "video/quicktime".to_string(),                   // mov
        "application/pdf".to_string(),                   // pdf
        "image/png".to_string(),                         // png
        "image/svg+xml".to_string(),                     // svg
        "image/tiff".to_string(),                        // tif, tiff
        "audio/x-wav".to_string(),                       // wav
        "image/webp".to_string(),                        // webp
    ]
    .contains(&mime_type)
}

fn is_supported_exif_mime_type(mime_type: String) -> bool {
    [
        "image/heif",
        "image/jpeg",
        "image/png",
        "image/tiff",
        "image/webp",
    ]
    .contains(&mime_type.as_str())
}

/*
struct InputProofFiles {
    keys: Vec<PGPKey>,
    files: Vec<InputProofFile>,
}
*/

#[derive(Serialize, Deserialize, Clone)]
struct PGPInfoDetail {
    verified: bool,
    key_id: String,
    key: Vec<u8>,
    details: String,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct PGPInfo {
    media: Option<PGPInfoDetail>,
    json: Option<PGPInfoDetail>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct C2PAInfo {
    manifest_info: Value,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct OpenTimestampsInfo {
    verified: bool,
    timestamp: String,
    digest_type: String,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct DeviceCheckInfo {
    verified: bool,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct SafetyNetInfo {
    verified: bool,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct PlayIntegrityInfo {
    verified: bool,
}

type ExifInfo = Map<std::string::String, Value>;

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct IntegrityResult {
    pub pgp: Option<PGPInfo>,
    #[cfg(feature = "c2pa")]
    pub c2pa: Option<C2PAInfo>,
    pub opentimestamps: Option<OpenTimestampsInfo>,
    pub device_check: Option<DeviceCheckInfo>,
    pub safety_net: Option<SafetyNetInfo>,
    pub play_integrity: Option<PlayIntegrityInfo>,
    pub exif: Option<ExifInfo>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct IntegrityPGP {
    media: usize,
    proof: usize,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct IntegritySummary {
    pub pgp: IntegrityPGP,
    #[cfg(feature = "c2pa")]
    pub c2pa: usize,
    pub opentimestamps: usize,
    pub device_check: usize,
    pub safety_net: usize,
}

fn check_pgp_individual(
    file_data: &Vec<u8>,
    signature_data: &Vec<u8>,
    keys: &Vec<PGPKey>,
) -> Option<PGPInfoDetail> {
    let cursor = Cursor::new(signature_data);
    let signature_info = DetachedSignature::from_armor_single(cursor);

    if let Ok(signature_info) = signature_info {
        let (signature, _) = signature_info;
        for key in keys {
            let key_data = key.key.clone();
            let key_cursor = Cursor::new(key_data.clone());
            let key_info = SignedPublicKey::from_armor_single(key_cursor);

            if let Ok(key_info) = key_info {
                let (key, _) = key_info;
                let result = signature.verify(&key, file_data.as_slice());

                if result.is_ok() {
                    return Some(PGPInfoDetail {
                        verified: true,
                        key_id: hex::encode(key.legacy_key_id().as_ref()),
                        key: key_data.clone(),
                        details: "".to_string(),
                    });
                }
            }
        }
    }

    None
}

fn check_pgp(file: &InputProofFile, keys: &Vec<PGPKey>) -> Option<PGPInfo> {
    let media = check_pgp_individual(&file.data, &file.signature, keys);
    let json = check_pgp_individual(&file.json, &file.json_signature, keys);

    if media.is_some() || json.is_some() {
        return Some(PGPInfo { media, json });
    }

    None
}

#[cfg(feature = "c2pa")]
fn check_c2pa(mime_type: String, file_data: &[u8]) -> Option<C2PAInfo> {
    if is_supported_c2pa_mime_type(mime_type.clone()) {
        let cursor = Cursor::new(file_data);
        let reader = Reader::from_stream(&mime_type, cursor);

        if let Ok(reader) = reader {
            let manifest_info: Result<Value, _> = serde_json::from_str(&reader.json());

            if let Ok(manifest_info) = manifest_info {
                return Some(C2PAInfo { manifest_info });
            }
        }
    }
    None
}

fn check_opentimestamps(file: &InputProofFile) -> Option<OpenTimestampsInfo> {
    let file_data = file.opentimestamps.clone();
    let cursor = Cursor::new(file_data);
    let ots = DetachedTimestampFile::from_reader(cursor);

    if let Ok(ots) = ots {
        return Some(OpenTimestampsInfo {
            verified: true,
            timestamp: ots.clone().timestamp.to_string(),
            digest_type: ots.clone().digest_type.to_string(),
        });
    }

    None
}

fn check_device_check() -> Option<DeviceCheckInfo> {
    None
}

fn check_safety_net(_jwt: &Vec<u8>) -> Option<SafetyNetInfo> {
    // let jwt_string = String::from_utf8(jwt).unwrap();
    // let decoded = decode::from_ed_components(&jwt_string);

    None
}

fn check_play_integrity(_file: &InputProofFile) -> Option<PlayIntegrityInfo> {
    None
}

fn check_exif(file: &InputProofFile) -> Option<ExifInfo> {
    if !is_supported_exif_mime_type(file.mime_type.clone()) {
        return None;
    }

    let data = file.data.clone();
    let cursor = Cursor::new(data);
    let mut bufreader = std::io::BufReader::new(cursor);
    let exifreader = exif::Reader::new();
    let exif = exifreader.read_from_container(&mut bufreader);

    if let Ok(exif) = exif {
        let mut exif_info = serde_json::Map::new();
        for field in exif.fields() {
            let tag = field.tag;
            let value = field.value.display_as(field.tag).to_string();
            exif_info.insert(tag.to_string(), serde_json::Value::String(value));
        }
        return Some(exif_info);
    }

    None
}

pub fn check_integrity(file: &InputProofFile, keys: &Vec<PGPKey>) -> IntegrityResult {
    IntegrityResult {
        pgp: check_pgp(file, keys),
        #[cfg(feature = "c2pa")]
        c2pa: check_c2pa(file.mime_type.clone(), &file.data),
        opentimestamps: check_opentimestamps(file),
        device_check: check_device_check(),
        safety_net: check_safety_net(&file.safety_net),
        play_integrity: check_play_integrity(file),
        exif: check_exif(file),
    }
}

pub fn get_integrity_summary(files: &[ProofCheckFile]) -> IntegritySummary {
    let pgp_media = files
        .iter()
        .filter(|f| f.integrity.pgp.clone().and_then(|p| p.media).is_some())
        .count();

    let pgp_proof = files
        .iter()
        .filter(|f| f.integrity.pgp.clone().and_then(|p| p.json).is_some())
        .count();

    #[cfg(feature = "c2pa")]
    let c2pa = files.iter().filter(|f| f.integrity.c2pa.is_some()).count();

    #[cfg(not(feature = "c2pa"))]
    let _c2pa = 0;

    let opentimestamps = files
        .iter()
        .filter(|f| f.integrity.opentimestamps.is_some())
        .count();

    let device_check = files
        .iter()
        .filter(|f| f.integrity.device_check.is_some())
        .count();

    let safety_net = files
        .iter()
        .filter(|f| f.integrity.safety_net.is_some())
        .count();

    IntegritySummary {
        pgp: IntegrityPGP {
            media: pgp_media,
            proof: pgp_proof,
        },
        #[cfg(feature = "c2pa")]
        c2pa,
        opentimestamps,
        device_check,
        safety_net,
    }
}