sync15/bso/
crypto.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! Support for "encrypted bso"s, as received by the storage servers.
6//! This module decrypts them into IncomingBso's suitable for use by the
7//! engines.
8use super::{IncomingBso, IncomingEnvelope, OutgoingBso, OutgoingEnvelope};
9use crate::error;
10use crate::key_bundle::KeyBundle;
11use crate::EncryptedPayload;
12use serde::{de::DeserializeOwned, Deserialize, Serialize};
13
14// The BSO implementation we use for encrypted payloads.
15// Note that this is almost identical to the IncomingBso implementations, except
16// instead of a String payload we use an EncryptedPayload. Obviously we *could*
17// just use a String payload and transform it into an EncryptedPayload - any maybe we
18// should - but this is marginally optimal in terms of deserialization.
19#[derive(Deserialize, Debug)]
20pub struct IncomingEncryptedBso {
21    #[serde(flatten)]
22    pub envelope: IncomingEnvelope,
23    #[serde(
24        with = "as_json",
25        bound(deserialize = "EncryptedPayload: DeserializeOwned")
26    )]
27    pub(crate) payload: EncryptedPayload,
28}
29
30impl IncomingEncryptedBso {
31    pub fn new(envelope: IncomingEnvelope, payload: EncryptedPayload) -> Self {
32        Self { envelope, payload }
33    }
34    /// Decrypt a BSO, consuming it into a clear-text version.
35    pub fn into_decrypted(self, key: &KeyBundle) -> error::Result<IncomingBso> {
36        Ok(IncomingBso::new(self.envelope, self.payload.decrypt(key)?))
37    }
38}
39
40#[derive(Serialize, Debug)]
41pub struct OutgoingEncryptedBso {
42    #[serde(flatten)]
43    pub envelope: OutgoingEnvelope,
44    #[serde(with = "as_json", bound(serialize = "EncryptedPayload: Serialize"))]
45    payload: EncryptedPayload,
46}
47
48impl OutgoingEncryptedBso {
49    pub fn new(envelope: OutgoingEnvelope, payload: EncryptedPayload) -> Self {
50        Self { envelope, payload }
51    }
52
53    #[inline]
54    pub fn serialized_payload_len(&self) -> usize {
55        self.payload.serialized_len()
56    }
57}
58
59impl OutgoingBso {
60    pub fn into_encrypted(self, key: &KeyBundle) -> error::Result<OutgoingEncryptedBso> {
61        Ok(OutgoingEncryptedBso {
62            envelope: self.envelope,
63            payload: EncryptedPayload::from_cleartext(key, self.payload)?,
64        })
65    }
66}
67
68// The BSOs we write to the servers expect a "payload" attribute which is a JSON serialized
69// string, rather than the JSON representation of the object.
70// ie, the serialized object is expected to look like:
71// `{"id": "some-guid", "payload": "{\"IV\": ... }"}` <-- payload is a string.
72// However, if we just serialize it directly, we end up with:
73// `{"id": "some-guid", "payload": {"IV":  ... }}` <-- payload is an object.
74// The magic here means we can serialize and deserialize directly into/from the object, correctly
75// working with the payload as a string, instead of needing to explicitly stringify/parse the
76// payload as an extra step.
77//
78// This would work for any <T>, but we only use it for EncryptedPayload - the way our cleartext
79// BSOs work mean it's not necessary there as they define the payload as a String - ie, they do
80// explicitly end up doing 2 JSON operations as an ergonomic design choice.
81mod as_json {
82    use serde::de::{self, Deserialize, DeserializeOwned, Deserializer};
83    use serde::ser::{self, Serialize, Serializer};
84
85    pub fn serialize<T, S>(t: &T, serializer: S) -> Result<S::Ok, S::Error>
86    where
87        T: Serialize,
88        S: Serializer,
89    {
90        let j = serde_json::to_string(t).map_err(ser::Error::custom)?;
91        serializer.serialize_str(&j)
92    }
93
94    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
95    where
96        T: DeserializeOwned,
97        D: Deserializer<'de>,
98    {
99        let j = String::deserialize(deserializer)?;
100        serde_json::from_str(&j).map_err(de::Error::custom)
101    }
102}
103
104// Lots of stuff for testing the sizes of encrypted records, because the servers have
105// certain limits in terms of max-POST sizes, forcing us to chunk uploads, but
106// we need to calculate based on encrypted record size rather than the raw <T> size.
107//
108// This is a little cludgey but I couldn't think of another way to have easy deserialization
109// without a bunch of wrapper types, while still only serializing a single time in the
110// postqueue.
111#[cfg(test)]
112impl OutgoingEncryptedBso {
113    /// Return the length of the serialized payload.
114    pub fn payload_serialized_len(&self) -> usize {
115        self.payload.serialized_len()
116    }
117
118    // self.payload is private, but tests want to create funky things.
119    // XXX - test only, but test in another crate :(
120    //#[cfg(test)]
121    pub fn make_test_bso(ciphertext: String) -> Self {
122        Self {
123            envelope: OutgoingEnvelope {
124                id: "".into(),
125                sortindex: None,
126                ttl: None,
127            },
128            payload: EncryptedPayload {
129                iv: "".into(),
130                hmac: "".into(),
131                ciphertext,
132            },
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::bso::OutgoingEnvelope;
141
142    #[test]
143    fn test_deserialize_enc() {
144        let serialized = r#"{
145            "id": "1234",
146            "collection": "passwords",
147            "modified": 12344321.0,
148            "payload": "{\"IV\": \"aaaaa\", \"hmac\": \"bbbbb\", \"ciphertext\": \"ccccc\"}"
149        }"#;
150        let record: IncomingEncryptedBso = serde_json::from_str(serialized).unwrap();
151        assert_eq!(&record.envelope.id, "1234");
152        assert_eq!((record.envelope.modified.0 - 12_344_321_000).abs(), 0);
153        assert_eq!(record.envelope.sortindex, None);
154        assert_eq!(&record.payload.iv, "aaaaa");
155        assert_eq!(&record.payload.hmac, "bbbbb");
156        assert_eq!(&record.payload.ciphertext, "ccccc");
157    }
158
159    #[test]
160    fn test_deserialize_autofields() {
161        let serialized = r#"{
162            "id": "1234",
163            "collection": "passwords",
164            "modified": 12344321.0,
165            "sortindex": 100,
166            "ttl": 99,
167            "payload": "{\"IV\": \"aaaaa\", \"hmac\": \"bbbbb\", \"ciphertext\": \"ccccc\"}"
168        }"#;
169        let record: IncomingEncryptedBso = serde_json::from_str(serialized).unwrap();
170        assert_eq!(record.envelope.sortindex, Some(100));
171        assert_eq!(record.envelope.ttl, Some(99));
172    }
173
174    #[test]
175    fn test_serialize_enc() {
176        let goal = r#"{"id":"1234","payload":"{\"IV\":\"aaaaa\",\"hmac\":\"bbbbb\",\"ciphertext\":\"ccccc\"}"}"#;
177        let record = OutgoingEncryptedBso {
178            envelope: OutgoingEnvelope {
179                id: "1234".into(),
180                ..Default::default()
181            },
182            payload: EncryptedPayload {
183                iv: "aaaaa".into(),
184                hmac: "bbbbb".into(),
185                ciphertext: "ccccc".into(),
186            },
187        };
188        let actual = serde_json::to_string(&record).unwrap();
189        assert_eq!(actual, goal);
190
191        let val_str_payload: serde_json::Value = serde_json::from_str(goal).unwrap();
192        assert_eq!(
193            val_str_payload["payload"].as_str().unwrap().len(),
194            record.payload.serialized_len()
195        )
196    }
197}