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, SymmetricKeyAlgorithm, Utf8Bytes,
14 XAes256GcmKey, XChaCha20Poly1305Key,
15 cose::{XAES_256_GCM, XCHACHA20_POLY1305, symmetric::CoseContentEncryptionAlgorithm},
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 key_id(&self) -> Option<KeyId> {
201 let EncString::Cose_Encrypt0_B64 { data } = self else {
202 return None;
203 };
204
205 let msg = coset::CoseEncrypt0::from_slice(data.as_slice()).ok()?;
206 KeyId::try_from(msg.protected.header.key_id.as_slice()).ok()
207 }
208
209 pub fn algorithm(&self) -> Option<SymmetricKeyAlgorithm> {
212 let data = match self {
213 EncString::Aes256Cbc_HmacSha256_B64 { .. } => {
214 return Some(SymmetricKeyAlgorithm::Aes256CbcHmac);
215 }
216 EncString::Cose_Encrypt0_B64 { data } => data,
217 EncString::Aes256Cbc_B64 { .. } | EncString::Unparseable { .. } => return None,
218 };
219
220 let msg = coset::CoseEncrypt0::from_slice(data.as_slice()).ok()?;
221 let algorithm = msg.protected.header.alg.as_ref()?;
222 let algorithm = match CoseContentEncryptionAlgorithm::try_from(algorithm).ok()? {
223 CoseContentEncryptionAlgorithm::Aes256Gcm => SymmetricKeyAlgorithm::Aes256Gcm,
224 CoseContentEncryptionAlgorithm::XAes256Gcm => SymmetricKeyAlgorithm::XAes256Gcm,
225 CoseContentEncryptionAlgorithm::XChaCha20Poly1305 => {
226 SymmetricKeyAlgorithm::XChaCha20Poly1305
227 }
228 CoseContentEncryptionAlgorithm::Aes256CbcHmacSha256 => {
229 SymmetricKeyAlgorithm::Aes256CbcHmac
230 }
231 };
232 Some(algorithm)
233 }
234
235 pub fn try_from_optional(s: Option<String>) -> Result<Option<EncString>, CryptoError> {
237 s.map(|s| s.parse()).transpose()
238 }
239
240 pub fn from_buffer(buf: &[u8]) -> Result<Self> {
243 if buf.is_empty() {
244 return Err(EncStringParseError::NoType.into());
245 }
246 let enc_type = buf[0];
247
248 match enc_type {
249 AES256_CBC_TYPE => {
250 check_length(buf, 18)?;
251 let iv = buf[1..17].try_into().expect("Valid length");
252 let data = buf[17..].to_vec();
253
254 Ok(EncString::Aes256Cbc_B64 { iv, data })
255 }
256 AES256_CBC_HMAC_TYPE => {
257 check_length(buf, 50)?;
258 let iv = buf[1..17].try_into().expect("Valid length");
259 let mac = buf[17..49].try_into().expect("Valid length");
260 let data = buf[49..].to_vec();
261
262 Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
263 }
264 COSE_ENCRYPT0_TYPE => Ok(EncString::Cose_Encrypt0_B64 {
265 data: buf[1..].to_vec(),
266 }),
267 _ => Err(EncStringParseError::InvalidTypeSymm {
268 enc_type: enc_type.to_string(),
269 parts: 1,
270 }
271 .into()),
272 }
273 }
274
275 #[allow(missing_docs)]
276 pub fn to_buffer(&self) -> Result<Vec<u8>> {
277 let mut buf;
278 let enc_type = self.enc_type().ok_or(CryptoError::UnparseableEncString)?;
279
280 match self {
281 EncString::Aes256Cbc_B64 { iv, data } => {
282 buf = Vec::with_capacity(1 + 16 + data.len());
283 buf.push(enc_type);
284 buf.extend_from_slice(iv);
285 buf.extend_from_slice(data);
286 }
287 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
288 buf = Vec::with_capacity(1 + 16 + 32 + data.len());
289 buf.push(enc_type);
290 buf.extend_from_slice(iv);
291 buf.extend_from_slice(mac);
292 buf.extend_from_slice(data);
293 }
294 EncString::Cose_Encrypt0_B64 { data } => {
295 buf = Vec::with_capacity(1 + data.len());
296 buf.push(enc_type);
297 buf.extend_from_slice(data);
298 }
299 EncString::Unparseable { .. } => return Err(CryptoError::UnparseableEncString),
300 }
301
302 Ok(buf)
303 }
304}
305
306#[allow(clippy::to_string_trait_impl)]
311impl ToString for EncString {
312 fn to_string(&self) -> String {
313 fn fmt_parts(enc_type: u8, parts: &[&[u8]]) -> String {
314 let encoded_parts: Vec<String> = parts
315 .iter()
316 .map(|part| B64::from(*part).to_string())
317 .collect();
318 format!("{}.{}", enc_type, encoded_parts.join("|"))
319 }
320
321 match &self {
322 EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(AES256_CBC_TYPE, &[iv, data]),
323 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
324 fmt_parts(AES256_CBC_HMAC_TYPE, &[iv, data, mac])
325 }
326 EncString::Cose_Encrypt0_B64 { data } => fmt_parts(COSE_ENCRYPT0_TYPE, &[data]),
327 EncString::Unparseable { raw } => raw.clone(),
328 }
329 }
330}
331
332impl std::fmt::Debug for EncString {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 match self {
335 EncString::Aes256Cbc_B64 { iv, data } => {
336 let mut debug_struct = f.debug_struct("EncString::Aes256Cbc");
337 #[cfg(feature = "dangerous-crypto-debug")]
338 {
339 debug_struct.field("iv", &hex::encode(iv));
340 debug_struct.field("data", &hex::encode(data));
341 }
342 #[cfg(not(feature = "dangerous-crypto-debug"))]
343 {
344 _ = iv;
345 _ = data;
346 }
347 debug_struct.finish()
348 }
349 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
350 let mut debug_struct = f.debug_struct("EncString::Aes256CbcHmacSha256");
351 #[cfg(feature = "dangerous-crypto-debug")]
352 {
353 debug_struct.field("iv", &hex::encode(iv));
354 debug_struct.field("data", &hex::encode(data));
355 debug_struct.field("mac", &hex::encode(mac));
356 }
357 #[cfg(not(feature = "dangerous-crypto-debug"))]
358 {
359 _ = iv;
360 _ = data;
361 _ = mac;
362 }
363 debug_struct.finish()
364 }
365 EncString::Unparseable { raw } => {
366 let mut debug_struct = f.debug_struct("EncString::Unparseable");
367 #[cfg(feature = "dangerous-crypto-debug")]
368 debug_struct.field("raw", raw);
369 #[cfg(not(feature = "dangerous-crypto-debug"))]
370 {
371 _ = raw;
372 }
373 debug_struct.finish()
374 }
375 EncString::Cose_Encrypt0_B64 { data } => {
376 let mut debug_struct = f.debug_struct("EncString::CoseEncrypt0");
377
378 match coset::CoseEncrypt0::from_slice(data.as_slice()) {
379 Ok(msg) => {
380 if let Some(ref alg) = msg.protected.header.alg {
381 let alg_name = match alg {
382 coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
383 "XChaCha20-Poly1305"
384 }
385 coset::Algorithm::PrivateUse(XAES_256_GCM) => "XAES-256-GCM",
386 other => return debug_struct.field("algorithm", other).finish(),
387 };
388 debug_struct.field("algorithm", &alg_name);
389 }
390
391 let key_id = &msg.protected.header.key_id;
392 if let Ok(key_id) = KeyId::try_from(key_id.as_slice()) {
393 debug_struct.field("key_id", &key_id);
394 }
395 debug_struct.field("nonce", &hex::encode(msg.unprotected.iv.as_slice()));
396 if let Some(ref content_type) = msg.protected.header.content_type {
397 debug_struct.field("content_type", content_type);
398 }
399
400 #[cfg(feature = "dangerous-crypto-debug")]
401 if let Some(ref ciphertext) = msg.ciphertext {
402 debug_struct.field("ciphertext", &hex::encode(ciphertext));
403 }
404 }
405 Err(_) => {
406 debug_struct.field("error", &"INVALID_COSE");
407 }
408 }
409
410 debug_struct.finish()
411 }
412 }
413 }
414}
415
416impl<'de> Deserialize<'de> for EncString {
417 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
418 where
419 D: serde::Deserializer<'de>,
420 {
421 deserializer.deserialize_str(FromStrVisitor::new())
422 }
423}
424
425impl serde::Serialize for EncString {
426 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
427 where
428 S: serde::Serializer,
429 {
430 serializer.serialize_str(&self.to_string())
431 }
432}
433
434impl EncString {
435 pub(crate) fn encrypt_aes256_hmac(
436 data_dec: &[u8],
437 key: &Aes256CbcHmacKey,
438 ) -> Result<EncString> {
439 let mut iv = [0u8; 16];
440 bitwarden_random::rng().fill(&mut iv);
441 let (mac, data) = Aes256CbcHmacSha256::encrypt(&iv, data_dec, key.as_composite_key());
442 Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
443 }
444
445 pub(crate) fn encrypt_xchacha20_poly1305(
446 data_dec: &[u8],
447 key: &XChaCha20Poly1305Key,
448 content_format: ContentFormat,
449 ) -> Result<EncString> {
450 let data =
451 crate::cose::symmetric::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
452 Ok(EncString::Cose_Encrypt0_B64 {
453 data: data.to_vec(),
454 })
455 }
456
457 pub(crate) fn encrypt_xaes256_gcm(
458 data_dec: &[u8],
459 key: &XAes256GcmKey,
460 content_format: ContentFormat,
461 ) -> Result<EncString> {
462 let data = crate::cose::symmetric::encrypt_xaes256_gcm(data_dec, key, content_format)?;
463 Ok(EncString::Cose_Encrypt0_B64 {
464 data: data.to_vec(),
465 })
466 }
467
468 const fn enc_type(&self) -> Option<u8> {
471 match self {
472 EncString::Aes256Cbc_B64 { .. } => Some(AES256_CBC_TYPE),
473 EncString::Aes256Cbc_HmacSha256_B64 { .. } => Some(AES256_CBC_HMAC_TYPE),
474 EncString::Cose_Encrypt0_B64 { .. } => Some(COSE_ENCRYPT0_TYPE),
475 EncString::Unparseable { .. } => None,
476 }
477 }
478}
479
480impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
481 fn encrypt_with_key(
482 self,
483 key: &SymmetricCryptoKey,
484 content_format: ContentFormat,
485 ) -> Result<EncString> {
486 match key {
487 SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
488 SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
489 if !inner_key
490 .supported_operations
491 .contains(&KeyOperation::Encrypt)
492 {
493 return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
494 }
495 EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
496 }
497 SymmetricCryptoKey::XAes256GcmKey(key) => {
498 if !key.supported_operations.contains(&KeyOperation::Encrypt) {
499 return Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt));
500 }
501 EncString::encrypt_xaes256_gcm(self, key, content_format)
502 }
503 SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
504 UnsupportedOperationError::EncryptionNotImplementedForKey,
505 )),
506 SymmetricCryptoKey::Aes256GcmKey(_) => Err(CryptoError::OperationNotSupported(
507 UnsupportedOperationError::EncryptionNotImplementedForKey,
508 )),
509 }
510 }
511}
512
513impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
514 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
515 match (self, key) {
516 (EncString::Aes256Cbc_B64 { .. }, SymmetricCryptoKey::Aes256CbcKey(_)) => {
517 Err(CryptoError::OperationNotSupported(
518 UnsupportedOperationError::DecryptionNotImplementedForKey,
519 ))
520 }
521 (
522 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
523 SymmetricCryptoKey::Aes256CbcHmacKey(key),
524 ) => Aes256CbcHmacSha256::decrypt(iv, data, mac, key.as_composite_key())
525 .map_err(|_| CryptoError::Decrypt),
526 (
527 EncString::Cose_Encrypt0_B64 { data },
528 SymmetricCryptoKey::XChaCha20Poly1305Key(key),
529 ) => {
530 let (decrypted_message, _) = crate::cose::symmetric::decrypt_xchacha20_poly1305(
531 &CoseEncrypt0Bytes::from(data.as_slice()),
532 key,
533 )?;
534 Ok(decrypted_message)
535 }
536 (EncString::Cose_Encrypt0_B64 { data }, SymmetricCryptoKey::XAes256GcmKey(key)) => {
537 let (decrypted, _) = crate::cose::symmetric::decrypt_xaes256_gcm(
538 &CoseEncrypt0Bytes::from(data.as_slice()),
539 key,
540 )?;
541 Ok(decrypted)
542 }
543 (EncString::Unparseable { .. }, _) => Err(CryptoError::UnparseableEncString),
544 (_, SymmetricCryptoKey::XAes256GcmKey(_)) => Err(CryptoError::WrongKeyType),
545 _ => Err(CryptoError::WrongKeyType),
546 }
547 }
548}
549
550impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
551 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
552 Utf8Bytes::from(self).encrypt_with_key(key)
553 }
554}
555
556impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
557 fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
558 Utf8Bytes::from(self).encrypt_with_key(key)
559 }
560}
561
562impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
563 #[bitwarden_logging::instrument(err)]
564 fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
565 let dec: Vec<u8> = self.decrypt_with_key(key)?;
566 String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
567 }
568}
569
570impl schemars::JsonSchema for EncString {
573 fn schema_name() -> Cow<'static, str> {
574 "EncString".into()
575 }
576
577 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
578 generator.subschema_for::<String>()
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use coset::iana::KeyOperation;
585 use schemars::schema_for;
586
587 use super::EncString;
588 use crate::{
589 CryptoError, KEY_ID_SIZE, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
590 SymmetricKeyAlgorithm, derive_symmetric_key,
591 };
592
593 fn xaes_key(operations: Vec<KeyOperation>) -> SymmetricCryptoKey {
594 SymmetricCryptoKey::XAes256GcmKey(crate::XAes256GcmKey {
595 key_id: [0u8; KEY_ID_SIZE].into(),
596 enc_key: Box::pin([0u8; 32].into()),
597 supported_operations: operations,
598 })
599 }
600
601 fn encrypt_with_xaes(plaintext: &str) -> EncString {
602 plaintext
603 .to_owned()
604 .encrypt_with_key(&xaes_key(vec![
605 coset::iana::KeyOperation::Decrypt,
606 coset::iana::KeyOperation::Encrypt,
607 coset::iana::KeyOperation::WrapKey,
608 coset::iana::KeyOperation::UnwrapKey,
609 ]))
610 .expect("encryption works")
611 }
612
613 fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
614 let key_id = [0u8; KEY_ID_SIZE];
615 let enc_key = [0u8; 32];
616 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
617 key_id: key_id.into(),
618 enc_key: Box::pin(enc_key.into()),
619 supported_operations: vec![
620 coset::iana::KeyOperation::Decrypt,
621 coset::iana::KeyOperation::Encrypt,
622 coset::iana::KeyOperation::WrapKey,
623 coset::iana::KeyOperation::UnwrapKey,
624 ],
625 });
626
627 plaintext.encrypt_with_key(&key).expect("encryption works")
628 }
629
630 #[test]
631 fn test_key_id_of_cose_enc_string() {
632 let enc_string = encrypt_with_xaes("Test key id");
633 assert_eq!(enc_string.key_id(), Some([0u8; KEY_ID_SIZE].into()));
634 }
635
636 #[test]
637 fn test_algorithm_of_enc_string() {
638 let v1 = EncString::encrypt_aes256_hmac(b"Test", &derive_symmetric_key("test")).unwrap();
639 assert_eq!(v1.algorithm(), Some(SymmetricKeyAlgorithm::Aes256CbcHmac));
640 assert_eq!(
641 encrypt_with_xaes("Test").algorithm(),
642 Some(SymmetricKeyAlgorithm::XAes256Gcm)
643 );
644 assert_eq!(
645 encrypt_with_xchacha20("Test").algorithm(),
646 Some(SymmetricKeyAlgorithm::XChaCha20Poly1305)
647 );
648 }
649
650 #[test]
651 fn test_key_id_of_legacy_enc_string_is_none() {
652 let enc_string =
653 EncString::encrypt_aes256_hmac(b"Test key id", &derive_symmetric_key("test")).unwrap();
654 assert_eq!(enc_string.key_id(), None);
655 }
656
657 #[test]
658 #[ignore = "Manual test to verify debug format"]
659 fn test_debug() {
660 let enc_string = encrypt_with_xchacha20("Test debug string");
661 println!("{:?}", enc_string);
662 let enc_string_aes =
663 EncString::encrypt_aes256_hmac(b"Test debug string", &derive_symmetric_key("test"))
664 .unwrap();
665 println!("{:?}", enc_string_aes);
666 }
667
668 #[test]
672 fn test_xchacha20_encstring_string_padding_block_sizes() {
673 let cases = [
674 ("", 32), (&"a".repeat(31), 32), (&"a".repeat(32), 64), (&"a".repeat(63), 64), (&"a".repeat(64), 96), ];
680
681 let ciphertext_lengths: Vec<_> = cases
682 .iter()
683 .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
684 .collect();
685
686 assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
688 assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
690 assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
691 assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
693 }
694
695 #[test]
696 fn test_enc_roundtrip_xchacha20() {
697 let key_id = [0u8; KEY_ID_SIZE];
698 let enc_key = [0u8; 32];
699 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
700 key_id: key_id.into(),
701 enc_key: Box::pin(enc_key.into()),
702 supported_operations: vec![
703 coset::iana::KeyOperation::Decrypt,
704 coset::iana::KeyOperation::Encrypt,
705 coset::iana::KeyOperation::WrapKey,
706 coset::iana::KeyOperation::UnwrapKey,
707 ],
708 });
709
710 let test_string = "encrypted_test_string";
711 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
712 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
713 assert_eq!(decrypted_str, test_string);
714 }
715
716 #[test]
717 fn test_xaes_encstring_string_roundtrips() {
718 let key = xaes_key(vec![
719 coset::iana::KeyOperation::Decrypt,
720 coset::iana::KeyOperation::Encrypt,
721 coset::iana::KeyOperation::WrapKey,
722 coset::iana::KeyOperation::UnwrapKey,
723 ]);
724 for plaintext in ["", "encrypted_test_string"] {
725 let encrypted = plaintext.to_owned().encrypt_with_key(&key).unwrap();
726 let decrypted: String = encrypted.decrypt_with_key(&key).unwrap();
727 assert_eq!(decrypted, plaintext);
728 }
729 }
730
731 #[test]
732 fn test_xaes_encstring_string_padding_block_sizes() {
733 let lengths = [0, 31, 32, 63, 64]
742 .map(|length| encrypt_with_xaes(&"a".repeat(length)).to_string().len());
743
744 assert_eq!(lengths[0], lengths[1]); assert_ne!(lengths[1], lengths[2]); assert_eq!(lengths[2], lengths[3]); assert_ne!(lengths[3], lengths[4]); }
749
750 #[test]
751 fn test_xaes_encryption_requires_encrypt_operation() {
752 assert!(matches!(
753 "plaintext"
754 .to_owned()
755 .encrypt_with_key(&xaes_key(vec![KeyOperation::Decrypt])),
756 Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
757 ));
758 }
759
760 #[test]
761 fn test_xaes_rejects_unsupported_encstring_variant() {
762 let encrypted =
763 EncString::encrypt_aes256_hmac(b"plaintext", &derive_symmetric_key("wrapping key"))
764 .unwrap();
765 let result: Result<Vec<u8>, CryptoError> =
766 encrypted.decrypt_with_key(&xaes_key(vec![KeyOperation::Encrypt]));
767 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
768 }
769
770 #[test]
771 fn test_xaes_encstring_debug_is_readable() {
772 let debug = format!("{:?}", encrypt_with_xaes("plaintext"));
773 assert!(debug.contains("EncString::CoseEncrypt0"));
774 assert!(debug.contains("XAES-256-GCM"));
775 assert!(debug.contains("KeyId(00000000000000000000000000000000)"));
776 assert!(debug.contains("nonce"));
777 assert!(debug.contains("content_type"));
778 }
779
780 #[test]
781 fn test_enc_string_roundtrip() {
782 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
783
784 let test_string = "encrypted_test_string";
785 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
786
787 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
788 assert_eq!(decrypted_str, test_string);
789 }
790
791 #[test]
792 fn test_enc_roundtrip_xchacha20_empty() {
793 let key_id = [0u8; KEY_ID_SIZE];
794 let enc_key = [0u8; 32];
795 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
796 key_id: key_id.into(),
797 enc_key: Box::pin(enc_key.into()),
798 supported_operations: vec![
799 coset::iana::KeyOperation::Decrypt,
800 coset::iana::KeyOperation::Encrypt,
801 coset::iana::KeyOperation::WrapKey,
802 coset::iana::KeyOperation::UnwrapKey,
803 ],
804 });
805
806 let test_string = "";
807 let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
808 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
809 assert_eq!(decrypted_str, test_string);
810 }
811
812 #[test]
813 fn test_enc_string_roundtrip_empty() {
814 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
815
816 let test_string = "";
817 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
818
819 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
820 assert_eq!(decrypted_str, test_string);
821 }
822
823 #[test]
824 fn test_enc_string_ref_roundtrip() {
825 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
826
827 let test_string: &'static str = "encrypted_test_string";
828 let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
829
830 let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
831 assert_eq!(decrypted_str, test_string);
832 }
833
834 #[test]
835 fn test_enc_string_serialization() {
836 #[derive(serde::Serialize, serde::Deserialize)]
837 struct Test {
838 key: EncString,
839 }
840
841 let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
842 let serialized = format!("{{\"key\":\"{cipher}\"}}");
843
844 let t = serde_json::from_str::<Test>(&serialized).unwrap();
845 assert_eq!(t.key.enc_type(), Some(2));
846 assert_eq!(t.key.to_string(), cipher);
847 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
848 }
849
850 #[test]
851 fn test_enc_from_to_buffer() {
852 let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
853 let enc_string: EncString = enc_str.parse().unwrap();
854
855 let enc_buf = enc_string.to_buffer().unwrap();
856
857 assert_eq!(
858 enc_buf,
859 vec![
860 2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
861 163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
862 220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
863 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
864 ]
865 );
866
867 let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
868
869 assert_eq!(enc_string_new.to_string(), enc_str)
870 }
871
872 #[test]
873 fn test_from_str_cbc256() {
874 let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
875 let enc_string: EncString = enc_str.parse().unwrap();
876
877 assert_eq!(enc_string.enc_type(), Some(0));
878 if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
879 assert_eq!(
880 iv,
881 &[
882 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150
883 ]
884 );
885 assert_eq!(
886 data,
887 &[
888 93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215
889 ]
890 );
891 } else {
892 panic!("Invalid variant")
893 };
894 }
895
896 #[test]
897 fn test_decrypt_fails_for_cbc256_keys() {
898 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
899 let key = SymmetricCryptoKey::try_from(key).unwrap();
900
901 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
902 let enc_string: EncString = enc_str.parse().unwrap();
903 assert_eq!(enc_string.enc_type(), Some(0));
904
905 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
906 assert!(
907 matches!(
908 result,
909 Err(CryptoError::OperationNotSupported(
910 crate::error::UnsupportedOperationError::DecryptionNotImplementedForKey
911 )),
912 ),
913 "Expected decrypt to fail when using deprecated type 0 key",
914 );
915 }
916
917 #[test]
918 fn test_decrypt_downgrade_encstring_prevention() {
919 let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
922 let key = SymmetricCryptoKey::try_from(key).unwrap();
923
924 let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
928 let enc_string: EncString = enc_str.parse().unwrap();
929 assert_eq!(enc_string.enc_type(), Some(0));
930
931 let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
932 assert!(matches!(result, Err(CryptoError::WrongKeyType)));
933 }
934
935 #[test]
936 fn test_encrypt_fails_when_operation_not_allowed() {
937 let key_id = [0u8; KEY_ID_SIZE];
939 let enc_key = [0u8; 32];
940 let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
941 key_id: key_id.into(),
942 enc_key: Box::pin(enc_key.into()),
943 supported_operations: vec![KeyOperation::Decrypt],
944 });
945
946 let plaintext = "should fail";
947 let result = plaintext.encrypt_with_key(&key);
948 assert!(
949 matches!(
950 result,
951 Err(CryptoError::KeyOperationNotSupported(KeyOperation::Encrypt))
952 ),
953 "Expected encrypt to fail with KeyOperationNotSupported, got: {result:?}"
954 );
955 }
956
957 #[test]
960 fn test_from_str_unparseable_roundtrips() {
961 let cases = [
962 "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "2.AAECAwQFBgcICQoLDA0ODw==|Y3Q=|AAECAw==", "0.AAECAw==|Y3Q=", "2.!!!!|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "8.ABC", "7.ABC|DEF", ];
969
970 for enc_str in cases {
971 let enc_string: EncString = enc_str.parse().expect("parsing never fails");
972 assert!(
973 matches!(enc_string, EncString::Unparseable { .. }),
974 "Expected {enc_str} to parse as unparseable",
975 );
976 assert_eq!(enc_string.to_string(), enc_str);
977 assert_eq!(enc_string.enc_type(), None);
978 }
979 }
980
981 #[test]
982 fn test_from_string_and_str() {
983 let known = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
984 let unknown = "8.ABC";
985
986 assert_eq!(EncString::from(known).enc_type(), Some(2));
987 assert_eq!(EncString::from(known.to_owned()).enc_type(), Some(2));
988
989 assert!(matches!(
990 EncString::from(unknown),
991 EncString::Unparseable { .. }
992 ));
993 assert_eq!(EncString::from(unknown.to_owned()).to_string(), unknown);
994 }
995
996 #[test]
997 fn test_parse_strict_rejects_unparseable() {
998 let cases = [
999 "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=", "2.AAECAwQFBgcICQoLDA0ODw==|Y3Q=|AAECAw==", "8.ABC", ];
1003
1004 for enc_str in cases {
1005 assert!(
1006 matches!(
1007 EncString::parse_strict(enc_str),
1008 Err(CryptoError::UnparseableEncString)
1009 ),
1010 "Expected {enc_str} to be rejected",
1011 );
1012 }
1013 }
1014
1015 #[test]
1016 fn test_parse_strict_accepts_known_formats() {
1017 let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
1018 let enc_string = EncString::parse_strict(enc_str).unwrap();
1019
1020 assert_eq!(enc_string.enc_type(), Some(2));
1021 assert_eq!(enc_string.to_string(), enc_str);
1022 }
1023
1024 #[test]
1025 fn test_unparseable_serde_roundtrip() {
1026 #[derive(serde::Serialize, serde::Deserialize)]
1027 struct Test {
1028 key: EncString,
1029 }
1030
1031 let cipher = "2.AAECAw==|Y3Q=|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
1032 let serialized = format!("{{\"key\":\"{cipher}\"}}");
1033
1034 let t = serde_json::from_str::<Test>(&serialized).unwrap();
1035 assert!(matches!(t.key, EncString::Unparseable { .. }));
1036 assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
1037 }
1038
1039 #[test]
1040 fn test_unparseable_decrypt_fails() {
1041 let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
1042 let enc_string: EncString = "2.AAECAw==|Y3Q=|AAECAw==".parse().unwrap();
1043
1044 let result: Result<Vec<u8>, CryptoError> = enc_string.decrypt_with_key(&key);
1045 assert!(matches!(result, Err(CryptoError::UnparseableEncString)));
1046 }
1047
1048 #[test]
1049 fn test_unparseable_to_buffer_fails() {
1050 let enc_string: EncString = "2.AAECAw==|Y3Q=|AAECAw==".parse().unwrap();
1051
1052 assert!(matches!(
1053 enc_string.to_buffer(),
1054 Err(CryptoError::UnparseableEncString)
1055 ));
1056 }
1057
1058 #[test]
1059 fn test_from_buffer_stays_strict() {
1060 assert!(EncString::from_buffer(&[]).is_err());
1061 assert!(EncString::from_buffer(&[2, 0, 0]).is_err());
1062 assert!(EncString::from_buffer(&[8, 0, 0]).is_err());
1063 }
1064
1065 #[test]
1066 #[cfg(not(feature = "dangerous-crypto-debug"))]
1067 fn test_debug_format() {
1068 let enc_str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
1069 let enc_string: EncString = enc_str.parse().unwrap();
1070 assert_eq!(
1071 "EncString::Aes256CbcHmacSha256".to_string(),
1072 format!("{:?}", enc_string)
1073 );
1074 }
1075
1076 #[test]
1077 fn test_json_schema() {
1078 let schema = schema_for!(EncString);
1079
1080 assert_eq!(
1081 serde_json::to_string(&schema).unwrap(),
1082 r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
1083 );
1084 }
1085}