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