bitwarden_crypto/enc_string/
asymmetric.rs

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