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