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:
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)
+ }
+}