//! Narrow, version-checked hardening of UniFFI 0.32's Kotlin diagnostics. //! Records remain immutable data classes with unchanged wire layouts/checksums. use std::{error::Error, fs, path::Path}; use regex::Regex; pub fn prepare(input: &Path, output: &Path) -> Result<(), Box> { if !input.is_dir() || input.is_symlink() || output.is_symlink() { return Err("binding input and output must be distinct directories".into()); } let input = input.canonicalize()?; let destination = if output.exists() { output.canonicalize()? } else { output .parent() .ok_or("output needs a parent")? .canonicalize()? .join(output.file_name().ok_or("output needs a name")?) }; if input.starts_with(&destination) || destination.starts_with(&input) { return Err("binding input/output directories must not overlap".into()); } if output.exists() { fs::remove_dir_all(output)?; } copy(&input, output) } fn copy(input: &Path, output: &Path) -> Result<(), Box> { fs::create_dir_all(output)?; let mut found = false; for entry in fs::read_dir(input)? { let entry = entry?; let destination = output.join(entry.file_name()); if entry.file_type()?.is_dir() { copy(&entry.path(), &destination)?; found = true; } else if entry.file_type()?.is_file() && entry.path().extension().is_some_and(|ext| ext == "kt") { let source = fs::read_to_string(entry.path())?; fs::write(destination, harden_source(&source)?)?; found = true; } else { return Err("unexpected file in generated Kotlin bindings".into()); } } if !found { return Err("UniFFI produced no Kotlin bindings".into()); } Ok(()) } fn harden_source(source: &str) -> Result> { let internal = "class InternalException(message: String) : kotlin.Exception(message)"; let callback = "val err = try { e.stackTraceToString() } catch(_: Throwable) { \"\" }"; if !source.contains(internal) || source.matches(callback).count() != 2 { return Err("UniFFI unexpected-error template changed; review secret redaction".into()); } let initializer = "public fun uniffiEnsureInitialized() {"; if source.matches(initializer).count() != 1 { return Err( "UniFFI initialization template changed; review eager contract initialization".into(), ); } // Object references deliberately trigger Kotlin initialization and integrity guards. let source = source.replace( initializer, "@Suppress(\"UNUSED_EXPRESSION\")\npublic fun uniffiEnsureInitialized() {", ); let source = source.replace(internal, "class InternalException(@Suppress(\"UNUSED_PARAMETER\") message: String) : kotlin.Exception(\"Native Rust failure ([REDACTED])\")") .replace(callback, "val err = \"Native callback failed ([REDACTED])\""); // Callback results are operation-owned; erase plaintext after copying into the wire buffer. let returned_secret = "val writeReturn = { value: kotlin.ByteArray -> uniffiOutReturn.setValue(FfiConverterByteArray.lower(value)) }"; if source.contains("internal object uniffiCallbackInterfaceMobileAndroidKeystore") && source.matches(returned_secret).count() != 2 { return Err("UniFFI secret callback template changed; review byte-array lifetime".into()); } let source = source.replace(returned_secret, "val writeReturn = { value: kotlin.ByteArray -> try { uniffiOutReturn.setValue(FfiConverterByteArray.lower(value)) } finally { value.fill(0) } }"); let records = Regex::new(r"(?ms)^([ \t]*)data class (\w+)\s*\((.*?)\)[^\{]*\{\n")?; if !records.is_match(&source) { return Err("UniFFI record template changed; review diagnostic hardening".into()); } let source = records.replace_all(&source, |captures: ®ex::Captures<'_>| { format!( "{} override fun toString(): String = \"{}([REDACTED])\"\n", &captures[0], &captures[2] ) }); let errors = Regex::new(r#"(?m)(override val message\n\s*)get\(\) = ".*"$"#)?; // Kotlin Exception.message is reserved; keep the Rust field/wire checksum unchanged. let source = source .replace( "val `message`: kotlin.String", "val `rustMessage`: kotlin.String", ) .replace("value.`message`", "value.`rustMessage`"); let source = errors.replace_all( &source, "${1}get() = \"Rust operation failed ([REDACTED])\"", ); let contract = "throw RuntimeException(\"UniFFI contract version mismatch: try cleaning and rebuilding your project\")"; let checksum = "throw RuntimeException(\"UniFFI API checksum mismatch: try cleaning and rebuilding your project\")"; if !source.contains(contract) || !source.contains(checksum) { return Err( "UniFFI integrity template changed; checksum checks must remain enabled".into(), ); } let checksum_guard = Regex::new( r#"if \(lib\.(\w+)\(\) != (-?\d+)\) \{\s*throw RuntimeException\("UniFFI API checksum mismatch: try cleaning and rebuilding your project"\)\s*\}"#, )?; let contract_guard = Regex::new( r#"if \(bindings_contract_version != scaffolding_contract_version\) \{\s*throw RuntimeException\("UniFFI contract version mismatch: try cleaning and rebuilding your project"\)\s*\}"#, )?; if !checksum_guard.is_match(&source) || !contract_guard.is_match(&source) { return Err( "UniFFI integrity guard changed; refusing to generate unchecked bindings".into(), ); } let output = checksum_guard.replace_all(&source, "de.rfc1437.ironstorage.bridge.NativeBindingChecks.requireMatch(lib.${1}().toLong(), ${2}.toLong(), de.rfc1437.ironstorage.bridge.NativeFailureKind.ChecksumMismatch)"); let output = contract_guard.replace_all(&output, "de.rfc1437.ironstorage.bridge.NativeBindingChecks.requireMatch(scaffolding_contract_version.toLong(), bindings_contract_version.toLong(), de.rfc1437.ironstorage.bridge.NativeFailureKind.ContractMismatch)").into_owned(); if output.contains(contract) || output.contains(checksum) { return Err("UniFFI integrity guard was not translated completely".into()); } Ok(output) } #[cfg(test)] mod tests { use super::*; const SOURCE: &str = "public fun uniffiEnsureInitialized() {\n IntegrityCheckingUniffiLib\n UniffiLib\n}\ndata class Secret (\n val value: String\n)\n{\n companion object\n}\nclass Failure {\n override val message\n get() = \"value=${value}\"\n}\nif (bindings_contract_version != scaffolding_contract_version) { throw RuntimeException(\"UniFFI contract version mismatch: try cleaning and rebuilding your project\") }\nif (lib.checksum() != 1234) { throw RuntimeException(\"UniFFI API checksum mismatch: try cleaning and rebuilding your project\") }\nclass InternalException(message: String) : kotlin.Exception(message)\nval err = try { e.stackTraceToString() } catch(_: Throwable) { \"\" }\nval err = try { e.stackTraceToString() } catch(_: Throwable) { \"\" }\n"; #[test] fn redacts_record_and_error_diagnostics_without_changing_fields_or_checksums() { let hardened = harden_source(SOURCE).unwrap(); assert!( hardened .contains("@Suppress(\"UNUSED_EXPRESSION\")\npublic fun uniffiEnsureInitialized()") ); assert!(hardened.contains(" IntegrityCheckingUniffiLib\n")); assert!(hardened.contains("val value: String")); assert!(hardened.contains("Secret([REDACTED])")); assert!(!hardened.contains("get() = \"value=")); assert!(hardened.contains("requireMatch(lib.checksum().toLong(), 1234.toLong()")); assert!(hardened.contains("NativeFailureKind.ChecksumMismatch")); assert!(hardened.contains("NativeFailureKind.ContractMismatch")); assert!(!hardened.contains("stackTraceToString")); assert!(!hardened.contains("kotlin.Exception(message)")); } #[test] fn erases_owned_callback_results_after_serialization_even_on_failure() { let source = SOURCE.to_owned() + "val writeReturn = { value: kotlin.ByteArray -> uniffiOutReturn.setValue(FfiConverterByteArray.lower(value)) }"; let hardened = harden_source(&source).unwrap(); assert!(hardened.contains("finally { value.fill(0) }")); assert!(hardened.contains("FfiConverterByteArray.lower(value)")); assert!( harden_source( &(SOURCE.to_owned() + "internal object uniffiCallbackInterfaceMobileAndroidKeystore") ) .is_err() ); } #[test] fn fails_closed_if_upstream_templates_or_integrity_checks_change() { assert!(harden_source("data class Secret(val value: String)").is_err()); assert!(harden_source(&SOURCE.replace("UniFFI API checksum mismatch", "changed")).is_err()); } #[test] fn preserves_rust_message_fields_without_colliding_with_kotlin_exception_message() { let source = SOURCE.replace("val value: String", "val `message`: kotlin.String") + "FfiConverterString.write(value.`message`, buf)"; let hardened = harden_source(&source).unwrap(); assert!(hardened.contains("val `rustMessage`: kotlin.String")); assert!(hardened.contains("FfiConverterString.write(value.`rustMessage`, buf)")); assert!(hardened.contains("override val message")); } }