p2panda_encryption/two_party/
x3dh.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Extended Triple Diffie-Hellman (X3DH) key agreement protocol as specified by Signal.
4//!
5//! X3DH establishes a shared secret key between two parties who mutually authenticate each other
6//! based on public keys. X3DH provides forward secrecy and cryptographic deniability.
7//!
8//! X3DH is designed for asynchronous settings where one user ("Bob") is offline but has published
9//! public key bundles in the network. Another user ("Alice") wants to use that information to send
10//! encrypted data to Bob, and also establish a shared secret key for future communication.
11//!
12//! <https://signal.org/docs/specifications/x3dh/>
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::crypto::aead::{AeadError, AeadNonce, aead_decrypt, aead_encrypt};
17use crate::crypto::hkdf::{HkdfError, hkdf};
18use crate::crypto::x25519::{PublicKey, SecretKey, X25519Error};
19use crate::crypto::{Rng, RngError};
20use crate::key_bundle::{KeyBundleError, OneTimePreKeyId, PreKeyId};
21use crate::traits::KeyBundle;
22
23/// ASCII string identifying the application as specified in X3DH used for KDF.
24const KDF_INFO: &[u8; 7] = b"p2panda";
25
26/// Message containing encrypted payload and X3DH session-data to be delivered from sender to
27/// receiver.
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub struct X3dhCiphertext {
30    /// Identity of the sender.
31    pub identity_key: PublicKey,
32
33    /// Identifier of the used one-time pre-key. Is none when no one-time key was used (for example
34    /// in long-term key bundles).
35    pub onetime_prekey_id: Option<OneTimePreKeyId>,
36
37    /// Identifier of the used long-term pre-key.
38    pub prekey_id: PreKeyId,
39
40    /// Encrypted payload for the receiver.
41    pub ciphertext: Vec<u8>,
42
43    /// Ephemeral public key used for this session.
44    pub ephemeral_key: PublicKey,
45}
46
47/// Encrypt message towards a receiver using X3DH protocol with their public pre-key material.
48pub fn x3dh_encrypt<KB: KeyBundle>(
49    plaintext: &[u8],
50    our_identity_secret: &SecretKey,
51    their_prekey_bundle: &KB,
52    rng: &Rng,
53) -> Result<X3dhCiphertext, X3dhError> {
54    // Ensure we're not using an expired bundle of receiver.
55    their_prekey_bundle.verify()?;
56
57    let our_identity_key = our_identity_secret.public_key()?;
58
59    let our_ephemeral_secret = SecretKey::from_bytes(rng.random_array()?);
60    let our_ephemeral_key = our_ephemeral_secret.public_key()?;
61
62    let mut ikm = Vec::with_capacity({
63        if their_prekey_bundle.onetime_prekey().is_none() {
64            32 * 4
65        } else {
66            32 * 5
67        }
68    });
69
70    ikm.extend_from_slice(&[0xFFu8; 32]); // "discontinuity bytes"
71
72    // DH1 = DH(IKA, SPKB)
73    ikm.extend_from_slice(
74        &our_identity_secret.calculate_agreement(their_prekey_bundle.signed_prekey())?,
75    );
76
77    // DH2 = DH(EKA, IKB)
78    ikm.extend_from_slice(
79        &our_ephemeral_secret.calculate_agreement(their_prekey_bundle.identity_key())?,
80    );
81
82    // DH3 = DH(EKA, SPKB)
83    ikm.extend_from_slice(
84        &our_ephemeral_secret.calculate_agreement(their_prekey_bundle.signed_prekey())?,
85    );
86
87    // DH4 = DH(EKA, OPKB)
88    if let Some(onetime_prekey) = their_prekey_bundle.onetime_prekey() {
89        ikm.extend_from_slice(&our_ephemeral_secret.calculate_agreement(onetime_prekey)?);
90    }
91
92    // SK = KDF(DH1 || DH2 || DH3 || DH4)
93    let sk: [u8; 32] = {
94        let salt = vec![0_u8; 32];
95        hkdf(&salt, &ikm, Some(KDF_INFO))?
96    };
97
98    drop(our_ephemeral_secret);
99    drop(ikm);
100
101    // AD = Encode(IKA) || Encode(IKB)
102    let ad = {
103        let mut buf = Vec::new();
104        buf.extend_from_slice(our_identity_key.as_bytes());
105        buf.extend_from_slice(their_prekey_bundle.identity_key().as_bytes());
106        buf
107    };
108
109    let nonce: AeadNonce = hkdf(b"", &sk, None)?;
110    let ciphertext = aead_encrypt(&sk, plaintext, nonce, Some(&ad))?;
111
112    Ok(X3dhCiphertext {
113        ciphertext,
114        ephemeral_key: our_ephemeral_key,
115        identity_key: our_identity_key,
116        prekey_id: *their_prekey_bundle.signed_prekey(),
117        onetime_prekey_id: their_prekey_bundle.onetime_prekey_id(),
118    })
119}
120
121/// Decrypt message using the X3DH protocol and the secrets of the key material the sender used to
122/// encrypt the payload towards us.
123///
124/// Note that an application using X3DH should reject the received ciphertext when an expired
125/// pre-key or already used one-time pre-key was used by the sender.
126pub fn x3dh_decrypt(
127    their_ciphertext: &X3dhCiphertext,
128    our_identity_secret: &SecretKey,
129    our_prekey_secret: &SecretKey,
130    our_onetime_secret: Option<&SecretKey>,
131) -> Result<Vec<u8>, X3dhError> {
132    let our_identity_key = our_identity_secret.public_key()?;
133
134    let mut ikm = Vec::with_capacity(if our_onetime_secret.is_none() {
135        32 * 4
136    } else {
137        32 * 5
138    });
139
140    ikm.extend_from_slice(&[0xFFu8; 32]); // "discontinuity bytes"
141
142    // DH1 = DH(IKA, SPKB)
143    ikm.extend_from_slice(&our_prekey_secret.calculate_agreement(&their_ciphertext.identity_key)?);
144
145    // DH2 = DH(EKA, IKB)
146    ikm.extend_from_slice(
147        &our_identity_secret.calculate_agreement(&their_ciphertext.ephemeral_key)?,
148    );
149
150    // DH3 = DH(EKA, SPKB)
151    ikm.extend_from_slice(&our_prekey_secret.calculate_agreement(&their_ciphertext.ephemeral_key)?);
152
153    // DH4 = DH(EKA, OPKB)
154    if let Some(our_onetime_secret) = our_onetime_secret {
155        ikm.extend_from_slice(
156            &our_onetime_secret.calculate_agreement(&their_ciphertext.ephemeral_key)?,
157        );
158    }
159
160    // SK = KDF(DH1 || DH2 || DH3 || DH4)
161    let sk: [u8; 32] = {
162        let salt = vec![0_u8; 32];
163        hkdf(&salt, &ikm, Some(KDF_INFO))?
164    };
165
166    drop(ikm);
167
168    // AD = Encode(IKA) || Encode(IKB)
169    let ad = {
170        let mut buf = Vec::new();
171        buf.extend_from_slice(their_ciphertext.identity_key.as_bytes());
172        buf.extend_from_slice(our_identity_key.as_bytes());
173        buf
174    };
175
176    let nonce: AeadNonce = hkdf(b"", &sk, None)?;
177    let plaintext = aead_decrypt(&sk, &their_ciphertext.ciphertext, nonce, Some(&ad))?;
178
179    Ok(plaintext)
180}
181
182#[derive(Debug, Error)]
183pub enum X3dhError {
184    #[error(transparent)]
185    Rng(#[from] RngError),
186
187    #[error(transparent)]
188    Aead(#[from] AeadError),
189
190    #[error(transparent)]
191    Hkdf(#[from] HkdfError),
192
193    #[error(transparent)]
194    X25519(#[from] X25519Error),
195
196    #[error(transparent)]
197    KeyBundle(#[from] KeyBundleError),
198}
199
200#[cfg(test)]
201mod tests {
202    use crate::crypto::Rng;
203    use crate::crypto::x25519::SecretKey;
204    use crate::key_bundle::{Lifetime, LongTermKeyBundle, OneTimeKeyBundle, OneTimePreKey, PreKey};
205
206    use super::{x3dh_decrypt, x3dh_encrypt};
207
208    #[test]
209    fn encrypt_decrypt() {
210        let rng = Rng::from_seed([1; 32]);
211
212        let bob_identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
213
214        let bob_prekey_secret = SecretKey::from_bytes(rng.random_array().unwrap());
215        let bob_signed_prekey =
216            PreKey::new(bob_prekey_secret.public_key().unwrap(), Lifetime::default());
217
218        let bob_onetime_secret = SecretKey::from_bytes(rng.random_array().unwrap());
219        let bob_onetime_prekey = OneTimePreKey::new(bob_onetime_secret.public_key().unwrap(), 2);
220
221        let bob_prekey_signature = bob_signed_prekey.sign(&bob_identity_secret, &rng).unwrap();
222
223        let bob_prekey_bundle = OneTimeKeyBundle::new(
224            bob_identity_secret.public_key().unwrap(),
225            bob_signed_prekey,
226            bob_prekey_signature,
227            Some(bob_onetime_prekey),
228        );
229
230        let alice_identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
231
232        let ciphertext = x3dh_encrypt(
233            b"Hello, Panda!",
234            &alice_identity_secret,
235            &bob_prekey_bundle,
236            &rng,
237        )
238        .unwrap();
239
240        let plaintext = x3dh_decrypt(
241            &ciphertext,
242            &bob_identity_secret,
243            &bob_prekey_secret,
244            Some(&bob_onetime_secret),
245        )
246        .unwrap();
247
248        assert_eq!(b"Hello, Panda!", plaintext.as_slice());
249    }
250
251    #[test]
252    fn longterm_key_bundle() {
253        let rng = Rng::from_seed([1; 32]);
254
255        let bob_identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
256
257        let bob_prekey_secret = SecretKey::from_bytes(rng.random_array().unwrap());
258        let bob_signed_prekey =
259            PreKey::new(bob_prekey_secret.public_key().unwrap(), Lifetime::default());
260
261        let bob_prekey_signature = bob_signed_prekey.sign(&bob_identity_secret, &rng).unwrap();
262
263        let bob_prekey_bundle = LongTermKeyBundle::new(
264            bob_identity_secret.public_key().unwrap(),
265            bob_signed_prekey,
266            bob_prekey_signature,
267        );
268
269        let alice_identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
270
271        let ciphertext = x3dh_encrypt(
272            b"Hello, Panda!",
273            &alice_identity_secret,
274            &bob_prekey_bundle,
275            &rng,
276        )
277        .unwrap();
278
279        let plaintext =
280            x3dh_decrypt(&ciphertext, &bob_identity_secret, &bob_prekey_secret, None).unwrap();
281
282        assert_eq!(b"Hello, Panda!", plaintext.as_slice());
283    }
284}