Skip to main content

mostro_core/chat/
shared_key.rs

1//! ECDH shared secret used as IKM for Mostro P2P chat key derivation.
2//!
3//! The two parties of a chat (buyer/seller during a trade or admin/party
4//! during a dispute) each compute the same 32-byte ECDH output from their
5//! own secret key and the counterparty's public key. That output is the
6//! input keying material for [`crate::chat::derive_chat_keys_from_shared`],
7//! which produces `K_conv` and `K_sign`.
8//!
9//! Clients MAY persist the ECDH secret (via [`SharedKey::to_hex`]) and
10//! re-derive chat keys on load. The ECDH secret itself is **not** the wire
11//! address: `pub(K_conv)` is the `p` tag and `pub(K_sign)` is the author.
12
13use nostr_sdk::prelude::*;
14
15use crate::chat::keys::derive_chat_keys_from_shared;
16use crate::error::{MostroError, ServiceError};
17
18/// Shared ECDH secret between two parties' trade (or admin) keys.
19///
20/// Internally a `Keys` instance whose secret is the 32-byte ECDH output of
21/// `(local_secret, counterparty_pubkey)`. Prefer [`SharedKey::chat_keys`] for
22/// the on-the-wire `K_conv` / `K_sign` pair.
23#[derive(Debug, Clone)]
24pub struct SharedKey(Keys);
25
26impl SharedKey {
27    /// Derive the ECDH shared secret from a local secret key and the
28    /// counterparty's public key.
29    ///
30    /// Both peers obtain the same `SharedKey` by swapping arguments
31    /// (`A.derive(a_sk, b_pk) == B.derive(b_sk, a_pk)`).
32    pub fn derive(secret: &SecretKey, counterparty: &PublicKey) -> Result<Self, MostroError> {
33        let bytes = nostr_sdk::util::generate_shared_key(secret, counterparty).map_err(|e| {
34            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
35                "shared key derivation failed: {e}"
36            )))
37        })?;
38        let secret = SecretKey::from_slice(&bytes).map_err(|e| {
39            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
40                "invalid shared secret: {e}"
41            )))
42        })?;
43        Ok(Self(Keys::new(secret)))
44    }
45
46    /// Build a `SharedKey` from an already-derived `Keys` value.
47    pub fn from_keys(keys: Keys) -> Self {
48        Self(keys)
49    }
50
51    /// Borrow the underlying ECDH `Keys` (IKM as a keypair).
52    ///
53    /// For gift-wrap dual-read decrypt only. New envelopes use [`Self::chat_keys`].
54    pub fn keys(&self) -> &Keys {
55        &self.0
56    }
57
58    /// Public key of the raw ECDH secret interpreted as a keypair.
59    ///
60    /// This was the GiftWrap `p` tag under the superseded envelope. The new
61    /// envelope uses `pub(K_conv)` from [`Self::chat_keys`] instead.
62    pub fn public_key(&self) -> PublicKey {
63        self.0.public_key()
64    }
65
66    /// Borrow the underlying ECDH secret key.
67    pub fn secret_key(&self) -> &SecretKey {
68        self.0.secret_key()
69    }
70
71    /// Derive `(K_conv, K_sign)` from this ECDH secret.
72    pub fn chat_keys(&self) -> Result<(Keys, Keys), MostroError> {
73        derive_chat_keys_from_shared(self.secret_key().as_secret_bytes())
74    }
75
76    /// Serialize the ECDH secret as lower-case hex for client persistence.
77    pub fn to_hex(&self) -> String {
78        self.0.secret_key().to_secret_hex()
79    }
80
81    /// Rebuild from hex previously produced by [`SharedKey::to_hex`].
82    pub fn from_hex(hex: &str) -> Result<Self, MostroError> {
83        let secret = SecretKey::from_hex(hex).map_err(|e| {
84            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
85                "invalid shared key hex: {e}"
86            )))
87        })?;
88        Ok(Self(Keys::new(secret)))
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn derive_is_symmetric_between_peers() {
98        let alice = Keys::generate();
99        let bob = Keys::generate();
100
101        let from_alice = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
102        let from_bob = SharedKey::derive(bob.secret_key(), &alice.public_key()).unwrap();
103
104        assert_eq!(from_alice.public_key(), from_bob.public_key());
105        assert_eq!(from_alice.to_hex(), from_bob.to_hex());
106
107        let (ac, as_) = from_alice.chat_keys().unwrap();
108        let (bc, bs) = from_bob.chat_keys().unwrap();
109        assert_eq!(ac.public_key(), bc.public_key());
110        assert_eq!(as_.public_key(), bs.public_key());
111    }
112
113    #[test]
114    fn derive_shared_key_hex_roundtrip() {
115        let alice = Keys::generate();
116        let bob = Keys::generate();
117        let derived = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
118
119        let hex = derived.to_hex();
120        let restored = SharedKey::from_hex(&hex).unwrap();
121
122        assert_eq!(derived.public_key(), restored.public_key());
123        assert_eq!(derived.to_hex(), restored.to_hex());
124    }
125
126    #[test]
127    fn derive_shared_key_different_peers_produce_different_keys() {
128        let alice = Keys::generate();
129        let bob = Keys::generate();
130        let carol = Keys::generate();
131
132        let with_bob = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
133        let with_carol = SharedKey::derive(alice.secret_key(), &carol.public_key()).unwrap();
134
135        assert_ne!(with_bob.public_key(), with_carol.public_key());
136    }
137
138    #[test]
139    fn from_hex_rejects_invalid_input() {
140        let err = SharedKey::from_hex("not-a-hex-string").unwrap_err();
141        assert!(matches!(err, MostroError::MostroInternalErr(_)));
142    }
143}