1use std::{borrow::Cow, str::FromStr};
2
3use bitwarden_encoding::{B64, FromStrVisitor};
4use coset::{CborSerializable, iana::KeyOperation};
5use serde::Deserialize;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
8
9use super::{check_length, from_b64, from_b64_vec, split_enc_string};
10use crate::{
11 Aes256CbcHmacKey, ContentFormat, CoseEncrypt0Bytes, KeyDecryptable, KeyEncryptable,
12 KeyEncryptableWithContentType, SymmetricCryptoKey, Utf8Bytes, XAes256GcmKey,
13 XChaCha20Poly1305Key,
14 cose::{XAES_256_GCM, 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 coset::Algorithm::PrivateUse(XAES_256_GCM) => "XAES-256-GCM",
304 other => return debug_struct.field("algorithm", other).finish(),
305 };
306 debug_struct.field("algorithm", &alg_name);
307 }
308
309 let key_id = &msg.protected.header.key_id;
310 if let Ok(key_id) = KeyId::try_from(key_id.as_slice()) {
311 debug_struct.field("key_id", &key_id);
312 }
313 debug_struct.field("nonce", &hex::encode(msg.unprotected.iv.as_slice()));
314 if let Some(ref content_type) = msg.protected.header.content_type {
315 debug_struct.field("content_type", content_type);
316 }
317
318 #[cfg(feature = "dangerous-crypto-debug")]
319 if let Some(ref ciphertext) = msg.ciphertext {
320 debug_struct.field("ciphertext", &hex::encode(ciphertext));
321 }
322 }
323 Err(_) => {
324 debug_struct.field("error", &"INVALID_COSE");
325 }
326 }
327
328 debug_struct.finish()
329 }
330 }
331 }
332}
333
334impl<'de> Deserialize<'de> for EncString {
335 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
336 where
337 D: serde::Deserializer<'de>,
338 {
339 deserializer.deserialize_str(FromStrVisitor::new())
340 }
341}
342
343impl serde::Serialize for EncString {
344 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
345 where
346 S: serde::Serializer,
347 {
348 serializer.serialize_str(&self.to_string())
349 }
350}
351
352impl EncString {
353 pub(crate) fn encrypt_aes256_hmac(
354 data_dec: &[u8],
355 key: &Aes256CbcHmacKey,
356 ) -> Result<EncString> {
357 let (iv, mac, data) =
358 crate::aes::encrypt_aes256_hmac(data_dec, &key.mac_key, &key.enc_key)?;
359 Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
360 }
361
362 pub(crate) fn encrypt_xchacha20_poly1305(
363 data_dec: &[u8],
364 key: &XChaCha20Poly1305Key,
365 content_format: ContentFormat,
366 ) -> Result<EncString> {
367 let data =
368 crate::cose::symmetric::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
369 Ok(EncString::Cose_Encrypt0_B64 {
370 data: data.to_vec(),
371 })
372 }
373
374 pub(crate) fn encrypt_xaes256_gcm(
375 data_dec: &[u8],
376 key: &XAes256GcmKey,
377 content_format: ContentFormat,
378 ) -> Result<EncString> {
379 let data = crate::cose::symmetric::encrypt_xaes256_gcm(data_dec, key, content_format)?;
380 Ok(EncString::Cose_Encrypt0_B64 {
381 data: data.to_vec(),
382 })
383 }
384
385 const fn enc_type(&self) -> u8 {
387 match self {
388 EncString::Aes256Cbc_B64 { .. } => 0,
389 EncString::Aes256Cbc_HmacSha256_B64 { .. } => 2,
390 EncString::Cose_Encrypt0_B64 { .. } => 7,
391 }
392 }
393}
394
395impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
396 fn encrypt_with_key(
397 self,
398 key: &SymmetricCryptoKey,
399 content_format: ContentFormat,
400 ) -> Result<EncString> {
401 match key {
402 SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
403 SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
404 if !inner_key
405 .supported_operations
406 .contains(&KeyOperation::Encrypt)
407 {
408 return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
409 }
410 EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
411 }
412 SymmetricCryptoKey::XAes256GcmKey(key) => {
413 if !key.supported_operations.contains(&KeyOperation::Encrypt) {
414 return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
415 }
416 EncString::encrypt_xaes256_gcm(self, key, content_format)
417 }
418 SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
419 UnsupportedOperationError::EncryptionNotImplementedForKey,
420 )),
421 SymmetricCryptoKey::Aes256GcmKey(_) => Err(CryptoError::OperationNotSupported(
422 UnsupportedOperationError::EncryptionNotImplementedForKey,
423 )),
424 }
425 }
426}
427
428impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
429 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
430 match (self, key) {
431 (EncString::Aes256Cbc_B64 { .. }, SymmetricCryptoKey::Aes256CbcKey(_)) => {
432 Err(CryptoError::OperationNotSupported(
433 UnsupportedOperationError::DecryptionNotImplementedForKey,
434 ))
435 }
436 (
437 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
438 SymmetricCryptoKey::Aes256CbcHmacKey(key),
439 ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key)
440 .map_err(|_| CryptoError::Decrypt),
441 (
442 EncString::Cose_Encrypt0_B64 { data },
443 SymmetricCryptoKey::XChaCha20Poly1305Key(key),
444 ) => {
445 let (decrypted_message, _) = crate::cose::symmetric::decrypt_xchacha20_poly1305(
446 &CoseEncrypt0Bytes::from(data.as_slice()),
447 key,
448 )?;
449 Ok(decrypted_message)
450 }
451 (EncString::Cose_Encrypt0_B64 { data }, SymmetricCryptoKey::XAes256GcmKey(key)) => {
452 let (decrypted, _) = crate::cose::symmetric::decrypt_xaes256_gcm(
453 &CoseEncrypt0Bytes::from(data.as_slice()),
454 key,
455 )?;
456 Ok(decrypted)
457 }
458 (_, SymmetricCryptoKey::XAes256GcmKey(_)) => Err(CryptoError::WrongKeyType),
459 _ => Err(CryptoError::WrongKeyType),
460 }
461 }
462}
463
464impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
465 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
466 Utf8Bytes::from(self).encrypt_with_key(key)
467 }
468}
469
470impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
471 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
472 Utf8Bytes::from(self).encrypt_with_key(key)
473 }
474}
475
476impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
477 #[bitwarden_logging::instrument(err)]
478 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
479 let dec: Vec<u8> = self.decrypt_with_key(key)?;
480 String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
481 }
482}
483
484impl schemars::JsonSchema for EncString {
487 fn schema_name() -> Cow<'static, str> {
488 "EncString".into()
489 }
490
491 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
492 generator.subschema_for::<String>()
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use coset::iana::KeyOperation;
499 use schemars::schema_for;
500
501 use super::EncString;
502 use crate::{
503 CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
504 derive_symmetric_key,
505 };
506
507 fn xaes_key(operations: Vec<KeyOperation>) -> SymmetricCryptoKey {
508 SymmetricCryptoKey::XAes256GcmKey(crate::XAes256GcmKey {
509 key_id: [0u8; KEY_ID_SIZE].into(),
510 enc_key: Box::pin([0u8; 32].into()),
511 supported_operations: operations,
512 })
513 }
514
515 fn encrypt_with_xaes(plaintext: &str) -> EncString {
516 plaintext
517 .to_owned()
518 .encrypt_with_key(&xaes_key(vec![
519 coset::iana::KeyOperation::Decrypt,
520 coset::iana::KeyOperation::Encrypt,
521 coset::iana::KeyOperation::WrapKey,
522 coset::iana::KeyOperation::UnwrapKey,
523 ]))
524 .expect("encryption works")
525 }
526
527 fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
528 let key_id = [0u8; KEY_ID_SIZE];
529 let enc_key = [0u8; 32];
530 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
531 key_id: key_id.into(),
532 enc_key: Box::pin(enc_key.into()),
533 supported_operations: vec![
534 coset::iana::KeyOperation::Decrypt,
535 coset::iana::KeyOperation::Encrypt,
536 coset::iana::KeyOperation::WrapKey,
537 coset::iana::KeyOperation::UnwrapKey,
538 ],
539 });
540
541 plaintext.encrypt_with_key(&key).expect("encryption works")
542 }
543
544 #[test]
545 #[ignore = "Manual test to verify debug format"]
546 fn test_debug() {
547 let enc_string = encrypt_with_xchacha20("Test debug string");
548 println!("{:?}", enc_string);
549 let enc_string_aes =
550 EncString::encrypt_aes256_hmac(b"Test debug string", &derive_symmetric_key("test"))
551 .unwrap();
552 println!("{:?}", enc_string_aes);
553 }
554
555 #[test]
559 fn test_xchacha20_encstring_string_padding_block_sizes() {
560 let cases = [
561 ("", 32), (&"a".repeat(31), 32), (&"a".repeat(32), 64), (&"a".repeat(63), 64), (&"a".repeat(64), 96), ];
567
568 let ciphertext_lengths: Vec<_> = cases
569 .iter()
570 .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
571 .collect();
572
573 assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
575 assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
577 assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
578 assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
580 }
581
582 #[test]
583 fn test_enc_roundtrip_xchacha20() {
584 let key_id = [0u8; KEY_ID_SIZE];
585 let enc_key = [0u8; 32];
586 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
587 key_id: key_id.into(),
588 enc_key: Box::pin(enc_key.into()),
589 supported_operations: vec![
590 coset::iana::KeyOperation::Decrypt,
591 coset::iana::KeyOperation::Encrypt,
592 coset::iana::KeyOperation::WrapKey,
593 coset::iana::KeyOperation::UnwrapKey,
594 ],
595 });
596
597 let test_string = "encrypted_test_string";
598 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
599 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
600 assert_eq!(decrypted_str, test_string);
601 }
602
603 #[test]
604 fn test_xaes_encstring_string_roundtrips() {
605 let key = xaes_key(vec![
606 coset::iana::KeyOperation::Decrypt,
607 coset::iana::KeyOperation::Encrypt,
608 coset::iana::KeyOperation::WrapKey,
609 coset::iana::KeyOperation::UnwrapKey,
610 ]);
611 for plaintext in ["", "encrypted_test_string"] {
612 let encrypted = plaintext.to_owned().encrypt_with_key(&key).unwrap();
613 let decrypted: String = encrypted.decrypt_with_key(&key).unwrap();
614 assert_eq!(decrypted, plaintext);
615 }
616 }
617
618 #[test]
619 fn test_xaes_encstring_string_padding_block_sizes() {
620 let lengths = [0, 31, 32, 63, 64]
629 .map(|length| encrypt_with_xaes(&"a".repeat(length)).to_string().len());
630
631 assert_eq!(lengths[0], lengths[1]); assert_ne!(lengths[1], lengths[2]); assert_eq!(lengths[2], lengths[3]); assert_ne!(lengths[3], lengths[4]); }
636
637 #[test]
638 fn test_xaes_encryption_requires_encrypt_operation() {
639 assert!(matches!(
640 "plaintext"
641 .to_owned()
642 .encrypt_with_key(&xaes_key(vec![KeyOperation::Decrypt])),
643 Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
644 ));
645 }
646
647 #[test]
648 fn test_xaes_rejects_unsupported_encstring_variant() {
649 let encrypted =
650 EncString::encrypt_aes256_hmac(b"plaintext", &derive_symmetric_key("wrapping key"))
651 .unwrap();
652 let result: Result<Vec<u8>, CryptoError> =
653 encrypted.decrypt_with_key(&xaes_key(vec![KeyOperation::Encrypt]));
654 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
655 }
656
657 #[test]
658 fn test_xaes_encstring_debug_is_readable() {
659 let debug = format!("{:?}", encrypt_with_xaes("plaintext"));
660 assert!(debug.contains("EncString::CoseEncrypt0"));
661 assert!(debug.contains("XAES-256-GCM"));
662 assert!(debug.contains("KeyId(00000000000000000000000000000000)"));
663 assert!(debug.contains("nonce"));
664 assert!(debug.contains("content_type"));
665 }
666
667 #[test]
668 fn test_enc_string_roundtrip() {
669 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
670
671 let test_string = "encrypted_test_string";
672 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
673
674 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
675 assert_eq!(decrypted_str, test_string);
676 }
677
678 #[test]
679 fn test_enc_roundtrip_xchacha20_empty() {
680 let key_id = [0u8; KEY_ID_SIZE];
681 let enc_key = [0u8; 32];
682 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
683 key_id: key_id.into(),
684 enc_key: Box::pin(enc_key.into()),
685 supported_operations: vec![
686 coset::iana::KeyOperation::Decrypt,
687 coset::iana::KeyOperation::Encrypt,
688 coset::iana::KeyOperation::WrapKey,
689 coset::iana::KeyOperation::UnwrapKey,
690 ],
691 });
692
693 let test_string = "";
694 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
695 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
696 assert_eq!(decrypted_str, test_string);
697 }
698
699 #[test]
700 fn test_enc_string_roundtrip_empty() {
701 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
702
703 let test_string = "";
704 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
705
706 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
707 assert_eq!(decrypted_str, test_string);
708 }
709
710 #[test]
711 fn test_enc_string_ref_roundtrip() {
712 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
713
714 let test_string: &'static str = "encrypted_test_string";
715 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
716
717 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
718 assert_eq!(decrypted_str, test_string);
719 }
720
721 #[test]
722 fn test_enc_string_serialization() {
723 #[derive(serde::Serialize, serde::Deserialize)]
724 struct Test {
725 key: EncString,
726 }
727
728 let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
729 let serialized = format!("{{\"key\":\"{cipher}\"}}");
730
731 let t = serde_json::from_str::<Test>(&serialized).unwrap();
732 assert_eq!(t.key.enc_type(), 2);
733 assert_eq!(t.key.to_string(), cipher);
734 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
735 }
736
737 #[test]
738 fn test_enc_from_to_buffer() {
739 let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
740 let enc_string: EncString = enc_str.parse().unwrap();
741
742 let enc_buf = enc_string.to_buffer().unwrap();
743
744 assert_eq!(
745 enc_buf,
746 vec![
747 2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
748 163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
749 220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
750 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
751 ]
752 );
753
754 let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
755
756 assert_eq!(enc_string_new.to_string(), enc_str)
757 }
758
759 #[test]
760 fn test_from_str_cbc256() {
761 let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
762 let enc_string: EncString = enc_str.parse().unwrap();
763
764 assert_eq!(enc_string.enc_type(), 0);
765 if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
766 assert_eq!(
767 iv,
768 &[
769 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
770 ]
771 );
772 assert_eq!(
773 data,
774 &[
775 93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
776 ]
777 );
778 } else {
779 panic!("Invalid variant")
780 };
781 }
782
783 #[test]
784 fn test_decrypt_fails_for_cbc256_keys() {
785 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
786 let key = SymmetricCryptoKey::try_from(key).unwrap();
787
788 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
789 let enc_string: EncString = enc_str.parse().unwrap();
790 assert_eq!(enc_string.enc_type(), 0);
791
792 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
793 assert!(
794 matches!(
795 result,
796 Err(CryptoError::OperationNotSupported(
797 crate::error::UnsupportedOperationError::DecryptionNotImplementedForKey
798 )),
799 ),
800 "Expected decrypt to fail when using deprecated type 0 key",
801 );
802 }
803
804 #[test]
805 fn test_decrypt_downgrade_encstring_prevention() {
806 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
809 let key = SymmetricCryptoKey::try_from(key).unwrap();
810
811 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
815 let enc_string: EncString = enc_str.parse().unwrap();
816 assert_eq!(enc_string.enc_type(), 0);
817
818 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
819 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
820 }
821
822 #[test]
823 fn test_encrypt_fails_when_operation_not_allowed() {
824 let key_id = [0u8; KEY_ID_SIZE];
826 let enc_key = [0u8; 32];
827 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
828 key_id: key_id.into(),
829 enc_key: Box::pin(enc_key.into()),
830 supported_operations: vec![KeyOperation::Decrypt],
831 });
832
833 let plaintext = "should fail";
834 let result = plaintext.encrypt_with_key(&key);
835 assert!(
836 matches!(
837 result,
838 Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
839 ),
840 "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
841 );
842 }
843
844 #[test]
845 fn test_from_str_invalid() {
846 let enc_str = "8.ABC";
847 let enc_string: Result<EncString, _> = enc_str.parse();
848
849 let err = enc_string.unwrap_err();
850 assert_eq!(
851 err.to_string(),
852 "EncString error, Invalid symmetric type, got type 8 with 1 parts"
853 );
854 }
855
856 #[test]
857 #[cfg(not(feature = "dangerous-crypto-debug"))]
858 fn test_debug_format() {
859 let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
860 let enc_string: EncString = enc_str.parse().unwrap();
861 assert_eq!(
862 "EncString::Aes256CbcHmacSha256".to_string(),
863 format!("{:?}", enc_string)
864 );
865 }
866
867 #[test]
868 fn test_json_schema() {
869 let schema = schema_for!(EncString);
870
871 assert_eq!(
872 serde_json::to_string(&schema).unwrap(),
873 r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
874 );
875 }
876}