Skip to main content

mostro_core/chat/
keys.rs

1//! Domain-separated chat key derivation (`K_conv` / `K_sign`).
2//!
3//! The ECDH shared secret between two trade keys (or admin ↔ party trade key)
4//! is **not** used on the wire. ECDH itself is computed by the local
5//! [`generate_shared_key`] helper (`nostr::util::generate_shared_key` is
6//! crate-private in 0.45). HKDF-SHA256 then splits that secret into:
7//!
8//! * [`K_conv`](derive_chat_keys) — NIP-44 encryption and the outer `p` tag
9//! * [`K_sign`](derive_chat_keys) — signs the outer kind 14 event (author filter)
10//!
11//! See <https://mostro.network/protocol/chat.html#key-derivation>.
12
13// Leading `::` selects the `hkdf` crate: `nostr_sdk::prelude` also exports a
14// module by that name, so a plain `use hkdf::Hkdf` is ambiguous.
15use ::hkdf::Hkdf;
16use nostr_sdk::prelude::*;
17use secp256k1::{ecdh, PublicKey as Secp256k1PublicKey};
18use sha2::Sha256;
19
20use crate::error::{MostroError, ServiceError};
21
22/// HKDF `info` for `K_conv`. Changing this value changes the wire format.
23pub const CHAT_CONV_INFO: &[u8] = b"mostro:chat:conv:v1";
24/// HKDF `info` for `K_sign`. Changing this value changes the wire format.
25pub const CHAT_SIGN_INFO: &[u8] = b"mostro:chat:sign:v1";
26
27/// Raw x25519-style ECDH shared secret (even-parity assumption, per NIP-04/44).
28///
29/// Replaces `nostr::util::generate_shared_key`, which is crate-private in 0.45.
30pub(crate) fn generate_shared_key(
31    secret_key: &SecretKey,
32    public_key: &PublicKey,
33) -> Result<[u8; 32], MostroError> {
34    let mut compressed = [0u8; 33];
35    compressed[0] = 0x02; // assume even parity, as NIP-04/44 do
36    compressed[1..].copy_from_slice(public_key.as_bytes());
37    let normalized = Secp256k1PublicKey::from_slice(&compressed).map_err(|e| {
38        MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
39            "invalid peer pubkey: {e}"
40        )))
41    })?;
42
43    let secret_key =
44        secp256k1::SecretKey::from_byte_array(&secret_key.to_secret_bytes()).map_err(|e| {
45            MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
46                "invalid local secret key: {e}"
47            )))
48        })?;
49
50    let point = ecdh::shared_secret_point(&normalized, &secret_key);
51    let mut shared = [0u8; 32];
52    shared.copy_from_slice(&point[..32]);
53    Ok(shared)
54}
55
56/// Derive `(K_conv, K_sign)` from a party's trade keys and the peer's trade pubkey.
57///
58/// Both peers obtain the same pair by swapping arguments
59/// (`A.derive(a, B) == B.derive(b, A)`).
60pub fn derive_chat_keys(
61    own_trade: &Keys,
62    peer_trade: &PublicKey,
63) -> Result<(Keys, Keys), MostroError> {
64    let shared = generate_shared_key(own_trade.secret_key(), peer_trade)?;
65    derive_chat_keys_from_shared(&shared)
66}
67
68/// Derive `(K_conv, K_sign)` from an already-computed 32-byte ECDH secret.
69///
70/// Clients that persist the ECDH output (e.g. Mostrix `order_chat_shared_key_hex`)
71/// call this on load instead of re-running ECDH.
72pub fn derive_chat_keys_from_shared(shared: &[u8]) -> Result<(Keys, Keys), MostroError> {
73    if shared.len() != 32 {
74        return Err(MostroError::MostroInternalErr(
75            ServiceError::EncryptionError(format!(
76                "chat shared secret must be 32 bytes, got {}",
77                shared.len()
78            )),
79        ));
80    }
81    let hkdf = Hkdf::<Sha256>::new(None, shared);
82
83    let derive = |info: &[u8]| -> Result<Keys, MostroError> {
84        // Retry with a counter byte on the negligible chance that the output
85        // is not a valid secp256k1 secret key (spec requirement).
86        for counter in 0u16..=255 {
87            let mut labelled = info.to_vec();
88            if counter > 0 {
89                labelled.push(counter as u8);
90            }
91            let mut out = [0u8; 32];
92            hkdf.expand(&labelled, &mut out).map_err(|e| {
93                MostroError::MostroInternalErr(ServiceError::EncryptionError(format!(
94                    "HKDF expand failed: {e}"
95                )))
96            })?;
97            if let Ok(sk) = SecretKey::from_slice(&out) {
98                return Ok(Keys::new(sk));
99            }
100        }
101        Err(MostroError::MostroInternalErr(
102            ServiceError::EncryptionError("HKDF failed to produce a valid secret key".to_string()),
103        ))
104    };
105
106    Ok((derive(CHAT_CONV_INFO)?, derive(CHAT_SIGN_INFO)?))
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    /// Published test vector from <https://mostro.network/protocol/chat.html#test-vector>.
114    #[test]
115    fn protocol_test_vector_derived_pubkeys() {
116        let alice = Keys::parse("548f68890c49fa42f104c60352395e60ff030b0b407e955f1eed1400d6c0347a")
117            .unwrap();
118        let bob = Keys::parse("f258e73f07386d37133718b6127f873dd7c391b8f43b331ff8254034a13d2943")
119            .unwrap();
120
121        assert_eq!(
122            alice.public_key().to_hex(),
123            "000053c3b4773182e7c4c1b72b272d34be01bf4414a6a25c998977c516a46a01"
124        );
125        assert_eq!(
126            bob.public_key().to_hex(),
127            "000009ae5cff9f6ba9b05159ec5ed58c187f5882ea77c81ed5dd19163272a5d7"
128        );
129
130        let shared = generate_shared_key(alice.secret_key(), &bob.public_key()).unwrap();
131        let expected_shared =
132            SecretKey::from_hex("def6633a53d07d1e829484c4d4bdbbeed2f4b14c21743e63871c174338e39475")
133                .unwrap()
134                .to_secret_bytes();
135        assert_eq!(shared, expected_shared);
136
137        let (alice_conv, alice_sign) = derive_chat_keys(&alice, &bob.public_key()).unwrap();
138        let (bob_conv, bob_sign) = derive_chat_keys(&bob, &alice.public_key()).unwrap();
139
140        assert_eq!(alice_conv.public_key(), bob_conv.public_key());
141        assert_eq!(alice_sign.public_key(), bob_sign.public_key());
142        assert_eq!(
143            alice_conv.public_key().to_hex(),
144            "bceb1cd2a8e98ee9729122a1693edcc39c3ace04582ff96a26705c5e4078a6f2"
145        );
146        assert_eq!(
147            alice_sign.public_key().to_hex(),
148            "1dba04571059183f76b148119cfa6f8004dad30cb4e810180a6df17386a7f0b4"
149        );
150
151        let from_shared = derive_chat_keys_from_shared(&shared).unwrap();
152        assert_eq!(from_shared.0.public_key(), alice_conv.public_key());
153        assert_eq!(from_shared.1.public_key(), alice_sign.public_key());
154    }
155
156    #[test]
157    fn k_conv_cannot_derive_k_sign() {
158        let alice = Keys::generate();
159        let bob = Keys::generate();
160        let (conv, sign) = derive_chat_keys(&alice, &bob.public_key()).unwrap();
161        // Holding only K_conv must not yield K_sign (observer is read-only).
162        assert_ne!(
163            conv.secret_key().to_secret_bytes(),
164            sign.secret_key().to_secret_bytes()
165        );
166        assert_ne!(conv.public_key(), sign.public_key());
167    }
168
169    #[test]
170    fn derive_from_shared_rejects_wrong_length() {
171        let err = derive_chat_keys_from_shared(&[0u8; 16]).unwrap_err();
172        assert!(matches!(err, MostroError::MostroInternalErr(_)));
173    }
174}