Implement Rust-owned Android local and HTTPS onboarding (#95)
Android ARM verification / rust (push) Failing after 46m9s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 21s

This commit is contained in:
hugo committed 2026-10-09 11:31:50 +02:00
1 parent 66b1d4a4c4
commit 36e5f55283
46 files changed
+3002 -79

No files matched your search

@@ -0,0 +1,187 @@
//! Mechanical Android onboarding exports; all setup decisions remain in storage.
use crate::{
MobileOnboardingDiscovery, MobileOnboardingFfiError, MobileOnboardingOutcome,
MobileOnboardingProgress, MobilePrivateRuntime, MobileSecretBackend,
platform::SecretBackendAdapter,
};
use ironstorage::{
mobile_onboarding::{
MobileOnboardingRequest,
android::{AndroidOnboarding, AndroidOnboardingStatus},
},
repository::SecretBytes,
};
use std::sync::Arc;
#[derive(Clone, uniffi::Record)]
pub struct MobileAndroidSetupKey {
pub fingerprint: String,
pub user_ids: Vec<String>,
}
#[derive(Clone, uniffi::Record)]
pub struct MobileAndroidSetupStatus {
pub configured: bool,
pub connected: bool,
pub existing_clone: bool,
pub keys: Vec<MobileAndroidSetupKey>,
pub selected_key: Option<String>,
pub title: String,
pub detail: String,
}
impl From<AndroidOnboardingStatus> for MobileAndroidSetupStatus {
fn from(status: AndroidOnboardingStatus) -> Self {
Self {
title: status.title,
detail: status.detail,
configured: status.configured,
connected: status.connected,
existing_clone: status.existing_clone,
selected_key: status.selected_key,
keys: status
.keys
.into_iter()
.map(|key| MobileAndroidSetupKey {
fingerprint: key.fingerprint().as_str().into(),
user_ids: key.user_ids().to_vec(),
})
.collect(),
}
}
}
#[derive(uniffi::Object)]
pub struct MobileAndroidOnboarding {
setup: Arc<AndroidOnboarding>,
}
impl MobileAndroidOnboarding {
pub(crate) fn new(
runtime: &MobilePrivateRuntime,
backend: Arc<dyn MobileSecretBackend>,
) -> Result<Arc<Self>, MobileOnboardingFfiError> {
Ok(Arc::new(Self {
setup: Arc::new(AndroidOnboarding::new(
&runtime.storage,
Arc::new(SecretBackendAdapter(backend)),
)?),
}))
}
}
fn secret(value: Option<String>) -> Option<SecretBytes> {
value.map(|value| SecretBytes::new(value.into_bytes()))
}
fn outcome(
value: ironstorage::mobile_onboarding::MobileOnboardingOutcome,
) -> MobileOnboardingOutcome {
MobileOnboardingOutcome {
title: value.title().into(),
detail: value.detail().into(),
}
}
#[uniffi::export]
impl MobileAndroidOnboarding {
pub fn status(&self) -> Result<MobileAndroidSetupStatus, MobileOnboardingFfiError> {
Ok(self.setup.status()?.into())
}
pub fn generate(
&self,
user_id: String,
passphrase: String,
) -> Result<MobileAndroidSetupStatus, MobileOnboardingFfiError> {
Ok(self
.setup
.generate(&user_id, SecretBytes::new(passphrase.into_bytes()))?
.into())
}
pub fn import_key(
&self,
armor: String,
passphrase: Option<String>,
) -> Result<MobileAndroidSetupStatus, MobileOnboardingFfiError> {
Ok(self
.setup
.import(SecretBytes::new(armor.into_bytes()), secret(passphrase))?
.into())
}
pub fn setup_local(
&self,
fingerprint: String,
passphrase: Option<String>,
) -> Result<MobileOnboardingOutcome, MobileOnboardingFfiError> {
Ok(outcome(
self.setup.setup_local(&fingerprint, secret(passphrase))?,
))
}
pub fn remote(
&self,
server_url: String,
account: String,
repository_path: String,
application_token: String,
) -> Result<Arc<MobileAndroidRemoteOnboarding>, MobileOnboardingFfiError> {
Ok(Arc::new(MobileAndroidRemoteOnboarding {
setup: Arc::clone(&self.setup),
request: MobileOnboardingRequest::new(
server_url,
account,
repository_path,
application_token.into_bytes(),
)?,
}))
}
pub fn replace_token(
&self,
account: String,
application_token: String,
) -> Result<(), MobileOnboardingFfiError> {
Ok(self
.setup
.replace_token(account, SecretBytes::new(application_token.into_bytes()))?)
}
pub fn cancel(&self) {
self.setup.cancel();
}
pub fn progress(&self) -> MobileOnboardingProgress {
let progress = self.setup.progress();
MobileOnboardingProgress {
phase: progress.phase().into(),
title: progress.title().into(),
detail: progress.detail().into(),
}
}
}
#[derive(uniffi::Object)]
pub struct MobileAndroidRemoteOnboarding {
setup: Arc<AndroidOnboarding>,
request: MobileOnboardingRequest,
}
#[uniffi::export]
impl MobileAndroidRemoteOnboarding {
pub fn discover(&self) -> Result<MobileOnboardingDiscovery, MobileOnboardingFfiError> {
let value = self.setup.discover(&self.request)?;
Ok(MobileOnboardingDiscovery {
branches: value.branches().to_vec(),
selected_branch: value.selected_branch() as u32,
})
}
pub fn clone_store(
&self,
branch: String,
fingerprint: String,
passphrase: Option<String>,
use_existing: bool,
) -> Result<MobileOnboardingOutcome, MobileOnboardingFfiError> {
Ok(outcome(self.setup.clone_store(
&self.request,
&branch,
&fingerprint,
secret(passphrase),
use_existing,
)?))
}
pub fn connect(&self) -> Result<MobileOnboardingOutcome, MobileOnboardingFfiError> {
Ok(outcome(self.setup.connect(&self.request)?))
}
pub fn cancel(&self) {
self.setup.cancel();
}
}
+2
View File
@@ -81,6 +81,8 @@ uniffi::setup_scaffolding!();
mod android_keystore;
pub use android_keystore::*;
mod android_onboarding;
pub use android_onboarding::*;
mod platform;
pub use platform::*;
+7 -1
View File
@@ -96,10 +96,16 @@ pub struct MobilePrivateStorageLayout {
#[derive(uniffi::Object)]
pub struct MobilePrivateRuntime {
storage: PrivateStorage,
pub(crate) storage: PrivateStorage,
}
#[uniffi::export]
impl MobilePrivateRuntime {
pub fn android_onboarding(
&self,
backend: Arc<dyn MobileSecretBackend>,
) -> Result<Arc<crate::MobileAndroidOnboarding>, crate::MobileOnboardingFfiError> {
crate::MobileAndroidOnboarding::new(self, backend)
}
pub fn android_secret_backend(
&self,
keystore: Arc<dyn crate::MobileAndroidKeystore>,