.
This commit is contained in:
@@ -91,7 +91,7 @@ impl AccountSelector {
|
|||||||
|
|
||||||
fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let task = nostr.read(cx).get_signer(&public_key, cx);
|
let task = nostr.read(cx).get_secret(public_key, cx);
|
||||||
|
|
||||||
// Mark the public key as being logged in
|
// Mark the public key as being logged in
|
||||||
self.set_logging_in(public_key, cx);
|
self.set_logging_in(public_key, cx);
|
||||||
@@ -117,7 +117,7 @@ impl AccountSelector {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
nostr.update(cx, |this, cx| {
|
nostr.update(cx, |this, cx| {
|
||||||
this.remove_signer(&public_key, cx);
|
this.remove_secret(&public_key, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -79,16 +80,19 @@ pub struct NostrRegistry {
|
|||||||
/// Nostr signer
|
/// Nostr signer
|
||||||
signer: Arc<CoopSigner>,
|
signer: Arc<CoopSigner>,
|
||||||
|
|
||||||
/// Local public keys
|
/// All local stored identities
|
||||||
npubs: Entity<Vec<PublicKey>>,
|
npubs: Entity<Vec<PublicKey>>,
|
||||||
|
|
||||||
/// App keys
|
/// Keys directory
|
||||||
|
key_dir: PathBuf,
|
||||||
|
|
||||||
|
/// Master app keys used for various operations.
|
||||||
///
|
///
|
||||||
/// Used for Nostr Connect and NIP-4e operations
|
/// Example: Nostr Connect and NIP-4e operations
|
||||||
app_keys: Keys,
|
app_keys: Keys,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
tasks: Vec<Task<()>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<StateEvent> for NostrRegistry {}
|
impl EventEmitter<StateEvent> for NostrRegistry {}
|
||||||
@@ -106,12 +110,20 @@ impl NostrRegistry {
|
|||||||
|
|
||||||
/// Create a new nostr instance
|
/// Create a new nostr instance
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let key_dir = config_dir().join("keys");
|
||||||
|
let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate());
|
||||||
|
|
||||||
// Construct the nostr signer
|
// Construct the nostr signer
|
||||||
let app_keys = get_or_init_app_keys().unwrap_or(Keys::generate());
|
|
||||||
let signer = Arc::new(CoopSigner::new(app_keys.clone()));
|
let signer = Arc::new(CoopSigner::new(app_keys.clone()));
|
||||||
|
|
||||||
// Construct the nostr npubs entity
|
// Get all local stored npubs
|
||||||
let npubs = cx.new(|_| vec![]);
|
let npubs = cx.new(|_| match Self::discover(&key_dir) {
|
||||||
|
Ok(npubs) => npubs,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to discover npubs: {e}");
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Construct the nostr lmdb instance
|
// Construct the nostr lmdb instance
|
||||||
let lmdb = cx.foreground_executor().block_on(async move {
|
let lmdb = cx.foreground_executor().block_on(async move {
|
||||||
@@ -148,6 +160,7 @@ impl NostrRegistry {
|
|||||||
client,
|
client,
|
||||||
signer,
|
signer,
|
||||||
npubs,
|
npubs,
|
||||||
|
key_dir,
|
||||||
app_keys,
|
app_keys,
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
}
|
}
|
||||||
@@ -173,6 +186,33 @@ impl NostrRegistry {
|
|||||||
self.app_keys.clone()
|
self.app_keys.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Discover all npubs in the keys directory
|
||||||
|
fn discover(dir: &PathBuf) -> Result<Vec<PublicKey>, Error> {
|
||||||
|
// Ensure keys directory exists
|
||||||
|
std::fs::create_dir_all(dir)?;
|
||||||
|
|
||||||
|
let files = std::fs::read_dir(dir)?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let mut npubs: Vec<PublicKey> = Vec::new();
|
||||||
|
|
||||||
|
for file in files.flatten() {
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
let modified_time = metadata.modified()?;
|
||||||
|
let name = file.file_name().into_string().unwrap().replace(".npub", "");
|
||||||
|
entries.push((modified_time, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by modification time (most recent first)
|
||||||
|
entries.sort_by(|a, b| b.0.cmp(&a.0));
|
||||||
|
|
||||||
|
for (_, name) in entries {
|
||||||
|
let public_key = PublicKey::parse(&name)?;
|
||||||
|
npubs.push(public_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(npubs)
|
||||||
|
}
|
||||||
|
|
||||||
/// Connect to the bootstrapping relays
|
/// Connect to the bootstrapping relays
|
||||||
fn connect(&mut self, cx: &mut Context<Self>) {
|
fn connect(&mut self, cx: &mut Context<Self>) {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
@@ -211,105 +251,139 @@ impl NostrRegistry {
|
|||||||
// Emit connecting event
|
// Emit connecting event
|
||||||
cx.emit(StateEvent::Connecting);
|
cx.emit(StateEvent::Connecting);
|
||||||
|
|
||||||
self.tasks
|
|
||||||
.push(cx.spawn(async move |this, cx| match task.await {
|
|
||||||
Ok(_) => {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
cx.emit(StateEvent::Connected);
|
|
||||||
this.get_npubs(cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |_this, cx| {
|
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all used npubs
|
|
||||||
fn get_npubs(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let npubs = self.npubs.downgrade();
|
|
||||||
|
|
||||||
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
|
||||||
let dir = config_dir().join("keys");
|
|
||||||
// Ensure keys directory exists
|
|
||||||
smol::fs::create_dir_all(&dir).await?;
|
|
||||||
|
|
||||||
let mut files = smol::fs::read_dir(&dir).await?;
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
|
|
||||||
while let Some(Ok(entry)) = files.next().await {
|
|
||||||
let metadata = entry.metadata().await?;
|
|
||||||
let modified_time = metadata.modified()?;
|
|
||||||
let name = entry
|
|
||||||
.file_name()
|
|
||||||
.into_string()
|
|
||||||
.unwrap()
|
|
||||||
.replace(".npub", "");
|
|
||||||
|
|
||||||
entries.push((modified_time, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by modification time (most recent first)
|
|
||||||
entries.sort_by(|a, b| b.0.cmp(&a.0));
|
|
||||||
|
|
||||||
let mut npubs = Vec::new();
|
|
||||||
|
|
||||||
for (_, name) in entries {
|
|
||||||
let public_key = PublicKey::parse(&name)?;
|
|
||||||
npubs.push(public_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(npubs)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match task.await {
|
if let Err(e) = task.await {
|
||||||
Ok(public_keys) => match public_keys.is_empty() {
|
this.update(cx, |_this, cx| {
|
||||||
true => {
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
this.update(cx, |this, cx| {
|
})?;
|
||||||
this.create_identity(cx);
|
} else {
|
||||||
})
|
this.update(cx, |_this, cx| {
|
||||||
.ok();
|
cx.emit(StateEvent::Connected);
|
||||||
}
|
})?;
|
||||||
false => {
|
|
||||||
// TODO: auto login
|
|
||||||
npubs
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.extend(public_keys);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
this.update(cx, |_this, cx| {
|
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the secret for a given npub.
|
||||||
|
pub fn get_secret(
|
||||||
|
&self,
|
||||||
|
public_key: PublicKey,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
|
||||||
|
let npub = public_key.to_bech32().unwrap();
|
||||||
|
let key_path = self.key_dir.join(format!("{}.npub", npub));
|
||||||
|
let app_keys = self.app_keys.clone();
|
||||||
|
|
||||||
|
if let Ok(payload) = std::fs::read_to_string(key_path) {
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let decrypted = app_keys.nip44_decrypt(&public_key, &payload).await?;
|
||||||
|
let secret = SecretKey::parse(&decrypted)?;
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
|
||||||
|
Ok(keys.into_nostr_signer())
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.get_secret_keyring(&npub, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the secret for a given npub in the OS credentials store.
|
||||||
|
#[deprecated = "Use get_secret instead"]
|
||||||
|
fn get_secret_keyring(
|
||||||
|
&self,
|
||||||
|
user: &str,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
|
||||||
|
let read = cx.read_credentials(user);
|
||||||
|
let app_keys = self.app_keys.clone();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let (_, secret) = read
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
|
||||||
|
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
|
||||||
|
|
||||||
|
// Try to parse as a direct secret key first
|
||||||
|
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
|
||||||
|
return Ok(Keys::new(secret_key).into_nostr_signer());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the secret into string
|
||||||
|
let sec = String::from_utf8(secret)
|
||||||
|
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
|
||||||
|
|
||||||
|
// Try to parse as a NIP-46 URI
|
||||||
|
let uri =
|
||||||
|
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
|
||||||
|
|
||||||
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
|
||||||
|
|
||||||
|
// Set the auth URL handler
|
||||||
|
nip46.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
Ok(nip46.into_nostr_signer())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new npub to the keys directory
|
||||||
|
fn write_secret(
|
||||||
|
&self,
|
||||||
|
public_key: PublicKey,
|
||||||
|
secret: String,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Result<(), Error>> {
|
||||||
|
let npub = public_key.to_bech32().unwrap();
|
||||||
|
let key_path = self.key_dir.join(format!("{}.npub", npub));
|
||||||
|
let app_keys = self.app_keys.clone();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
// If the secret starts with "bunker://" (nostr connect), use it directly; otherwise, encrypt it
|
||||||
|
let content = if secret.starts_with("bunker://") {
|
||||||
|
secret
|
||||||
|
} else {
|
||||||
|
app_keys.nip44_encrypt(&public_key, &secret).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write the encrypted secret to the keys directory
|
||||||
|
smol::fs::write(key_path, &content).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a secret
|
||||||
|
pub fn remove_secret(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||||
|
let public_key = public_key.to_owned();
|
||||||
|
let npub = public_key.to_bech32().unwrap();
|
||||||
|
|
||||||
|
let keys_dir = config_dir().join("keys");
|
||||||
|
let key_path = keys_dir.join(format!("{}.npub", npub));
|
||||||
|
|
||||||
|
// Remove the secret file from the keys directory
|
||||||
|
std::fs::remove_file(key_path).ok();
|
||||||
|
|
||||||
|
self.npubs.update(cx, |this, cx| {
|
||||||
|
this.retain(|k| k != &public_key);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new identity
|
/// Create a new identity
|
||||||
fn create_identity(&mut self, cx: &mut Context<Self>) {
|
pub fn create_identity(&mut self, cx: &mut Context<Self>) {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let async_keys = keys.clone();
|
let async_keys = keys.clone();
|
||||||
|
|
||||||
let username = keys.public_key().to_bech32().unwrap();
|
|
||||||
let secret = keys.secret_key().to_secret_bytes();
|
|
||||||
|
|
||||||
// Create a write credential task
|
|
||||||
let write_credential = cx.write_credentials(&username, &username, &secret);
|
|
||||||
|
|
||||||
// Emit creating event
|
// Emit creating event
|
||||||
cx.emit(StateEvent::Creating);
|
cx.emit(StateEvent::Creating);
|
||||||
|
|
||||||
|
// Create the write secret task
|
||||||
|
let write_secret =
|
||||||
|
self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx);
|
||||||
|
|
||||||
// Run async tasks in background
|
// Run async tasks in background
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let signer = async_keys.into_nostr_signer();
|
let signer = async_keys.into_nostr_signer();
|
||||||
@@ -361,7 +435,7 @@ impl NostrRegistry {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Write user's credentials to the system keyring
|
// Write user's credentials to the system keyring
|
||||||
write_credential.await?;
|
write_secret.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -371,58 +445,19 @@ impl NostrRegistry {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_signer(keys, cx);
|
this.set_signer(keys, cx);
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the signer in keyring by username
|
|
||||||
pub fn get_signer(
|
|
||||||
&self,
|
|
||||||
public_key: &PublicKey,
|
|
||||||
cx: &App,
|
|
||||||
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
|
|
||||||
let username = public_key.to_bech32().unwrap();
|
|
||||||
let app_keys = self.app_keys.clone();
|
|
||||||
let read_credential = cx.read_credentials(&username);
|
|
||||||
|
|
||||||
cx.spawn(async move |_cx| {
|
|
||||||
let (_, secret) = read_credential
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
|
|
||||||
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
|
|
||||||
|
|
||||||
// Try to parse as a direct secret key first
|
|
||||||
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
|
|
||||||
return Ok(Keys::new(secret_key).into_nostr_signer());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the secret into string
|
|
||||||
let sec = String::from_utf8(secret)
|
|
||||||
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
|
|
||||||
|
|
||||||
// Try to parse as a NIP-46 URI
|
|
||||||
let uri =
|
|
||||||
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
|
|
||||||
|
|
||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
|
||||||
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
|
|
||||||
|
|
||||||
// Set the auth URL handler
|
|
||||||
nip46.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
Ok(nip46.into_nostr_signer())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the signer for the nostr client and verify the public key
|
/// Set the signer for the nostr client and verify the public key
|
||||||
pub fn set_signer<T>(&mut self, new: T, cx: &mut Context<Self>)
|
pub fn set_signer<T>(&mut self, new: T, cx: &mut Context<Self>)
|
||||||
where
|
where
|
||||||
@@ -430,6 +465,7 @@ impl NostrRegistry {
|
|||||||
{
|
{
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
let signer = self.signer();
|
let signer = self.signer();
|
||||||
|
let key_dir = self.key_dir.clone();
|
||||||
|
|
||||||
// Create a task to update the signer and verify the public key
|
// Create a task to update the signer and verify the public key
|
||||||
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||||
@@ -441,15 +477,6 @@ impl NostrRegistry {
|
|||||||
let signer = client.signer().context("Signer not found")?;
|
let signer = client.signer().context("Signer not found")?;
|
||||||
let public_key = signer.get_public_key().await?;
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
let npub = public_key.to_bech32().unwrap();
|
|
||||||
let keys_dir = config_dir().join("keys");
|
|
||||||
|
|
||||||
// Ensure keys directory exists
|
|
||||||
smol::fs::create_dir_all(&keys_dir).await?;
|
|
||||||
|
|
||||||
let key_path = keys_dir.join(format!("{}.npub", npub));
|
|
||||||
smol::fs::write(key_path, "").await?;
|
|
||||||
|
|
||||||
log::info!("Signer's public key: {}", public_key);
|
log::info!("Signer's public key: {}", public_key);
|
||||||
Ok(public_key)
|
Ok(public_key)
|
||||||
});
|
});
|
||||||
@@ -457,9 +484,7 @@ impl NostrRegistry {
|
|||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
Ok(public_key) => {
|
Ok(public_key) => {
|
||||||
// Update states
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.ensure_relay_list(&public_key, cx);
|
|
||||||
// Add public key to npubs if not already present
|
// Add public key to npubs if not already present
|
||||||
this.npubs.update(cx, |this, cx| {
|
this.npubs.update(cx, |this, cx| {
|
||||||
if !this.contains(&public_key) {
|
if !this.contains(&public_key) {
|
||||||
@@ -467,65 +492,43 @@ impl NostrRegistry {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit signer changed event
|
// Emit signer changed event
|
||||||
cx.emit(StateEvent::SignerSet);
|
cx.emit(StateEvent::SignerSet);
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a signer from the keyring
|
Ok(())
|
||||||
pub fn remove_signer(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
|
||||||
let public_key = public_key.to_owned();
|
|
||||||
let npub = public_key.to_bech32().unwrap();
|
|
||||||
let keys_dir = config_dir().join("keys");
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
let key_path = keys_dir.join(format!("{}.npub", npub));
|
|
||||||
smol::fs::remove_file(key_path).await.ok();
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.npubs().update(cx, |this, cx| {
|
|
||||||
this.retain(|k| k != &public_key);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a key signer to keyring
|
/// Add a key signer to keyring
|
||||||
pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context<Self>) {
|
pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context<Self>) {
|
||||||
let keys = keys.clone();
|
let keys = keys.clone();
|
||||||
let username = keys.public_key().to_bech32().unwrap();
|
let write_secret =
|
||||||
let secret = keys.secret_key().to_secret_bytes();
|
self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx);
|
||||||
|
|
||||||
// Write the credential to the keyring
|
|
||||||
let write_credential = cx.write_credentials(&username, "keys", &secret);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match write_credential.await {
|
match write_secret.await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_signer(keys, cx);
|
this.set_signer(keys, cx);
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,39 +549,32 @@ impl NostrRegistry {
|
|||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
Ok((public_key, uri)) => {
|
Ok((public_key, uri)) => {
|
||||||
let username = public_key.to_bech32().unwrap();
|
// Create the write secret task
|
||||||
let write_credential = this
|
let write_secret = this.read_with(cx, |this, cx| {
|
||||||
.read_with(cx, |_this, cx| {
|
this.write_secret(public_key, uri.to_string(), cx)
|
||||||
cx.write_credentials(
|
})?;
|
||||||
&username,
|
|
||||||
"nostrconnect",
|
|
||||||
uri.to_string().as_bytes(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
match write_credential.await {
|
match write_secret.await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_signer(nip46, cx);
|
this.set_signer(nip46, cx);
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,17 +590,17 @@ impl NostrRegistry {
|
|||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.ensure_connection(&event, cx);
|
this.ensure_connection(&event, cx);
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::RelayNotConfigured);
|
cx.emit(StateEvent::RelayNotConfigured);
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,17 +649,17 @@ impl NostrRegistry {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::RelayConnected);
|
cx.emit(StateEvent::RelayConnected);
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
cx.emit(StateEvent::RelayNotConfigured);
|
cx.emit(StateEvent::RelayNotConfigured);
|
||||||
cx.emit(StateEvent::error(e.to_string()));
|
cx.emit(StateEvent::error(e.to_string()));
|
||||||
})
|
})?;
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,29 +845,33 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get or create a new app keys
|
/// Get or create new app keys
|
||||||
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
fn get_or_init_app_keys(cx: &App) -> Result<Keys, Error> {
|
||||||
let dir = config_dir().join(".app_keys");
|
let read = cx.read_credentials(CLIENT_NAME);
|
||||||
|
let stored_keys: Option<Keys> = cx.foreground_executor().block_on(async move {
|
||||||
let content = match std::fs::read(&dir) {
|
if let Ok(Some((_, secret))) = read.await {
|
||||||
Ok(content) => content,
|
SecretKey::from_slice(&secret).map(Keys::new).ok()
|
||||||
Err(_) => {
|
} else {
|
||||||
// Generate new keys if file doesn't exist
|
None
|
||||||
let keys = Keys::generate();
|
|
||||||
let secret_key = keys.secret_key();
|
|
||||||
|
|
||||||
// Create directory and write secret key
|
|
||||||
std::fs::create_dir_all(dir.parent().unwrap())?;
|
|
||||||
std::fs::write(&dir, secret_key.to_secret_bytes())?;
|
|
||||||
|
|
||||||
return Ok(keys);
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
let secret_key = SecretKey::from_slice(&content)?;
|
if let Some(keys) = stored_keys {
|
||||||
let keys = Keys::new(secret_key);
|
Ok(keys)
|
||||||
|
} else {
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let user = keys.public_key().to_hex();
|
||||||
|
let secret = keys.secret_key().to_secret_bytes();
|
||||||
|
let write = cx.write_credentials(CLIENT_NAME, &user, &secret);
|
||||||
|
|
||||||
Ok(keys)
|
cx.foreground_executor().block_on(async move {
|
||||||
|
if let Err(e) = write.await {
|
||||||
|
log::error!("Keyring not available or panic: {e}")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(keys)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
|
fn default_relay_list() -> Vec<(RelayUrl, Option<RelayMetadata>)> {
|
||||||
|
|||||||
141
crates/state/src/npubs.rs
Normal file
141
crates/state/src/npubs.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{Error, anyhow};
|
||||||
|
use common::config_dir;
|
||||||
|
use gpui::{App, Context};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
|
||||||
|
use crate::{CLIENT_NAME, NOSTR_CONNECT_TIMEOUT};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NostrRing {
|
||||||
|
/// Keys directory
|
||||||
|
dir: PathBuf,
|
||||||
|
|
||||||
|
/// Master app keys used for various operations.
|
||||||
|
///
|
||||||
|
/// Example: Nostr Connect and NIP-4e operations
|
||||||
|
app_keys: Keys,
|
||||||
|
|
||||||
|
/// All local stored identities
|
||||||
|
npubs: Vec<PublicKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NostrRing {
|
||||||
|
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let dir = config_dir().join("keys");
|
||||||
|
let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate());
|
||||||
|
|
||||||
|
// Get all local stored npubs
|
||||||
|
let npubs = match Self::discover(&dir) {
|
||||||
|
Ok(npubs) => npubs,
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to discover npubs: {e}");
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
dir,
|
||||||
|
app_keys,
|
||||||
|
npubs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the secret for a given npub, if it exists
|
||||||
|
fn get_secret(&self, public_key: &PublicKey, cx: &App) -> Result<Arc<dyn NostrSigner>, Error> {
|
||||||
|
let npub = public_key.to_bech32()?;
|
||||||
|
let key_path = self.dir.join(format!("{}.npub", npub));
|
||||||
|
|
||||||
|
if let Ok(secret) = std::fs::read_to_string(key_path) {
|
||||||
|
let secret = SecretKey::parse(&secret)?;
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
|
||||||
|
Ok(keys.into_nostr_signer())
|
||||||
|
} else {
|
||||||
|
self.get_secret_keyring(&npub, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the secret for a given npub in the os credentials store
|
||||||
|
#[deprecated = "Use get_secret instead"]
|
||||||
|
fn get_secret_keyring(&self, user: &str, cx: &App) -> Result<Arc<dyn NostrSigner>, Error> {
|
||||||
|
let read = cx.read_credentials(user);
|
||||||
|
let app_keys = self.app_keys.clone();
|
||||||
|
|
||||||
|
cx.foreground_executor().block_on(async move {
|
||||||
|
let (_, secret) = read
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
|
||||||
|
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
|
||||||
|
|
||||||
|
// Try to parse as a direct secret key first
|
||||||
|
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
|
||||||
|
return Ok(Keys::new(secret_key).into_nostr_signer());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the secret into string
|
||||||
|
let sec = String::from_utf8(secret)
|
||||||
|
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
|
||||||
|
|
||||||
|
// Try to parse as a NIP-46 URI
|
||||||
|
let uri =
|
||||||
|
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
|
||||||
|
|
||||||
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
|
||||||
|
|
||||||
|
// Set the auth URL handler
|
||||||
|
nip46.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
Ok(nip46.into_nostr_signer())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new npub to the keys directory
|
||||||
|
fn add(&mut self, public_key: PublicKey, secret: &str) -> Result<(), Error> {
|
||||||
|
let npub = public_key.to_bech32()?;
|
||||||
|
let key_path = self.dir.join(format!("{}.npub", npub));
|
||||||
|
std::fs::write(key_path, secret)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a npub from the keys directory
|
||||||
|
fn remove(&self, public_key: &PublicKey) -> Result<(), Error> {
|
||||||
|
let npub = public_key.to_bech32()?;
|
||||||
|
let key_path = self.dir.join(format!("{}.npub", npub));
|
||||||
|
std::fs::remove_file(key_path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discover all npubs in the keys directory
|
||||||
|
fn discover(dir: &PathBuf) -> Result<Vec<PublicKey>, Error> {
|
||||||
|
// Ensure keys directory exists
|
||||||
|
std::fs::create_dir_all(dir)?;
|
||||||
|
|
||||||
|
let files = std::fs::read_dir(dir)?;
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let mut npubs: Vec<PublicKey> = Vec::new();
|
||||||
|
|
||||||
|
for file in files.flatten() {
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
let modified_time = metadata.modified()?;
|
||||||
|
let name = file.file_name().into_string().unwrap().replace(".npub", "");
|
||||||
|
entries.push((modified_time, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by modification time (most recent first)
|
||||||
|
entries.sort_by(|a, b| b.0.cmp(&a.0));
|
||||||
|
|
||||||
|
for (_, name) in entries {
|
||||||
|
let public_key = PublicKey::parse(&name)?;
|
||||||
|
npubs.push(public_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(npubs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@ impl CoopSigner {
|
|||||||
/// Get public key
|
/// Get public key
|
||||||
///
|
///
|
||||||
/// Ensure to call this method after the signer has been initialized.
|
/// Ensure to call this method after the signer has been initialized.
|
||||||
/// Otherwise, this method will panic.
|
/// Otherwise, it will panic.
|
||||||
pub fn public_key(&self) -> Option<PublicKey> {
|
pub fn public_key(&self) -> Option<PublicKey> {
|
||||||
*self.signer_pkey.read_blocking()
|
*self.signer_pkey.read_blocking()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user