From 6505c6efb1713fee2a0c2b942a5898747f18e963 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Fri, 5 Apr 2024 10:41:24 +0200 Subject: [PATCH] keyring: add `nostr-keyring` Closes https://github.com/rust-nostr/nostr/issues/392 Signed-off-by: Yuki Kishimoto --- Cargo.lock | 36 ++++++++++ crates/nostr-keyring/Cargo.toml | 20 ++++++ crates/nostr-keyring/README.md | 15 +++++ crates/nostr-keyring/src/constants.rs | 9 +++ crates/nostr-keyring/src/dat.rs | 78 +++++++++++++++++++++ crates/nostr-keyring/src/dir.rs | 68 +++++++++++++++++++ crates/nostr-keyring/src/lib.rs | 97 +++++++++++++++++++++++++++ 7 files changed, 323 insertions(+) create mode 100644 crates/nostr-keyring/Cargo.toml create mode 100644 crates/nostr-keyring/README.md create mode 100644 crates/nostr-keyring/src/constants.rs create mode 100644 crates/nostr-keyring/src/dat.rs create mode 100644 crates/nostr-keyring/src/dir.rs create mode 100644 crates/nostr-keyring/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f7c70931e..6d2acbe62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,6 +763,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -773,6 +782,18 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1700,6 +1721,15 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "nostr-keyring" +version = "0.29.0" +dependencies = [ + "dirs", + "nostr", + "thiserror", +] + [[package]] name = "nostr-ndb" version = "0.32.0" @@ -1931,6 +1961,12 @@ dependencies = [ "log", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" diff --git a/crates/nostr-keyring/Cargo.toml b/crates/nostr-keyring/Cargo.toml new file mode 100644 index 000000000..c1f4f7ed3 --- /dev/null +++ b/crates/nostr-keyring/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nostr-keyring" +version = "0.29.0" +edition = "2021" +description = "Nostr Keyring" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +readme = "README.md" +rust-version.workspace = true +keywords = ["nostr", "keyring"] + +[dependencies] +#keyring = "2.3" # TODO: use only for ios target? +nostr = { workspace = true, default-features = false, features = ["std", "nip49"] } +thiserror.workspace = true + +[target.'cfg(not(all(target_os = "android", target_os = "ios")))'.dependencies] +dirs = "5.0" diff --git a/crates/nostr-keyring/README.md b/crates/nostr-keyring/README.md new file mode 100644 index 000000000..6573b261c --- /dev/null +++ b/crates/nostr-keyring/README.md @@ -0,0 +1,15 @@ +# Nostr Keyring + +Keyring for Nostr apps. + +## State + +**This library is in an ALPHA state**, things that are implemented generally work but the API will change in breaking ways. + +## Donations + +`rust-nostr` is free and open-source. This means we do not earn any revenue by selling it. Instead, we rely on your financial support. If you actively use any of the `rust-nostr` libs/software/services, then please [donate](https://rust-nostr.org/donate). + +## License + +This project is distributed under the MIT software license - see the [LICENSE](../../LICENSE) file for details \ No newline at end of file diff --git a/crates/nostr-keyring/src/constants.rs b/crates/nostr-keyring/src/constants.rs new file mode 100644 index 000000000..7a7c594fd --- /dev/null +++ b/crates/nostr-keyring/src/constants.rs @@ -0,0 +1,9 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +pub const ACCOUNT: u8 = 0x00; +pub const NAME: u8 = 0x01; +pub const KEY_UNENCRYPTED: u8 = 0x02; +pub const KEY_ENCRYPTED: u8 = 0x03; +pub const KEY_WATCH_ONLY: u8 = 0x04; diff --git a/crates/nostr-keyring/src/dat.rs b/crates/nostr-keyring/src/dat.rs new file mode 100644 index 000000000..288b3f0e0 --- /dev/null +++ b/crates/nostr-keyring/src/dat.rs @@ -0,0 +1,78 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +//! Accounts.dat format +//! +//! Fields: +//! * Version: 4 bytes, little-endian +//! * Accounts count: variable +//! * Accounts: variable +//! +//! Account format: +//! * Identifier bit (ex. 0x01) +//! * Name: variable (TLV) +//! * Identifier (0x02) +//! * Len: u8 +//! * Value: variable +//! * Key: variable (TLV) + +use std::collections::BTreeSet; +use std::iter::Skip; +use std::slice::Iter; + +use nostr::prelude::*; + +use super::constants::ACCOUNT; +use super::Version; + +pub struct NostrKeyringDat { + version: Version, + list: BTreeSet, +} + +impl NostrKeyringDat { + pub fn parse(slice: &[u8]) -> Self { + // Get version + // TODO + + let mut iter: Skip> = slice.iter().skip(4); + + // TODO: match version + + // V1 + // Get number of accounts + // Start iterating and parsing accounts + //let account = Account::parse(slice, version)?; + + todo!() + } +} + +pub struct Account { + name: String, + key: AccountKey, +} + +impl Account { + fn parse(slice: &mut [u8], version: Version) -> Self { + match version { + Version::V1 => { + // Get identifier + let identifier: u8 = slice.first().copied().unwrap(); + + if identifier == ACCOUNT { + // TODO: parse name and key + } + + todo!() + } + } + } +} + +pub enum AccountKey { + Unencrypted(SecretKey), + Encrypted(EncryptedSecretKey), + WatchOnly(PublicKey), +} diff --git a/crates/nostr-keyring/src/dir.rs b/crates/nostr-keyring/src/dir.rs new file mode 100644 index 000000000..853440fa8 --- /dev/null +++ b/crates/nostr-keyring/src/dir.rs @@ -0,0 +1,68 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; + +use thiserror::Error; + +pub(crate) const ACCOUNT_EXTENSION: &str = "ncryptsec"; +pub(crate) const ACCOUNT_DOT_EXTENSION: &str = ".ncryptsec"; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + IO(#[from] std::io::Error), + #[error("Impossible to get file name")] + FailedToGetFileName, +} + +pub fn accounts_dir

(base_path: P) -> Result +where + P: AsRef, +{ + let base_path: &Path = base_path.as_ref(); + fs::create_dir_all(&base_path)?; + + let accounts_path: PathBuf = base_path.join("keys"); + Ok(accounts_path) +} + +pub fn get_accounts_list

(path: P) -> Result, Error> +where + P: AsRef, +{ + let mut names: BTreeSet = BTreeSet::new(); + + // Get and iterate all paths + let paths = fs::read_dir(path)?; + for path in paths { + let path: PathBuf = path?.path(); + + // Check if path has file name + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + // Check if file name terminate with extension + if name.ends_with(ACCOUNT_DOT_EXTENSION) { + // Split file name and extension + let mut split = name.split(ACCOUNT_DOT_EXTENSION); + if let Some(value) = split.next() { + names.insert(value.to_string()); + } + } + } + } + + Ok(names) +} + +pub(crate) fn get_account_file(base_path: P, name: S) -> Result +where + P: AsRef, + S: Into, +{ + let mut keychain_file: PathBuf = base_path.as_ref().join(name.into()); + keychain_file.set_extension(ACCOUNT_EXTENSION); + Ok(keychain_file) +} diff --git a/crates/nostr-keyring/src/lib.rs b/crates/nostr-keyring/src/lib.rs new file mode 100644 index 000000000..2165defba --- /dev/null +++ b/crates/nostr-keyring/src/lib.rs @@ -0,0 +1,97 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +#![forbid(unsafe_code)] +#![warn(missing_docs)] +#![warn(rustdoc::bare_urls)] + +//! Nostr Keyring + +// * Save in OS keyring? +// * Save in ~/.nostr/accounts.dat or ~/.nostr/keys.dat + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +use nostr::prelude::*; +use thiserror::Error; + +mod constants; +mod dat; +mod dir; + +use crate::dat::AccountKey; + +/// Nostr Keyring error +#[derive(Debug, Error)] +pub enum Error { + /// Dir error + #[error(transparent)] + Dir(#[from] dir::Error), + /// Can't get home directory + #[error("Can't get home directory")] + CantGetHomeDir, +} + +/// Nostr Keyring version +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Version { + #[default] + V1, +} + +/// Nostr Keyring +pub struct NostrKeyring { + path: PathBuf, + version: Version, + accounts: BTreeMap, +} + +impl NostrKeyring { + /// Get list of available accounts + #[cfg(not(all(target_os = "android", target_os = "ios")))] + pub fn open() -> Result { + let home_dir: PathBuf = dirs::home_dir().ok_or(Error::CantGetHomeDir)?; + todo!() + } + + /// Open Nostr Keyring from custom path + /// + /// Automatically create it if not exists. + pub fn open_in

(base_path: P) -> Result + where + P: AsRef, + { + todo!() + } + + /// Get keyring version + #[inline(always)] + pub fn version(&self) -> Version { + self.version + } + + /// Get list of available accounts + #[inline(always)] + pub fn accounts(&self) -> &BTreeMap { + &self.accounts + } + + /// Add account + #[inline(always)] + pub fn add_account(&mut self, name: String, key: AccountKey) { + self.accounts.insert(name, key); + } + + /// Remove account from keyring + #[inline(always)] + pub fn remove_account(&mut self, name: &str) { + self.accounts.remove(name); + } + + /// Write keyring to file + pub fn save(&self) -> Result<(), Error> { + todo!() + } +}