From 7fe3e6f9c3fcc504fbd25b071fdcef3c5ab336d6 Mon Sep 17 00:00:00 2001 From: Blake McAnally Date: Thu, 5 Nov 2026 19:14:39 -0600 Subject: [PATCH] fix(cli): sign and encrypt sidebar events through Credentials main moved BuzzClient to Credentials: no `keys()`, an async `sign_event`, and NIP-44 through `Credentials::nip44_encrypt` and `nip44_decrypt`. The sidebar sync and its tests now use those, so a signer without a local key (desktop provider) reads and writes the sidebar documents too. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Blake McAnally --- crates/buzz-cli/src/commands/sidebar.rs | 4 +- crates/buzz-cli/src/commands/sidebar_flags.rs | 7 +++- crates/buzz-cli/src/commands/sidebar_sort.rs | 6 ++- crates/buzz-cli/src/commands/sidebar_sync.rs | 38 ++++++++++--------- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/crates/buzz-cli/src/commands/sidebar.rs b/crates/buzz-cli/src/commands/sidebar.rs index bd4882f443..77c2620914 100644 --- a/crates/buzz-cli/src/commands/sidebar.rs +++ b/crates/buzz-cli/src/commands/sidebar.rs @@ -204,6 +204,7 @@ mod tests { use super::super::sidebar_sync::now; use super::super::sidebar_sync::test_support::{self, Shared}; use super::*; + use crate::credentials::Credentials; use nostr::Keys; async fn setup() -> (BuzzClient, Shared) { @@ -387,7 +388,8 @@ mod tests { assert_eq!(list["sections"][0]["name"], json!(winner)); // Another author's head is never read as ours. - let other = BuzzClient::new(String::new(), Keys::generate(), None, None).unwrap(); + let other = + BuzzClient::new(String::new(), Credentials::from_keys(Keys::generate())).unwrap(); seed_at(&other, &relay, &doc_with_sections(&["Theirs"]), future + 5); // An uppercase channel id is stored in desktop's canonical form. diff --git a/crates/buzz-cli/src/commands/sidebar_flags.rs b/crates/buzz-cli/src/commands/sidebar_flags.rs index e216b92408..796ec94eb8 100644 --- a/crates/buzz-cli/src/commands/sidebar_flags.rs +++ b/crates/buzz-cli/src/commands/sidebar_flags.rs @@ -319,6 +319,7 @@ mod tests { published, published_count, seed_at, setup, Shared, }; use super::*; + use crate::credentials::Credentials; use nostr::Keys; const C1: &str = "11111111-1111-4111-8111-111111111111"; @@ -468,7 +469,8 @@ mod tests { "{refused}" ); let (client, relay) = setup().await; - let other = BuzzClient::new(String::new(), Keys::generate(), None, None).unwrap(); + let other = + BuzzClient::new(String::new(), Credentials::from_keys(Keys::generate())).unwrap(); seed(&other, &relay, &MUTES, json!({"version": 1}), 10); relay.lock().unwrap().leaky = true; let refused = run(&MUTES, set(&[C1], true), &client).await.unwrap_err(); @@ -480,7 +482,8 @@ mod tests { // Another author's head is never read as ours. let (client, relay) = setup().await; - let other = BuzzClient::new(String::new(), Keys::generate(), None, None).unwrap(); + let other = + BuzzClient::new(String::new(), Credentials::from_keys(Keys::generate())).unwrap(); seed( &other, &relay, diff --git a/crates/buzz-cli/src/commands/sidebar_sort.rs b/crates/buzz-cli/src/commands/sidebar_sort.rs index 122d76a14a..13635987eb 100644 --- a/crates/buzz-cli/src/commands/sidebar_sort.rs +++ b/crates/buzz-cli/src/commands/sidebar_sort.rs @@ -389,7 +389,11 @@ mod tests { assert_eq!(doc["meta"]["g"]["dms"][0], json!(far_v + 1)); // Another author's head is never read as ours. - let other = BuzzClient::new(String::new(), nostr::Keys::generate(), None, None).unwrap(); + let other = BuzzClient::new( + String::new(), + crate::credentials::Credentials::from_keys(nostr::Keys::generate()), + ) + .unwrap(); seed_sort( &other, &relay, diff --git a/crates/buzz-cli/src/commands/sidebar_sync.rs b/crates/buzz-cli/src/commands/sidebar_sync.rs index f0bb9faba5..07544e2509 100644 --- a/crates/buzz-cli/src/commands/sidebar_sync.rs +++ b/crates/buzz-cli/src/commands/sidebar_sync.rs @@ -4,7 +4,6 @@ use std::time::SystemTime; use buzz_core::kind::KIND_READ_STATE; -use nostr::nips::nip44; use nostr::{EventBuilder, Kind, Tag, Timestamp}; use serde_json::{json, Value}; @@ -58,7 +57,7 @@ pub(crate) async fn fetch_head( lane: Lane, decode: impl FnOnce(&Value, u64) -> Option, ) -> Result, CliError> { - let me = client.keys().public_key(); + let me = client.public_key(); let filter = json!({ "kinds": [KIND_READ_STATE], "authors": [me.to_hex()], @@ -103,8 +102,11 @@ pub(crate) async fn fetch_head( event.id.to_hex() )) }; - let plaintext = nip44::decrypt(client.keys().secret_key(), &me, &event.content) - .map_err(|e| unreadable(e.to_string()))?; + let plaintext = client + .credentials() + .nip44_decrypt(&me, &event.content) + .await? + .ok_or_else(|| unreadable("not valid ciphertext for this key".into()))?; let json: Value = serde_json::from_str(&plaintext).map_err(|e| unreadable(e.to_string()))?; let created_at = event.created_at.as_secs(); let doc = decode(&json, created_at).ok_or_else(|| { @@ -125,7 +127,7 @@ pub(crate) async fn publish( plaintext: String, head_created_at: u64, ) -> Result { - let keys = client.keys(); + let me = client.public_key(); let too_large = || { CliError::Usage(format!( "{} would exceed the relay's event size limit", @@ -136,13 +138,11 @@ pub(crate) async fn publish( if plaintext.len() > u16::MAX as usize { return Err(too_large()); } - let content = nip44::encrypt( - keys.secret_key(), - &keys.public_key(), - plaintext, - nip44::Version::V2, - ) - .map_err(|e| CliError::Other(format!("failed to encrypt {}: {e}", lane.label)))?; + let content = client + .credentials() + .nip44_encrypt(&me, &plaintext) + .await + .map_err(|e| CliError::Other(format!("failed to encrypt {}: {e}", lane.label)))?; if content.len() > MAX_CIPHERTEXT_BYTES { return Err(too_large()); } @@ -150,7 +150,9 @@ pub(crate) async fn publish( let builder = EventBuilder::new(Kind::Custom(KIND_READ_STATE as u16), content) .tags([Tag::identifier(lane.d_tag), Tag::hashtag(lane.d_tag)]) .custom_created_at(Timestamp::from(created_at)); - let raw = client.submit_event(client.sign_event(builder)?).await?; + let raw = client + .submit_event(client.sign_event(builder).await?) + .await?; let normalized = parse_write_response( &raw, &format!("a newer {} update won; run the command again", lane.label), @@ -210,7 +212,9 @@ pub(crate) fn unchanged() -> Value { #[cfg(test)] pub(crate) mod test_support { use super::*; + use crate::credentials::Credentials; use axum::{body::Bytes, extract::State, routing::post, Json, Router}; + use nostr::nips::nip44; use nostr::Keys; use std::sync::{Arc, Mutex}; use tokio::net::TcpListener; @@ -279,7 +283,7 @@ pub(crate) mod test_support { pub async fn setup() -> (BuzzClient, Shared) { let relay = Shared::default(); let url = fake_relay(relay.clone()).await; - let client = BuzzClient::new(url, Keys::generate(), None, None).unwrap(); + let client = BuzzClient::new(url, Credentials::from_keys(Keys::generate())).unwrap(); (client, relay) } @@ -290,7 +294,7 @@ pub(crate) mod test_support { plaintext: &str, created_at: u64, ) -> Value { - let keys = client.keys(); + let keys = client.credentials().test_keys(); let content = nip44::encrypt( keys.secret_key(), &keys.public_key(), @@ -301,7 +305,7 @@ pub(crate) mod test_support { let event = EventBuilder::new(Kind::Custom(KIND_READ_STATE as u16), content) .tags([Tag::identifier(lane.d_tag)]) .custom_created_at(Timestamp::from(created_at)) - .sign_with_keys(keys) + .sign_with_keys(&keys) .unwrap(); let event = serde_json::to_value(event).unwrap(); relay.lock().unwrap().events.push(event.clone()); @@ -312,7 +316,7 @@ pub(crate) mod test_support { pub fn published(client: &BuzzClient, relay: &Shared) -> (nostr::Event, Value) { let raw = relay.lock().unwrap().events.last().cloned().unwrap(); let event: nostr::Event = serde_json::from_value(raw).unwrap(); - let keys = client.keys(); + let keys = client.credentials().test_keys(); let plaintext = nip44::decrypt(keys.secret_key(), &keys.public_key(), &event.content).unwrap(); (event, serde_json::from_str(&plaintext).unwrap())