implement nip4e interop tags
This commit is contained in:
+293
-24
@@ -474,7 +474,7 @@ impl DeviceRegistry {
|
|||||||
})?;
|
})?;
|
||||||
} else {
|
} else {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.request(cx);
|
this.request(Some(device_pubkey), cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -502,7 +502,11 @@ impl DeviceRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Request encryption keys from other device
|
/// Request encryption keys from other device
|
||||||
pub(crate) fn request(&mut self, cx: &mut Context<Self>) {
|
pub(crate) fn request(
|
||||||
|
&mut self,
|
||||||
|
expected_encryption: Option<PublicKey>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
@@ -529,9 +533,26 @@ impl DeviceRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No usable approval event found, construct a request event
|
// No usable approval event found, construct a request event.
|
||||||
|
//
|
||||||
|
// The `pubkey` tag is a compatibility alias for the draft's `P`,
|
||||||
|
// `relay` hints tell other devices where the response is accepted,
|
||||||
|
// `n` names the exact key this device expects to be transferred
|
||||||
|
let mut tags = vec![
|
||||||
|
Tag::custom("P", vec![app_pubkey]),
|
||||||
|
Tag::custom("pubkey", vec![app_pubkey]),
|
||||||
|
];
|
||||||
|
|
||||||
|
for url in write_relay_hints(&client, public_key).await? {
|
||||||
|
tags.push(Tag::custom("relay", vec![url.to_string()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expected) = expected_encryption {
|
||||||
|
tags.push(Tag::custom("n", vec![expected]));
|
||||||
|
}
|
||||||
|
|
||||||
let event = EventBuilder::new(Kind::Custom(4454), "")
|
let event = EventBuilder::new(Kind::Custom(4454), "")
|
||||||
.tags(vec![Tag::custom("P", vec![app_pubkey])])
|
.tags(tags)
|
||||||
.finalize_async(&signer)
|
.finalize_async(&signer)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -692,7 +713,6 @@ impl DeviceRegistry {
|
|||||||
let client = nostr.read(cx).client();
|
let client = nostr.read(cx).client();
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
|
|
||||||
// Get user's write relays
|
|
||||||
let event = event.clone();
|
let event = event.clone();
|
||||||
let request_id = event.id;
|
let request_id = event.id;
|
||||||
|
|
||||||
@@ -700,10 +720,20 @@ impl DeviceRegistry {
|
|||||||
// Get device keys
|
// Get device keys
|
||||||
let keys = get_keys(&client, &signer).await?;
|
let keys = get_keys(&client, &signer).await?;
|
||||||
let secret = keys.secret_key().to_secret_hex();
|
let secret = keys.secret_key().to_secret_hex();
|
||||||
|
let identity = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
// Extract the target public key from the event tags
|
// Extract the target public key from the event tags
|
||||||
let target =
|
let target =
|
||||||
client_pubkey_from_request(&event).context("Target is not a valid public key")?;
|
client_pubkey_from_event(&event).context("Target is not a valid public key")?;
|
||||||
|
|
||||||
|
// The requester may pin the exact key it expects
|
||||||
|
if let Some(requested) = requested_encryption_key(&event)
|
||||||
|
&& requested != keys.public_key()
|
||||||
|
{
|
||||||
|
return Err(anyhow!(
|
||||||
|
"The requesting device expects an encryption key this device no longer holds"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Encrypt to a fresh one-time keypair so the transfer event
|
// Encrypt to a fresh one-time keypair so the transfer event
|
||||||
// can never be decrypted with the long-lived encryption key.
|
// can never be decrypted with the long-lived encryption key.
|
||||||
@@ -713,17 +743,28 @@ impl DeviceRegistry {
|
|||||||
// Construct the response event
|
// Construct the response event
|
||||||
//
|
//
|
||||||
// P tag: the one-time sender key used for the encryption
|
// P tag: the one-time sender key used for the encryption
|
||||||
|
// p tag: the account identity, for inbox routing
|
||||||
// p tag: the requester's client key
|
// p tag: the requester's client key
|
||||||
let event = EventBuilder::new(Kind::Custom(4455), payload)
|
let response = EventBuilder::new(Kind::Custom(4455), payload)
|
||||||
.tags(vec![
|
.tags(vec![
|
||||||
Tag::custom("P", vec![sender.public_key().to_hex()]),
|
Tag::custom("P", vec![sender.public_key().to_hex()]),
|
||||||
|
Tag::public_key(identity),
|
||||||
Tag::public_key(target),
|
Tag::public_key(target),
|
||||||
])
|
])
|
||||||
.finalize_async(&signer)
|
.finalize_async(&signer)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Send the response event to the user's relay list
|
let relays = merge_relays(
|
||||||
client.send_event(&event).to_nip65().await?;
|
write_relay_hints(&client, identity).await?,
|
||||||
|
request_relay_hints(&event),
|
||||||
|
);
|
||||||
|
|
||||||
|
let send = client.send_event(&response);
|
||||||
|
if relays.is_empty() {
|
||||||
|
send.to_nip65().await?;
|
||||||
|
} else {
|
||||||
|
send.to(relays).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -761,7 +802,7 @@ impl DeviceRegistry {
|
|||||||
|
|
||||||
/// Handle encryption request
|
/// Handle encryption request
|
||||||
fn ask_for_approval(&mut self, event: Event, cx: &mut Context<Self>) {
|
fn ask_for_approval(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||||
let Some(client_key) = client_pubkey_from_request(&event) else {
|
let Some(client_key) = client_pubkey_from_event(&event) else {
|
||||||
log::warn!("Ignoring invalid encryption key request: no valid requester key");
|
log::warn!("Ignoring invalid encryption key request: no valid requester key");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -848,13 +889,16 @@ impl DeviceRegistry {
|
|||||||
device.";
|
device.";
|
||||||
|
|
||||||
let requester =
|
let requester =
|
||||||
client_pubkey_from_request(&event).context("Request has no valid requester key")?;
|
client_pubkey_from_event(&event).context("Request has no valid requester key")?;
|
||||||
|
|
||||||
let code = authorization_code(&requester);
|
let code = authorization_code(&requester);
|
||||||
let directory = self.directory.clone();
|
|
||||||
directory.update(cx, |directory, cx| directory.track([requester], cx));
|
|
||||||
let code_label = code.clone();
|
let code_label = code.clone();
|
||||||
|
|
||||||
|
let directory = self.directory.clone();
|
||||||
|
directory.update(cx, |directory, cx| {
|
||||||
|
directory.track([requester], cx);
|
||||||
|
});
|
||||||
|
|
||||||
let entity = cx.entity().downgrade();
|
let entity = cx.entity().downgrade();
|
||||||
let loading = Rc::new(Cell::new(false));
|
let loading = Rc::new(Cell::new(false));
|
||||||
|
|
||||||
@@ -982,14 +1026,23 @@ fn parse_processed_requests(bytes: &[u8]) -> VecDeque<EventId> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract the requester client key from a key request's `P` tag
|
/// Extract the client public key from an event.
|
||||||
fn client_pubkey_from_request(event: &Event) -> Option<PublicKey> {
|
fn client_pubkey_from_event(event: &Event) -> Option<PublicKey> {
|
||||||
event
|
let key = event
|
||||||
.tags
|
.tags
|
||||||
.iter()
|
.iter()
|
||||||
.find(|tag| tag.kind() == "P")
|
.find(|tag| tag.kind() == "P")
|
||||||
.and_then(|tag| tag.content())
|
.and_then(|tag| tag.content())
|
||||||
.and_then(|content| PublicKey::parse(content).ok())
|
.and_then(|content| PublicKey::parse(content).ok());
|
||||||
|
|
||||||
|
key.or_else(|| {
|
||||||
|
event
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.find(|tag| tag.kind() == "pubkey")
|
||||||
|
.and_then(|tag| tag.content())
|
||||||
|
.and_then(|content| PublicKey::parse(content).ok())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the transfer event is p-tagged to the given client key
|
/// Whether the transfer event is p-tagged to the given client key
|
||||||
@@ -1003,6 +1056,57 @@ fn transfer_addresses(event: &Event, client_key: PublicKey) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse the `relay` hints attached to a key request.
|
||||||
|
fn request_relay_hints(event: &Event) -> Vec<RelayUrl> {
|
||||||
|
event
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.filter(|tag| tag.kind() == "relay")
|
||||||
|
.filter_map(|tag| {
|
||||||
|
tag.content()
|
||||||
|
.and_then(|content| RelayUrl::parse(content).ok())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the optional `n` tag of a key request.
|
||||||
|
fn requested_encryption_key(event: &Event) -> Option<PublicKey> {
|
||||||
|
event
|
||||||
|
.tags
|
||||||
|
.iter()
|
||||||
|
.find(|tag| tag.kind() == "n")
|
||||||
|
.and_then(|tag| tag.content())
|
||||||
|
.and_then(|content| PublicKey::parse(content).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect the user's NIP-65 write relays.
|
||||||
|
async fn write_relay_hints(client: &Client, identity: PublicKey) -> Result<Vec<RelayUrl>, Error> {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::RelayList)
|
||||||
|
.author(identity)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let Some(event) = client.database().query(filter).await?.into_iter().next() else {
|
||||||
|
return Ok(vec![]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unmarked relays serve both reads and writes
|
||||||
|
Ok(nip65::extract_relay_list(&event)
|
||||||
|
.filter(|(_, metadata)| *metadata != Some(RelayMetadata::Read))
|
||||||
|
.map(|(url, _)| url)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge relay lists, dropping duplicates
|
||||||
|
fn merge_relays(first: Vec<RelayUrl>, second: Vec<RelayUrl>) -> Vec<RelayUrl> {
|
||||||
|
first
|
||||||
|
.into_iter()
|
||||||
|
.chain(second)
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// The first eight hexadecimal characters of the client public key.
|
/// The first eight hexadecimal characters of the client public key.
|
||||||
fn authorization_code(public_key: &PublicKey) -> SharedString {
|
fn authorization_code(public_key: &PublicKey) -> SharedString {
|
||||||
let hex = public_key.to_hex();
|
let hex = public_key.to_hex();
|
||||||
@@ -1017,13 +1121,7 @@ async fn decrypt_approval(
|
|||||||
identity: PublicKey,
|
identity: PublicKey,
|
||||||
event: &Event,
|
event: &Event,
|
||||||
) -> Result<Keys, Error> {
|
) -> Result<Keys, Error> {
|
||||||
let master = event
|
let master = client_pubkey_from_event(event).context("Invalid event's tags")?;
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.find(|tag| tag.kind() == "P")
|
|
||||||
.and_then(|tag| tag.content())
|
|
||||||
.and_then(|content| PublicKey::parse(content).ok())
|
|
||||||
.context("Invalid event's tags")?;
|
|
||||||
|
|
||||||
let decrypted = app_keys
|
let decrypted = app_keys
|
||||||
.nip44_decrypt_async(&master, event.content.as_str())
|
.nip44_decrypt_async(&master, event.content.as_str())
|
||||||
@@ -1127,3 +1225,174 @@ where
|
|||||||
Err(anyhow!("Key not found"))
|
Err(anyhow!("Key not found"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Compose a minimal event whose validity doesn't matter for the helpers
|
||||||
|
fn event_with_tags(kind: Kind, tags: Vec<Tag>) -> Event {
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let public_key = keys.public_key();
|
||||||
|
let created_at = Timestamp::now();
|
||||||
|
let tags = Tags::from_list(tags);
|
||||||
|
let id = EventId::compute(&public_key, &created_at, &kind, &tags, "");
|
||||||
|
|
||||||
|
Event::new(
|
||||||
|
id,
|
||||||
|
public_key,
|
||||||
|
created_at,
|
||||||
|
kind,
|
||||||
|
tags,
|
||||||
|
"",
|
||||||
|
Signature::from_slice(&[0u8; 64]).expect("signature should parse"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_processed_requests_roundtrip() {
|
||||||
|
let ids: Vec<EventId> = (0u8..4)
|
||||||
|
.map(|i| {
|
||||||
|
let mut bytes = [0u8; 32];
|
||||||
|
bytes[0] = i;
|
||||||
|
EventId::from_byte_array(bytes)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let bytes: Vec<u8> = ids.iter().flat_map(|id| id.to_hex().into_bytes()).collect();
|
||||||
|
let parsed = parse_processed_requests(&bytes);
|
||||||
|
|
||||||
|
assert_eq!(parsed.len(), 4);
|
||||||
|
assert!(ids.iter().all(|id| parsed.contains(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_processed_requests_ignores_garbage() {
|
||||||
|
assert!(parse_processed_requests(&[]).is_empty());
|
||||||
|
assert!(parse_processed_requests(b"not hex at all").is_empty());
|
||||||
|
assert!(parse_processed_requests(&[b'a'; 63]).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_pubkey_from_event_reads_p_tag() {
|
||||||
|
let client_key = Keys::generate().public_key();
|
||||||
|
let event = event_with_tags(
|
||||||
|
Kind::Custom(4454),
|
||||||
|
vec![Tag::custom("P", vec![client_key.to_hex()])],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(client_pubkey_from_event(&event), Some(client_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_pubkey_from_event_rejects_missing_or_invalid() {
|
||||||
|
let event = event_with_tags(Kind::Custom(4454), vec![]);
|
||||||
|
assert_eq!(client_pubkey_from_event(&event), None);
|
||||||
|
|
||||||
|
let event = event_with_tags(Kind::Custom(4454), vec![Tag::custom("P", vec!["nope"])]);
|
||||||
|
assert_eq!(client_pubkey_from_event(&event), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_pubkey_from_event_falls_back_to_alias() {
|
||||||
|
let client_key = Keys::generate().public_key();
|
||||||
|
|
||||||
|
let event = event_with_tags(
|
||||||
|
Kind::Custom(4454),
|
||||||
|
vec![Tag::custom("pubkey", vec![client_key.to_hex()])],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(client_pubkey_from_event(&event), Some(client_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_pubkey_from_event_prefers_p_tag() {
|
||||||
|
let client_key = Keys::generate().public_key();
|
||||||
|
let alias = Keys::generate().public_key();
|
||||||
|
|
||||||
|
let event = event_with_tags(
|
||||||
|
Kind::Custom(4454),
|
||||||
|
vec![
|
||||||
|
Tag::custom("P", vec![client_key.to_hex()]),
|
||||||
|
Tag::custom("pubkey", vec![alias.to_hex()]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(client_pubkey_from_event(&event), Some(client_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transfer_addresses_matches_client_key_p_tag() {
|
||||||
|
let client_key = Keys::generate().public_key();
|
||||||
|
let other = Keys::generate().public_key();
|
||||||
|
let identity = Keys::generate().public_key();
|
||||||
|
|
||||||
|
let event = event_with_tags(
|
||||||
|
Kind::Custom(4455),
|
||||||
|
vec![
|
||||||
|
Tag::custom("P", vec![other.to_hex()]),
|
||||||
|
Tag::public_key(client_key),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(transfer_addresses(&event, client_key));
|
||||||
|
assert!(!transfer_addresses(&event, other));
|
||||||
|
assert!(!transfer_addresses(&event, identity));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transfer_addresses_ignores_uppercase_p_tag() {
|
||||||
|
let client_key = Keys::generate().public_key();
|
||||||
|
|
||||||
|
let event = event_with_tags(
|
||||||
|
Kind::Custom(4455),
|
||||||
|
vec![Tag::custom("P", vec![client_key.to_hex()])],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!transfer_addresses(&event, client_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_relay_hints_parses_and_drops_invalid() {
|
||||||
|
let valid = RelayUrl::parse("wss://relay.example.com").expect("relay should parse");
|
||||||
|
|
||||||
|
let event = event_with_tags(
|
||||||
|
Kind::Custom(4454),
|
||||||
|
vec![
|
||||||
|
Tag::custom("relay", vec!["wss://relay.example.com"]),
|
||||||
|
Tag::custom("relay", vec!["not a relay"]),
|
||||||
|
Tag::custom("relay", vec!["wss://other.example.com"]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let hints = request_relay_hints(&event);
|
||||||
|
assert_eq!(hints.len(), 2);
|
||||||
|
assert_eq!(hints[0], valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requested_encryption_key_reads_n_tag() {
|
||||||
|
let encryption_key = Keys::generate().public_key();
|
||||||
|
|
||||||
|
let event = event_with_tags(
|
||||||
|
Kind::Custom(4454),
|
||||||
|
vec![Tag::custom("n", vec![encryption_key.to_hex()])],
|
||||||
|
);
|
||||||
|
assert_eq!(requested_encryption_key(&event), Some(encryption_key));
|
||||||
|
|
||||||
|
let event = event_with_tags(Kind::Custom(4454), vec![]);
|
||||||
|
assert_eq!(requested_encryption_key(&event), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn merge_relays_deduplicates() {
|
||||||
|
let a = RelayUrl::parse("wss://a.example.com").expect("relay should parse");
|
||||||
|
let b = RelayUrl::parse("wss://b.example.com").expect("relay should parse");
|
||||||
|
|
||||||
|
let merged = merge_relays(vec![a.clone(), b.clone()], vec![a.clone()]);
|
||||||
|
|
||||||
|
assert_eq!(merged.len(), 2);
|
||||||
|
assert!(merged.contains(&a));
|
||||||
|
assert!(merged.contains(&b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ impl EventEmitter<PersonEvent> for PersonRegistry {}
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Dispatch {
|
enum Dispatch {
|
||||||
Person(Person),
|
Person(Box<Person>),
|
||||||
Announcement(Event),
|
Announcement(Event),
|
||||||
Relays(Event),
|
Relays(Event),
|
||||||
Emojis(Event),
|
Emojis(Event),
|
||||||
@@ -111,7 +111,7 @@ impl PersonRegistry {
|
|||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
match event {
|
match event {
|
||||||
Dispatch::Person(person) => {
|
Dispatch::Person(person) => {
|
||||||
this.insert(person, cx);
|
this.insert(*person, cx);
|
||||||
}
|
}
|
||||||
Dispatch::Announcement(event) => {
|
Dispatch::Announcement(event) => {
|
||||||
this.set_announcement(&event, cx);
|
this.set_announcement(&event, cx);
|
||||||
@@ -175,7 +175,11 @@ impl PersonRegistry {
|
|||||||
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||||
let person = Person::new(event.pubkey, metadata);
|
let person = Person::new(event.pubkey, metadata);
|
||||||
|
|
||||||
if tx.send_async(Dispatch::Person(person)).await.is_err() {
|
if tx
|
||||||
|
.send_async(Dispatch::Person(Box::new(person)))
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
log::warn!("Channel closed, dropping metadata event");
|
log::warn!("Channel closed, dropping metadata event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ impl Person {
|
|||||||
|
|
||||||
/// Set profile encryption keys announcement
|
/// Set profile encryption keys announcement
|
||||||
pub(crate) fn set_announcement(&mut self, announcement: Announcement) {
|
pub(crate) fn set_announcement(&mut self, announcement: Announcement) {
|
||||||
|
// A delayed or repeated announcement must not roll the key back
|
||||||
|
if let Some(current) = &self.announcement
|
||||||
|
&& !announcement.is_newer_than(current)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.announcement = Some(announcement);
|
self.announcement = Some(announcement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ use nostr_sdk::prelude::*;
|
|||||||
pub struct Announcement {
|
pub struct Announcement {
|
||||||
/// The public key of the device that created this announcement.
|
/// The public key of the device that created this announcement.
|
||||||
public_key: PublicKey,
|
public_key: PublicKey,
|
||||||
|
/// The id of the announcement event, for ordering
|
||||||
|
event_id: EventId,
|
||||||
|
/// The creation time of the announcement event
|
||||||
|
created_at: Timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Announcement {
|
impl Announcement {
|
||||||
@@ -16,11 +20,20 @@ impl Announcement {
|
|||||||
.and_then(|tag| tag.content())
|
.and_then(|tag| tag.content())
|
||||||
.and_then(|c| PublicKey::parse(c).ok())?;
|
.and_then(|c| PublicKey::parse(c).ok())?;
|
||||||
|
|
||||||
Some(Self { public_key })
|
Some(Self {
|
||||||
|
public_key,
|
||||||
|
event_id: event.id,
|
||||||
|
created_at: event.created_at,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the public key of the device that created this announcement.
|
/// Returns the public key of the device that created this announcement.
|
||||||
pub fn public_key(&self) -> PublicKey {
|
pub fn public_key(&self) -> PublicKey {
|
||||||
self.public_key
|
self.public_key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_newer_than(&self, other: &Self) -> bool {
|
||||||
|
self.created_at > other.created_at
|
||||||
|
|| (self.created_at == other.created_at && self.event_id < other.event_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,8 +89,9 @@ with a documented [client profile](https://github.com/CodyTseng/psstpsst/blob/ma
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Section A (items 1-5) is implemented in `crates/nip4e/src/lib.rs` and
|
### Section A (items 1-5) — implemented
|
||||||
`crates/state/src/credentials.rs`:
|
|
||||||
|
Implemented in `crates/nip4e/src/lib.rs` and `crates/state/src/credentials.rs`:
|
||||||
|
|
||||||
- Ephemeral per-request client key, held in memory for the lifetime of one
|
- Ephemeral per-request client key, held in memory for the lifetime of one
|
||||||
request (retries reuse it so an open approval sheet keeps targeting a key the
|
request (retries reuse it so an open approval sheet keeps targeting a key the
|
||||||
@@ -105,4 +106,29 @@ Section A (items 1-5) is implemented in `crates/nip4e/src/lib.rs` and
|
|||||||
- An open approval is withdrawn when a `4455` addressed to the same client key
|
- An open approval is withdrawn when a `4455` addressed to the same client key
|
||||||
arrives (another device already answered).
|
arrives (another device already answered).
|
||||||
|
|
||||||
Items 6-12 remain open.
|
### Section B (items 6-8) — implemented
|
||||||
|
|
||||||
|
- **Interop tags on `4454`** (`crates/nip4e/src/lib.rs`): requests now carry
|
||||||
|
the draft's `P` tag plus the `pubkey` compatibility alias, `relay` hints (the
|
||||||
|
requester's NIP-65 write relays), and an optional `n` tag pinning the exact
|
||||||
|
encryption key the requester observed in its announcement. Responder readers
|
||||||
|
accept `P` or `pubkey` on both `4454` and `4455`.
|
||||||
|
- **Responder honors the request** (`approve`): the identity `p` tag is added
|
||||||
|
to `4455` for account inbox routing; the response is published to the union
|
||||||
|
of the responder's NIP-65 write relays and the request's normalized `relay`
|
||||||
|
hints (each hint parsed as untrusted input), falling back to the NIP-65
|
||||||
|
policy when no relays are known; when the request pins an `n` key this device
|
||||||
|
no longer holds, the transfer is refused instead of sending a key the
|
||||||
|
requester would reject.
|
||||||
|
- **Peer announcement ordering** (`crates/state/src/nip4e.rs`,
|
||||||
|
`crates/person/src/person.rs`): `Announcement` now carries the event id and
|
||||||
|
creation time, and `is_newer_than` implements the psstpsst ordering (newer
|
||||||
|
`created_at` wins, ties go to the lexicographically smaller event id).
|
||||||
|
`Person::set_announcement` only replaces an existing announcement with a
|
||||||
|
newer one, so delayed or out-of-order `10044` events cannot roll a peer's
|
||||||
|
key back. Freshness remains subscription-based: peer `10044` is re-subscribed
|
||||||
|
whenever a room opens, over the connected relay set that includes
|
||||||
|
discovery/bootstrap relays — push-based and stronger than psstpsst's one-day
|
||||||
|
TTL polling, so no separate announcement cache was added.
|
||||||
|
|
||||||
|
Items 9-12 remain open.
|
||||||
|
|||||||
Reference in New Issue
Block a user