Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though

This commit is contained in:
Ryan Ward
2026-05-07 11:29:34 -05:00
parent f4e2475c60
commit a41cbd8cc7
2433 changed files with 14208 additions and 9409 deletions
@@ -0,0 +1,345 @@
//! Implementation of the [`SecureStorage`] service for the Linux platform.
use std::{cell::OnceCell, collections::HashMap, path::PathBuf};
use anyhow::{anyhow, Context};
use rand::RngCore;
use ring::aead;
use secret_service::{
blocking::{Item, SecretService},
EncryptionType,
};
use super::Error;
/// Implementation of the SecureStorage service using the Secret Service API.
pub struct SecureStorage {
/// The value to set for the "service" attribute, used to define a
/// namespace for keys for the application.
service_name: String,
/// A lazily-initialized reference to the default secret collection as
/// provided by the installed Secret Service API provider.
collection: OnceCell<Option<Collection>>,
/// The fallback path to a directory in case a secret collection is
/// not available.
fallback_dir: Option<PathBuf>,
/// The encryption fallback key.
encryption_key: OnceCell<Option<aead::LessSafeKey>>,
}
impl SecureStorage {
/// Creates a new [`SecureStorage`] instance.
///
/// This does not eagerly open a connection to dbus or the underlying
/// Secret Service provider.
pub fn new(service_name: &str) -> Self {
Self {
service_name: service_name.to_owned(),
collection: OnceCell::new(),
fallback_dir: None,
encryption_key: OnceCell::new(),
}
}
/// Creates a new [`SecureStorage`] instance with disk fallback
///
/// Does the same work as [`SecureStorage::new`], as well as storing
/// a path to a fallback directory.
pub fn new_with_fallback(service_name: &str, fallback_dir: PathBuf) -> Self {
Self {
service_name: service_name.to_owned(),
collection: OnceCell::new(),
fallback_dir: Some(fallback_dir),
encryption_key: OnceCell::new(),
}
}
/// Returns a reference to the default secret collection, lazily
/// instantiating the underlying service and collection reference,
/// returning an error if the connection cannot be established or the
/// collection cannot be opened.
///
/// TODO(vorporeal): Decide whether we want to "cache" a failed connection
/// or instead only store the collection on a successful connection and
/// return the error on an initialization failure.
fn collection(&self) -> Result<&secret_service::blocking::Collection<'_>, Error> {
self.collection
.get_or_init(|| match Collection::open_default_collection() {
Ok(collection) => Some(collection),
Err(err) => {
log::error!("Failed to acquire default Secret Service collection: {err:#}");
None
}
})
.as_ref()
.ok_or_else(|| {
Error::Unknown(anyhow!("Failed to initialize Secret Service connection"))
})
.and_then(|collection| {
let collection = collection.borrow_collection();
// Ensure that the collection is unlocked, otherwise we will be unable
// add or modify items.
collection.unlock()?;
Ok(collection)
})
}
/// Returns an encryption key that can be used to encrypt
/// values if the default secret collection is not available
/// or otherwise not working. The key is lazy initialized since
/// it does not need to be created unless the main method of
/// storing secrets has failed
fn encryption_key(&self) -> Result<&aead::LessSafeKey, Error> {
self.encryption_key
.get_or_init(|| {
// We can use whatever super duper foolproof secure key we want here.
// Here we are specifically choosing a value that will look inconspicuous
// in case someone chooses to scan our binary for strings.
let mut key_bytes = Vec::from("https://releases.warp.dev/channel_versions.json");
key_bytes.resize(aead::AES_256_GCM.key_len(), 0);
match aead::UnboundKey::new(&aead::AES_256_GCM, key_bytes.as_slice()) {
Ok(key) => Some(aead::LessSafeKey::new(key)),
Err(_) => {
log::error!("Failed to initialize fallback encryption key");
None
}
}
})
.as_ref()
.ok_or_else(|| Error::Unknown(anyhow!("Invalid encryption key")))
}
/// Returns the set of attributes which should be used when interacting
/// with a secret item that is identified by the given key.
fn attributes_for_key<'a>(&'a self, key: &'a str) -> HashMap<&'static str, &'a str> {
HashMap::from([
// Ensure our keys don't conflict with ones stored by another
// application.
("service", self.service_name.as_str()),
// Specify the key for the secret.
("key", key),
])
}
/// Provides the given function access to a secret item with the given key
/// in order to read or manipulate the item.
fn with_item<T>(
&self,
key: &str,
func: impl FnOnce(&Item) -> Result<T, Error>,
) -> Result<T, Error> {
let collection = self.collection()?;
let items = collection.search_items(self.attributes_for_key(key))?;
let Some(item) = items.first() else {
return Err(Error::NotFound);
};
func(item)
}
fn write_secret_value(&self, key: &str, value: &str) -> Result<(), Error> {
let collection = self.collection()?;
// Construct a slightly more human-readable label for the secret than
// using the key alone.
let label = format!("{}: {key}", self.service_name);
collection.create_item(
&label,
self.attributes_for_key(key),
value.as_bytes(),
// replace the existing key, if any
true,
"text/plain",
)?;
Ok(())
}
fn fallback_encrypt(&self, value: &str) -> Result<Vec<u8>, Error> {
let encryption_key = self.encryption_key()?;
// Generates nonce by randomly generating numbers
// This is not the official best way to do this, but it should
// be fine for our purposes.
let mut rng = rand::thread_rng();
let mut nonce_bytes = [0u8; aead::NONCE_LEN];
rng.fill_bytes(&mut nonce_bytes);
let nonce = aead::Nonce::assume_unique_for_key(nonce_bytes);
let mut data = value.as_bytes().to_vec();
encryption_key
.seal_in_place_append_tag(nonce, aead::Aad::empty(), &mut data)
.map_err(Into::<Error>::into)
.context("Fallback encryption failed")?;
// We serialize this to disk as the 12 byte nonce followed by the message.
let mut output = Vec::<u8>::with_capacity(aead::NONCE_LEN + data.len());
output.extend_from_slice(&nonce_bytes);
output.append(&mut data);
Ok(output)
}
fn fallback_decrypt(&self, value: &[u8]) -> Result<String, Error> {
if value.len() < aead::NONCE_LEN + 1 {
return Err(Error::Unknown(anyhow!(
"Attempting to decrypt too small value for fallback decryption"
)));
}
let encryption_key = self.encryption_key()?;
// The first 12 bytes of the message are the nonce.
let nonce_bytes = &value[0..aead::NONCE_LEN];
let nonce = aead::Nonce::try_assume_unique_for_key(nonce_bytes)
.map_err(Into::<Error>::into)
.context("Failed to parse nonce for fallback decryption")?;
// The remaining bytes in the message are the data.
// We convert this to owned b/c the decryption happens in place.
let mut data_bytes = value[aead::NONCE_LEN..].to_owned();
let decrypted_length = encryption_key
.open_in_place(nonce, aead::Aad::empty(), &mut data_bytes)
.map_err(Into::<Error>::into)
.context("Fallback decryption failed")?
.len();
// The decryption happens in place, but does not resize the vec.
// Meanwhile, a slice referring to the decrypted data is returned.
// We use the length of that slice to resize the currently owned Vec,
// so it can be consumed by String::from_utf8 later on.
data_bytes.resize(decrypted_length, 0);
String::from_utf8(data_bytes).map_err(|err| Error::DecodeError(err.utf8_error()))
}
fn fallback_file(&self, key: &str) -> Result<PathBuf, Error> {
let Some(dir) = &self.fallback_dir else {
return Err(Error::NotFound);
};
let filename = format!("{}-{key}", self.service_name);
let mut path = dir.clone();
path.push(filename);
Ok(path)
}
fn write_fallback_value(&self, key: &str, value: &str) -> Result<(), Error> {
let fallback_file = self.fallback_file(key)?;
let encrypted = self.fallback_encrypt(value)?;
std::fs::write(fallback_file, encrypted).map_err(|err| Error::Unknown(err.into()))
}
fn read_fallback_value(&self, key: &str) -> Result<String, Error> {
let fallback_file = self.fallback_file(key)?;
let data = std::fs::read(fallback_file).map_err(|_| Error::NotFound)?;
self.fallback_decrypt(&data)
}
fn delete_fallback_value(&self, key: &str) -> Result<(), Error> {
let fallback_file = self.fallback_file(key)?;
std::fs::remove_file(fallback_file).map_err(|err| match err {
ref io_error if io_error.kind() == std::io::ErrorKind::NotFound => Error::NotFound,
io_error => Error::Unknown(io_error.into()),
})
}
}
impl super::SecureStorage for SecureStorage {
fn write_value(&self, key: &str, value: &str) -> Result<(), Error> {
let secret_result = self.write_secret_value(key, value);
match secret_result {
Ok(_) => {
// If we are able to write the secret value, we attempt to delete any fallback values
let _ = self.delete_fallback_value(key);
Ok(())
}
Err(_) => self.write_fallback_value(key, value),
}
}
fn read_value(&self, key: &str) -> Result<String, Error> {
let secret_result = self.with_item(key, |item| {
let bytes = item.get_secret()?;
String::from_utf8(bytes).map_err(|err| Error::DecodeError(err.utf8_error()))
});
match secret_result {
Ok(value) => {
// If we are able to read the secret value, we attempt to delete any fallback values
let _ = self.delete_fallback_value(key);
Ok(value)
}
// TODO(daprahamian): We might want to filter on specific error values, rather than all errors
Err(_) => self.read_fallback_value(key),
}
}
fn remove_value(&self, key: &str) -> Result<(), Error> {
let secret_result = self.with_item(key, |item| item.delete().map_err(Into::into));
let fs_result = self.delete_fallback_value(key);
// We delete both the value in the secret store and the fallback values.
// As long as one succeeds, we consider the delete a success.
match (secret_result, fs_result) {
(Err(secret_err), Err(_)) => Err(secret_err),
_ => Ok(()),
}
}
}
impl From<secret_service::Error> for Error {
fn from(value: secret_service::Error) -> Self {
// TODO(vorporeal): Check to see if we can return any more specific
// values.
Error::Unknown(anyhow!(value))
}
}
impl From<ring::error::Unspecified> for Error {
fn from(value: ring::error::Unspecified) -> Self {
Error::Unknown(anyhow!(value))
}
}
/// A helper structure that maintains access to the default collection.
///
/// [`secret_service::SecretService`] is a self-referential struct that leaks
/// its internal reference lifetime, which is why we use [`ouroboros`] here to
/// provide a safe API for interacting with the service and collection.
#[ouroboros::self_referencing]
struct Collection {
/// An encrypted dbus connection to the Secret Service API provider.
#[borrows()]
#[covariant]
service: SecretService<'this>,
/// A reference to the default secret collection, which can be used to
/// add, remove and read secrets.
#[borrows(service)]
#[covariant]
collection: secret_service::blocking::Collection<'this>,
}
impl Collection {
/// Tries to open the default secret collection via the Secret Service
/// API.
fn open_default_collection() -> Result<Self, Error> {
SecretService::connect(EncryptionType::Plain)
.and_then(|service| {
CollectionTryBuilder {
service,
collection_builder: |service| service.get_default_collection(),
}
.try_build()
})
.map_err(Into::into)
}
}
#[cfg(test)]
#[path = "linux_test.rs"]
mod tests;
@@ -0,0 +1,46 @@
use super::Error;
use super::SecureStorage;
#[test]
fn test_encrypt_decrypt_returns_same_value() {
let storage = SecureStorage::new("darmok");
let input = String::from("darmok and jalad at tanagra");
let encrypted = storage.fallback_encrypt(&input).unwrap();
let output = storage.fallback_decrypt(&encrypted).unwrap();
assert_eq!(input, output)
}
#[test]
fn test_encrypt_decrypt_works_across_storage_instances() {
let storage_1 = SecureStorage::new("darmok");
let storage_2 = SecureStorage::new("jalad");
let input = String::from("shaka when the walls fell");
let encrypted = storage_1.fallback_encrypt(&input).unwrap();
let output = storage_2.fallback_decrypt(&encrypted).unwrap();
assert_eq!(input, output)
}
#[test]
fn test_decrypt_fails_on_malformed_data() {
let storage = SecureStorage::new("darmok");
let bad_datas: [&[u8]; 4] = [&[], &[0; 1], &[0; 11], &[0; 12]];
for bad_data in bad_datas {
let result = storage.fallback_decrypt(bad_data);
assert!(result.is_err());
let error = result.unwrap_err();
let Error::Unknown(err) = error else {
panic!("Expected error variant to be Error::Unknown, but found {error:?}")
};
assert_eq!(
format!("{err}"),
"Attempting to decrypt too small value for fallback decryption"
);
}
}
@@ -0,0 +1,62 @@
//! Implementations of the [`SecureStorage`] service for the macOS platform.
use anyhow::anyhow;
use security_framework::os::macos::{
keychain::SecKeychain, keychain_item::SecKeychainItem, passwords::SecKeychainItemPassword,
};
use super::Error;
/// Implementation of the SecureStorage service using macOS Security
/// framework keychains.
pub struct SecureStorage {
/// The name of the service under which to store the values.
service_name: String,
}
impl SecureStorage {
pub fn new(service_name: &str) -> Self {
Self {
service_name: service_name.to_owned(),
}
}
}
impl super::SecureStorage for SecureStorage {
fn write_value(&self, key: &str, value: &str) -> Result<(), Error> {
let keychain = SecKeychain::default()?;
keychain
.set_generic_password(self.service_name.as_str(), key, value.as_bytes())
.map_err(Into::into)
}
fn read_value(&self, key: &str) -> Result<String, Error> {
let (password, _) = self.get_password_item(key)?;
String::from_utf8(password.as_ref().to_vec())
.map_err(|err| Error::DecodeError(err.utf8_error()))
}
fn remove_value(&self, key: &str) -> Result<(), Error> {
let (_, item) = self.get_password_item(key)?;
item.delete();
Ok(())
}
}
impl SecureStorage {
fn get_password_item(
&self,
key: &str,
) -> Result<(SecKeychainItemPassword, SecKeychainItem), Error> {
let keychain = SecKeychain::default()?;
keychain
.find_generic_password(&self.service_name, key)
.map_err(|_| Error::NotFound)
}
}
impl From<security_framework::base::Error> for Error {
fn from(value: security_framework::base::Error) -> Self {
Error::Unknown(anyhow!(value))
}
}
@@ -0,0 +1,184 @@
//! Secure storage for passwords and other application secrets.
//!
//! This defines an API for interacting with an underlying secure storage
//! system, implementations of the API for various platforms, testing
//! utilities, and extension traits to improve ergonomics of using the APIs.
#[cfg(not(target_family = "wasm"))]
#[cfg_attr(target_os = "macos", path = "mac.rs")]
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
mod imp;
mod noop;
// Treat this as a noop on web, as there is no backing storage which is "secure".
#[cfg(target_family = "wasm")]
use noop as imp;
#[cfg(target_os = "windows")]
mod windows_only {
pub(super) use std::string::FromUtf8Error;
}
#[cfg(target_os = "windows")]
use windows_only::*;
/// A type alias for the concrete type stored within a galaxyui
/// app context, enabling usage such as:
///
/// ```
/// use galaxyui::{App, SingletonEntity};
/// use galaxyui_extras::secure_storage;
///
/// App::test((), |mut app| async move {
/// app.update(|ctx| {
/// #[cfg(not(windows))]
/// secure_storage::register("service_name", ctx);
/// #[cfg(windows)]
/// secure_storage::register_with_dir("service_name", std::path::PathBuf::from(r"C:\some\path"), ctx);
///
/// let _ = secure_storage::Model::handle(ctx).as_ref(ctx).read_value("some_key");
/// });
/// });
/// ```
/// Note that the above rustdoc example is `ignore`d in compilation
/// due to API differences across platforms.
pub type Model = Box<dyn SecureStorage>;
/// Registers a platform-native Secure Storage provider with the application.
///
/// The service name is used as a namespace for the application's secrets. It
/// is recommended that this be a unique identifier for the application; one
/// common scheme is reverse-DNS notation (e.g.: "dev.warp.Warp").
#[cfg(not(target_os = "windows"))]
pub fn register(service_name: &str, ctx: &mut galaxyui::AppContext) {
ctx.add_singleton_model(|_| -> Model { Box::new(imp::SecureStorage::new(service_name)) });
}
/// Registers a no-op Secure Storage provider with the application.
pub fn register_noop(service_name: &str, ctx: &mut galaxyui::AppContext) {
ctx.add_singleton_model(|_| -> Model { Box::new(noop::SecureStorage::new(service_name)) });
}
#[cfg(target_os = "linux")]
pub fn register_with_fallback(
service_name: &str,
fallback_dir: std::path::PathBuf,
ctx: &mut galaxyui::AppContext,
) {
ctx.add_singleton_model(|_| -> Model {
Box::new(imp::SecureStorage::new_with_fallback(
service_name,
fallback_dir,
))
});
}
/// Registers a Windows-native Secure Storage provider
/// that uses the provided directory to store data in encrypted files.
#[cfg(target_os = "windows")]
pub fn register_with_dir(
service_name: &str,
storage_dir: std::path::PathBuf,
ctx: &mut galaxyui::AppContext,
) {
ctx.add_singleton_model(|_| -> Model {
Box::new(imp::SecureStorage::new_with_path(service_name, storage_dir))
});
}
/// A trait representing a secure store for key-value pairs.
///
/// This is typically backed by an OS-provided secure storage system.
pub trait SecureStorage {
/// Writes a value at the given key.
fn write_value(&self, key: &str, value: &str) -> Result<(), Error>;
/// Reads the value stored at the given key.
fn read_value(&self, key: &str) -> Result<String, Error>;
/// Removes the value stored at the given key, if any.
fn remove_value(&self, key: &str) -> Result<(), Error>;
}
impl galaxyui::Entity for Model {
type Event = ();
}
impl galaxyui::SingletonEntity for Model {}
/// Enumerates the various errors that can occur when interacting with secure
/// storage.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The item with the given key was not found in secure storage.
///
/// This is not guaranteed to be returned in all cases where the item is
/// not found; if we are not able to interpret the error returned by the
/// underlying implementation, [`SecureStorageError::Unknown`] may be
/// returned.
#[error("item not found")]
NotFound,
/// Failed to decode the stored bytes into a UTF-8 string.
#[error("failed to decode UTF-8 string from bytes")]
DecodeError(#[from] std::str::Utf8Error),
/// Encountered an error when reading to or from a file.
#[cfg(windows)]
#[error("File I/O error")]
IOError(#[from] std::io::Error),
/// An error was encountered while using the windows CryptProtect API.
#[cfg(windows)]
#[error("Windows CryptProtect API error")]
WindowsAPIError(#[from] windows::core::Error),
/// The provided secure storage directory path was not valid.
#[cfg(windows)]
#[error("Invalid secure storage location")]
InvalidLocation,
/// Catch-all for unclassifiable errors.
#[error("unknown error")]
Unknown(#[from] anyhow::Error),
}
#[cfg(windows)]
impl From<FromUtf8Error> for Error {
fn from(value: FromUtf8Error) -> Self {
Self::DecodeError(value.utf8_error())
}
}
/// An extension trait to make secure storage easier to use.
///
/// ```
/// use galaxyui::{App, SingletonEntity};
/// use galaxyui_extras::secure_storage;
///
/// App::test((), |mut app| async move {
/// app.update(|ctx| {
/// #[cfg(not(windows))]
/// secure_storage::register("service_name", ctx);
/// #[cfg(windows)]
/// secure_storage::register_with_dir("service_name", std::path::PathBuf::from(r"C:\some\path"), ctx);
///
/// use secure_storage::AppContextExt;
/// let _ = ctx.secure_storage().read_value("some_key");
/// });
/// });
/// ```
/// Note that the above rustdoc example is `ignore`d in compilation
/// due to API differences across platforms.
pub trait AppContextExt {
fn secure_storage(&self) -> &dyn SecureStorage;
}
impl AppContextExt for galaxyui::AppContext {
fn secure_storage(&self) -> &dyn SecureStorage {
use galaxyui::SingletonEntity;
<Model as SingletonEntity>::as_ref(self).as_ref()
}
}
@@ -0,0 +1,26 @@
//! No-op [`SecureStorage`] service for use in unit and integration tests.
use super::Error;
#[derive(Default)]
pub struct SecureStorage {}
impl SecureStorage {
pub fn new(_service_name: &str) -> Self {
Self {}
}
}
impl super::SecureStorage for SecureStorage {
fn write_value(&self, _key: &str, _value: &str) -> Result<(), Error> {
Ok(())
}
fn read_value(&self, _key: &str) -> Result<String, Error> {
Ok("".to_string())
}
fn remove_value(&self, _key: &str) -> Result<(), Error> {
Ok(())
}
}
@@ -0,0 +1,109 @@
use std::path::PathBuf;
use windows::{
core::BSTR,
Win32::{
Foundation::{LocalFree, HLOCAL},
Security::Cryptography::{CryptProtectData, CryptUnprotectData, CRYPT_INTEGER_BLOB},
},
};
use super::Error;
#[derive(Default)]
pub struct SecureStorage {
service_name: String,
storage_dir: PathBuf,
}
impl SecureStorage {
pub fn new_with_path(service_name: &str, storage_dir: PathBuf) -> Self {
Self {
service_name: service_name.to_string(),
storage_dir,
}
}
fn storage_file(&self, key: &str) -> PathBuf {
let filename = format!("{}-{key}", self.service_name);
self.storage_dir.join(filename)
}
fn byte_vec_to_blob(byte_vec: &mut Vec<u8>) -> CRYPT_INTEGER_BLOB {
let byte_slice = byte_vec.as_mut_slice();
CRYPT_INTEGER_BLOB {
cbData: byte_slice.len() as u32,
pbData: byte_slice.as_mut_ptr(),
}
}
fn encrypt(key: &str, mut plaintext: String) -> Result<Vec<u8>, Error> {
let mut encrypted_blob = CRYPT_INTEGER_BLOB::default();
let encrypted_bytes = unsafe {
let plaintext_bytes = plaintext.as_bytes_mut();
let plaintext_blob = CRYPT_INTEGER_BLOB {
cbData: plaintext_bytes.len() as u32,
pbData: plaintext_bytes.as_mut_ptr(),
};
CryptProtectData(
&plaintext_blob,
&BSTR::from(key),
None,
None,
None,
0,
&mut encrypted_blob,
)?;
let encrypted_bytes =
std::slice::from_raw_parts(encrypted_blob.pbData, encrypted_blob.cbData as usize)
.to_vec();
LocalFree(Some(HLOCAL(encrypted_blob.pbData.cast())));
encrypted_bytes
};
Ok(encrypted_bytes)
}
fn decrypt(mut encrypted_bytes: Vec<u8>) -> Result<String, Error> {
let encrypted_blob = Self::byte_vec_to_blob(&mut encrypted_bytes);
let mut decrypted_blob = CRYPT_INTEGER_BLOB::default();
let decrypted_bytes = unsafe {
CryptUnprotectData(
&encrypted_blob,
None,
None,
None,
None,
0,
&mut decrypted_blob,
)?;
let byte_vec =
std::slice::from_raw_parts(decrypted_blob.pbData, decrypted_blob.cbData as usize)
.to_vec();
LocalFree(Some(HLOCAL(decrypted_blob.pbData.cast())));
byte_vec
};
Ok(String::from_utf8(decrypted_bytes)?)
}
}
impl super::SecureStorage for SecureStorage {
fn write_value(&self, key: &str, value: &str) -> Result<(), Error> {
let storage_file = self.storage_file(key);
let encrypted_bytes = Self::encrypt(key, value.to_string())?;
std::fs::write(storage_file, encrypted_bytes).map_err(Error::from)
}
fn read_value(&self, key: &str) -> Result<String, Error> {
let storage_file = self.storage_file(key);
let file_bytes = std::fs::read(storage_file)?;
Self::decrypt(file_bytes)
}
fn remove_value(&self, key: &str) -> Result<(), Error> {
let storage_file = self.storage_file(key);
std::fs::remove_file(storage_file).map_err(Error::from)
}
}
#[cfg(test)]
#[path = "windows_test.rs"]
mod test;
@@ -0,0 +1,30 @@
use super::SecureStorage;
#[test]
fn test_encrypt_decrypt_returns_same_value() {
let key = String::from("key");
let inputs = [
"freckles grain uncaring strict stumbling reappear basil uproar",
"ideology shifting overview cognition uniformed armory mummify editor",
"",
"{",
"\'",
"\"",
"{\"test\"}",
"defender french skating sweat neurotic extruding cadet mute headcount unaligned prognosis heroics geography deafening customer juicy scuttle blissful scrambler spleen embark engine shield banter botanist singing plutonium grafted carton playable approve astonish",
"{\"id_token\":{\"id_token\":\"This is an ID token.\",\"refresh_token\":\"This is a refresh token.\",\"expiration_time\":\"2025-10-22T15:26:51.091844800-04:00\"},\"refresh_token\":\"\",\"local_id\":\"test_user_uid\",\"email\":\"test_user@warp.dev\",\"display_name\":\"abcdef\",\"photo_url\":\"some-photo-url=\",\"is_onboarded\":true,\"needs_sso_link\":false,\"anonymous_user_type\":null,\"expires_at\":null,\"linked_at\":null,\"is_guaranteed_expired\":false,\"is_on_work_domain\":false}",
].map(String::from);
fn encrypt_then_decrypt(key: &str, input: String) -> String {
let encrypted = SecureStorage::encrypt(key, input).unwrap();
SecureStorage::decrypt(encrypted).unwrap()
}
for input in inputs {
assert_eq!(
encrypt_then_decrypt(&key, input.to_owned()),
input,
"Encrypting and decrypting {input:?}"
);
}
}