Implement Rust-owned Android Keystore secure storage (#93)
Dependency security audit / rustsec (push) Failing after 23s

This commit is contained in:
hugo committed 2026-10-08 16:22:57 +02:00
1 parent 874c8139a4
commit 1abd75282e
36 files changed
+4380 -37

No files matched your search

@@ -0,0 +1,282 @@
//! Android OS calls; all envelopes, policy and lifecycle remain in storage.
use crate::{MobileSecretBackend, MobileSecretError, MobileSecretLocator, MobileSecretProtection};
use ironstorage::{
repository::SecretBytes,
secret_store::{
SecretLocator, SecretProtection, SecretStoreBackend, SecretStoreError, android as os,
},
};
use std::{fmt, sync::Arc};
#[derive(Clone, Copy, uniffi::Record)]
pub struct MobileAndroidDeviceState {
pub api: u32,
pub locked: bool,
pub secure: bool,
pub strong_biometric: bool,
pub strongbox: bool,
}
#[derive(Clone, Copy, uniffi::Enum)]
pub enum MobileAndroidAuthorization {
None,
StrongBiometric,
UserPresence,
}
#[derive(Clone, uniffi::Record)]
pub struct MobileAndroidKeySpecification {
pub alias: String,
pub authorization: MobileAndroidAuthorization,
pub unlocked_device: bool,
pub invalidate_on_enrollment: bool,
pub strongbox: bool,
}
#[derive(Clone, uniffi::Record)]
pub struct MobileAndroidCiphertext {
pub nonce: Vec<u8>,
pub bytes: Vec<u8>,
}
#[derive(Clone, Copy, Debug, uniffi::Error)]
pub enum MobileAndroidKeystoreError {
MissingKey,
Invalidated,
Lockout,
Locked,
Denied,
Cancelled,
Corrupted,
UnsupportedProtection,
StrongboxUnavailable,
AlreadyExists,
Unavailable,
}
impl fmt::Display for MobileAndroidKeystoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Android Keystore operation failed")
}
}
impl std::error::Error for MobileAndroidKeystoreError {}
#[uniffi::export(with_foreign)]
pub trait MobileAndroidKeystore: Send + Sync {
fn device_state(&self) -> Result<MobileAndroidDeviceState, MobileAndroidKeystoreError>;
fn create_key(
&self,
specification: MobileAndroidKeySpecification,
) -> Result<(), MobileAndroidKeystoreError>;
fn encrypt(
&self,
alias: String,
authorization: MobileAndroidAuthorization,
aad: Vec<u8>,
value: Vec<u8>,
) -> Result<MobileAndroidCiphertext, MobileAndroidKeystoreError>;
fn decrypt(
&self,
alias: String,
authorization: MobileAndroidAuthorization,
aad: Vec<u8>,
value: MobileAndroidCiphertext,
) -> Result<Vec<u8>, MobileAndroidKeystoreError>;
fn delete_key(&self, alias: String) -> Result<(), MobileAndroidKeystoreError>;
fn aliases(&self) -> Result<Vec<String>, MobileAndroidKeystoreError>;
fn cancel_pending(&self);
}
pub(crate) struct KeystoreAdapter(pub Arc<dyn MobileAndroidKeystore>);
impl From<os::AndroidAuthorization> for MobileAndroidAuthorization {
fn from(value: os::AndroidAuthorization) -> Self {
match value {
os::AndroidAuthorization::None => Self::None,
os::AndroidAuthorization::StrongBiometric => Self::StrongBiometric,
os::AndroidAuthorization::UserPresence => Self::UserPresence,
}
}
}
impl From<MobileAndroidKeystoreError> for os::AndroidKeystoreError {
fn from(value: MobileAndroidKeystoreError) -> Self {
match value {
MobileAndroidKeystoreError::MissingKey => Self::MissingKey,
MobileAndroidKeystoreError::Invalidated => Self::Invalidated,
MobileAndroidKeystoreError::Lockout => Self::Lockout,
MobileAndroidKeystoreError::Locked => Self::Locked,
MobileAndroidKeystoreError::Denied => Self::Denied,
MobileAndroidKeystoreError::Cancelled => Self::Cancelled,
MobileAndroidKeystoreError::Corrupted => Self::Corrupted,
MobileAndroidKeystoreError::UnsupportedProtection => Self::UnsupportedProtection,
MobileAndroidKeystoreError::StrongboxUnavailable => Self::StrongboxUnavailable,
MobileAndroidKeystoreError::AlreadyExists => Self::AlreadyExists,
MobileAndroidKeystoreError::Unavailable => Self::Unavailable,
}
}
}
impl os::AndroidKeystore for KeystoreAdapter {
fn device_state(&self) -> Result<os::AndroidDeviceState, os::AndroidKeystoreError> {
self.0
.device_state()
.map(|s| os::AndroidDeviceState {
api: s.api,
locked: s.locked,
secure: s.secure,
strong_biometric: s.strong_biometric,
strongbox: s.strongbox,
})
.map_err(Into::into)
}
fn create_key(&self, s: &os::AndroidKeySpecification) -> Result<(), os::AndroidKeystoreError> {
self.0
.create_key(MobileAndroidKeySpecification {
alias: s.alias.clone(),
authorization: s.authorization.into(),
unlocked_device: s.unlocked_device,
invalidate_on_enrollment: s.invalidate_on_enrollment,
strongbox: s.strongbox,
})
.map_err(Into::into)
}
fn encrypt(
&self,
alias: &str,
authorization: os::AndroidAuthorization,
aad: &[u8],
value: &SecretBytes,
) -> Result<os::AndroidCiphertext, os::AndroidKeystoreError> {
self.0
.encrypt(
alias.into(),
authorization.into(),
aad.to_vec(),
value.expose().to_vec(),
)
.map(|v| os::AndroidCiphertext {
nonce: v.nonce,
bytes: v.bytes,
})
.map_err(Into::into)
}
fn decrypt(
&self,
alias: &str,
authorization: os::AndroidAuthorization,
aad: &[u8],
value: &os::AndroidCiphertext,
) -> Result<SecretBytes, os::AndroidKeystoreError> {
self.0
.decrypt(
alias.into(),
authorization.into(),
aad.to_vec(),
MobileAndroidCiphertext {
nonce: value.nonce.clone(),
bytes: value.bytes.clone(),
},
)
.map(SecretBytes::new)
.map_err(Into::into)
}
fn delete_key(&self, alias: &str) -> Result<(), os::AndroidKeystoreError> {
self.0.delete_key(alias.into()).map_err(Into::into)
}
fn aliases(&self) -> Result<Vec<String>, os::AndroidKeystoreError> {
self.0.aliases().map_err(Into::into)
}
fn cancel_pending(&self) {
self.0.cancel_pending();
}
}
pub(crate) struct RustSecretBackend(pub os::AndroidSecretBackend);
impl TryFrom<MobileSecretLocator> for SecretLocator {
type Error = MobileSecretError;
fn try_from(value: MobileSecretLocator) -> Result<Self, Self::Error> {
Ok(match value {
MobileSecretLocator::ApplicationBinding => Self::ApplicationBinding,
MobileSecretLocator::OpenPgpPassphrase { fingerprint } => {
Self::OpenPgpPassphrase { fingerprint }
}
MobileSecretLocator::HttpsGitCredential {
server_id,
application_id,
} => Self::HttpsGitCredential {
server_id,
application_id,
},
MobileSecretLocator::SshKeyPassphrase { fingerprint } => Self::SshKeyPassphrase {
fingerprint: ironstorage::config::SshFingerprint::parse(&fingerprint)
.map_err(|_| MobileSecretError::Corrupted)?,
},
})
}
}
impl From<MobileSecretProtection> for SecretProtection {
fn from(value: MobileSecretProtection) -> Self {
match value {
MobileSecretProtection::DeviceUnlocked => Self::DeviceUnlocked,
MobileSecretProtection::RequireUserPresence => Self::RequireUserPresence,
MobileSecretProtection::BiometryCurrentSet => Self::BiometryCurrentSet,
}
}
}
impl From<SecretStoreError> for MobileSecretError {
fn from(value: SecretStoreError) -> Self {
match value {
SecretStoreError::AlreadyExists => Self::AlreadyExists,
SecretStoreError::Missing => Self::Missing,
SecretStoreError::Locked => Self::Locked,
SecretStoreError::Denied => Self::Denied,
SecretStoreError::Cancelled => Self::Cancelled,
SecretStoreError::Corrupted => Self::Corrupted,
SecretStoreError::UnsupportedProtection => Self::UnsupportedProtection,
SecretStoreError::Unavailable => Self::Unavailable,
SecretStoreError::DeviceKeyMissing => Self::DeviceKeyMissing,
SecretStoreError::Invalidated => Self::Invalidated,
SecretStoreError::Lockout => Self::Lockout,
_ => Self::Corrupted,
}
}
}
impl MobileSecretBackend for RustSecretBackend {
fn create(
&self,
locator: MobileSecretLocator,
protection: MobileSecretProtection,
value: Vec<u8>,
) -> Result<(), MobileSecretError> {
let secret = SecretBytes::new(value);
self.0
.create(&locator.try_into()?, protection.into(), secret.expose())
.map_err(Into::into)
}
fn replace(
&self,
locator: MobileSecretLocator,
protection: MobileSecretProtection,
value: Vec<u8>,
) -> Result<(), MobileSecretError> {
let secret = SecretBytes::new(value);
self.0
.replace(&locator.try_into()?, protection.into(), secret.expose())
.map_err(Into::into)
}
fn retrieve(
&self,
locator: MobileSecretLocator,
protection: MobileSecretProtection,
) -> Result<Vec<u8>, MobileSecretError> {
self.0
.retrieve(&locator.try_into()?, protection.into())
.map(|v| v.expose().to_vec())
.map_err(Into::into)
}
fn delete(
&self,
locator: MobileSecretLocator,
protection: MobileSecretProtection,
) -> Result<(), MobileSecretError> {
self.0
.delete(&locator.try_into()?, protection.into())
.map_err(Into::into)
}
fn lock(&self) -> Result<(), MobileSecretError> {
self.0.lock().map_err(Into::into)
}
fn unlock(&self) -> Result<(), MobileSecretError> {
self.0.unlock().map_err(Into::into)
}
}
+2
View File
@@ -79,6 +79,8 @@ use ironstorage::{
uniffi::setup_scaffolding!();
mod android_keystore;
pub use android_keystore::*;
mod platform;
pub use platform::*;
+6
View File
@@ -130,6 +130,9 @@ pub enum MobileSecretError {
Corrupted,
UnsupportedProtection,
Unavailable,
DeviceKeyMissing,
Invalidated,
Lockout,
}
impl fmt::Display for MobileSecretError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -148,6 +151,9 @@ impl From<MobileSecretError> for SecretStoreError {
MobileSecretError::Corrupted => Self::Corrupted,
MobileSecretError::UnsupportedProtection => Self::UnsupportedProtection,
MobileSecretError::Unavailable => Self::Unavailable,
MobileSecretError::DeviceKeyMissing => Self::DeviceKeyMissing,
MobileSecretError::Invalidated => Self::Invalidated,
MobileSecretError::Lockout => Self::Lockout,
}
}
}
@@ -100,6 +100,17 @@ pub struct MobilePrivateRuntime {
}
#[uniffi::export]
impl MobilePrivateRuntime {
pub fn android_secret_backend(
&self,
keystore: Arc<dyn crate::MobileAndroidKeystore>,
) -> Result<Arc<dyn MobileSecretBackend>, MobilePrivateStorageFfiError> {
let backend = self
.storage
.android_secret_backend(Arc::new(crate::android_keystore::KeystoreAdapter(keystore)))?;
Ok(Arc::new(crate::android_keystore::RustSecretBackend(
backend,
)))
}
pub fn activate(&self) -> Result<(), MobilePrivateStorageFfiError> {
self.storage.activate().map_err(Into::into)
}