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