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#[cfg(feature = "wasm")]
8use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
9
10use super::{from_b64_vec, split_enc_string};
11use crate::{
12 BitwardenLegacyKeyBytes, KeySlotIds, KeyStoreContext, PrivateKey, PublicKey, RawPrivateKey,
13 RawPublicKey, SymmetricCryptoKey,
14 error::{CryptoError, EncStringParseError, Result},
15 rsa::encrypt_rsa2048_oaep_sha1,
16};
17#[allow(deprecated)]
20mod internal {
21 #[cfg(feature = "wasm")]
22 #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
23 const TS_CUSTOM_TYPES: &'static str = r#"
24 export type UnsignedSharedKey = Tagged<string, "UnsignedSharedKey">;
25 "#;
26
27 #[allow(missing_docs)]
61 #[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)]
62 #[allow(unused, non_camel_case_types)]
63 pub enum UnsignedSharedKey {
64 Rsa2048_OaepSha256_B64 { data: Vec<u8> },
66 Rsa2048_OaepSha1_B64 { data: Vec<u8> },
68 #[deprecated]
70 Rsa2048_OaepSha256_HmacSha256_B64 { data: Vec<u8>, mac: Vec<u8> },
71 #[deprecated]
73 Rsa2048_OaepSha1_HmacSha256_B64 { data: Vec<u8>, mac: Vec<u8> },
74 }
75}
76
77impl std::fmt::Debug for UnsignedSharedKey {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("UnsignedSharedKey").finish()
82 }
83}
84
85impl FromStr for UnsignedSharedKey {
87 type Err = CryptoError;
88
89 fn from_str(s: &str) -> Result<Self, Self::Err> {
90 let (enc_type, parts) = split_enc_string(s);
91 match (enc_type, parts.len()) {
92 ("3", 1) => {
93 let data = from_b64_vec(parts[0])?;
94 Ok(UnsignedSharedKey::Rsa2048_OaepSha256_B64 { data })
95 }
96 ("4", 1) => {
97 let data = from_b64_vec(parts[0])?;
98 Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data })
99 }
100 #[allow(deprecated)]
101 ("5", 2) => {
102 let data = from_b64_vec(parts[0])?;
103 let mac: Vec<u8> = from_b64_vec(parts[1])?;
104 Ok(UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac })
105 }
106 #[allow(deprecated)]
107 ("6", 2) => {
108 let data = from_b64_vec(parts[0])?;
109 let mac: Vec<u8> = from_b64_vec(parts[1])?;
110 Ok(UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac })
111 }
112
113 (enc_type, parts) => Err(EncStringParseError::InvalidTypeAsymm {
114 enc_type: enc_type.to_string(),
115 parts,
116 }
117 .into()),
118 }
119 }
120}
121
122impl Display for UnsignedSharedKey {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 let parts: Vec<&[u8]> = match self {
125 UnsignedSharedKey::Rsa2048_OaepSha256_B64 { data } => vec![data],
126 UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data } => vec![data],
127 #[allow(deprecated)]
128 UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac } => {
129 vec![data, mac]
130 }
131 #[allow(deprecated)]
132 UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac } => {
133 vec![data, mac]
134 }
135 };
136
137 let encoded_parts: Vec<String> = parts
138 .iter()
139 .map(|part| B64::from(*part).to_string())
140 .collect();
141
142 write!(f, "{}.{}", self.enc_type(), encoded_parts.join("|"))?;
143
144 Ok(())
145 }
146}
147
148#[cfg(feature = "wasm")]
149impl wasm_bindgen::describe::WasmDescribe for UnsignedSharedKey {
150 fn describe() {
151 <String as wasm_bindgen::describe::WasmDescribe>::describe();
152 }
153}
154
155#[cfg(feature = "wasm")]
156impl FromWasmAbi for UnsignedSharedKey {
157 type Abi = <String as FromWasmAbi>::Abi;
158
159 unsafe fn from_abi(abi: Self::Abi) -> Self {
160 use wasm_bindgen::UnwrapThrowExt;
161
162 let s = unsafe { String::from_abi(abi) };
163 Self::from_str(&s).unwrap_throw()
164 }
165}
166
167#[cfg(feature = "wasm")]
168impl OptionFromWasmAbi for UnsignedSharedKey {
169 fn is_none(abi: &Self::Abi) -> bool {
170 <String as OptionFromWasmAbi>::is_none(abi)
171 }
172}
173
174#[cfg(feature = "wasm")]
175impl IntoWasmAbi for UnsignedSharedKey {
176 type Abi = <String as IntoWasmAbi>::Abi;
177
178 fn into_abi(self) -> Self::Abi {
179 self.to_string().into_abi()
180 }
181}
182
183#[cfg(feature = "wasm")]
184impl TryFrom<wasm_bindgen::JsValue> for UnsignedSharedKey {
185 type Error = CryptoError;
186
187 fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
188 let string = value
189 .as_string()
190 .ok_or(EncStringParseError::NoType)
191 .map_err(CryptoError::from)?;
192 Self::from_str(&string)
193 }
194}
195
196impl<'de> Deserialize<'de> for UnsignedSharedKey {
197 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198 where
199 D: serde::Deserializer<'de>,
200 {
201 deserializer.deserialize_str(FromStrVisitor::new())
202 }
203}
204
205impl serde::Serialize for UnsignedSharedKey {
206 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
207 where
208 S: serde::Serializer,
209 {
210 serializer.serialize_str(&self.to_string())
211 }
212}
213
214impl UnsignedSharedKey {
215 #[deprecated(note = "Use encapsulate() instead")]
219 pub fn encapsulate_key_unsigned(
220 encapsulated_key: &SymmetricCryptoKey,
221 encapsulation_key: &PublicKey,
222 ) -> Result<UnsignedSharedKey> {
223 match encapsulation_key.inner() {
224 RawPublicKey::RsaOaepSha1(rsa_public_key) => {
225 Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 {
226 data: encrypt_rsa2048_oaep_sha1(
227 rsa_public_key,
228 encapsulated_key.to_encoded().as_ref(),
229 )?,
230 })
231 }
232 }
233 }
234
235 pub fn encapsulate<Ids: KeySlotIds>(
239 key_to_encapsulate: Ids::Symmetric,
240 encapsulation_key: &PublicKey,
241 ctx: &KeyStoreContext<Ids>,
242 ) -> Result<UnsignedSharedKey> {
243 #[expect(deprecated)]
245 let encapsulated_key = ctx.dangerous_get_symmetric_key(key_to_encapsulate)?;
246 #[expect(deprecated)]
248 Self::encapsulate_key_unsigned(encapsulated_key, encapsulation_key)
249 }
250
251 const fn enc_type(&self) -> u8 {
253 match self {
254 UnsignedSharedKey::Rsa2048_OaepSha256_B64 { .. } => 3,
255 UnsignedSharedKey::Rsa2048_OaepSha1_B64 { .. } => 4,
256 #[allow(deprecated)]
257 UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { .. } => 5,
258 #[allow(deprecated)]
259 UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { .. } => 6,
260 }
261 }
262}
263
264impl UnsignedSharedKey {
265 pub fn decapsulate<Ids: KeySlotIds>(
268 &self,
269 decapsulation_key: Ids::Private,
270 ctx: &mut KeyStoreContext<Ids>,
271 ) -> Result<Ids::Symmetric> {
272 #[expect(deprecated)]
274 let private_key = ctx.dangerous_get_private_key(decapsulation_key)?;
275 #[expect(deprecated)]
276 let key = Self::decapsulate_key_unsigned(self, private_key)
277 .map_err(|_| CryptoError::KeyDecrypt)?;
278 Ok(ctx.add_local_symmetric_key(key))
279 }
280
281 #[deprecated(note = "Use decapsulate() instead")]
285 pub fn decapsulate_key_unsigned(
286 &self,
287 decapsulation_key: &PrivateKey,
288 ) -> Result<SymmetricCryptoKey> {
289 match decapsulation_key.inner() {
290 RawPrivateKey::RsaOaepSha1(rsa_private_key) => {
291 use UnsignedSharedKey::*;
292 let key_data = match self {
293 Rsa2048_OaepSha256_B64 { data } => {
294 rsa_private_key.decrypt(Oaep::<sha2::Sha256>::new(), data)
295 }
296 Rsa2048_OaepSha1_B64 { data } => {
297 rsa_private_key.decrypt(Oaep::<sha1::Sha1>::new(), data)
298 }
299 #[allow(deprecated)]
300 Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => {
301 rsa_private_key.decrypt(Oaep::<sha2::Sha256>::new(), data)
302 }
303 #[allow(deprecated)]
304 Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => {
305 rsa_private_key.decrypt(Oaep::<sha1::Sha1>::new(), data)
306 }
307 }
308 .map_err(|_| CryptoError::KeyDecrypt)?;
309 SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_data))
310 }
311 }
312 }
313}
314
315impl schemars::JsonSchema for UnsignedSharedKey {
319 fn schema_name() -> Cow<'static, str> {
320 "UnsignedSharedKey".into()
321 }
322
323 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
324 generator.subschema_for::<String>()
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use schemars::schema_for;
331
332 use super::UnsignedSharedKey;
333 use crate::{PrivateKey, SymmetricCryptoKey};
334
335 const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
336MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS
3378HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2
338e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9
3394LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa
340F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A
341QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16
342l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq
34392qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP
344dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a
345PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX
346UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR
347buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2
348hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc
349vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA
350hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs
351tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY
352UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs
353zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX
354E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP
355jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez
356MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9
357JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg
358AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp
359Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3
360WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz
361XKZBokBGnjFnTnKcs7nv/O8=
362-----END PRIVATE KEY-----";
363
364 #[test]
365 fn test_enc_string_rsa2048_oaep_sha256_b64() {
366 let key_pair = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
367 let enc_str: &str = "3.SUx5gWrgmAKs/S1BoQrqOmx2Hl5fPVBVHokW17Flvm4TpBnJJRkfoitp7Jc4dfazPYjWGlckJz6X+qe+/AWilS1mxtzS0PmDy7tS5xP0GRlB39dstCd5jDw1wPmTbXiLcQ5VTvzpRAfRMEYVveTsEvVTByvEYAGSn4TnCsUDykyhRbD0YcJ4r1KHLs1b3BCBy2M1Gl5nmwckH08CAXaf8VfuBFStAGRKueovqp4euneQla+4G4fXdVvb8qKPnu0iVuALIE6nUNmeOiA3xN3d+akMxbbGxrQ1Ca4TYWjHVdj9C6abngQHkjKNYQwGUXrYo160hP4LIHn/huK6bZe5dQ==";
368 let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
369
370 let test_key = SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
371 assert_eq!(enc_string.enc_type(), 3);
372
373 #[expect(deprecated)]
374 let res = enc_string.decapsulate_key_unsigned(&key_pair).unwrap();
375 assert_eq!(res, test_key);
376 }
377
378 #[test]
379 fn test_enc_string_rsa2048_oaep_sha1_b64() {
380 let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
381 let enc_str: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
382 let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
383
384 let test_key = SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
385 assert_eq!(enc_string.enc_type(), 4);
386
387 #[expect(deprecated)]
388 let res = enc_string.decapsulate_key_unsigned(&private_key).unwrap();
389 assert_eq!(res, test_key);
390 }
391
392 #[test]
393 fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() {
394 let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
395 let enc_str: &str = "6.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==|AA==";
396 let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
397
398 let test_key: SymmetricCryptoKey =
399 SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
400 assert_eq!(enc_string.enc_type(), 6);
401
402 #[expect(deprecated)]
403 let res = enc_string.decapsulate_key_unsigned(&private_key).unwrap();
404 assert_eq!(res.to_base64(), test_key.to_base64());
405 }
406
407 #[test]
408 fn test_enc_string_serialization() {
409 #[derive(serde::Serialize, serde::Deserialize)]
410 struct Test {
411 key: UnsignedSharedKey,
412 }
413
414 let cipher = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ=";
415 let serialized = format!("{{\"key\":\"{cipher}\"}}");
416
417 let t = serde_json::from_str::<Test>(&serialized).unwrap();
418 assert_eq!(t.key.enc_type(), 6);
419 assert_eq!(t.key.to_string(), cipher);
420 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
421 }
422
423 #[test]
424 fn test_from_str_invalid() {
425 let enc_str = "7.ABC";
426 let enc_string: Result<UnsignedSharedKey, _> = enc_str.parse();
427
428 let err = enc_string.unwrap_err();
429 assert_eq!(
430 err.to_string(),
431 "EncString error, Invalid asymmetric type, got type 7 with 1 parts"
432 );
433 }
434
435 #[test]
436 fn test_debug_format() {
437 let enc_str: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
438 let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
439
440 let debug_string = format!("{enc_string:?}");
441 assert_eq!(debug_string, "UnsignedSharedKey");
442 }
443
444 #[test]
445 fn test_json_schema() {
446 let schema = schema_for!(UnsignedSharedKey);
447
448 assert_eq!(
449 serde_json::to_string(&schema).unwrap(),
450 r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"UnsignedSharedKey","type":"string"}"#
451 );
452 }
453}