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(), "video/avi".to_string(), "application-msvideo".to_string(), "image/avif".to_string(), "application/x-c2pa-manifest-store".to_string(), "image/x-adobe-dng".to_string(), "image/heic".to_string(), "image/heif".to_string(), "image/jpeg".to_string(), "audio/mp4".to_string(), "audio/mpeg".to_string(), "video/mp4".to_string(), "application/mp4".to_string(), "video/quicktime".to_string(), "application/pdf".to_string(), "image/png".to_string(), "image/svg+xml".to_string(), "image/tiff".to_string(), "audio/x-wav".to_string(), "image/webp".to_string(), ]
.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())
}
#[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> {
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,
}
}