mirror of
https://github.com/block/buzz.git
synced 2026-11-08 02:11:36 +00:00
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 <noreply@anthropic.com> Signed-off-by: Blake McAnally <bmcanally@squareup.com>
This commit is contained in:
@@ -204,6 +204,7 @@ mod tests {
|
|||||||
use super::super::sidebar_sync::now;
|
use super::super::sidebar_sync::now;
|
||||||
use super::super::sidebar_sync::test_support::{self, Shared};
|
use super::super::sidebar_sync::test_support::{self, Shared};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::credentials::Credentials;
|
||||||
use nostr::Keys;
|
use nostr::Keys;
|
||||||
|
|
||||||
async fn setup() -> (BuzzClient, Shared) {
|
async fn setup() -> (BuzzClient, Shared) {
|
||||||
@@ -387,7 +388,8 @@ mod tests {
|
|||||||
assert_eq!(list["sections"][0]["name"], json!(winner));
|
assert_eq!(list["sections"][0]["name"], json!(winner));
|
||||||
|
|
||||||
// Another author's head is never read as ours.
|
// 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);
|
seed_at(&other, &relay, &doc_with_sections(&["Theirs"]), future + 5);
|
||||||
|
|
||||||
// An uppercase channel id is stored in desktop's canonical form.
|
// An uppercase channel id is stored in desktop's canonical form.
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ mod tests {
|
|||||||
published, published_count, seed_at, setup, Shared,
|
published, published_count, seed_at, setup, Shared,
|
||||||
};
|
};
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::credentials::Credentials;
|
||||||
use nostr::Keys;
|
use nostr::Keys;
|
||||||
|
|
||||||
const C1: &str = "11111111-1111-4111-8111-111111111111";
|
const C1: &str = "11111111-1111-4111-8111-111111111111";
|
||||||
@@ -468,7 +469,8 @@ mod tests {
|
|||||||
"{refused}"
|
"{refused}"
|
||||||
);
|
);
|
||||||
let (client, relay) = setup().await;
|
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);
|
seed(&other, &relay, &MUTES, json!({"version": 1}), 10);
|
||||||
relay.lock().unwrap().leaky = true;
|
relay.lock().unwrap().leaky = true;
|
||||||
let refused = run(&MUTES, set(&[C1], true), &client).await.unwrap_err();
|
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.
|
// Another author's head is never read as ours.
|
||||||
let (client, relay) = setup().await;
|
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(
|
seed(
|
||||||
&other,
|
&other,
|
||||||
&relay,
|
&relay,
|
||||||
|
|||||||
@@ -389,7 +389,11 @@ mod tests {
|
|||||||
assert_eq!(doc["meta"]["g"]["dms"][0], json!(far_v + 1));
|
assert_eq!(doc["meta"]["g"]["dms"][0], json!(far_v + 1));
|
||||||
|
|
||||||
// Another author's head is never read as ours.
|
// 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(
|
seed_sort(
|
||||||
&other,
|
&other,
|
||||||
&relay,
|
&relay,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use buzz_core::kind::KIND_READ_STATE;
|
use buzz_core::kind::KIND_READ_STATE;
|
||||||
use nostr::nips::nip44;
|
|
||||||
use nostr::{EventBuilder, Kind, Tag, Timestamp};
|
use nostr::{EventBuilder, Kind, Tag, Timestamp};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ pub(crate) async fn fetch_head<T>(
|
|||||||
lane: Lane,
|
lane: Lane,
|
||||||
decode: impl FnOnce(&Value, u64) -> Option<T>,
|
decode: impl FnOnce(&Value, u64) -> Option<T>,
|
||||||
) -> Result<Head<T>, CliError> {
|
) -> Result<Head<T>, CliError> {
|
||||||
let me = client.keys().public_key();
|
let me = client.public_key();
|
||||||
let filter = json!({
|
let filter = json!({
|
||||||
"kinds": [KIND_READ_STATE],
|
"kinds": [KIND_READ_STATE],
|
||||||
"authors": [me.to_hex()],
|
"authors": [me.to_hex()],
|
||||||
@@ -103,8 +102,11 @@ pub(crate) async fn fetch_head<T>(
|
|||||||
event.id.to_hex()
|
event.id.to_hex()
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
let plaintext = nip44::decrypt(client.keys().secret_key(), &me, &event.content)
|
let plaintext = client
|
||||||
.map_err(|e| unreadable(e.to_string()))?;
|
.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 json: Value = serde_json::from_str(&plaintext).map_err(|e| unreadable(e.to_string()))?;
|
||||||
let created_at = event.created_at.as_secs();
|
let created_at = event.created_at.as_secs();
|
||||||
let doc = decode(&json, created_at).ok_or_else(|| {
|
let doc = decode(&json, created_at).ok_or_else(|| {
|
||||||
@@ -125,7 +127,7 @@ pub(crate) async fn publish(
|
|||||||
plaintext: String,
|
plaintext: String,
|
||||||
head_created_at: u64,
|
head_created_at: u64,
|
||||||
) -> Result<Value, CliError> {
|
) -> Result<Value, CliError> {
|
||||||
let keys = client.keys();
|
let me = client.public_key();
|
||||||
let too_large = || {
|
let too_large = || {
|
||||||
CliError::Usage(format!(
|
CliError::Usage(format!(
|
||||||
"{} would exceed the relay's event size limit",
|
"{} would exceed the relay's event size limit",
|
||||||
@@ -136,13 +138,11 @@ pub(crate) async fn publish(
|
|||||||
if plaintext.len() > u16::MAX as usize {
|
if plaintext.len() > u16::MAX as usize {
|
||||||
return Err(too_large());
|
return Err(too_large());
|
||||||
}
|
}
|
||||||
let content = nip44::encrypt(
|
let content = client
|
||||||
keys.secret_key(),
|
.credentials()
|
||||||
&keys.public_key(),
|
.nip44_encrypt(&me, &plaintext)
|
||||||
plaintext,
|
.await
|
||||||
nip44::Version::V2,
|
.map_err(|e| CliError::Other(format!("failed to encrypt {}: {e}", lane.label)))?;
|
||||||
)
|
|
||||||
.map_err(|e| CliError::Other(format!("failed to encrypt {}: {e}", lane.label)))?;
|
|
||||||
if content.len() > MAX_CIPHERTEXT_BYTES {
|
if content.len() > MAX_CIPHERTEXT_BYTES {
|
||||||
return Err(too_large());
|
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)
|
let builder = EventBuilder::new(Kind::Custom(KIND_READ_STATE as u16), content)
|
||||||
.tags([Tag::identifier(lane.d_tag), Tag::hashtag(lane.d_tag)])
|
.tags([Tag::identifier(lane.d_tag), Tag::hashtag(lane.d_tag)])
|
||||||
.custom_created_at(Timestamp::from(created_at));
|
.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(
|
let normalized = parse_write_response(
|
||||||
&raw,
|
&raw,
|
||||||
&format!("a newer {} update won; run the command again", lane.label),
|
&format!("a newer {} update won; run the command again", lane.label),
|
||||||
@@ -210,7 +212,9 @@ pub(crate) fn unchanged() -> Value {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod test_support {
|
pub(crate) mod test_support {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::credentials::Credentials;
|
||||||
use axum::{body::Bytes, extract::State, routing::post, Json, Router};
|
use axum::{body::Bytes, extract::State, routing::post, Json, Router};
|
||||||
|
use nostr::nips::nip44;
|
||||||
use nostr::Keys;
|
use nostr::Keys;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
@@ -279,7 +283,7 @@ pub(crate) mod test_support {
|
|||||||
pub async fn setup() -> (BuzzClient, Shared) {
|
pub async fn setup() -> (BuzzClient, Shared) {
|
||||||
let relay = Shared::default();
|
let relay = Shared::default();
|
||||||
let url = fake_relay(relay.clone()).await;
|
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)
|
(client, relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +294,7 @@ pub(crate) mod test_support {
|
|||||||
plaintext: &str,
|
plaintext: &str,
|
||||||
created_at: u64,
|
created_at: u64,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let keys = client.keys();
|
let keys = client.credentials().test_keys();
|
||||||
let content = nip44::encrypt(
|
let content = nip44::encrypt(
|
||||||
keys.secret_key(),
|
keys.secret_key(),
|
||||||
&keys.public_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)
|
let event = EventBuilder::new(Kind::Custom(KIND_READ_STATE as u16), content)
|
||||||
.tags([Tag::identifier(lane.d_tag)])
|
.tags([Tag::identifier(lane.d_tag)])
|
||||||
.custom_created_at(Timestamp::from(created_at))
|
.custom_created_at(Timestamp::from(created_at))
|
||||||
.sign_with_keys(keys)
|
.sign_with_keys(&keys)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let event = serde_json::to_value(event).unwrap();
|
let event = serde_json::to_value(event).unwrap();
|
||||||
relay.lock().unwrap().events.push(event.clone());
|
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) {
|
pub fn published(client: &BuzzClient, relay: &Shared) -> (nostr::Event, Value) {
|
||||||
let raw = relay.lock().unwrap().events.last().cloned().unwrap();
|
let raw = relay.lock().unwrap().events.last().cloned().unwrap();
|
||||||
let event: nostr::Event = serde_json::from_value(raw).unwrap();
|
let event: nostr::Event = serde_json::from_value(raw).unwrap();
|
||||||
let keys = client.keys();
|
let keys = client.credentials().test_keys();
|
||||||
let plaintext =
|
let plaintext =
|
||||||
nip44::decrypt(keys.secret_key(), &keys.public_key(), &event.content).unwrap();
|
nip44::decrypt(keys.secret_key(), &keys.public_key(), &event.content).unwrap();
|
||||||
(event, serde_json::from_str(&plaintext).unwrap())
|
(event, serde_json::from_str(&plaintext).unwrap())
|
||||||
|
|||||||
Reference in New Issue
Block a user