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:
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(