nip4e: fix message encryption and sync #57
+23
-22
@@ -39,7 +39,7 @@ pub enum DeviceEvent {
|
|||||||
/// User have not setup encryption key
|
/// User have not setup encryption key
|
||||||
NotSet,
|
NotSet,
|
||||||
/// The device is requesting an encryption key
|
/// The device is requesting an encryption key
|
||||||
Requesting,
|
Requesting(SharedString),
|
||||||
/// An error occurred
|
/// An error occurred
|
||||||
Error(SharedString),
|
Error(SharedString),
|
||||||
}
|
}
|
||||||
@@ -319,10 +319,7 @@ impl DeviceRegistry {
|
|||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
// Construct an announcement event
|
// Construct an announcement event
|
||||||
let event = EventBuilder::new(Kind::Custom(10044), "")
|
let event = EventBuilder::new(Kind::Custom(10044), "")
|
||||||
.tags(vec![
|
.tag(Tag::custom("n", vec![n]))
|
||||||
Tag::custom("n", vec![n]),
|
|
||||||
Tag::custom("client", vec![CLIENT_NAME]),
|
|
||||||
])
|
|
||||||
.finalize_async(&signer)
|
.finalize_async(&signer)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -433,10 +430,7 @@ impl DeviceRegistry {
|
|||||||
|
|
||||||
// No usable approval event found, construct a request event
|
// No usable approval event found, construct a request event
|
||||||
let event = EventBuilder::new(Kind::Custom(4454), "")
|
let event = EventBuilder::new(Kind::Custom(4454), "")
|
||||||
.tags(vec![
|
.tags(vec![Tag::custom("P", vec![app_pubkey])])
|
||||||
Tag::custom("P", vec![app_pubkey]),
|
|
||||||
Tag::custom("client", vec![CLIENT_NAME]),
|
|
||||||
])
|
|
||||||
.finalize_async(&signer)
|
.finalize_async(&signer)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -475,8 +469,6 @@ impl DeviceRegistry {
|
|||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let app_keys_task = get_or_init_app_keys(cx);
|
let app_keys_task = get_or_init_app_keys(cx);
|
||||||
|
|
||||||
cx.emit(DeviceEvent::Requesting);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
let app_keys = match app_keys_task.await {
|
let app_keys = match app_keys_task.await {
|
||||||
Ok(app_keys) => app_keys,
|
Ok(app_keys) => app_keys,
|
||||||
@@ -488,6 +480,12 @@ impl DeviceRegistry {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(DeviceEvent::Requesting(authorization_code(
|
||||||
|
&app_keys.public_key(),
|
||||||
|
)));
|
||||||
|
})?;
|
||||||
|
|
||||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
let public_key = signer.get_public_key_async().await?;
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
|
||||||
@@ -694,7 +692,9 @@ impl DeviceRegistry {
|
|||||||
/// Build a notification for the encryption request.
|
/// Build a notification for the encryption request.
|
||||||
fn notification(&self, event: Event, cx: &Context<Self>) -> Result<Notification, Error> {
|
fn notification(&self, event: Event, cx: &Context<Self>) -> Result<Notification, Error> {
|
||||||
const MSG: &str = "You've requested an encryption key from another device. \
|
const MSG: &str = "You've requested an encryption key from another device. \
|
||||||
Approve to allow Coop to share with it.";
|
Approve to allow Coop to share with it. Before approving, \
|
||||||
|
compare the code with the one shown on the requesting \
|
||||||
|
device.";
|
||||||
|
|
||||||
let requester = event
|
let requester = event
|
||||||
.tags
|
.tags
|
||||||
@@ -704,16 +704,10 @@ impl DeviceRegistry {
|
|||||||
.and_then(|content| PublicKey::parse(content).ok())
|
.and_then(|content| PublicKey::parse(content).ok())
|
||||||
.context("Request has no valid requester key")?;
|
.context("Request has no valid requester key")?;
|
||||||
|
|
||||||
let client_name = event
|
let code = authorization_code(&requester);
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.find(|tag| tag.kind() == "client")
|
|
||||||
.and_then(|tag| tag.content())
|
|
||||||
.map(|c| c.to_string());
|
|
||||||
|
|
||||||
let request = Announcement::new(requester, client_name);
|
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let profile = persons.read(cx).get(&requester, cx);
|
let profile = persons.read(cx).get(&requester, cx);
|
||||||
|
let code_label = code.clone();
|
||||||
|
|
||||||
let entity = cx.entity().downgrade();
|
let entity = cx.entity().downgrade();
|
||||||
let loading = Rc::new(Cell::new(false));
|
let loading = Rc::new(Cell::new(false));
|
||||||
@@ -776,7 +770,7 @@ impl DeviceRegistry {
|
|||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(SharedString::from("Client:")),
|
.child(SharedString::from("Code:")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -785,7 +779,7 @@ impl DeviceRegistry {
|
|||||||
.px_2()
|
.px_2()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.bg(cx.theme().elevated_surface_background)
|
.bg(cx.theme().elevated_surface_background)
|
||||||
.child(request.client_name()),
|
.child(code_label.clone()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -817,6 +811,13 @@ impl DeviceRegistry {
|
|||||||
|
|
||||||
struct DeviceNotification;
|
struct DeviceNotification;
|
||||||
|
|
||||||
|
/// The first eight hexadecimal characters of the client public key.
|
||||||
|
fn authorization_code(public_key: &PublicKey) -> SharedString {
|
||||||
|
let hex = public_key.to_hex();
|
||||||
|
let code = hex[..8].to_uppercase();
|
||||||
|
SharedString::from(format!("{} {}", &code[..4], &code[4..]))
|
||||||
|
}
|
||||||
|
|
||||||
/// Get or create new app keys (async, returns a task)
|
/// Get or create new app keys (async, returns a task)
|
||||||
fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
|
fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
|
||||||
let read = cx.read_credentials(CLIENT_NAME);
|
let read = cx.read_credentials(CLIENT_NAME);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use gpui::SharedString;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
/// Announcement
|
/// Announcement
|
||||||
@@ -6,12 +5,12 @@ 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 name of the device that created this announcement.
|
|
||||||
client_name: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Announcement {
|
impl Announcement {
|
||||||
|
/// Parse an announcement from an event, returning `None` when the `n`
|
||||||
|
/// tag is missing or malformed so callers never mistake the author's
|
||||||
|
/// identity key for an encryption key.
|
||||||
pub fn from_event(event: &Event) -> Option<Self> {
|
pub fn from_event(event: &Event) -> Option<Self> {
|
||||||
let public_key = event
|
let public_key = event
|
||||||
.tags
|
.tags
|
||||||
@@ -20,33 +19,15 @@ 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())?;
|
||||||
|
|
||||||
let client_name = event
|
Some(Self { public_key })
|
||||||
.tags
|
|
||||||
.iter()
|
|
||||||
.find(|tag| tag.kind() == "client")
|
|
||||||
.and_then(|tag| tag.content())
|
|
||||||
.map(|c| c.to_string());
|
|
||||||
|
|
||||||
Some(Self::new(public_key, client_name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(public_key: PublicKey, client_name: Option<String>) -> Self {
|
pub fn new(public_key: PublicKey) -> Self {
|
||||||
Self {
|
Self { public_key }
|
||||||
public_key,
|
|
||||||
client_name,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the client name of the device that created this announcement.
|
|
||||||
pub fn client_name(&self) -> SharedString {
|
|
||||||
self.client_name
|
|
||||||
.as_ref()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or(SharedString::from("Unknown"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,14 +106,15 @@ impl Workspace {
|
|||||||
// Observe all events emitted by the device registry
|
// Observe all events emitted by the device registry
|
||||||
cx.subscribe_in(&device, window, |_this, _device, event, window, cx| {
|
cx.subscribe_in(&device, window, |_this, _device, event, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
DeviceEvent::Requesting => {
|
DeviceEvent::Requesting(code) => {
|
||||||
const MSG: &str =
|
const MSG: &str = "Please open another client and approve the request \
|
||||||
"Please open other client and approve the request for encryption key.";
|
for the encryption key. Compare the code with the \
|
||||||
|
one shown there before approving.";
|
||||||
|
|
||||||
let note = Notification::new()
|
let note = Notification::new()
|
||||||
.id::<DeviceNotifcation>()
|
.id::<DeviceNotifcation>()
|
||||||
.autohide(false)
|
.autohide(false)
|
||||||
.title("Wait for approval")
|
.title(format!("Wait for approval · {code}"))
|
||||||
.message(MSG)
|
.message(MSG)
|
||||||
.with_kind(NotificationKind::Info);
|
.with_kind(NotificationKind::Info);
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -23,9 +23,13 @@
|
|||||||
> pubkey, the 4454 notification parses the requester key from the `P` tag, the
|
> pubkey, the 4454 notification parses the requester key from the `P` tag, the
|
||||||
> NotSet "Retry" button re-checks for an announcement instead of silently
|
> NotSet "Retry" button re-checks for an announcement instead of silently
|
||||||
> generating (rotating) a new encryption key, and the restore dialog's
|
> generating (rotating) a new encryption key, and the restore dialog's
|
||||||
> inverted empty-key check is fixed. Still open: the remaining security notes
|
> inverted empty-key check is fixed. Additionally, the `client` tag and
|
||||||
> in section 4 (plaintext backup file, one-click approval without a
|
> `client_name` were dropped from announcements/requests, and the PsstPsst
|
||||||
> verification code).
|
> authorization code was adapted: both sides display the first 8 hex chars of
|
||||||
|
> the requester client pubkey (`XXXX XXXX`) — the approver in the request
|
||||||
|
> notification, the requester in the "Wait for approval" notification — so the
|
||||||
|
> transfer can be verified out of band before approving. Still open: the
|
||||||
|
> plaintext backup file in section 4.
|
||||||
|
|
||||||
**Scope**: `crates/nip4e/src/lib.rs` (key announcement/sync), the send path (`crates/chat/src/room.rs`), the receive path (`crates/chat/src/lib.rs`), UI integration (`crates/workspace`), compared against the [NIP-4e draft](https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md) and [PsstPsst's implementation](https://github.com/CodyTseng/psstpsst) (`docs/protocols/nip4e.md` + `docs/protocols/nip17-extensions.md`, plus rust-nostr's NIP-59 source to verify crypto mechanics).
|
**Scope**: `crates/nip4e/src/lib.rs` (key announcement/sync), the send path (`crates/chat/src/room.rs`), the receive path (`crates/chat/src/lib.rs`), UI integration (`crates/workspace`), compared against the [NIP-4e draft](https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md) and [PsstPsst's implementation](https://github.com/CodyTseng/psstpsst) (`docs/protocols/nip4e.md` + `docs/protocols/nip17-extensions.md`, plus rust-nostr's NIP-59 source to verify crypto mechanics).
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user