bitwarden_crypto/enc_string/
symmetric.rs

1use std::{borrow::Cow, str::FromStr};
2
3use bitwarden_encoding::{FromStrVisitor, B64};
4use coset::CborSerializable;
5use serde::Deserialize;
6
7use super::{check_length, from_b64, from_b64_vec, split_enc_string};
8use crate::{
9    error::{CryptoError, EncStringParseError, Result, UnsupportedOperation},
10    Aes256CbcHmacKey, ContentFormat, KeyDecryptable, KeyEncryptable, KeyEncryptableWithContentType,
11    SymmetricCryptoKey, Utf8Bytes, XChaCha20Poly1305Key,
12};
13
14#[cfg(feature = "wasm")]
15#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
16const TS_CUSTOM_TYPES: &'static str = r#"
17export type EncString = Tagged<string, "EncString">;
18"#;
19
20/// # Encrypted string primitive
21///
22/// [EncString] is a Bitwarden specific primitive that represents a symmetrically encrypted piece of
23/// data, encoded as a string. They are are used together with the [KeyDecryptable] and
24/// [KeyEncryptable] traits to encrypt and decrypt data using [SymmetricCryptoKey]s.
25///
26/// The flexibility of the [EncString] type allows for different encryption algorithms to be used
27/// which is represented by the different variants of the enum.
28///
29/// ## Note
30///
31/// For backwards compatibility we will rarely if ever be able to remove support for decrypting old
32/// variants, but we should be opinionated in which variants are used for encrypting.
33///
34/// ## Variants
35/// - [Aes256Cbc_B64](EncString::Aes256Cbc_B64) - Deprecated and MUST NOT be used for encrypting as
36///   it is not authenticated
37/// - [Aes256Cbc_HmacSha256_B64](EncString::Aes256Cbc_HmacSha256_B64)
38/// - [Cose_Encrypt0_B64](EncString::Cose_Encrypt0_B64) - The preferred variant for encrypting data.
39///
40/// ## Serialization
41///
42/// [EncString] implements [ToString] and [FromStr] to allow for easy serialization and uses a
43/// custom scheme to represent the different variants.
44///
45/// The scheme is one of the following schemes:
46/// - `[type].[iv]|[data]`
47/// - `[type].[iv]|[data]|[mac]`
48/// - `[type].[cose_encrypt0_bytes]`
49///
50/// Where:
51/// - `[type]`: is a digit number representing the variant.
52/// - `[iv]`: (optional) is the initialization vector used for encryption.
53/// - `[data]`: is the encrypted data.
54/// - `[mac]`: (optional) is the MAC used to validate the integrity of the data.
55/// - `[cose_encrypt0_bytes]`: is the COSE Encrypt0 message, serialized to bytes
56#[allow(missing_docs)]
57#[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)]
58#[allow(unused, non_camel_case_types)]
59pub enum EncString {
60    /// 0
61    Aes256Cbc_B64 {
62        iv: [u8; 16],
63        data: Vec<u8>,
64    },
65    /// 1 was the now removed `AesCbc128_HmacSha256_B64`.
66    /// 2
67    Aes256Cbc_HmacSha256_B64 {
68        iv: [u8; 16],
69        mac: [u8; 32],
70        data: Vec<u8>,
71    },
72    // 7 The actual enc type is contained in the cose struct
73    Cose_Encrypt0_B64 {
74        data: Vec<u8>,
75    },
76}
77
78/// Deserializes an [EncString] from a string.
79impl FromStr for EncString {
80    type Err = CryptoError;
81
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        let (enc_type, parts) = split_enc_string(s);
84        match (enc_type, parts.len()) {
85            ("0", 2) => {
86                let iv = from_b64(parts[0])?;
87                let data = from_b64_vec(parts[1])?;
88
89                Ok(EncString::Aes256Cbc_B64 { iv, data })
90            }
91            ("2", 3) => {
92                let iv = from_b64(parts[0])?;
93                let data = from_b64_vec(parts[1])?;
94                let mac = from_b64(parts[2])?;
95
96                Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
97            }
98            ("7", 1) => {
99                let buffer = from_b64_vec(parts[0])?;
100
101                Ok(EncString::Cose_Encrypt0_B64 { data: buffer })
102            }
103            (enc_type, parts) => Err(EncStringParseError::InvalidTypeSymm {
104                enc_type: enc_type.to_string(),
105                parts,
106            }
107            .into()),
108        }
109    }
110}
111
112impl EncString {
113    /// Synthetic sugar for mapping `Option<String>` to `Result<Option<EncString>>`
114    pub fn try_from_optional(s: Option<String>) -> Result<Option<EncString>, CryptoError> {
115        s.map(|s| s.parse()).transpose()
116    }
117
118    #[allow(missing_docs)]
119    pub fn from_buffer(buf: &[u8]) -> Result<Self> {
120        if buf.is_empty() {
121            return Err(EncStringParseError::NoType.into());
122        }
123        let enc_type = buf[0];
124
125        match enc_type {
126            0 => {
127                check_length(buf, 18)?;
128                let iv = buf[1..17].try_into().expect("Valid length");
129                let data = buf[17..].to_vec();
130
131                Ok(EncString::Aes256Cbc_B64 { iv, data })
132            }
133            2 => {
134                check_length(buf, 50)?;
135                let iv = buf[1..17].try_into().expect("Valid length");
136                let mac = buf[17..49].try_into().expect("Valid length");
137                let data = buf[49..].to_vec();
138
139                Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
140            }
141            7 => Ok(EncString::Cose_Encrypt0_B64 {
142                data: buf[1..].to_vec(),
143            }),
144            _ => Err(EncStringParseError::InvalidTypeSymm {
145                enc_type: enc_type.to_string(),
146                parts: 1,
147            }
148            .into()),
149        }
150    }
151
152    #[allow(missing_docs)]
153    pub fn to_buffer(&self) -> Result<Vec<u8>> {
154        let mut buf;
155
156        match self {
157            EncString::Aes256Cbc_B64 { iv, data } => {
158                buf = Vec::with_capacity(1 + 16 + data.len());
159                buf.push(self.enc_type());
160                buf.extend_from_slice(iv);
161                buf.extend_from_slice(data);
162            }
163            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
164                buf = Vec::with_capacity(1 + 16 + 32 + data.len());
165                buf.push(self.enc_type());
166                buf.extend_from_slice(iv);
167                buf.extend_from_slice(mac);
168                buf.extend_from_slice(data);
169            }
170            EncString::Cose_Encrypt0_B64 { data } => {
171                buf = Vec::with_capacity(1 + data.len());
172                buf.push(self.enc_type());
173                buf.extend_from_slice(data);
174            }
175        }
176
177        Ok(buf)
178    }
179}
180
181// `Display` is not implemented here because printing for debug purposes should be different
182// from serializing to a string. For Aes256_Cbc, or Aes256_Cbc_Hmac, `ToString` and `Debug`
183// are the same. For `Cose_Encrypt0`, `Debug` will print the decoded COSE message, while
184// `ToString` will print the Cose_Encrypt0 bytes, encoded in base64.
185#[allow(clippy::to_string_trait_impl)]
186impl ToString for EncString {
187    fn to_string(&self) -> String {
188        fn fmt_parts(enc_type: u8, parts: &[&[u8]]) -> String {
189            let encoded_parts: Vec<String> = parts
190                .iter()
191                .map(|part| B64::from(*part).to_string())
192                .collect();
193            format!("{}.{}", enc_type, encoded_parts.join("|"))
194        }
195
196        let enc_type = self.enc_type();
197        match &self {
198            EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(enc_type, &[iv, data]),
199            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
200                fmt_parts(enc_type, &[iv, data, mac])
201            }
202            EncString::Cose_Encrypt0_B64 { data } => fmt_parts(enc_type, &[data]),
203        }
204    }
205}
206
207impl std::fmt::Debug for EncString {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        fn fmt_parts(
210            f: &mut std::fmt::Formatter<'_>,
211            enc_type: u8,
212            parts: &[&[u8]],
213        ) -> std::fmt::Result {
214            let encoded_parts: Vec<String> = parts
215                .iter()
216                .map(|part| B64::from(*part).to_string())
217                .collect();
218            write!(f, "{}.{}", enc_type, encoded_parts.join("|"))
219        }
220
221        let enc_type = self.enc_type();
222
223        match self {
224            EncString::Aes256Cbc_B64 { iv, data } => fmt_parts(f, enc_type, &[iv, data]),
225            EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
226                fmt_parts(f, enc_type, &[iv, data, mac])
227            }
228            EncString::Cose_Encrypt0_B64 { data } => {
229                let msg = coset::CoseEncrypt0::from_slice(data.as_slice())
230                    .map(|msg| format!("{msg:?}"))
231                    .unwrap_or_else(|_| "INVALID_COSE".to_string());
232                write!(f, "{enc_type}.{msg}")
233            }
234        }
235    }
236}
237
238impl<'de> Deserialize<'de> for EncString {
239    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
240    where
241        D: serde::Deserializer<'de>,
242    {
243        deserializer.deserialize_str(FromStrVisitor::new())
244    }
245}
246
247impl serde::Serialize for EncString {
248    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
249    where
250        S: serde::Serializer,
251    {
252        serializer.serialize_str(&self.to_string())
253    }
254}
255
256impl EncString {
257    pub(crate) fn encrypt_aes256_hmac(
258        data_dec: &[u8],
259        key: &Aes256CbcHmacKey,
260    ) -> Result<EncString> {
261        let (iv, mac, data) =
262            crate::aes::encrypt_aes256_hmac(data_dec, &key.mac_key, &key.enc_key)?;
263        Ok(EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data })
264    }
265
266    pub(crate) fn encrypt_xchacha20_poly1305(
267        data_dec: &[u8],
268        key: &XChaCha20Poly1305Key,
269        content_format: ContentFormat,
270    ) -> Result<EncString> {
271        let data = crate::cose::encrypt_xchacha20_poly1305(data_dec, key, content_format)?;
272        Ok(EncString::Cose_Encrypt0_B64 { data })
273    }
274
275    /// The numerical representation of the encryption type of the [EncString].
276    const fn enc_type(&self) -> u8 {
277        match self {
278            EncString::Aes256Cbc_B64 { .. } => 0,
279            EncString::Aes256Cbc_HmacSha256_B64 { .. } => 2,
280            EncString::Cose_Encrypt0_B64 { .. } => 7,
281        }
282    }
283}
284
285impl KeyEncryptableWithContentType<SymmetricCryptoKey, EncString> for &[u8] {
286    fn encrypt_with_key(
287        self,
288        key: &SymmetricCryptoKey,
289        content_format: ContentFormat,
290    ) -> Result<EncString> {
291        match key {
292            SymmetricCryptoKey::Aes256CbcHmacKey(key) => EncString::encrypt_aes256_hmac(self, key),
293            SymmetricCryptoKey::XChaCha20Poly1305Key(inner_key) => {
294                EncString::encrypt_xchacha20_poly1305(self, inner_key, content_format)
295            }
296            SymmetricCryptoKey::Aes256CbcKey(_) => Err(CryptoError::OperationNotSupported(
297                UnsupportedOperation::EncryptionNotImplementedForKey,
298            )),
299        }
300    }
301}
302
303impl KeyDecryptable<SymmetricCryptoKey, Vec<u8>> for EncString {
304    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
305        match (self, key) {
306            (EncString::Aes256Cbc_B64 { iv, data }, SymmetricCryptoKey::Aes256CbcKey(key)) => {
307                crate::aes::decrypt_aes256(iv, data.clone(), &key.enc_key)
308            }
309            (
310                EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data },
311                SymmetricCryptoKey::Aes256CbcHmacKey(key),
312            ) => crate::aes::decrypt_aes256_hmac(iv, mac, data.clone(), &key.mac_key, &key.enc_key),
313            (
314                EncString::Cose_Encrypt0_B64 { data },
315                SymmetricCryptoKey::XChaCha20Poly1305Key(key),
316            ) => {
317                let (decrypted_message, _) =
318                    crate::cose::decrypt_xchacha20_poly1305(data.as_slice(), key)?;
319                Ok(decrypted_message)
320            }
321            _ => Err(CryptoError::WrongKeyType),
322        }
323    }
324}
325
326impl KeyEncryptable<SymmetricCryptoKey, EncString> for String {
327    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
328        Utf8Bytes::from(self).encrypt_with_key(key)
329    }
330}
331
332impl KeyEncryptable<SymmetricCryptoKey, EncString> for &str {
333    fn encrypt_with_key(self, key: &SymmetricCryptoKey) -> Result<EncString> {
334        Utf8Bytes::from(self).encrypt_with_key(key)
335    }
336}
337
338impl KeyDecryptable<SymmetricCryptoKey, String> for EncString {
339    fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result<String> {
340        let dec: Vec<u8> = self.decrypt_with_key(key)?;
341        String::from_utf8(dec).map_err(|_| CryptoError::InvalidUtf8String)
342    }
343}
344
345/// Usually we wouldn't want to expose EncStrings in the API or the schemas.
346/// But during the transition phase we will expose endpoints using the EncString type.
347impl schemars::JsonSchema for EncString {
348    fn schema_name() -> Cow<'static, str> {
349        "EncString".into()
350    }
351
352    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
353        generator.subschema_for::<String>()
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use schemars::schema_for;
360
361    use super::EncString;
362    use crate::{
363        derive_symmetric_key, CryptoError, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey,
364        KEY_ID_SIZE,
365    };
366
367    fn encrypt_with_xchacha20(plaintext: &str) -> EncString {
368        let key_id = [0u8; KEY_ID_SIZE];
369        let enc_key = [0u8; 32];
370        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
371            key_id,
372            enc_key: Box::pin(enc_key.into()),
373        });
374
375        plaintext.encrypt_with_key(&key).expect("encryption works")
376    }
377
378    /// XChaCha20Poly1305 encstrings should be padded in blocks of 32 bytes. This ensures that the
379    /// encstring length does not reveal more than the 32-byte range of lengths that the contained
380    /// string falls into.
381    #[test]
382    fn test_xchacha20_encstring_string_padding_block_sizes() {
383        let cases = [
384            ("", 32),              // empty string, padded to 32
385            (&"a".repeat(31), 32), // largest in first block
386            (&"a".repeat(32), 64), // smallest in second block
387            (&"a".repeat(63), 64), // largest in second block
388            (&"a".repeat(64), 96), // smallest in third block
389        ];
390
391        let ciphertext_lengths: Vec<_> = cases
392            .iter()
393            .map(|(plaintext, _)| encrypt_with_xchacha20(plaintext).to_string().len())
394            .collect();
395
396        // Block 1: 0-31 (same length)
397        assert_eq!(ciphertext_lengths[0], ciphertext_lengths[1]);
398        // Block 2: 32-63 (same length, different from block 1)
399        assert_ne!(ciphertext_lengths[1], ciphertext_lengths[2]);
400        assert_eq!(ciphertext_lengths[2], ciphertext_lengths[3]);
401        // Block 3: 64+ (different from block 2)
402        assert_ne!(ciphertext_lengths[3], ciphertext_lengths[4]);
403    }
404
405    #[test]
406    fn test_enc_roundtrip_xchacha20() {
407        let key_id = [0u8; KEY_ID_SIZE];
408        let enc_key = [0u8; 32];
409        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
410            key_id,
411            enc_key: Box::pin(enc_key.into()),
412        });
413
414        let test_string = "encrypted_test_string";
415        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
416        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
417        assert_eq!(decrypted_str, test_string);
418    }
419
420    #[test]
421    fn test_enc_string_roundtrip() {
422        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
423
424        let test_string = "encrypted_test_string";
425        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
426
427        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
428        assert_eq!(decrypted_str, test_string);
429    }
430
431    #[test]
432    fn test_enc_roundtrip_xchacha20_empty() {
433        let key_id = [0u8; KEY_ID_SIZE];
434        let enc_key = [0u8; 32];
435        let key = SymmetricCryptoKey::XChaCha20Poly1305Key(crate::XChaCha20Poly1305Key {
436            key_id,
437            enc_key: Box::pin(enc_key.into()),
438        });
439
440        let test_string = "";
441        let cipher = test_string.to_owned().encrypt_with_key(&key).unwrap();
442        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
443        assert_eq!(decrypted_str, test_string);
444    }
445
446    #[test]
447    fn test_enc_string_roundtrip_empty() {
448        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
449
450        let test_string = "";
451        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
452
453        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
454        assert_eq!(decrypted_str, test_string);
455    }
456
457    #[test]
458    fn test_enc_string_ref_roundtrip() {
459        let key = SymmetricCryptoKey::Aes256CbcHmacKey(derive_symmetric_key("test"));
460
461        let test_string: &'static str = "encrypted_test_string";
462        let cipher = test_string.to_string().encrypt_with_key(&key).unwrap();
463
464        let decrypted_str: String = cipher.decrypt_with_key(&key).unwrap();
465        assert_eq!(decrypted_str, test_string);
466    }
467
468    #[test]
469    fn test_enc_string_serialization() {
470        #[derive(serde::Serialize, serde::Deserialize)]
471        struct Test {
472            key: EncString,
473        }
474
475        let cipher = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
476        let serialized = format!("{{\"key\":\"{cipher}\"}}");
477
478        let t = serde_json::from_str::<Test>(&serialized).unwrap();
479        assert_eq!(t.key.enc_type(), 2);
480        assert_eq!(t.key.to_string(), cipher);
481        assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
482    }
483
484    #[test]
485    fn test_enc_from_to_buffer() {
486        let enc_str: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
487        let enc_string: EncString = enc_str.parse().unwrap();
488
489        let enc_buf = enc_string.to_buffer().unwrap();
490
491        assert_eq!(
492            enc_buf,
493            vec![
494                2, 164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150, 67,
495                163, 228, 185, 63, 138, 95, 246, 177, 174, 3, 125, 185, 176, 249, 2, 57, 54, 96,
496                220, 49, 66, 72, 44, 221, 98, 76, 209, 45, 48, 180, 111, 93, 118, 241, 43, 16, 211,
497                135, 233, 150, 136, 221, 71, 140, 125, 141, 215
498            ]
499        );
500
501        let enc_string_new = EncString::from_buffer(&enc_buf).unwrap();
502
503        assert_eq!(enc_string_new.to_string(), enc_str)
504    }
505
506    #[test]
507    fn test_from_str_cbc256() {
508        let enc_str = "0.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==";
509        let enc_string: EncString = enc_str.parse().unwrap();
510
511        assert_eq!(enc_string.enc_type(), 0);
512        if let EncString::Aes256Cbc_B64 { iv, data } = &enc_string {
513            assert_eq!(
514                iv,
515                &[164, 196, 186, 254, 39, 19, 64, 0, 109, 186, 92, 57, 218, 154, 182, 150]
516            );
517            assert_eq!(
518                data,
519                &[93, 118, 241, 43, 16, 211, 135, 233, 150, 136, 221, 71, 140, 125, 141, 215]
520            );
521        } else {
522            panic!("Invalid variant")
523        };
524    }
525
526    #[test]
527    fn test_decrypt_cbc256() {
528        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe08=".to_string();
529        let key = SymmetricCryptoKey::try_from(key).unwrap();
530
531        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
532        let enc_string: EncString = enc_str.parse().unwrap();
533        assert_eq!(enc_string.enc_type(), 0);
534
535        let dec_str: String = enc_string.decrypt_with_key(&key).unwrap();
536        assert_eq!(dec_str, "EncryptMe!");
537    }
538
539    #[test]
540    fn test_decrypt_downgrade_encstring_prevention() {
541        // Simulate a potential downgrade attack by removing the mac portion of the `EncString` and
542        // attempt to decrypt it using a `SymmetricCryptoKey` with a mac key.
543        let key = "hvBMMb1t79YssFZkpetYsM3deyVuQv4r88Uj9gvYe0+G8EwxvW3v1iywVmSl61iwzd17JW5C/ivzxSP2C9h7Tw==".to_string();
544        let key = SymmetricCryptoKey::try_from(key).unwrap();
545
546        // A "downgraded" `EncString` from `EncString::Aes256Cbc_HmacSha256_B64` (2) to
547        // `EncString::Aes256Cbc_B64` (0), with the mac portion removed.
548        // <enc_string>
549        let enc_str = "0.NQfjHLr6za7VQVAbrpL81w==|wfrjmyJ0bfwkQlySrhw8dA==";
550        let enc_string: EncString = enc_str.parse().unwrap();
551        assert_eq!(enc_string.enc_type(), 0);
552
553        let result: Result<String, CryptoError> = enc_string.decrypt_with_key(&key);
554        assert!(matches!(result, Err(CryptoError::WrongKeyType)));
555    }
556
557    #[test]
558    fn test_from_str_invalid() {
559        let enc_str = "8.ABC";
560        let enc_string: Result<EncString, _> = enc_str.parse();
561
562        let err = enc_string.unwrap_err();
563        assert_eq!(
564            err.to_string(),
565            "EncString error, Invalid symmetric type, got type 8 with 1 parts"
566        );
567    }
568
569    #[test]
570    fn test_debug_format() {
571        let enc_str  = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
572        let enc_string: EncString = enc_str.parse().unwrap();
573
574        let debug_string = format!("{enc_string:?}");
575        assert_eq!(debug_string, enc_str);
576    }
577
578    #[test]
579    fn test_json_schema() {
580        let schema = schema_for!(EncString);
581
582        assert_eq!(
583            serde_json::to_string(&schema).unwrap(),
584            r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"EncString","type":"string"}"#
585        );
586    }
587}