notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 2d939c6b07da10043d1e9e08bd511d90fd7e4690
parent fd7238bc4c072a8e023a0433c43e94a56a9211df
Author: William Casarin <jb55@jb55.com>
Date:   Thu,  4 Dec 2025 01:02:13 -0800

Merge keyring by kernel #1191

kernelkind (9):
      refactor: appease clippy
      cargo: add `keyring`
      feat(error): add `Error::Keyring`
      feat(keyring): add keyring helper
      cargo: add `android-keyring`
      feat(android-keyring): use correct android keyring builder
      feat(key-store): upgrade key storage to use OS secure store
      test: secure store key mgmt
      fix: don't exit early in account retrieval

Diffstat:
MCargo.lock | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
MCargo.toml | 2++
Mcrates/notedeck/Cargo.toml | 1+
Mcrates/notedeck/src/error.rs | 3+++
Mcrates/notedeck/src/storage/account_storage.rs | 170++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Acrates/notedeck/src/storage/keyring_store.rs | 192+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck/src/storage/mod.rs | 2++
Mcrates/notedeck/src/user_account.rs | 1+
Mcrates/notedeck_chrome/Cargo.toml | 1+
Mcrates/notedeck_chrome/src/android.rs | 2++
Mcrates/notedeck_columns/src/ui/profile/mod.rs | 2+-
Mcrates/notedeck_columns/src/ui/search/mod.rs | 2+-
12 files changed, 518 insertions(+), 24 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -144,6 +144,20 @@ dependencies = [ ] [[package]] +name = "android-keyring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b051e1fab4f4c15e384424252c57321173b8fb274d50f30bd46145c35cd0a6a2" +dependencies = [ + "base64 0.22.1", + "jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)", + "keyring", + "ndk-context", + "thiserror 2.0.12", + "tracing", +] + +[[package]] name = "android-properties" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1173,7 +1187,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "libc", ] @@ -1333,6 +1347,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" [[package]] +name = "dbus" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "openssl", + "zeroize", +] + +[[package]] name = "deranged" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2052,12 +2088,21 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared", + "foreign-types-shared 0.3.1", ] [[package]] @@ -2073,6 +2118,12 @@ dependencies = [ [[package]] name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" @@ -3082,6 +3133,23 @@ dependencies = [ ] [[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "linux-keyutils", + "log", + "openssl", + "security-framework 2.11.1", + "security-framework 3.2.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] name = "khronos-egl" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3132,6 +3200,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] +name = "libdbus-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] name = "libfuzzer-sys" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3184,6 +3262,16 @@ dependencies = [ ] [[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + +[[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3359,7 +3447,7 @@ dependencies = [ "bitflags 2.9.1", "block", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", "log", "objc", "paste", @@ -3680,6 +3768,7 @@ dependencies = [ "image", "indexmap 2.9.0", "jni 0.21.1 (registry+https://github.com/rust-lang/crates.io-index)", + "keyring", "lightning-invoice", "md5", "mime_guess", @@ -3715,6 +3804,7 @@ dependencies = [ name = "notedeck_chrome" version = "0.7.1" dependencies = [ + "android-keyring", "bitflags 2.9.1", "eframe", "egui", @@ -4305,12 +4395,60 @@ dependencies = [ ] [[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] +name = "openssl-src" +version = "300.5.4+3.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6706,6 +6844,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] name = "vec1" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -8009,6 +8153,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml @@ -95,6 +95,8 @@ re_memory = "0.23.4" oot_bitset = "0.1.1" blurhash = "0.2.3" android-activity = { git = "https://github.com/damus-io/android-activity", rev = "4ee16f1585e4a75031dc10785163d4b920f95805", features = [ "game-activity" ] } +keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "linux-native-sync-persistent", "vendored"] } +android-keyring = "0.2.0" [profile.small] inherits = 'release' diff --git a/crates/notedeck/Cargo.toml b/crates/notedeck/Cargo.toml @@ -59,6 +59,7 @@ hyper-util = { workspace = true } http-body-util = { workspace = true } hyper-rustls = { workspace = true } rustls = { workspace = true } +keyring = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/notedeck/src/error.rs b/crates/notedeck/src/error.rs @@ -21,6 +21,9 @@ pub enum Error { #[error("io error: {0}")] Nostrdb(#[from] nostrdb::Error), + #[error("keyring error: {0}")] + Keyring(#[from] keyring::Error), + #[error("generic error: {0}")] Generic(String), diff --git a/crates/notedeck/src/storage/account_storage.rs b/crates/notedeck/src/storage/account_storage.rs @@ -2,22 +2,39 @@ use crate::{user_account::UserAccountSerializable, Result}; use enostr::{Keypair, Pubkey, SerializableKeypair}; use tokenator::{TokenParser, TokenSerializable, TokenWriter}; -use super::file_storage::{delete_file, write_file, Directory}; +use super::{ + file_storage::{delete_file, write_file, Directory}, + keyring_store::KeyringStore, +}; static SELECTED_PUBKEY_FILE_NAME: &str = "selected_pubkey"; -/// An OS agnostic file key storage implementation -#[derive(Debug, PartialEq, Clone)] +/// An OS agnostic key storage implementation backed by the operating system's secure store. +#[derive(Debug, Clone)] pub struct AccountStorage { accounts_directory: Directory, selected_key_directory: Directory, + keyring: KeyringStore, } impl AccountStorage { pub fn new(accounts_directory: Directory, selected_key_directory: Directory) -> Self { + Self::with_keyring( + accounts_directory, + selected_key_directory, + KeyringStore::default(), + ) + } + + pub(crate) fn with_keyring( + accounts_directory: Directory, + selected_key_directory: Directory, + keyring: KeyringStore, + ) -> Self { Self { accounts_directory, selected_key_directory, + keyring, } } @@ -27,6 +44,29 @@ impl AccountStorage { AccountStorageWriter::new(self), ) } + + fn persist_account(&self, account: &UserAccountSerializable) -> Result<()> { + if let Some(secret) = account.key.secret_key.as_ref() { + self.keyring.store_secret(&account.key.pubkey, secret)?; + self.write_account_without_secret(account)?; + } else { + // if the account is npub only, make sure the db doesn't somehow have the nsec + self.keyring.remove_secret(&account.key.pubkey)?; + } + + Ok(()) + } + + fn write_account_without_secret(&self, account: &UserAccountSerializable) -> Result<()> { + let mut writer = TokenWriter::new("\t"); + sanitized_account(account).serialize_tokens(&mut writer); + + write_file( + &self.accounts_directory.file_path, + account.key.pubkey.hex(), + writer.str(), + ) + } } pub struct AccountStorageWriter { @@ -39,17 +79,13 @@ impl AccountStorageWriter { } pub fn write_account(&self, account: &UserAccountSerializable) -> Result<()> { - let mut writer = TokenWriter::new("\t"); - account.serialize_tokens(&mut writer); - write_file( - &self.storage.accounts_directory.file_path, - account.key.pubkey.hex(), - writer.str(), - ) + self.storage.persist_account(account) } pub fn remove_key(&self, key: &Keypair) -> Result<()> { - delete_file(&self.storage.accounts_directory.file_path, key.pubkey.hex()) + delete_file(&self.storage.accounts_directory.file_path, key.pubkey.hex())?; + self.storage.keyring.remove_secret(&key.pubkey)?; + Ok(()) } pub fn select_key(&self, pubkey: Option<Pubkey>) -> Result<()> { @@ -86,14 +122,46 @@ impl AccountStorageReader { } pub fn get_accounts(&self) -> Result<Vec<UserAccountSerializable>> { - let keys = self + let accounts = self .storage .accounts_directory .get_files()? .values() - .filter_map(|serialized| deserialize_storage(serialized).ok()) - .collect(); - Ok(keys) + .filter_map(|serialized| match deserialize_storage(serialized) { + Ok(account) => Some(account), + Err(err) => { + tracing::error!("failed to deserialize stored account: {err}"); + None + } + }) + // sanitize our storage of secrets & inject the secret from `keyring` into `UserAccountSerializable` + .map(|mut account| -> Result<UserAccountSerializable> { + if let Some(secret) = &account.key.secret_key { + match self + .storage + .keyring + .store_secret(&account.key.pubkey, secret) + { + Ok(_) => { + if let Err(e) = self.storage.write_account_without_secret(&account) { + tracing::error!( + "failed to write account {:?} without secret: {e}", + account.key.pubkey + ); + } + } + Err(e) => tracing::error!("failed to store secret in OS secure store: {e}"), + } + } else if let Ok(Some(secret)) = + self.storage.keyring.get_secret(&account.key.pubkey) + { + account.key.secret_key = Some(secret); + } + Ok(account) + }) + .collect::<Result<Vec<_>>>()?; + + Ok(accounts) } pub fn get_selected_key(&self) -> Result<Option<Pubkey>> { @@ -127,10 +195,18 @@ fn old_deserialization(serialized: &str) -> Result<Keypair> { Ok(serde_json::from_str::<SerializableKeypair>(serialized)?.to_keypair("")) } +fn sanitized_account(account: &UserAccountSerializable) -> UserAccountSerializable { + let mut sanitized = account.clone(); + sanitized.key.secret_key = None; + sanitized +} + #[cfg(test)] mod tests { use std::path::PathBuf; + use crate::storage::KeyringStore; + use super::Result; use super::*; @@ -139,10 +215,11 @@ mod tests { impl AccountStorage { fn mock() -> Result<Self> { - Ok(Self { - accounts_directory: Directory::new(CREATE_TMP_DIR()?), - selected_key_directory: Directory::new(CREATE_TMP_DIR()?), - }) + Ok(Self::with_keyring( + Directory::new(CREATE_TMP_DIR()?), + Directory::new(CREATE_TMP_DIR()?), + KeyringStore::in_memory(), + )) } } @@ -159,6 +236,61 @@ mod tests { assert_num_storage(&reader.get_accounts(), 0); } + #[test] + fn test_secret_persisted_in_keyring_not_on_disk() { + let kp = enostr::FullKeypair::generate().to_keypair(); + let (reader, writer) = AccountStorage::mock().unwrap().rw(); + + writer + .write_account(&UserAccountSerializable::new(kp.clone())) + .unwrap(); + + let files = reader + .storage + .accounts_directory + .get_files() + .expect("files"); + + let stored = files + .get(&kp.pubkey.hex()) + .expect("account file should exist"); + + let secret_hex = { + let secret = kp.secret_key.as_ref().expect("secret key"); + hex::encode(secret.to_secret_bytes()) + }; + assert!( + !stored.contains(&secret_hex), + "secret key unexpectedly persisted to disk" + ); + + let accounts = reader.get_accounts().expect("accounts"); + assert_eq!(accounts.len(), 1); + assert!(accounts[0].key.secret_key.is_some()); + } + + #[test] + fn test_remove_key_removes_secret() { + let kp = enostr::FullKeypair::generate().to_keypair(); + let (reader, writer) = AccountStorage::mock().unwrap().rw(); + + writer + .write_account(&UserAccountSerializable::new(kp.clone())) + .expect("write account"); + + writer.remove_key(&kp).expect("remove key"); + + assert!( + reader + .storage + .keyring + .get_secret(&kp.pubkey) + .expect("keyring read") + .is_none(), + "secret key should be removed from keyring" + ); + } + fn assert_num_storage(keys_response: &Result<Vec<UserAccountSerializable>>, n: usize) { match keys_response { Ok(keys) => { diff --git a/crates/notedeck/src/storage/keyring_store.rs b/crates/notedeck/src/storage/keyring_store.rs @@ -0,0 +1,192 @@ +use enostr::{Pubkey, SecretKey}; +use keyring::Entry; + +use crate::{Error, Result}; + +const KEYRING_SERVICE_NAME: &str = "com.damus.notedeck"; + +type BackendResult<T> = std::result::Result<T, keyring::Error>; + +#[derive(Clone, Debug)] +enum KeyringBackendType { + OS(OsKeyringBackend), + #[cfg(test)] + Memory(MemoryKeyringBackend), +} + +impl KeyringBackendType { + pub fn set(&self, service: &str, account: &str, secret: &str) -> BackendResult<()> { + match self { + KeyringBackendType::OS(os_keyring_backend) => { + os_keyring_backend.set(service, account, secret) + } + #[cfg(test)] + KeyringBackendType::Memory(mem) => mem.set(service, account, secret), + } + } + + pub fn get(&self, service: &str, account: &str) -> BackendResult<Option<String>> { + match self { + KeyringBackendType::OS(os_keyring_backend) => os_keyring_backend.get(service, account), + #[cfg(test)] + KeyringBackendType::Memory(memory_keyring_backend) => { + memory_keyring_backend.get(service, account) + } + } + } + + pub fn delete(&self, service: &str, account: &str) -> BackendResult<()> { + match self { + KeyringBackendType::OS(os_keyring_backend) => { + os_keyring_backend.delete(service, account) + } + #[cfg(test)] + KeyringBackendType::Memory(memory_keyring_backend) => { + memory_keyring_backend.delete(service, account) + } + } + } +} + +#[derive(Clone, Debug)] +struct OsKeyringBackend; + +impl OsKeyringBackend { + fn set(&self, service: &str, account: &str, secret: &str) -> BackendResult<()> { + let entry = Entry::new(service, account)?; + entry.set_password(secret) + } + + fn get(&self, service: &str, account: &str) -> BackendResult<Option<String>> { + let entry = Entry::new(service, account)?; + + match entry.get_password() { + Ok(secret) => Ok(Some(secret)), + Err(keyring::Error::NoEntry) => Ok(None), + Err(err) => Err(err), + } + } + + fn delete(&self, service: &str, account: &str) -> BackendResult<()> { + let entry = Entry::new(service, account)?; + + match entry.delete_credential() { + Ok(_) => Ok(()), + Err(keyring::Error::NoEntry) => Ok(()), + Err(err) => Err(err), + } + } +} + +#[derive(Clone, Debug)] +pub struct KeyringStore { + backend: KeyringBackendType, +} + +impl KeyringStore { + #[cfg(test)] + pub fn in_memory() -> Self { + Self { + backend: KeyringBackendType::Memory(MemoryKeyringBackend::default()), + } + } + + pub fn store_secret(&self, pubkey: &Pubkey, secret: &SecretKey) -> Result<()> { + let res = self + .backend + .set( + KEYRING_SERVICE_NAME, + &Self::account_id(pubkey), + &secret.to_secret_hex(), + ) + .map_err(Error::from); + + tracing::trace!("Store secret result: {res:?}"); + + res + } + + pub fn get_secret(&self, pubkey: &Pubkey) -> Result<Option<SecretKey>> { + let maybe_secret = self + .backend + .get(KEYRING_SERVICE_NAME, &Self::account_id(pubkey)) + .map_err(Error::from); + + let secret_hex = match maybe_secret { + Ok(m_secret) => { + let Some(secret) = m_secret else { + tracing::trace!("Keyring gave us empty secret for {pubkey}"); + return Ok(None); + }; + tracing::trace!("Received an actual secret for {pubkey} successfully"); + secret + } + Err(e) => { + tracing::trace!("Failed to retrieve secret for {pubkey}: {e}"); + return Err(e); + } + }; + + let secret_key = SecretKey::from_hex(secret_hex).map_err(|err| { + Error::Generic(format!( + "invalid secret key from keyring for {}: {err}", + Self::account_id(pubkey) + )) + })?; + + Ok(Some(secret_key)) + } + + pub fn remove_secret(&self, pubkey: &Pubkey) -> Result<()> { + self.backend + .delete(KEYRING_SERVICE_NAME, &Self::account_id(pubkey)) + .map_err(Error::from) + } + + fn account_id(pubkey: &Pubkey) -> String { + pubkey.hex() + } +} + +impl Default for KeyringStore { + fn default() -> Self { + Self { + backend: KeyringBackendType::OS(OsKeyringBackend), + } + } +} + +#[cfg(test)] +#[derive(Clone, Default, Debug)] +struct MemoryKeyringBackend { + // RwLock to not make the KeyringBackendType api mutable... it's only for testing so it's ok + entries: std::sync::Arc<std::sync::RwLock<std::collections::HashMap<(String, String), String>>>, +} + +#[cfg(test)] +impl MemoryKeyringBackend { + fn set(&self, service: &str, account: &str, secret: &str) -> BackendResult<()> { + self.entries + .write() + .unwrap() + .insert((service.to_owned(), account.to_owned()), secret.to_owned()); + Ok(()) + } + + fn get(&self, service: &str, account: &str) -> BackendResult<Option<String>> { + Ok(self + .entries + .read() + .unwrap() + .get(&(service.to_owned(), account.to_owned())) + .cloned()) + } + + fn delete(&self, service: &str, account: &str) -> BackendResult<()> { + self.entries + .write() + .unwrap() + .remove(&(service.to_owned(), account.to_owned())); + Ok(()) + } +} diff --git a/crates/notedeck/src/storage/mod.rs b/crates/notedeck/src/storage/mod.rs @@ -1,5 +1,7 @@ mod account_storage; mod file_storage; +mod keyring_store; pub use account_storage::{AccountStorage, AccountStorageReader, AccountStorageWriter}; pub use file_storage::{delete_file, write_file, DataPath, DataPathType, Directory}; +pub use keyring_store::KeyringStore; diff --git a/crates/notedeck/src/user_account.rs b/crates/notedeck/src/user_account.rs @@ -39,6 +39,7 @@ impl UserAccount { } } +#[derive(Clone)] pub struct UserAccountSerializable { pub key: Keypair, pub wallet: Option<WalletSerializable>, diff --git a/crates/notedeck_chrome/Cargo.toml b/crates/notedeck_chrome/Cargo.toml @@ -61,6 +61,7 @@ tracing-logcat = "0.1.0" #log = { workspace = true } #android-activity = { version = "0.6", features = [ "game-activity" ] } egui-winit.workspace = true +android-keyring = { workspace = true } [package.metadata.bundle] name = "Notedeck" diff --git a/crates/notedeck_chrome/src/android.rs b/crates/notedeck_chrome/src/android.rs @@ -41,6 +41,8 @@ pub async fn android_main(android_app: AndroidApp) { .with(fmt_layer) .init(); + let _ = android_keyring::set_android_keyring_credential_builder(); + let path = android_app.internal_data_path().expect("data path"); let mut options = eframe::NativeOptions { depth_buffer: 24, diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -342,7 +342,7 @@ fn profile_stats( ui.horizontal(|ui| { let resp = ui .label( - RichText::new(format!("{} ", following_count)) + RichText::new(format!("{following_count} ")) .size(notedeck::fonts::get_font_size( ui.ctx(), &NotedeckTextStyle::Small, diff --git a/crates/notedeck_columns/src/ui/search/mod.rs b/crates/notedeck_columns/src/ui/search/mod.rs @@ -875,7 +875,7 @@ fn search_posts_button(query: &str, is_selected: bool, width: f32) -> impl egui: ui.put(icon_rect, search_icon(min_img_size / 2.0, min_img_size)); - let text = format!("Search posts for \"{}\"", query); + let text = format!("Search posts for \"{query}\""); let name_font = egui::FontId::new(body_font_size, NotedeckTextStyle::Body.font_family()); let painter = ui.painter(); let text_galley = painter.layout(