proofmode 0.9.0

Capture, share, and preserve verifiable photos and videos
Documentation
use crate::check::integrity::{check_integrity, IntegrityResult};
use crate::check::translate::{translate_to_latest_schema, ProofModeJSON};
use ::zip::ZipArchive;

use pgp::composed::{Deserializable, SignedPublicKey};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::io::{Cursor, Read, Seek};

#[cfg(feature = "wasm")]
use wasm_bindgen_file_reader::WebSysFile;

pub trait ReadFile: Read + Seek {}
impl<T: Read + Seek> ReadFile for T {}

pub trait RawFile {
    fn name(&self) -> String;
    fn error(&self) -> Option<String>;
    fn data(&mut self) -> &mut dyn ReadFile;
}

pub struct FetchFile {
    pub name: String,
    pub error: Option<String>,
    pub data: Cursor<Vec<u8>>,
}

impl RawFile for FetchFile {
    fn name(&self) -> String {
        self.name.clone()
    }

    fn error(&self) -> Option<String> {
        self.error.clone()
    }

    fn data(&mut self) -> &mut dyn ReadFile {
        &mut self.data
    }
}

#[cfg(feature = "wasm")]
pub struct WebFile {
    pub name: String,
    pub error: Option<String>,
    pub data: WebSysFile,
}

#[cfg(feature = "wasm")]
impl RawFile for WebFile {
    fn name(&self) -> String {
        self.name.clone()
    }

    fn error(&self) -> Option<String> {
        self.error.clone()
    }

    fn data(&mut self) -> &mut dyn ReadFile {
        &mut self.data
    }
}

pub struct LocalFile {
    pub name: String,
    pub error: Option<String>,
    pub data: Cursor<Vec<u8>>,
}

impl RawFile for LocalFile {
    fn name(&self) -> String {
        self.name.clone()
    }

    fn error(&self) -> Option<String> {
        self.error.clone()
    }

    fn data(&mut self) -> &mut dyn ReadFile {
        &mut self.data
    }
}

#[derive(Serialize, Deserialize, Clone)]
pub struct InputFiles {
    files: Vec<InputFile>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct InputFile {
    pub name: String,
    pub data: Vec<u8>,
    pub hash: String,
    pub mime_type: String,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ProofCheckFile {
    pub name: String,
    pub hash: String,
    pub mime_type: String,
    pub data: Vec<u8>,
    pub json: Option<ProofModeJSON>,
    pub integrity: IntegrityResult,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ProofCheckError {
    pub name: String,
    pub error: String,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct PGPKey {
    pub key: Vec<u8>,
}

#[derive(Serialize, Deserialize, Clone)]
struct InputProofFiles {
    keys: Vec<PGPKey>,
    files: Vec<InputProofFile>,
}

#[derive(Serialize, Deserialize, Clone)]
pub struct InputProofFile {
    pub name: String,
    pub hash: String,
    pub mime_type: String,
    pub data: Vec<u8>,
    pub signature: Vec<u8>,
    pub json: Vec<u8>,
    pub json_signature: Vec<u8>,
    pub opentimestamps: Vec<u8>,
    pub device_check: Vec<u8>,
    pub safety_net: Vec<u8>,
}

fn get_mime_type(file_name: &str, file_data: &[u8]) -> String {
    let file_name = file_name.to_lowercase();
    if file_name.ends_with(".heic") {
        return "image/heic".to_string();
    } else if file_name.ends_with(".heif") {
        return "image/heif".to_string();
    } else if file_name.ends_with(".ots") {
        return "application/opentimestamps".to_string();
    } else if file_name.ends_with(".devicecheck") {
        return "application/devicecheck".to_string();
    } else if file_name.ends_with(".asc")
        && (file_name.ends_with("pubkey.asc")
            || SignedPublicKey::from_armor_single(Cursor::new(file_data)).is_ok())
    {
        return "application/pgp-keys".to_string();
    }

    mime_guess::from_path(file_name)
        .first_or_octet_stream()
        .to_string()
}

fn calculate_hash(file_data: &[u8]) -> String {
    let mut hasher = Sha256::new();
    hasher.update(file_data);
    let result = hasher.finalize();
    format!("{:x}", result)
}

fn extract_zip_files<T: RawFile>(mut file: T) -> Vec<InputFile> {
    let data = file.data();
    let mut archive = ZipArchive::new(data).unwrap();
    let mut input_files = Vec::new();
    let len = archive.len();
    let mut i = 0;
    while i < len {
        let mut entry = archive.by_index(i).unwrap();
        let mut data = Vec::new();
        entry.read_to_end(&mut data).unwrap();
        // let length_string = format!("{} bytes", data.len());
        let path = entry.enclosed_name().unwrap();
        if path.to_str().unwrap().ends_with('/') || path.to_str().unwrap().starts_with("__MACOSX") {
            i += 1;
            continue;
        }
        let path_string = path.to_str().unwrap();
        let mime_type = get_mime_type(path_string, &data);
        let hash = calculate_hash(&data);
        let input = InputFile {
            name: path_string.to_string(),
            data,
            hash,
            mime_type,
        };
        input_files.push(input);
        i += 1;
    }
    input_files
}

fn is_media(mime_type: &str) -> bool {
    mime_type.starts_with("image")
        || mime_type.starts_with("video")
        || mime_type.starts_with("audio")
}

fn is_pgp_key(mime_type: &str) -> bool {
    mime_type == "application/pgp-keys"
}

fn group_files(input_files: InputFiles) -> InputProofFiles {
    let mut sorted_files: Vec<InputFile> = input_files.files.clone();
    sorted_files.sort_by(|a, b| {
        if is_media(&a.mime_type) && !is_media(&b.mime_type) {
            std::cmp::Ordering::Less
        } else if !is_media(&a.mime_type) && is_media(&b.mime_type) {
            std::cmp::Ordering::Greater
        } else {
            std::cmp::Ordering::Equal
        }
    });
    let mut public_keys = Vec::new();
    let mut proof_files = Vec::new();
    let mut file_map = HashMap::new();
    for input_file in sorted_files {
        let mime_type = input_file.mime_type;
        if is_media(&mime_type) {
            let proof_file = InputProofFile {
                name: input_file.name.clone(),
                hash: input_file.hash.clone(),
                mime_type,
                data: input_file.data,
                signature: Vec::new(),
                json: Vec::new(),
                json_signature: Vec::new(),
                opentimestamps: Vec::new(),
                device_check: Vec::new(),
                safety_net: Vec::new(),
            };

            file_map.insert(input_file.hash.clone(), proof_file);
        } else {
            let file_name = input_file.name.clone();
            let file_hash = file_name.split('.').next().unwrap().to_string();
            if is_pgp_key(&mime_type) {
                let key = PGPKey {
                    key: input_file.data.clone(),
                };
                public_keys.push(key);
            } else if file_map.contains_key(&file_hash) {
                let proof_file = file_map.get_mut(&file_hash).unwrap();
                let file_signature = format!("{}.asc", file_hash);
                if file_name.ends_with(".json") {
                    proof_file.json = input_file.data.clone();
                } else if file_name.ends_with(".json.asc") {
                    proof_file.json_signature = input_file.data.clone();
                } else if file_name.ends_with(".ots") {
                    proof_file.opentimestamps = input_file.data.clone();
                } else if file_name.ends_with(".devicecheck") {
                    proof_file.device_check = input_file.data.clone();
                } else if file_name.ends_with(".safetynet") {
                    proof_file.safety_net = input_file.data.clone();
                } else if file_name.ends_with(file_signature.as_str()) {
                    proof_file.signature = input_file.data.clone();
                }
            }
        }
    }

    for (_hash, proof_file) in file_map {
        proof_files.push(proof_file);
    }

    InputProofFiles {
        keys: public_keys,
        files: proof_files,
    }
}

fn extract_input_files<T: RawFile>(mut file: T) -> Vec<InputFile> {
    let name = file.name();
    if name.ends_with(".zip") {
        return extract_zip_files(file);
    }

    let mut data = Vec::new();
    file.data().read_to_end(&mut data).unwrap();
    let mime_type = get_mime_type(&name, &data);
    let hash = calculate_hash(&data);
    let input = InputFile {
        name,
        data,
        hash,
        mime_type,
    };
    vec![input]
}

pub fn prepare_files<T: RawFile>(files: Vec<T>) -> (Vec<ProofCheckFile>, Vec<PGPKey>) {
    let mut input_files = vec![];
    for file in files {
        if file.error().is_none() {
            let extracted_files = extract_input_files(file);
            input_files.extend(extracted_files);
        }
    }
    let all_input_files = InputFiles { files: input_files };
    let grouped_files = group_files(all_input_files);
    let keys = grouped_files.keys;
    let proof_files: Vec<ProofCheckFile> = grouped_files
        .files
        .iter()
        .map(|f| ProofCheckFile {
            name: f.name.clone(),
            mime_type: f.mime_type.clone(),
            hash: f.hash.clone(),
            data: f.data.clone(),
            json: translate_to_latest_schema(&f.json),
            integrity: check_integrity(f, &keys),
        })
        .collect();

    (proof_files, keys)
}