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 }
339 (
340 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
341 SymmetricCryptoKey::Aes256CbcHmacKey(key),
342 ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key),
343 (
344 EncString::Cose_Encrypt0_B64 { data },
345 SymmetricCryptoKey::XChaCha20Poly1305Key(key),
346 ) => {
347 let (decrypted_message, _) = crate::cose::decrypt_xchacha20_poly1305(
348 &CoseEncrypt0Bytes::from(data.as_slice()),
349 key,
350 )?;
351 Ok(decrypted_message)
352 }
353 _ => Err(CryptoError::WrongKeyType),
354 }
355 }
356}
357
358impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
359 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
360 Utf8Bytes::from(self).encrypt_with_key(key)
361 }
362}
363
364impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
365 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
366 Utf8Bytes::from(self).encrypt_with_key(key)
367 }
368}
369
370impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
371 #[instrument(err, skip_all)]
372 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
373 let dec: Vec<u8> = self.decrypt_with_key(key)?;
374 String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
375 }
376}
377
378impl schemars::JsonSchema for EncString {
381 fn schema_name() -> Cow<'static, str> {
382 "EncString".into()
383 }
384
385 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
386 generator.subschema_for::<String>()
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use coset::iana::KeyOperation;
393 use schemars::schema_for;
394
395 use super::EncString;
396 use crate::{
397 CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
398 derive_symmetric_key,
399 };
400
401 fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
402 let key_id = [0u8; KEY_ID_SIZE];
403 let enc_key = [0u8; 32];
404 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
405 key_id,
406 enc_key: Box::pin(enc_key.into()),
407 supported_operations: vec![
408 coset::iana::KeyOperation::Decrypt,
409 coset::iana::KeyOperation::Encrypt,
410 coset::iana::KeyOperation::WrapKey,
411 coset::iana::KeyOperation::UnwrapKey,
412 ],
413 });
414
415 plaintext.encrypt_with_key(&key).expect("encryption works")
416 }
417
418 #[test]
422 fn test_xchacha20_encstring_string_padding_block_sizes() {
423 let cases = [
424 ("", 32), (&"a".repeat(31), 32), (&"a".repeat(32), 64), (&"a".repeat(63), 64), (&"a".repeat(64), 96), ];
430
431 let ciphertext_lengths: Vec<_> = cases
432 .iter()
433 .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
434 .collect();
435
436 assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
438 assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
440 assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
441 assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
443 }
444
445 #[test]
446 fn test_enc_roundtrip_xchacha20() {
447 let key_id = [0u8; KEY_ID_SIZE];
448 let enc_key = [0u8; 32];
449 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
450 key_id,
451 enc_key: Box::pin(enc_key.into()),
452 supported_operations: vec![
453 coset::iana::KeyOperation::Decrypt,
454 coset::iana::KeyOperation::Encrypt,
455 coset::iana::KeyOperation::WrapKey,
456 coset::iana::KeyOperation::UnwrapKey,
457 ],
458 });
459
460 let test_string = "encrypted_test_string";
461 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
462 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
463 assert_eq!(decrypted_str, test_string);
464 }
465
466 #[test]
467 fn test_enc_string_roundtrip() {
468 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
469
470 let test_string = "encrypted_test_string";
471 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
472
473 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
474 assert_eq!(decrypted_str, test_string);
475 }
476
477 #[test]
478 fn test_enc_roundtrip_xchacha20_empty() {
479 let key_id = [0u8; KEY_ID_SIZE];
480 let enc_key = [0u8; 32];
481 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
482 key_id,
483 enc_key: Box::pin(enc_key.into()),
484 supported_operations: vec![
485 coset::iana::KeyOperation::Decrypt,
486 coset::iana::KeyOperation::Encrypt,
487 coset::iana::KeyOperation::WrapKey,
488 coset::iana::KeyOperation::UnwrapKey,
489 ],
490 });
491
492 let test_string = "";
493 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
494 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
495 assert_eq!(decrypted_str, test_string);
496 }
497
498 #[test]
499 fn test_enc_string_roundtrip_empty() {
500 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
501
502 let test_string = "";
503 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
504
505 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
506 assert_eq!(decrypted_str, test_string);
507 }
508
509 #[test]
510 fn test_enc_string_ref_roundtrip() {
511 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
512
513 let test_string: &'static str = "encrypted_test_string";
514 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
515
516 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
517 assert_eq!(decrypted_str, test_string);
518 }
519
520 #[test]
521 fn test_enc_string_serialization() {
522 #[derive(serde::Serialize, serde::Deserialize)]
523 struct Test {
524 key: EncString,
525 }
526
527 let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
528 let serialized = format!("{{\"key\":\"{cipher}\"}}");
529
530 let t = serde_json::from_str::<Test>(&serialized).unwrap();
531 assert_eq!(t.key.enc_type(), 2);
532 assert_eq!(t.key.to_string(), cipher);
533 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
534 }
535
536 #[test]
537 fn test_enc_from_to_buffer() {
538 let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
539 let enc_string: EncString = enc_str.parse().unwrap();
540
541 let enc_buf = enc_string.to_buffer().unwrap();
542
543 assert_eq!(
544 enc_buf,
545 vec![
546 2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
547 163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
548 220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
549 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
550 ]
551 );
552
553 let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
554
555 assert_eq!(enc_string_new.to_string(), enc_str)
556 }
557
558 #[test]
559 fn test_from_str_cbc256() {
560 let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
561 let enc_string: EncString = enc_str.parse().unwrap();
562
563 assert_eq!(enc_string.enc_type(), 0);
564 if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
565 assert_eq!(
566 iv,
567 &[
568 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
569 ]
570 );
571 assert_eq!(
572 data,
573 &[
574 93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
575 ]
576 );
577 } else {
578 panic!("Invalid variant")
579 };
580 }
581
582 #[test]
583 fn test_decrypt_cbc256() {
584 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
585 let key = SymmetricCryptoKey::try_from(key).unwrap();
586
587 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
588 let enc_string: EncString = enc_str.parse().unwrap();
589 assert_eq!(enc_string.enc_type(), 0);
590
591 let dec_str: String = enc_string.decrypt_with_key(&key).unwrap();
592 assert_eq!(dec_str, "EncryptMe!");
593 }
594
595 #[test]
596 fn test_decrypt_downgrade_encstring_prevention() {
597 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
600 let key = SymmetricCryptoKey::try_from(key).unwrap();
601
602 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
606 let enc_string: EncString = enc_str.parse().unwrap();
607 assert_eq!(enc_string.enc_type(), 0);
608
609 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
610 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
611 }
612
613 #[test]
614 fn test_encrypt_fails_when_operation_not_allowed() {
615 let key_id = [0u8; KEY_ID_SIZE];
617 let enc_key = [0u8; 32];
618 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
619 key_id,
620 enc_key: Box::pin(enc_key.into()),
621 supported_operations: vec![KeyOperation::Decrypt],
622 });
623
624 let plaintext = "should fail";
625 let result = plaintext.encrypt_with_key(&key);
626 assert!(
627 matches!(
628 result,
629 Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
630 ),
631 "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
632 );
633 }
634
635 #[test]
636 fn test_from_str_invalid() {
637 let enc_str = "8.ABC";
638 let enc_string: Result<EncString, _> = enc_str.parse();
639
640 let err = enc_string.unwrap_err();
641 assert_eq!(
642 err.to_string(),
643 "EncString error, Invalid symmetric type, got type 8 with 1 parts"
644 );
645 }
646
647 #[test]
648 fn test_debug_format() {
649 let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
650 let enc_string: EncString = enc_str.parse().unwrap();
651
652 let debug_string = format!("{enc_string:?}");
653 assert_eq!(debug_string, enc_str);
654 }
655
656 #[test]
657 fn test_json_schema() {
658 let schema = schema_for!(EncString);
659
660 assert_eq!(
661 serde_json::to_string(&schema).unwrap(),
662 r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
663 );
664 }
665}