bitwarden_crypto/enc_string/
asymmetric.rs

1use std::{fmt::Display, str::FromStr};
2
3use base64::{engine::general_purpose::STANDARD, Engine};
4pub use internal::UnsignedSharedKey;
5use rsa::Oaep;
6use serde::Deserialize;
7
8use super::{from_b64_vec, split_enc_string};
9use crate::{
10    error::{CryptoError, EncStringParseError, Result},
11    rsa::encrypt_rsa2048_oaep_sha1,
12    AsymmetricCryptoKey, AsymmetricEncryptable, SymmetricCryptoKey,
13};
14// This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop
15// macro expansion
16#[allow(deprecated)]
17mod internal {
18    #[cfg(feature = "wasm")]
19    #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
20    const TS_CUSTOM_TYPES: &'static str = r#"
21    export type UnsignedSharedKey = string;
22    "#;
23
24    /// # Encrypted string primitive
25    ///
26    /// [UnsignedSharedKey] is a Bitwarden specific primitive that represents an
27    /// asymmetrically encrypted symmetric key. Since the symmetric key is directly encrypted
28    /// with the public key, without any further signature, the receiver cannot guarantee the
29    /// senders identity.
30    ///
31    /// [UnsignedSharedKey] type allows for different encryption algorithms
32    /// to be used which is represented by the different variants of the enum.
33    ///
34    /// ## Note
35    ///
36    /// For backwards compatibility we will rarely if ever be able to remove support for decrypting
37    /// old variants, but we should be opinionated in which variants are used for encrypting.
38    ///
39    /// ## Variants
40    /// - [Rsa2048_OaepSha256_B64](UnsignedSharedKey::Rsa2048_OaepSha256_B64)
41    /// - [Rsa2048_OaepSha1_B64](UnsignedSharedKey::Rsa2048_OaepSha1_B64)
42    ///
43    /// ## Serialization
44    ///
45    /// [UnsignedSharedKey] implements [std::fmt::Display] and [std::str::FromStr] to allow
46    /// for easy serialization and uses a custom scheme to represent the different variants.
47    ///
48    /// The scheme is one of the following schemes:
49    /// - `[type].[data]`
50    ///
51    /// Where:
52    /// - `[type]`: is a digit number representing the variant.
53    /// - `[data]`: is the encrypted data.
54    #[derive(Clone, zeroize::ZeroizeOnDrop)]
55    #[allow(unused, non_camel_case_types)]
56    pub enum UnsignedSharedKey {
57        /// 3
58        Rsa2048_OaepSha256_B64 { data: Vec<u8> },
59        /// 4
60        Rsa2048_OaepSha1_B64 { data: Vec<u8> },
61        /// 5
62        #[deprecated]
63        Rsa2048_OaepSha256_HmacSha256_B64 { data: Vec<u8>, mac: Vec<u8> },
64        /// 6
65        #[deprecated]
66        Rsa2048_OaepSha1_HmacSha256_B64 { data: Vec<u8>, mac: Vec<u8> },
67    }
68}
69
70/// To avoid printing sensitive information, [UnsignedSharedKey] debug prints to
71/// `UnsignedSharedKey`.
72impl std::fmt::Debug for UnsignedSharedKey {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.debug_struct("UnsignedSharedKey").finish()
75    }
76}
77
78/// Deserializes an [UnsignedSharedKey] from a string.
79impl FromStr for UnsignedSharedKey {
80    type Err = CryptoError;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        let (enc_type, parts) = split_enc_string(s);
84        match (enc_type, parts.len()) {
85            ("3", 1) => {
86                let data = from_b64_vec(parts[0])?;
87                Ok(UnsignedSharedKey::Rsa2048_OaepSha256_B64 { data })
88            }
89            ("4", 1) => {
90                let data = from_b64_vec(parts[0])?;
91                Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data })
92            }
93            #[allow(deprecated)]
94            ("5", 2) => {
95                let data = from_b64_vec(parts[0])?;
96                let mac: Vec<u8> = from_b64_vec(parts[1])?;
97                Ok(UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac })
98            }
99            #[allow(deprecated)]
100            ("6", 2) => {
101                let data = from_b64_vec(parts[0])?;
102                let mac: Vec<u8> = from_b64_vec(parts[1])?;
103                Ok(UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac })
104            }
105
106            (enc_type, parts) => Err(EncStringParseError::InvalidTypeAsymm {
107                enc_type: enc_type.to_string(),
108                parts,
109            }
110            .into()),
111        }
112    }
113}
114
115impl Display for UnsignedSharedKey {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        let parts: Vec<&[u8]> = match self {
118            UnsignedSharedKey::Rsa2048_OaepSha256_B64 { data } => vec![data],
119            UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data } => vec![data],
120            #[allow(deprecated)]
121            UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac } => {
122                vec![data, mac]
123            }
124            #[allow(deprecated)]
125            UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac } => {
126                vec![data, mac]
127            }
128        };
129
130        let encoded_parts: Vec<String> = parts.iter().map(|part| STANDARD.encode(part)).collect();
131
132        write!(f, "{}.{}", self.enc_type(), encoded_parts.join("|"))?;
133
134        Ok(())
135    }
136}
137
138impl<'de> Deserialize<'de> for UnsignedSharedKey {
139    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140    where
141        D: serde::Deserializer<'de>,
142    {
143        deserializer.deserialize_str(super::FromStrVisitor::new())
144    }
145}
146
147impl serde::Serialize for UnsignedSharedKey {
148    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149    where
150        S: serde::Serializer,
151    {
152        serializer.serialize_str(&self.to_string())
153    }
154}
155
156impl UnsignedSharedKey {
157    /// Encapsulate a symmetric key, to be shared asymmetrically. Produces a
158    /// [UnsignedSharedKey::Rsa2048_OaepSha1_B64] variant. Note, this does not sign the data
159    /// and thus does not guarantee sender authenticity.
160    pub fn encapsulate_key_unsigned(
161        encapsulated_key: &SymmetricCryptoKey,
162        encapsulation_key: &dyn AsymmetricEncryptable,
163    ) -> Result<UnsignedSharedKey> {
164        let enc = encrypt_rsa2048_oaep_sha1(
165            encapsulation_key.to_public_key(),
166            &encapsulated_key.to_encoded(),
167        )?;
168        Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data: enc })
169    }
170
171    /// The numerical representation of the encryption type of the [UnsignedSharedKey].
172    const fn enc_type(&self) -> u8 {
173        match self {
174            UnsignedSharedKey::Rsa2048_OaepSha256_B64 { .. } => 3,
175            UnsignedSharedKey::Rsa2048_OaepSha1_B64 { .. } => 4,
176            #[allow(deprecated)]
177            UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { .. } => 5,
178            #[allow(deprecated)]
179            UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { .. } => 6,
180        }
181    }
182}
183
184impl UnsignedSharedKey {
185    /// Decapsulate a symmetric key, shared asymmetrically.
186    /// Note: The shared key does not have a sender signature and sender authenticity is not
187    /// guaranteed.
188    pub fn decapsulate_key_unsigned(
189        &self,
190        decapsulation_key: &AsymmetricCryptoKey,
191    ) -> Result<SymmetricCryptoKey> {
192        use UnsignedSharedKey::*;
193        let mut key_data = match self {
194            Rsa2048_OaepSha256_B64 { data } => decapsulation_key
195                .key
196                .decrypt(Oaep::new::<sha2::Sha256>(), data),
197            Rsa2048_OaepSha1_B64 { data } => decapsulation_key
198                .key
199                .decrypt(Oaep::new::<sha1::Sha1>(), data),
200            #[allow(deprecated)]
201            Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => decapsulation_key
202                .key
203                .decrypt(Oaep::new::<sha2::Sha256>(), data),
204            #[allow(deprecated)]
205            Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => decapsulation_key
206                .key
207                .decrypt(Oaep::new::<sha1::Sha1>(), data),
208        }
209        .map_err(|_| CryptoError::KeyDecrypt)?;
210        SymmetricCryptoKey::try_from(key_data.as_mut_slice())
211    }
212}
213
214/// Usually we wouldn't want to expose UnsignedSharedKeys in the API or the schemas.
215/// But during the transition phase we will expose endpoints using the UnsignedSharedKey
216/// type.
217impl schemars::JsonSchema for UnsignedSharedKey {
218    fn schema_name() -> String {
219        "UnsignedSharedKey".to_string()
220    }
221
222    fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
223        generator.subschema_for::<String>()
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use schemars::schema_for;
230
231    use super::{AsymmetricCryptoKey, UnsignedSharedKey};
232    use crate::SymmetricCryptoKey;
233
234    const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
235MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS
2368HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2
237e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9
2384LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa
239F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A
240QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16
241l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq
24292qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP
243dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a
244PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX
245UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR
246buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2
247hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc
248vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA
249hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs
250tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY
251UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs
252zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX
253E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP
254jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez
255MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9
256JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg
257AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp
258Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3
259WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz
260XKZBokBGnjFnTnKcs7nv/O8=
261-----END PRIVATE KEY-----";
262
263    #[test]
264    fn test_enc_string_rsa2048_oaep_sha256_b64() {
265        let key_pair = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap();
266        let enc_str: &str = "3.SUx5gWrgmAKs/S1BoQrqOmx2Hl5fPVBVHokW17Flvm4TpBnJJRkfoitp7Jc4dfazPYjWGlckJz6X+qe+/AWilS1mxtzS0PmDy7tS5xP0GRlB39dstCd5jDw1wPmTbXiLcQ5VTvzpRAfRMEYVveTsEvVTByvEYAGSn4TnCsUDykyhRbD0YcJ4r1KHLs1b3BCBy2M1Gl5nmwckH08CAXaf8VfuBFStAGRKueovqp4euneQla+4G4fXdVvb8qKPnu0iVuALIE6nUNmeOiA3xN3d+akMxbbGxrQ1Ca4TYWjHVdj9C6abngQHkjKNYQwGUXrYo160hP4LIHn/huK6bZe5dQ==";
267        let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
268
269        let test_key = SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
270        assert_eq!(enc_string.enc_type(), 3);
271
272        let res = enc_string.decapsulate_key_unsigned(&key_pair).unwrap();
273        assert_eq!(res, test_key);
274    }
275
276    #[test]
277    fn test_enc_string_rsa2048_oaep_sha1_b64() {
278        let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap();
279        let enc_str: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
280        let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
281
282        let test_key = SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
283        assert_eq!(enc_string.enc_type(), 4);
284
285        let res = enc_string.decapsulate_key_unsigned(&private_key).unwrap();
286        assert_eq!(res, test_key);
287    }
288
289    #[test]
290    fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() {
291        let private_key = AsymmetricCryptoKey::from_pem(RSA_PRIVATE_KEY).unwrap();
292        let enc_str: &str = "6.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==|AA==";
293        let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
294
295        let test_key: SymmetricCryptoKey =
296            SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
297        assert_eq!(enc_string.enc_type(), 6);
298
299        let res = enc_string.decapsulate_key_unsigned(&private_key).unwrap();
300        assert_eq!(res.to_base64(), test_key.to_base64());
301    }
302
303    #[test]
304    fn test_enc_string_serialization() {
305        #[derive(serde::Serialize, serde::Deserialize)]
306        struct Test {
307            key: UnsignedSharedKey,
308        }
309
310        let cipher = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ=";
311        let serialized = format!("{{\"key\":\"{cipher}\"}}");
312
313        let t = serde_json::from_str::<Test>(&serialized).unwrap();
314        assert_eq!(t.key.enc_type(), 6);
315        assert_eq!(t.key.to_string(), cipher);
316        assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
317    }
318
319    #[test]
320    fn test_from_str_invalid() {
321        let enc_str = "7.ABC";
322        let enc_string: Result<UnsignedSharedKey, _> = enc_str.parse();
323
324        let err = enc_string.unwrap_err();
325        assert_eq!(
326            err.to_string(),
327            "EncString error, Invalid asymmetric type, got type 7 with 1 parts"
328        );
329    }
330
331    #[test]
332    fn test_debug_format() {
333        let enc_str: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
334        let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
335
336        let debug_string = format!("{:?}", enc_string);
337        assert_eq!(debug_string, "UnsignedSharedKey");
338    }
339
340    #[test]
341    fn test_json_schema() {
342        let schema = schema_for!(UnsignedSharedKey);
343
344        assert_eq!(
345            serde_json::to_string(&schema).unwrap(),
346            r#"{"$schema":"http://json-schema.org/draft-07/schema#","title":"UnsignedSharedKey","type":"string"}"#
347        );
348    }
349}