diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1a19c17d..248e46f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,10 +67,14 @@ android { applicationId = "net.opendasharchive.openarchive" minSdk = 29 targetSdk = 36 - versionCode = 30041 + versionCode = 30043 versionName = "4.0.5" multiDexEnabled = true vectorDrawables.useSupportLibrary = true + // minSdk=29 (Android 10) requires 64-bit hardware — all supported devices are arm64 + ndk { + abiFilters += listOf("arm64-v8a") + } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" val localProps = loadLocalProperties() resValue("string", "mixpanel_key", localProps.getProperty("MIXPANELKEY") ?: "") @@ -119,10 +123,6 @@ android { val localProps = loadLocalProperties() val acraEmail = localProps.getProperty("ACRA_EMAIL") ?: System.getenv("ACRA_EMAIL") ?: "" buildConfigField("String", "ACRA_EMAIL", "\"$acraEmail\"") - // No real devices use x86/x86_64 — emulators can use armeabi-v7a via translation - ndk { - abiFilters += listOf("arm64-v8a", "armeabi-v7a") - } } // Environment dimension @@ -310,21 +310,17 @@ dependencies { "gmsImplementation"(libs.google.play.review.ktx) "gmsImplementation"(libs.google.play.app.update.ktx) "gmsImplementation"("com.google.android.gms:play-services-location:21.1.0") + "gmsImplementation"("com.google.android.play:integrity:1.4.0") // Tor implementation(libs.tor.android) implementation(libs.jtorctl) - // C2PA - Content Authenticity - // TODO: Add actual C2PA library once available - // simple-c2pa (org.witness:simple-c2pa:0.0.13) is not available in Maven - // Options: - // 1. Use c2pa-android (https://github.com/contentauth/c2pa-android) - // 2. Use c2pa-rs directly via JNI - // 3. Build simple-c2pa from source - // For now, using stub implementation in C2paHelper - // implementation(libs.simple.c2pa) - // implementation(libs.jna) + // C2PA - Content Authenticity (contentauth/c2pa-android) + implementation("com.github.contentauth:c2pa-android:0.0.9") + implementation("org.bouncycastle:bcprov-jdk18on:1.81") + implementation("org.bouncycastle:bcpkix-jdk18on:1.81") + implementation("org.bouncycastle:bcpg-jdk18on:1.81") // Utilities implementation(libs.timber) @@ -368,241 +364,6 @@ detekt { ignoreFailures = true } -// ============================================================ -// C2PA Rust FFI — Auto-build for FOSS variants -// ============================================================ -// -// Task graph (runs only for FOSS builds): -// mergeXxxJniLibFolders -// └── buildC2paRustLibs [incremental: skipped if .so files are up-to-date] -// └── generateCargoConfig [skipped if .cargo/config.toml already exists] -// └── installRustTargets [idempotent rustup target add] -// └── restoreC2paSource [skipped if Cargo.toml exists] -// -// To force a full rebuild: ./gradlew buildC2paRustLibs --rerun-tasks -// To regenerate NDK config: delete rust-c2pa-ffi/.cargo/config.toml and rebuild - -val preferredNdkVersion = "27.1.12297006" -val androidApiLevel = "30" -val rustC2paDir = rootProject.file("rust-c2pa-ffi") -val jniLibsDir = project.file("src/main/jniLibs") - -// Rust target triple → Android ABI directory name -val rustToAbi = linkedMapOf( - "aarch64-linux-android" to "arm64-v8a", - "armv7-linux-androideabi" to "armeabi-v7a", - "i686-linux-android" to "x86", - "x86_64-linux-android" to "x86_64" -) - -fun findNdk(): File { - // 1. Explicit NDK env var (highest priority) - listOfNotNull( - System.getenv("ANDROID_NDK_HOME"), - System.getenv("ANDROID_NDK_ROOT"), - ).map(::File).firstOrNull { it.isDirectory }?.let { return it } - - // 2. Find SDK, then look for ndk/ subdirectory - val sdk = listOfNotNull( - System.getenv("ANDROID_HOME"), - System.getenv("ANDROID_SDK_ROOT"), - "${System.getProperty("user.home")}/Library/Android/sdk", // macOS default - "${System.getProperty("user.home")}/Android/Sdk", // Linux/Windows default - ).map(::File).firstOrNull { it.isDirectory } - ?: throw GradleException( - "Android SDK not found.\n" + - "Set ANDROID_HOME to your SDK root directory and try again." - ) - - val ndkParent = File(sdk, "ndk") - if (!ndkParent.isDirectory) throw GradleException( - "Android NDK not found at $ndkParent.\n" + - "Install it via Android Studio → SDK Manager → SDK Tools → NDK (Side by side)." - ) - - // Prefer the pinned version; otherwise use the latest installed - File(ndkParent, preferredNdkVersion).takeIf { it.isDirectory }?.let { return it } - return ndkParent.listFiles() - ?.filter { it.isDirectory } - ?.maxByOrNull { it.name } - ?: throw GradleException("No NDK versions found in $ndkParent") -} - -fun ndkBinDir(ndk: File): File { - val prebuilt = File(ndk, "toolchains/llvm/prebuilt") - return prebuilt.listFiles() - ?.firstOrNull { it.isDirectory } - ?.let { File(it, "bin") } - ?: throw GradleException("NDK prebuilt toolchain not found under $prebuilt") -} - -/** - * Resolves a bare executable name (e.g. "cargo", "rustup") to its absolute path. - * Gradle's daemon process does not inherit the user's shell PATH, so tools installed - * in ~/.cargo/bin are not visible to a plain ProcessBuilder call. Searching common - * locations here bypasses that limitation without requiring a shell wrapper. - */ -fun resolveExe(name: String): String { - if (name.contains("/")) return name // already a path - val searchDirs = listOf( - "${System.getProperty("user.home")}/.cargo/bin", - "/usr/local/bin", - "/usr/bin", - "/bin", - ) + (System.getenv("PATH") ?: "").split(File.pathSeparatorChar) - return searchDirs - .map { File(it, name) } - .firstOrNull { it.canExecute() } - ?.absolutePath - ?: name -} - -/** Runs a command via ProcessBuilder, streams output to stdout/stderr, throws on non-zero exit. */ -fun runCommand(vararg cmd: String, workDir: File = rootProject.projectDir, env: Map = emptyMap()) { - val resolved = listOf(resolveExe(cmd[0])) + cmd.drop(1) - val pb = ProcessBuilder(resolved).directory(workDir).inheritIO() - if (env.isNotEmpty()) pb.environment().putAll(env) - val result = pb.start().waitFor() - check(result == 0) { "Command failed (exit $result): ${cmd.joinToString(" ")}" } -} - -// Maps each Rust triple to its (clang binary prefix, CC env-var key) -val targetDetails = linkedMapOf( - "aarch64-linux-android" to Pair("aarch64-linux-android${androidApiLevel}", "aarch64_linux_android"), - "armv7-linux-androideabi" to Pair("armv7a-linux-androideabi${androidApiLevel}", "armv7_linux_androideabi"), - "i686-linux-android" to Pair("i686-linux-android${androidApiLevel}", "i686_linux_android"), - "x86_64-linux-android" to Pair("x86_64-linux-android${androidApiLevel}", "x86_64_linux_android"), -) - -// ─── Task 1: Restore source from git if the directory was deleted ──────────── -val restoreC2paSource by tasks.registering { - group = "c2pa" - description = "Restores rust-c2pa-ffi/ from git HEAD if the directory is missing" - onlyIf { !File(rustC2paDir, "Cargo.toml").exists() } - doLast { - logger.lifecycle("rust-c2pa-ffi/ missing — restoring from git HEAD...") - runCommand("git", "checkout", "HEAD", "--", "rust-c2pa-ffi") - logger.lifecycle("✓ rust-c2pa-ffi/ restored from git") - } -} - -// ─── Task 2: Validate Rust toolchain; auto-install Android targets ─────────── -val installRustTargets by tasks.registering { - group = "c2pa" - description = "Verifies Cargo is installed and ensures all Android Rust targets are added" - dependsOn(restoreC2paSource) - doLast { - val cargoOk = ProcessBuilder(resolveExe("cargo"), "--version") - .redirectErrorStream(true) - .start() - .waitFor() == 0 - if (!cargoOk) throw GradleException( - "Cargo not found. Install Rust from https://rustup.rs/\n" + - "Then run: rustup target add ${rustToAbi.keys.joinToString(" ")}" - ) - // rustup target add is idempotent — safe to call on every build - runCommand(*( listOf("rustup", "target", "add") + rustToAbi.keys ).toTypedArray()) - logger.lifecycle("✓ Rust Android targets verified") - } -} - -// ─── Task 3: Generate .cargo/config.toml using the installed NDK ───────────── -// Runs on every build but only writes to disk when content changes, so -// buildC2paRustLibs (which uses the file as input) stays UP-TO-DATE when NDK is unchanged. -val generateCargoConfig by tasks.registering { - group = "c2pa" - description = "Generates rust-c2pa-ffi/.cargo/config.toml with NDK linker paths" - dependsOn(installRustTargets) - doLast { - val ndk = findNdk() - val bin = ndkBinDir(ndk) - - // NOTE: CC_*/AR_* env vars must live in the top-level [env] section. - // [target.X.env] is NOT a valid Cargo config key and is silently ignored, - // which causes cc-rs (used by the ring crate) to fail with "tool not found". - val newContent = buildString { - appendLine("# Auto-generated by Gradle — do not edit manually.") - appendLine("# Regenerate: run ./gradlew generateCargoConfig --rerun-tasks") - appendLine("# NDK: ${ndk.absolutePath}") - appendLine() - appendLine("[env]") - appendLine("ANDROID_NDK_HOME = \"${ndk.absolutePath}\"") - // CC/CXX/AR vars for cc-rs and other build scripts (one entry per ABI, all in [env]) - for ((_, pair) in targetDetails) { - val (clangPrefix, envKey) = pair - appendLine("CC_$envKey = \"$bin/${clangPrefix}-clang\"") - appendLine("CXX_$envKey = \"$bin/${clangPrefix}-clang++\"") - appendLine("AR_$envKey = \"$bin/llvm-ar\"") - } - appendLine() - for ((triple, pair) in targetDetails) { - val (clangPrefix, _) = pair - appendLine("[target.$triple]") - appendLine("linker = \"$bin/${clangPrefix}-clang\"") - appendLine("ar = \"$bin/llvm-ar\"") - appendLine("rustflags = [\"-C\", \"link-arg=-Wl,-z,max-page-size=16384\"]") - appendLine() - } - appendLine("[build]") - appendLine("target-dir = \"target\"") - } - - val configFile = File(rustC2paDir, ".cargo/config.toml") - File(rustC2paDir, ".cargo").mkdirs() - if (!configFile.exists() || configFile.readText() != newContent) { - configFile.writeText(newContent) - logger.lifecycle("✓ .cargo/config.toml written (NDK: ${ndk.absolutePath})") - } else { - logger.lifecycle("✓ .cargo/config.toml unchanged") - } - } -} - -// ─── Task 4: Compile the Rust library for all Android ABIs ─────────────────── -val buildC2paRustLibs by tasks.registering { - group = "c2pa" - description = "Compiles libc2pa_ffi.so for all Android ABIs (incremental)" - dependsOn(generateCargoConfig) - - // Gradle skips this task automatically when inputs are unchanged and outputs exist - inputs.dir(File(rustC2paDir, "src")) - inputs.files( - File(rustC2paDir, "Cargo.toml"), - File(rustC2paDir, "Cargo.lock"), - File(rustC2paDir, ".cargo/config.toml"), - ) - outputs.files(rustToAbi.values.map { abi -> jniLibsDir.resolve("$abi/libc2pa_ffi.so") }) - - doLast { - val bin = ndkBinDir(findNdk()) - rustToAbi.forEach { (rustTarget, abi) -> - logger.lifecycle(" Building $abi ($rustTarget)...") - val (clangPrefix, envKey) = targetDetails[rustTarget]!! - // Pass CC/AR explicitly — belt-and-suspenders alongside .cargo/config.toml [env] - val env = mapOf( - "CC_$envKey" to "$bin/${clangPrefix}-clang", - "CXX_$envKey" to "$bin/${clangPrefix}-clang++", - "AR_$envKey" to "$bin/llvm-ar", - ) - runCommand("cargo", "build", "--target", rustTarget, "--release", workDir = rustC2paDir, env = env) - val soFile = rustC2paDir.resolve("target/$rustTarget/release/libc2pa_ffi.so") - check(soFile.exists()) { - "cargo build succeeded but .so not found at: ${soFile.absolutePath}" - } - jniLibsDir.resolve(abi).mkdirs() - soFile.copyTo(jniLibsDir.resolve("$abi/libc2pa_ffi.so"), overwrite = true) - logger.lifecycle(" ✓ $abi/libc2pa_ffi.so (${soFile.length() / 1024} KB)") - } - logger.lifecycle("C2PA Rust FFI build complete.") - } -} - -// ─── Wire buildC2paRustLibs into ALL variant builds (GMS + FOSS) ───────────── -afterEvaluate { - tasks.matching { it.name.startsWith("merge") && it.name.endsWith("JniLibFolders") } - .configureEach { dependsOn(buildC2paRustLibs) } -} - // Conditionally apply Google Services plugins only for GMS builds if (gradle.startParameter.taskRequests.toString().contains("Gms", ignoreCase = true)) { apply(plugin = "com.google.gms.google-services") diff --git a/app/src/foss/java/net/opendasharchive/openarchive/util/SafetyNetHelper.kt b/app/src/foss/java/net/opendasharchive/openarchive/util/SafetyNetHelper.kt new file mode 100644 index 00000000..d5906e57 --- /dev/null +++ b/app/src/foss/java/net/opendasharchive/openarchive/util/SafetyNetHelper.kt @@ -0,0 +1,9 @@ +package net.opendasharchive.openarchive.util + +import android.content.Context +import java.io.File + +object SafetyNetHelper { + // Play Integrity API requires GMS — not available on FOSS builds. + fun requestGst(context: Context, mediaHash: String, outFile: File) = Unit +} diff --git a/app/src/gms/java/net/opendasharchive/openarchive/util/SafetyNetHelper.kt b/app/src/gms/java/net/opendasharchive/openarchive/util/SafetyNetHelper.kt new file mode 100644 index 00000000..de793431 --- /dev/null +++ b/app/src/gms/java/net/opendasharchive/openarchive/util/SafetyNetHelper.kt @@ -0,0 +1,30 @@ +package net.opendasharchive.openarchive.util + +import android.content.Context +import android.util.Base64 +import com.google.android.play.core.integrity.IntegrityManagerFactory +import com.google.android.play.core.integrity.IntegrityTokenRequest +import com.google.android.gms.tasks.Tasks +import net.opendasharchive.openarchive.core.logger.AppLogger +import java.io.File + +object SafetyNetHelper { + /** + * Requests a Play Integrity token and writes it as a JWT string to [outFile]. + * No-op on network or API failure — outFile simply won't exist, and the caller + * skips it in the upload list. + */ + fun requestGst(context: Context, mediaHash: String, outFile: File) { + try { + val nonce = Base64.encodeToString(mediaHash.toByteArray(Charsets.UTF_8), + Base64.URL_SAFE or Base64.NO_WRAP) + val manager = IntegrityManagerFactory.create(context.applicationContext) + val request = IntegrityTokenRequest.builder().setNonce(nonce).build() + val response = Tasks.await(manager.requestIntegrityToken(request)) + outFile.writeText(response.token()) + AppLogger.i("[GST] Play Integrity token written for $mediaHash") + } catch (e: Exception) { + AppLogger.w("[GST] Play Integrity request failed: ${e.message}") + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt index aecb6e95..d60471be 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/SaveApp.kt @@ -31,7 +31,6 @@ import net.opendasharchive.openarchive.services.tor.TorServiceManager import kotlinx.coroutines.launch import net.opendasharchive.openarchive.analytics.api.AnalyticsManager import net.opendasharchive.openarchive.analytics.api.session.SessionTracker -import net.opendasharchive.openarchive.core.security.C2paKeyStore import net.opendasharchive.openarchive.analytics.di.analyticsModule import net.opendasharchive.openarchive.db.AppDatabase import net.opendasharchive.openarchive.db.MigrationWorker @@ -43,7 +42,8 @@ import net.opendasharchive.openarchive.core.di.passcodeModule import net.opendasharchive.openarchive.features.settings.passcode.PasscodeGate import net.opendasharchive.openarchive.core.di.retrofitModule import net.opendasharchive.openarchive.core.logger.AppLogger -import net.opendasharchive.openarchive.util.C2paHelper +import net.opendasharchive.openarchive.util.ProofCompanionGenerator +import net.opendasharchive.openarchive.util.ProofmodeC2paManager import net.opendasharchive.openarchive.core.repositories.MediaRepository import net.opendasharchive.openarchive.util.CleanInsightsManager import net.opendasharchive.openarchive.util.Prefs @@ -102,8 +102,11 @@ class SaveApp : SugarApp(), SingletonImageLoader.Factory, DefaultLifecycleObserv Prefs.load(this) - // Initialize C2PA Helper - C2paHelper.init(this) + // Initialize ProofmodeC2paManager (generates self-signed key on first run) + ProofmodeC2paManager.init(this) + + // Initialize PGP key for proof companion files + ProofCompanionGenerator.init(this) // --- 2-launch synchronous migration strategy --- // L1: If Sugar DB exists and Room migration hasn't run yet, open Room directly @@ -185,12 +188,6 @@ class SaveApp : SugarApp(), SingletonImageLoader.Factory, DefaultLifecycleObserv applyTheme() - // Migrate C2PA keys from plaintext SharedPreferences to SecureStorage (one-time) - val c2paKeyStore: C2paKeyStore by inject() - c2paKeyStore.migrateFromPrefsIfNeeded( - androidx.preference.PreferenceManager.getDefaultSharedPreferences(this) - ) - // Schedule periodic cache cleanup (runs every 7 days when battery is not low) WorkManager.getInstance(this).enqueueUniquePeriodicWork( CacheCleanupWorker.WORK_NAME, diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt index ff2edaeb..63b3541b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/di/CoreModule.kt @@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers import android.content.SharedPreferences import androidx.preference.PreferenceManager import net.opendasharchive.openarchive.core.config.AppConfig -import net.opendasharchive.openarchive.core.security.C2paKeyStore import net.opendasharchive.openarchive.core.security.SecurityManager import net.opendasharchive.openarchive.features.core.dialog.DefaultResourceProvider import net.opendasharchive.openarchive.features.core.dialog.DialogStateManager @@ -68,8 +67,6 @@ val coreModule = module { // Centrally managed security flags single { SecurityManager(get(named("default_prefs"))) } - // Secure storage for C2PA signing keys (Android Keystore backed) - single { C2paKeyStore(androidApplication()) } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelper.kt b/app/src/main/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelper.kt index 833d61ae..878cee41 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelper.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/core/repositories/FileCleanupHelper.kt @@ -4,7 +4,6 @@ import android.content.Context import net.opendasharchive.openarchive.core.domain.Evidence import net.opendasharchive.openarchive.core.logger.AppLogger import net.opendasharchive.openarchive.db.sugar.Media -import net.opendasharchive.openarchive.util.C2paHelper import java.io.File /** @@ -17,7 +16,6 @@ class FileCleanupHelper(private val context: Context) { */ fun deleteUploadedMediaFiles(evidence: Evidence) { deleteInternalMediaFile(evidence) - deleteC2paSidecar(evidence.mediaHashString) } /** @@ -42,7 +40,6 @@ class FileCleanupHelper(private val context: Context) { } } - deleteC2paSidecar(media.mediaHashString) } private fun deleteInternalMediaFile(evidence: Evidence) { @@ -58,12 +55,6 @@ class FileCleanupHelper(private val context: Context) { } } - private fun deleteC2paSidecar(mediaHashString: String) { - if (mediaHashString.isNotEmpty()) { - C2paHelper.removeC2paFiles(context, mediaHashString) - } - } - /** * Checks if a file resides within the application's internal files directory. * We only want to delete files we imported to our internal storage. diff --git a/app/src/main/java/net/opendasharchive/openarchive/core/security/C2paKeyStore.kt b/app/src/main/java/net/opendasharchive/openarchive/core/security/C2paKeyStore.kt deleted file mode 100644 index c802b762..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/core/security/C2paKeyStore.kt +++ /dev/null @@ -1,63 +0,0 @@ -package net.opendasharchive.openarchive.core.security - -import android.content.Context -import android.content.SharedPreferences -import android.util.Base64 -import net.opendasharchive.openarchive.core.security.SecureStorage - -/** - * Secure storage for C2PA signing keys using Android Keystore (AES-GCM). - * Replaces plaintext storage in SharedPreferences. - */ -class C2paKeyStore(context: Context) { - - private val secureStorage = SecureStorage(context, alias = "C2paKeyStore") - - companion object { - private const val KEY_SIGNING_KEY = "c2pa_signing_key" - private const val KEY_PASSPHRASE = "c2pa_passphrase" - - // Legacy SharedPreferences keys used before this class was introduced - private const val LEGACY_KEY_SIGNING_KEY = "c2pa_signing_key" - private const val LEGACY_KEY_PASSPHRASE = "c2pa_encrypted_passphrase" - } - - fun getSigningKey(): String? = secureStorage.getString(KEY_SIGNING_KEY) - - fun putSigningKey(value: String?) = secureStorage.putString(KEY_SIGNING_KEY, value) - - fun getPassphrase(): ByteArray? = secureStorage.getString(KEY_PASSPHRASE) - ?.let { Base64.decode(it, Base64.DEFAULT) } - - fun putPassphrase(value: ByteArray?) { - val encoded = value?.let { Base64.encodeToString(it, Base64.DEFAULT) } - secureStorage.putString(KEY_PASSPHRASE, encoded) - } - - fun clear() { - secureStorage.remove(KEY_SIGNING_KEY) - secureStorage.remove(KEY_PASSPHRASE) - } - - /** - * One-time migration: moves C2PA keys from plaintext SharedPreferences to SecureStorage, - * then removes them from SharedPreferences. - */ - fun migrateFromPrefsIfNeeded(prefs: SharedPreferences) { - val legacyKey = prefs.getString(LEGACY_KEY_SIGNING_KEY, null) - if (legacyKey != null && secureStorage.getString(KEY_SIGNING_KEY) == null) { - secureStorage.putString(KEY_SIGNING_KEY, legacyKey) - } - if (legacyKey != null) { - prefs.edit().remove(LEGACY_KEY_SIGNING_KEY).apply() - } - - val legacyPassphrase = prefs.getString(LEGACY_KEY_PASSPHRASE, null) - if (legacyPassphrase != null && secureStorage.getString(KEY_PASSPHRASE) == null) { - secureStorage.putString(KEY_PASSPHRASE, legacyPassphrase) - } - if (legacyPassphrase != null) { - prefs.edit().remove(LEGACY_KEY_PASSPHRASE).apply() - } - } -} diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDao.kt b/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDao.kt index b02dd26d..be5cc320 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDao.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/db/EvidenceDao.kt @@ -7,36 +7,16 @@ import net.opendasharchive.openarchive.core.domain.EvidenceStatus @Dao interface EvidenceDao { @Query(""" - SELECT * FROM evidence - WHERE submissionId = :submissionId - ORDER BY - CASE - WHEN status IN (2, 4) THEN 0 -- Active (Queued, Uploading) - WHEN status = 9 THEN 1 -- Error - WHEN status IN (0, 1) THEN 2 -- Local/New - WHEN status = 5 THEN 3 -- Uploaded - ELSE 4 - END, - CASE WHEN status = 5 THEN uploadedAt ELSE 0 END DESC, - priority DESC, - id DESC + SELECT * FROM evidence + WHERE submissionId = :submissionId + ORDER BY id ASC """) fun observeBySubmission(submissionId: Long): Flow> @Query(""" - SELECT * FROM evidence - WHERE archiveId = :archiveId - ORDER BY - CASE - WHEN status IN (2, 4) THEN 0 -- Active - WHEN status = 9 THEN 1 -- Error - WHEN status IN (0, 1) THEN 2 -- Local/New - WHEN status = 5 THEN 3 -- Uploaded - ELSE 4 - END, - CASE WHEN status = 5 THEN uploadedAt ELSE 0 END DESC, - priority DESC, - id DESC + SELECT * FROM evidence + WHERE archiveId = :archiveId + ORDER BY id ASC """) fun observeByArchive(archiveId: Long): Flow> @@ -53,7 +33,7 @@ interface EvidenceDao { ORDER BY CASE WHEN status = 9 THEN 1 ELSE 0 END, priority DESC, - id DESC + id ASC """) suspend fun getQueueWithErrorsLast(now: Long): List diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/HomeActivity.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/HomeActivity.kt index a78a0b70..87c32a9e 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/HomeActivity.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/HomeActivity.kt @@ -28,7 +28,6 @@ import org.koin.androidx.scope.activityRetainedScope import net.opendasharchive.openarchive.features.main.ui.SharedImportState import net.opendasharchive.openarchive.upload.UploadGate import net.opendasharchive.openarchive.upload.UploadJobScheduler -import net.opendasharchive.openarchive.util.C2paHelper class HomeActivity : BaseComposeActivity(), AndroidScopeComponent { @@ -98,8 +97,6 @@ class HomeActivity : BaseComposeActivity(), AndroidScopeComponent { override fun onStart() { super.onStart() - C2paHelper.init(this) - // On every foreground return: if already unlocked, re-schedule any queued uploads. // This recovers from stalled JobService runs (e.g. OS killed the job mid-upload). if (!passcodeGate.locked.value) { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/MediaPicker.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/MediaPicker.kt index 5e1b971c..6d7732f2 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/media/MediaPicker.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/MediaPicker.kt @@ -7,7 +7,6 @@ import net.opendasharchive.openarchive.core.domain.Evidence import net.opendasharchive.openarchive.core.domain.EvidenceStatus import net.opendasharchive.openarchive.core.domain.VaultType import net.opendasharchive.openarchive.core.logger.AppLogger -import net.opendasharchive.openarchive.util.C2paHelper import net.opendasharchive.openarchive.util.DateUtils import net.opendasharchive.openarchive.util.MediaThumbnailGenerator import net.opendasharchive.openarchive.util.Utility @@ -128,19 +127,6 @@ object MediaPicker { "" } - if (logC2pa) { - AppLogger.d("[C2PA_DEBUG] MediaPicker hash of copied file: $mediaHashString (file size=${file?.length()})") - val expectedManifest = C2paHelper.getC2paFile(context, mediaHashString) - AppLogger.d("[C2PA_DEBUG] Expected C2PA manifest path: ${expectedManifest.absolutePath}, exists=${expectedManifest.exists()}") - if (!expectedManifest.exists()) { - AppLogger.w("[C2PA_DEBUG] *** C2PA MANIFEST MISSING for hash $mediaHashString — upload will skip C2PA ***") - // List all existing manifests for comparison - val c2paDir = expectedManifest.parentFile - val existing = c2paDir?.listFiles()?.map { it.name } ?: emptyList() - AppLogger.d("[C2PA_DEBUG] Existing manifests in dir: $existing") - } - } - val thumbnail = try { file?.let { MediaThumbnailGenerator.generateThumbnailBytes(it, mimeType) } } catch (e: Exception) { diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/media/camera/CameraViewModel.kt b/app/src/main/java/net/opendasharchive/openarchive/features/media/camera/CameraViewModel.kt index f31bb97c..e60dab37 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/media/camera/CameraViewModel.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/media/camera/CameraViewModel.kt @@ -21,8 +21,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.core.logger.AppLogger -import net.opendasharchive.openarchive.util.C2paHelper import net.opendasharchive.openarchive.util.MetadataCollector +import net.opendasharchive.openarchive.util.ProofCompanionGenerator +import net.opendasharchive.openarchive.util.ProofmodeC2paManager import net.opendasharchive.openarchive.util.Utility import java.io.File import java.security.MessageDigest @@ -251,18 +252,16 @@ class CameraViewModel : ViewModel() { val metadata = MetadataCollector.collectMetadata(context) MetadataCollector.writeExifMetadata(file, metadata) AppLogger.d("[C2PA_DEBUG] EXIF written, file size after EXIF: ${file.length()}") - val hash = sha256(file) - AppLogger.d("[C2PA_DEBUG] SHA-256 of original capture file: $hash") - if (hash.isNotEmpty()) { - val manifest = C2paHelper.generateManifest( - context = context, - mediaFile = file, - mediaHash = hash, - metadata = buildProofMetadata(context, file, hash, metadata) - ) - AppLogger.d("[C2PA_DEBUG] Manifest generated: ${manifest?.absolutePath}, exists=${manifest?.exists()}") + val mimeType = android.webkit.MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(file.extension.lowercase()) + ?: "image/jpeg" + val signedFile = ProofmodeC2paManager.embedProof(file, mimeType, metadata, context) + if (signedFile != null) { + AppLogger.d("[C2PA] Proof embedded: ${signedFile.name}") + val hash = computeFileHash(signedFile) + if (hash.isNotEmpty()) ProofCompanionGenerator.generateLocalProof(context, signedFile, hash, metadata) } else { - AppLogger.w("[C2PA_DEBUG] Empty hash — manifest NOT generated for ${file.name}") + AppLogger.w("[C2PA] Proof embedding skipped/failed for ${file.name}") } } catch (e: Exception) { AppLogger.e("Provenance write failed for ${file.name}", e) @@ -273,81 +272,35 @@ class CameraViewModel : ViewModel() { try { AppLogger.d("[C2PA_DEBUG] writeProvenanceForVideo: file=${file.absolutePath} exists=${file.exists()} size=${file.length()}") val metadata = MetadataCollector.collectMetadata(context) - val hash = sha256(file) - AppLogger.d("[C2PA_DEBUG] SHA-256 of original capture file (video): $hash") - if (hash.isNotEmpty()) { - val manifest = C2paHelper.generateManifest( - context = context, - mediaFile = file, - mediaHash = hash, - metadata = buildProofMetadata(context, file, hash, metadata) - ) - AppLogger.d("[C2PA_DEBUG] Manifest generated: ${manifest?.absolutePath}, exists=${manifest?.exists()}") + val mimeType = android.webkit.MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(file.extension.lowercase()) + ?: "video/mp4" + val signedFile = ProofmodeC2paManager.embedProof(file, mimeType, metadata, context) + if (signedFile != null) { + AppLogger.d("[C2PA] Proof embedded: ${signedFile.name}") + val hash = computeFileHash(signedFile) + if (hash.isNotEmpty()) ProofCompanionGenerator.generateLocalProof(context, signedFile, hash, metadata) } else { - AppLogger.w("[C2PA_DEBUG] Empty hash — manifest NOT generated for ${file.name}") + AppLogger.w("[C2PA] Proof embedding skipped/failed for ${file.name}") } } catch (e: Exception) { AppLogger.e("Provenance write failed for ${file.name}", e) } } - private fun sha256(file: File): String = try { + private fun computeFileHash(file: File): String = try { val digest = MessageDigest.getInstance("SHA-256") file.inputStream().use { stream -> - val buffer = ByteArray(8192) - var bytesRead: Int - while (stream.read(buffer).also { bytesRead = it } != -1) { - digest.update(buffer, 0, bytesRead) - } + val buf = ByteArray(8192) + var n: Int + while (stream.read(buf).also { n = it } != -1) digest.update(buf, 0, n) } digest.digest().joinToString("") { "%02x".format(it) } } catch (e: Exception) { - AppLogger.e("SHA-256 hash failed for ${file.name}", e) + AppLogger.e("[C2PA] Hash computation failed for ${file.name}", e) "" } - private fun buildProofMetadata( - context: Context, - file: File, - hash: String, - metadata: MetadataCollector.CaptureMetadata - ): Map { - val isoFmt = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US).also { - it.timeZone = java.util.TimeZone.getTimeZone("UTC") - } - val mimeType = android.webkit.MimeTypeMap.getSingleton() - .getMimeTypeFromExtension(file.extension.lowercase()) - ?: "application/octet-stream" - return buildMap { - put("title", file.name) - put("mimeType", mimeType) - put("File Hash SHA256", hash) - put("File Path", file.absolutePath) - put("File Created", isoFmt.format(java.util.Date(metadata.captureTime))) - put("File Modified", isoFmt.format(java.util.Date(file.lastModified()))) - put("Proof Generated", isoFmt.format(java.util.Date(metadata.captureTime))) - put("Notes", "${metadata.appName} ${metadata.appVersion}") - put("Manufacturer", metadata.deviceMake) - put("Hardware", "${metadata.deviceMake} ${metadata.deviceModel}") - put("Locale", metadata.locale) - put("Language", metadata.language) - metadata.screenSizeInches?.let { put("ScreenSize", it.toString()) } - metadata.latitude?.let { put("Location.Latitude", it.toString()) } - metadata.longitude?.let { put("Location.Longitude", it.toString()) } - metadata.locationAltitude?.let { put("Location.Altitude", it.toString()) } - metadata.locationAccuracy?.let { put("Location.Accuracy", it.toString()) } - metadata.locationBearing?.let { put("Location.Bearing", it.toString()) } - metadata.locationSpeed?.let { put("Location.Speed", it.toString()) } - metadata.locationTime?.let { put("Location.Time", it.toString()) } - metadata.locationProvider?.let { put("Location.Provider", it) } - metadata.networkType?.let { put("NetworkType", it) } - metadata.networkType?.let { put("DataType", it) } - metadata.ipv4?.let { put("IPv4", it) } - metadata.ipv6?.let { put("IPv6", it) } - metadata.cellInfo?.let { put("CellInfo", it) } - } - } - fun stopVideoRecording() { if (_state.value.isRecording) { currentRecording?.stop() diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/C2paScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/C2paScreen.kt index cac53810..86c9392c 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/C2paScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/C2paScreen.kt @@ -62,7 +62,7 @@ fun C2paScreen( DefaultScaffold( topAppBar = { ComposeAppBar( - title = stringResource(R.string.c2pa_content_authenticity), + title = stringResource(R.string.proofmode), onNavigateBack = { onNavigateBack() } @@ -141,11 +141,11 @@ fun C2paScreenContent() { ) { Column(modifier = Modifier.weight(1f)) { Text( - text = stringResource(R.string.prefs_use_c2pa_title), + text = stringResource(R.string.prefs_use_proofmode_title), style = MaterialTheme.typography.bodyLarge ) Text( - text = stringResource(R.string.prefs_use_c2pa_summary), + text = stringResource(R.string.prefs_use_proofmode_summary), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -195,7 +195,7 @@ fun C2paScreenContent() { item { Box(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { HtmlText( - textRes = R.string.prefs_use_c2pa_description, + textRes = R.string.prefs_use_proofmode_description, linkRes = R.string.c2pa_learn_more_url, fontSize = 11.sp, linkColor = MaterialTheme.colorScheme.tertiary diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt index a2337746..e1c0170b 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/settings/SettingsScreen.kt @@ -175,8 +175,8 @@ fun SettingsScreen( summaryMediaServers = stringResource(R.string.pref_summary_media_servers), titleMediaFolders = stringResource(R.string.pref_title_media_folders), summaryMediaFolders = stringResource(R.string.pref_summary_media_folders), - titleC2pa = stringResource(R.string.c2pa_content_authenticity), - summaryC2pa = stringResource(R.string.prefs_use_c2pa_summary), + titleC2pa = stringResource(R.string.proofmode), + summaryC2pa = stringResource(R.string.prefs_use_proofmode_summary), titleTor = stringResource(R.string.prefs_use_tor_title), titleDarkMode = stringResource(R.string.pref_title_dark_mode), titleLanguage = stringResource(R.string.pref_title_language), diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/Conduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/Conduit.kt index 8bc47c84..6957219e 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/Conduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/Conduit.kt @@ -30,7 +30,7 @@ import net.opendasharchive.openarchive.services.internetarchive.data.IaConduit import net.opendasharchive.openarchive.services.webdav.data.WebDavConduit import net.opendasharchive.openarchive.upload.BroadcastManager import net.opendasharchive.openarchive.upload.UploadEventBus -import net.opendasharchive.openarchive.util.C2paHelper +import net.opendasharchive.openarchive.util.ProofCompanionGenerator import net.opendasharchive.openarchive.util.Prefs import net.opendasharchive.openarchive.util.toJavaDate import kotlinx.serialization.encodeToString @@ -125,35 +125,13 @@ abstract class Conduit( } /** - * Get C2PA manifest file for this media - * Returns the sidecar .c2pa.json file if C2PA is enabled and manifest exists + * Returns proof companion files for upload — camera captures only. + * Gallery imports never create the proof_companions dir, so they return empty here. */ - fun getC2paManifest(): File? { - if (!Prefs.useC2pa) { - AppLogger.d("[C2PA] Disabled, skipping manifest retrieval") - return null - } - - try { - AppLogger.d("[C2PA_DEBUG] getC2paManifest: evidenceHash=${mEvidence.mediaHashString} evidenceFile=${mEvidence.originalFilePath}") - val manifestFile = C2paHelper.getC2paFile(mContext, mEvidence.mediaHashString) - AppLogger.d("[C2PA_DEBUG] Looking for manifest at: ${manifestFile.absolutePath}") - - if (manifestFile.exists()) { - AppLogger.d("[C2PA] Manifest found: ${manifestFile.absolutePath}") - return manifestFile - } else { - AppLogger.w("[C2PA] Manifest not found for ${mEvidence.mediaHashString}") - // List existing manifests to help diagnose hash mismatch - val c2paDir = manifestFile.parentFile - val existing = c2paDir?.listFiles()?.map { it.name } ?: emptyList() - AppLogger.d("[C2PA_DEBUG] Existing manifests: $existing") - return null - } - } catch (exception: Exception) { - AppLogger.e("[C2PA] Error retrieving manifest", exception) - return null - } + suspend fun getProofFiles(): List { + val proofDir = java.io.File(mContext.filesDir, "proof_companions/${mEvidence.mediaHashString}") + if (!proofDir.exists()) return emptyList() + return ProofCompanionGenerator.prepareForUpload(mContext, mEvidence.file, mEvidence.mediaHashString) } /** diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/data/IaConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/data/IaConduit.kt index 361b58a9..b0f13a72 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/data/IaConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/data/IaConduit.kt @@ -133,11 +133,14 @@ class IaConduit(evidence: Evidence, context: Context) : Conduit(evidence, contex } } - // Upload metadata after content succeeds — non-fatal if it fails. - // IA item already exists with content; missing meta.json just means IA uses defaults. + // Upload metadata after content succeeds. Content + metadata are treated as one + // unit: a meta.json failure fails the whole job (jobFailed below via the outer + // catch) so the queue re-attempts. On retry, isAlreadyUploaded() (above) skips + // the already-present content and only meta.json is re-sent. var metadataUploaded = false var metaAttempt = 0 val maxMetaRetries = 3 + var lastMetaError: Throwable? = null while (!metadataUploaded && metaAttempt < maxMetaRetries) { if (metaAttempt > 0) delay(2_000L * metaAttempt) metaAttempt++ @@ -145,11 +148,12 @@ class IaConduit(evidence: Evidence, context: Context) : Conduit(evidence, contex client.uploadMetaData(metaJson, fileName, auth) metadataUploaded = true } catch (e: Throwable) { + lastMetaError = e AppLogger.w("meta.json attempt $metaAttempt/$maxMetaRetries failed for $fileName: ${e.message}") } } if (!metadataUploaded) { - AppLogger.e("meta.json failed all retries for $fileName — item uploaded without custom metadata") + throw lastMetaError ?: IOException("meta.json upload failed for $fileName") } jobSucceeded() diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/data/WebDavConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/data/WebDavConduit.kt index bc149c72..433e2fb5 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/webdav/data/WebDavConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/webdav/data/WebDavConduit.kt @@ -260,17 +260,21 @@ class WebDavConduit(evidence: Evidence, context: Context) : Conduit(evidence, co .build() ) - val c2paManifest = getC2paManifest() - if (c2paManifest != null) { + // Upload ProofMode companion files: .proof.json, .asc (PGP sigs), .ots (OpenTimestamps), pubkey.asc + for (proofFile in getProofFiles()) { if (mCancelled) throw Exception("Cancelled") - AppLogger.d("Uploading C2PA manifest: ${c2paManifest.name}") + AppLogger.d("[ProofMode] Uploading companion file: ${proofFile.name}") execute( Request.Builder() - .url(construct(base, path, c2paManifest.name)) - .put(c2paManifest.readBytes().toRequestBody("application/json".toMediaTypeOrNull())) + .url(construct(base, path, proofFile.name)) + .put(proofFile.readBytes().toRequestBody("application/octet-stream".toMediaTypeOrNull())) .build() ) } + + // Delete companion files from local storage after successful upload + java.io.File(mContext.filesDir, "proof_companions/${mEvidence.mediaHashString}") + .deleteRecursively() } // --- WebDAV HTTP helpers --- diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/C2paFfi.kt b/app/src/main/java/net/opendasharchive/openarchive/util/C2paFfi.kt deleted file mode 100644 index ea448623..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/util/C2paFfi.kt +++ /dev/null @@ -1,145 +0,0 @@ -package net.opendasharchive.openarchive.util - -import net.opendasharchive.openarchive.core.logger.AppLogger - -/** - * JNI wrapper for C2PA Rust FFI library - * - * This class provides a Kotlin interface to the native Rust implementation of C2PA. - * The Rust library is compiled from source for FOSS builds, ensuring F-Droid compliance. - */ -object C2paFfi { - - private var initialized = false - private var libraryLoaded = false - - init { - try { - // Load the native library compiled from Rust - System.loadLibrary("c2pa_ffi") - libraryLoaded = true - AppLogger.d("C2PA FFI native library loaded successfully") - } catch (e: UnsatisfiedLinkError) { - AppLogger.e("Failed to load C2PA FFI native library", e) - AppLogger.e("Make sure Rust is installed and the library is compiled") - libraryLoaded = false - } - } - - /** - * Initialize the C2PA FFI library - * Must be called before using any other functions - * - * @return true if initialization succeeded, false otherwise - */ - fun initialize(): Boolean { - if (!libraryLoaded) { - AppLogger.w("Cannot initialize C2PA FFI - native library not loaded") - return false - } - - if (initialized) { - AppLogger.d("C2PA FFI already initialized") - return true - } - - return try { - val result = nativeInit() - initialized = result - if (result) { - AppLogger.i("C2PA FFI initialized successfully") - } else { - AppLogger.w("C2PA FFI initialization returned false") - } - result - } catch (e: Exception) { - AppLogger.e("Failed to initialize C2PA FFI", e) - false - } - } - - /** - * Generate a C2PA manifest for a media file - * - * @param filePath Absolute path to the media file - * @param metadataJson JSON string containing metadata (title, description, author, etc.) - * @return JSON string containing the C2PA manifest, or null if generation failed - */ - fun generateManifest(filePath: String, metadataJson: String): String? { - if (!initialized) { - AppLogger.w("C2PA FFI not initialized, attempting to initialize now") - if (!initialize()) { - AppLogger.e("Cannot generate manifest - FFI initialization failed") - return null - } - } - - return try { - val result = nativeGenerateManifest(filePath, metadataJson) - if (result != null) { - AppLogger.d("C2PA manifest generated for: $filePath") - } else { - AppLogger.w("C2PA manifest generation returned null for: $filePath") - } - result - } catch (e: Exception) { - AppLogger.e("Failed to generate C2PA manifest", e) - null - } - } - - /** - * Verify a C2PA manifest - * - * @param manifestJson JSON string containing the C2PA manifest - * @return true if the manifest is valid, false otherwise - */ - fun verifyManifest(manifestJson: String): Boolean { - if (!initialized) { - AppLogger.w("C2PA FFI not initialized, attempting to initialize now") - if (!initialize()) { - AppLogger.e("Cannot verify manifest - FFI initialization failed") - return false - } - } - - return try { - val result = nativeVerifyManifest(manifestJson) - AppLogger.d("C2PA manifest verification result: $result") - result - } catch (e: Exception) { - AppLogger.e("Failed to verify C2PA manifest", e) - false - } - } - - /** - * Check if the native library is loaded and ready to use - * - * @return true if library is loaded, false otherwise - */ - fun isAvailable(): Boolean { - return libraryLoaded && initialized - } - - // Native method declarations - // These are implemented in rust-c2pa-ffi/src/lib.rs - - /** - * Initialize the C2PA FFI library (native implementation) - */ - private external fun nativeInit(): Boolean - - /** - * Generate C2PA manifest (native implementation) - */ - private external fun nativeGenerateManifest( - filePath: String, - metadataJson: String - ): String? - - /** - * Verify C2PA manifest (native implementation) - */ - private external fun nativeVerifyManifest(manifestJson: String): Boolean -} diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/C2paHelper.kt b/app/src/main/java/net/opendasharchive/openarchive/util/C2paHelper.kt deleted file mode 100644 index d80b7185..00000000 --- a/app/src/main/java/net/opendasharchive/openarchive/util/C2paHelper.kt +++ /dev/null @@ -1,178 +0,0 @@ -package net.opendasharchive.openarchive.util - -import android.content.Context -import net.opendasharchive.openarchive.BuildConfig -import net.opendasharchive.openarchive.core.logger.AppLogger -import java.io.File - -/** - * C2PA Helper for generating cryptographic content provenance manifests - * - * This implementation uses a custom sidecar approach: - * - Generates C2PA manifests - * - Extracts manifest JSON before embedding - * - Saves as separate .c2pa.json files - * - Original files remain COMPLETELY UNTOUCHED (MD5 preserved) - */ -object C2paHelper { - - private var initialized = false - - /** - * Initialize C2PA library - */ - fun init(context: Context) { - if (initialized) return - - try { - // Try to initialize FFI (FOSS builds with Rust compiled from source) - if (C2paFfi.initialize()) { - initialized = true - AppLogger.i("C2PA Helper initialized with FFI (Rust native)") - } else { - // Fall back to stub for GMS builds or if FFI unavailable - initialized = true - AppLogger.d("C2PA Helper initialized (stub implementation - FFI unavailable)") - } - } catch (e: Exception) { - AppLogger.e("Failed to initialize C2PA", e) - // Still mark as initialized to allow stub functionality - initialized = true - } - } - - /** - * Generate C2PA manifest for a media file - * Saves as SIDECAR .c2pa.json file (does NOT modify original) - * - * @param context Application context - * @param mediaFile The original media file - * @param mediaHash MD5 hash string for the media - * @param metadata Additional metadata to include in manifest - * @return The sidecar manifest file, or null if disabled/failed - */ - fun generateManifest( - context: Context, - mediaFile: File, - mediaHash: String, - metadata: Map = emptyMap() - ): File? { - AppLogger.d("[C2PA] generateManifest called - mediaFile: ${mediaFile.absolutePath}, mediaHash: $mediaHash") - - if (!Prefs.useC2pa) { - AppLogger.w("[C2PA] Disabled in preferences") - return null - } - - if (!mediaFile.exists()) { - AppLogger.e("[C2PA] Media file does not exist: ${mediaFile.absolutePath}") - return null - } - - AppLogger.d("[C2PA] Media file exists, size: ${mediaFile.length()} bytes") - - try { - // Try to use FFI implementation (FOSS builds) - val manifestJson = if (C2paFfi.isAvailable()) { - AppLogger.d("[C2PA] Using FFI to generate manifest") - val metadataJson = metadata.toJsonString() - AppLogger.d("[C2PA] Calling FFI with metadata: $metadataJson") - val result = C2paFfi.generateManifest(mediaFile.absolutePath, metadataJson) - if (result == null) { - AppLogger.w("[C2PA] FFI returned null, falling back to stub") - buildStubManifestJson(mediaFile, metadata) - } else { - AppLogger.d("[C2PA] FFI returned manifest, length: ${result.length}") - result - } - } else { - AppLogger.d("[C2PA] FFI unavailable, using stub implementation") - buildStubManifestJson(mediaFile, metadata) - } - - // Save as sidecar file - val sidecarFile = getC2paFile(context, mediaHash) - AppLogger.d("[C2PA] Sidecar file path: ${sidecarFile.absolutePath}") - - val parentCreated = sidecarFile.parentFile?.mkdirs() ?: false - AppLogger.d("[C2PA] Parent directory created/exists: $parentCreated, path: ${sidecarFile.parentFile?.absolutePath}") - - sidecarFile.writeText(manifestJson) - AppLogger.i("[C2PA] Manifest saved successfully: ${sidecarFile.absolutePath}, size: ${sidecarFile.length()} bytes") - - return sidecarFile - - } catch (e: Exception) { - AppLogger.e("[C2PA] Failed to generate manifest", e) - return null - } - } - - /** - * Temporary stub implementation - builds a placeholder manifest JSON - * This will be replaced with actual C2PA library calls - */ - private fun buildStubManifestJson( - mediaFile: File, - metadata: Map - ): String { - // TODO: Replace with actual C2PA manifest generation using simple-c2pa - return """ - { - "claim_generator": "OpenArchive Save/${BuildConfig.VERSION_NAME}", - "assertions": [ - { - "label": "stds.schema-org.CreativeWork", - "data": ${metadata.toJsonString()} - } - ], - "signature": { - "note": "Placeholder - will be replaced with actual C2PA signature" - } - } - """.trimIndent() - } - - /** - * Convert metadata map to JSON string - */ - private fun Map.toJsonString(): String { - // Properly escape JSON values - val jsonEntries = this.entries.joinToString(",\n ") { (key, value) -> - val escapedValue = value - .replace("\\", "\\\\") // Escape backslashes - .replace("\"", "\\\"") // Escape quotes - .replace("\n", "\\n") // Escape newlines - .replace("\r", "\\r") // Escape carriage returns - .replace("\t", "\\t") // Escape tabs - "\"$key\": \"$escapedValue\"" - } - return "{\n $jsonEntries\n}" - } - - /** - * Get the sidecar C2PA manifest file for a given media hash - * - * @param context Application context - * @param mediaHash MD5 hash string for the media - * @return File object pointing to where the .c2pa.json sidecar should be stored - */ - fun getC2paFile(context: Context, mediaHash: String): File { - val c2paDir = File(context.filesDir, "c2pa_manifests") - return File(c2paDir, "$mediaHash.c2pa.json") - } - - /** - * Remove C2PA manifest files for a given media hash - * - * @param context Application context - * @param mediaHash MD5 hash string for the media - */ - fun removeC2paFiles(context: Context, mediaHash: String) { - val c2paFile = getC2paFile(context, mediaHash) - if (c2paFile.exists()) { - c2paFile.delete() - AppLogger.d("Deleted C2PA manifest: ${c2paFile.absolutePath}") - } - } -} diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/MetadataCollector.kt b/app/src/main/java/net/opendasharchive/openarchive/util/MetadataCollector.kt index 3a3800c1..affb5109 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/util/MetadataCollector.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/util/MetadataCollector.kt @@ -217,9 +217,24 @@ object MetadataCollector { "${metadata.appName} ${metadata.appVersion}" ) - // --- Capture datetime --- + // --- Capture datetime (local time, per EXIF spec) --- val dtFormat = SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US) - exif.setAttribute(ExifInterface.TAG_DATETIME, dtFormat.format(Date(metadata.captureTime))) + val captureDate = Date(metadata.captureTime) + val localDateStr = dtFormat.format(captureDate) + exif.setAttribute(ExifInterface.TAG_DATETIME, localDateStr) + exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, localDateStr) + exif.setAttribute(ExifInterface.TAG_DATETIME_DIGITIZED, localDateStr) + + // UTC offset (EXIF 2.31) — makes timestamps timezone-unambiguous for forensic tools + val offsetMs = java.util.TimeZone.getDefault().getOffset(metadata.captureTime) + val offsetSign = if (offsetMs >= 0) "+" else "-" + val absOffset = kotlin.math.abs(offsetMs) + val offsetHours = absOffset / 3_600_000 + val offsetMins = (absOffset % 3_600_000) / 60_000 + val offsetStr = "%s%02d:%02d".format(offsetSign, offsetHours, offsetMins) + exif.setAttribute(ExifInterface.TAG_OFFSET_TIME, offsetStr) + exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_ORIGINAL, offsetStr) + exif.setAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED, offsetStr) exif.saveAttributes() AppLogger.d("[Metadata] EXIF written to ${file.name}") @@ -369,7 +384,10 @@ object MetadataCollector { if (entries.isEmpty()) return null entries.joinToString(",", "[", "]") { entry -> - entry.entries.joinToString(",", "{", "}") { (k, v) -> "\"$k\":$v" } + // Android returns Int.MAX_VALUE as sentinel for unavailable cell fields — exclude them + entry.entries + .filter { (_, v) -> v !is Int || (v != Int.MAX_VALUE && v != Int.MIN_VALUE) } + .joinToString(",", "{", "}") { (k, v) -> "\"$k\":$v" } } } catch (e: Exception) { AppLogger.w("[Metadata] CellInfo unavailable: ${e.message}") diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/ProofCompanionGenerator.kt b/app/src/main/java/net/opendasharchive/openarchive/util/ProofCompanionGenerator.kt new file mode 100644 index 00000000..a7ee5b97 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/util/ProofCompanionGenerator.kt @@ -0,0 +1,632 @@ +package net.opendasharchive.openarchive.util + +import android.content.Context +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import androidx.exifinterface.media.ExifInterface +import net.opendasharchive.openarchive.BuildConfig +import net.opendasharchive.openarchive.core.logger.AppLogger +import org.bouncycastle.bcpg.ArmoredOutputStream +import org.bouncycastle.bcpg.BCPGOutputStream +import org.bouncycastle.bcpg.HashAlgorithmTags +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openpgp.PGPEncryptedData +import org.bouncycastle.openpgp.PGPKeyRingGenerator +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPUtil +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator +import org.bouncycastle.openpgp.PGPSignature +import org.bouncycastle.openpgp.PGPSignatureGenerator +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder +import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder +import java.io.ByteArrayOutputStream +import java.io.File +import java.net.HttpURLConnection +import java.net.URL +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.SecureRandom +import java.security.Security +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.Date +import java.util.Locale +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.GCMParameterSpec + +object ProofCompanionGenerator { + + private const val SECRET_KEY_FILE = "proof_pgp_secret.bpg" + private const val PUBLIC_KEY_FILE = "pubkey.asc" + private const val PENDING_OTS_FILE = "pending_ots.txt" + private const val OTS_QUEUE_MAX = 500 + private const val PGP_IDENTITY = "OpenArchive Save " + private const val OTS_CALENDAR = "https://a.pool.opentimestamps.org/digest" + private const val OTS_TIMEOUT_MS = 15_000 + + // OpenTimestamps detached-file format: fixed 31-byte magic + 1-byte major version. + // Every valid .ots file (and every OTS verifier, incl. opentimestamps.org) requires this + // preamble before the hash-op tag + digest + serialized timestamp. + private val OTS_HEADER_MAGIC = byteArrayOf( + 0x00, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, + 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x00, + 0xbf.toByte(), 0x89.toByte(), 0xe2.toByte(), 0xe8.toByte(), 0x84.toByte(), 0xe8.toByte(), 0x92.toByte(), 0x94.toByte() + ) + private const val OTS_MAJOR_VERSION: Byte = 0x01 + private const val OTS_OP_SHA256: Byte = 0x08 + + // PGP BouncyCastle API requires a passphrase for its internal AES layer. + // The secret key ring file itself is additionally encrypted at rest via a + // Keystore-backed AES-256-GCM key (see writeEncryptedKey / readEncryptedKey). + private val PASSPHRASE = charArrayOf() + + private const val KEYSTORE_ALIAS = "openarchive_pgp_wrap_key" + private const val KEYSTORE_PROVIDER = "AndroidKeyStore" + private const val AES_GCM_TRANSFORM = "AES/GCM/NoPadding" + private const val GCM_TAG_LEN = 128 + + // DateTimeFormatter is immutable + thread-safe — safe for concurrent capture coroutines + private val isoFmt: DateTimeFormatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").withZone(ZoneOffset.UTC) + + fun init(context: Context) { + try { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.insertProviderAt(BouncyCastleProvider(), 1) + ensureKeyExists(context) + AppLogger.i("[Proof] ProofCompanionGenerator initialized") + } catch (e: Exception) { + AppLogger.e("[Proof] Init failed", e) + } + } + + /** + * Called at capture time. Generates proof.json, proof.csv, PGP signatures, and + * HowToVerifyProofData.txt immediately after C2PA embedding while the file bytes are + * known-final. Passing [metadata] ensures all sensor/device fields make it into the proof + * without round-tripping through EXIF. Also submits the OTS timestamp and requests device + * attestation (.gst) right away — all proof material is complete at capture time; nothing + * is generated later at upload time. + */ + suspend fun generateLocalProof( + context: Context, + evidenceFile: File, + mediaHash: String, + metadata: MetadataCollector.CaptureMetadata? = null + ) { + if (!Prefs.useC2pa) return + withContext(Dispatchers.IO) { + try { + val outDir = File(context.filesDir, "proof_companions/$mediaHash").also { it.mkdirs() } + + val fields = buildProofFields(evidenceFile, mediaHash, metadata) + + val proofJson = buildProofJson(fields) + val proofCsv = buildProofCsv(fields) + + val proofJsonFile = File(outDir, "$mediaHash.proof.json") + val proofCsvFile = File(outDir, "$mediaHash.proof.csv") + proofJsonFile.writeText(proofJson) + proofCsvFile.writeText(proofCsv) + + pgpSignStream(context, evidenceFile, File(outDir, "$mediaHash.asc")) + pgpSign(context, proofJson.toByteArray(Charsets.UTF_8), File(outDir, "$mediaHash.proof.json.asc")) + pgpSign(context, proofCsv.toByteArray(Charsets.UTF_8), File(outDir, "$mediaHash.proof.csv.asc")) + + File(outDir, "HowToVerifyProofData.txt").writeText( + buildHowToVerify(evidenceFile.name, mediaHash) + ) + + // OTS calendar submission and device attestation — done now so every proof + // artifact exists immediately after capture, not deferred to upload time. + submitOts(context, mediaHash, File(outDir, "$mediaHash.ots")) + SafetyNetHelper.requestGst(context, mediaHash, File(outDir, "$mediaHash.gst")) + + AppLogger.i("[Proof] Local proof generated at capture time for $mediaHash") + } catch (e: Exception) { + AppLogger.e("[Proof] Local proof generation failed for $mediaHash", e) + } + } + } + + /** + * Called at upload time. All proof material — including OTS and .gst — is generated at + * capture time by [generateLocalProof]; this only assembles the file list, with a + * defensive fallback that regenerates anything missing (should not occur for camera + * captures, but covers edge cases like a process death between capture and upload). + */ + suspend fun prepareForUpload(context: Context, evidenceFile: File, mediaHash: String): List = withContext(Dispatchers.IO) { + if (!Prefs.useC2pa) return@withContext emptyList() + try { + val outDir = File(context.filesDir, "proof_companions/$mediaHash").also { it.mkdirs() } + val proofJsonFile = File(outDir, "$mediaHash.proof.json") + val proofCsvFile = File(outDir, "$mediaHash.proof.csv") + val mediaSigFile = File(outDir, "$mediaHash.asc") + val jsonSigFile = File(outDir, "$mediaHash.proof.json.asc") + val csvSigFile = File(outDir, "$mediaHash.proof.csv.asc") + val howToFile = File(outDir, "HowToVerifyProofData.txt") + val otsFile = File(outDir, "$mediaHash.ots") + val gstFile = File(outDir, "$mediaHash.gst") + + // Defensive fallback — should not occur for camera captures but handles edge cases + // (e.g. process death between capture and upload) + if (!proofJsonFile.exists() || !mediaSigFile.exists() || !jsonSigFile.exists()) { + AppLogger.w("[Proof] Local proof missing for $mediaHash — generating at upload time") + val fields = buildProofFields(evidenceFile, mediaHash, null) + proofJsonFile.writeText(buildProofJson(fields)) + proofCsvFile.writeText(buildProofCsv(fields)) + pgpSignStream(context, evidenceFile, mediaSigFile) + pgpSign(context, proofJsonFile.readBytes(), jsonSigFile) + pgpSign(context, proofCsvFile.readBytes(), csvSigFile) + howToFile.writeText(buildHowToVerify(evidenceFile.name, mediaHash)) + } + if (!otsFile.exists()) { + AppLogger.w("[Proof] OTS missing for $mediaHash — submitting at upload time") + submitOts(context, mediaHash, otsFile) + } + if (!gstFile.exists()) { + SafetyNetHelper.requestGst(context, mediaHash, gstFile) + } + + val pubKeyFile = File(context.filesDir, PUBLIC_KEY_FILE) + buildList { + add(proofJsonFile) + add(proofCsvFile) + add(mediaSigFile) + add(jsonSigFile) + if (csvSigFile.exists()) add(csvSigFile) + if (pubKeyFile.exists()) add(pubKeyFile) + if (otsFile.exists()) add(otsFile) + if (gstFile.exists()) add(gstFile) + if (howToFile.exists()) add(howToFile) + }.also { + AppLogger.i("[Proof] ${it.size} companion files ready for upload: $mediaHash") + } + } catch (e: Exception) { + AppLogger.e("[Proof] Upload prep failed for $mediaHash", e) + emptyList() + } + } + + // --------------------------------------------------------------------------- + // Proof fields — shared between JSON and CSV builders + // --------------------------------------------------------------------------- + + private fun buildProofFields( + file: File, + hash: String, + metadata: MetadataCollector.CaptureMetadata? + ): LinkedHashMap { + val now = isoFmt.format(Instant.now()) + + val exifDtFormat = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss").withZone(ZoneOffset.UTC) + val created = try { + ExifInterface(file.absolutePath) + .getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL) + ?.let { raw -> + val ldt = java.time.LocalDateTime.parse(raw, exifDtFormat) + isoFmt.format(ldt.toInstant(ZoneOffset.UTC)) + } + ?: isoFmt.format(Instant.ofEpochMilli(file.lastModified())) + } catch (_: Exception) { + isoFmt.format(Instant.ofEpochMilli(file.lastModified())) + } + + val fields = linkedMapOf() + fields["File Hash SHA256"] = hash + fields["File Path"] = file.absolutePath + fields["File Created"] = created + fields["File Modified"] = created + fields["Proof Generated"] = now + fields["Notes"] = "${metadata?.appName ?: "OpenArchive Save"} ${BuildConfig.VERSION_NAME}" + fields["App.Name"] = metadata?.appName + fields["Manufacturer"] = metadata?.deviceMake ?: Build.MANUFACTURER + fields["Device.Brand"] = metadata?.deviceBrand ?: Build.BRAND + fields["Device.Model"] = metadata?.deviceModel ?: Build.MODEL + fields["Hardware"] = "${metadata?.deviceMake ?: Build.MANUFACTURER} ${metadata?.deviceModel ?: Build.MODEL}" + fields["Locale"] = metadata?.locale ?: Locale.getDefault().country + fields["Language"] = metadata?.language ?: Locale.getDefault().displayLanguage + fields["Screen.Size"] = metadata?.screenSizeInches?.let { "%.2f".format(it) } + fields["Network.Type"] = metadata?.networkType + fields["Network.IPv4"] = metadata?.ipv4 + fields["Network.IPv6"] = metadata?.ipv6 + fields["Network.CellInfo"] = metadata?.cellInfo + + // GPS — prefer live metadata, fall back to EXIF + if (metadata?.latitude != null && metadata.longitude != null) { + fields["Location.Latitude"] = metadata.latitude.toString() + fields["Location.Longitude"] = metadata.longitude.toString() + metadata.locationAltitude?.let { fields["Location.Altitude"] = it.toString() } + metadata.locationSpeed?.let { fields["Location.Speed"] = it.toString() } + metadata.locationBearing?.let { fields["Location.Bearing"] = it.toString() } + metadata.locationProvider?.let { fields["Location.Provider"] = it } + metadata.locationAccuracy?.let { fields["Location.Accuracy"] = it.toString() } + metadata.locationTime?.let { fields["Location.Time"] = it.toString() } + } else { + try { + val exif = ExifInterface(file.absolutePath) + val latLon = exif.latLong + if (latLon != null) { + fields["Location.Latitude"] = latLon[0].toString() + fields["Location.Longitude"] = latLon[1].toString() + } + exif.getAttribute(ExifInterface.TAG_GPS_ALTITUDE) + ?.let { fields["Location.Altitude"] = it } + exif.getAttribute(ExifInterface.TAG_GPS_SPEED) + ?.let { fields["Location.Speed"] = it } + exif.getAttribute(ExifInterface.TAG_GPS_TRACK) + ?.let { fields["Location.Bearing"] = it } + exif.getAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD) + ?.removePrefix("charset=Ascii ")?.lowercase() + ?.let { fields["Location.Provider"] = it } + } catch (_: Exception) {} + } + + return fields + } + + // --------------------------------------------------------------------------- + // Proof JSON — ProofMode v1 field names + // --------------------------------------------------------------------------- + + private fun buildProofJson(fields: LinkedHashMap): String { + val sb = StringBuilder("{\n") + val entries = fields.entries.filter { it.value != null }.toList() + entries.forEachIndexed { i, (k, v) -> + sb.append(" ").append(jsonString(k)).append(": ").append(jsonString(v!!)) + if (i < entries.size - 1) sb.append(",") + sb.append("\n") + } + sb.append("}") + return sb.toString() + } + + // --------------------------------------------------------------------------- + // Proof CSV — same fields, header + data row + // --------------------------------------------------------------------------- + + private fun buildProofCsv(fields: LinkedHashMap): String { + val present = fields.entries.filter { it.value != null } + val header = present.joinToString(",") { csvEscape(it.key) } + val data = present.joinToString(",") { csvEscape(it.value!!) } + return "$header\n$data\n" + } + + // --------------------------------------------------------------------------- + // HowToVerifyProofData.txt — dynamically generated with real hash/filename + // --------------------------------------------------------------------------- + + private fun buildHowToVerify(mediaFileName: String, hash: String): String = """ +Brief information on how to verify the media file, proof and signatures contained in a ProofMode bundle. +Please visit https://proofmode.org or email support@guardianproject.info for more information. + +1) Import public key shared from ProofMode: + +gpg --import pubkey.asc + +gpg: key xxx: public key "proof@openarchive.app" imported +gpg: Total number processed: 1 +gpg: imported: 1 + +2) Check the hash of the media file against the hash in the proof metadata: + +sha256sum $mediaFileName + +$hash $mediaFileName + +3) Verify signature of the media file: + +gpg --dearmor pubkey.asc +gpg --no-default-keyring --keyring ./pubkey.asc.gpg --homedir ./ --verify $hash.asc $mediaFileName + +gpg: Good signature from "proof@openarchive.app" [unknown] + +4) Verify signature of the ProofMode CSV data: + +gpg --verify $hash.proof.csv.asc $hash.proof.csv + +gpg: Good signature from "proof@openarchive.app" [unknown] + +5) Verify signature of the ProofMode JSON data: + +gpg --verify $hash.proof.json.asc $hash.proof.json + +gpg: Good signature from "proof@openarchive.app" [unknown] + +6) If a .ots file is present, visit https://opentimestamps.org/ and upload $hash.ots to verify + the Bitcoin blockchain notarisation. (It can take several hours for the timestamp to confirm.) + +7) If a .gst file is present, that is a JWT from the Google Play Integrity API attesting device + integrity at capture time. Decode the JWT value at https://jwt.io/ to inspect the claims. +""".trimStart() + + // --------------------------------------------------------------------------- + // PGP key generation and detached signing + // --------------------------------------------------------------------------- + + private fun ensureKeyExists(context: Context) { + val secretFile = File(context.filesDir, SECRET_KEY_FILE) + val publicFile = File(context.filesDir, PUBLIC_KEY_FILE) + if (secretFile.exists() && publicFile.exists()) return + + AppLogger.i("[Proof] Generating Ed25519 PGP key pair") + + val kpg = KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME) + kpg.initialize(256, SecureRandom()) + val keyPair = kpg.generateKeyPair() + + val digestCalc = JcaPGPDigestCalculatorProviderBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build() + .get(HashAlgorithmTags.SHA1) + + val pgpKeyPair = JcaPGPKeyPair(PGPPublicKey.EDDSA, keyPair, Date()) + + val secretKeyEncryptor = JcePBESecretKeyEncryptorBuilder( + PGPEncryptedData.AES_256, digestCalc + ).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(PASSPHRASE) + + val signerBuilder = JcaPGPContentSignerBuilder( + pgpKeyPair.publicKey.algorithm, HashAlgorithmTags.SHA256 + ).setProvider(BouncyCastleProvider.PROVIDER_NAME) + + val keyRingGen = PGPKeyRingGenerator( + PGPSignature.POSITIVE_CERTIFICATION, + pgpKeyPair, + PGP_IDENTITY, + digestCalc, + null, + null, + signerBuilder, + secretKeyEncryptor + ) + + // Write secret key ring — encrypted with Keystore-backed AES-256-GCM + val secretBytes = ByteArrayOutputStream() + .also { keyRingGen.generateSecretKeyRing().encode(it) }.toByteArray() + writeEncryptedKey(secretFile, secretBytes) + + // Write public key in ASCII armor + publicFile.outputStream().use { out -> + ArmoredOutputStream(out).use { armoredOut -> + keyRingGen.generatePublicKeyRing().encode(armoredOut) + } + } + AppLogger.i("[Proof] PGP key pair written") + } + + private fun pgpSign(context: Context, data: ByteArray, outFile: File) { + val sigGen = initPgpSigGen(context) ?: return + sigGen.update(data) + writePgpSig(sigGen, outFile) + } + + private fun pgpSignStream(context: Context, sourceFile: File, outFile: File) { + val sigGen = initPgpSigGen(context) ?: return + sourceFile.inputStream().use { stream -> + val buf = ByteArray(8192) + var n: Int + while (stream.read(buf).also { n = it } != -1) sigGen.update(buf, 0, n) + } + writePgpSig(sigGen, outFile) + } + + private fun initPgpSigGen(context: Context): PGPSignatureGenerator? { + val secretFile = File(context.filesDir, SECRET_KEY_FILE) + if (!secretFile.exists()) { + ensureKeyExists(context) + if (!secretFile.exists()) return null + } + val secretBytes = readEncryptedKey(secretFile) ?: run { + // Keystore wrap key lost — wipe and regenerate both PGP key files + AppLogger.w("[Proof] Wrap key lost — wiping stale PGP key files and regenerating") + File(context.filesDir, SECRET_KEY_FILE).delete() + File(context.filesDir, PUBLIC_KEY_FILE).delete() + ensureKeyExists(context) + readEncryptedKey(File(context.filesDir, SECRET_KEY_FILE)) ?: run { + AppLogger.e("[Proof] Failed to decrypt PGP secret key after regeneration") + return null + } + } + val secretKeyRing = PGPSecretKeyRing( + PGPUtil.getDecoderStream(secretBytes.inputStream()), + JcaKeyFingerprintCalculator() + ) + val secretKey = secretKeyRing.secretKey + val privateKey = secretKey.extractPrivateKey( + JcePBESecretKeyDecryptorBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .build(PASSPHRASE) + ) + return PGPSignatureGenerator( + JcaPGPContentSignerBuilder(secretKey.publicKey.algorithm, HashAlgorithmTags.SHA256) + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + ).also { it.init(PGPSignature.BINARY_DOCUMENT, privateKey) } + } + + private fun writePgpSig(sigGen: PGPSignatureGenerator, outFile: File) { + ByteArrayOutputStream().use { baos -> + ArmoredOutputStream(baos).use { armoredOut -> + BCPGOutputStream(armoredOut).use { bcpgOut -> + sigGen.generate().encode(bcpgOut) + } + } + outFile.writeBytes(baos.toByteArray()) + } + } + + // --------------------------------------------------------------------------- + // OpenTimestamps — submit SHA256 hash to OTS calendar with retry queue + // --------------------------------------------------------------------------- + + private fun submitOts(context: Context, hexHash: String, outFile: File) { + // Drain any previously failed hashes before submitting the new one + drainPendingOts(context) + + if (!postOts(hexHash, outFile)) { + // Network unavailable — queue for next upload + enqueuePendingOts(context, hexHash) + AppLogger.w("[OTS] Queued $hexHash for retry") + } + } + + private fun drainPendingOts(context: Context) { + val queue = File(context.filesDir, PENDING_OTS_FILE) + if (!queue.exists()) return + val remaining = mutableListOf() + queue.readLines().filter { it.isNotBlank() }.forEach { hash -> + val companionDir = File(context.filesDir, "proof_companions/$hash") + // Companion dir deleted after upload — nothing left to anchor, drop from queue + if (!companionDir.exists()) return@forEach + val otsFile = File(companionDir, "$hash.ots") + if (otsFile.exists()) return@forEach // already succeeded + if (!postOts(hash, otsFile)) remaining.add(hash) + } + if (remaining.isEmpty()) queue.delete() else queue.writeText(remaining.joinToString("\n")) + } + + @Synchronized + private fun enqueuePendingOts(context: Context, hexHash: String) { + val queue = File(context.filesDir, PENDING_OTS_FILE) + val existing = if (queue.exists()) queue.readLines().filter { it.isNotBlank() } else emptyList() + if (hexHash in existing) return // already queued — no duplicate entries + val updated = (existing + hexHash).let { + if (it.size > OTS_QUEUE_MAX) it.drop(it.size - OTS_QUEUE_MAX) else it // bound size, drop oldest + } + queue.writeText(updated.joinToString("\n") + "\n") + } + + private fun postOts(hexHash: String, outFile: File): Boolean { + return try { + val hashBytes = hexToBytes(hexHash) + val conn = (URL(OTS_CALENDAR).openConnection() as HttpURLConnection).apply { + requestMethod = "POST" + doOutput = true + connectTimeout = OTS_TIMEOUT_MS + readTimeout = OTS_TIMEOUT_MS + setRequestProperty("Content-Type", "application/octet-stream") + setRequestProperty("Accept", "application/octet-stream") + } + conn.outputStream.use { it.write(hashBytes) } + val success = conn.responseCode == 200 + if (success) { + val calendarResponse = conn.inputStream.use { it.readBytes() } + outFile.parentFile?.mkdirs() + // Assemble a spec-compliant detached .ots: magic header + version + hash-op tag + // + digest, followed by the calendar's serialized (pending) timestamp. Writing + // the raw calendar response alone (previous behavior) produced a file no OTS + // verifier could read. + outFile.outputStream().use { out -> + out.write(OTS_HEADER_MAGIC) + out.write(byteArrayOf(OTS_MAJOR_VERSION, OTS_OP_SHA256)) + out.write(hashBytes) + out.write(calendarResponse) + } + AppLogger.i("[OTS] Timestamp received for $hexHash") + } else { + AppLogger.w("[OTS] Calendar returned HTTP ${conn.responseCode}") + } + conn.disconnect() + success + } catch (e: Exception) { + AppLogger.w("[OTS] Submission failed: ${e.message}") + false + } + } + + // --------------------------------------------------------------------------- + // AES-256-GCM key wrapping for PGP secret key ring + // --------------------------------------------------------------------------- + + private fun ensureWrapKey() { + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + if (ks.containsAlias(KEYSTORE_ALIAS)) return + val spec = KeyGenParameterSpec.Builder( + KEYSTORE_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build() + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER) + .also { it.init(spec) } + .generateKey() + } + + private fun writeEncryptedKey(file: File, plaintext: ByteArray) { + ensureWrapKey() + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + val key = ks.getKey(KEYSTORE_ALIAS, null) as javax.crypto.SecretKey + val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) + cipher.init(Cipher.ENCRYPT_MODE, key) + val iv = cipher.iv + val ciphertext = cipher.doFinal(plaintext) + file.outputStream().use { out -> + out.write(iv.size.to4Bytes()) + out.write(iv) + out.write(ciphertext) + } + } + + private fun readEncryptedKey(file: File): ByteArray? { + return try { + ensureWrapKey() + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + val key = ks.getKey(KEYSTORE_ALIAS, null) as javax.crypto.SecretKey + val bytes = file.readBytes() + val ivLen = bytes.sliceArray(0..3).from4Bytes() + val iv = bytes.sliceArray(4 until 4 + ivLen) + val ciphertext = bytes.sliceArray(4 + ivLen until bytes.size) + val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) + cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LEN, iv)) + cipher.doFinal(ciphertext) + } catch (e: Exception) { + AppLogger.e("[Proof] Failed to decrypt PGP key file", e) + null + } + } + + private fun Int.to4Bytes(): ByteArray = byteArrayOf( + (this shr 24).toByte(), (this shr 16).toByte(), (this shr 8).toByte(), this.toByte() + ) + + private fun ByteArray.from4Bytes(): Int = + ((this[0].toInt() and 0xFF) shl 24) or + ((this[1].toInt() and 0xFF) shl 16) or + ((this[2].toInt() and 0xFF) shl 8) or + (this[3].toInt() and 0xFF) + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private fun hexToBytes(hex: String): ByteArray { + val len = hex.length + return ByteArray(len / 2) { i -> + ((hex[i * 2].digitToInt(16) shl 4) + hex[i * 2 + 1].digitToInt(16)).toByte() + } + } + + private fun jsonString(s: String): String { + val escaped = s.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + return "\"$escaped\"" + } + + private fun csvEscape(s: String): String { + return if (s.contains(',') || s.contains('"') || s.contains('\n')) { + "\"${s.replace("\"", "\"\"")}\"" + } else { + s + } + } +} diff --git a/app/src/main/java/net/opendasharchive/openarchive/util/ProofmodeC2paManager.kt b/app/src/main/java/net/opendasharchive/openarchive/util/ProofmodeC2paManager.kt new file mode 100644 index 00000000..7b351289 --- /dev/null +++ b/app/src/main/java/net/opendasharchive/openarchive/util/ProofmodeC2paManager.kt @@ -0,0 +1,505 @@ +package net.opendasharchive.openarchive.util + +import android.content.Context +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import net.opendasharchive.openarchive.BuildConfig +import net.opendasharchive.openarchive.core.logger.AppLogger +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier +import org.bouncycastle.asn1.x509.BasicConstraints +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.ExtendedKeyUsage +import org.bouncycastle.asn1.x509.KeyPurposeId +import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.crypto.digests.SHA1Digest +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.util.io.pem.PemObject +import org.bouncycastle.util.io.pem.PemWriter +import org.contentauth.c2pa.Builder +import org.contentauth.c2pa.C2PA +import org.contentauth.c2pa.FileStream +import org.contentauth.c2pa.Reader +import org.contentauth.c2pa.Signer +import org.contentauth.c2pa.SigningAlgorithm +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.StringWriter +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.PrivateKey +import java.security.PublicKey +import java.security.SecureRandom +import java.security.Security +import java.security.Signature +import java.security.cert.X509Certificate +import java.security.spec.ECGenParameterSpec +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter +import java.util.Date +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.spec.GCMParameterSpec + +object ProofmodeC2paManager { + + // Two-key design for full hardware signing: + // + // SIGNING_KEY_ALIAS — EC P-256 in Android Keystore TEE. This key signs media content. + // Key bytes never leave secure hardware. Signing is done via Signer.withCallback + // (called directly — KeyStoreSigner.createSigner is broken in c2pa-android 0.0.9). + // + // Software CA key — BouncyCastle EC P-256, encrypted at rest with Keystore-backed + // AES-256-GCM. Only used to sign the leaf certificate (not media content). + // Cert building with a Keystore key via BouncyCastle fails (BouncyCastle can't + // access TEE key material), so we use a software key solely for cert signing. + // The leaf cert carries the Keystore key's PUBLIC key, so manifest signatures + // are verified against the TEE key. + private const val SIGNING_KEY_ALIAS = "openarchive_c2pa_signing_key" + private const val CA_KEY_FILE = "c2pa_ca_private.key" // encrypted, software CA + private const val CERT_CHAIN_FILE = "c2pa_cert_chain.pem" // public, on disk + private const val CA_CERT_FILE = "c2pa_ca_cert.pem" // public, on disk + private const val WRAP_KEY_ALIAS = "openarchive_c2pa_wrap_key" + private const val KEYSTORE_PROVIDER = "AndroidKeyStore" + private const val AES_GCM_TRANSFORM = "AES/GCM/NoPadding" + private const val GCM_TAG_LEN = 128 + + @Volatile private var trustSettingsLoaded = false + + // DateTimeFormatter is thread-safe (unlike SimpleDateFormat) — safe across concurrent coroutines + private val isoFormat: DateTimeFormatter = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneOffset.UTC) + + fun init(context: Context) { + try { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.insertProviderAt(BouncyCastleProvider(), 1) + trustSettingsLoaded = false + ensureCertsExist(context) + AppLogger.i("[C2PA] ProofmodeC2paManager initialized") + } catch (e: Exception) { + AppLogger.e("[C2PA] Initialization failed", e) + } + } + + suspend fun embedProof( + file: File, + mimeType: String, + metadata: MetadataCollector.CaptureMetadata, + context: Context, + ): File? { + if (!Prefs.useC2pa) return null + if (!file.exists()) { + AppLogger.e("[C2PA] File does not exist: ${file.absolutePath}") + return null + } + + val tmp = File(file.parent, "${file.nameWithoutExtension}_c2pa_tmp.${file.extension}") + return try { + val certChainPem = loadPlain(context, CERT_CHAIN_FILE) ?: run { + ensureCertsExist(context) + loadPlain(context, CERT_CHAIN_FILE) ?: return null + } + val caCertPem = loadPlain(context, CA_CERT_FILE) ?: run { + ensureCertsExist(context) + loadPlain(context, CA_CERT_FILE) ?: return null + } + + if (!trustSettingsLoaded) { + try { + C2PA.loadSettings(buildTrustSettings(caCertPem), "json") + trustSettingsLoaded = true + } catch (e: Exception) { + AppLogger.w("[C2PA] loadSettings warning: ${e.message}") + } + } + + val manifestJson = buildManifestJson(metadata) + tmp.delete() + + // Signer.withCallback called directly — bypasses the broken KeyStoreSigner.createSigner. + // The signing callback uses the Keystore TEE key: key never enters app memory. + Builder.fromJson(manifestJson).use { builder -> + Signer.withCallback(SigningAlgorithm.ES256, certChainPem, null) { data -> + keystoreSign(data) + }.use { signer -> + FileStream(file, FileStream.Mode.READ, false).use { source -> + FileStream(tmp, FileStream.Mode.READ_WRITE, true).use { dest -> + builder.sign(mimeType, source, dest, signer) + } + } + } + } + + val verified = try { + FileStream(tmp, FileStream.Mode.READ, false).use { s -> + Reader.fromStream(mimeType, s).use { it.isEmbedded() } + } + } catch (e: Exception) { + AppLogger.e("[C2PA] Post-sign verification threw: ${e.message}") + tmp.delete() + false + } + if (!verified) { + AppLogger.e("[C2PA] Post-sign verification failed — original preserved: ${file.name}") + tmp.delete() + return null + } + + if (!tmp.renameTo(file)) { file.delete(); tmp.renameTo(file) } + + AppLogger.i("[C2PA] Manifest embedded + verified (TEE key): ${file.name} (${file.length() / 1024} KB)") + file + } catch (e: Exception) { + AppLogger.e("[C2PA] Failed to embed proof in ${file.name}", e) + tmp.delete() // never leave a partial temp file behind + null + } + } + + // --------------------------------------------------------------------------- + // Key and cert management + // --------------------------------------------------------------------------- + + @Synchronized + private fun ensureCertsExist(context: Context) { + val caKeyFile = File(context.filesDir, CA_KEY_FILE) + val chainFile = File(context.filesDir, CERT_CHAIN_FILE) + val caFile = File(context.filesDir, CA_CERT_FILE) + // Validate (and repair) the Keystore signing key before anything else. + // A stale key from a prior app version may be unusable for ES256 signing — + // detect by test-sign and regenerate, so an in-place upgrade self-heals. + val signingKeyUsable = ensureUsableSigningKey() + + if (signingKeyUsable && caKeyFile.exists() && chainFile.exists() && caFile.exists()) { + if (readEncrypted(caKeyFile) != null && certMatchesKeystoreKey(chainFile)) return + AppLogger.w("[C2PA] Wrap key lost or cert/key mismatch — wiping files and regenerating") + } + // Wipe any partial state + listOf(caKeyFile, chainFile, caFile).forEach { it.delete() } + + AppLogger.i("[C2PA] Generating software CA key + cert chain (Keystore pubkey in leaf)") + + // Software CA key — only used to sign certificates, not content + val caKpg = KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) + caKpg.initialize(ECNamedCurveGenParameterSpec("secp256r1")) + val caKeyPair = caKpg.generateKeyPair() + + // Leaf cert carries the TEE key's PUBLIC KEY — manifest signatures verify against it + val keystorePublicKey = getKeystorePublicKey() + ?: throw IllegalStateException("Cannot read Keystore public key") + + val caCert = buildCaCert(caKeyPair.private, caKeyPair.public) + val leafCert = buildLeafCert(caKeyPair.private, keystorePublicKey, caCert) + + val chainPem = toPem("CERTIFICATE", leafCert.encoded) + toPem("CERTIFICATE", caCert.encoded) + val caCertPem = toPem("CERTIFICATE", caCert.encoded) + val caPrivKeyPem = toPem("PRIVATE KEY", caKeyPair.private.encoded) + + val tmpCaKey = File(context.filesDir, "$CA_KEY_FILE.tmp") + val tmpChain = File(context.filesDir, "$CERT_CHAIN_FILE.tmp") + val tmpCa = File(context.filesDir, "$CA_CERT_FILE.tmp") + try { + writeEncrypted(tmpCaKey, caPrivKeyPem.toByteArray(Charsets.UTF_8)) + tmpChain.writeText(chainPem) + tmpCa.writeText(caCertPem) + tmpCaKey.renameTo(caKeyFile) + tmpChain.renameTo(chainFile) + tmpCa.renameTo(caFile) + } catch (e: Exception) { + tmpCaKey.delete(); tmpChain.delete(); tmpCa.delete(); throw e + } + + trustSettingsLoaded = false + AppLogger.i("[C2PA] Keystore TEE signing key + cert chain ready") + } + + /** + * Guarantees a usable ES256 signing key exists in the Keystore under SIGNING_KEY_ALIAS. + * If the existing key is missing or cannot sign (stale key from a prior app version, + * wrong algorithm/purpose, or hardware-invalidated), it is deleted and regenerated. + * Returns true if the key was already present and usable (no regeneration needed). + */ + private fun ensureUsableSigningKey(): Boolean { + if (keystoreSignWorks()) return true + AppLogger.w("[C2PA] Signing key missing or unusable — regenerating") + runCatching { + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + if (ks.containsAlias(SIGNING_KEY_ALIAS)) ks.deleteEntry(SIGNING_KEY_ALIAS) + } + generateKeystoreSigningKey() + if (!keystoreSignWorks()) { + throw IllegalStateException("Regenerated Keystore signing key still unusable") + } + return false + } + + /** Test-signs a tiny buffer with the Keystore key to confirm it is present and usable. */ + private fun keystoreSignWorks(): Boolean = runCatching { + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + if (!ks.containsAlias(SIGNING_KEY_ALIAS)) return false + keystoreSign(byteArrayOf(0x01)).isNotEmpty() + }.getOrDefault(false) + + private fun generateKeystoreSigningKey() { + val spec = KeyGenParameterSpec.Builder(SIGNING_KEY_ALIAS, KeyProperties.PURPOSE_SIGN) + .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) + .setDigests(KeyProperties.DIGEST_SHA256) + .build() + KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, KEYSTORE_PROVIDER) + .also { it.initialize(spec) } + .generateKeyPair() + AppLogger.i("[C2PA] Keystore EC signing key generated") + } + + private fun getKeystorePublicKey(): PublicKey? = runCatching { + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + ks.getCertificate(SIGNING_KEY_ALIAS)?.publicKey + }.getOrNull() + + /** + * Parses the first cert in the on-disk chain file and checks its public key + * matches the current Keystore signing key. Guards against stale cert files + * left over from a previous key generation (e.g. after app update or key loss). + */ + private fun certMatchesKeystoreKey(chainFile: File): Boolean = runCatching { + val keystorePubKey = getKeystorePublicKey() ?: return false + // Parse the first (leaf) cert from the chain PEM + val leafBase64 = chainFile.readText() + .substringAfter("-----BEGIN CERTIFICATE-----") + .substringBefore("-----END CERTIFICATE-----") + .replace("\n", "") + .replace("\r", "") + .trim() + val certBytes = java.util.Base64.getDecoder().decode(leafBase64) + val leafCert = java.security.cert.CertificateFactory.getInstance("X.509") + .generateCertificate(certBytes.inputStream()) as java.security.cert.X509Certificate + leafCert.publicKey.encoded.contentEquals(keystorePubKey.encoded) + }.getOrElse { AppLogger.w("[C2PA] Cert/key match check failed: ${it.message}"); false } + + /** Signs data inside the TEE — Keystore private key never leaves secure hardware. */ + private fun keystoreSign(data: ByteArray): ByteArray { + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + val privateKey = ks.getKey(SIGNING_KEY_ALIAS, null) as PrivateKey + // Explicit provider ensures dispatch to Android Keystore on all OEM builds + return Signature.getInstance("SHA256withECDSA", "AndroidKeyStoreBCWorkaround").apply { + initSign(privateKey) + update(data) + }.sign() + } + + // --------------------------------------------------------------------------- + // AES-256-GCM wrap for software CA private key file + // --------------------------------------------------------------------------- + + private fun ensureWrapKey() { + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + if (ks.containsAlias(WRAP_KEY_ALIAS)) return + val spec = KeyGenParameterSpec.Builder( + WRAP_KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setKeySize(256) + .build() + KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER) + .also { it.init(spec) } + .generateKey() + } + + private fun writeEncrypted(file: File, plaintext: ByteArray) { + ensureWrapKey() + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + val key = ks.getKey(WRAP_KEY_ALIAS, null) as javax.crypto.SecretKey + val cipher = Cipher.getInstance(AES_GCM_TRANSFORM) + cipher.init(Cipher.ENCRYPT_MODE, key) + val iv = cipher.iv; val ct = cipher.doFinal(plaintext) + file.outputStream().use { it.write(iv.size.to4Bytes()); it.write(iv); it.write(ct) } + } + + private fun readEncrypted(file: File): ByteArray? = runCatching { + ensureWrapKey() + val ks = KeyStore.getInstance(KEYSTORE_PROVIDER).also { it.load(null) } + val key = ks.getKey(WRAP_KEY_ALIAS, null) as javax.crypto.SecretKey + val b = file.readBytes() + val ivLen = b.sliceArray(0..3).from4Bytes() + val iv = b.sliceArray(4 until 4 + ivLen) + val ct = b.sliceArray(4 + ivLen until b.size) + Cipher.getInstance(AES_GCM_TRANSFORM) + .also { it.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(GCM_TAG_LEN, iv)) } + .doFinal(ct) + }.getOrElse { AppLogger.e("[C2PA] Failed to decrypt CA key file", it); null } + + private fun loadPlain(context: Context, name: String): String? { + val f = File(context.filesDir, name) + return if (f.exists()) f.readText() else null + } + + private fun Int.to4Bytes(): ByteArray = byteArrayOf( + (this shr 24).toByte(), (this shr 16).toByte(), (this shr 8).toByte(), this.toByte() + ) + private fun ByteArray.from4Bytes(): Int = + ((this[0].toInt() and 0xFF) shl 24) or ((this[1].toInt() and 0xFF) shl 16) or + ((this[2].toInt() and 0xFF) shl 8) or (this[3].toInt() and 0xFF) + + // --------------------------------------------------------------------------- + // Certificate building — software CA key signs certs (works; no Keystore key used here) + // --------------------------------------------------------------------------- + + private fun buildCaCert(caPrivateKey: PrivateKey, caPublicKey: PublicKey): X509Certificate { + val subject = X500Name("CN=OpenArchive Save Root CA, O=OpenArchive, C=US") + val serial = BigInteger(64, SecureRandom()) + val notBefore = Date(System.currentTimeMillis() - 86_400_000L) + val notAfter = Date(System.currentTimeMillis() + 20L * 365 * 24 * 60 * 60 * 1000) + val builder = JcaX509v3CertificateBuilder(subject, serial, notBefore, notAfter, subject, caPublicKey) + val ski = computeSki(SubjectPublicKeyInfo.getInstance(caPublicKey.encoded)) + builder.addExtension(Extension.subjectKeyIdentifier, false, SubjectKeyIdentifier(ski)) + builder.addExtension(Extension.basicConstraints, true, BasicConstraints(true)) + builder.addExtension(Extension.keyUsage, true, + KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign or KeyUsage.digitalSignature)) + return JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCertificate(builder.build(BcContentSigner(caPrivateKey, "SHA256withECDSA"))) + } + + /** + * Leaf cert whose SubjectPublicKeyInfo carries [subjectPublicKey] (the Keystore TEE key). + * Signed by [caPrivateKey] (software CA key — BouncyCastle handles this correctly). + * Manifest signatures are made with the Keystore key and verified against this cert. + */ + private fun buildLeafCert( + caPrivateKey: PrivateKey, + subjectPublicKey: PublicKey, + caCert: X509Certificate, + ): X509Certificate { + val subject = X500Name("CN=OpenArchive Save, O=OpenArchive, C=US") + val issuer = X500Name("CN=OpenArchive Save Root CA, O=OpenArchive, C=US") + val serial = BigInteger(64, SecureRandom()) + val notBefore = Date(System.currentTimeMillis() - 86_400_000L) + val notAfter = Date(System.currentTimeMillis() + 10L * 365 * 24 * 60 * 60 * 1000) + val builder = JcaX509v3CertificateBuilder(issuer, serial, notBefore, notAfter, subject, subjectPublicKey) + val ski = computeSki(SubjectPublicKeyInfo.getInstance(subjectPublicKey.encoded)) + val caSki = computeSki(SubjectPublicKeyInfo.getInstance(caCert.publicKey.encoded)) + builder.addExtension(Extension.subjectKeyIdentifier, false, SubjectKeyIdentifier(ski)) + builder.addExtension(Extension.authorityKeyIdentifier, false, AuthorityKeyIdentifier(caSki)) + builder.addExtension(Extension.basicConstraints, true, BasicConstraints(false)) + builder.addExtension(Extension.keyUsage, true, + KeyUsage(KeyUsage.digitalSignature or KeyUsage.nonRepudiation)) + // id-kp-documentSigning (RFC 9336) + builder.addExtension(Extension.extendedKeyUsage, true, + ExtendedKeyUsage(KeyPurposeId.getInstance(ASN1ObjectIdentifier("1.3.6.1.5.5.7.3.36")))) + return JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME) + .getCertificate(builder.build(BcContentSigner(caPrivateKey, "SHA256withECDSA"))) + } + + private fun computeSki(spki: SubjectPublicKeyInfo): ByteArray { + val d = SHA1Digest(); val r = ByteArray(d.digestSize) + val b = spki.publicKeyData.bytes + d.update(b, 0, b.size); d.doFinal(r, 0); return r + } + + private fun toPem(type: String, encoded: ByteArray): String { + val sw = StringWriter() + PemWriter(sw).use { it.writeObject(PemObject(type, encoded)) } + return sw.toString() + } + + private class BcContentSigner( + private val privateKey: PrivateKey, + private val algorithm: String, + ) : ContentSigner { + private val sig = Signature.getInstance(algorithm, BouncyCastleProvider.PROVIDER_NAME) + .also { it.initSign(privateKey) } + private val buf = ByteArrayOutputStream() + override fun getAlgorithmIdentifier() = + AlgorithmIdentifier(ASN1ObjectIdentifier("1.2.840.10045.4.3.2")) + override fun getOutputStream(): java.io.OutputStream = buf + override fun getSignature(): ByteArray { sig.update(buf.toByteArray()); return sig.sign() } + } + + // --------------------------------------------------------------------------- + // Trust settings + manifest JSON + // --------------------------------------------------------------------------- + + private fun buildTrustSettings(caCertPem: String) = + """{"trust":{"trust_anchors":"${caCertPem.replace("\\","\\\\").replace("\"","\\\"").replace("\n","\\n").replace("\r","\\r")}"}}""" + + private fun buildManifestJson(m: MetadataCollector.CaptureMetadata): String { + val captureIso = isoFormat.format(Instant.ofEpochMilli(m.captureTime)) + val claimGenerator = "OpenArchive Save/${BuildConfig.VERSION_NAME} (build ${BuildConfig.VERSION_CODE})" + val assertions = buildList { + add(actionsAssertion()) + add(creativeWorkAssertion(m, captureIso)) + add(deviceAssertion(m)) + if (m.latitude != null && m.longitude != null) add(locationAssertion(m)) + if (m.networkType != null || m.ipv4 != null || m.ipv6 != null || m.cellInfo != null) add(networkAssertion(m)) + }.joinToString(",\n") + return """ + { + "claim_generator": ${claimGenerator.toJsonString()}, + "assertions": [ + $assertions + ] + } + """.trimIndent() + } + + private fun actionsAssertion() = """ + {"label":"c2pa.actions","data":{"actions":[{"action":"c2pa.created","digitalSourceType":"http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture"}]}} + """.trimIndent() + + private fun creativeWorkAssertion(m: MetadataCollector.CaptureMetadata, captureIso: String): String { + val locBlock = if (m.latitude != null && m.longitude != null) buildString { + append(""","locationCreated":{"@type":"Place","geo":{"@type":"GeoCoordinates","latitude":${m.latitude},"longitude":${m.longitude}""") + if (m.locationAltitude != null) append(""","elevation":${m.locationAltitude}""") + append("}}") + } else "" + return """ + {"label":"stds.schema-org.CreativeWork","data":{"@context":"https://schema.org","@type":"CreativeWork","author":[{"@type":"SoftwareApplication","name":"OpenArchive Save","version":${BuildConfig.VERSION_NAME.toJsonString()},"buildNumber":"${BuildConfig.VERSION_CODE}"}],"dateCreated":"$captureIso"$locBlock}} + """.trimIndent() + } + + private fun deviceAssertion(m: MetadataCollector.CaptureMetadata) = """ + {"label":"org.openarchive.save.device","data":{"make":${m.deviceMake.toJsonString()},"model":${m.deviceModel.toJsonString()},"brand":${m.deviceBrand.toJsonString()},"hardware":${m.hardware.toJsonString()},"locale":${m.locale.toJsonString()},"language":${m.language.toJsonString()}${if (m.screenSizeInches != null) ""","screenSizeInches":${m.screenSizeInches}""" else ""}}} + """.trimIndent() + + private fun locationAssertion(m: MetadataCollector.CaptureMetadata): String { + val fields = buildString { + append(""""latitude":${m.latitude}""") + append(""","longitude":${m.longitude}""") + if (m.locationAltitude != null) append(""","altitude":${m.locationAltitude}""") + if (m.locationAccuracy != null) append(""","accuracy":${m.locationAccuracy}""") + if (m.locationBearing != null) append(""","bearing":${m.locationBearing}""") + if (m.locationSpeed != null) append(""","speed":${m.locationSpeed}""") + if (m.locationProvider != null) append(""","provider":${m.locationProvider.toJsonString()}""") + if (m.locationTime != null) append(""","timestamp":${isoFormat.format(Instant.ofEpochMilli(m.locationTime)).toJsonString()}""") + } + return """{"label":"org.openarchive.save.location","data":{$fields}}""" + } + + private fun networkAssertion(m: MetadataCollector.CaptureMetadata): String { + val fields = buildList { + if (m.networkType != null) add(""""networkType":${m.networkType.toJsonString()}""") + if (m.ipv4 != null) add(""""ipv4":${m.ipv4.toJsonString()}""") + if (m.ipv6 != null) add(""""ipv6":${m.ipv6.toJsonString()}""") + if (m.cellInfo != null) add(""""cellInfo":${m.cellInfo}""") + }.joinToString(",") + return """{"label":"org.openarchive.save.network","data":{$fields}}""" + } + + private fun String.toJsonString(): String { + val e = replace("\\","\\\\").replace("\"","\\\"") + .replace("\n","\\n").replace("\r","\\r").replace("\t","\\t") + return "\"$e\"" + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a25d78c2..6e738749 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -240,8 +240,8 @@
Choose Creative Commons Licensing.]]>
https://creativecommons.org - C2PA content credentials.]]> - https://c2pa.org + Proofmode content credentials.]]> + https://proofmode.org Save always uploads over TLS (Transport Layer Security) to protect your media in transit.
To further enhance security, enable Tor to prevent interception of your media from your phone to the server.]]>
https://www.torproject.org @@ -543,7 +543,13 @@ Enable C2PA Generate cryptographic content credentials Learn more here.]]> - https://c2pa.org/ + https://proofmode.org/ + + + Proofmode + Enable Proofmode + Generate cryptographic content credentials + Learn more here.]]>
diff --git a/rust-c2pa-ffi/.cargo/config.toml.template b/rust-c2pa-ffi/.cargo/config.toml.template deleted file mode 100644 index 1851c59f..00000000 --- a/rust-c2pa-ffi/.cargo/config.toml.template +++ /dev/null @@ -1,67 +0,0 @@ -# Cargo configuration for Android builds -# This file configures the linkers and compilers for each Android architecture -# -# SETUP INSTRUCTIONS: -# 1. Copy this file to config.toml: cp config.toml.template config.toml -# 2. Replace {NDK_HOME} with your actual NDK path, for example: -# - macOS: /Users/USERNAME/Library/Android/sdk/ndk/27.1.12297006 -# - Linux: /home/USERNAME/Android/Sdk/ndk/27.1.12297006 -# - Windows: C:/Users/USERNAME/AppData/Local/Android/Sdk/ndk/27.1.12297006 -# 3. Replace {PLATFORM} with your host platform: -# - macOS: darwin-x86_64 -# - Linux: linux-x86_64 -# - Windows: windows-x86_64 - -[env] -# Set NDK path for easy reference -ANDROID_NDK_HOME = "{NDK_HOME}" - -[target.aarch64-linux-android] -linker = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/aarch64-linux-android30-clang" -ar = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/llvm-ar" -# Android 15+ 16 KB page size compatibility -rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] - -[target.aarch64-linux-android.env] -CC_aarch64_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/aarch64-linux-android30-clang" -CXX_aarch64_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/aarch64-linux-android30-clang++" -AR_aarch64_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/llvm-ar" - -[target.armv7-linux-androideabi] -linker = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/armv7a-linux-androideabi30-clang" -ar = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/llvm-ar" -# Android 15+ 16 KB page size compatibility -rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] - -[target.armv7-linux-androideabi.env] -CC_armv7_linux_androideabi = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/armv7a-linux-androideabi30-clang" -CXX_armv7_linux_androideabi = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/armv7a-linux-androideabi30-clang++" -AR_armv7_linux_androideabi = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/llvm-ar" - -[target.i686-linux-android] -linker = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/i686-linux-android30-clang" -ar = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/llvm-ar" -# Android 15+ 16 KB page size compatibility -rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] - -[target.i686-linux-android.env] -CC_i686_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/i686-linux-android30-clang" -CXX_i686_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/i686-linux-android30-clang++" -AR_i686_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/llvm-ar" - -[target.x86_64-linux-android] -linker = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/x86_64-linux-android30-clang" -ar = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/llvm-ar" -# Android 15+ 16 KB page size compatibility -rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"] - -[target.x86_64-linux-android.env] -CC_x86_64_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/x86_64-linux-android30-clang" -CXX_x86_64_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/x86_64-linux-android30-clang++" -AR_x86_64_linux_android = "{NDK_HOME}/toolchains/llvm/prebuilt/{PLATFORM}/bin/llvm-ar" - -# Build settings -[build] -target-dir = "target" - -# Profile settings inherited from Cargo.toml diff --git a/rust-c2pa-ffi/Cargo.lock b/rust-c2pa-ffi/Cargo.lock deleted file mode 100644 index 54309822..00000000 --- a/rust-c2pa-ffi/Cargo.lock +++ /dev/null @@ -1,3018 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_log-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" - -[[package]] -name = "android_logger" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" -dependencies = [ - "android_log-sys", - "env_filter", - "log", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "asn1-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" -dependencies = [ - "asn1-rs-derive 0.4.0", - "asn1-rs-impl 0.1.0", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror", -] - -[[package]] -name = "asn1-rs" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" -dependencies = [ - "asn1-rs-derive 0.5.1", - "asn1-rs-impl 0.2.0", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror", - "time", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure 0.12.6", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure 0.13.2", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-generic" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf3728566eefa873833159754f5732fb0951d3649e6e5b891cc70d56dd41673" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "atree" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132573478eb9ff973c6f75d0ed425ac12da77d266506483345f46743ecc83a98" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" - -[[package]] -name = "bcder" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d" -dependencies = [ - "bytes", - "smallvec", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "bitvec-nom2" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d988fcc40055ceaa85edc55875a08f8abd29018582647fd82ad6128dba14a5f0" -dependencies = [ - "bitvec", - "nom", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "byteordered" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf2cd9424f5ff404aba1959c835cbc448ee8b689b870a9981c76c0fd46280e6" -dependencies = [ - "byteorder", -] - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "c2pa" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "211fe13328036af8ec57e039379e38ec01a9c6fb275b8752d46d22e6992530e8" -dependencies = [ - "asn1-rs 0.5.2", - "async-generic", - "async-recursion", - "async-trait", - "atree", - "base64 0.22.1", - "bcder", - "byteorder", - "byteordered", - "bytes", - "chrono", - "ciborium", - "config", - "console_log", - "conv", - "coset", - "ed25519-dalek", - "extfmt", - "fast-xml", - "getrandom 0.2.16", - "hex", - "id3", - "img-parts", - "instant", - "jfifdump", - "js-sys", - "lazy_static", - "log", - "memchr", - "mp4", - "pem 3.0.6", - "png_pong", - "rand", - "rand_chacha", - "rand_core 0.9.3", - "range-set", - "rasn", - "rasn-ocsp", - "rasn-pkix", - "riff", - "rsa", - "serde", - "serde-transcode", - "serde-wasm-bindgen", - "serde_bytes", - "serde_cbor", - "serde_derive", - "serde_json", - "serde_with", - "sha1", - "sha2", - "spki", - "tempfile", - "thiserror", - "treeline", - "ureq", - "url", - "uuid", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "x509-certificate", - "x509-parser", - "zip", -] - -[[package]] -name = "c2pa-ffi" -version = "0.1.0" -dependencies = [ - "android_logger", - "anyhow", - "c2pa", - "jni", - "log", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "cc" -version = "1.2.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "ciborium" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" -dependencies = [ - "ciborium-io", - "ciborium-ll", - "serde", -] - -[[package]] -name = "ciborium-io" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" - -[[package]] -name = "ciborium-ll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" -dependencies = [ - "ciborium-io", - "half 2.7.1", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "config" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" -dependencies = [ - "json5", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", -] - -[[package]] -name = "console_log" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" -dependencies = [ - "log", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "const_panic" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" -dependencies = [ - "typewit", -] - -[[package]] -name = "conv" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299" -dependencies = [ - "custom_derive", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "coset" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8cc80f631f8307b887faca24dcc3abc427cd0367f6eb6188f6e8f5b7ad8fb" -dependencies = [ - "ciborium", - "ciborium-io", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "custom_derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.114", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "der-parser" -version = "9.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" -dependencies = [ - "asn1-rs 0.6.2", - "displaydoc", - "nom", - "num-bigint", - "num-traits", - "rusticata-macros", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - -[[package]] -name = "doc-comment" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "env_filter" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "extfmt" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a48fe53466ab1f4ea6303bf9d7a0ca8060778590f09fd6c3304cc817aeb9935d" - -[[package]] -name = "fast-xml" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7ffc2f9e1373cd82b4d4ce2f84f8162edd48e3932abeb19c06170b66dbbd6c" -dependencies = [ - "memchr", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" - -[[package]] -name = "flate2" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "half" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id3" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f4e785f2c700217ee82a1c727c720449421742abd5fcb2f1df04e1244760e9" -dependencies = [ - "bitflags", - "byteorder", - "flate2", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "img-parts" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4e24cfdc6f897b582508e3c382eaf5378076898f80500a80d10d761ae85e90" -dependencies = [ - "bytes", - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jfifdump" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "326647c83ea8fb213e58a272f11d4569611277d8730f9905a59fba9d30b12c05" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] -name = "konst" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" -dependencies = [ - "const_panic", - "konst_kernel", - "typewit", -] - -[[package]] -name = "konst_kernel" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" -dependencies = [ - "typewit", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin 0.9.8", -] - -[[package]] -name = "libc" -version = "0.2.179" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mp4" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9ef834d5ed55e494a2ae350220314dc4aacd1c43a9498b00e320e0ea352a5c3" -dependencies = [ - "byteorder", - "bytes", - "num-rational", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "oid-registry" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" -dependencies = [ - "asn1-rs 0.6.2", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - -[[package]] -name = "parsenic" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c695d2b8bcf1dd62a5173a9c43e6ebbe9261701c002f9b462fc9690c144bb61" -dependencies = [ - "traitful", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pem" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b13fe415cdf3c8e44518e18a7c95a13431d9bdf6d15367d82b23c377fdd441a" -dependencies = [ - "base64 0.21.7", - "serde", -] - -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "pest_meta" -version = "2.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "pix" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6574ffbea793ab0c9a9b09ae99831f1513fdd7732b12aca4c7182c46dc3bc9f" - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "png_pong" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a767b8b17e388cb7f1e74cf3c2ebce14e6b7ae123fa27150889a353fd9eb981f" -dependencies = [ - "miniz_oxide", - "parsenic", - "pix", - "simd-adler32", - "traitful", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" - -[[package]] -name = "range-set" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714bc4849c399f77ab82177e9b4f012e02f3a7d6191023e016334edde5b21050" -dependencies = [ - "num-traits", - "smallvec", -] - -[[package]] -name = "rasn" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a9162f91f371403ed7469be915bc0d39b84246ac2783822c4197fcaf3dbfe2" -dependencies = [ - "arrayvec", - "bitvec", - "bitvec-nom2", - "bytes", - "chrono", - "either", - "hashbrown 0.14.5", - "konst", - "nom", - "num-bigint", - "num-integer", - "num-traits", - "once_cell", - "rasn-derive", - "serde_json", - "snafu", -] - -[[package]] -name = "rasn-derive" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd02be4b376758d6a12b2751c5e86f0befc8e47bd6847b1fc126dcb79cb005e" -dependencies = [ - "either", - "itertools", - "proc-macro2", - "quote", - "rayon", - "syn 1.0.109", - "uuid", -] - -[[package]] -name = "rasn-ocsp" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ecc5a6f83a09d3c8f2bd8c200a28986db30b1ad0a33bb828442d2efee5871f1" -dependencies = [ - "rasn", - "rasn-pkix", -] - -[[package]] -name = "rasn-pkix" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3da2b04cf81b8135d5b362cf29143018eb708039670c2680092f1f1b79d501f" -dependencies = [ - "rasn", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "riff" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c601484456988d75017d86700d3743b949c21cdc7399f940c75e34680d185c5" - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags", - "serde", - "serde_derive", -] - -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "sha2", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rust-ini" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "log", - "once_cell", - "ring 0.17.14", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "ring 0.17.14", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-transcode" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590c0e25c2a5bb6e85bf5c1bce768ceb86b316e7a01bdf07d2cb4ec2271990e2" -dependencies = [ - "serde", -] - -[[package]] -name = "serde-wasm-bindgen" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half 1.8.3", - "serde", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "indexmap 2.13.0", - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_with" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.13.0", - "schemars 0.9.0", - "schemars 1.2.0", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "snafu" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap 2.13.0", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "traitful" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d856e22ead1fb79b9fc3cec63300086f680924f2f7b0e2701f6835a28b9c4425" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "treeline" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "typewit" -version = "1.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" -dependencies = [ - "typewit_proc_macros", -] - -[[package]] -name = "typewit_proc_macros" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64 0.22.1", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots 0.26.11", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.114", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.5", -] - -[[package]] -name = "webpki-roots" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "x509-certificate" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5d27c90840e84503cf44364de338794d5d5680bdd1da6272d13f80b0769ee0" -dependencies = [ - "bcder", - "bytes", - "chrono", - "der", - "hex", - "pem 2.0.1", - "ring 0.16.20", - "signature", - "spki", - "thiserror", -] - -[[package]] -name = "x509-parser" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" -dependencies = [ - "asn1-rs 0.6.2", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror", - "time", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure 0.13.2", -] - -[[package]] -name = "zerocopy" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", - "synstructure 0.13.2", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "byteorder", - "crc32fast", - "crossbeam-utils", -] - -[[package]] -name = "zmij" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" diff --git a/rust-c2pa-ffi/Cargo.toml b/rust-c2pa-ffi/Cargo.toml deleted file mode 100644 index 9e68d2e9..00000000 --- a/rust-c2pa-ffi/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "c2pa-ffi" -version = "0.1.0" -edition = "2021" -rust-version = "1.88.0" - -[lib] -name = "c2pa_ffi" -crate-type = ["cdylib"] - -[dependencies] -# C2PA library for content authenticity -c2pa = "0.36" - -# JNI bindings for Android -jni = "0.21" - -# JSON serialization -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -# Error handling -anyhow = "1.0" - -# Logging -log = "0.4" -android_logger = "0.14" - -# Hashing -sha2 = "0.10" - -[profile.release] -opt-level = "z" # Optimize for size -lto = true # Enable Link Time Optimization -codegen-units = 1 # Reduce parallel codegen units for better optimization -strip = "debuginfo" # Strip debug info only, keep exported JNI symbols diff --git a/rust-c2pa-ffi/build-android.sh b/rust-c2pa-ffi/build-android.sh deleted file mode 100755 index 52fa6fbb..00000000 --- a/rust-c2pa-ffi/build-android.sh +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env bash -# build-android.sh — Standalone build script for C2PA Rust FFI (all Android ABIs) -# -# Usage: -# ./build-android.sh # build all ABIs -# ./build-android.sh arm64-v8a # build a single ABI -# -# Prerequisites: -# - Rust + Cargo (https://rustup.rs/) -# - Android NDK (via Android Studio → SDK Manager → SDK Tools → NDK) -# - Android Rust targets (auto-installed by this script) -# -# The Gradle task `buildC2paRustLibs` calls this logic automatically for FOSS builds. -# Use this script for manual builds or CI environments without Gradle. - -set -euo pipefail - -BOLD='\033[1m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' - -info() { echo -e "${GREEN}✓ $*${NC}"; } -warn() { echo -e "${YELLOW}⚠ $*${NC}"; } -error() { echo -e "${RED}✗ $*${NC}" >&2; exit 1; } -step() { echo -e "${BOLD}▸ $*${NC}"; } - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -JNILIBS_DIR="$PROJECT_ROOT/app/src/main/jniLibs" -ANDROID_API_LEVEL="30" -PREFERRED_NDK_VERSION="27.1.12297006" - -# ─── Find Android NDK ──────────────────────────────────────────────────────── -find_ndk() { - # 1. Explicit NDK env vars - for var in ANDROID_NDK_HOME ANDROID_NDK_ROOT; do - if [[ -n "${!var:-}" && -d "${!var}" ]]; then - echo "${!var}"; return - fi - done - - # 2. Derive from SDK root - local sdk="" - for candidate in \ - "${ANDROID_HOME:-}" \ - "${ANDROID_SDK_ROOT:-}" \ - "$HOME/Library/Android/sdk" \ - "$HOME/Android/Sdk"; do - if [[ -n "$candidate" && -d "$candidate" ]]; then - sdk="$candidate"; break - fi - done - - [[ -z "$sdk" ]] && error \ - "Android SDK not found.\nSet ANDROID_HOME to your SDK directory." - - local ndk_parent="$sdk/ndk" - [[ -d "$ndk_parent" ]] || error \ - "NDK not found at $ndk_parent.\nInstall it via Android Studio → SDK Manager → SDK Tools → NDK (Side by side)." - - # Prefer the pinned version; otherwise use the latest - if [[ -d "$ndk_parent/$PREFERRED_NDK_VERSION" ]]; then - echo "$ndk_parent/$PREFERRED_NDK_VERSION"; return - fi - - local latest - latest=$(ls -1 "$ndk_parent" | sort -V | tail -1) - [[ -n "$latest" ]] || error "No NDK versions found in $ndk_parent" - warn "NDK $PREFERRED_NDK_VERSION not found — using $latest" - echo "$ndk_parent/$latest" -} - -# ─── Detect host OS/arch for toolchain prebuilt dir ───────────────────────── -detect_host() { - local os arch - case "$(uname -s)" in - Darwin) os="darwin" ;; - Linux) os="linux" ;; - *) error "Unsupported host OS: $(uname -s)" ;; - esac - case "$(uname -m)" in - x86_64) arch="x86_64" ;; - arm64|aarch64) arch="x86_64" ;; # NDK ships x86_64 host tools even on Apple Silicon - *) error "Unsupported host arch: $(uname -m)" ;; - esac - echo "${os}-${arch}" -} - -# ─── Prerequisite checks ───────────────────────────────────────────────────── -step "Checking prerequisites" - -command -v cargo >/dev/null 2>&1 || error \ - "Cargo not found. Install Rust from https://rustup.rs/\nThen run: source \$HOME/.cargo/env" - -command -v rustup >/dev/null 2>&1 || error \ - "rustup not found. Install Rust from https://rustup.rs/" - -info "Rust $(rustc --version)" - -# ─── Determine NDK and toolchain paths ─────────────────────────────────────── -step "Locating Android NDK" -NDK="$(find_ndk)" -HOST="$(detect_host)" -BIN="$NDK/toolchains/llvm/prebuilt/$HOST/bin" - -[[ -d "$BIN" ]] || error "NDK toolchain bin not found at $BIN" -info "NDK: $NDK" - -# ─── Install Android Rust targets (idempotent) ─────────────────────────────── -step "Ensuring Android Rust targets are installed" -RUST_TARGETS=( - "aarch64-linux-android" - "armv7-linux-androideabi" - "i686-linux-android" - "x86_64-linux-android" -) -rustup target add "${RUST_TARGETS[@]}" -info "Rust targets verified" - -# ─── Generate .cargo/config.toml ───────────────────────────────────────────── -step "Generating .cargo/config.toml" -mkdir -p "$SCRIPT_DIR/.cargo" -cat > "$SCRIPT_DIR/.cargo/config.toml" < jboolean { - // Initialize Android logger - android_logger::init_once( - android_logger::Config::default() - .with_max_level(LevelFilter::Debug) - .with_tag("C2PA-FFI"), - ); - - log::info!("C2PA FFI initialized"); - 1 // true -} - -/// C2PA Manifest structure for JSON serialization -#[derive(Serialize, Deserialize, Debug)] -struct C2paManifest { - claim_generator: String, - assertions: Vec, - signature: SignatureInfo, -} - -#[derive(Serialize, Deserialize, Debug)] -struct Assertion { - label: String, - data: serde_json::Value, -} - -#[derive(Serialize, Deserialize, Debug)] -struct SignatureInfo { - algorithm: String, - value: String, -} - -/// Generate a C2PA manifest for a media file -/// -/// # Arguments -/// * `file_path` - Path to the media file -/// * `metadata_json` - JSON string containing metadata (title, description, etc.) -/// -/// # Returns -/// JSON string containing the C2PA manifest, or null on error -#[no_mangle] -pub extern "C" fn Java_net_opendasharchive_openarchive_util_C2paFfi_nativeGenerateManifest( - mut env: JNIEnv, - _class: JClass, - file_path: JString, - metadata_json: JString, -) -> jstring { - // Convert JString to Rust String - let file_path_str: String = match env.get_string(&file_path) { - Ok(s) => s.into(), - Err(e) => { - log::error!("Failed to get file path string: {:?}", e); - return JObject::null().into_raw(); - } - }; - - let metadata_str: String = match env.get_string(&metadata_json) { - Ok(s) => s.into(), - Err(e) => { - log::error!("Failed to get metadata JSON string: {:?}", e); - return JObject::null().into_raw(); - } - }; - - log::info!("Generating C2PA manifest for: {}", file_path_str); - - // Parse metadata JSON - let metadata: serde_json::Value = match serde_json::from_str(&metadata_str) { - Ok(m) => m, - Err(e) => { - log::error!("Failed to parse metadata JSON: {:?}", e); - return JObject::null().into_raw(); - } - }; - - // TODO: Implement actual C2PA manifest generation using c2pa crate - // For now, create a stub manifest structure - let manifest = create_stub_manifest(&file_path_str, metadata); - - // Serialize manifest to JSON - let manifest_json = match serde_json::to_string_pretty(&manifest) { - Ok(json) => json, - Err(e) => { - log::error!("Failed to serialize manifest: {:?}", e); - return JObject::null().into_raw(); - } - }; - - // Convert Rust String to JString - match env.new_string(manifest_json) { - Ok(s) => s.into_raw(), - Err(e) => { - log::error!("Failed to create JString: {:?}", e); - JObject::null().into_raw() - } - } -} - -/// Create a stub C2PA manifest -/// TODO: Replace with actual c2pa library implementation -fn create_stub_manifest(file_path: &str, metadata: serde_json::Value) -> C2paManifest { - // Calculate file hash for signature - let file_hash = calculate_file_hash(file_path).unwrap_or_else(|_| "unknown".to_string()); - - C2paManifest { - claim_generator: "OpenArchive Save/Rust FFI".to_string(), - assertions: vec![ - Assertion { - label: "stds.schema-org.CreativeWork".to_string(), - data: metadata, - }, - Assertion { - label: "c2pa.hash.data".to_string(), - data: serde_json::json!({ - "alg": "sha256", - "hash": file_hash, - "name": "jumbf manifest" - }), - }, - ], - signature: SignatureInfo { - algorithm: "es256".to_string(), - value: format!("stub_signature_{}", &file_hash[..16]), - }, - } -} - -/// Calculate SHA-256 hash of a file -fn calculate_file_hash(file_path: &str) -> Result { - use sha2::{Digest, Sha256}; - - let contents = fs::read(file_path)?; - let hash = Sha256::digest(&contents); - Ok(format!("{:x}", hash)) -} - -/// Verify a C2PA manifest -/// -/// # Arguments -/// * `manifest_json` - JSON string containing the C2PA manifest -/// -/// # Returns -/// true if manifest is valid, false otherwise -#[no_mangle] -pub extern "C" fn Java_net_opendasharchive_openarchive_util_C2paFfi_nativeVerifyManifest( - mut env: JNIEnv, - _class: JClass, - manifest_json: JString, -) -> jboolean { - let manifest_str: String = match env.get_string(&manifest_json) { - Ok(s) => s.into(), - Err(e) => { - log::error!("Failed to get manifest JSON string: {:?}", e); - return 0; // false - } - }; - - // Parse manifest JSON - match serde_json::from_str::(&manifest_str) { - Ok(_manifest) => { - log::info!("Manifest verification: valid structure"); - // TODO: Implement actual signature verification using c2pa crate - 1 // true - } - Err(e) => { - log::error!("Manifest verification failed: {:?}", e); - 0 // false - } - } -} - -// Add sha2 dependency for hashing -// This will be added to Cargo.toml diff --git a/settings.gradle.kts b/settings.gradle.kts index 894d5ba4..32ca80bc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -85,8 +85,10 @@ dependencyResolutionManagement { content { includeModule("com.github.esafirm", "android-image-picker") includeModule("com.github.abdularis", "circularimageview") + includeModule("com.github.contentauth", "c2pa-android") } } + } } rootProject.name = "save-android-old"