1use std::{borrow::Cow, str::FromStr};
2
3use bitwarden_encoding::{B64, FromStrVisitor};
4use coset::{CborSerializable, iana::KeyOperation};
5use serde::Deserialize;
6use tracing::instrument;
7#[cfg(feature = "wasm")]
8use wasm_bindgen::convert::FromWasmAbi;
9
10use super::{check_length, from_b64, from_b64_vec, split_enc_string};
11use crate::{
12 Aes256CbcHmacKey, ContentFormat, CoseEncrypt0Bytes, KeyDecryptable, KeyEncryptable,
13 KeyEncryptableWithContentType, SymmetricCryptoKey, Utf8Bytes, XChaCha20Poly1305Key,
14 error::{CryptoError, EncStringParseError, Result, UnsupportedOperationError},
15};
16
17#[cfg(feature = "wasm")]
18#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
19const TS_CUSTOM_TYPES: &'static str = r#"
20export type EncString = Tagged<string, "EncString">;
21"#;
22
23#[allow(missing_docs)]
60#[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)]
61#[allow(unused, non_camel_case_types)]
62pub enum EncString {
63 Aes256Cbc_B64 {
65 iv: [u8; 16],
66 data: Vec<u8>,
67 },
68 Aes256Cbc_HmacSha256_B64 {
71 iv: [u8; 16],
72 mac: [u8; 32],
73 data: Vec<u8>,
74 },
75 Cose_Encrypt0_B64 {
77 data: Vec<u8>,
78 },
79}
80
81#[cfg(feature = "wasm")]
82impl wasm_bindgen::describe::WasmDescribe for EncString {
83 fn describe() {
84 <String as wasm_bindgen::describe::WasmDescribe>::describe();
85 }
86}
87
88#[cfg(feature = "wasm")]
89impl FromWasmAbi for EncString {
90 type Abi = <String as FromWasmAbi>::Abi;
91
92 unsafe fn from_abi(abi: Self::Abi) -> Self {
93 use wasm_bindgen::UnwrapThrowExt;
94
95 let s = unsafe { String::from_abi(abi) };
96 Self::from_str(&s).unwrap_throw()
97 }
98}
99
100impl FromStr for EncString {
102 type Err = CryptoError;
103
104 fn from_str(s: &str) -> Result<Self, Self::Err> {
105 let (enc_type, parts) = split_enc_string(s);
106 match (enc_type, parts.len()) {
107 ("0", 2) => {
108 let iv = from_b64(parts[0])?;
109 let data = from_b64_vec(parts[1])?;
110
111 Ok(EncString::Aes256Cbc_B64 { iv, data })
112 }
113 ("2", 3) => {
114 let iv = from_b64(parts[0])?;
115 let data = from_b64_vec(parts[1])?;
116 let mac = from_b64(parts[2])?;
117
118 Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
119 }
120 ("7", 1) => {
121 let buffer = from_b64_vec(parts[0])?;
122
123 Ok(EncString::Cose_Encrypt0_B64 { data: buffer })
124 }
125 (enc_type, parts) => Err(EncStringParseError::InvalidTypeSymm {
126 enc_type: enc_type.to_string(),
127 parts,
128 }
129 .into()),
130 }
131 }
132}
133
134impl EncString {
135 pub fn try_from_optional(s: Option<String>) -> Result<Option<EncString>, CryptoError> {
137 s.map(|s| s.parse()).transpose()
138 }
139
140 #[allow(missing_docs)]
141 pub fn from_buffer(buf: &[u8]) -> Result<Self> {
142 if buf.is_empty() {
143 return Err(EncStringParseError::NoType.into());
144 }
145 let enc_type = buf[0];
146
147 match enc_type {
148 0 => {
149 check_length(buf, 18)?;
150 let iv = buf[1..17].try_into().expect("Valid length");
151 let data = buf[17..].to_vec();
152
153 Ok(EncString::Aes256Cbc_B64 { iv, data })
154 }
155 2 => {
156 check_length(buf, 50)?;
157 let iv = buf[1..17].try_into().expect("Valid length");
158 let mac = buf[17..49].try_into().expect("Valid length");
159 let data = buf[49..].to_vec();
160
161 Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
162 }
163 7 => Ok(EncString::Cose_Encrypt0_B64 {
164 data: buf[1..].to_vec(),
165 }),
166 _ => Err(EncStringParseError::InvalidTypeSymm {
167 enc_type: enc_type.to_string(),
168 parts: 1,
169 }
170 .into()),
171 }
172 }
173
174 #[allow(missing_docs)]
175 pub fn to_buffer(&self) -> Result<Vec<u8>> {
176 let mut buf;
177
178 match self {
179 EncString::Aes256Cbc_B64 { iv, data } => {
180 buf = Vec::with_capacity(1 + 16 + data.len());
181 buf.push(self.enc_type());
182 buf.extend_from_slice(iv);
183 buf.extend_from_slice(data);
184 }
185 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
186 buf = Vec::with_capacity(1 + 16 + 32 + data.len());
187 buf.push(self.enc_type());
188 buf.extend_from_slice(iv);
189 buf.extend_from_slice(mac);
190 buf.extend_from_slice(data);
191 }
192 EncString::Cose_Encrypt0_B64 { data } => {
193 buf = Vec::with_capacity(1 + data.len());
194 buf.push(self.enc_type());
195 buf.extend_from_slice(data);
196 }
197 }
198
199 Ok(buf)
200 }
201}
202
203#[allow(clippy::to_string_trait_impl)]
208impl ToString for EncString {
209 fn to_string(&self) -> String {
210 fn fmt_parts(enc_type: u8, parts: &[&[u8]]) -> String {
211 let encoded_parts: Vec<String> = parts
212 .iter()
213 .map(|part| B64::from(*part).to_string())
214 .collect();
215 format!("{}.{}", enc_type, encoded_parts.join("|"))
216 }
217
218 let enc_type = self.enc_type();
219 match &self {
220 EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(enc_type, &[iv, data]),
221 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
222 fmt_parts(enc_type, &[iv, data, mac])
223 }
224 EncString::Cose_Encrypt0_B64 { data } => fmt_parts(enc_type, &[data]),
225 }
226 }
227}
228
229impl std::fmt::Debug for EncString {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 fn fmt_parts(
232 f: &mut std::fmt::Formatter<'_>,
233 enc_type: u8,
234 parts: &[&[u8]],
235 ) -> std::fmt::Result {
236 let encoded_parts: Vec<String> = parts
237 .iter()
238 .map(|part| B64::from(*part).to_string())
239 .collect();
240 write!(f, "{}.{}", enc_type, encoded_parts.join("|"))
241 }
242
243 let enc_type = self.enc_type();
244
245 match self {
246 EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(f, enc_type, &[iv, data]),
247 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
248 fmt_parts(f, enc_type, &[iv, data, mac])
249 }
250 EncString::Cose_Encrypt0_B64 { data } => {
251 let msg = coset::CoseEncrypt0::from_slice(data.as_slice())
252 .map(|msg| format!("{msg:?}"))
253 .unwrap_or_else(|_| "INVALID_COSE".to_string());
254 write!(f, "{enc_type}.{msg}")
255 }
256 }
257 }
258}
259
260impl<'de> Deserialize<'de> for EncString {
261 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
262 where
263 D: serde::Deserializer<'de>,
264 {
265 deserializer.deserialize_str(FromStrVisitor::new())
266 }
267}
268
269impl serde::Serialize for EncString {
270 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
271 where
272 S: serde::Serializer,
273 {
274 serializer.serialize_str(&self.to_string())
275 }
276}
277
278impl EncString {
279 pub(crate) fn encrypt_aes256_hmac(
280 data_dec: &[u8],
281 key: &Aes256CbcHmacKey,
282 ) -> Result<EncString> {
283 let (iv, mac, data) =
284 crate::aes::encrypt_aes256_hmac(data_dec, &key.mac_key, &key.enc_key)?;
285 Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
286 }
287
288 pub(crate) fn encrypt_xchacha20_poly1305(
289 data_dec: &[u8],
290 key: &XChaCha20Poly1305Key,
291 content_format: ContentFormat,
292 ) -> Result<EncString> {
293 let data = crate::cose::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
294 Ok(EncString::Cose_Encrypt0_B64 {
295 data: data.to_vec(),
296 })
297 }
298
299 const fn enc_type(&self) -> u8 {
301 match self {
302 EncString::Aes256Cbc_B64 { .. } => 0,
303 EncString::Aes256Cbc_HmacSha256_B64 { .. } => 2,
304 EncString::Cose_Encrypt0_B64 { .. } => 7,
305 }
306 }
307}
308
309impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
310 fn encrypt_with_key(
311 self,
312 key: &SymmetricCryptoKey,
313 content_format: ContentFormat,
314 ) -> Result<EncString> {
315 match key {
316 SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
317 SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
318 if !inner_key
319 .supported_operations
320 .contains(&KeyOperation::Encrypt)
321 {
322 return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
323 }
324 EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
325 }
326 SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
327 UnsupportedOperationError::EncryptionNotImplementedForKey,
328 )),
329 }
330 }
331}
332
333impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
334 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
335 match (self, key) {
336 (EncString::Aes256Cbc_B64 { iv, data }, SymmetricCryptoKey::Aes256CbcKey(key)) => {
337 crate::aes::decrypt_aes256(iv, data.clone(), &key.enc_key)
338 .map_err(|_| CryptoError::Decrypt)
339 }
340 (
341 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
342 SymmetricCryptoKey::Aes256CbcHmacKey(key),
343 ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key)
344 .map_err(|_| CryptoError::Decrypt),
345 (
346 EncString::Cose_Encrypt0_B64 { data },
347 SymmetricCryptoKey::XChaCha20Poly1305Key(key),
348 ) => {
349 let (decrypted_message, _) = crate::cose::decrypt_xchacha20_poly1305(
350 &CoseEncrypt0Bytes::from(data.as_slice()),
351 key,
352 )?;
353 Ok(decrypted_message)
354 }
355 _ => Err(CryptoError::WrongKeyType),
356 }
357 }
358}
359
360impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
361 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
362 Utf8Bytes::from(self).encrypt_with_key(key)
363 }
364}
365
366impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
367 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
368 Utf8Bytes::from(self).encrypt_with_key(key)
369 }
370}
371
372impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
373 #[instrument(err, skip_all)]
374 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
375 let dec: Vec<u8> = self.decrypt_with_key(key)?;
376 String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
377 }
378}
379
380impl schemars::JsonSchema for EncString {
383 fn schema_name() -> Cow<'static, str> {
384 "EncString".into()
385 }
386
387 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
388 generator.subschema_for::<String>()
389 }
390}
391
392#[cfg(test)]
393mod tests {
394 use coset::iana::KeyOperation;
395 use schemars::schema_for;
396
397 use super::EncString;
398 use crate::{
399 CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
400 derive_symmetric_key,
401 };
402
403 fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
404 let key_id = [0u8; KEY_ID_SIZE];
405 let enc_key = [0u8; 32];
406 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
407 key_id,
408 enc_key: Box::pin(enc_key.into()),
409 supported_operations: vec![
410 coset::iana::KeyOperation::Decrypt,
411 coset::iana::KeyOperation::Encrypt,
412 coset::iana::KeyOperation::WrapKey,
413 coset::iana::KeyOperation::UnwrapKey,
414 ],
415 });
416
417 plaintext.encrypt_with_key(&key).expect("encryption works")
418 }
419
420 #[test]
424 fn test_xchacha20_encstring_string_padding_block_sizes() {
425 let cases = [
426 ("", 32), (&"a".repeat(31), 32), (&"a".repeat(32), 64), (&"a".repeat(63), 64), (&"a".repeat(64), 96), ];
432
433 let ciphertext_lengths: Vec<_> = cases
434 .iter()
435 .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
436 .collect();
437
438 assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
440 assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
442 assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
443 assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
445 }
446
447 #[test]
448 fn test_enc_roundtrip_xchacha20() {
449 let key_id = [0u8; KEY_ID_SIZE];
450 let enc_key = [0u8; 32];
451 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
452 key_id,
453 enc_key: Box::pin(enc_key.into()),
454 supported_operations: vec![
455 coset::iana::KeyOperation::Decrypt,
456 coset::iana::KeyOperation::Encrypt,
457 coset::iana::KeyOperation::WrapKey,
458 coset::iana::KeyOperation::UnwrapKey,
459 ],
460 });
461
462 let test_string = "encrypted_test_string";
463 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
464 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
465 assert_eq!(decrypted_str, test_string);
466 }
467
468 #[test]
469 fn test_enc_string_roundtrip() {
470 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
471
472 let test_string = "encrypted_test_string";
473 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
474
475 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
476 assert_eq!(decrypted_str, test_string);
477 }
478
479 #[test]
480 fn test_enc_roundtrip_xchacha20_empty() {
481 let key_id = [0u8; KEY_ID_SIZE];
482 let enc_key = [0u8; 32];
483 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
484 key_id,
485 enc_key: Box::pin(enc_key.into()),
486 supported_operations: vec![
487 coset::iana::KeyOperation::Decrypt,
488 coset::iana::KeyOperation::Encrypt,
489 coset::iana::KeyOperation::WrapKey,
490 coset::iana::KeyOperation::UnwrapKey,
491 ],
492 });
493
494 let test_string = "";
495 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
496 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
497 assert_eq!(decrypted_str, test_string);
498 }
499
500 #[test]
501 fn test_enc_string_roundtrip_empty() {
502 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
503
504 let test_string = "";
505 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
506
507 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
508 assert_eq!(decrypted_str, test_string);
509 }
510
511 #[test]
512 fn test_enc_string_ref_roundtrip() {
513 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
514
515 let test_string: &'static str = "encrypted_test_string";
516 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
517
518 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
519 assert_eq!(decrypted_str, test_string);
520 }
521
522 #[test]
523 fn test_enc_string_serialization() {
524 #[derive(serde::Serialize, serde::Deserialize)]
525 struct Test {
526 key: EncString,
527 }
528
529 let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
530 let serialized = format!("{{\"key\":\"{cipher}\"}}");
531
532 let t = serde_json::from_str::<Test>(&serialized).unwrap();
533 assert_eq!(t.key.enc_type(), 2);
534 assert_eq!(t.key.to_string(), cipher);
535 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
536 }
537
538 #[test]
539 fn test_enc_from_to_buffer() {
540 let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
541 let enc_string: EncString = enc_str.parse().unwrap();
542
543 let enc_buf = enc_string.to_buffer().unwrap();
544
545 assert_eq!(
546 enc_buf,
547 vec![
548 2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
549 163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
550 220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
551 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
552 ]
553 );
554
555 let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
556
557 assert_eq!(enc_string_new.to_string(), enc_str)
558 }
559
560 #[test]
561 fn test_from_str_cbc256() {
562 let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
563 let enc_string: EncString = enc_str.parse().unwrap();
564
565 assert_eq!(enc_string.enc_type(), 0);
566 if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
567 assert_eq!(
568 iv,
569 &[
570 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
571 ]
572 );
573 assert_eq!(
574 data,
575 &[
576 93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
577 ]
578 );
579 } else {
580 panic!("Invalid variant")
581 };
582 }
583
584 #[test]
585 fn test_decrypt_cbc256() {
586 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
587 let key = SymmetricCryptoKey::try_from(key).unwrap();
588
589 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
590 let enc_string: EncString = enc_str.parse().unwrap();
591 assert_eq!(enc_string.enc_type(), 0);
592
593 let dec_str: String = enc_string.decrypt_with_key(&key).unwrap();
594 assert_eq!(dec_str, "EncryptMe!");
595 }
596
597 #[test]
598 fn test_decrypt_downgrade_encstring_prevention() {
599 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
602 let key = SymmetricCryptoKey::try_from(key).unwrap();
603
604 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
608 let enc_string: EncString = enc_str.parse().unwrap();
609 assert_eq!(enc_string.enc_type(), 0);
610
611 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
612 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
613 }
614
615 #[test]
616 fn test_encrypt_fails_when_operation_not_allowed() {
617 let key_id = [0u8; KEY_ID_SIZE];
619 let enc_key = [0u8; 32];
620 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
621 key_id,
622 enc_key: Box::pin(enc_key.into()),
623 supported_operations: vec![KeyOperation::Decrypt],
624 });
625
626 let plaintext = "should fail";
627 let result = plaintext.encrypt_with_key(&key);
628 assert!(
629 matches!(
630 result,
631 Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
632 ),
633 "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
634 );
635 }
636
637 #[test]
638 fn test_from_str_invalid() {
639 let enc_str = "8.ABC";
640 let enc_string: Result<EncString, _> = enc_str.parse();
641
642 let err = enc_string.unwrap_err();
643 assert_eq!(
644 err.to_string(),
645 "EncString error, Invalid symmetric type, got type 8 with 1 parts"
646 );
647 }
648
649 #[test]
650 fn test_debug_format() {
651 let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
652 let enc_string: EncString = enc_str.parse().unwrap();
653
654 let debug_string = format!("{enc_string:?}");
655 assert_eq!(debug_string, enc_str);
656 }
657
658 #[test]
659 fn test_json_schema() {
660 let schema = schema_for!(EncString);
661
662 assert_eq!(
663 serde_json::to_string(&schema).unwrap(),
664 r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
665 );
666 }
667}