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