Skip to main content

bitwarden_crypto/enc_string/
asymmetric.rs

1use std::{borrow::Cow, fmt::Display, str::FromStr};
2
3use bitwarden_encoding::{B64, FromStrVisitor};
4pub use internal::UnsignedSharedKey;
5use rsa::Oaep;
6use serde::Deserialize;
7#[cfg(feature = "wasm")]
8use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi};
9
10use super::{from_b64_vec, split_enc_string};
11use crate::{
12    BitwardenLegacyKeyBytes, KeySlotIds, KeyStoreContext, PrivateKey, PublicKey, RawPrivateKey,
13    RawPublicKey, SymmetricCryptoKey,
14    error::{CryptoError, EncStringParseError, Result},
15    rsa::encrypt_rsa2048_oaep_sha1,
16};
17// This module is a workaround to avoid deprecated warnings that come from the ZeroizeOnDrop
18// macro expansion
19#[allow(deprecated)]
20mod internal {
21    #[cfg(feature = "wasm")]
22    #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
23    const TS_CUSTOM_TYPES: &'static str = r#"
24    export type UnsignedSharedKey = Tagged<string, "UnsignedSharedKey">;
25    "#;
26
27    /// # Encrypted string primitive
28    ///
29    /// WARNING: This should not be used for new cryptographic constructions, since it does not
30    /// provide sender authentication, or cryptographic namespacing.
31    ///
32    /// [UnsignedSharedKey] is a Bitwarden specific primitive that represents an
33    /// asymmetrically encrypted symmetric key. Since the symmetric key is directly encrypted
34    /// with the public key, without any further signature, the receiver cannot guarantee the
35    /// senders identity.
36    ///
37    /// [UnsignedSharedKey] type allows for different encryption algorithms
38    /// to be used which is represented by the different variants of the enum.
39    ///
40    /// ## Note
41    ///
42    /// For backwards compatibility we will rarely if ever be able to remove support for decrypting
43    /// old variants, but we should be opinionated in which variants are used for encrypting.
44    ///
45    /// ## Variants
46    /// - [Rsa2048_OaepSha256_B64](UnsignedSharedKey::Rsa2048_OaepSha256_B64)
47    /// - [Rsa2048_OaepSha1_B64](UnsignedSharedKey::Rsa2048_OaepSha1_B64)
48    ///
49    /// ## Serialization
50    ///
51    /// [UnsignedSharedKey] implements [std::fmt::Display] and [std::str::FromStr] to allow
52    /// for easy serialization and uses a custom scheme to represent the different variants.
53    ///
54    /// The scheme is one of the following schemes:
55    /// - `[type].[data]`
56    ///
57    /// Where:
58    /// - `[type]`: is a digit number representing the variant.
59    /// - `[data]`: is the encrypted data.
60    #[allow(missing_docs)]
61    #[derive(Clone, zeroize::ZeroizeOnDrop, PartialEq)]
62    #[allow(unused, non_camel_case_types)]
63    pub enum UnsignedSharedKey {
64        /// 3
65        Rsa2048_OaepSha256_B64 { data: Vec<u8> },
66        /// 4
67        Rsa2048_OaepSha1_B64 { data: Vec<u8> },
68        /// 5
69        #[deprecated]
70        Rsa2048_OaepSha256_HmacSha256_B64 { data: Vec<u8>, mac: Vec<u8> },
71        /// 6
72        #[deprecated]
73        Rsa2048_OaepSha1_HmacSha256_B64 { data: Vec<u8>, mac: Vec<u8> },
74    }
75}
76
77/// To avoid printing sensitive information, [UnsignedSharedKey] debug prints to
78/// `UnsignedSharedKey`.
79impl std::fmt::Debug for UnsignedSharedKey {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.debug_struct("UnsignedSharedKey").finish()
82    }
83}
84
85/// Deserializes an [UnsignedSharedKey] from a string.
86impl FromStr for UnsignedSharedKey {
87    type Err = CryptoError;
88
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        let (enc_type, parts) = split_enc_string(s);
91        match (enc_type, parts.len()) {
92            ("3", 1) => {
93                let data = from_b64_vec(parts[0])?;
94                Ok(UnsignedSharedKey::Rsa2048_OaepSha256_B64 { data })
95            }
96            ("4", 1) => {
97                let data = from_b64_vec(parts[0])?;
98                Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data })
99            }
100            #[allow(deprecated)]
101            ("5", 2) => {
102                let data = from_b64_vec(parts[0])?;
103                let mac: Vec<u8> = from_b64_vec(parts[1])?;
104                Ok(UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac })
105            }
106            #[allow(deprecated)]
107            ("6", 2) => {
108                let data = from_b64_vec(parts[0])?;
109                let mac: Vec<u8> = from_b64_vec(parts[1])?;
110                Ok(UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac })
111            }
112
113            (enc_type, parts) => Err(EncStringParseError::InvalidTypeAsymm {
114                enc_type: enc_type.to_string(),
115                parts,
116            }
117            .into()),
118        }
119    }
120}
121
122impl Display for UnsignedSharedKey {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        let parts: Vec<&[u8]> = match self {
125            UnsignedSharedKey::Rsa2048_OaepSha256_B64 { data } => vec![data],
126            UnsignedSharedKey::Rsa2048_OaepSha1_B64 { data } => vec![data],
127            #[allow(deprecated)]
128            UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { data, mac } => {
129                vec![data, mac]
130            }
131            #[allow(deprecated)]
132            UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { data, mac } => {
133                vec![data, mac]
134            }
135        };
136
137        let encoded_parts: Vec<String> = parts
138            .iter()
139            .map(|part| B64::from(*part).to_string())
140            .collect();
141
142        write!(f, "{}.{}", self.enc_type(), encoded_parts.join("|"))?;
143
144        Ok(())
145    }
146}
147
148#[cfg(feature = "wasm")]
149impl wasm_bindgen::describe::WasmDescribe for UnsignedSharedKey {
150    fn describe() {
151        <String as wasm_bindgen::describe::WasmDescribe>::describe();
152    }
153}
154
155#[cfg(feature = "wasm")]
156impl FromWasmAbi for UnsignedSharedKey {
157    type Abi = <String as FromWasmAbi>::Abi;
158
159    unsafe fn from_abi(abi: Self::Abi) -> Self {
160        use wasm_bindgen::UnwrapThrowExt;
161
162        let s = unsafe { String::from_abi(abi) };
163        Self::from_str(&s).unwrap_throw()
164    }
165}
166
167#[cfg(feature = "wasm")]
168impl OptionFromWasmAbi for UnsignedSharedKey {
169    fn is_none(abi: &Self::Abi) -> bool {
170        <String as OptionFromWasmAbi>::is_none(abi)
171    }
172}
173
174#[cfg(feature = "wasm")]
175impl IntoWasmAbi for UnsignedSharedKey {
176    type Abi = <String as IntoWasmAbi>::Abi;
177
178    fn into_abi(self) -> Self::Abi {
179        self.to_string().into_abi()
180    }
181}
182
183#[cfg(feature = "wasm")]
184impl TryFrom<wasm_bindgen::JsValue> for UnsignedSharedKey {
185    type Error = CryptoError;
186
187    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
188        let string = value
189            .as_string()
190            .ok_or(EncStringParseError::NoType)
191            .map_err(CryptoError::from)?;
192        Self::from_str(&string)
193    }
194}
195
196impl<'de> Deserialize<'de> for UnsignedSharedKey {
197    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
198    where
199        D: serde::Deserializer<'de>,
200    {
201        deserializer.deserialize_str(FromStrVisitor::new())
202    }
203}
204
205impl serde::Serialize for UnsignedSharedKey {
206    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
207    where
208        S: serde::Serializer,
209    {
210        serializer.serialize_str(&self.to_string())
211    }
212}
213
214impl UnsignedSharedKey {
215    /// Encapsulate a symmetric key, to be shared asymmetrically. Produces a
216    /// [UnsignedSharedKey::Rsa2048_OaepSha1_B64] variant. Note, this does not sign the data
217    /// and thus does not guarantee sender authenticity.
218    #[deprecated(note = "Use encapsulate() instead")]
219    pub fn encapsulate_key_unsigned(
220        encapsulated_key: &SymmetricCryptoKey,
221        encapsulation_key: &PublicKey,
222    ) -> Result<UnsignedSharedKey> {
223        match encapsulation_key.inner() {
224            RawPublicKey::RsaOaepSha1(rsa_public_key) => {
225                Ok(UnsignedSharedKey::Rsa2048_OaepSha1_B64 {
226                    data: encrypt_rsa2048_oaep_sha1(
227                        rsa_public_key,
228                        encapsulated_key.to_encoded().as_ref(),
229                    )?,
230                })
231            }
232        }
233    }
234
235    /// Encapsulate a symmetric key, to be shared asymmetrically. Produces a
236    /// [UnsignedSharedKey::Rsa2048_OaepSha1_B64] variant. Note, this does not sign the data
237    /// and thus does not guarantee sender authenticity.
238    pub fn encapsulate<Ids: KeySlotIds>(
239        key_to_encapsulate: Ids::Symmetric,
240        encapsulation_key: &PublicKey,
241        ctx: &KeyStoreContext<Ids>,
242    ) -> Result<UnsignedSharedKey> {
243        // Internal usage to the crypto crate is allowed
244        #[expect(deprecated)]
245        let encapsulated_key = ctx.dangerous_get_symmetric_key(key_to_encapsulate)?;
246        // Will be replaced once callers have been moved over
247        #[expect(deprecated)]
248        Self::encapsulate_key_unsigned(encapsulated_key, encapsulation_key)
249    }
250
251    /// The numerical representation of the encryption type of the [UnsignedSharedKey].
252    const fn enc_type(&self) -> u8 {
253        match self {
254            UnsignedSharedKey::Rsa2048_OaepSha256_B64 { .. } => 3,
255            UnsignedSharedKey::Rsa2048_OaepSha1_B64 { .. } => 4,
256            #[allow(deprecated)]
257            UnsignedSharedKey::Rsa2048_OaepSha256_HmacSha256_B64 { .. } => 5,
258            #[allow(deprecated)]
259            UnsignedSharedKey::Rsa2048_OaepSha1_HmacSha256_B64 { .. } => 6,
260        }
261    }
262}
263
264impl UnsignedSharedKey {
265    /// Decapsulate a symmetric key using an asymmetric decapsulation key from the key store.
266    /// Returns the key ID of the decapsulated symmetric key added to the context.
267    pub fn decapsulate<Ids: KeySlotIds>(
268        &self,
269        decapsulation_key: Ids::Private,
270        ctx: &mut KeyStoreContext<Ids>,
271    ) -> Result<Ids::Symmetric> {
272        // Internal usage to the crypto crate is allowed
273        #[expect(deprecated)]
274        let private_key = ctx.dangerous_get_private_key(decapsulation_key)?;
275        #[expect(deprecated)]
276        let key = Self::decapsulate_key_unsigned(self, private_key)
277            .map_err(|_| CryptoError::KeyDecrypt)?;
278        Ok(ctx.add_local_symmetric_key(key))
279    }
280
281    /// Decapsulate a symmetric key, shared asymmetrically.
282    /// Note: The shared key does not have a sender signature and sender authenticity is not
283    /// guaranteed.
284    #[deprecated(note = "Use decapsulate() instead")]
285    pub fn decapsulate_key_unsigned(
286        &self,
287        decapsulation_key: &PrivateKey,
288    ) -> Result<SymmetricCryptoKey> {
289        match decapsulation_key.inner() {
290            RawPrivateKey::RsaOaepSha1(rsa_private_key) => {
291                use UnsignedSharedKey::*;
292                let key_data = match self {
293                    Rsa2048_OaepSha256_B64 { data } => {
294                        rsa_private_key.decrypt(Oaep::<sha2::Sha256>::new(), data)
295                    }
296                    Rsa2048_OaepSha1_B64 { data } => {
297                        rsa_private_key.decrypt(Oaep::<sha1::Sha1>::new(), data)
298                    }
299                    #[allow(deprecated)]
300                    Rsa2048_OaepSha256_HmacSha256_B64 { data, .. } => {
301                        rsa_private_key.decrypt(Oaep::<sha2::Sha256>::new(), data)
302                    }
303                    #[allow(deprecated)]
304                    Rsa2048_OaepSha1_HmacSha256_B64 { data, .. } => {
305                        rsa_private_key.decrypt(Oaep::<sha1::Sha1>::new(), data)
306                    }
307                }
308                .map_err(|_| CryptoError::KeyDecrypt)?;
309                SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(key_data))
310            }
311        }
312    }
313}
314
315/// Usually we wouldn't want to expose UnsignedSharedKeys in the API or the schemas.
316/// But during the transition phase we will expose endpoints using the UnsignedSharedKey
317/// type.
318impl schemars::JsonSchema for UnsignedSharedKey {
319    fn schema_name() -> Cow<'static, str> {
320        "UnsignedSharedKey".into()
321    }
322
323    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
324        generator.subschema_for::<String>()
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use schemars::schema_for;
331
332    use super::UnsignedSharedKey;
333    use crate::{PrivateKey, SymmetricCryptoKey};
334
335    const RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
336MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS
3378HzYUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2
338e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86LnhD56A9FDUfuI0dVnPcrwNv0YJIo9
3394LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfa
340F4/YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6A
341QOajdZijfEvepgnOe7cQ7aeatiOJFrjTApKPGxOVRzEMX4XS4xbyhH0QxQeB6l16
342l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq
34392qBuwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tP
344dr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapjWpxEF+11x7r+wM+0xRZQ8sNFYG46a
345PfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLX
346UIh5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTR
347buDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2
348hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxuc
349vOUBeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjA
350hCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIfTFKC/hDk6FKZlgwvupWYJyU9Rkyfs
351tPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQY
352UcUq4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vs
353zv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVvq1UTXIeQcQnoY5lGHJl3K8mbS3TnX
354E6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEP
355jNX5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBez
356MRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1eLLGd7YV0H+J3fgNc7gGWK51hOrF9
357JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXg
358AoEZ18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGp
359Is3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8+tPVgppLcG0+tMdLjigFQiDUQk2y3
360WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEz
361XKZBokBGnjFnTnKcs7nv/O8=
362-----END PRIVATE KEY-----";
363
364    #[test]
365    fn test_enc_string_rsa2048_oaep_sha256_b64() {
366        let key_pair = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
367        let enc_str: &str = "3.SUx5gWrgmAKs/S1BoQrqOmx2Hl5fPVBVHokW17Flvm4TpBnJJRkfoitp7Jc4dfazPYjWGlckJz6X+qe+/AWilS1mxtzS0PmDy7tS5xP0GRlB39dstCd5jDw1wPmTbXiLcQ5VTvzpRAfRMEYVveTsEvVTByvEYAGSn4TnCsUDykyhRbD0YcJ4r1KHLs1b3BCBy2M1Gl5nmwckH08CAXaf8VfuBFStAGRKueovqp4euneQla+4G4fXdVvb8qKPnu0iVuALIE6nUNmeOiA3xN3d+akMxbbGxrQ1Ca4TYWjHVdj9C6abngQHkjKNYQwGUXrYo160hP4LIHn/huK6bZe5dQ==";
368        let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
369
370        let test_key = SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
371        assert_eq!(enc_string.enc_type(), 3);
372
373        #[expect(deprecated)]
374        let res = enc_string.decapsulate_key_unsigned(&key_pair).unwrap();
375        assert_eq!(res, test_key);
376    }
377
378    #[test]
379    fn test_enc_string_rsa2048_oaep_sha1_b64() {
380        let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
381        let enc_str: &str = "4.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==";
382        let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
383
384        let test_key = SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
385        assert_eq!(enc_string.enc_type(), 4);
386
387        #[expect(deprecated)]
388        let res = enc_string.decapsulate_key_unsigned(&private_key).unwrap();
389        assert_eq!(res, test_key);
390    }
391
392    #[test]
393    fn test_enc_string_rsa2048_oaep_sha1_hmac_sha256_b64() {
394        let private_key = PrivateKey::from_pem(RSA_PRIVATE_KEY).unwrap();
395        let enc_str: &str = "6.DMD1D5r6BsDDd7C/FE1eZbMCKrmryvAsCKj6+bO54gJNUxisOI7SDcpPLRXf+JdhqY15pT+wimQ5cD9C+6OQ6s71LFQHewXPU29l9Pa1JxGeiKqp37KLYf+1IS6UB2K3ANN35C52ZUHh2TlzIS5RuntxnpCw7APbcfpcnmIdLPJBtuj/xbFd6eBwnI3GSe5qdS6/Ixdd0dgsZcpz3gHJBKmIlSo0YN60SweDq3kTJwox9xSqdCueIDg5U4khc7RhjYx8b33HXaNJj3DwgIH8iLj+lqpDekogr630OhHG3XRpvl4QzYO45bmHb8wAh67Dj70nsZcVg6bAEFHdSFohww==|AA==";
396        let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
397
398        let test_key: SymmetricCryptoKey =
399            SymmetricCryptoKey::generate_seeded_for_unit_tests("test");
400        assert_eq!(enc_string.enc_type(), 6);
401
402        #[expect(deprecated)]
403        let res = enc_string.decapsulate_key_unsigned(&private_key).unwrap();
404        assert_eq!(res.to_base64(), test_key.to_base64());
405    }
406
407    #[test]
408    fn test_enc_string_serialization() {
409        #[derive(serde::Serialize, serde::Deserialize)]
410        struct Test {
411            key: UnsignedSharedKey,
412        }
413
414        let cipher = "6.ThnNc67nNr7GELyuhGGfsXNP2zJnNqhrIsjntEQ27r2qmn8vwdHbTbfO0cwt6YgSibDN0PjiCZ1O3Wb/IFq+vwvyRwFqF9145wBF8CQCbkhV+M0XvO99kh0daovtt120Nve/5ETI5PbPag9VdalKRQWZypJaqQHm5TAQVf4F5wtLlCLMBkzqTk+wkFe7BPMTGn07T+O3eJbTxXvyMZewQ7icJF0MZVA7VyWX9qElmZ89FCKowbf1BMr5pbcQ+0KdXcSVW3to43VkTp7k7COwsuH3M/i1AuVP5YN8ixjyRpvaeGqX/ap2nCHK2Wj5VxgCGT7XEls6ZknnAp9nB9qVjQ==|s3ntw5H/KKD/qsS0lUghTHl5Sm9j6m7YEdNHf0OeAFQ=";
415        let serialized = format!("{{\"key\":\"{cipher}\"}}");
416
417        let t = serde_json::from_str::<Test>(&serialized).unwrap();
418        assert_eq!(t.key.enc_type(), 6);
419        assert_eq!(t.key.to_string(), cipher);
420        assert_eq!(serde_json::to_string(&t).unwrap(), serialized);
421    }
422
423    #[test]
424    fn test_from_str_invalid() {
425        let enc_str = "7.ABC";
426        let enc_string: Result<UnsignedSharedKey, _> = enc_str.parse();
427
428        let err = enc_string.unwrap_err();
429        assert_eq!(
430            err.to_string(),
431            "EncString error, Invalid asymmetric type, got type 7 with 1 parts"
432        );
433    }
434
435    #[test]
436    fn test_debug_format() {
437        let enc_str: &str = "4.ZheRb3PCfAunyFdQYPfyrFqpuvmln9H9w5nDjt88i5A7ug1XE0LJdQHCIYJl0YOZ1gCOGkhFu/CRY2StiLmT3iRKrrVBbC1+qRMjNNyDvRcFi91LWsmRXhONVSPjywzrJJXglsztDqGkLO93dKXNhuKpcmtBLsvgkphk/aFvxbaOvJ/FHdK/iV0dMGNhc/9tbys8laTdwBlI5xIChpRcrfH+XpSFM88+Bu03uK67N9G6eU1UmET+pISJwJvMuIDMqH+qkT7OOzgL3t6I0H2LDj+CnsumnQmDsvQzDiNfTR0IgjpoE9YH2LvPXVP2wVUkiTwXD9cG/E7XeoiduHyHjw==";
438        let enc_string: UnsignedSharedKey = enc_str.parse().unwrap();
439
440        let debug_string = format!("{enc_string:?}");
441        assert_eq!(debug_string, "UnsignedSharedKey");
442    }
443
444    #[test]
445    fn test_json_schema() {
446        let schema = schema_for!(UnsignedSharedKey);
447
448        assert_eq!(
449            serde_json::to_string(&schema).unwrap(),
450            r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"UnsignedSharedKey","type":"string"}"#
451        );
452    }
453}