Skip to main content

magic_wormhole/core/
key.rs

1use crate::core::*;
2use crypto_secretbox as secretbox;
3use crypto_secretbox::{
4    KeyInit, XSalsa20Poly1305,
5    aead::{Aead, AeadCore, generic_array::GenericArray},
6};
7use hkdf::Hkdf;
8use serde_derive::{Deserialize, Serialize};
9use sha2::{Digest, Sha256, digest::FixedOutput};
10use spake2::{Ed25519Group, Identity, Password, Spake2};
11
12/// Marker trait to give encryption keys a "purpose", to not confuse them
13///
14/// See [`Key`].
15pub trait KeyPurpose: std::fmt::Debug {}
16
17/// The type of main key of the Wormhole
18#[derive(Debug)]
19pub struct WormholeKey;
20impl KeyPurpose for WormholeKey {}
21
22/// A generic key purpose for ad-hoc subkeys or if you don't care.
23#[derive(Debug)]
24pub(crate) struct GenericKey;
25impl KeyPurpose for GenericKey {}
26
27/**
28 * The symmetric encryption key used to communicate with the other side.
29 *
30 * You don't need to do any crypto, but you might need it to derive subkeys for sub-protocols.
31 */
32#[derive(Debug, Clone, derive_more::Display)]
33#[display("{:?}", _0)]
34pub struct Key<P: KeyPurpose>(Box<secretbox::Key>, std::marker::PhantomData<P>);
35
36impl Key<WormholeKey> {
37    /**
38     * Derive the sub-key used for transit
39     *
40     * This one's a bit special, since the Wormhole's AppID is included in the purpose. Different kinds of applications
41     * can't talk to each other, not even accidentally, by design.
42     *
43     * The new key is derived with the `"{appid}/transit-key"` purpose.
44     */
45    #[cfg(feature = "transit")]
46    pub(crate) fn derive_transit_key(&self, appid: &AppID) -> Key<crate::transit::TransitKey> {
47        let transit_purpose = format!("{appid}/transit-key");
48        let derived_key = self.derive_subkey_from_purpose(&transit_purpose);
49        tracing::trace!(
50            "Input key: {}, Transit key: {}, Transit purpose: '{}'",
51            self.to_hex(),
52            derived_key.to_hex(),
53            &transit_purpose
54        );
55        derived_key
56    }
57}
58
59impl<P: KeyPurpose> Key<P> {
60    /// Create a new key
61    pub fn new(key: Box<secretbox::Key>) -> Self {
62        Self(key, std::marker::PhantomData)
63    }
64
65    /// Encode a key as a hex string
66    pub fn to_hex(&self) -> String {
67        hex::encode(*self.0)
68    }
69
70    /**
71     * Derive a new sub-key from this one
72     */
73    pub fn derive_subkey_from_purpose<NewP: KeyPurpose>(&self, purpose: &str) -> Key<NewP> {
74        Key(
75            Box::new(derive_key(&self.0, purpose.as_bytes())),
76            std::marker::PhantomData,
77        )
78    }
79}
80
81impl<P: KeyPurpose> AsRef<secretbox::Key> for Key<P> {
82    fn as_ref(&self) -> &secretbox::Key {
83        &self.0
84    }
85}
86
87impl<P: KeyPurpose> AsRef<[u8]> for Key<P> {
88    fn as_ref(&self) -> &[u8] {
89        self.0.as_slice()
90    }
91}
92
93#[derive(Serialize, Deserialize, Debug)]
94struct PhaseMessage {
95    #[serde(with = "hex::serde")]
96    pake_v1: Vec<u8>,
97}
98
99/// TODO doc
100///
101/// The "password" usually is the code, but it needs not to. The only requirement
102/// is that both sides use the same value, and agree on that.
103pub fn make_pake(password: &str, appid: &AppID) -> (Spake2<Ed25519Group>, Vec<u8>) {
104    let (pake_state, msg1) = Spake2::<Ed25519Group>::start_symmetric(
105        &Password::new(password.as_bytes()),
106        &Identity::new(appid.0.as_bytes()),
107    );
108    let pake_msg = PhaseMessage { pake_v1: msg1 };
109    let pake_msg_ser = serde_json::to_vec(&pake_msg).unwrap();
110    (pake_state, pake_msg_ser)
111}
112
113#[derive(Clone, Debug, Default, Serialize, Deserialize)]
114pub struct VersionsMessage {
115    #[serde(default)]
116    pub abilities: Vec<String>,
117    #[serde(default)]
118    pub app_versions: serde_json::Value,
119    // resume: Option<WormholeResume>,
120}
121
122impl VersionsMessage {
123    pub fn new() -> Self {
124        Default::default()
125    }
126
127    pub fn set_app_versions(&mut self, versions: serde_json::Value) {
128        self.app_versions = versions;
129    }
130
131    // pub fn add_resume_ability(&mut self, _resume: ()) {
132    //     self.abilities.push("resume-v1".into())
133    // }
134}
135
136pub fn build_version_msg(
137    side: &MySide,
138    key: &secretbox::Key,
139    versions: &VersionsMessage,
140) -> (Phase, Vec<u8>) {
141    let phase = Phase::VERSION;
142    let data_key = derive_phase_key(side, key, &phase);
143    let plaintext = serde_json::to_vec(versions).unwrap();
144    let (_nonce, encrypted) = encrypt_data(&data_key, &plaintext);
145    (phase, encrypted)
146}
147
148pub fn extract_pake_msg(body: &[u8]) -> Result<Vec<u8>, WormholeError> {
149    serde_json::from_slice(body)
150        .map(|res: PhaseMessage| res.pake_v1)
151        .map_err(WormholeError::ProtocolJson)
152}
153
154fn encrypt_data_with_nonce(
155    key: &secretbox::Key,
156    plaintext: &[u8],
157    nonce: &secretbox::Nonce,
158) -> Vec<u8> {
159    let cipher = XSalsa20Poly1305::new(GenericArray::from_slice(key));
160    let mut ciphertext = cipher.encrypt(nonce, plaintext).unwrap();
161    let mut nonce_and_ciphertext = vec![];
162    nonce_and_ciphertext.extend_from_slice(nonce);
163    nonce_and_ciphertext.append(&mut ciphertext);
164    nonce_and_ciphertext
165}
166
167pub fn encrypt_data(key: &secretbox::Key, plaintext: &[u8]) -> (secretbox::Nonce, Vec<u8>) {
168    let nonce = secretbox::SecretBox::<secretbox::XSalsa20Poly1305>::generate_nonce(
169        &mut rand::thread_rng(),
170    );
171    let nonce_and_ciphertext = encrypt_data_with_nonce(key, plaintext, &nonce);
172    (nonce, nonce_and_ciphertext)
173}
174
175// TODO: return a Result with a proper error type
176pub fn decrypt_data(key: &secretbox::Key, encrypted: &[u8]) -> Option<Vec<u8>> {
177    use secretbox::aead::generic_array::typenum::marker_traits::Unsigned;
178    let nonce_size = <XSalsa20Poly1305 as AeadCore>::NonceSize::to_usize();
179    let (nonce, ciphertext) = encrypted.split_at(nonce_size);
180    assert_eq!(nonce.len(), nonce_size);
181    let cipher = XSalsa20Poly1305::new(GenericArray::from_slice(key));
182    cipher
183        .decrypt(GenericArray::from_slice(nonce), ciphertext)
184        .ok()
185}
186
187fn sha256_digest(input: &[u8]) -> Vec<u8> {
188    let mut hasher = Sha256::default();
189    hasher.update(input);
190    hasher.finalize_fixed().to_vec()
191}
192
193pub fn derive_key(key: &secretbox::Key, purpose: &[u8]) -> secretbox::Key {
194    let hk = Hkdf::<Sha256>::new(None, key);
195    let mut key = secretbox::Key::default();
196    hk.expand(purpose, &mut key).unwrap();
197    key
198}
199
200pub fn derive_phase_key(side: &EitherSide, key: &secretbox::Key, phase: &Phase) -> secretbox::Key {
201    let side_digest: Vec<u8> = sha256_digest(side.0.as_bytes());
202    let phase_digest: Vec<u8> = sha256_digest(phase.0.as_bytes());
203    let mut purpose_vec: Vec<u8> = b"wormhole:phase:".to_vec();
204    purpose_vec.extend(side_digest);
205    purpose_vec.extend(phase_digest);
206
207    derive_key(key, &purpose_vec)
208}
209
210pub fn derive_verifier(key: &crypto_secretbox::Key) -> crypto_secretbox::Key {
211    derive_key(key, b"wormhole:verifier")
212}
213
214#[cfg(test)]
215mod test {
216    use super::*;
217    use crate::core::EitherSide;
218
219    #[test]
220    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
221    fn test_extract_pake_msg() {
222        // let _key = super::KeyMachine::new(
223        //     &AppID::new("appid"),
224        //     &MySide::unchecked_from_string(String::from("side1")),
225        //     json!({}),
226        // );
227
228        let s1 = "7b2270616b655f7631223a22353337363331646366643064336164386130346234663531643935336131343563386538626663373830646461393834373934656634666136656536306339663665227d";
229        let pake_msg = super::extract_pake_msg(&hex::decode(s1).unwrap());
230        assert_eq!(
231            pake_msg.ok(),
232            Some(
233                hex::decode("537631dcfd0d3ad8a04b4f51d953a145c8e8bfc780dda984794ef4fa6ee60c9f6e")
234                    .unwrap()
235            )
236        );
237    }
238
239    #[test]
240    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
241    fn test_derive_key() {
242        let main = secretbox::Key::from_exact_iter(
243            hex::decode("588ba9eef353778b074413a0140205d90d7479e36e0dd4ee35bb729d26131ef1")
244                .unwrap(),
245        )
246        .unwrap();
247        let dk1 = derive_key(&main, b"purpose1");
248        assert_eq!(
249            hex::encode(dk1),
250            "835b5df80ce9ca46908e8524fb308649122cfbcefbeaa7e65061c6ef08ee1b2a"
251        );
252
253        /* The API doesn't support non-standard length keys anymore.
254         * But we may want to add that back in in the future.
255         */
256        // let dk2 = derive_key(&main, b"purpose2", 10);
257        // assert_eq!(hex::encode(dk2), "f2238e84315b47eb6279");
258    }
259
260    #[test]
261    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
262    fn test_derive_phase_key() {
263        let main = secretbox::Key::from_exact_iter(
264            hex::decode("588ba9eef353778b074413a0140205d90d7479e36e0dd4ee35bb729d26131ef1")
265                .unwrap(),
266        )
267        .unwrap();
268        let dk11 = derive_phase_key(&EitherSide::from("side1"), &main, &Phase("phase1".into()));
269        assert_eq!(
270            hex::encode(&*dk11),
271            "3af6a61d1a111225cc8968c6ca6265efe892065c3ab46de79dda21306b062990"
272        );
273        let dk12 = derive_phase_key(&EitherSide::from("side1"), &main, &Phase("phase2".into()));
274        assert_eq!(
275            hex::encode(&*dk12),
276            "88a1dd12182d989ff498022a9656d1e2806f17328d8bf5d8d0c9753e4381a752"
277        );
278        let dk21 = derive_phase_key(&EitherSide::from("side2"), &main, &Phase("phase1".into()));
279        assert_eq!(
280            hex::encode(&*dk21),
281            "a306627b436ec23bdae3af8fa90c9ac927780d86be1831003e7f617c518ea689"
282        );
283        let dk22 = derive_phase_key(&EitherSide::from("side2"), &main, &Phase("phase2".into()));
284        assert_eq!(
285            hex::encode(&*dk22),
286            "bf99e3e16420f2dad33f9b1ccb0be1462b253d639dacdb50ed9496fa528d8758"
287        );
288    }
289
290    #[test]
291    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
292    fn test_derive_phase_key2() {
293        // feed python's derive_phase_key with these inputs:
294        // key = b"key"
295        // side = u"side"
296        // phase = u"phase1"
297        // output of derive_phase_key is:
298        // "\xfe\x93\x15r\x96h\xa6'\x8a\x97D\x9d\xc9\x9a_L!\x02\xa6h\xc6\x8538\x15)\x06\xbbuRj\x96"
299        // hexlified output: fe9315729668a6278a97449dc99a5f4c2102a668c6853338152906bb75526a96
300        // let _k = KeyMachine::new(
301        //     &AppID::new("appid1"),
302        //     &MySide::unchecked_from_string(String::from("side")),
303        //     json!({}),
304        // );
305
306        /* This test is disabled for now because the used key length is not compatible with our API */
307        // let key = Key(b"key".to_vec());
308        // let side = "side";
309        // let phase = Phase(String::from("phase1"));
310        // let phase1_key = derive_phase_key(&EitherSide::from(side), &key, &phase);
311
312        // assert_eq!(
313        //     hex::encode(&*phase1_key),
314        //     "fe9315729668a6278a97449dc99a5f4c2102a668c6853338152906bb75526a96"
315        // );
316    }
317
318    #[test]
319    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
320    fn test_encrypt_data() {
321        let k = secretbox::Key::from_exact_iter(
322            hex::decode("ddc543ef8e4629a603d39dd0307a51bb1e7adb9cb259f6b085c91d0842a18679")
323                .unwrap(),
324        )
325        .unwrap();
326        let plaintext = hex::decode("edc089a518219ec1cee184e89d2d37af").unwrap();
327        assert_eq!(plaintext.len(), 16);
328        let nonce = secretbox::Nonce::from_exact_iter(
329            hex::decode("2d5e43eb465aa42e750f991e425bee485f06abad7e04af80").unwrap(),
330        )
331        .unwrap();
332        assert_eq!(nonce.len(), 24);
333        let msg = encrypt_data_with_nonce(&k, &plaintext, &nonce);
334        assert_eq!(
335            hex::encode(msg),
336            "2d5e43eb465aa42e750f991e425bee485f06abad7e04af80fe318e39d0e4ce932d2b54b300c56d2cda55ee5f0488d63eb1d5f76f7919a49a"
337        );
338    }
339
340    #[test]
341    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
342    fn test_decrypt_data() {
343        let k = secretbox::Key::from_exact_iter(
344            hex::decode("ddc543ef8e4629a603d39dd0307a51bb1e7adb9cb259f6b085c91d0842a18679")
345                .unwrap(),
346        )
347        .unwrap();
348        let encrypted = hex::decode("2d5e43eb465aa42e750f991e425bee485f06abad7e04af80fe318e39d0e4ce932d2b54b300c56d2cda55ee5f0488d63eb1d5f76f7919a49a").unwrap();
349        match decrypt_data(&k, &encrypted) {
350            Some(plaintext) => {
351                assert_eq!(hex::encode(plaintext), "edc089a518219ec1cee184e89d2d37af");
352            },
353            None => {
354                panic!("failed to decrypt");
355            },
356        };
357    }
358
359    /* This test is disabled for now because the used key length is not compatible with our API */
360    // #[test]
361    // fn test_encrypt_data_decrypt_data_roundtrip() {
362    //     let key = Key(b"key".to_vec());
363    //     let side = "side";
364    //     let phase = Phase(String::from("phase"));
365    //     let data_key = derive_phase_key(&EitherSide::from(side), &key, &phase);
366    //     let plaintext = "hello world";
367
368    //     let (_nonce, encrypted) = encrypt_data(&data_key, &plaintext.as_bytes());
369    //     let maybe_plaintext = decrypt_data(&data_key, &encrypted);
370    //     match maybe_plaintext {
371    //         Some(plaintext_decrypted) => {
372    //             assert_eq!(plaintext.as_bytes().to_vec(), plaintext_decrypted);
373    //         },
374    //         None => panic!(),
375    //     }
376    // }
377}