bitwarden_crypto/enc_string/
asymmetric.rs1use 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, KeySlotIds, KeyStoreContext, PrivateKey, PublicKey, RawPrivateKey,
11 RawPublicKey, SymmetricCryptoKey,
12 error::{CryptoError, EncStringParseError, Result},
13 rsa::encrypt_rsa2048_oaep_sha1,
14};
15#[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 #[allow(missing_docs)]
59 #[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)]
60 #[allow(unused, non_camel_case_types)]
61 pub enum UnsignedSharedKey {
62 Rsa2048_OaepSha256_B64 { data: Vec<u8> },
64 Rsa2048_OaepSha1_B64 { data: Vec<u8> },
66 #[deprecated]
68 Rsa2048_OaepSha256_HmacSha256_B64 { data: Vec<u8>, mac: Vec<u8> },
69 #[deprecated]
71 Rsa2048_OaepSha1_HmacSha256_B64 { data: Vec<u8>, mac: Vec<u8> },
72 }
73}
74
75impl 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
83impl 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
146bitwarden_ffi::impl_wire_string!(UnsignedSharedKey);
147
148#[cfg(feature = "wasm")]
149impl TryFrom<wasm_bindgen::JsValue> for UnsignedSharedKey {
150 type Error = CryptoError;
151
152 fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
153 let string = value
154 .as_string()
155 .ok_or(EncStringParseError::NoType)
156 .map_err(CryptoError::from)?;
157 Self::from_str(&string)
158 }
159}
160
161impl<'de> Deserialize<'de> for UnsignedSharedKey {
162 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
163 where
164 D: serde::Deserializer<'de>,
165 {
166 deserializer.deserialize_str(FromStrVisitor::new())
167 }
168}
169
170impl serde::Serialize for UnsignedSharedKey {
171 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
172 where
173 S: serde::Serializer,
174 {
175 serializer.serialize_str(&self.to_string())
176 }
177}
178
179impl UnsignedSharedKey {
180 #[deprecated(note = "Use encapsulate() instead")]
184 pub fn encapsulate_key_unsigned(
185 encapsulated_key: &SymmetricCryptoKey,
186 encapsulation_key: &PublicKey,
187 ) -> Result<UnsignedSharedKey> {
188 match encapsulation_key.inner() {
189 RawPublicKey::RsaOaepSha1(rsa_public_key) => {
190 Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 {
191 data: encrypt_rsa2048_oaep_sha1(
192 rsa_public_key,
193 encapsulated_key.to_encoded().as_ref(),
194 )?,
195 })
196 }
197 }
198 }
199
200 pub fn encapsulate<Ids: KeySlotIds>(
204 key_to_encapsulate: Ids::Symmetric,
205 encapsulation_key: &PublicKey,
206 ctx: &KeyStoreContext<Ids>,
207 ) -> Result<UnsignedSharedKey> {
208 #[expect(deprecated)]
210 let encapsulated_key = ctx.dangerous_get_symmetric_key(key_to_encapsulate)?;
211 #[expect(deprecated)]
213 Self::encapsulate_key_unsigned(encapsulated_key, encapsulation_key)
214 }
215
216 const fn enc_type(&self) -> u8 {
218 match self {
219 UnsignedSharedKey::Rsa2048_OaepSha256_B64 { .. } => 3,
220 UnsignedSharedKey::Rsa2048_OaepSha1_B64 { .. } => 4,
221 #[allow(deprecated)]
222 UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { .. } => 5,
223 #[allow(deprecated)]
224 UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { .. } => 6,
225 }
226 }
227}
228
229impl UnsignedSharedKey {
230 pub fn decapsulate<Ids: KeySlotIds>(
233 &self,
234 decapsulation_key: Ids::Private,
235 ctx: &mut KeyStoreContext<Ids>,
236 ) -> Result<Ids::Symmetric> {
237 #[expect(deprecated)]
239 let private_key = ctx.dangerous_get_private_key(decapsulation_key)?;
240 #[expect(deprecated)]
241 let key = Self::decapsulate_key_unsigned(self, private_key)
242 .map_err(|_| CryptoError::KeyDecrypt)?;
243 Ok(ctx.add_local_symmetric_key(key))
244 }
245
246 #[deprecated(note = "Use decapsulate() instead")]
250 pub fn decapsulate_key_unsigned(
251 &self,
252 decapsulation_key: &PrivateKey,
253 ) -> Result<SymmetricCryptoKey> {
254 match decapsulation_key.inner() {
255 RawPrivateKey::RsaOaepSha1(rsa_private_key) => {
256 use UnsignedSharedKey::*;
257 let key_data = match self {
258 Rsa2048_OaepSha256_B64 { data } => {
259 rsa_private_key.decrypt(Oaep::<sha2::Sha256>::new(), data)
260 }
261 Rsa2048_OaepSha1_B64 { data } => {
262 rsa_private_key.decrypt(Oaep::<sha1::Sha1>::new(), data)
263 }
264 #[allow(deprecated)]
265 Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => {
266 rsa_private_key.decrypt(Oaep::<sha2::Sha256>::new(), data)
267 }
268 #[allow(deprecated)]
269 Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => {
270 rsa_private_key.decrypt(Oaep::<sha1::Sha1>::new(), data)
271 }
272 }
273 .map_err(|_| CryptoError::KeyDecrypt)?;
274 SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_data))
275 }
276 }
277 }
278}
279
280impl schemars::JsonSchema for UnsignedSharedKey {
284 fn schema_name() -> Cow<'static, str> {
285 "UnsignedSharedKey".into()
286 }
287
288 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
289 generator.subschema_for::<String>()
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use schemars::schema_for;
296
297 use super::UnsignedSharedKey;
298 use crate::{PrivateKey, SymmetricCryptoKey};
299
300 const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
301MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS
3028HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2
303e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9
3044LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa
305F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A
306QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16
307l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq
30892qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP
309dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a
310PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX
311UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR
312buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2
313hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc
314vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA
315hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs
316tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY
317UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs
318zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX
319E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP
320jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez
321MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9
322JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg
323AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp
324Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3
325WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz
326XKZBokBGnjFnTnKcs7nv/O8=
327-----END PRIVATE KEY-----";
328
329 #[test]
330 fn test_enc_string_rsa2048_oaep_sha256_b64() {
331 let key_pair = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
332 let enc_str: &str = "3.SUx5gWrgmAKs/S1BoQrqOmx2Hl5fPVBVHokW17Flvm4TpBnJJRkfoitp7Jc4dfazPYjWGlckJz6X+qe+/AWilS1mxtzS0PmDy7tS5xP0GRlB39dstCd5jDw1wPmTbXiLcQ5VTvzpRAfRMEYVveTsEvVTByvEYAGSn4TnCsUDykyhRbD0YcJ4r1KHLs1b3BCBy2M1Gl5nmwckH08CAXaf8VfuBFStAGRKueovqp4euneQla+4G4fXdVvb8qKPnu0iVuALIE6nUNmeOiA3xN3d+akMxbbGxrQ1Ca4TYWjHVdj9C6abngQHkjKNYQwGUXrYo160hP4LIHn/huK6bZe5dQ==";
333 let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
334
335 let test_key = SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
336 assert_eq!(enc_string.enc_type(), 3);
337
338 #[expect(deprecated)]
339 let res = enc_string.decapsulate_key_unsigned(&key_pair).unwrap();
340 assert_eq!(res, test_key);
341 }
342
343 #[test]
344 fn test_enc_string_rsa2048_oaep_sha1_b64() {
345 let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
346 let enc_str: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
347 let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
348
349 let test_key = SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
350 assert_eq!(enc_string.enc_type(), 4);
351
352 #[expect(deprecated)]
353 let res = enc_string.decapsulate_key_unsigned(&private_key).unwrap();
354 assert_eq!(res, test_key);
355 }
356
357 #[test]
358 fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() {
359 let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
360 let enc_str: &str = "6.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==|AA==";
361 let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
362
363 let test_key: SymmetricCryptoKey =
364 SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
365 assert_eq!(enc_string.enc_type(), 6);
366
367 #[expect(deprecated)]
368 let res = enc_string.decapsulate_key_unsigned(&private_key).unwrap();
369 assert_eq!(res.to_base64(), test_key.to_base64());
370 }
371
372 #[test]
373 fn test_enc_string_serialization() {
374 #[derive(serde::Serialize, serde::Deserialize)]
375 struct Test {
376 key: UnsignedSharedKey,
377 }
378
379 let cipher = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ=";
380 let serialized = format!("{{\"key\":\"{cipher}\"}}");
381
382 let t = serde_json::from_str::<Test>(&serialized).unwrap();
383 assert_eq!(t.key.enc_type(), 6);
384 assert_eq!(t.key.to_string(), cipher);
385 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
386 }
387
388 #[test]
389 fn test_from_str_invalid() {
390 let enc_str = "7.ABC";
391 let enc_string: Result<UnsignedSharedKey, _> = enc_str.parse();
392
393 let err = enc_string.unwrap_err();
394 assert_eq!(
395 err.to_string(),
396 "EncString error, Invalid asymmetric type, got type 7 with 1 parts"
397 );
398 }
399
400 #[test]
401 fn test_debug_format() {
402 let enc_str: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
403 let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
404
405 let debug_string = format!("{enc_string:?}");
406 assert_eq!(debug_string, "UnsignedSharedKey");
407 }
408
409 #[test]
410 fn test_json_schema() {
411 let schema = schema_for!(UnsignedSharedKey);
412
413 assert_eq!(
414 serde_json::to_string(&schema).unwrap(),
415 r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"UnsignedSharedKey","type":"string"}"#
416 );
417 }
418}