1use std::fmt;
2use std::str::FromStr;
3
4use nostr::{Keys, PublicKey, SecretKey};
5
6use crate::diagnostics::{self, Operation};
7use crate::nip44_profile;
8use crate::{NostrEventDraft, SignedNostrEvent, SoftchatError};
9
10pub const NIP44_V2_ALGORITHM: &str = "nip44-v2";
12
13pub const MAX_NIP44_WRITER_PLAINTEXT_BYTES: usize = 65_535;
19
20pub const MAX_NIP44_BINARY_WRITER_PLAINTEXT_BYTES: usize = 65_408;
26
27pub const MAX_NIP44_READER_PLAINTEXT_BYTES: usize = 327_680;
32
33pub const MAX_NIP44_ENCODED_PAYLOAD_BYTES: usize = 512 * 1024;
38
39pub const MAX_PLAINTEXT_BYTES: usize = MAX_NIP44_BINARY_WRITER_PLAINTEXT_BYTES;
41
42pub const MAX_CIPHERTEXT_BYTES: usize = MAX_NIP44_ENCODED_PAYLOAD_BYTES;
44
45#[derive(Clone, Eq, Hash, PartialEq)]
47pub struct NostrPublicKey {
48 inner: PublicKey,
49}
50
51impl NostrPublicKey {
52 pub fn from_hex(value: &str) -> Result<Self, SoftchatError> {
59 if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
60 return Err(SoftchatError::InvalidPublicKey);
61 }
62
63 let inner = PublicKey::from_hex(value).map_err(|_| SoftchatError::InvalidPublicKey)?;
64 inner.xonly().map_err(|_| SoftchatError::InvalidPublicKey)?;
65 Ok(Self { inner })
66 }
67
68 #[must_use]
70 pub fn to_hex(&self) -> String {
71 self.inner.to_hex()
72 }
73
74 pub(crate) fn from_inner(inner: PublicKey) -> Self {
75 Self { inner }
76 }
77
78 pub(crate) const fn as_inner(&self) -> &PublicKey {
79 &self.inner
80 }
81}
82
83impl fmt::Debug for NostrPublicKey {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 formatter
86 .debug_tuple("NostrPublicKey")
87 .field(&self.to_hex())
88 .finish()
89 }
90}
91
92impl fmt::Display for NostrPublicKey {
93 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 formatter.write_str(&self.to_hex())
95 }
96}
97
98impl FromStr for NostrPublicKey {
99 type Err = SoftchatError;
100
101 fn from_str(value: &str) -> Result<Self, Self::Err> {
102 Self::from_hex(value)
103 }
104}
105
106#[derive(Clone, Eq, Hash, PartialEq)]
108pub struct Nip44Payload {
109 encoded: String,
110}
111
112impl Nip44Payload {
113 pub fn from_encoded(encoded: impl Into<String>) -> Result<Self, SoftchatError> {
123 let encoded = encoded.into();
124 if encoded.len() > MAX_NIP44_ENCODED_PAYLOAD_BYTES {
125 return Err(SoftchatError::CiphertextTooLarge);
126 }
127 Ok(Self { encoded })
128 }
129
130 #[must_use]
132 pub fn as_str(&self) -> &str {
133 &self.encoded
134 }
135
136 #[must_use]
138 pub fn into_string(self) -> String {
139 self.encoded
140 }
141}
142
143impl fmt::Debug for Nip44Payload {
144 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145 formatter
146 .debug_struct("Nip44Payload")
147 .field("encoded_bytes", &self.encoded.len())
148 .finish()
149 }
150}
151
152#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct Nip44EncryptedMessage {
155 sender_public_key: NostrPublicKey,
156 payload: Nip44Payload,
157}
158
159impl Nip44EncryptedMessage {
160 #[must_use]
162 pub const fn new(sender_public_key: NostrPublicKey, payload: Nip44Payload) -> Self {
163 Self {
164 sender_public_key,
165 payload,
166 }
167 }
168
169 #[must_use]
171 pub const fn sender_public_key(&self) -> &NostrPublicKey {
172 &self.sender_public_key
173 }
174
175 #[must_use]
177 pub const fn payload(&self) -> &Nip44Payload {
178 &self.payload
179 }
180
181 pub(crate) fn into_parts(self) -> (NostrPublicKey, Nip44Payload) {
182 (self.sender_public_key, self.payload)
183 }
184}
185
186pub struct LocalIdentity {
192 keys: Keys,
193}
194
195impl LocalIdentity {
196 pub fn from_secret_bytes(secret_key: &[u8]) -> Result<Self, SoftchatError> {
205 if secret_key.len() != SecretKey::LEN {
206 return Err(SoftchatError::InvalidSecretKey);
207 }
208
209 let secret_key =
210 SecretKey::from_slice(secret_key).map_err(|_| SoftchatError::InvalidSecretKey)?;
211 Ok(Self {
212 keys: Keys::new(secret_key),
213 })
214 }
215
216 pub fn from_secret_hex(secret_key: &str) -> Result<Self, SoftchatError> {
225 if secret_key.len() != 64 || !secret_key.bytes().all(|byte| byte.is_ascii_hexdigit()) {
226 return Err(SoftchatError::InvalidSecretKey);
227 }
228
229 let secret_key =
230 SecretKey::from_hex(secret_key).map_err(|_| SoftchatError::InvalidSecretKey)?;
231 Ok(Self {
232 keys: Keys::new(secret_key),
233 })
234 }
235
236 #[must_use]
238 pub fn public_key(&self) -> NostrPublicKey {
239 NostrPublicKey::from_inner(self.keys.public_key())
240 }
241
242 pub fn sign_event(&self, draft: NostrEventDraft) -> Result<SignedNostrEvent, SoftchatError> {
251 let input_bytes = draft.content().len();
252 let result = draft.sign_with_keys(&self.keys);
253 if result.is_ok() {
254 diagnostics::record_success(Operation::SignEvent, input_bytes, 64);
255 } else {
256 diagnostics::record_rejection(input_bytes);
257 }
258 result
259 }
260
261 pub fn encrypt(
267 &self,
268 recipient: &NostrPublicKey,
269 plaintext: &[u8],
270 ) -> Result<Nip44EncryptedMessage, SoftchatError> {
271 validate_binary_plaintext(plaintext)?;
272
273 let encoded =
274 nip44_profile::encrypt_binary(self.keys.secret_key(), recipient.as_inner(), plaintext)
275 .map_err(|_| SoftchatError::EncryptionFailed)?;
276
277 let message =
278 Nip44EncryptedMessage::new(self.public_key(), Nip44Payload::from_encoded(encoded)?);
279 diagnostics::record_success(
280 Operation::Nip44Encrypt,
281 plaintext.len(),
282 message.payload().as_str().len(),
283 );
284 Ok(message)
285 }
286
287 pub fn encrypt_utf8(
293 &self,
294 recipient: &NostrPublicKey,
295 plaintext: &str,
296 ) -> Result<Nip44EncryptedMessage, SoftchatError> {
297 validate_plaintext(plaintext.as_bytes())?;
298
299 let encoded =
300 nip44_profile::encrypt_utf8(self.keys.secret_key(), recipient.as_inner(), plaintext)
301 .map_err(|_| SoftchatError::EncryptionFailed)?;
302
303 let message =
304 Nip44EncryptedMessage::new(self.public_key(), Nip44Payload::from_encoded(encoded)?);
305 diagnostics::record_success(
306 Operation::Nip44Encrypt,
307 plaintext.len(),
308 message.payload().as_str().len(),
309 );
310 Ok(message)
311 }
312
313 pub fn decrypt(&self, message: &Nip44EncryptedMessage) -> Result<Vec<u8>, SoftchatError> {
320 let input_bytes = message.payload().as_str().len();
321 let result = nip44_profile::decrypt(
322 self.keys.secret_key(),
323 message.sender_public_key().as_inner(),
324 message.payload().as_str(),
325 )
326 .map_err(|_| SoftchatError::DecryptionFailed);
327 match &result {
328 Ok(plaintext) => {
329 diagnostics::record_success(Operation::Nip44Decrypt, input_bytes, plaintext.len());
330 }
331 Err(_) => diagnostics::record_rejection(input_bytes),
332 }
333 result
334 }
335
336 pub fn decrypt_utf8(&self, message: &Nip44EncryptedMessage) -> Result<String, SoftchatError> {
343 String::from_utf8(self.decrypt(message)?).map_err(|_| SoftchatError::DecryptionFailed)
344 }
345
346 pub(crate) const fn keys(&self) -> &Keys {
347 &self.keys
348 }
349}
350
351impl fmt::Debug for LocalIdentity {
352 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
353 formatter
354 .debug_struct("LocalIdentity")
355 .field("public_key", &self.public_key())
356 .field("secret_key", &"[redacted]")
357 .finish()
358 }
359}
360
361fn validate_plaintext(plaintext: &[u8]) -> Result<(), SoftchatError> {
362 if plaintext.is_empty() {
363 return Err(SoftchatError::EmptyPlaintext);
364 }
365 if plaintext.len() > MAX_NIP44_WRITER_PLAINTEXT_BYTES {
366 return Err(SoftchatError::PlaintextTooLarge);
367 }
368 Ok(())
369}
370
371fn validate_binary_plaintext(plaintext: &[u8]) -> Result<(), SoftchatError> {
372 if plaintext.is_empty() {
373 return Err(SoftchatError::EmptyPlaintext);
374 }
375 if plaintext.len() > MAX_NIP44_BINARY_WRITER_PLAINTEXT_BYTES {
376 return Err(SoftchatError::PlaintextTooLarge);
377 }
378 Ok(())
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 const ALICE_SECRET: &str = "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a";
386 const BOB_SECRET: &str = "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d";
387 const CAROL_SECRET: &str = "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c";
388
389 #[test]
390 fn typed_identities_round_trip_utf8_and_bytes() -> Result<(), SoftchatError> {
391 let alice = LocalIdentity::from_secret_hex(ALICE_SECRET)?;
392 let bob = LocalIdentity::from_secret_hex(BOB_SECRET)?;
393
394 let text = alice.encrypt_utf8(&bob.public_key(), "Hello โ ่กจ ๐")?;
395 assert_eq!(bob.decrypt_utf8(&text)?, "Hello โ ่กจ ๐");
396
397 let bytes = alice.encrypt(&bob.public_key(), &[0, 1, 127, 128, 255])?;
398 assert_eq!(bob.decrypt(&bytes)?, [0, 1, 127, 128, 255]);
399 Ok(())
400 }
401
402 #[test]
403 fn typed_values_reject_invalid_inputs() {
404 assert!(matches!(
405 NostrPublicKey::from_hex("ff"),
406 Err(SoftchatError::InvalidPublicKey)
407 ));
408 assert!(matches!(
409 Nip44Payload::from_encoded("A".repeat(MAX_CIPHERTEXT_BYTES + 1)),
410 Err(SoftchatError::CiphertextTooLarge)
411 ));
412 assert!(matches!(
413 LocalIdentity::from_secret_bytes(&[1; 31]),
414 Err(SoftchatError::InvalidSecretKey)
415 ));
416 }
417
418 #[test]
419 fn binary_writer_uses_its_explicit_legacy_bound() -> Result<(), SoftchatError> {
420 let alice = LocalIdentity::from_secret_hex(ALICE_SECRET)?;
421 let bob = LocalIdentity::from_secret_hex(BOB_SECRET)?;
422
423 let maximum = vec![b'b'; MAX_NIP44_BINARY_WRITER_PLAINTEXT_BYTES];
424 let message = alice.encrypt(&bob.public_key(), &maximum)?;
425 assert_eq!(bob.decrypt(&message)?, maximum);
426 assert!(matches!(
427 alice.encrypt(
428 &bob.public_key(),
429 &vec![b'b'; MAX_NIP44_BINARY_WRITER_PLAINTEXT_BYTES + 1],
430 ),
431 Err(SoftchatError::PlaintextTooLarge)
432 ));
433 Ok(())
434 }
435
436 #[test]
437 fn typed_identity_reports_redacted_failure_for_wrong_recipient() -> Result<(), SoftchatError> {
438 let alice = LocalIdentity::from_secret_hex(ALICE_SECRET)?;
439 let bob = LocalIdentity::from_secret_hex(BOB_SECRET)?;
440 let carol = LocalIdentity::from_secret_hex(CAROL_SECRET)?;
441 let message = alice.encrypt_utf8(&bob.public_key(), "authenticated")?;
442
443 assert!(matches!(
444 carol.decrypt_utf8(&message),
445 Err(SoftchatError::DecryptionFailed)
446 ));
447 Ok(())
448 }
449
450 #[test]
451 fn identity_debug_is_redacted() -> Result<(), SoftchatError> {
452 let identity = LocalIdentity::from_secret_hex(ALICE_SECRET)?;
453 let debug = format!("{identity:?}");
454
455 assert!(debug.contains("[redacted]"));
456 assert!(!debug.contains(ALICE_SECRET));
457 Ok(())
458 }
459}