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 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)
}