import Foundation
import UIKit
import Photos
import CryptoKit
import UniformTypeIdentifiers
class ProofModeService: NSObject, ObservableObject {
@Published var isGeneratingProof = false
@Published var isVerifying = false
@Published var progress: Double = 0.0
@Published var status: String = ""
// MARK: - Generate Proof
func createProof(for asset: PHAsset, completion: @escaping (Result<Proof, Error>) -> Void) {
isGeneratingProof = true
progress = 0.0
// Get the image data
let options = PHImageRequestOptions()
options.isSynchronous = false
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { [weak self] data, dataUTI, orientation, info in
guard let self = self, let imageData = data else {
DispatchQueue.main.async {
self?.isGeneratingProof = false
completion(.failure(ProofModeServiceError.failedToLoadImage))
}
return
}
// Calculate hash using the real Rust function
let hash = getFileHash(mediaData: imageData)
// Create metadata
var metadata = self.createMetadata(for: asset)
// Pass MIME type from UTI so the Rust C2PA embedder knows the format
if let uti = dataUTI, let utType = UTType(uti) {
metadata["mime_type"] = utType.preferredMIMEType ?? "image/jpeg"
}
// Create config
let config = ProofModeConfig(
autoNotarize: false,
trackLocation: true,
trackDeviceId: true,
trackNetwork: true,
addCredentials: false,
embedC2pa: true
)
// Generate proof on a background thread since the call is synchronous
DispatchQueue.global(qos: .userInitiated).async {
do {
let callbacks = ProofModeCallbacksHandler(service: self)
let _ = try generateProof(
mediaData: imageData,
metadata: metadata,
config: config,
callbacks: callbacks
)
// Create proof object
let proof = Proof(
fileName: asset.value(forKey: "filename") as? String ?? "image.jpg",
hash: hash,
createdAt: asset.creationDate ?? Date(),
fileSize: Int64(imageData.count),
mimeType: dataUTI ?? "image/jpeg",
isVerified: true,
location: self.createLocationData(from: asset.location),
deviceInfo: self.createDeviceData(),
networkInfo: self.createNetworkData(),
signature: "generated_signature",
signedBy: "ProofMode",
imageData: imageData
)
DispatchQueue.main.async {
self.isGeneratingProof = false
self.progress = 1.0
completion(.success(proof))
}
} catch {
DispatchQueue.main.async {
self.isGeneratingProof = false
completion(.failure(error))
}
}
}
}
}
// MARK: - Verify Proof
func verifyProof(_ proof: Proof, completion: @escaping (Result<String, Error>) -> Void) {
isVerifying = true
progress = 0.0
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {
// Create temporary directory for verification
let tempDir = self.createTemporaryDirectory()
// Copy proof files from persistent storage to temp directory
self.copyProofFilesForVerification(hash: proof.hash, to: tempDir)
// Look for a C2PA-embedded file first (it contains the manifest)
let files = try FileManager.default.contentsOfDirectory(at: tempDir, includingPropertiesForKeys: nil)
let c2paFile = files.first { $0.lastPathComponent.contains(".c2pa.") }
let verifyFile: URL
if let c2paFile = c2paFile {
// Use the C2PA-embedded file for verification
verifyFile = c2paFile
print("DEBUG: Using C2PA-embedded file for verification: \(c2paFile.lastPathComponent)")
} else {
// Fallback: write image data to temp file
let imageFile = tempDir.appendingPathComponent(proof.fileName)
if let imageData = proof.fullImage?.jpegData(compressionQuality: 1.0) {
try imageData.write(to: imageFile)
} else {
throw ProofModeServiceError.failedToCreateTempFile
}
verifyFile = imageFile
}
// Create callbacks
let callbacks = ProofModeCallbacksHandler(service: self)
print("DEBUG: Calling checkFiles with path: \(verifyFile.path)")
print("DEBUG: File exists: \(FileManager.default.fileExists(atPath: verifyFile.path))")
// List all files in temp directory for debugging
print("DEBUG: Files in temp directory: \(files.map { $0.lastPathComponent })")
// Verify using the real Rust library (synchronous call)
let result = try checkFiles(
filePaths: [verifyFile.path],
callbacks: callbacks
)
// Filter out binary data from the JSON response
let finalResult = self.filterBinaryDataFromJSON(result)
print("DEBUG: checkFiles returned (filtered): '\(finalResult.prefix(500))...'")
// Clean up temporary files
try? FileManager.default.removeItem(at: tempDir)
DispatchQueue.main.async {
self.isVerifying = false
self.progress = 1.0
completion(.success(finalResult))
}
} catch {
DispatchQueue.main.async {
self.isVerifying = false
completion(.failure(error))
}
}
}
}
// MARK: - Hash Calculation
func calculateHash(for data: Data) -> String {
return getFileHash(mediaData: data)
}
// MARK: - File Management
private func createTemporaryDirectory() -> URL {
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try? FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
return tempDir
}
private func copyProofFilesForVerification(hash: String, to destinationDir: URL) {
guard let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first else {
return
}
let proofmodeDir = documentsPath.appendingPathComponent("proofmode")
let hashDir = proofmodeDir.appendingPathComponent(hash)
do {
// Check if hash directory exists
guard FileManager.default.fileExists(atPath: hashDir.path) else {
print("DEBUG: No proof files found for hash: \(hash)")
return
}
// Copy all files from hash directory to temp directory
let files = try FileManager.default.contentsOfDirectory(at: hashDir, includingPropertiesForKeys: nil)
for file in files {
let destFile = destinationDir.appendingPathComponent(file.lastPathComponent)
try FileManager.default.copyItem(at: file, to: destFile)
print("DEBUG: Copied proof file: \(file.lastPathComponent)")
}
} catch {
print("ERROR: Failed to copy proof files: \(error)")
}
}
// MARK: - Public Methods for Proof Management
func listSavedProofs() -> [String] {
guard let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first else {
return []
}
let proofmodeDir = documentsPath.appendingPathComponent("proofmode")
do {
let hashDirs = try FileManager.default.contentsOfDirectory(at: proofmodeDir,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
return hashDirs.compactMap { $0.lastPathComponent }
} catch {
print("ERROR: Failed to list saved proofs: \(error)")
return []
}
}
func getProofFiles(for hash: String) -> [URL] {
guard let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first else {
return []
}
let hashDir = documentsPath.appendingPathComponent("proofmode").appendingPathComponent(hash)
do {
return try FileManager.default.contentsOfDirectory(at: hashDir,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
} catch {
print("ERROR: Failed to get proof files: \(error)")
return []
}
}
// MARK: - Private Helpers
private func filterBinaryDataFromJSON(_ jsonString: String) -> String {
guard let data = jsonString.data(using: .utf8) else {
return jsonString
}
do {
// Parse the JSON
if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
// Create a mutable copy
var filteredObject = jsonObject
// Check if there's a files array
if var files = filteredObject["files"] as? [[String: Any]] {
// Remove the 'data' key from each file object
for i in 0..<files.count {
files[i].removeValue(forKey: "data")
}
filteredObject["files"] = files
}
// Convert back to JSON
let filteredData = try JSONSerialization.data(withJSONObject: filteredObject, options: [.prettyPrinted])
return String(data: filteredData, encoding: .utf8) ?? jsonString
} else if let jsonArray = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] {
// Handle case where the top level is an array
var filteredArray = jsonArray
for i in 0..<filteredArray.count {
if var files = filteredArray[i]["files"] as? [[String: Any]] {
for j in 0..<files.count {
files[j].removeValue(forKey: "data")
}
filteredArray[i]["files"] = files
}
}
// Convert back to JSON
let filteredData = try JSONSerialization.data(withJSONObject: filteredArray, options: [.prettyPrinted])
return String(data: filteredData, encoding: .utf8) ?? jsonString
}
} catch {
print("DEBUG: Failed to filter JSON: \(error)")
}
return jsonString
}
private func createMetadata(for asset: PHAsset) -> [String: String] {
var metadata: [String: String] = [:]
// Basic metadata
metadata["timestamp"] = ISO8601DateFormatter().string(from: asset.creationDate ?? Date())
metadata["media_type"] = asset.mediaType == .image ? "image" : "video"
// Location metadata
if let location = asset.location {
metadata["latitude"] = String(location.coordinate.latitude)
metadata["longitude"] = String(location.coordinate.longitude)
metadata["altitude"] = String(location.altitude)
}
// Asset metadata
metadata["width"] = String(asset.pixelWidth)
metadata["height"] = String(asset.pixelHeight)
metadata["duration"] = String(asset.duration)
return metadata
}
private func createLocationData(from location: CLLocation?) -> LocationData? {
guard let location = location else { return nil }
return LocationData(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
altitude: location.altitude,
accuracy: location.horizontalAccuracy,
provider: "CoreLocation"
)
}
private func createDeviceData() -> DeviceData {
return DeviceData(
manufacturer: "Apple",
model: UIDevice.current.model,
osVersion: UIDevice.current.systemVersion,
deviceId: UIDevice.current.identifierForVendor?.uuidString
)
}
private func createNetworkData() -> NetworkData {
return NetworkData(
networkType: "wifi",
wifiSsid: nil,
cellInfo: nil
)
}
}
// MARK: - ProofMode Callbacks Implementation
class ProofModeCallbacksHandler: ProofModeCallbacks, @unchecked Sendable {
weak var service: ProofModeService?
init(service: ProofModeService) {
self.service = service
}
func getLocation() -> LocationData? {
// In a real app, you would get actual location from CoreLocation
// For now, return nil to indicate no location available
return nil
}
func getDeviceInfo() -> DeviceData? {
return DeviceData(
manufacturer: "Apple",
model: UIDevice.current.model,
osVersion: UIDevice.current.systemVersion,
deviceId: UIDevice.current.identifierForVendor?.uuidString
)
}
func getNetworkInfo() -> NetworkData? {
return NetworkData(
networkType: "wifi",
wifiSsid: nil,
cellInfo: nil
)
}
func saveData(hash: String, filename: String, data: Data) -> Bool {
// Get documents directory
guard let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first else {
return false
}
// Create proofmode directory if it doesn't exist
let proofmodeDir = documentsPath.appendingPathComponent("proofmode")
let hashDir = proofmodeDir.appendingPathComponent(hash)
do {
try FileManager.default.createDirectory(at: hashDir,
withIntermediateDirectories: true,
attributes: nil)
// Save the data file
let fileURL = hashDir.appendingPathComponent(filename)
try data.write(to: fileURL)
print("DEBUG: Saved data file to: \(fileURL.path)")
return true
} catch {
print("ERROR: Failed to save data file: \(error)")
return false
}
}
func saveText(hash: String, filename: String, text: String) -> Bool {
// Get documents directory
guard let documentsPath = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first else {
return false
}
// Create proofmode directory if it doesn't exist
let proofmodeDir = documentsPath.appendingPathComponent("proofmode")
let hashDir = proofmodeDir.appendingPathComponent(hash)
do {
try FileManager.default.createDirectory(at: hashDir,
withIntermediateDirectories: true,
attributes: nil)
// Save the text file
let fileURL = hashDir.appendingPathComponent(filename)
try text.write(to: fileURL, atomically: true, encoding: .utf8)
print("DEBUG: Saved text file to: \(fileURL.path)")
return true
} catch {
print("ERROR: Failed to save text file: \(error)")
return false
}
}
func signData(data: Data) -> Data? {
// In a real app, implement actual signing
// For now, return nil to indicate no signing
return nil
}
func reportProgress(message: String) {
DispatchQueue.main.async { [weak self] in
self?.service?.status = message
}
}
}
// MARK: - Errors
enum ProofModeServiceError: LocalizedError {
case failedToLoadImage
case failedToCreateTempFile
var errorDescription: String? {
switch self {
case .failedToLoadImage:
return "Failed to load image data"
case .failedToCreateTempFile:
return "Failed to create temporary file for verification"
}
}
}