use crate::crypto::hash::calculate_hash;
use crate::generate_error::Result;
use crate::generate_types::*;
use chrono::Utc;
use std::collections::HashMap;
pub trait PlatformCallbacks {
fn get_device_info(&self) -> Option<DeviceData>;
fn get_location_info(&self) -> Option<LocationData>;
fn get_network_info(&self) -> Option<NetworkData>;
fn save_data(&self, hash: &str, filename: &str, data: &[u8]) -> Result<()>;
fn save_text(&self, hash: &str, filename: &str, text: &str) -> Result<()>;
fn sign_data(&self, data: &[u8]) -> Result<Option<Vec<u8>>>;
fn notarize_hash(&self, hash: &str) -> Result<Option<NotarizationData>>;
fn report_progress(&self, message: &str);
}
pub struct ProofGenerator {
config: ProofModeConfig,
}
impl ProofGenerator {
pub fn new(config: ProofModeConfig) -> Self {
Self { config }
}
pub fn generate_proof<C: PlatformCallbacks>(
&self,
media_data: &[u8],
metadata: HashMap<String, String>,
callbacks: &C,
) -> Result<String> {
let hash = calculate_hash(media_data);
let proof_data = self.create_proof_data(&hash, metadata.clone(), callbacks)?;
self.save_proof_files(&hash, &proof_data, callbacks)?;
#[cfg(all(feature = "c2pa", not(target_arch = "wasm32")))]
if self.config.embed_c2pa {
match crate::generate::c2pa::embed_and_save_c2pa(
media_data,
&hash,
&proof_data,
&metadata,
callbacks,
) {
Ok(ext) => {
callbacks.report_progress(&format!(
"C2PA manifest embedded successfully (.c2pa.{})",
ext
));
}
Err(e) => {
callbacks.report_progress(&format!("C2PA embedding failed: {}", e));
}
}
}
Ok(hash)
}
fn create_proof_data<C: PlatformCallbacks>(
&self,
hash: &str,
mut metadata: HashMap<String, String>,
callbacks: &C,
) -> Result<ProofData> {
let now = Utc::now();
if self.config.track_device_id {
if let Some(device_data) = callbacks.get_device_info() {
metadata.insert(
"device_manufacturer".to_string(),
device_data.manufacturer.clone(),
);
metadata.insert("device_model".to_string(), device_data.model.clone());
metadata.insert("device_os".to_string(), device_data.os_version.clone());
if let Some(id) = &device_data.device_id {
metadata.insert("device_id".to_string(), id.clone());
}
}
}
if self.config.track_network {
if let Some(network_data) = callbacks.get_network_info() {
metadata.insert(
"network_type".to_string(),
network_data.network_type.clone(),
);
if let Some(ssid) = &network_data.wifi_ssid {
metadata.insert("wifi_ssid".to_string(), ssid.clone());
}
}
}
let location = if self.config.track_location {
callbacks.get_location_info()
} else {
None
};
let device = if self.config.track_device_id {
callbacks.get_device_info()
} else {
None
};
let network = if self.config.track_network {
callbacks.get_network_info()
} else {
None
};
let timestamps = TimestampData {
created_at: now,
modified_at: None,
proof_generated_at: now,
};
let signature = if self.config.add_credentials {
let proof_json = serde_json::to_string(&ProofData {
file_hash_sha256: hash.to_string(),
metadata: metadata.clone(),
location: location.clone(),
device: device.clone(),
network: network.clone(),
timestamps: timestamps.clone(),
signature: None,
notarization: None,
})?;
match callbacks.sign_data(proof_json.as_bytes()) {
Ok(Some(sig)) => Some(hex::encode(sig)),
Ok(None) => None,
Err(_) => None,
}
} else {
None
};
let notarization = if self.config.auto_notarize {
callbacks.notarize_hash(hash).unwrap_or_default()
} else {
None
};
Ok(ProofData {
file_hash_sha256: hash.to_string(),
metadata,
location,
device,
network,
timestamps,
signature,
notarization,
})
}
fn save_proof_files<C: PlatformCallbacks>(
&self,
hash: &str,
proof_data: &ProofData,
callbacks: &C,
) -> Result<()> {
let proof_json = serde_json::to_string_pretty(proof_data)?;
callbacks.save_text(
hash,
&format!("{}{}", hash, PROOF_FILE_JSON_TAG),
&proof_json,
)?;
let csv_data = self.proof_to_csv(proof_data)?;
callbacks.save_text(hash, &format!("{}{}", hash, PROOF_FILE_TAG), &csv_data)?;
if let Some(signature) = &proof_data.signature {
if let Ok(sig_bytes) = hex::decode(signature) {
callbacks.save_data(hash, &format!("{}{}", hash, OPENPGP_FILE_TAG), &sig_bytes)?;
}
}
Ok(())
}
fn proof_to_csv(&self, proof_data: &ProofData) -> Result<String> {
let mut csv = String::new();
csv.push_str("key,value\n");
csv.push_str(&format!(
"file_hash_sha256,{}\n",
proof_data.file_hash_sha256
));
csv.push_str(&format!(
"proof_generated_at,{}\n",
proof_data.timestamps.proof_generated_at.to_rfc3339()
));
for (key, value) in &proof_data.metadata {
csv.push_str(&format!("{},{}\n", key, value));
}
if let Some(location) = &proof_data.location {
csv.push_str(&format!("latitude,{}\n", location.latitude));
csv.push_str(&format!("longitude,{}\n", location.longitude));
if let Some(alt) = location.altitude {
csv.push_str(&format!("altitude,{}\n", alt));
}
}
if let Some(device) = &proof_data.device {
csv.push_str(&format!("device_manufacturer,{}\n", device.manufacturer));
csv.push_str(&format!("device_model,{}\n", device.model));
csv.push_str(&format!("device_os,{}\n", device.os_version));
}
if let Some(network) = &proof_data.network {
csv.push_str(&format!("network_type,{}\n", network.network_type));
if let Some(ssid) = &network.wifi_ssid {
csv.push_str(&format!("wifi_ssid,{}\n", ssid));
}
}
Ok(csv)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockCallbacks {
should_fail_save: bool,
device_info: Option<DeviceData>,
location_info: Option<LocationData>,
network_info: Option<NetworkData>,
sign_data_result: Result<Option<Vec<u8>>>,
saved_files: std::cell::RefCell<Vec<(String, String, Vec<u8>)>>,
saved_texts: std::cell::RefCell<Vec<(String, String, String)>>,
}
impl MockCallbacks {
fn new() -> Self {
Self {
should_fail_save: false,
device_info: Some(DeviceData {
manufacturer: "TestManufacturer".to_string(),
model: "TestModel".to_string(),
os_version: "TestOS 1.0".to_string(),
device_id: Some("test-device-123".to_string()),
}),
location_info: Some(LocationData {
latitude: 40.7128,
longitude: -74.0060,
altitude: Some(10.0),
accuracy: Some(5.0),
provider: Some("GPS".to_string()),
}),
network_info: Some(NetworkData {
network_type: "WiFi".to_string(),
wifi_ssid: Some("TestNetwork".to_string()),
cell_info: None,
}),
sign_data_result: Ok(Some(vec![0x01, 0x02, 0x03, 0x04])),
saved_files: std::cell::RefCell::new(Vec::new()),
saved_texts: std::cell::RefCell::new(Vec::new()),
}
}
fn minimal() -> Self {
Self {
should_fail_save: false,
device_info: None,
location_info: None,
network_info: None,
sign_data_result: Ok(None),
saved_files: std::cell::RefCell::new(Vec::new()),
saved_texts: std::cell::RefCell::new(Vec::new()),
}
}
}
impl PlatformCallbacks for MockCallbacks {
fn get_device_info(&self) -> Option<DeviceData> {
self.device_info.clone()
}
fn get_location_info(&self) -> Option<LocationData> {
self.location_info.clone()
}
fn get_network_info(&self) -> Option<NetworkData> {
self.network_info.clone()
}
fn save_data(&self, hash: &str, filename: &str, data: &[u8]) -> Result<()> {
if self.should_fail_save {
return Err(crate::generate_error::ProofModeError::Storage(
"Mock save failure".to_string(),
));
}
self.saved_files.borrow_mut().push((
hash.to_string(),
filename.to_string(),
data.to_vec(),
));
Ok(())
}
fn save_text(&self, hash: &str, filename: &str, text: &str) -> Result<()> {
if self.should_fail_save {
return Err(crate::generate_error::ProofModeError::Storage(
"Mock save failure".to_string(),
));
}
self.saved_texts.borrow_mut().push((
hash.to_string(),
filename.to_string(),
text.to_string(),
));
Ok(())
}
fn sign_data(&self, _data: &[u8]) -> Result<Option<Vec<u8>>> {
self.sign_data_result.clone()
}
fn notarize_hash(&self, _hash: &str) -> Result<Option<NotarizationData>> {
Ok(None)
}
fn report_progress(&self, _message: &str) {}
}
fn create_test_config() -> ProofModeConfig {
ProofModeConfig {
auto_notarize: false,
track_location: true,
track_device_id: true,
track_network: true,
add_credentials: true,
embed_c2pa: false,
}
}
#[test]
fn test_proof_generator_new() {
let config = create_test_config();
let generator = ProofGenerator::new(config);
assert!(true); }
#[test]
fn test_generate_proof_basic() {
let config = create_test_config();
let generator = ProofGenerator::new(config);
let callbacks = MockCallbacks::new();
let data = b"test media data";
let metadata = HashMap::new();
let result = generator.generate_proof(data, metadata, &callbacks);
assert!(result.is_ok());
let hash = result.unwrap();
assert_eq!(hash, calculate_hash(data));
let saved_texts = callbacks.saved_texts.borrow();
assert!(!saved_texts.is_empty());
let json_files: Vec<_> = saved_texts
.iter()
.filter(|(_, filename, _)| filename.ends_with(".proof.json"))
.collect();
let csv_files: Vec<_> = saved_texts
.iter()
.filter(|(_, filename, _)| filename.ends_with(".proof.csv"))
.collect();
assert_eq!(json_files.len(), 1);
assert_eq!(csv_files.len(), 1);
}
#[test]
fn test_generate_proof_with_metadata() {
let config = create_test_config();
let generator = ProofGenerator::new(config);
let callbacks = MockCallbacks::new();
let data = b"test media with metadata";
let mut metadata = HashMap::new();
metadata.insert("description".to_string(), "Test description".to_string());
metadata.insert("tags".to_string(), "test,proof".to_string());
let result = generator.generate_proof(data, metadata, &callbacks);
assert!(result.is_ok());
let saved_texts = callbacks.saved_texts.borrow();
let json_content = &saved_texts
.iter()
.find(|(_, filename, _)| filename.ends_with(".proof.json"))
.unwrap()
.2;
assert!(json_content.contains("Test description"));
assert!(json_content.contains("test,proof"));
}
#[test]
fn test_generate_proof_minimal_config() {
let mut config = create_test_config();
config.track_location = false;
config.track_device_id = false;
config.track_network = false;
config.add_credentials = false;
let generator = ProofGenerator::new(config);
let callbacks = MockCallbacks::minimal();
let data = b"minimal test data";
let metadata = HashMap::new();
let result = generator.generate_proof(data, metadata, &callbacks);
assert!(result.is_ok());
}
#[test]
fn test_generate_proof_save_failure() {
let config = create_test_config();
let generator = ProofGenerator::new(config);
let mut callbacks = MockCallbacks::new();
callbacks.should_fail_save = true;
let data = b"test data";
let metadata = HashMap::new();
let result = generator.generate_proof(data, metadata, &callbacks);
assert!(result.is_err());
}
#[test]
fn test_proof_to_csv() {
let config = create_test_config();
let generator = ProofGenerator::new(config);
let proof_data = ProofData {
file_hash_sha256: "test_hash".to_string(),
metadata: {
let mut map = HashMap::new();
map.insert("key1".to_string(), "value1".to_string());
map.insert("key2".to_string(), "value2".to_string());
map
},
location: Some(LocationData {
latitude: 40.7128,
longitude: -74.0060,
altitude: Some(10.0),
accuracy: Some(5.0),
provider: Some("GPS".to_string()),
}),
device: Some(DeviceData {
manufacturer: "TestCorp".to_string(),
model: "TestDevice".to_string(),
os_version: "TestOS 1.0".to_string(),
device_id: Some("test123".to_string()),
}),
network: Some(NetworkData {
network_type: "WiFi".to_string(),
wifi_ssid: Some("TestWiFi".to_string()),
cell_info: None,
}),
timestamps: TimestampData {
created_at: Utc::now(),
modified_at: None,
proof_generated_at: Utc::now(),
},
signature: Some("test_signature".to_string()),
notarization: None,
};
let csv = generator.proof_to_csv(&proof_data).unwrap();
assert!(csv.contains("key,value"));
assert!(csv.contains("file_hash_sha256,test_hash"));
assert!(csv.contains("key1,value1"));
assert!(csv.contains("key2,value2"));
assert!(csv.contains("latitude,40.7128"));
assert!(csv.contains("longitude,-74.006"));
assert!(csv.contains("device_manufacturer,TestCorp"));
assert!(csv.contains("network_type,WiFi"));
}
#[test]
fn test_create_proof_data_all_features() {
let config = create_test_config();
let generator = ProofGenerator::new(config);
let callbacks = MockCallbacks::new();
let hash = "test_hash";
let metadata = HashMap::new();
let proof_data = generator
.create_proof_data(hash, metadata, &callbacks)
.unwrap();
assert_eq!(proof_data.file_hash_sha256, hash);
assert!(proof_data.device.is_some());
assert!(proof_data.location.is_some());
assert!(proof_data.network.is_some());
assert!(proof_data.signature.is_some());
let device = proof_data.device.unwrap();
assert_eq!(device.manufacturer, "TestManufacturer");
assert_eq!(device.model, "TestModel");
let location = proof_data.location.unwrap();
assert_eq!(location.latitude, 40.7128);
assert_eq!(location.longitude, -74.0060);
let network = proof_data.network.unwrap();
assert_eq!(network.network_type, "WiFi");
assert_eq!(network.wifi_ssid, Some("TestNetwork".to_string()));
}
}