// // SignPDFKitWrapper.swift // SignPDFKit // // Created by Karyadidk on 10/10/25. // import Foundation //import forios // Assuming this links to your Rust/C header // MARK: - Enums enum Visibility: Int32 { case invisible = 0 case visibleImage = 1 case visibleQR = 2 // case visibleImageFromChar = 3 // case visibleQRFromChar = 4 } enum Subfilter: Int32 { case adbe = 0 case pades = 1 } enum SignatureType: Int32 { case signature = 0 case seal = 1 } enum DSS: Int32 { case no = 0 case yes = 1 } // MARK: - Typealias typealias SignDigestHandler = (_ digest: String, _ options: [String: String]) -> String // MARK: - Global Storage private var staticSignDigestHandler: SignDigestHandler? private var staticOptions: [String: String] = [:] // MARK: - Swift Class class SignPDFKitSign { init(signDigestHandler: @escaping SignDigestHandler, options: [String: String]) { staticSignDigestHandler = signDigestHandler staticOptions = options } func sign(inputPath: String, outputPath: String, imagePath: String, url: String, location: String, reason: String, contact_info: String, field_id: String, character: String, signatureType: SignatureType, page: Int32, isPades: Subfilter, typ: Visibility, x: Double, y: Double, width: Double, height: Double, dss: DSS) async -> String { var jsonData: [String: Any] = [ "response_code": 0, "response_status": "Success" ] let pre_sign = calculate_digest( inputPath, imagePath, url, location, reason, contact_info, field_id, character, signatureType.rawValue, page, isPades.rawValue, typ.rawValue, x, y, width, height, dss.rawValue ) // Check pre_sign if pre_sign == nil { jsonData["response_code"] = 4 jsonData["response_status"] = "Failed when process PDF" return jsonString(from: jsonData) } let preSign = String(cString: pre_sign!) // Parse JSON guard let data = try? JSONSerialization.jsonObject(with: Data(preSign.utf8)) as? [String: Any] else { jsonData["response_code"] = 4 jsonData["response_status"] = "Invalid JSON format" return jsonString(from: jsonData) } let responseCode = data["response_code"] as? Int ?? 4 if responseCode == 0 { // Extract digest var digest: String? = nil if let inner = data["data"] as? [String: Any] { digest = inner["digest"] as? String } guard let digest = digest else { jsonData["response_code"] = 4 jsonData["response_status"] = "Missing digest" return jsonString(from: jsonData) } guard let handler = staticSignDigestHandler else { return "" } let cms = handler(digest, staticOptions) // print(cms) // Equivalent of: const response_str = await this.get_revocation(cms, dss) let responseStr = await getRevocation(cms: cms, dss: dss) // Equivalent of: const result = this.ffi.embed_cms(pre_sign, response_str, output_path) let result = embed_cms(pre_sign, responseStr, outputPath) if result == 0 { return jsonString(from: jsonData) } else { jsonData["response_code"] = result jsonData["response_status"] = "Failed when process PDF" } } else { jsonData["response_code"] = responseCode switch responseCode { case 1: jsonData["response_status"] = "Failed to open/read document" case 4: jsonData["response_status"] = "Failed when process PDF" case 5: jsonData["response_status"] = "PDF File not found" case 6: jsonData["response_status"] = "Visualization Image not found" default: jsonData["response_status"] = "Failed when process PDF" } } return jsonString(from: jsonData) // pdf_sign( // inputPath, // outputPath, // imagePath, // url, // location, // reason, // contact_info, // field_id, // character, // signatureType.rawValue, // page, // isPades.rawValue, // typ.rawValue, // x, y, width, height, // staticCSignDigestCallback, // dss.rawValue // ) } func getRevocation(cms: String, dss: DSS) async -> String? { // Prepare jsonData structure var jsonData: [String: Any] = [ "cms": cms, "ocsp": [], "crl": [] ] do { // Call FFI to get revocation parameters let revocationResult = get_revocation_parameters(cms) // Check pre_sign if revocationResult == nil { // Convert back to JSON string let finalData = try JSONSerialization.data(withJSONObject: jsonData, options: [.prettyPrinted]) return String(data: finalData, encoding: .utf8) } let revocationData = String(cString: revocationResult!) // Parse JSON if let data = revocationData.data(using: .utf8) { do { if let jsonArray = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] { // If DSS is YES, process revocation data if dss == .yes { await processRevocationData(data: jsonArray, jsonData: &jsonData) } // Convert back to JSON string let finalData = try JSONSerialization.data(withJSONObject: jsonData, options: [.prettyPrinted]) return String(data: finalData, encoding: .utf8) } } catch { print("❌ Failed to parse JSON:", error) } } // Convert back to JSON string let finalData = try JSONSerialization.data(withJSONObject: jsonData, options: [.prettyPrinted]) return String(data: finalData, encoding: .utf8) } catch { print("❌ Error in get_revocation: \(error.localizedDescription)") // Convert back to JSON string return nil } } // MARK: - Process Revocation Data func processRevocationData(data: [[String: Any]], jsonData: inout [String: Any]) async { let accumulator = JSONDataAccumulator(jsonData: jsonData) await withTaskGroup(of: Void.self) { group in for item in data { guard let type = item["type"] as? String else { continue } group.addTask { if type.lowercased() == "ocsp" { if let value = await self.processOCSPItem(item: item) { await accumulator.appendOCSP(value) } } else if type.lowercased() == "crl" { if let value = await self.processCRLItem(item: item) { await accumulator.appendCRL(value) } } } } } // Merge results back to jsonData jsonData = await accumulator.getJSON() } // MARK: - Process OCSP Item private func processOCSPItem(item: [String: Any]) async -> String? { guard let requestBase64 = item["request"] as? String, let urlString = item["url"] as? String, let url = URL(string: urlString), let ocspRequestData = Data(base64Encoded: requestBase64) else { print("❌ Invalid OCSP item data") return nil } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/ocsp-request", forHTTPHeaderField: "Content-Type") request.setValue("application/ocsp-response", forHTTPHeaderField: "Accept") request.httpBody = ocspRequestData do { let (data, response) = try await URLSession.shared.data(for: request) if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { print("✅ OCSP processed successfully") return data.base64EncodedString() } else { print("❌ OCSP failed, status: \((response as? HTTPURLResponse)?.statusCode ?? -1)") return nil } } catch { print("❌ OCSP request failed: \(error.localizedDescription)") return nil } } // MARK: - Process CRL Item private func processCRLItem(item: [String: Any]) async -> String? { guard let urlString = item["url"] as? String, let url = URL(string: urlString) else { print("❌ Invalid CRL URL") return nil } do { let (data, response) = try await URLSession.shared.data(from: url) if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { let crlDer = extractCRLDER(content: data) print("✅ CRL processed successfully") return crlDer.base64EncodedString() } else { print("❌ CRL failed, status: \((response as? HTTPURLResponse)?.statusCode ?? -1)") return nil } } catch { print("❌ CRL request failed: \(error.localizedDescription)") return nil } } // MARK: - Extract CRL DER private func extractCRLDER(content: Data) -> Data { if let contentStr = String(data: content, encoding: .utf8), contentStr.contains("BEGIN X509 CRL") { let lines = contentStr.components(separatedBy: .newlines) let pemBody = lines.filter { !$0.starts(with: "---") && !$0.isEmpty }.joined() if let decoded = Data(base64Encoded: pemBody) { print("✅ Extracted CRL DER from PEM") return decoded } } return content } } class SignPDFKitVerify { init() {} func verife(inputPath: String) -> String { // Convert Swift string to C string guard let cString = inputPath.cString(using: .utf8) else { return "Invalid input path" } // Call the Rust/C function guard let resultPtr = verify(cString) else { return "Verification failed" } // Convert C string to Swift string let result = String(cString: resultPtr) // Free the C string returned by Rust if using CString::into_raw() free_c_string(UnsafeMutablePointer(mutating: resultPtr)) return result } } class SignPDFKitCheck { init() {} func check(inputPath: String) -> Bool { // Convert Swift string to C string guard let cString = inputPath.cString(using: .utf8) else { return false } // Call the Rust/C function let result: Int32 = is_signature_exist(cString) // Return true if result is non-zero return result != 0 } } // MARK: - Helper for converting dictionary to JSON string func jsonString(from dict: [String: Any]) -> String { if let data = try? JSONSerialization.data(withJSONObject: dict, options: [.prettyPrinted]), let str = String(data: data, encoding: .utf8) { return str } return "{}" } actor JSONDataAccumulator { private(set) var jsonData: [String: Any] init(jsonData: [String: Any]) { self.jsonData = jsonData } func appendOCSP(_ value: String) { var ocsp = jsonData["ocsp"] as? [String] ?? [] ocsp.append(value) jsonData["ocsp"] = ocsp } func appendCRL(_ value: String) { var crl = jsonData["crl"] as? [String] ?? [] crl.append(value) jsonData["crl"] = crl } func getJSON() -> [String: Any] { jsonData } }