Skip to content

Commit

Permalink
keyring: add nostr-keyring
Browse files Browse the repository at this point in the history
Closes #392

Signed-off-by: Yuki Kishimoto <[email protected]>
  • Loading branch information
yukibtc committed Jun 10, 2024
1 parent caf7a2e commit 6505c6e
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 0 deletions.
36 changes: 36 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions crates/nostr-keyring/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
15 changes: 15 additions & 0 deletions crates/nostr-keyring/README.md
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions crates/nostr-keyring/src/constants.rs
Original file line number Diff line number Diff line change
@@ -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;
78 changes: 78 additions & 0 deletions crates/nostr-keyring/src/dat.rs
Original file line number Diff line number Diff line change
@@ -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<Account>,
}

impl NostrKeyringDat {
pub fn parse(slice: &[u8]) -> Self {
// Get version
// TODO

let mut iter: Skip<Iter<u8>> = 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),
}
68 changes: 68 additions & 0 deletions crates/nostr-keyring/src/dir.rs
Original file line number Diff line number Diff line change
@@ -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<P>(base_path: P) -> Result<PathBuf, Error>
where
P: AsRef<Path>,
{
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<P>(path: P) -> Result<BTreeSet<String>, Error>
where
P: AsRef<Path>,
{
let mut names: BTreeSet<String> = 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<P, S>(base_path: P, name: S) -> Result<PathBuf, Error>
where
P: AsRef<Path>,
S: Into<String>,
{
let mut keychain_file: PathBuf = base_path.as_ref().join(name.into());
keychain_file.set_extension(ACCOUNT_EXTENSION);
Ok(keychain_file)
}
97 changes: 97 additions & 0 deletions crates/nostr-keyring/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String, AccountKey>,
}

impl NostrKeyring {
/// Get list of available accounts
#[cfg(not(all(target_os = "android", target_os = "ios")))]
pub fn open() -> Result<Self, Error> {
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<P>(base_path: P) -> Result<Self, Error>
where
P: AsRef<Path>,
{
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<String, AccountKey> {
&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!()
}
}

0 comments on commit 6505c6e

Please sign in to comment.