Skip to main content

mostro_core/chat/
shared_key.rs

1//! ECDH-derived shared key used as the addressable identity of a Mostro
2//! P2P chat channel.
3//!
4//! The two parties of a chat (buyer/seller during a trade or admin/party
5//! during a dispute) each compute the same `SharedKey` from their own trade
6//! secret key and the counterparty's trade public key, so both can encrypt
7//! and decrypt the conversation, and so relays can route gift wraps through
8//! a `p` tag bound to the shared public key — without leaking either real
9//! pubkey on the wire.
10
11use nostr_sdk::prelude::*;
12
13use crate::error::{MostroError, ServiceError};
14
15/// Shared key derived via ECDH between two parties' trade keys.
16///
17/// Internally a `Keys` instance whose secret is the 32-byte ECDH output of
18/// `(local_secret, counterparty_pubkey)`. Both sides of the conversation
19/// derive an identical value and therefore an identical public key, which
20/// is what gift wraps are addressed to.
21#[derive(Debug, Clone)]
22pub struct SharedKey(Keys);
23
24impl SharedKey {
25    /// Derive a shared key from a local secret key and the counterparty's
26    /// public key using the secp256k1 ECDH primitive exposed by
27    /// [`nostr_sdk::util::generate_shared_key`].
28    ///
29    /// Both peers obtain the same `SharedKey` by swapping arguments
30    /// (`A.derive(a_sk, b_pk) == B.derive(b_sk, a_pk)`).
31    pub fn derive(secret: &SecretKey, counterparty: &PublicKey) -> Result<Self, MostroError> {
32        let bytes = nostr_sdk::util::generate_shared_key(secret, counterparty).map_err(|e| {
33            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
34                "shared key derivation failed: {e}"
35            )))
36        })?;
37        let secret = SecretKey::from_slice(&bytes).map_err(|e| {
38            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
39                "invalid shared secret: {e}"
40            )))
41        })?;
42        Ok(Self(Keys::new(secret)))
43    }
44
45    /// Build a `SharedKey` from an already-derived `Keys` value.
46    ///
47    /// Useful when a client persists a freshly-generated `Keys` instead of
48    /// re-deriving it on every load.
49    pub fn from_keys(keys: Keys) -> Self {
50        Self(keys)
51    }
52
53    /// Borrow the underlying `Keys`.
54    pub fn keys(&self) -> &Keys {
55        &self.0
56    }
57
58    /// Public key of this shared key — the value used as the `p` tag on
59    /// every gift wrap belonging to the channel.
60    pub fn public_key(&self) -> PublicKey {
61        self.0.public_key()
62    }
63
64    /// Borrow the underlying secret key.
65    pub fn secret_key(&self) -> &SecretKey {
66        self.0.secret_key()
67    }
68
69    /// Serialize the secret as a lower-case hex string suitable for client
70    /// persistence. Pair with [`SharedKey::from_hex`] to round-trip.
71    pub fn to_hex(&self) -> String {
72        self.0.secret_key().to_secret_hex()
73    }
74
75    /// Rebuild a `SharedKey` from a hex-encoded secret previously produced
76    /// by [`SharedKey::to_hex`].
77    pub fn from_hex(hex: &str) -> Result<Self, MostroError> {
78        let secret = SecretKey::from_hex(hex).map_err(|e| {
79            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
80                "invalid shared key hex: {e}"
81            )))
82        })?;
83        Ok(Self(Keys::new(secret)))
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn derive_is_symmetric_between_peers() {
93        let alice = Keys::generate();
94        let bob = Keys::generate();
95
96        let from_alice = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
97        let from_bob = SharedKey::derive(bob.secret_key(), &alice.public_key()).unwrap();
98
99        assert_eq!(from_alice.public_key(), from_bob.public_key());
100        assert_eq!(from_alice.to_hex(), from_bob.to_hex());
101    }
102
103    #[test]
104    fn derive_shared_key_hex_roundtrip() {
105        let alice = Keys::generate();
106        let bob = Keys::generate();
107        let derived = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
108
109        let hex = derived.to_hex();
110        let restored = SharedKey::from_hex(&hex).unwrap();
111
112        assert_eq!(derived.public_key(), restored.public_key());
113        assert_eq!(derived.to_hex(), restored.to_hex());
114    }
115
116    #[test]
117    fn derive_shared_key_different_peers_produce_different_keys() {
118        let alice = Keys::generate();
119        let bob = Keys::generate();
120        let carol = Keys::generate();
121
122        let with_bob = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
123        let with_carol = SharedKey::derive(alice.secret_key(), &carol.public_key()).unwrap();
124
125        assert_ne!(with_bob.public_key(), with_carol.public_key());
126    }
127
128    #[test]
129    fn from_hex_rejects_invalid_input() {
130        let err = SharedKey::from_hex("not-a-hex-string").unwrap_err();
131        assert!(matches!(err, MostroError::MostroInternalErr(_)));
132    }
133}