//! Enrollment: where the backend is, and the Keystore-sealed token to //! reach it. This crate does not reimplement the Android Keystore AES-GCM //! sealing in Rust -- it calls the same `wg-app-link` `ServerStore` Kotlin //! class the production app already uses (see `ServerConfig.kt`), through //! JNI, for two reasons: that code is shared with Dev Updater and already //! tested, and the sealed value on a real phone is keyed to the exact //! Keystore alias that class already uses -- reimplementing the crypto //! here would either duplicate it or invalidate an existing enrollment. use jni::Env; use jni::errors::Result; use jni::objects::{JObject, JString, JValue}; /// Where the backend is and how to authenticate to it -- the Rust twin of /// `wg-app-link`'s `ServerSettings` data class, read back field by field /// rather than kept as a live JNI reference, so it can cross a thread /// boundary (a `JObject` is tied to one `Env`/thread). #[derive(Debug, Clone)] pub struct ServerSettings { pub host: String, pub port: i32, pub token: String, } impl ServerSettings { pub fn base_url(&self) -> String { format!("https://{}:{}", self.host, self.port) } } /// This experiment's own scheme and Keystore alias -- distinct from the /// production app's (`aiapp` / `aiapp-token-key`) so the two can be /// installed side by side on the same development device without /// colliding over which one a scanned QR or a deep link resolves to. See /// RUST.md's E3 entry for why they are not the same value. pub(crate) const SCHEME: &str = "aiappshell"; const KEY_ALIAS: &str = "aiapp-shell-token-key"; const STORE_CLASS: &str = "com/example/wgapplink/ServerStore"; const SETTINGS_CLASS: &str = "com/example/wgapplink/ServerSettings"; fn new_store<'l>(env: &mut Env<'l>) -> Result> { let scheme = crate::jcall::jstr_obj(env, SCHEME)?; let alias = crate::jcall::jstr_obj(env, KEY_ALIAS)?; crate::jcall::new_object( env, STORE_CLASS, "(Ljava/lang/String;Ljava/lang/String;)V", &[JValue::Object(&scheme), JValue::Object(&alias)], ) } fn read_settings(env: &mut Env, settings_obj: &JObject) -> Result { let host = get_string(env, settings_obj, "getHost")?; let port = crate::jcall::call_method(env, settings_obj, "getPort", "()I", &[])?.i()?; let token = get_string(env, settings_obj, "getToken")?; Ok(ServerSettings { host, port, token }) } fn get_string(env: &mut Env, obj: &JObject, getter: &str) -> Result { let value = crate::jcall::call_method(env, obj, getter, "()Ljava/lang/String;", &[])?.l()?; let jstr: JString = env.cast_local::(value)?; jstr.try_to_string(env) } /// The stored enrollment, or `None` when there is not one -- mirrors /// `ServerConfig.kt`'s `loadServerSettings`. pub fn load(env: &mut Env, context: &JObject) -> Result> { let store = new_store(env)?; let settings_obj = crate::jcall::call_method( env, &store, "load", "(Landroid/content/Context;)Lcom/example/wgapplink/ServerSettings;", &[JValue::Object(context)], )? .l()?; if settings_obj.is_null() { return Ok(None); } Ok(Some(read_settings(env, &settings_obj)?)) } /// Seals and stores `settings` -- mirrors `ServerConfig.kt`'s `saveServerSettings`. pub fn save(env: &mut Env, context: &JObject, settings: &ServerSettings) -> Result<()> { let store = new_store(env)?; let host = crate::jcall::jstr_obj(env, &settings.host)?; let token = crate::jcall::jstr_obj(env, &settings.token)?; let settings_obj = crate::jcall::new_object( env, SETTINGS_CLASS, "(Ljava/lang/String;ILjava/lang/String;)V", &[ JValue::Object(&host), JValue::Int(settings.port), JValue::Object(&token), ], )?; crate::jcall::call_method( env, &store, "save", "(Landroid/content/Context;Lcom/example/wgapplink/ServerSettings;)V", &[JValue::Object(context), JValue::Object(&settings_obj)], )?; Ok(()) } /// Parses an `aiappshell://enroll?...` URI -- mirrors `ServerConfig.kt`'s /// `parseEnrollmentUri`, asking the same Kotlin code that already owns the /// query-parameter rules rather than re-deriving them here. pub fn parse_enrollment_uri(env: &mut Env, uri: &JObject) -> Result> { let store = new_store(env)?; let settings_obj = crate::jcall::call_method( env, &store, "parseEnrollmentUri", "(Landroid/net/Uri;)Lcom/example/wgapplink/ServerSettings;", &[JValue::Object(uri)], )? .l()?; if settings_obj.is_null() { return Ok(None); } Ok(Some(read_settings(env, &settings_obj)?)) } /// The CA this build pins, generated at build time the same way /// `androidApp`'s `generatePinnedCert` task does (see `build.gradle.kts`) /// but into a plain Java constant, since this module has no Kotlin of its /// own to generate into. pub fn load_pinned_ca(env: &mut Env) -> Result> { let value = crate::jcall::get_static_field( env, "com/example/aiapp/shell/PinnedCa", "PINNED_CA_PEM", "Ljava/lang/String;", )? .l()?; let jstr: JString = env.cast_local::(value)?; Ok(jstr.try_to_string(env)?.into_bytes()) }