//! Mechanical adapters: foreign code performs OS calls, storage owns behavior. use std::{fmt, sync::Arc, time::Duration}; use ironstorage::{ authentication::AuthenticationClock, config::Config, mobile_platform as storage, presentation::{self, ClipboardBackend}, repository::SecretBytes, secret_store::{SecretLocator, SecretProtection, SecretStoreBackend, SecretStoreError}, }; use crate::{ MobileAuthentication, MobileAuthenticationFfiError, MobileAuthenticationState, MobileEntryCopy, }; #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Error)] pub enum MobilePlatformFfiError { InvalidPaths, InvalidCameraFrame, QrNotFound, InvalidQrPayload, InvalidCredentialRequest, ClipboardFailed, Cancelled, } impl fmt::Display for MobilePlatformFfiError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Mobile platform operation failed: {self:?}") } } impl std::error::Error for MobilePlatformFfiError {} #[derive(Clone, uniffi::Record)] pub struct MobilePlatformPaths { pub data_root: String, pub cache_root: String, } impl TryFrom for storage::MobilePlatformPaths { type Error = MobilePlatformFfiError; fn try_from(paths: MobilePlatformPaths) -> Result { Self::new(paths.data_root.into(), paths.cache_root.into()) .map_err(|_| MobilePlatformFfiError::InvalidPaths) } } #[derive(Clone, uniffi::Record)] pub struct MobilePlatformLayout { pub configuration: String, pub vault: String, pub keys: String, pub cache: String, } #[uniffi::export] pub fn mobile_platform_layout( paths: MobilePlatformPaths, ) -> Result { let paths = storage::MobilePlatformPaths::try_from(paths)?; Ok(MobilePlatformLayout { configuration: paths.configuration().to_string_lossy().into_owned(), vault: paths.vault().to_string_lossy().into_owned(), keys: paths.keys().to_string_lossy().into_owned(), cache: paths.cache().to_string_lossy().into_owned(), }) } #[derive(Clone, uniffi::Enum)] pub enum MobileSecretLocator { OpenPgpPassphrase { fingerprint: String, }, HttpsGitCredential { server_id: String, application_id: String, }, SshKeyPassphrase { fingerprint: String, }, } impl From<&SecretLocator> for MobileSecretLocator { fn from(locator: &SecretLocator) -> Self { match locator { SecretLocator::OpenPgpPassphrase { fingerprint } => Self::OpenPgpPassphrase { fingerprint: fingerprint.clone(), }, SecretLocator::HttpsGitCredential { server_id, application_id, } => Self::HttpsGitCredential { server_id: server_id.clone(), application_id: application_id.clone(), }, SecretLocator::SshKeyPassphrase { fingerprint } => Self::SshKeyPassphrase { fingerprint: fingerprint.to_string(), }, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileSecretProtection { DeviceUnlocked, RequireUserPresence, BiometryCurrentSet, } impl From for MobileSecretProtection { fn from(value: SecretProtection) -> Self { match value { SecretProtection::DeviceUnlocked => Self::DeviceUnlocked, SecretProtection::RequireUserPresence => Self::RequireUserPresence, SecretProtection::BiometryCurrentSet => Self::BiometryCurrentSet, } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Error)] pub enum MobileSecretError { AlreadyExists, Missing, Locked, Denied, Cancelled, Corrupted, UnsupportedProtection, Unavailable, } impl fmt::Display for MobileSecretError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "OS secret operation failed: {self:?}") } } impl std::error::Error for MobileSecretError {} impl From for SecretStoreError { fn from(value: MobileSecretError) -> Self { match value { MobileSecretError::AlreadyExists => Self::AlreadyExists, MobileSecretError::Missing => Self::Missing, MobileSecretError::Locked => Self::Locked, MobileSecretError::Denied => Self::Denied, MobileSecretError::Cancelled => Self::Cancelled, MobileSecretError::Corrupted => Self::Corrupted, MobileSecretError::UnsupportedProtection => Self::UnsupportedProtection, MobileSecretError::Unavailable => Self::Unavailable, } } } /// Receives storage-encoded opaque bytes; cannot select accounts, leases or protection. #[uniffi::export(rust, foreign)] pub trait MobileSecretBackend: Send + Sync { fn create( &self, locator: MobileSecretLocator, protection: MobileSecretProtection, value: Vec, ) -> Result<(), MobileSecretError>; fn retrieve( &self, locator: MobileSecretLocator, protection: MobileSecretProtection, ) -> Result, MobileSecretError>; fn replace( &self, locator: MobileSecretLocator, protection: MobileSecretProtection, value: Vec, ) -> Result<(), MobileSecretError>; fn delete( &self, locator: MobileSecretLocator, protection: MobileSecretProtection, ) -> Result<(), MobileSecretError>; fn lock(&self) -> Result<(), MobileSecretError>; fn unlock(&self) -> Result<(), MobileSecretError>; } pub(crate) struct SecretBackendAdapter(pub Arc); impl SecretStoreBackend for SecretBackendAdapter { fn create( &self, locator: &SecretLocator, protection: SecretProtection, value: &[u8], ) -> Result<(), SecretStoreError> { self.0 .create(locator.into(), protection.into(), value.to_vec()) .map_err(Into::into) } fn retrieve( &self, locator: &SecretLocator, protection: SecretProtection, ) -> Result { self.0 .retrieve(locator.into(), protection.into()) .map(SecretBytes::new) .map_err(Into::into) } fn replace( &self, locator: &SecretLocator, protection: SecretProtection, value: &[u8], ) -> Result<(), SecretStoreError> { self.0 .replace(locator.into(), protection.into(), value.to_vec()) .map_err(Into::into) } fn delete( &self, locator: &SecretLocator, protection: SecretProtection, ) -> Result<(), SecretStoreError> { self.0 .delete(locator.into(), protection.into()) .map_err(Into::into) } fn lock(&self) -> Result<(), SecretStoreError> { self.0.lock().map_err(Into::into) } fn unlock(&self) -> Result<(), SecretStoreError> { self.0.unlock().map_err(Into::into) } } #[uniffi::export(rust, foreign)] pub trait MobileClock: Send + Sync { fn monotonic_milliseconds(&self) -> u64; } struct ClockAdapter(Arc); impl AuthenticationClock for ClockAdapter { fn now(&self) -> Duration { Duration::from_millis(self.0.monotonic_milliseconds()) } } #[uniffi::export] pub fn mobile_authentication_with_platform( paths: MobilePlatformPaths, backend: Arc, clock: Arc, ) -> Result, MobileAuthenticationFfiError> { let paths = storage::MobilePlatformPaths::try_from(paths).map_err(|_| { MobileAuthenticationFfiError::Failed { kind: crate::MobileAuthenticationErrorKind::Configuration, title: "Invalid Private Paths".into(), detail: "The OS private roots are invalid.".into(), } })?; let config = Config::load(Some(&paths.configuration())).map_err(|error| { MobileAuthenticationFfiError::Failed { kind: crate::MobileAuthenticationErrorKind::Configuration, title: "Configuration Is Unavailable".into(), detail: error.to_string(), } })?; let authentication = ironstorage::mobile_authentication::MobileAuthentication::with_platform( config, Arc::new(SecretBackendAdapter(backend)), Arc::new(ClockAdapter(clock)), )?; Ok(Arc::new(MobileAuthentication { authentication: Arc::new(authentication), })) } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileLifecycleEvent { Foreground, Background, DeviceLocked, UserActivity, Cancel, } impl From for storage::MobileLifecycleEvent { fn from(event: MobileLifecycleEvent) -> Self { match event { MobileLifecycleEvent::Foreground => Self::Foreground, MobileLifecycleEvent::Background => Self::Background, MobileLifecycleEvent::DeviceLocked => Self::DeviceLocked, MobileLifecycleEvent::UserActivity => Self::UserActivity, MobileLifecycleEvent::Cancel => Self::Cancel, } } } #[uniffi::export] impl MobileAuthentication { pub fn lifecycle( &self, event: MobileLifecycleEvent, ) -> Result { self.authentication .lifecycle(event.into()) .map(Into::into) .map_err(Into::into) } } #[derive(Clone, uniffi::Record)] pub struct MobileCameraFrame { pub width: u32, pub height: u32, pub row_stride: u32, pub pixel_stride: u32, pub luminance: Vec, } #[uniffi::export] pub fn decode_mobile_camera_frame( frame: MobileCameraFrame, ) -> Result, MobilePlatformFfiError> { storage::MobileCameraFrame { width: frame.width, height: frame.height, row_stride: frame.row_stride, pixel_stride: frame.pixel_stride, luminance: SecretBytes::new(frame.luminance), } .decode() .map(|payload| payload.expose().to_vec()) .map_err(|error| match error { presentation::QrError::NotFound => MobilePlatformFfiError::QrNotFound, presentation::QrError::InvalidPayload => MobilePlatformFfiError::InvalidQrPayload, _ => MobilePlatformFfiError::InvalidCameraFrame, }) } #[derive(Clone, uniffi::Enum)] pub enum MobileClipboardContent { Text { value: Vec }, EmptyOrNonText, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileClipboardWait { Elapsed, Cancelled, } #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] pub enum MobileClipboardDisposition { RestoredPrevious, Cleared, PreservedNewer, } #[uniffi::export(rust, foreign)] pub trait MobileClipboard: Send + Sync { fn read(&self) -> Result; fn write(&self, value: Vec) -> Result<(), MobilePlatformFfiError>; fn clear(&self) -> Result<(), MobilePlatformFfiError>; fn wait_for_timeout(&self, milliseconds: u64) -> MobileClipboardWait; } struct ClipboardAdapter(Arc); impl ClipboardBackend for ClipboardAdapter { fn read(&mut self) -> Result { self.0 .read() .map(|value| match value { MobileClipboardContent::Text { value } => { presentation::ClipboardContent::text(value) } MobileClipboardContent::EmptyOrNonText => { presentation::ClipboardContent::EmptyOrNonText } }) .map_err(|_| presentation::ClipboardError::ReadFailed) } fn write(&mut self, value: &SecretBytes) -> Result<(), presentation::ClipboardError> { self.0 .write(value.expose().to_vec()) .map_err(|_| presentation::ClipboardError::WriteFailed) } fn clear(&mut self) -> Result<(), presentation::ClipboardError> { self.0 .clear() .map_err(|_| presentation::ClipboardError::CleanupFailed) } } #[uniffi::export] pub fn copy_mobile_entry( copy: MobileEntryCopy, clipboard: Arc, ) -> Result { let timeout = presentation::ClipboardTimeout::new(Duration::from_secs(copy.timeout_seconds)) .map_err(|_| MobilePlatformFfiError::ClipboardFailed)?; let mut manager = presentation::ClipboardManager::new(ClipboardAdapter(Arc::clone(&clipboard)), timeout); manager .copy_with( &SecretBytes::new(copy.value.into_bytes()), |duration| match clipboard.wait_for_timeout(duration.as_millis() as u64) { MobileClipboardWait::Elapsed => presentation::ClipboardWait::Elapsed, MobileClipboardWait::Cancelled => presentation::ClipboardWait::Cancelled, }, ) .map(|result| match result { presentation::ClipboardDisposition::RestoredPrevious => { MobileClipboardDisposition::RestoredPrevious } presentation::ClipboardDisposition::Cleared => MobileClipboardDisposition::Cleared, presentation::ClipboardDisposition::PreservedNewer => { MobileClipboardDisposition::PreservedNewer } }) .map_err(|error| { if error == presentation::ClipboardError::Cancelled { MobilePlatformFfiError::Cancelled } else { MobilePlatformFfiError::ClipboardFailed } }) } #[derive(Clone, uniffi::Record)] pub struct MobileCredentialRequest { pub request_id: String, pub package: String, pub signing_certificate_sha256: String, pub origins: Vec, } #[uniffi::export] pub fn validate_mobile_credential_request( request: MobileCredentialRequest, ) -> Result { let request = storage::MobileCredentialRequest::new( request.request_id, request.package, request.signing_certificate_sha256, request.origins, ) .map_err(|_| MobilePlatformFfiError::InvalidCredentialRequest)?; Ok(MobileCredentialRequest { request_id: request.request_id().to_owned(), package: request.package().to_owned(), signing_certificate_sha256: request.signing_certificate_sha256().to_owned(), origins: request.origins().to_vec(), }) }