1use std::{borrow::Cow, str::FromStr};
2
3use bitwarden_encoding::{FromStrVisitor, B64};
4use coset::CborSerializable;
5use serde::Deserialize;
6
7use super::{check_length, from_b64, from_b64_vec, split_enc_string};
8use crate::{
9 error::{CryptoError, EncStringParseError, Result, UnsupportedOperation},
10 Aes256CbcHmacKey, ContentFormat, KeyDecryptable, KeyEncryptable, KeyEncryptableWithContentType,
11 SymmetricCryptoKey, Utf8Bytes, XChaCha20Poly1305Key,
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 { data })
273 }
274
275 const fn enc_type(&self) -> u8 {
277 match self {
278 EncString::Aes256Cbc_B64 { .. } => 0,
279 EncString::Aes256Cbc_HmacSha256_B64 { .. } => 2,
280 EncString::Cose_Encrypt0_B64 { .. } => 7,
281 }
282 }
283}
284
285impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
286 fn encrypt_with_key(
287 self,
288 key: &SymmetricCryptoKey,
289 content_format: ContentFormat,
290 ) -> Result<EncString> {
291 match key {
292 SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
293 SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
294 EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
295 }
296 SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
297 UnsupportedOperation::EncryptionNotImplementedForKey,
298 )),
299 }
300 }
301}
302
303impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
304 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
305 match (self, key) {
306 (EncString::Aes256Cbc_B64 { iv, data }, SymmetricCryptoKey::Aes256CbcKey(key)) => {
307 crate::aes::decrypt_aes256(iv, data.clone(), &key.enc_key)
308 }
309 (
310 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
311 SymmetricCryptoKey::Aes256CbcHmacKey(key),
312 ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key),
313 (
314 EncString::Cose_Encrypt0_B64 { data },
315 SymmetricCryptoKey::XChaCha20Poly1305Key(key),
316 ) => {
317 let (decrypted_message, _) =
318 crate::cose::decrypt_xchacha20_poly1305(data.as_slice(), key)?;
319 Ok(decrypted_message)
320 }
321 _ => Err(CryptoError::WrongKeyType),
322 }
323 }
324}
325
326impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
327 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
328 Utf8Bytes::from(self).encrypt_with_key(key)
329 }
330}
331
332impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
333 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
334 Utf8Bytes::from(self).encrypt_with_key(key)
335 }
336}
337
338impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
339 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
340 let dec: Vec<u8> = self.decrypt_with_key(key)?;
341 String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
342 }
343}
344
345impl schemars::JsonSchema for EncString {
348 fn schema_name() -> Cow<'static, str> {
349 "EncString".into()
350 }
351
352 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
353 generator.subschema_for::<String>()
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use schemars::schema_for;
360
361 use super::EncString;
362 use crate::{
363 derive_symmetric_key, CryptoError, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
364 KEY_ID_SIZE,
365 };
366
367 fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
368 let key_id = [0u8; KEY_ID_SIZE];
369 let enc_key = [0u8; 32];
370 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
371 key_id,
372 enc_key: Box::pin(enc_key.into()),
373 });
374
375 plaintext.encrypt_with_key(&key).expect("encryption works")
376 }
377
378 #[test]
382 fn test_xchacha20_encstring_string_padding_block_sizes() {
383 let cases = [
384 ("", 32), (&"a".repeat(31), 32), (&"a".repeat(32), 64), (&"a".repeat(63), 64), (&"a".repeat(64), 96), ];
390
391 let ciphertext_lengths: Vec<_> = cases
392 .iter()
393 .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
394 .collect();
395
396 assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
398 assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
400 assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
401 assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
403 }
404
405 #[test]
406 fn test_enc_roundtrip_xchacha20() {
407 let key_id = [0u8; KEY_ID_SIZE];
408 let enc_key = [0u8; 32];
409 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
410 key_id,
411 enc_key: Box::pin(enc_key.into()),
412 });
413
414 let test_string = "encrypted_test_string";
415 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
416 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
417 assert_eq!(decrypted_str, test_string);
418 }
419
420 #[test]
421 fn test_enc_string_roundtrip() {
422 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
423
424 let test_string = "encrypted_test_string";
425 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
426
427 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
428 assert_eq!(decrypted_str, test_string);
429 }
430
431 #[test]
432 fn test_enc_roundtrip_xchacha20_empty() {
433 let key_id = [0u8; KEY_ID_SIZE];
434 let enc_key = [0u8; 32];
435 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
436 key_id,
437 enc_key: Box::pin(enc_key.into()),
438 });
439
440 let test_string = "";
441 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
442 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
443 assert_eq!(decrypted_str, test_string);
444 }
445
446 #[test]
447 fn test_enc_string_roundtrip_empty() {
448 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
449
450 let test_string = "";
451 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
452
453 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
454 assert_eq!(decrypted_str, test_string);
455 }
456
457 #[test]
458 fn test_enc_string_ref_roundtrip() {
459 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
460
461 let test_string: &'static str = "encrypted_test_string";
462 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
463
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_serialization() {
470 #[derive(serde::Serialize, serde::Deserialize)]
471 struct Test {
472 key: EncString,
473 }
474
475 let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
476 let serialized = format!("{{\"key\":\"{cipher}\"}}");
477
478 let t = serde_json::from_str::<Test>(&serialized).unwrap();
479 assert_eq!(t.key.enc_type(), 2);
480 assert_eq!(t.key.to_string(), cipher);
481 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
482 }
483
484 #[test]
485 fn test_enc_from_to_buffer() {
486 let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
487 let enc_string: EncString = enc_str.parse().unwrap();
488
489 let enc_buf = enc_string.to_buffer().unwrap();
490
491 assert_eq!(
492 enc_buf,
493 vec![
494 2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
495 163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
496 220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
497 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
498 ]
499 );
500
501 let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
502
503 assert_eq!(enc_string_new.to_string(), enc_str)
504 }
505
506 #[test]
507 fn test_from_str_cbc256() {
508 let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
509 let enc_string: EncString = enc_str.parse().unwrap();
510
511 assert_eq!(enc_string.enc_type(), 0);
512 if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
513 assert_eq!(
514 iv,
515 &[164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150]
516 );
517 assert_eq!(
518 data,
519 &[93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215]
520 );
521 } else {
522 panic!("Invalid variant")
523 };
524 }
525
526 #[test]
527 fn test_decrypt_cbc256() {
528 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
529 let key = SymmetricCryptoKey::try_from(key).unwrap();
530
531 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
532 let enc_string: EncString = enc_str.parse().unwrap();
533 assert_eq!(enc_string.enc_type(), 0);
534
535 let dec_str: String = enc_string.decrypt_with_key(&key).unwrap();
536 assert_eq!(dec_str, "EncryptMe!");
537 }
538
539 #[test]
540 fn test_decrypt_downgrade_encstring_prevention() {
541 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
544 let key = SymmetricCryptoKey::try_from(key).unwrap();
545
546 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
550 let enc_string: EncString = enc_str.parse().unwrap();
551 assert_eq!(enc_string.enc_type(), 0);
552
553 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
554 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
555 }
556
557 #[test]
558 fn test_from_str_invalid() {
559 let enc_str = "8.ABC";
560 let enc_string: Result<EncString, _> = enc_str.parse();
561
562 let err = enc_string.unwrap_err();
563 assert_eq!(
564 err.to_string(),
565 "EncString error, Invalid symmetric type, got type 8 with 1 parts"
566 );
567 }
568
569 #[test]
570 fn test_debug_format() {
571 let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
572 let enc_string: EncString = enc_str.parse().unwrap();
573
574 let debug_string = format!("{enc_string:?}");
575 assert_eq!(debug_string, enc_str);
576 }
577
578 #[test]
579 fn test_json_schema() {
580 let schema = schema_for!(EncString);
581
582 assert_eq!(
583 serde_json::to_string(&schema).unwrap(),
584 r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
585 );
586 }
587}