lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit b05c47eddcbb54bf95fe03779e98c0bb04ac5662
parent c5c7e8bd6894d6d0120cffc3d9eab8bcac51923b
Author: triesap <tyson@radroots.org>
Date:   Sun, 24 Aug 2025 23:19:16 +0000

identity: add `radroots-identity` crate for managing cryptographic identities

Diffstat:
MCargo.lock | 24++++++++++++++++++++++++
MCargo.toml | 3+++
Acrates/identity/Cargo.toml | 19+++++++++++++++++++
Acrates/identity/src/error.rs | 19+++++++++++++++++++
Acrates/identity/src/lib.rs | 8++++++++
Acrates/identity/src/spec.rs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 178 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1623,6 +1623,18 @@ dependencies = [ ] [[package]] +name = "radroots-identity" +version = "0.1.0" +dependencies = [ + "nostr", + "radroots-runtime", + "serde", + "thiserror 1.0.69", + "tracing", + "uuid", +] + +[[package]] name = "radroots-nostr" version = "0.1.0" dependencies = [ @@ -2637,6 +2649,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -15,7 +15,9 @@ radroots-core = { path = "crates/core", version = "0.1.0", default-features = fa radroots-events = { path = "crates/events", version = "0.1.0", default-features = false } radroots-events-codec = { path = "crates/events-codec", version = "0.1.0", default-features = false } radroots-events-indexed = { path = "crates/events-indexed", version = "0.1.0", default-features = false } +radroots-identity = { path = "crates/identity", version = "0.1.0", default-features = false } radroots-nostr = { path = "crates/nostr", version = "0.1.0", default-features = false } +radroots-runtime = { path = "crates/runtime", version = "0.1.0", default-features = false } radroots-trade = { path = "crates/trade", version = "0.1.0", default-features = false } anyhow = { version = "1" } @@ -36,3 +38,4 @@ tracing = { version = "0.1" } tracing-subscriber = { version = "0.3" } tracing-appender = { version = "0.2" } typeshare = { version = "1" } +uuid = { version = "1.16.0" } diff --git a/crates/identity/Cargo.toml b/crates/identity/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "radroots-identity" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true + +[features] +default = ["std"] +std = [] + +[dependencies] +radroots-runtime = { workspace = true } +nostr = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +uuid = { workspace = true, features = ["v4", "serde"] } +tracing = { workspace = true } diff --git a/crates/identity/src/error.rs b/crates/identity/src/error.rs @@ -0,0 +1,19 @@ +use radroots_runtime::RuntimeJsonError; +use std::path::PathBuf; +use thiserror::Error; + +/// Errors when loading or generating an identity. +#[derive(Debug, Error)] +pub enum IdentityError { + #[error(transparent)] + Store(#[from] RuntimeJsonError), + + #[error("invalid identity: {0}")] + Invalid(#[source] Box<dyn std::error::Error + Send + Sync>), + + #[error( + "identity file missing at {0} and generation is not permitted \ + (pass --allow-generate-identity)" + )] + GenerationNotAllowed(PathBuf), +} diff --git a/crates/identity/src/lib.rs b/crates/identity/src/lib.rs @@ -0,0 +1,8 @@ +pub mod error; +pub mod spec; + +pub use error::IdentityError; +pub use spec::{to_keys, load_or_generate, IdentitySpec, MinimalIdentity, ExtendedIdentity}; + +/// The canonical default identity file path. +pub const DEFAULT_IDENTITY_PATH: &str = "identity.json"; diff --git a/crates/identity/src/spec.rs b/crates/identity/src/spec.rs @@ -0,0 +1,105 @@ +use crate::{DEFAULT_IDENTITY_PATH, error::IdentityError}; +use radroots_runtime::JsonFile; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; +use uuid::Uuid; + +/// Trait that identity file types must implement. +pub trait IdentitySpec: Serialize + DeserializeOwned + Sized { + /// The runtime key material type (e.g. `nostr::Keys`). + type Keys; + + /// Error type when parsing stored material into keys. + type ParseError: std::error::Error + Send + Sync + 'static; + + /// Create a brand new identity value if the file does not exist. + fn generate_new() -> Self; + + /// Turn this identity into runtime key material. + fn to_keys(&self) -> Result<Self::Keys, Self::ParseError>; +} + +/// Convert an identity into its keys, mapped into the shared error type. +pub fn to_keys<I: IdentitySpec>(id: &I) -> Result<I::Keys, IdentityError> { + id.to_keys() + .map_err(|e| IdentityError::Invalid(Box::new(e))) +} + +/// Load an identity file, or generate a new one if allowed. +/// Defaults to [`DEFAULT_IDENTITY_PATH`] if no path is provided. +pub fn load_or_generate<I, P>( + path: Option<P>, + allow_generate: bool, +) -> Result<JsonFile<I>, IdentityError> +where + I: IdentitySpec + Serialize + for<'de> Deserialize<'de>, + P: AsRef<Path>, +{ + let p = path + .map(|p| p.as_ref().to_path_buf()) + .unwrap_or_else(|| PathBuf::from(DEFAULT_IDENTITY_PATH)); + + if p.exists() { + let store = JsonFile::load(&p)?; + return Ok(store); + } + + if !allow_generate { + return Err(IdentityError::GenerationNotAllowed(p)); + } + + let store = JsonFile::load_or_create_with(&p, I::generate_new)?; + Ok(store) +} + +/// A minimal identity: just a secret key string. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimalIdentity { + pub key: String, +} + +impl IdentitySpec for MinimalIdentity { + type Keys = nostr::Keys; + type ParseError = nostr::key::Error; + + fn generate_new() -> Self { + let keys = nostr::Keys::generate(); + Self { + key: keys.secret_key().to_secret_hex(), + } + } + + fn to_keys(&self) -> Result<Self::Keys, Self::ParseError> { + nostr::Keys::from_str(&self.key) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExtendedIdentity { + pub key: String, + pub identifier: String, + pub metadata: Option<nostr::Event>, + pub application_handler: Option<nostr::Event>, +} + +impl IdentitySpec for ExtendedIdentity { + type Keys = nostr::Keys; + type ParseError = nostr::key::Error; + + fn generate_new() -> Self { + let keys = nostr::Keys::generate(); + Self { + key: keys.secret_key().to_secret_hex(), + identifier: Uuid::new_v4().to_string(), + metadata: None, + application_handler: None, + } + } + + fn to_keys(&self) -> Result<Self::Keys, Self::ParseError> { + nostr::Keys::from_str(&self.key) + } +}