1use crate::{
4 EventDraft, MAX_NIP44_WRITER_PLAINTEXT_BYTES, Nip44EncryptedMessage, Nip44Payload,
5 NostrEventDraft, NostrEventId, NostrEventSignature, NostrPublicKey, NostrRumor, NostrTag,
6 SignedEvent, SignedNostrEvent, SoftchatError,
7};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
12pub struct ExternalSignerCapabilities {
13 pub sign_events: bool,
15 pub nip44_encrypt: bool,
17 pub nip44_decrypt: bool,
19}
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum ExternalSignerFailure {
24 Cancelled,
26 Rejected,
28 CapabilityUnavailable,
30}
31
32impl From<ExternalSignerFailure> for SoftchatError {
33 fn from(failure: ExternalSignerFailure) -> Self {
34 match failure {
35 ExternalSignerFailure::Cancelled => Self::ExternalSignerCancelled,
36 ExternalSignerFailure::Rejected => Self::ExternalSignerRejected,
37 ExternalSignerFailure::CapabilityUnavailable => {
38 Self::ExternalSignerCapabilityUnavailable
39 }
40 }
41 }
42}
43
44#[derive(Clone, Debug, Eq, PartialEq)]
50#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
51pub struct ExternalEventSigningRequest {
52 pub public_key: String,
54 pub event_id: String,
56 pub draft: EventDraft,
58}
59
60pub trait ExternalSigner: Send + Sync {
66 fn capabilities(&self) -> ExternalSignerCapabilities;
68
69 fn public_key(&self) -> Result<NostrPublicKey, ExternalSignerFailure>;
71
72 fn sign_event(
74 &self,
75 request: &ExternalEventSigningRequest,
76 ) -> Result<String, ExternalSignerFailure>;
77
78 fn nip44_encrypt(
80 &self,
81 _recipient: &NostrPublicKey,
82 _plaintext: &str,
83 ) -> Result<String, ExternalSignerFailure> {
84 Err(ExternalSignerFailure::CapabilityUnavailable)
85 }
86
87 fn nip44_decrypt(
89 &self,
90 _sender: &NostrPublicKey,
91 _payload: &Nip44Payload,
92 ) -> Result<String, ExternalSignerFailure> {
93 Err(ExternalSignerFailure::CapabilityUnavailable)
94 }
95}
96
97#[cfg_attr(feature = "native-bindings", uniffi::export)]
103pub fn prepare_external_event_signing(
104 public_key: String,
105 draft: EventDraft,
106) -> Result<ExternalEventSigningRequest, SoftchatError> {
107 let public_key = NostrPublicKey::from_hex(&public_key)?;
108 let typed_draft = NostrEventDraft::try_from(draft.clone())?;
109 let event_id = NostrRumor::new(public_key.clone(), typed_draft)?.id();
110 Ok(ExternalEventSigningRequest {
111 public_key: public_key.to_hex(),
112 event_id: event_id.to_hex(),
113 draft,
114 })
115}
116
117#[cfg_attr(feature = "native-bindings", uniffi::export)]
127pub fn complete_external_event_signing(
128 request: ExternalEventSigningRequest,
129 signature: String,
130) -> Result<SignedEvent, SoftchatError> {
131 complete_typed_external_event_signing(request, signature).map(|event| SignedEvent::from(&event))
132}
133
134pub fn sign_event_with_external(
140 signer: &dyn ExternalSigner,
141 draft: NostrEventDraft,
142) -> Result<SignedNostrEvent, SoftchatError> {
143 if !signer.capabilities().sign_events {
144 return Err(SoftchatError::ExternalSignerCapabilityUnavailable);
145 }
146 let public_key = signer.public_key().map_err(SoftchatError::from)?;
147 let request = prepare_external_event_signing(public_key.to_hex(), EventDraft::from(&draft))?;
148 let signature = signer.sign_event(&request).map_err(SoftchatError::from)?;
149 complete_typed_external_event_signing(request, signature)
150}
151
152pub fn encrypt_with_external(
158 signer: &dyn ExternalSigner,
159 recipient: &NostrPublicKey,
160 plaintext: &str,
161) -> Result<Nip44EncryptedMessage, SoftchatError> {
162 if !signer.capabilities().nip44_encrypt {
163 return Err(SoftchatError::ExternalSignerCapabilityUnavailable);
164 }
165 if plaintext.is_empty() {
166 return Err(SoftchatError::EmptyPlaintext);
167 }
168 if plaintext.len() > MAX_NIP44_WRITER_PLAINTEXT_BYTES {
169 return Err(SoftchatError::PlaintextTooLarge);
170 }
171 let public_key = signer.public_key().map_err(SoftchatError::from)?;
172 let payload = signer
173 .nip44_encrypt(recipient, plaintext)
174 .map_err(SoftchatError::from)
175 .and_then(Nip44Payload::from_encoded)?;
176 Ok(Nip44EncryptedMessage::new(public_key, payload))
177}
178
179pub fn decrypt_with_external(
185 signer: &dyn ExternalSigner,
186 message: &Nip44EncryptedMessage,
187) -> Result<String, SoftchatError> {
188 if !signer.capabilities().nip44_decrypt {
189 return Err(SoftchatError::ExternalSignerCapabilityUnavailable);
190 }
191 let plaintext = signer
192 .nip44_decrypt(message.sender_public_key(), message.payload())
193 .map_err(SoftchatError::from)?;
194 if plaintext.is_empty() || plaintext.len() > MAX_NIP44_WRITER_PLAINTEXT_BYTES {
195 return Err(SoftchatError::DecryptionFailed);
196 }
197 Ok(plaintext)
198}
199
200fn complete_typed_external_event_signing(
201 request: ExternalEventSigningRequest,
202 signature: String,
203) -> Result<SignedNostrEvent, SoftchatError> {
204 let public_key = NostrPublicKey::from_hex(&request.public_key)
205 .map_err(|_| SoftchatError::InvalidExternalSignature)?;
206 let expected_id = NostrEventId::from_hex(&request.event_id)
207 .map_err(|_| SoftchatError::InvalidExternalSignature)?;
208 let draft = NostrEventDraft::try_from(request.draft)
209 .map_err(|_| SoftchatError::InvalidExternalSignature)?;
210 let recomputed = NostrRumor::new(public_key.clone(), draft.clone())
211 .map_err(|_| SoftchatError::InvalidExternalSignature)?;
212 if recomputed.id() != expected_id {
213 return Err(SoftchatError::InvalidExternalSignature);
214 }
215 let tags = draft
216 .tags()
217 .map(<[String]>::to_vec)
218 .map(NostrTag::new)
219 .collect::<Result<Vec<_>, _>>()
220 .map_err(|_| SoftchatError::InvalidExternalSignature)?;
221 let signature = NostrEventSignature::from_hex(&signature)
222 .map_err(|_| SoftchatError::InvalidExternalSignature)?;
223 SignedNostrEvent::new(
224 expected_id,
225 public_key,
226 draft.created_at(),
227 draft.kind(),
228 tags,
229 draft.content().to_owned(),
230 signature,
231 )
232 .map_err(|_| SoftchatError::InvalidExternalSignature)
233}
234
235#[cfg(test)]
236mod tests {
237 use std::sync::Mutex;
238
239 use super::*;
240 use crate::{LocalIdentity, NostrEventKind};
241
242 const ALICE_SECRET: &str = "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a";
243
244 struct TestSigner {
245 identity: LocalIdentity,
246 failure: Mutex<Option<ExternalSignerFailure>>,
247 }
248
249 impl ExternalSigner for TestSigner {
250 fn capabilities(&self) -> ExternalSignerCapabilities {
251 ExternalSignerCapabilities {
252 sign_events: true,
253 nip44_encrypt: false,
254 nip44_decrypt: false,
255 }
256 }
257
258 fn public_key(&self) -> Result<NostrPublicKey, ExternalSignerFailure> {
259 Ok(self.identity.public_key())
260 }
261
262 fn sign_event(
263 &self,
264 request: &ExternalEventSigningRequest,
265 ) -> Result<String, ExternalSignerFailure> {
266 if let Some(failure) = *self
267 .failure
268 .lock()
269 .map_err(|_| ExternalSignerFailure::Rejected)?
270 {
271 return Err(failure);
272 }
273 let draft = NostrEventDraft::try_from(request.draft.clone())
274 .map_err(|_| ExternalSignerFailure::Rejected)?;
275 self.identity
276 .sign_event(draft)
277 .map(|event| event.signature().to_hex())
278 .map_err(|_| ExternalSignerFailure::Rejected)
279 }
280 }
281
282 fn draft() -> Result<NostrEventDraft, SoftchatError> {
283 NostrEventDraft::new(
284 1_700_000_000,
285 NostrEventKind::SHORT_TEXT_NOTE,
286 Vec::new(),
287 "external",
288 )
289 }
290
291 #[test]
292 fn signs_without_exporting_private_material() -> Result<(), SoftchatError> {
293 let signer = TestSigner {
294 identity: LocalIdentity::from_secret_hex(ALICE_SECRET)?,
295 failure: Mutex::new(None),
296 };
297 let event = sign_event_with_external(&signer, draft()?)?;
298 assert_eq!(event.content(), "external");
299 assert_eq!(event.public_key(), signer.identity.public_key());
300 Ok(())
301 }
302
303 #[test]
304 fn maps_cancel_reject_and_invalid_signature_to_stable_errors() -> Result<(), SoftchatError> {
305 let signer = TestSigner {
306 identity: LocalIdentity::from_secret_hex(ALICE_SECRET)?,
307 failure: Mutex::new(Some(ExternalSignerFailure::Cancelled)),
308 };
309 assert!(matches!(
310 sign_event_with_external(&signer, draft()?),
311 Err(SoftchatError::ExternalSignerCancelled)
312 ));
313
314 let request = prepare_external_event_signing(
315 signer.identity.public_key().to_hex(),
316 EventDraft::from(&draft()?),
317 )?;
318 assert!(matches!(
319 complete_external_event_signing(request, "00".repeat(64)),
320 Err(SoftchatError::InvalidExternalSignature)
321 ));
322 Ok(())
323 }
324}