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
27const AES256_CBC_TYPE: u8 = 0;
30const AES256_CBC_HMAC_TYPE: u8 = 2;
31const COSE_ENCRYPT0_TYPE: u8 = 7;
32
33#[allow(missing_docs)]
70#[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)]
71#[allow(unused, non_camel_case_types)]
72pub enum EncString {
73 Aes256Cbc_B64 {
75 iv: [u8; 16],
76 data: Vec<u8>,
77 },
78 Aes256Cbc_HmacSha256_B64 {
81 iv: [u8; 16],
82 mac: [u8; 32],
83 data: Vec<u8>,
84 },
85 Cose_Encrypt0_B64 {
87 data: Vec<u8>,
88 },
89 Unparseable {
92 raw: String,
93 },
94}
95
96#[cfg(feature = "wasm")]
97impl wasm_bindgen::describe::WasmDescribe for EncString {
98 fn describe() {
99 <String as wasm_bindgen::describe::WasmDescribe>::describe();
100 }
101}
102
103#[cfg(feature = "wasm")]
104impl FromWasmAbi for EncString {
105 type Abi = <String as FromWasmAbi>::Abi;
106
107 unsafe fn from_abi(abi: Self::Abi) -> Self {
108 use wasm_bindgen::UnwrapThrowExt;
109
110 let s = unsafe { String::from_abi(abi) };
111 Self::from_str(&s).unwrap_throw()
112 }
113}
114
115#[cfg(feature = "wasm")]
116impl OptionFromWasmAbi for EncString {
117 fn is_none(abi: &Self::Abi) -> bool {
118 <String as OptionFromWasmAbi>::is_none(abi)
119 }
120}
121
122#[cfg(feature = "wasm")]
123impl IntoWasmAbi for EncString {
124 type Abi = <String as IntoWasmAbi>::Abi;
125
126 fn into_abi(self) -> Self::Abi {
127 self.to_string().into_abi()
128 }
129}
130
131#[cfg(feature = "wasm")]
132impl TryFrom<wasm_bindgen::JsValue> for EncString {
133 type Error = CryptoError;
134
135 fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
136 let string = value
137 .as_string()
138 .ok_or(EncStringParseError::NoType)
139 .map_err(CryptoError::from)?;
140 Self::from_str(&string)
141 }
142}
143
144impl FromStr for EncString {
146 type Err = CryptoError;
147
148 fn from_str(s: &str) -> Result<Self, Self::Err> {
151 Ok(Self::parse_known_format(s)
152 .unwrap_or_else(|| EncString::Unparseable { raw: s.to_owned() }))
153 }
154}
155
156impl From<&str> for EncString {
159 fn from(s: &str) -> Self {
160 Self::parse_known_format(s).unwrap_or_else(|| EncString::Unparseable { raw: s.to_owned() })
161 }
162}
163
164impl From<String> for EncString {
167 fn from(s: String) -> Self {
168 Self::parse_known_format(&s).unwrap_or(EncString::Unparseable { raw: s })
169 }
170}
171
172impl EncString {
173 fn parse_known_format(s: &str) -> Option<Self> {
175 let (enc_type, parts) = split_enc_string(s);
176 match (enc_type, parts.len()) {
177 ("0", 2) => Some(EncString::Aes256Cbc_B64 {
178 iv: from_b64(parts[0]).ok()?,
179 data: from_b64_vec(parts[1]).ok()?,
180 }),
181 ("2", 3) => Some(EncString::Aes256Cbc_HmacSha256_B64 {
182 iv: from_b64(parts[0]).ok()?,
183 data: from_b64_vec(parts[1]).ok()?,
184 mac: from_b64(parts[2]).ok()?,
185 }),
186 ("7", 1) => Some(EncString::Cose_Encrypt0_B64 {
187 data: from_b64_vec(parts[0]).ok()?,
188 }),
189 _ => None,
190 }
191 }
192
193 pub fn parse_strict(s: &str) -> Result<Self, CryptoError> {
195 Self::parse_known_format(s).ok_or(CryptoError::UnparseableEncString)
196 }
197
198 pub fn try_from_optional(s: Option<String>) -> Result<Option<EncString>, CryptoError> {
200 s.map(|s| s.parse()).transpose()
201 }
202
203 pub fn from_buffer(buf: &[u8]) -> Result<Self> {
206 if buf.is_empty() {
207 return Err(EncStringParseError::NoType.into());
208 }
209 let enc_type = buf[0];
210
211 match enc_type {
212 AES256_CBC_TYPE => {
213 check_length(buf, 18)?;
214 let iv = buf[1..17].try_into().expect("Valid length");
215 let data = buf[17..].to_vec();
216
217 Ok(EncString::Aes256Cbc_B64 { iv, data })
218 }
219 AES256_CBC_HMAC_TYPE => {
220 check_length(buf, 50)?;
221 let iv = buf[1..17].try_into().expect("Valid length");
222 let mac = buf[17..49].try_into().expect("Valid length");
223 let data = buf[49..].to_vec();
224
225 Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
226 }
227 COSE_ENCRYPT0_TYPE => Ok(EncString::Cose_Encrypt0_B64 {
228 data: buf[1..].to_vec(),
229 }),
230 _ => Err(EncStringParseError::InvalidTypeSymm {
231 enc_type: enc_type.to_string(),
232 parts: 1,
233 }
234 .into()),
235 }
236 }
237
238 #[allow(missing_docs)]
239 pub fn to_buffer(&self) -> Result<Vec<u8>> {
240 let mut buf;
241 let enc_type = self.enc_type().ok_or(CryptoError::UnparseableEncString)?;
242
243 match self {
244 EncString::Aes256Cbc_B64 { iv, data } => {
245 buf = Vec::with_capacity(1 + 16 + data.len());
246 buf.push(enc_type);
247 buf.extend_from_slice(iv);
248 buf.extend_from_slice(data);
249 }
250 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
251 buf = Vec::with_capacity(1 + 16 + 32 + data.len());
252 buf.push(enc_type);
253 buf.extend_from_slice(iv);
254 buf.extend_from_slice(mac);
255 buf.extend_from_slice(data);
256 }
257 EncString::Cose_Encrypt0_B64 { data } => {
258 buf = Vec::with_capacity(1 + data.len());
259 buf.push(enc_type);
260 buf.extend_from_slice(data);
261 }
262 EncString::Unparseable { .. } => return Err(CryptoError::UnparseableEncString),
263 }
264
265 Ok(buf)
266 }
267}
268
269#[allow(clippy::to_string_trait_impl)]
274impl ToString for EncString {
275 fn to_string(&self) -> String {
276 fn fmt_parts(enc_type: u8, parts: &[&[u8]]) -> String {
277 let encoded_parts: Vec<String> = parts
278 .iter()
279 .map(|part| B64::from(*part).to_string())
280 .collect();
281 format!("{}.{}", enc_type, encoded_parts.join("|"))
282 }
283
284 match &self {
285 EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(AES256_CBC_TYPE, &[iv, data]),
286 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
287 fmt_parts(AES256_CBC_HMAC_TYPE, &[iv, data, mac])
288 }
289 EncString::Cose_Encrypt0_B64 { data } => fmt_parts(COSE_ENCRYPT0_TYPE, &[data]),
290 EncString::Unparseable { raw } => raw.clone(),
291 }
292 }
293}
294
295impl std::fmt::Debug for EncString {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 match self {
298 EncString::Aes256Cbc_B64 { iv, data } => {
299 let mut debug_struct = f.debug_struct("EncString::Aes256Cbc");
300 #[cfg(feature = "dangerous-crypto-debug")]
301 {
302 debug_struct.field("iv", &hex::encode(iv));
303 debug_struct.field("data", &hex::encode(data));
304 }
305 #[cfg(not(feature = "dangerous-crypto-debug"))]
306 {
307 _ = iv;
308 _ = data;
309 }
310 debug_struct.finish()
311 }
312 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
313 let mut debug_struct = f.debug_struct("EncString::Aes256CbcHmacSha256");
314 #[cfg(feature = "dangerous-crypto-debug")]
315 {
316 debug_struct.field("iv", &hex::encode(iv));
317 debug_struct.field("data", &hex::encode(data));
318 debug_struct.field("mac", &hex::encode(mac));
319 }
320 #[cfg(not(feature = "dangerous-crypto-debug"))]
321 {
322 _ = iv;
323 _ = data;
324 _ = mac;
325 }
326 debug_struct.finish()
327 }
328 EncString::Unparseable { raw } => {
329 let mut debug_struct = f.debug_struct("EncString::Unparseable");
330 #[cfg(feature = "dangerous-crypto-debug")]
331 debug_struct.field("raw", raw);
332 #[cfg(not(feature = "dangerous-crypto-debug"))]
333 {
334 _ = raw;
335 }
336 debug_struct.finish()
337 }
338 EncString::Cose_Encrypt0_B64 { data } => {
339 let mut debug_struct = f.debug_struct("EncString::CoseEncrypt0");
340
341 match coset::CoseEncrypt0::from_slice(data.as_slice()) {
342 Ok(msg) => {
343 if let Some(ref alg) = msg.protected.header.alg {
344 let alg_name = match alg {
345 coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
346 "XChaCha20-Poly1305"
347 }
348 coset::Algorithm::PrivateUse(XAES_256_GCM) => "XAES-256-GCM",
349 other => return debug_struct.field("algorithm", other).finish(),
350 };
351 debug_struct.field("algorithm", &alg_name);
352 }
353
354 let key_id = &msg.protected.header.key_id;
355 if let Ok(key_id) = KeyId::try_from(key_id.as_slice()) {
356 debug_struct.field("key_id", &key_id);
357 }
358 debug_struct.field("nonce", &hex::encode(msg.unprotected.iv.as_slice()));
359 if let Some(ref content_type) = msg.protected.header.content_type {
360 debug_struct.field("content_type", content_type);
361 }
362
363 #[cfg(feature = "dangerous-crypto-debug")]
364 if let Some(ref ciphertext) = msg.ciphertext {
365 debug_struct.field("ciphertext", &hex::encode(ciphertext));
366 }
367 }
368 Err(_) => {
369 debug_struct.field("error", &"INVALID_COSE");
370 }
371 }
372
373 debug_struct.finish()
374 }
375 }
376 }
377}
378
379impl<'de> Deserialize<'de> for EncString {
380 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
381 where
382 D: serde::Deserializer<'de>,
383 {
384 deserializer.deserialize_str(FromStrVisitor::new())
385 }
386}
387
388impl serde::Serialize for EncString {
389 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
390 where
391 S: serde::Serializer,
392 {
393 serializer.serialize_str(&self.to_string())
394 }
395}
396
397impl EncString {
398 pub(crate) fn encrypt_aes256_hmac(
399 data_dec: &[u8],
400 key: &Aes256CbcHmacKey,
401 ) -> Result<EncString> {
402 let mut iv = [0u8; 16];
403 bitwarden_random::rng().fill(&mut iv);
404 let (mac, data) = Aes256CbcHmacSha256::encrypt(&iv, data_dec, key.as_composite_key());
405 Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
406 }
407
408 pub(crate) fn encrypt_xchacha20_poly1305(
409 data_dec: &[u8],
410 key: &XChaCha20Poly1305Key,
411 content_format: ContentFormat,
412 ) -> Result<EncString> {
413 let data =
414 crate::cose::symmetric::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
415 Ok(EncString::Cose_Encrypt0_B64 {
416 data: data.to_vec(),
417 })
418 }
419
420 pub(crate) fn encrypt_xaes256_gcm(
421 data_dec: &[u8],
422 key: &XAes256GcmKey,
423 content_format: ContentFormat,
424 ) -> Result<EncString> {
425 let data = crate::cose::symmetric::encrypt_xaes256_gcm(data_dec, key, content_format)?;
426 Ok(EncString::Cose_Encrypt0_B64 {
427 data: data.to_vec(),
428 })
429 }
430
431 const fn enc_type(&self) -> Option<u8> {
434 match self {
435 EncString::Aes256Cbc_B64 { .. } => Some(AES256_CBC_TYPE),
436 EncString::Aes256Cbc_HmacSha256_B64 { .. } => Some(AES256_CBC_HMAC_TYPE),
437 EncString::Cose_Encrypt0_B64 { .. } => Some(COSE_ENCRYPT0_TYPE),
438 EncString::Unparseable { .. } => None,
439 }
440 }
441}
442
443impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
444 fn encrypt_with_key(
445 self,
446 key: &SymmetricCryptoKey,
447 content_format: ContentFormat,
448 ) -> Result<EncString> {
449 match key {
450 SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
451 SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
452 if !inner_key
453 .supported_operations
454 .contains(&KeyOperation::Encrypt)
455 {
456 return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
457 }
458 EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
459 }
460 SymmetricCryptoKey::XAes256GcmKey(key) => {
461 if !key.supported_operations.contains(&KeyOperation::Encrypt) {
462 return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
463 }
464 EncString::encrypt_xaes256_gcm(self, key, content_format)
465 }
466 SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
467 UnsupportedOperationError::EncryptionNotImplementedForKey,
468 )),
469 SymmetricCryptoKey::Aes256GcmKey(_) => Err(CryptoError::OperationNotSupported(
470 UnsupportedOperationError::EncryptionNotImplementedForKey,
471 )),
472 }
473 }
474}
475
476impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
477 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
478 match (self, key) {
479 (EncString::Aes256Cbc_B64 { .. }, SymmetricCryptoKey::Aes256CbcKey(_)) => {
480 Err(CryptoError::OperationNotSupported(
481 UnsupportedOperationError::DecryptionNotImplementedForKey,
482 ))
483 }
484 (
485 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
486 SymmetricCryptoKey::Aes256CbcHmacKey(key),
487 ) => Aes256CbcHmacSha256::decrypt(iv, data, mac, key.as_composite_key())
488 .map_err(|_| CryptoError::Decrypt),
489 (
490 EncString::Cose_Encrypt0_B64 { data },
491 SymmetricCryptoKey::XChaCha20Poly1305Key(key),
492 ) => {
493 let (decrypted_message, _) = crate::cose::symmetric::decrypt_xchacha20_poly1305(
494 &CoseEncrypt0Bytes::from(data.as_slice()),
495 key,
496 )?;
497 Ok(decrypted_message)
498 }
499 (EncString::Cose_Encrypt0_B64 { data }, SymmetricCryptoKey::XAes256GcmKey(key)) => {
500 let (decrypted, _) = crate::cose::symmetric::decrypt_xaes256_gcm(
501 &CoseEncrypt0Bytes::from(data.as_slice()),
502 key,
503 )?;
504 Ok(decrypted)
505 }
506 (EncString::Unparseable { .. }, _) => Err(CryptoError::UnparseableEncString),
507 (_, SymmetricCryptoKey::XAes256GcmKey(_)) => Err(CryptoError::WrongKeyType),
508 _ => Err(CryptoError::WrongKeyType),
509 }
510 }
511}
512
513impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
514 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
515 Utf8Bytes::from(self).encrypt_with_key(key)
516 }
517}
518
519impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
520 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
521 Utf8Bytes::from(self).encrypt_with_key(key)
522 }
523}
524
525impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
526 #[bitwarden_logging::instrument(err)]
527 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
528 let dec: Vec<u8> = self.decrypt_with_key(key)?;
529 String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
530 }
531}
532
533impl schemars::JsonSchema for EncString {
536 fn schema_name() -> Cow<'static, str> {
537 "EncString".into()
538 }
539
540 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
541 generator.subschema_for::<String>()
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use coset::iana::KeyOperation;
548 use schemars::schema_for;
549
550 use super::EncString;
551 use crate::{
552 CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
553 derive_symmetric_key,
554 };
555
556 fn xaes_key(operations: Vec<KeyOperation>) -> SymmetricCryptoKey {
557 SymmetricCryptoKey::XAes256GcmKey(crate::XAes256GcmKey {
558 key_id: [0u8; KEY_ID_SIZE].into(),
559 enc_key: Box::pin([0u8; 32].into()),
560 supported_operations: operations,
561 })
562 }
563
564 fn encrypt_with_xaes(plaintext: &str) -> EncString {
565 plaintext
566 .to_owned()
567 .encrypt_with_key(&xaes_key(vec![
568 coset::iana::KeyOperation::Decrypt,
569 coset::iana::KeyOperation::Encrypt,
570 coset::iana::KeyOperation::WrapKey,
571 coset::iana::KeyOperation::UnwrapKey,
572 ]))
573 .expect("encryption works")
574 }
575
576 fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
577 let key_id = [0u8; KEY_ID_SIZE];
578 let enc_key = [0u8; 32];
579 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
580 key_id: key_id.into(),
581 enc_key: Box::pin(enc_key.into()),
582 supported_operations: vec![
583 coset::iana::KeyOperation::Decrypt,
584 coset::iana::KeyOperation::Encrypt,
585 coset::iana::KeyOperation::WrapKey,
586 coset::iana::KeyOperation::UnwrapKey,
587 ],
588 });
589
590 plaintext.encrypt_with_key(&key).expect("encryption works")
591 }
592
593 #[test]
594 #[ignore = "Manual test to verify debug format"]
595 fn test_debug() {
596 let enc_string = encrypt_with_xchacha20("Test debug string");
597 println!("{:?}", enc_string);
598 let enc_string_aes =
599 EncString::encrypt_aes256_hmac(b"Test debug string", &derive_symmetric_key("test"))
600 .unwrap();
601 println!("{:?}", enc_string_aes);
602 }
603
604 #[test]
608 fn test_xchacha20_encstring_string_padding_block_sizes() {
609 let cases = [
610 ("", 32), (&"a".repeat(31), 32), (&"a".repeat(32), 64), (&"a".repeat(63), 64), (&"a".repeat(64), 96), ];
616
617 let ciphertext_lengths: Vec<_> = cases
618 .iter()
619 .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
620 .collect();
621
622 assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
624 assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
626 assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
627 assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
629 }
630
631 #[test]
632 fn test_enc_roundtrip_xchacha20() {
633 let key_id = [0u8; KEY_ID_SIZE];
634 let enc_key = [0u8; 32];
635 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
636 key_id: key_id.into(),
637 enc_key: Box::pin(enc_key.into()),
638 supported_operations: vec![
639 coset::iana::KeyOperation::Decrypt,
640 coset::iana::KeyOperation::Encrypt,
641 coset::iana::KeyOperation::WrapKey,
642 coset::iana::KeyOperation::UnwrapKey,
643 ],
644 });
645
646 let test_string = "encrypted_test_string";
647 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
648 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
649 assert_eq!(decrypted_str, test_string);
650 }
651
652 #[test]
653 fn test_xaes_encstring_string_roundtrips() {
654 let key = xaes_key(vec![
655 coset::iana::KeyOperation::Decrypt,
656 coset::iana::KeyOperation::Encrypt,
657 coset::iana::KeyOperation::WrapKey,
658 coset::iana::KeyOperation::UnwrapKey,
659 ]);
660 for plaintext in ["", "encrypted_test_string"] {
661 let encrypted = plaintext.to_owned().encrypt_with_key(&key).unwrap();
662 let decrypted: String = encrypted.decrypt_with_key(&key).unwrap();
663 assert_eq!(decrypted, plaintext);
664 }
665 }
666
667 #[test]
668 fn test_xaes_encstring_string_padding_block_sizes() {
669 let lengths = [0, 31, 32, 63, 64]
678 .map(|length| encrypt_with_xaes(&"a".repeat(length)).to_string().len());
679
680 assert_eq!(lengths[0], lengths[1]); assert_ne!(lengths[1], lengths[2]); assert_eq!(lengths[2], lengths[3]); assert_ne!(lengths[3], lengths[4]); }
685
686 #[test]
687 fn test_xaes_encryption_requires_encrypt_operation() {
688 assert!(matches!(
689 "plaintext"
690 .to_owned()
691 .encrypt_with_key(&xaes_key(vec![KeyOperation::Decrypt])),
692 Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
693 ));
694 }
695
696 #[test]
697 fn test_xaes_rejects_unsupported_encstring_variant() {
698 let encrypted =
699 EncString::encrypt_aes256_hmac(b"plaintext", &derive_symmetric_key("wrapping key"))
700 .unwrap();
701 let result: Result<Vec<u8>, CryptoError> =
702 encrypted.decrypt_with_key(&xaes_key(vec![KeyOperation::Encrypt]));
703 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
704 }
705
706 #[test]
707 fn test_xaes_encstring_debug_is_readable() {
708 let debug = format!("{:?}", encrypt_with_xaes("plaintext"));
709 assert!(debug.contains("EncString::CoseEncrypt0"));
710 assert!(debug.contains("XAES-256-GCM"));
711 assert!(debug.contains("KeyId(00000000000000000000000000000000)"));
712 assert!(debug.contains("nonce"));
713 assert!(debug.contains("content_type"));
714 }
715
716 #[test]
717 fn test_enc_string_roundtrip() {
718 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
719
720 let test_string = "encrypted_test_string";
721 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
722
723 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
724 assert_eq!(decrypted_str, test_string);
725 }
726
727 #[test]
728 fn test_enc_roundtrip_xchacha20_empty() {
729 let key_id = [0u8; KEY_ID_SIZE];
730 let enc_key = [0u8; 32];
731 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
732 key_id: key_id.into(),
733 enc_key: Box::pin(enc_key.into()),
734 supported_operations: vec![
735 coset::iana::KeyOperation::Decrypt,
736 coset::iana::KeyOperation::Encrypt,
737 coset::iana::KeyOperation::WrapKey,
738 coset::iana::KeyOperation::UnwrapKey,
739 ],
740 });
741
742 let test_string = "";
743 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
744 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
745 assert_eq!(decrypted_str, test_string);
746 }
747
748 #[test]
749 fn test_enc_string_roundtrip_empty() {
750 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
751
752 let test_string = "";
753 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
754
755 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
756 assert_eq!(decrypted_str, test_string);
757 }
758
759 #[test]
760 fn test_enc_string_ref_roundtrip() {
761 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
762
763 let test_string: &'static str = "encrypted_test_string";
764 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
765
766 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
767 assert_eq!(decrypted_str, test_string);
768 }
769
770 #[test]
771 fn test_enc_string_serialization() {
772 #[derive(serde::Serialize, serde::Deserialize)]
773 struct Test {
774 key: EncString,
775 }
776
777 let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
778 let serialized = format!("{{\"key\":\"{cipher}\"}}");
779
780 let t = serde_json::from_str::<Test>(&serialized).unwrap();
781 assert_eq!(t.key.enc_type(), Some(2));
782 assert_eq!(t.key.to_string(), cipher);
783 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
784 }
785
786 #[test]
787 fn test_enc_from_to_buffer() {
788 let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
789 let enc_string: EncString = enc_str.parse().unwrap();
790
791 let enc_buf = enc_string.to_buffer().unwrap();
792
793 assert_eq!(
794 enc_buf,
795 vec![
796 2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
797 163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
798 220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
799 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
800 ]
801 );
802
803 let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
804
805 assert_eq!(enc_string_new.to_string(), enc_str)
806 }
807
808 #[test]
809 fn test_from_str_cbc256() {
810 let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
811 let enc_string: EncString = enc_str.parse().unwrap();
812
813 assert_eq!(enc_string.enc_type(), Some(0));
814 if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
815 assert_eq!(
816 iv,
817 &[
818 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
819 ]
820 );
821 assert_eq!(
822 data,
823 &[
824 93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
825 ]
826 );
827 } else {
828 panic!("Invalid variant")
829 };
830 }
831
832 #[test]
833 fn test_decrypt_fails_for_cbc256_keys() {
834 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
835 let key = SymmetricCryptoKey::try_from(key).unwrap();
836
837 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
838 let enc_string: EncString = enc_str.parse().unwrap();
839 assert_eq!(enc_string.enc_type(), Some(0));
840
841 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
842 assert!(
843 matches!(
844 result,
845 Err(CryptoError::OperationNotSupported(
846 crate::error::UnsupportedOperationError::DecryptionNotImplementedForKey
847 )),
848 ),
849 "Expected decrypt to fail when using deprecated type 0 key",
850 );
851 }
852
853 #[test]
854 fn test_decrypt_downgrade_encstring_prevention() {
855 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
858 let key = SymmetricCryptoKey::try_from(key).unwrap();
859
860 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
864 let enc_string: EncString = enc_str.parse().unwrap();
865 assert_eq!(enc_string.enc_type(), Some(0));
866
867 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
868 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
869 }
870
871 #[test]
872 fn test_encrypt_fails_when_operation_not_allowed() {
873 let key_id = [0u8; KEY_ID_SIZE];
875 let enc_key = [0u8; 32];
876 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
877 key_id: key_id.into(),
878 enc_key: Box::pin(enc_key.into()),
879 supported_operations: vec![KeyOperation::Decrypt],
880 });
881
882 let plaintext = "should fail";
883 let result = plaintext.encrypt_with_key(&key);
884 assert!(
885 matches!(
886 result,
887 Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
888 ),
889 "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
890 );
891 }
892
893 #[test]
896 fn test_from_str_unparseable_roundtrips() {
897 let cases = [
898 "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "2.AAECAwQFBgcICQoLDA0ODw==|Y3Q=|AAECAw==", "0.AAECAw==|Y3Q=", "2.!!!!|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "8.ABC", "7.ABC|DEF", ];
905
906 for enc_str in cases {
907 let enc_string: EncString = enc_str.parse().expect("parsing never fails");
908 assert!(
909 matches!(enc_string, EncString::Unparseable { .. }),
910 "Expected {enc_str} to parse as unparseable",
911 );
912 assert_eq!(enc_string.to_string(), enc_str);
913 assert_eq!(enc_string.enc_type(), None);
914 }
915 }
916
917 #[test]
918 fn test_from_string_and_str() {
919 let known = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
920 let unknown = "8.ABC";
921
922 assert_eq!(EncString::from(known).enc_type(), Some(2));
923 assert_eq!(EncString::from(known.to_owned()).enc_type(), Some(2));
924
925 assert!(matches!(
926 EncString::from(unknown),
927 EncString::Unparseable { .. }
928 ));
929 assert_eq!(EncString::from(unknown.to_owned()).to_string(), unknown);
930 }
931
932 #[test]
933 fn test_parse_strict_rejects_unparseable() {
934 let cases = [
935 "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "2.AAECAwQFBgcICQoLDA0ODw==|Y3Q=|AAECAw==", "8.ABC", ];
939
940 for enc_str in cases {
941 assert!(
942 matches!(
943 EncString::parse_strict(enc_str),
944 Err(CryptoError::UnparseableEncString)
945 ),
946 "Expected {enc_str} to be rejected",
947 );
948 }
949 }
950
951 #[test]
952 fn test_parse_strict_accepts_known_formats() {
953 let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
954 let enc_string = EncString::parse_strict(enc_str).unwrap();
955
956 assert_eq!(enc_string.enc_type(), Some(2));
957 assert_eq!(enc_string.to_string(), enc_str);
958 }
959
960 #[test]
961 fn test_unparseable_serde_roundtrip() {
962 #[derive(serde::Serialize, serde::Deserialize)]
963 struct Test {
964 key: EncString,
965 }
966
967 let cipher = "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
968 let serialized = format!("{{\"key\":\"{cipher}\"}}");
969
970 let t = serde_json::from_str::<Test>(&serialized).unwrap();
971 assert!(matches!(t.key, EncString::Unparseable { .. }));
972 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
973 }
974
975 #[test]
976 fn test_unparseable_decrypt_fails() {
977 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
978 let enc_string: EncString = "2.AAECAw==|Y3Q=|AAECAw==".parse().unwrap();
979
980 let result: Result<Vec<u8>, CryptoError> = enc_string.decrypt_with_key(&key);
981 assert!(matches!(result, Err(CryptoError::UnparseableEncString)));
982 }
983
984 #[test]
985 fn test_unparseable_to_buffer_fails() {
986 let enc_string: EncString = "2.AAECAw==|Y3Q=|AAECAw==".parse().unwrap();
987
988 assert!(matches!(
989 enc_string.to_buffer(),
990 Err(CryptoError::UnparseableEncString)
991 ));
992 }
993
994 #[test]
995 fn test_from_buffer_stays_strict() {
996 assert!(EncString::from_buffer(&[]).is_err());
997 assert!(EncString::from_buffer(&[2, 0, 0]).is_err());
998 assert!(EncString::from_buffer(&[8, 0, 0]).is_err());
999 }
1000
1001 #[test]
1002 #[cfg(not(feature = "dangerous-crypto-debug"))]
1003 fn test_debug_format() {
1004 let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
1005 let enc_string: EncString = enc_str.parse().unwrap();
1006 assert_eq!(
1007 "EncString::Aes256CbcHmacSha256".to_string(),
1008 format!("{:?}", enc_string)
1009 );
1010 }
1011
1012 #[test]
1013 fn test_json_schema() {
1014 let schema = schema_for!(EncString);
1015
1016 assert_eq!(
1017 serde_json::to_string(&schema).unwrap(),
1018 r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
1019 );
1020 }
1021}