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, generate_shared_key};
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 = generate_shared_key(secret, counterparty)?;
34        let secret = SecretKey::from_slice(&bytes).map_err(|e| {
35            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
36                "invalid shared secret: {e}"
37            )))
38        })?;
39        Ok(Self(Keys::new(secret)))
40    }
41
42    /// Build a `SharedKey` from an already-derived `Keys` value.
43    pub fn from_keys(keys: Keys) -> Self {
44        Self(keys)
45    }
46
47    /// Borrow the underlying ECDH `Keys` (IKM as a keypair).
48    ///
49    /// For gift-wrap dual-read decrypt only. New envelopes use [`Self::chat_keys`].
50    pub fn keys(&self) -> &Keys {
51        &self.0
52    }
53
54    /// Public key of the raw ECDH secret interpreted as a keypair.
55    ///
56    /// This was the GiftWrap `p` tag under the superseded envelope. The new
57    /// envelope uses `pub(K_conv)` from [`Self::chat_keys`] instead.
58    pub fn public_key(&self) -> PublicKey {
59        self.0.public_key()
60    }
61
62    /// Borrow the underlying ECDH secret key.
63    pub fn secret_key(&self) -> &SecretKey {
64        self.0.secret_key()
65    }
66
67    /// Derive `(K_conv, K_sign)` from this ECDH secret.
68    pub fn chat_keys(&self) -> Result<(Keys, Keys), MostroError> {
69        derive_chat_keys_from_shared(self.secret_key().as_secret_bytes())
70    }
71
72    /// Serialize the ECDH secret as lower-case hex for client persistence.
73    pub fn to_hex(&self) -> String {
74        self.0.secret_key().to_secret_hex()
75    }
76
77    /// Rebuild from hex previously produced by [`SharedKey::to_hex`].
78    pub fn from_hex(hex: &str) -> Result<Self, MostroError> {
79        let secret = SecretKey::from_hex(hex).map_err(|e| {
80            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
81                "invalid shared key hex: {e}"
82            )))
83        })?;
84        Ok(Self(Keys::new(secret)))
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn derive_is_symmetric_between_peers() {
94        let alice = Keys::generate();
95        let bob = Keys::generate();
96
97        let from_alice = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
98        let from_bob = SharedKey::derive(bob.secret_key(), &alice.public_key()).unwrap();
99
100        assert_eq!(from_alice.public_key(), from_bob.public_key());
101        assert_eq!(from_alice.to_hex(), from_bob.to_hex());
102
103        let (ac, as_) = from_alice.chat_keys().unwrap();
104        let (bc, bs) = from_bob.chat_keys().unwrap();
105        assert_eq!(ac.public_key(), bc.public_key());
106        assert_eq!(as_.public_key(), bs.public_key());
107    }
108
109    #[test]
110    fn derive_shared_key_hex_roundtrip() {
111        let alice = Keys::generate();
112        let bob = Keys::generate();
113        let derived = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
114
115        let hex = derived.to_hex();
116        let restored = SharedKey::from_hex(&hex).unwrap();
117
118        assert_eq!(derived.public_key(), restored.public_key());
119        assert_eq!(derived.to_hex(), restored.to_hex());
120    }
121
122    #[test]
123    fn derive_shared_key_different_peers_produce_different_keys() {
124        let alice = Keys::generate();
125        let bob = Keys::generate();
126        let carol = Keys::generate();
127
128        let with_bob = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
129        let with_carol = SharedKey::derive(alice.secret_key(), &carol.public_key()).unwrap();
130
131        assert_ne!(with_bob.public_key(), with_carol.public_key());
132    }
133
134    #[test]
135    fn from_hex_rejects_invalid_input() {
136        let err = SharedKey::from_hex("not-a-hex-string").unwrap_err();
137        assert!(matches!(err, MostroError::MostroInternalErr(_)));
138    }
139}