use std::path::PathBuf;
use thiserror::Error;
use zeroize::Zeroize;
#[derive(Debug, Error)]
pub enum StorageError {
#[error("HostKey not found")]
NotFound,
#[error("Storage backend not available: {0}")]
BackendUnavailable(String),
#[error("ANTQ_HOSTKEY_PASSWORD environment variable not set")]
PasswordRequired,
#[error("Cryptographic operation failed: {0}")]
CryptoError(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Invalid data format: {0}")]
InvalidFormat(String),
#[error("Keychain error: {0}")]
KeychainError(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
}
pub type StorageResult<T> = Result<T, StorageError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageSecurityLevel {
Secure,
Encrypted,
Insecure,
}
impl StorageSecurityLevel {
pub fn warning_message(&self) -> Option<&'static str> {
match self {
Self::Secure | Self::Encrypted => None,
Self::Insecure => Some(
"⚠️ HostKey stored WITHOUT ENCRYPTION!\n\
Anyone with file access can read and impersonate this node.\n\
To secure: set ANTQ_HOSTKEY_PASSWORD environment variable.",
),
}
}
pub fn is_secure(&self) -> bool {
matches!(self, Self::Secure | Self::Encrypted)
}
}
pub trait HostKeyStorage: Send + Sync {
fn store(&self, hostkey: &[u8; 32]) -> StorageResult<()>;
fn load(&self) -> StorageResult<[u8; 32]>;
fn delete(&self) -> StorageResult<()>;
fn exists(&self) -> bool;
fn backend_name(&self) -> &'static str;
fn security_level(&self) -> StorageSecurityLevel;
}
const FILE_FORMAT_VERSION: u8 = 1;
const SALT_SIZE: usize = 32;
pub struct EncryptedFileStorage {
path: PathBuf,
}
impl EncryptedFileStorage {
pub fn new() -> StorageResult<Self> {
let path = Self::default_path()?;
Ok(Self { path })
}
pub fn with_path(path: PathBuf) -> Self {
Self { path }
}
fn default_path() -> StorageResult<PathBuf> {
let config_dir = dirs::config_dir().ok_or_else(|| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not determine config directory",
))
})?;
let path = config_dir.join("ant-quic").join("hostkey.enc");
Ok(path)
}
fn get_password() -> StorageResult<String> {
std::env::var("ANTQ_HOSTKEY_PASSWORD").map_err(|_| StorageError::PasswordRequired)
}
fn derive_key_from_password(password: &str, salt: &[u8]) -> StorageResult<[u8; 32]> {
use aws_lc_rs::hkdf;
let hkdf_salt = hkdf::Salt::new(hkdf::HKDF_SHA256, salt);
let prk = hkdf_salt.extract(password.as_bytes());
let mut key = [0u8; 32];
let okm = prk
.expand(&[b"antq:hostkey-file:v1"], hkdf::HKDF_SHA256)
.map_err(|e| StorageError::CryptoError(format!("HKDF expand failed: {e}")))?;
okm.fill(&mut key)
.map_err(|e| StorageError::CryptoError(format!("HKDF fill failed: {e}")))?;
Ok(key)
}
fn encrypt(key: &[u8; 32], plaintext: &[u8; 32]) -> StorageResult<Vec<u8>> {
use aws_lc_rs::aead::{
self, Aad, BoundKey, CHACHA20_POLY1305, Nonce, NonceSequence, UnboundKey,
};
let mut nonce_bytes = [0u8; 12];
aws_lc_rs::rand::fill(&mut nonce_bytes)
.map_err(|e| StorageError::CryptoError(format!("Failed to generate nonce: {e}")))?;
let unbound_key = UnboundKey::new(&CHACHA20_POLY1305, key)
.map_err(|e| StorageError::CryptoError(format!("Failed to create key: {e}")))?;
struct SingleNonce(Option<[u8; 12]>);
impl NonceSequence for SingleNonce {
fn advance(&mut self) -> Result<Nonce, aws_lc_rs::error::Unspecified> {
self.0
.take()
.map(Nonce::assume_unique_for_key)
.ok_or(aws_lc_rs::error::Unspecified)
}
}
let mut sealing_key = aead::SealingKey::new(unbound_key, SingleNonce(Some(nonce_bytes)));
let mut in_out = plaintext.to_vec();
sealing_key
.seal_in_place_append_tag(Aad::empty(), &mut in_out)
.map_err(|e| StorageError::CryptoError(format!("Encryption failed: {e}")))?;
let mut result = Vec::with_capacity(12 + in_out.len());
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&in_out);
Ok(result)
}
fn decrypt(key: &[u8; 32], ciphertext: &[u8]) -> StorageResult<[u8; 32]> {
use aws_lc_rs::aead::{
self, Aad, BoundKey, CHACHA20_POLY1305, Nonce, NonceSequence, UnboundKey,
};
if ciphertext.len() < 12 + 16 {
return Err(StorageError::InvalidFormat(
"Ciphertext too short".to_string(),
));
}
let nonce_bytes: [u8; 12] = ciphertext[..12]
.try_into()
.map_err(|_| StorageError::InvalidFormat("Invalid nonce".to_string()))?;
let unbound_key = UnboundKey::new(&CHACHA20_POLY1305, key)
.map_err(|e| StorageError::CryptoError(format!("Failed to create key: {e}")))?;
struct SingleNonce(Option<[u8; 12]>);
impl NonceSequence for SingleNonce {
fn advance(&mut self) -> Result<Nonce, aws_lc_rs::error::Unspecified> {
self.0
.take()
.map(Nonce::assume_unique_for_key)
.ok_or(aws_lc_rs::error::Unspecified)
}
}
let mut opening_key = aead::OpeningKey::new(unbound_key, SingleNonce(Some(nonce_bytes)));
let mut in_out = ciphertext[12..].to_vec();
let plaintext = opening_key
.open_in_place(Aad::empty(), &mut in_out)
.map_err(|_| {
StorageError::CryptoError(
"Decryption failed - wrong password or corrupted data".to_string(),
)
})?;
if plaintext.len() != 32 {
return Err(StorageError::InvalidFormat(format!(
"Expected 32-byte HostKey, got {} bytes",
plaintext.len()
)));
}
let mut result = [0u8; 32];
result.copy_from_slice(plaintext);
Ok(result)
}
}
impl HostKeyStorage for EncryptedFileStorage {
fn store(&self, hostkey: &[u8; 32]) -> StorageResult<()> {
let password = Self::get_password()?;
let mut salt = [0u8; SALT_SIZE];
aws_lc_rs::rand::fill(&mut salt)
.map_err(|e| StorageError::CryptoError(format!("Failed to generate salt: {e}")))?;
let mut key = Self::derive_key_from_password(&password, &salt)?;
let ciphertext = Self::encrypt(&key, hostkey)?;
key.zeroize();
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut file_data = Vec::with_capacity(1 + SALT_SIZE + ciphertext.len());
file_data.push(FILE_FORMAT_VERSION);
file_data.extend_from_slice(&salt);
file_data.extend_from_slice(&ciphertext);
let temp_path = self.path.with_extension("tmp");
std::fs::write(&temp_path, &file_data)?;
std::fs::rename(&temp_path, &self.path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&self.path, permissions)?;
}
Ok(())
}
fn load(&self) -> StorageResult<[u8; 32]> {
if !self.path.exists() {
return Err(StorageError::NotFound);
}
let password = Self::get_password()?;
let file_data = std::fs::read(&self.path)?;
if file_data.is_empty() {
return Err(StorageError::InvalidFormat("Empty file".to_string()));
}
let version = file_data[0];
if version != FILE_FORMAT_VERSION {
return Err(StorageError::InvalidFormat(format!(
"Unsupported file format version: {version}"
)));
}
if file_data.len() < 1 + SALT_SIZE + 12 + 16 {
return Err(StorageError::InvalidFormat("File too short".to_string()));
}
let salt = &file_data[1..1 + SALT_SIZE];
let ciphertext = &file_data[1 + SALT_SIZE..];
let mut key = Self::derive_key_from_password(&password, salt)?;
let result = Self::decrypt(&key, ciphertext);
key.zeroize();
result
}
fn delete(&self) -> StorageResult<()> {
if self.path.exists() {
if let Ok(metadata) = std::fs::metadata(&self.path) {
let zeros = vec![0u8; metadata.len() as usize];
let _ = std::fs::write(&self.path, &zeros);
}
std::fs::remove_file(&self.path)?;
}
Ok(())
}
fn exists(&self) -> bool {
self.path.exists()
}
fn backend_name(&self) -> &'static str {
"EncryptedFile"
}
fn security_level(&self) -> StorageSecurityLevel {
StorageSecurityLevel::Encrypted
}
}
pub struct KeyringStorage {
service: &'static str,
username: &'static str,
}
impl KeyringStorage {
const SERVICE: &'static str = "ant-quic";
const USERNAME: &'static str = "hostkey";
pub fn new() -> StorageResult<Self> {
let _ = keyring::Entry::new(Self::SERVICE, Self::USERNAME)
.map_err(|e| StorageError::KeychainError(format!("Keyring unavailable: {e}")))?;
Ok(Self {
service: Self::SERVICE,
username: Self::USERNAME,
})
}
pub fn is_available() -> bool {
keyring::Entry::new(Self::SERVICE, Self::USERNAME).is_ok()
}
fn entry(&self) -> StorageResult<keyring::Entry> {
keyring::Entry::new(self.service, self.username)
.map_err(|e| StorageError::KeychainError(e.to_string()))
}
}
impl HostKeyStorage for KeyringStorage {
fn store(&self, hostkey: &[u8; 32]) -> StorageResult<()> {
let entry = self.entry()?;
let hex = hex::encode(hostkey);
entry
.set_password(&hex)
.map_err(|e| StorageError::KeychainError(e.to_string()))
}
fn load(&self) -> StorageResult<[u8; 32]> {
let entry = self.entry()?;
let hex = entry.get_password().map_err(|e| match e {
keyring::Error::NoEntry => StorageError::NotFound,
_ => StorageError::KeychainError(e.to_string()),
})?;
let bytes = hex::decode(&hex).map_err(|e| StorageError::InvalidFormat(e.to_string()))?;
if bytes.len() != 32 {
return Err(StorageError::InvalidFormat(format!(
"Expected 32 bytes, got {}",
bytes.len()
)));
}
let mut result = [0u8; 32];
result.copy_from_slice(&bytes);
Ok(result)
}
fn delete(&self) -> StorageResult<()> {
let entry = self.entry()?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(StorageError::KeychainError(e.to_string())),
}
}
fn exists(&self) -> bool {
self.entry()
.map(|e| e.get_password().is_ok())
.unwrap_or(false)
}
fn backend_name(&self) -> &'static str {
#[cfg(target_os = "macos")]
{
"macOS-Keychain"
}
#[cfg(target_os = "linux")]
{
"Linux-SecretService"
}
#[cfg(target_os = "windows")]
{
"Windows-CredentialManager"
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
"Keyring"
}
}
fn security_level(&self) -> StorageSecurityLevel {
StorageSecurityLevel::Secure
}
}
pub struct PlainFileStorage {
path: PathBuf,
}
impl PlainFileStorage {
pub fn new() -> StorageResult<Self> {
let path = Self::default_path()?;
Ok(Self { path })
}
pub fn with_path(path: PathBuf) -> Self {
Self { path }
}
fn default_path() -> StorageResult<PathBuf> {
let config_dir = dirs::config_dir().ok_or_else(|| {
StorageError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not determine config directory",
))
})?;
Ok(config_dir.join("ant-quic").join("hostkey.key"))
}
}
impl HostKeyStorage for PlainFileStorage {
fn store(&self, hostkey: &[u8; 32]) -> StorageResult<()> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
let temp_path = self.path.with_extension("tmp");
std::fs::write(&temp_path, hostkey)?;
std::fs::rename(&temp_path, &self.path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&self.path, permissions)?;
}
Ok(())
}
fn load(&self) -> StorageResult<[u8; 32]> {
if !self.path.exists() {
return Err(StorageError::NotFound);
}
let data = std::fs::read(&self.path)?;
if data.len() != 32 {
return Err(StorageError::InvalidFormat(format!(
"Expected 32 bytes, got {}",
data.len()
)));
}
let mut result = [0u8; 32];
result.copy_from_slice(&data);
Ok(result)
}
fn delete(&self) -> StorageResult<()> {
if self.path.exists() {
let _ = std::fs::write(&self.path, [0u8; 32]);
std::fs::remove_file(&self.path)?;
}
Ok(())
}
fn exists(&self) -> bool {
self.path.exists()
}
fn backend_name(&self) -> &'static str {
"PlainFile-INSECURE"
}
fn security_level(&self) -> StorageSecurityLevel {
StorageSecurityLevel::Insecure
}
}
pub struct StorageSelection {
pub storage: Box<dyn HostKeyStorage>,
pub security_level: StorageSecurityLevel,
}
pub fn auto_storage() -> StorageResult<StorageSelection> {
if KeyringStorage::is_available() {
if let Ok(storage) = KeyringStorage::new() {
let security_level = storage.security_level();
return Ok(StorageSelection {
storage: Box::new(storage),
security_level,
});
}
}
if std::env::var("ANTQ_HOSTKEY_PASSWORD").is_ok() {
let storage = EncryptedFileStorage::new()?;
return Ok(StorageSelection {
storage: Box::new(storage),
security_level: StorageSecurityLevel::Encrypted,
});
}
let storage = PlainFileStorage::new()?;
Ok(StorageSelection {
storage: Box::new(storage),
security_level: StorageSecurityLevel::Insecure,
})
}
#[deprecated(
since = "0.15.0",
note = "Use auto_storage() which returns StorageSelection"
)]
pub fn auto_storage_legacy() -> StorageResult<Box<dyn HostKeyStorage>> {
Ok(auto_storage()?.storage)
}
pub fn encrypted_file_storage() -> StorageResult<EncryptedFileStorage> {
EncryptedFileStorage::new()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use tempfile::TempDir;
static ENV_VAR_MUTEX: Mutex<()> = Mutex::new(());
fn with_password<T, F: FnOnce() -> T>(password: Option<&str>, f: F) -> T {
let _guard = ENV_VAR_MUTEX.lock().expect("ENV_VAR_MUTEX poisoned");
unsafe {
if let Some(pwd) = password {
std::env::set_var("ANTQ_HOSTKEY_PASSWORD", pwd);
} else {
std::env::remove_var("ANTQ_HOSTKEY_PASSWORD");
}
}
let result = f();
unsafe {
std::env::remove_var("ANTQ_HOSTKEY_PASSWORD");
}
result
}
#[test]
fn test_encrypted_file_storage_roundtrip() {
with_password(Some("test-password-12345"), || {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.enc");
let storage = EncryptedFileStorage::with_path(path);
let hostkey = [0xAB; 32];
storage.store(&hostkey).expect("Failed to store");
let loaded = storage.load().expect("Failed to load");
assert_eq!(loaded, hostkey);
});
}
#[test]
fn test_encrypted_file_storage_wrong_password() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.enc");
with_password(Some("correct-password"), || {
let storage = EncryptedFileStorage::with_path(path.clone());
let hostkey = [0xAB; 32];
storage.store(&hostkey).expect("Failed to store");
});
with_password(Some("wrong-password"), || {
let storage = EncryptedFileStorage::with_path(path.clone());
let result = storage.load();
assert!(result.is_err(), "Should fail with wrong password");
});
}
#[test]
fn test_encrypted_file_storage_missing_password() {
with_password(None, || {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.enc");
let storage = EncryptedFileStorage::with_path(path);
let hostkey = [0xCD; 32];
let result = storage.store(&hostkey);
assert!(matches!(result, Err(StorageError::PasswordRequired)));
});
}
#[test]
fn test_encrypted_file_storage_not_found() {
with_password(Some("test-password"), || {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("nonexistent.enc");
let storage = EncryptedFileStorage::with_path(path);
let result = storage.load();
assert!(matches!(result, Err(StorageError::NotFound)));
});
}
#[test]
fn test_encrypted_file_storage_delete() {
with_password(Some("test-password"), || {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.enc");
let storage = EncryptedFileStorage::with_path(path.clone());
let hostkey = [0xEF; 32];
storage.store(&hostkey).expect("Failed to store");
assert!(path.exists());
storage.delete().expect("Failed to delete");
assert!(!path.exists());
});
}
#[test]
fn test_key_derivation_deterministic() {
let password = "test-password";
let salt = [1u8; SALT_SIZE];
let key1 = EncryptedFileStorage::derive_key_from_password(password, &salt)
.expect("Key derivation failed");
let key2 = EncryptedFileStorage::derive_key_from_password(password, &salt)
.expect("Key derivation failed");
assert_eq!(key1, key2);
}
#[test]
fn test_different_salts_different_keys() {
let password = "test-password";
let salt1 = [1u8; SALT_SIZE];
let salt2 = [2u8; SALT_SIZE];
let key1 = EncryptedFileStorage::derive_key_from_password(password, &salt1)
.expect("Key derivation failed");
let key2 = EncryptedFileStorage::derive_key_from_password(password, &salt2)
.expect("Key derivation failed");
assert_ne!(key1, key2);
}
#[test]
fn test_encryption_roundtrip() {
let key = [0x42; 32];
let plaintext = [0xAB; 32];
let ciphertext =
EncryptedFileStorage::encrypt(&key, &plaintext).expect("Encryption failed");
assert!(ciphertext.len() > 32);
let decrypted =
EncryptedFileStorage::decrypt(&key, &ciphertext).expect("Decryption failed");
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_wrong_key_fails_decryption() {
let key1 = [0x42; 32];
let key2 = [0x43; 32];
let plaintext = [0xAB; 32];
let ciphertext =
EncryptedFileStorage::encrypt(&key1, &plaintext).expect("Encryption failed");
let result = EncryptedFileStorage::decrypt(&key2, &ciphertext);
assert!(result.is_err());
}
#[test]
fn test_plain_file_storage_roundtrip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.key");
let storage = PlainFileStorage::with_path(path);
let hostkey = [0xAB; 32];
storage.store(&hostkey).expect("Failed to store");
let loaded = storage.load().expect("Failed to load");
assert_eq!(loaded, hostkey);
}
#[test]
fn test_plain_file_storage_not_found() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("nonexistent.key");
let storage = PlainFileStorage::with_path(path);
let result = storage.load();
assert!(matches!(result, Err(StorageError::NotFound)));
}
#[test]
fn test_plain_file_storage_delete() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.key");
let storage = PlainFileStorage::with_path(path.clone());
let hostkey = [0xEF; 32];
storage.store(&hostkey).expect("Failed to store");
assert!(path.exists());
storage.delete().expect("Failed to delete");
assert!(!path.exists());
}
#[test]
fn test_plain_file_storage_exists() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.key");
let storage = PlainFileStorage::with_path(path);
assert!(!storage.exists());
let hostkey = [0xAB; 32];
storage.store(&hostkey).expect("Failed to store");
assert!(storage.exists());
}
#[test]
fn test_plain_file_storage_security_level() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.key");
let storage = PlainFileStorage::with_path(path);
assert_eq!(storage.security_level(), StorageSecurityLevel::Insecure);
assert!(storage.security_level().warning_message().is_some());
}
#[cfg(unix)]
#[test]
fn test_plain_file_storage_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.key");
let storage = PlainFileStorage::with_path(path.clone());
let hostkey = [0xAB; 32];
storage.store(&hostkey).expect("Failed to store");
let metadata = std::fs::metadata(&path).expect("Failed to get metadata");
let permissions = metadata.permissions();
assert_eq!(permissions.mode() & 0o777, 0o600);
}
#[test]
fn test_plain_file_storage_invalid_size() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let path = temp_dir.path().join("hostkey.key");
std::fs::write(&path, [0u8; 16]).expect("Failed to write");
let storage = PlainFileStorage::with_path(path);
let result = storage.load();
assert!(matches!(result, Err(StorageError::InvalidFormat(_))));
}
#[test]
#[ignore = "Requires system keyring daemon (run manually)"]
fn test_keyring_storage_roundtrip() {
if !KeyringStorage::is_available() {
println!("Keyring not available, skipping test");
return;
}
let storage = KeyringStorage::new().expect("Failed to create keyring storage");
let _ = storage.delete();
let hostkey = [0xAB; 32];
storage.store(&hostkey).expect("Failed to store");
let loaded = storage.load().expect("Failed to load");
assert_eq!(loaded, hostkey);
storage.delete().expect("Failed to delete");
}
#[test]
#[ignore = "Requires system keyring daemon (run manually)"]
fn test_keyring_storage_not_found() {
if !KeyringStorage::is_available() {
println!("Keyring not available, skipping test");
return;
}
let storage = KeyringStorage::new().expect("Failed to create keyring storage");
let _ = storage.delete();
let result = storage.load();
assert!(matches!(result, Err(StorageError::NotFound)));
}
#[test]
#[ignore = "Requires system keyring daemon (run manually)"]
fn test_keyring_storage_security_level() {
if !KeyringStorage::is_available() {
println!("Keyring not available, skipping test");
return;
}
let storage = KeyringStorage::new().expect("Failed to create keyring storage");
assert_eq!(storage.security_level(), StorageSecurityLevel::Secure);
assert!(storage.security_level().warning_message().is_none());
}
#[test]
fn test_security_level_warning_messages() {
assert!(StorageSecurityLevel::Secure.warning_message().is_none());
assert!(StorageSecurityLevel::Encrypted.warning_message().is_none());
assert!(StorageSecurityLevel::Insecure.warning_message().is_some());
}
#[test]
fn test_security_level_is_secure() {
assert!(StorageSecurityLevel::Secure.is_secure());
assert!(StorageSecurityLevel::Encrypted.is_secure());
assert!(!StorageSecurityLevel::Insecure.is_secure());
}
#[test]
fn test_auto_storage_fallback_to_plain_file() {
with_password(None, || {
let result = auto_storage();
assert!(result.is_ok());
let selection = result.expect("auto_storage should succeed");
assert!(
selection.security_level == StorageSecurityLevel::Secure
|| selection.security_level == StorageSecurityLevel::Insecure
);
});
}
#[test]
fn test_auto_storage_with_password() {
with_password(Some("test-password"), || {
let result = auto_storage();
assert!(result.is_ok());
let selection = result.expect("auto_storage should succeed");
assert!(
selection.security_level == StorageSecurityLevel::Secure
|| selection.security_level == StorageSecurityLevel::Encrypted
);
});
}
}