Implement Rust-owned Android Keystore secure storage (#93)
Dependency security audit / rustsec (push) Failing after 23s
Dependency security audit / rustsec (push) Failing after 23s
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,8 @@ use ironstorage::{
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
|
||||
mod android_keystore;
|
||||
pub use android_keystore::*;
|
||||
mod platform;
|
||||
pub use platform::*;
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user