Skip to main content

vector_core/crypto/
signer.rs

1//! GuardedSigner — a signer backed by a GuardedKey vault.
2//!
3//! Reads the secret key from the memory-hardened vault on every operation, so
4//! the key exists in plaintext only for microseconds during signing.
5//!
6//! Implements the synchronous capability traits as the primary form: a vault
7//! read plus local crypto never awaits, so the async impls just wrap them. The
8//! async side exists only to satisfy `VectorSigner`, which has to stay async for
9//! the bunker and NIP-55 backends.
10
11use nostr_sdk::prelude::*;
12use zeroize::Zeroize;
13
14use crate::signer::{BoxedFuture, SignerError};
15
16/// A signer backed by the `MY_SECRET_KEY` vault.
17///
18/// The secret key is never stored in this struct — it's fetched from the
19/// GuardedKey vault on every operation and zeroized immediately after use.
20#[derive(Debug, Clone)]
21pub struct GuardedSigner {
22    public_key: PublicKey,
23}
24
25impl GuardedSigner {
26    pub fn new(public_key: PublicKey) -> Self {
27        Self { public_key }
28    }
29
30    fn temp_keys(&self) -> Result<Keys, SignerError> {
31        crate::state::MY_SECRET_KEY
32            .to_keys()
33            .ok_or_else(|| SignerError::from("Secret key not available"))
34    }
35
36    /// The bare secret key, for operations that never need the public half: a
37    /// `Keys` derives it on construction, an elliptic-curve multiply per call.
38    fn temp_secret_key() -> Result<SecretKey, SignerError> {
39        let mut bytes = crate::state::MY_SECRET_KEY
40            .get()
41            .ok_or_else(|| SignerError::from("Secret key not available"))?;
42        let key = SecretKey::from_slice(&bytes).map_err(SignerError::backend);
43        bytes.zeroize();
44        key
45    }
46
47    /// NIP-59 unwrap under one vault read; see [`Self::unwrap_batch`].
48    pub fn unwrap_gift_wrap(gift_wrap: &Event) -> Result<UnwrappedGift, SignerError> {
49        Self::unwrap_batch(&[gift_wrap]).pop().unwrap_or_else(|| Err(SignerError::from("empty batch")))
50    }
51
52    /// NIP-59 unwrap of many wraps under one vault read, one result per wrap in order.
53    /// Checks what nostr's `from_gift_wrap` checks, but opens every layer before verifying
54    /// any seal, so the key lives only for the NIP-44 opens and never across a signature
55    /// check. Opening an unverified seal is safe: its content authenticates under ECDH with
56    /// its claimed author, and nothing from it is trusted until that author's signature
57    /// verifies.
58    ///
59    /// A sender's conversation key is derived once per batch and wiped with the secret key.
60    ///
61    /// No identity check against `my_public_key`: a vault key that isn't the identity can
62    /// only open wraps sealed to itself, which then authenticate their sender like any
63    /// DM. Signing keeps `active_signer`'s fail-closed check.
64    pub fn unwrap_batch(wraps: &[&Event]) -> Vec<Result<UnwrappedGift, SignerError>> {
65        let checked: Vec<Result<(), SignerError>> = wraps
66            .iter()
67            .map(|wrap| {
68                if wrap.kind != Kind::GiftWrap {
69                    return Err(SignerError::from("not a gift wrap"));
70                }
71                // A wrap's signature is a throwaway key's and proves nothing (NIP-59); the seal's
72                // does. Its id must still match its fields: dedup and the ledger key on it.
73                if !wrap.verify_id() {
74                    return Err(SignerError::from("gift wrap id does not match its content"));
75                }
76                Ok(())
77            })
78            .collect();
79
80        let key = if checked.iter().any(Result::is_ok) { Some(Self::temp_secret_key()) } else { None };
81        let mut senders: Vec<(PublicKey, nip44::v2::ConversationKey)> = Vec::new();
82        let opened: Vec<Result<(Event, String), SignerError>> = wraps
83            .iter()
84            .zip(checked)
85            .map(|(wrap, check)| {
86                check?;
87                match &key {
88                    Some(Ok(key)) => open_layers(key, wrap, &mut senders),
89                    Some(Err(e)) => Err(SignerError::from(e.to_string())),
90                    None => Err(SignerError::from("unreachable")),
91                }
92            })
93            .collect();
94        for (_, ck) in senders.iter_mut() {
95            wipe_conversation_key(ck);
96        }
97        drop(key);
98
99        opened
100            .into_iter()
101            .map(|slot| {
102                let (seal, rumor) = slot?;
103                seal.verify().map_err(SignerError::backend)?;
104                let rumor = UnsignedEvent::from_json(rumor).map_err(SignerError::backend)?;
105                if rumor.pubkey != seal.pubkey {
106                    return Err(SignerError::from("sender mismatch"));
107                }
108                Ok(UnwrappedGift { sender: seal.pubkey, rumor })
109            })
110            .collect()
111    }
112}
113
114/// Opens a wrap's two layers: the seal event, and the rumor's JSON (seal unverified).
115fn open_layers(
116    key: &SecretKey,
117    wrap: &Event,
118    senders: &mut Vec<(PublicKey, nip44::v2::ConversationKey)>,
119) -> Result<(Event, String), SignerError> {
120    let mut wrap_key = nip44::v2::ConversationKey::derive(key, &wrap.pubkey).map_err(SignerError::backend)?;
121    let seal = nip44_open(&wrap_key, &wrap.content);
122    wipe_conversation_key(&mut wrap_key);
123    let seal = Event::from_json(seal?).map_err(SignerError::backend)?;
124
125    let at = match senders.iter().position(|(pk, _)| *pk == seal.pubkey) {
126        Some(at) => at,
127        None => {
128            let ck = nip44::v2::ConversationKey::derive(key, &seal.pubkey).map_err(SignerError::backend)?;
129            senders.push((seal.pubkey, ck));
130            senders.len() - 1
131        }
132    };
133    let rumor = nip44_open(&senders[at].1, &seal.content)?;
134    Ok((seal, rumor))
135}
136
137/// nostr's own bound on an encoded v2 payload, checked before decoding allocates.
138const NIP44_MAX_ENCODED: usize = 87_472;
139
140/// `nip44::decrypt` with the conversation key already derived.
141fn nip44_open(ck: &nip44::v2::ConversationKey, payload: &str) -> Result<String, SignerError> {
142    if payload.len() > NIP44_MAX_ENCODED {
143        return Err(SignerError::from("nip44 payload too long"));
144    }
145    let bytes = base64_simd::STANDARD.decode_to_vec(payload.as_bytes()).map_err(SignerError::backend)?;
146    if bytes.first() != Some(&nip44::Version::V2.as_u8()) {
147        return Err(SignerError::from("unsupported nip44 version"));
148    }
149    let plain = nip44::v2::decrypt_to_bytes(ck, &bytes).map_err(SignerError::backend)?;
150    String::from_utf8(plain).map_err(SignerError::backend)
151}
152
153fn wipe_conversation_key(ck: &mut nip44::v2::ConversationKey) {
154    // SAFETY: `ck` is a valid, aligned, exclusive reference; the type has no Drop to skip.
155    unsafe { core::ptr::write_volatile(ck, nip44::v2::ConversationKey::new([0u8; 32])) };
156}
157
158// ---------------------------------------------------------------------------
159// Synchronous capabilities
160// ---------------------------------------------------------------------------
161
162impl GetPublicKey for GuardedSigner {
163    type Error = SignerError;
164
165    #[inline]
166    fn get_public_key(&self) -> Result<PublicKey, Self::Error> {
167        Ok(self.public_key)
168    }
169}
170
171impl SignEvent for GuardedSigner {
172    type Error = SignerError;
173
174    fn sign_event(&self, unsigned: UnsignedEvent) -> Result<Event, Self::Error> {
175        let keys = self.temp_keys()?;
176        SignEvent::sign_event(&keys, unsigned).map_err(SignerError::backend)
177    }
178}
179
180impl Nip04 for GuardedSigner {
181    type Error = SignerError;
182
183    fn nip04_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Self::Error> {
184        nip04::encrypt(&Self::temp_secret_key()?, public_key, content).map_err(SignerError::backend)
185    }
186
187    fn nip04_decrypt(&self, public_key: &PublicKey, payload: &str) -> Result<String, Self::Error> {
188        nip04::decrypt(&Self::temp_secret_key()?, public_key, payload).map_err(SignerError::backend)
189    }
190}
191
192impl Nip44 for GuardedSigner {
193    type Error = SignerError;
194
195    fn nip44_encrypt(&self, public_key: &PublicKey, content: &str) -> Result<String, Self::Error> {
196        nip44::encrypt(&Self::temp_secret_key()?, public_key, content, nip44::Version::default())
197            .map_err(SignerError::backend)
198    }
199
200    fn nip44_decrypt(&self, public_key: &PublicKey, payload: &str) -> Result<String, Self::Error> {
201        nip44::decrypt(&Self::temp_secret_key()?, public_key, payload).map_err(SignerError::backend)
202    }
203}
204
205// ---------------------------------------------------------------------------
206// Async forwarding — only so GuardedSigner satisfies `VectorSigner`
207// ---------------------------------------------------------------------------
208
209impl AsyncGetPublicKey for GuardedSigner {
210    type Error = SignerError;
211
212    #[inline]
213    fn get_public_key_async(&self) -> BoxedFuture<'_, Result<PublicKey, Self::Error>> {
214        let res = GetPublicKey::get_public_key(self);
215        Box::pin(async move { res })
216    }
217}
218
219impl AsyncSignEvent for GuardedSigner {
220    type Error = SignerError;
221
222    #[inline]
223    fn sign_event_async(&self, unsigned: UnsignedEvent) -> BoxedFuture<'_, Result<Event, Self::Error>> {
224        let res = SignEvent::sign_event(self, unsigned);
225        Box::pin(async move { res })
226    }
227}
228
229impl AsyncNip04 for GuardedSigner {
230    type Error = SignerError;
231
232    #[inline]
233    fn nip04_encrypt_async<'a>(
234        &'a self,
235        public_key: &'a PublicKey,
236        content: &'a str,
237    ) -> BoxedFuture<'a, Result<String, Self::Error>> {
238        let res = Nip04::nip04_encrypt(self, public_key, content);
239        Box::pin(async move { res })
240    }
241
242    #[inline]
243    fn nip04_decrypt_async<'a>(
244        &'a self,
245        public_key: &'a PublicKey,
246        encrypted_content: &'a str,
247    ) -> BoxedFuture<'a, Result<String, Self::Error>> {
248        let res = Nip04::nip04_decrypt(self, public_key, encrypted_content);
249        Box::pin(async move { res })
250    }
251}
252
253impl AsyncNip44 for GuardedSigner {
254    type Error = SignerError;
255
256    #[inline]
257    fn nip44_encrypt_async<'a>(
258        &'a self,
259        public_key: &'a PublicKey,
260        content: &'a str,
261    ) -> BoxedFuture<'a, Result<String, Self::Error>> {
262        let res = Nip44::nip44_encrypt(self, public_key, content);
263        Box::pin(async move { res })
264    }
265
266    #[inline]
267    fn nip44_decrypt_async<'a>(
268        &'a self,
269        public_key: &'a PublicKey,
270        payload: &'a str,
271    ) -> BoxedFuture<'a, Result<String, Self::Error>> {
272        let res = Nip44::nip44_decrypt(self, public_key, payload);
273        Box::pin(async move { res })
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    fn rumor(from: &PublicKey, to: &PublicKey) -> UnsignedEvent {
282        EventBuilder::new(Kind::PrivateDirectMessage, "see you at nine")
283            .tag(Tag::public_key(*to))
284            .finalize_unsigned(*from)
285    }
286
287    fn seal(author: &Keys, rumor: &UnsignedEvent, to: &PublicKey) -> Event {
288        let content = nip44::encrypt(author.secret_key(), to, rumor.as_json(), nip44::Version::default()).unwrap();
289        EventBuilder::new(Kind::Seal, content).finalize(author).unwrap()
290    }
291
292    fn wrap(seal: &Event, to: &PublicKey) -> Event {
293        let ephemeral = Keys::generate();
294        let content = nip44::encrypt(ephemeral.secret_key(), to, seal.as_json(), nip44::Version::default()).unwrap();
295        EventBuilder::new(Kind::GiftWrap, content).tag(Tag::public_key(*to)).finalize(&ephemeral).unwrap()
296    }
297
298    fn flip_sig(mut e: Event) -> Event {
299        let mut sig = e.sig.to_bytes();
300        sig[10] ^= 1;
301        e.sig = Signature::from_slice(&sig).unwrap();
302        e
303    }
304
305    /// Keeps the id and signature of `e` but not the fields they cover.
306    fn retime(mut e: Event) -> Event {
307        e.created_at = Timestamp::from_secs(e.created_at.as_secs() - 1);
308        e
309    }
310
311    /// Runs `f` with `me` in the vault; the vault is a process global, so this holds the
312    /// suite's shared guard.
313    fn with_me<R>(me: &Keys, f: impl FnOnce() -> R) -> R {
314        let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
315        crate::state::MY_SECRET_KEY.store_from_keys(me, &[]);
316        let out = f();
317        crate::state::MY_SECRET_KEY.clear(&[]);
318        out
319    }
320
321    #[test]
322    fn unwrap_matches_nostr_on_a_valid_wrap() {
323        let (me, sender) = (Keys::generate(), Keys::generate());
324        let gift = wrap(&seal(&sender, &rumor(&sender.public_key(), &me.public_key()), &me.public_key()), &me.public_key());
325        let reference = UnwrappedGift::from_gift_wrap(&me, &gift).unwrap();
326        let ours = with_me(&me, || GuardedSigner::unwrap_gift_wrap(&gift)).unwrap();
327        assert_eq!(ours, reference);
328        assert_eq!(ours.sender, sender.public_key());
329    }
330
331    #[test]
332    fn unwrap_rejects_everything_nostr_rejects() {
333        let (me, sender, stranger) = (Keys::generate(), Keys::generate(), Keys::generate());
334        let good_rumor = rumor(&sender.public_key(), &me.public_key());
335        let good_seal = seal(&sender, &good_rumor, &me.public_key());
336
337        let cases: Vec<(&str, Event)> = vec![
338            ("wrap id does not match its fields", retime(wrap(&good_seal, &me.public_key()))),
339            // Decrypts cleanly: only the signature check can refuse it.
340            ("forged seal signature", wrap(&flip_sig(good_seal.clone()), &me.public_key())),
341            ("rumor author is not the seal author",
342                wrap(&seal(&sender, &rumor(&stranger.public_key(), &me.public_key()), &me.public_key()), &me.public_key())),
343            ("wrap sealed to someone else", wrap(&good_seal, &stranger.public_key())),
344            ("seal sealed to someone else",
345                wrap(&seal(&sender, &good_rumor, &stranger.public_key()), &me.public_key())),
346            ("not a gift wrap", good_seal.clone()),
347        ];
348        for (what, gift) in cases {
349            assert!(UnwrappedGift::from_gift_wrap(&me, &gift).is_err(), "nostr accepts: {what}");
350            assert!(with_me(&me, || GuardedSigner::unwrap_gift_wrap(&gift)).is_err(), "we accept: {what}");
351        }
352    }
353
354    #[test]
355    fn a_wrap_is_checked_for_its_id_but_not_its_signature() {
356        let (me, sender) = (Keys::generate(), Keys::generate());
357        let good = wrap(&seal(&sender, &rumor(&sender.public_key(), &me.public_key()), &me.public_key()), &me.public_key());
358        let forged_sig = flip_sig(good.clone());
359        assert!(forged_sig.verify().is_err() && forged_sig.verify_id());
360        assert!(with_me(&me, || GuardedSigner::unwrap_gift_wrap(&forged_sig)).is_ok());
361        assert!(with_me(&me, || GuardedSigner::unwrap_gift_wrap(&retime(good))).is_err());
362    }
363
364    #[test]
365    fn a_batch_matches_unwrapping_one_by_one() {
366        let (me, stranger) = (Keys::generate(), Keys::generate());
367        let senders: Vec<Keys> = (0..3).map(|_| Keys::generate()).collect();
368        let mut gifts = Vec::new();
369        for i in 0..12 {
370            let from = &senders[i % 3];
371            let r = rumor(&from.public_key(), &me.public_key());
372            gifts.push(match i {
373                4 => wrap(&flip_sig(seal(from, &r, &me.public_key())), &me.public_key()),
374                7 => wrap(&seal(from, &r, &stranger.public_key()), &me.public_key()),
375                9 => retime(wrap(&seal(from, &r, &me.public_key()), &me.public_key())),
376                10 => seal(from, &r, &me.public_key()),
377                _ => wrap(&seal(from, &r, &me.public_key()), &me.public_key()),
378            });
379        }
380        let refs: Vec<&Event> = gifts.iter().collect();
381        let (batch, single) = with_me(&me, || {
382            let batch = GuardedSigner::unwrap_batch(&refs);
383            let single: Vec<_> = gifts.iter().map(GuardedSigner::unwrap_gift_wrap).collect();
384            (batch, single)
385        });
386        assert_eq!(batch.len(), gifts.len());
387        for (i, ((b, s), gift)) in batch.iter().zip(&single).zip(&gifts).enumerate() {
388            match (b, s) {
389                (Ok(b), Ok(s)) => {
390                    assert_eq!(b, s, "wrap {i}");
391                    assert_eq!(*b, UnwrappedGift::from_gift_wrap(&me, gift).unwrap(), "wrap {i}");
392                    assert_eq!(b.sender, senders[i % 3].public_key());
393                }
394                (Err(_), Err(_)) => assert!([4, 7, 9, 10].contains(&i), "wrap {i} refused"),
395                _ => panic!("wrap {i}: batch and single disagree"),
396            }
397        }
398    }
399
400    #[test]
401    fn a_batch_with_an_empty_vault_fails_every_wrap() {
402        let (me, sender) = (Keys::generate(), Keys::generate());
403        let gift = wrap(&seal(&sender, &rumor(&sender.public_key(), &me.public_key()), &me.public_key()), &me.public_key());
404        let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
405        crate::state::MY_SECRET_KEY.clear(&[]);
406        assert!(GuardedSigner::unwrap_batch(&[&gift, &gift]).iter().all(Result::is_err));
407        assert!(GuardedSigner::unwrap_batch(&[]).is_empty());
408    }
409
410    #[test]
411    fn nip44_open_accepts_and_refuses_exactly_what_nostr_does() {
412        let (a, b) = (Keys::generate(), Keys::generate());
413        let ck = nip44::v2::ConversationKey::derive(b.secret_key(), &a.public_key()).unwrap();
414        let same = |p: &str| {
415            let theirs = nip44::decrypt(b.secret_key(), &a.public_key(), p).ok();
416            let ours = nip44_open(&ck, p).ok();
417            assert_eq!(ours, theirs, "payload {p:?}");
418        };
419        let mut seed = 0x2545_f491_4f6c_dd1du64;
420        for len in [1usize, 2, 31, 32, 33, 100, 255, 256, 257, 1000, 5000, 65_000] {
421            let text: String = (0..len).map(|i| char::from(b'a' + (i % 26) as u8)).collect();
422            let p = nip44::encrypt(a.secret_key(), &b.public_key(), &text, nip44::Version::V2).unwrap();
423            same(&p);
424            for cut in 1..5 {
425                same(&p[..p.len() - cut]);
426            }
427            for extra in ["=", "A", "AAAA", " ", "\n"] {
428                same(&format!("{p}{extra}"));
429            }
430            let tail = p.trim_end_matches('=').len();
431            let spots: Vec<usize> = (1..=3).map(|k| tail - k).chain((0..40).map(|_| {
432                seed ^= seed << 13;
433                seed ^= seed >> 7;
434                seed ^= seed << 17;
435                (seed as usize) % p.len()
436            })).collect();
437            for at in spots {
438                for c in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'Q', 'g', 'w', '/', '+', '=', '-', '_', ' '] {
439                    let mut q = p.clone().into_bytes();
440                    q[at] = c as u8;
441                    same(&String::from_utf8(q).unwrap());
442                }
443            }
444        }
445        same("");
446        same("Ag==");
447        same(&"A".repeat(NIP44_MAX_ENCODED + 4));
448    }
449
450    #[test]
451    fn unwrap_with_an_empty_vault_fails() {
452        let (me, sender) = (Keys::generate(), Keys::generate());
453        let gift = wrap(&seal(&sender, &rumor(&sender.public_key(), &me.public_key()), &me.public_key()), &me.public_key());
454        let _guard = crate::db::DB_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
455        crate::state::MY_SECRET_KEY.clear(&[]);
456        assert!(GuardedSigner::unwrap_gift_wrap(&gift).is_err());
457    }
458
459    #[test]
460    fn nip44_and_nip04_match_keys_both_ways() {
461        let (me, peer) = (Keys::generate(), Keys::generate());
462        let signer = GuardedSigner::new(me.public_key());
463        with_me(&me, || {
464            let sealed = Nip44::nip44_encrypt(&signer, &peer.public_key(), "hello").unwrap();
465            assert_eq!(peer.nip44_decrypt(&me.public_key(), &sealed).unwrap(), "hello");
466            let from_peer = peer.nip44_encrypt(&me.public_key(), "back").unwrap();
467            assert_eq!(Nip44::nip44_decrypt(&signer, &peer.public_key(), &from_peer).unwrap(), "back");
468
469            let sealed = Nip04::nip04_encrypt(&signer, &peer.public_key(), "old").unwrap();
470            assert_eq!(peer.nip04_decrypt(&me.public_key(), &sealed).unwrap(), "old");
471            let from_peer = peer.nip04_encrypt(&me.public_key(), "school").unwrap();
472            assert_eq!(Nip04::nip04_decrypt(&signer, &peer.public_key(), &from_peer).unwrap(), "school");
473        });
474    }
475}