Skip to main content

bitwarden_crypto/keys/
public_key_encryption.rs

1use std::{fmt::Display, pin::Pin, str::FromStr};
2
3use bitwarden_encoding::{B64, FromStrVisitor};
4use ciborium::{Value, value::Integer};
5use coset::iana::{EnumI64, KeyParameter, KeyType, RsaKeyParameter};
6use rsa::{RsaPrivateKey, RsaPublicKey, pkcs8::DecodePublicKey, traits::PublicKeyParts};
7use serde::{Deserialize, Serialize};
8use serde_repr::{Deserialize_repr, Serialize_repr};
9#[cfg(feature = "wasm")]
10use wasm_bindgen::convert::FromWasmAbi;
11
12use super::key_encryptable::CryptoKey;
13use crate::{
14    CoseKeyThumbprint, Pkcs8PrivateKeyBytes, SpkiPublicKeyBytes,
15    cose::{CoseKeyThumbprintExt, thumbprint_from_required_params},
16    error::{CryptoError, Result},
17};
18
19#[cfg(feature = "wasm")]
20#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
21const TS_CUSTOM_TYPES: &'static str = r#"
22export type PublicKey = Tagged<string, "PublicKey">;
23"#;
24
25#[cfg(feature = "wasm")]
26impl wasm_bindgen::describe::WasmDescribe for PublicKey {
27    fn describe() {
28        <String as wasm_bindgen::describe::WasmDescribe>::describe();
29    }
30}
31
32#[cfg(feature = "wasm")]
33impl FromWasmAbi for PublicKey {
34    type Abi = <String as FromWasmAbi>::Abi;
35
36    unsafe fn from_abi(abi: Self::Abi) -> Self {
37        use wasm_bindgen::UnwrapThrowExt;
38
39        let s = unsafe { String::from_abi(abi) };
40        let bytes: Vec<u8> = s.parse::<bitwarden_encoding::B64>().unwrap_throw().into();
41        PublicKey::from_der(&SpkiPublicKeyBytes::from(bytes)).unwrap_throw()
42    }
43}
44
45/// Algorithm / public key encryption scheme used for encryption/decryption.
46#[derive(Debug, Serialize_repr, Deserialize_repr)]
47#[repr(u8)]
48pub enum PublicKeyEncryptionAlgorithm {
49    /// RSA with OAEP padding and SHA-1 hashing.
50    RsaOaepSha1 = 0,
51}
52
53#[derive(Clone, PartialEq)]
54pub(crate) enum RawPublicKey {
55    RsaOaepSha1(RsaPublicKey),
56}
57
58/// Public key of a key pair used in a public key encryption scheme. It is used for
59/// encrypting data.
60#[derive(Clone, PartialEq)]
61pub struct PublicKey {
62    inner: RawPublicKey,
63}
64
65impl std::fmt::Debug for PublicKey {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        let key_suffix = match &self.inner {
68            RawPublicKey::RsaOaepSha1(_) => "RsaOaepSha1",
69        };
70        let mut debug_struct = f.debug_struct(format!("PublicKey::{}", key_suffix).as_str());
71        match &self.inner {
72            RawPublicKey::RsaOaepSha1(_) => {
73                if let Ok(der) = self.to_der() {
74                    debug_struct.field("key", &hex::encode(der.as_ref()));
75                }
76            }
77        }
78        debug_struct.finish()
79    }
80}
81
82impl PublicKey {
83    pub(crate) fn inner(&self) -> &RawPublicKey {
84        &self.inner
85    }
86
87    /// Build a public key from the SubjectPublicKeyInfo DER.
88    #[bitwarden_logging::instrument(err)]
89    pub fn from_der(der: &SpkiPublicKeyBytes) -> Result<Self> {
90        Ok(PublicKey {
91            inner: RawPublicKey::RsaOaepSha1(
92                RsaPublicKey::from_public_key_der(der.as_ref())
93                    .map_err(|_| CryptoError::InvalidKey)?,
94            ),
95        })
96    }
97
98    /// Makes a SubjectPublicKeyInfo DER serialized version of the public key.
99    #[bitwarden_logging::instrument(err)]
100    pub fn to_der(&self) -> Result<SpkiPublicKeyBytes> {
101        use rsa::pkcs8::EncodePublicKey;
102        match &self.inner {
103            RawPublicKey::RsaOaepSha1(public_key) => Ok(public_key
104                .to_public_key_der()
105                .map_err(|_| CryptoError::InvalidKey)?
106                .as_bytes()
107                .to_owned()
108                .into()),
109        }
110    }
111}
112
113impl CoseKeyThumbprintExt for PublicKey {
114    fn thumbprint(&self) -> Result<CoseKeyThumbprint> {
115        let params = match &self.inner {
116            RawPublicKey::RsaOaepSha1(key) => vec![
117                (
118                    KeyParameter::Kty.to_i64(),
119                    Value::Integer(Integer::from(KeyType::RSA.to_i64())),
120                ),
121                // Per RFC 8230, `n` and `e` are unsigned big-endian byte strings with no leading
122                // zero or sign byte. `to_be_bytes_trimmed_vartime` yields exactly this minimal
123                // form; variable-time is acceptable as `n` and `e` are public.
124                (
125                    RsaKeyParameter::N.to_i64(),
126                    Value::Bytes(key.n().to_be_bytes_trimmed_vartime().into_vec()),
127                ),
128                (
129                    RsaKeyParameter::E.to_i64(),
130                    Value::Bytes(key.e().to_be_bytes_trimmed_vartime().into_vec()),
131                ),
132            ],
133        };
134        Ok(thumbprint_from_required_params(params))
135    }
136}
137
138impl FromStr for PublicKey {
139    type Err = ();
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        let bytes: Vec<u8> = s.parse::<B64>().map_err(|_| ())?.into();
143        Self::from_der(&SpkiPublicKeyBytes::from(bytes)).map_err(|_| ())
144    }
145}
146
147impl Display for PublicKey {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self.to_der() {
150            Ok(der) => write!(f, "{}", B64::from(der.as_ref())),
151            Err(_) => write!(f, "[INVALID PUBLIC KEY]"),
152        }
153    }
154}
155
156impl<'de> Deserialize<'de> for PublicKey {
157    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
158    where
159        D: serde::Deserializer<'de>,
160    {
161        deserializer.deserialize_str(FromStrVisitor::new())
162    }
163}
164
165impl Serialize for PublicKey {
166    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
167    where
168        S: serde::Serializer,
169    {
170        let der = self.to_der().map_err(serde::ser::Error::custom)?;
171        serializer.serialize_str(&B64::from(der.as_ref()).to_string())
172    }
173}
174
175#[derive(Clone)]
176pub(crate) enum RawPrivateKey {
177    // RsaPrivateKey is not a Copy type so this isn't completely necessary, but
178    // to keep the compiler from making stack copies when moving this struct around,
179    // we use a Box to keep the values on the heap. We also pin the box to make sure
180    // that the contents can't be pulled out of the box and moved
181    RsaOaepSha1(Pin<Box<RsaPrivateKey>>),
182}
183
184/// Private key of a key pair used in a public key encryption scheme. It is used for
185/// decrypting data that was encrypted with the corresponding public key.
186#[derive(Clone)]
187pub struct PrivateKey {
188    inner: RawPrivateKey,
189}
190
191// Note that RsaPrivateKey already implements ZeroizeOnDrop, so we don't need to do anything
192// We add this assertion to make sure that this is still true in the future
193const _: fn() = || {
194    fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
195    assert_zeroize_on_drop::<RsaPrivateKey>();
196};
197impl zeroize::ZeroizeOnDrop for PrivateKey {}
198impl CryptoKey for PrivateKey {}
199
200impl PrivateKey {
201    /// Generate a random PrivateKey (RSA-2048).
202    pub fn make(algorithm: PublicKeyEncryptionAlgorithm) -> Self {
203        Self::make_internal(algorithm, &mut bitwarden_random::rng())
204    }
205
206    fn make_internal<R: rand::CryptoRng + rand::Rng>(
207        algorithm: PublicKeyEncryptionAlgorithm,
208        rng: &mut R,
209    ) -> Self {
210        match algorithm {
211            PublicKeyEncryptionAlgorithm::RsaOaepSha1 => Self {
212                inner: RawPrivateKey::RsaOaepSha1(Box::pin(
213                    RsaPrivateKey::new(rng, 2048).expect("failed to generate a key"),
214                )),
215            },
216        }
217    }
218
219    #[allow(missing_docs)]
220    // Under `dangerous-crypto-debug` we intentionally log key material, so this arm uses
221    // `tracing::instrument` directly (the `bitwarden_logging` wrapper enforces `skip_all`).
222    // The production arm goes through the wrapper. The `allow` only applies when the dangerous
223    // arm is active.
224    #[cfg_attr(
225        feature = "dangerous-crypto-debug",
226        allow(unknown_lints, tracing_instrument)
227    )]
228    #[cfg_attr(feature = "dangerous-crypto-debug", tracing::instrument(err))]
229    #[cfg_attr(
230        not(feature = "dangerous-crypto-debug"),
231        bitwarden_logging::instrument(err)
232    )]
233    pub fn from_pem(pem: &str) -> Result<Self> {
234        use rsa::pkcs8::DecodePrivateKey;
235        Ok(Self {
236            inner: RawPrivateKey::RsaOaepSha1(Box::pin(
237                RsaPrivateKey::from_pkcs8_pem(pem).map_err(|_| CryptoError::InvalidKey)?,
238            )),
239        })
240    }
241
242    #[allow(missing_docs)]
243    #[cfg_attr(
244        feature = "dangerous-crypto-debug",
245        allow(unknown_lints, tracing_instrument)
246    )]
247    #[cfg_attr(feature = "dangerous-crypto-debug", tracing::instrument(err))]
248    #[cfg_attr(
249        not(feature = "dangerous-crypto-debug"),
250        bitwarden_logging::instrument(err)
251    )]
252    pub fn from_der(der: &Pkcs8PrivateKeyBytes) -> Result<Self> {
253        use rsa::pkcs8::DecodePrivateKey;
254        Ok(Self {
255            inner: RawPrivateKey::RsaOaepSha1(Box::pin(
256                RsaPrivateKey::from_pkcs8_der(der.as_ref()).map_err(|_| CryptoError::InvalidKey)?,
257            )),
258        })
259    }
260
261    #[allow(missing_docs)]
262    #[cfg_attr(
263        feature = "dangerous-crypto-debug",
264        allow(unknown_lints, tracing_instrument)
265    )]
266    #[cfg_attr(feature = "dangerous-crypto-debug", tracing::instrument(err))]
267    #[cfg_attr(
268        not(feature = "dangerous-crypto-debug"),
269        bitwarden_logging::instrument(err)
270    )]
271    pub fn to_der(&self) -> Result<Pkcs8PrivateKeyBytes> {
272        match &self.inner {
273            RawPrivateKey::RsaOaepSha1(private_key) => {
274                use rsa::pkcs8::EncodePrivateKey;
275                Ok(private_key
276                    .to_pkcs8_der()
277                    .map_err(|_| CryptoError::InvalidKey)?
278                    .as_bytes()
279                    .to_owned()
280                    .into())
281            }
282        }
283    }
284
285    /// Derives the public key corresponding to this private key. This is deterministic
286    /// and always derives the same public key.
287    pub fn to_public_key(&self) -> PublicKey {
288        match &self.inner {
289            RawPrivateKey::RsaOaepSha1(private_key) => PublicKey {
290                inner: RawPublicKey::RsaOaepSha1(private_key.to_public_key()),
291            },
292        }
293    }
294
295    pub(crate) fn inner(&self) -> &RawPrivateKey {
296        &self.inner
297    }
298}
299
300impl CoseKeyThumbprintExt for PrivateKey {
301    fn thumbprint(&self) -> Result<CoseKeyThumbprint> {
302        self.to_public_key().thumbprint()
303    }
304}
305
306// We manually implement these to make sure we don't print any sensitive data
307impl std::fmt::Debug for PrivateKey {
308    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        let key_suffix = match &self.inner {
310            RawPrivateKey::RsaOaepSha1(_) => "RsaOaepSha1",
311        };
312        let mut debug_struct = f.debug_struct(format!("PrivateKey::{}", key_suffix).as_str());
313        #[cfg(feature = "dangerous-crypto-debug")]
314        match &self.inner {
315            RawPrivateKey::RsaOaepSha1(_) => {
316                if let Ok(der) = self.to_der() {
317                    debug_struct.field("key", &hex::encode(der.as_ref()));
318                }
319            }
320        }
321        debug_struct.finish()
322    }
323}
324
325#[cfg(test)]
326mod tests {
327
328    use bitwarden_encoding::B64;
329
330    use crate::{
331        CoseKeyThumbprintExt, Pkcs8PrivateKeyBytes, PrivateKey, PublicKey,
332        PublicKeyEncryptionAlgorithm, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey,
333        content_format::{Bytes, Pkcs8PrivateKeyDerContentFormat},
334    };
335
336    #[test]
337    #[ignore = "Manual test to verify debug format"]
338    fn test_debug() {
339        let private_key = PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
340        println!("{:?}", private_key);
341        let public_key = private_key.to_public_key();
342        println!("{:?}", public_key);
343    }
344
345    #[test]
346    fn test_asymmetric_crypto_key() {
347        let pem_key_str = "-----BEGIN PRIVATE KEY-----
348MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDiTQVuzhdygFz5
349qv14i+XFDGTnDravzUQT1hPKPGUZOUSZ1gwdNgkWqOIaOnR65BHEnL0sp4bnuiYc
350afeK2JAW5Sc8Z7IxBNSuAwhQmuKx3RochMIiuCkI2/p+JvUQoJu6FBNm8OoJ4Cwm
351qqHGZESMfnpQDCuDrB3JdJEdXhtmnl0C48sGjOk3WaBMcgGqn8LbJDUlyu1zdqyv
352b0waJf0iV4PJm2fkUl7+57D/2TkpbCqURVnZK1FFIEg8mr6FzSN1F2pOfktkNYZw
353P7MSNR7o81CkRSCMr7EkIVa+MZYMBx106BMK7FXgWB7nbSpsWKxBk7ZDHkID2fam
354rEcVtrzDAgMBAAECggEBAKwq9OssGGKgjhvUnyrLJHAZ0dqIMyzk+dotkLjX4gKi
355szJmyqiep6N5sStLNbsZMPtoU/RZMCW0VbJgXFhiEp2YkZU/Py5UAoqw++53J+kx
3560d/IkPphKbb3xUec0+1mg5O6GljDCQuiZXS1dIa/WfeZcezclW6Dz9WovY6ePjJ+
3578vEBR1icbNKzyeINd6MtPtpcgQPHtDwHvhPyUDbKDYGbLvjh9nui8h4+ZUlXKuVR
358jB0ChxiKV1xJRjkrEVoulOOicd5r597WfB2ghax3pvRZ4MdXemCXm3gQYqPVKach
359vGU+1cPQR/MBJZpxT+EZA97xwtFS3gqwbxJaNFcoE8ECgYEA9OaeYZhQPDo485tI
3601u/Z7L/3PNape9hBQIXoW7+MgcQ5NiWqYh8Jnj43EIYa0wM/ECQINr1Za8Q5e6KR
361J30FcU+kfyjuQ0jeXdNELGU/fx5XXNg/vV8GevHwxRlwzqZTCg6UExUZzbYEQqd7
362l+wPyETGeua5xCEywA1nX/D101kCgYEA7I6aMFjhEjO71RmzNhqjKJt6DOghoOfQ
363TjhaaanNEhLYSbenFz1mlb21mW67ulmz162saKdIYLxQNJIP8ZPmxh4ummOJI8w9
364ClHfo8WuCI2hCjJ19xbQJocSbTA5aJg6lA1IDVZMDbQwsnAByPRGpaLHBT/Q9Bye
365KvCMB+9amXsCgYEAx65yXSkP4sumPBrVHUub6MntERIGRxBgw/drKcPZEMWp0FiN
366wEuGUBxyUWrG3F69QK/gcqGZE6F/LSu0JvptQaKqgXQiMYJsrRvhbkFvsHpQyUcZ
367UZL1ebFjm5HOxPAgrQaN/bEqxOwwNRjSUWEMzUImg3c06JIZCzbinvudtKECgYEA
368kY3JF/iIPI/yglP27lKDlCfeeHSYxI3+oTKRhzSAxx8rUGidenJAXeDGDauR/T7W
369pt3pGNfddBBK9Z3uC4Iq3DqUCFE4f/taj7ADAJ1Q0Vh7/28/IJM77ojr8J1cpZwN
370Zy2o6PPxhfkagaDjqEeN9Lrs5LD4nEvDkr5CG1vOjmMCgYEAvIBFKRm31NyF8jLi
371CVuPwC5PzrW5iThDmsWTaXFpB3esUsbICO2pEz872oeQS+Em4GO5vXUlpbbFPzup
372PFhA8iMJ8TAvemhvc7oM0OZqpU6p3K4seHf6BkwLxumoA3vDJfovu9RuXVcJVOnf
373DnqOsltgPomWZ7xVfMkm9niL2OA=
374-----END PRIVATE KEY-----";
375
376        let der_key: B64 = "MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDiTQVuzhdygFz5qv14i+XFDGTnDravzUQT1hPKPGUZOUSZ1gwdNgkWqOIaOnR65BHEnL0sp4bnuiYcafeK2JAW5Sc8Z7IxBNSuAwhQmuKx3RochMIiuCkI2/p+JvUQoJu6FBNm8OoJ4CwmqqHGZESMfnpQDCuDrB3JdJEdXhtmnl0C48sGjOk3WaBMcgGqn8LbJDUlyu1zdqyvb0waJf0iV4PJm2fkUl7+57D/2TkpbCqURVnZK1FFIEg8mr6FzSN1F2pOfktkNYZwP7MSNR7o81CkRSCMr7EkIVa+MZYMBx106BMK7FXgWB7nbSpsWKxBk7ZDHkID2famrEcVtrzDAgMBAAECggEBAKwq9OssGGKgjhvUnyrLJHAZ0dqIMyzk+dotkLjX4gKiszJmyqiep6N5sStLNbsZMPtoU/RZMCW0VbJgXFhiEp2YkZU/Py5UAoqw++53J+kx0d/IkPphKbb3xUec0+1mg5O6GljDCQuiZXS1dIa/WfeZcezclW6Dz9WovY6ePjJ+8vEBR1icbNKzyeINd6MtPtpcgQPHtDwHvhPyUDbKDYGbLvjh9nui8h4+ZUlXKuVRjB0ChxiKV1xJRjkrEVoulOOicd5r597WfB2ghax3pvRZ4MdXemCXm3gQYqPVKachvGU+1cPQR/MBJZpxT+EZA97xwtFS3gqwbxJaNFcoE8ECgYEA9OaeYZhQPDo485tI1u/Z7L/3PNape9hBQIXoW7+MgcQ5NiWqYh8Jnj43EIYa0wM/ECQINr1Za8Q5e6KRJ30FcU+kfyjuQ0jeXdNELGU/fx5XXNg/vV8GevHwxRlwzqZTCg6UExUZzbYEQqd7l+wPyETGeua5xCEywA1nX/D101kCgYEA7I6aMFjhEjO71RmzNhqjKJt6DOghoOfQTjhaaanNEhLYSbenFz1mlb21mW67ulmz162saKdIYLxQNJIP8ZPmxh4ummOJI8w9ClHfo8WuCI2hCjJ19xbQJocSbTA5aJg6lA1IDVZMDbQwsnAByPRGpaLHBT/Q9ByeKvCMB+9amXsCgYEAx65yXSkP4sumPBrVHUub6MntERIGRxBgw/drKcPZEMWp0FiNwEuGUBxyUWrG3F69QK/gcqGZE6F/LSu0JvptQaKqgXQiMYJsrRvhbkFvsHpQyUcZUZL1ebFjm5HOxPAgrQaN/bEqxOwwNRjSUWEMzUImg3c06JIZCzbinvudtKECgYEAkY3JF/iIPI/yglP27lKDlCfeeHSYxI3+oTKRhzSAxx8rUGidenJAXeDGDauR/T7Wpt3pGNfddBBK9Z3uC4Iq3DqUCFE4f/taj7ADAJ1Q0Vh7/28/IJM77ojr8J1cpZwNZy2o6PPxhfkagaDjqEeN9Lrs5LD4nEvDkr5CG1vOjmMCgYEAvIBFKRm31NyF8jLiCVuPwC5PzrW5iThDmsWTaXFpB3esUsbICO2pEz872oeQS+Em4GO5vXUlpbbFPzupPFhA8iMJ8TAvemhvc7oM0OZqpU6p3K4seHf6BkwLxumoA3vDJfovu9RuXVcJVOnfDnqOsltgPomWZ7xVfMkm9niL2OA=".parse().unwrap();
377        let der_key_vec: Vec<u8> = der_key.into();
378
379        // Load the two different formats and check they are the same key
380        let pem_key = PrivateKey::from_pem(pem_key_str).unwrap();
381        let der_key = PrivateKey::from_der(&Bytes::<Pkcs8PrivateKeyDerContentFormat>::from(
382            der_key_vec.clone(),
383        ))
384        .unwrap();
385        assert_eq!(pem_key.to_der().unwrap(), der_key.to_der().unwrap());
386
387        // Check that the keys can be converted back to DER
388
389        assert_eq!(der_key.to_der().unwrap().to_vec(), der_key_vec.clone());
390        assert_eq!(pem_key.to_der().unwrap().to_vec(), der_key_vec);
391    }
392
393    #[test]
394    fn test_encrypt_public_decrypt_private() {
395        let private_key: B64 = concat!(
396            "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu9xd+vmkIPoqH",
397            "NejsFZzkd1xuCn1TqGTT7ANhAEnbI/yaVt3caI30kwUC2WIToFpNgu7Ej0x2TteY",
398            "OgrLrdcC4jy1SifmKYv/v3ZZxrd/eqttmH2k588panseRwHK3LVk7xA+URhQ/bjL",
399            "gPM59V0uR1l+z1fmooeJPFz5WSXNObc9Jqnh45FND+U/UYHXTLSomTn7jgZFxJBK",
400            "veS7q6Lat7wAnYZCF2dnPmhZoJv+SKPltA8HAGsgQGWBF1p5qxV1HrAUk8kBBnG2",
401            "paj0w8p5UM6RpDdCuvKH7j1LiuWffn3b9Z4dgzmE7jsMmvzoQtypzIKaSxhqzvFO",
402            "od9V8dJdAgMBAAECggEAGGIYjOIB1rOKkDHP4ljXutI0mCRPl3FMDemiBeppoIfZ",
403            "G/Q3qpAKmndDt0Quwh/yfcNdvZhf1kwCCTWri/uPz5fSUIyDV3TaTRu0ZWoHaBVj",
404            "Hxylg+4HRZUQj+Vi50/PWr/jQmAAVMcrMfcoTl82q2ynmP/R1vM3EsXOCjTliv5B",
405            "XlMPRjj/9PDBH0dnnVcAPDOpflzOTL2f4HTFEMlmg9/tZBnd96J/cmfhjAv9XpFL",
406            "FBAFZzs5pz0rwCNSR8QZNonnK7pngVUlGDLORK58y84tGmxZhGdne3CtCWey/sJ4",
407            "7QF0Pe8YqWBU56926IY6DcSVBuQGZ6vMCNlU7J8D2QKBgQDXyh3t2TicM/n1QBLk",
408            "zLoGmVUmxUGziHgl2dnJiGDtyOAU3+yCorPgFaCie29s5qm4b0YEGxUxPIrRrEro",
409            "h0FfKn9xmr8CdmTPTcjJW1+M7bxxq7oBoU/QzKXgIHlpeCjjnvPJt0PcNkNTjCXv",
410            "shsrINh2rENoe/x79eEfM/N5eQKBgQDPkYSmYyALoNq8zq0A4BdR+F5lb5Fj5jBH",
411            "Jk68l6Uti+0hRbJ2d1tQTLkU+eCPQLGBl6fuc1i4K5FV7v14jWtRPdD7wxrkRi3j",
412            "ilqQwLBOU6Bj3FK4DvlLF+iYTuBWj2/KcxflXECmsjitKHLK6H7kFEiuJql+NAHU",
413            "U9EFXepLBQKBgQDQ+HCnZ1bFHiiP8m7Zl9EGlvK5SwlnPV9s+F1KJ4IGhCNM09UM",
414            "ZVfgR9F5yCONyIrPiyK40ylgtwqQJlOcf281I8irUXpsfg7+Gou5Q31y0r9NLUpC",
415            "Td8niyePtqMdGjouxD2+OHXFCd+FRxFt4IMi7vnxYr0csAVAXkqWlw7PsQKBgH/G",
416            "/PnQm7GM3BrOwAGB8dksJDAddkshMScblezTDYP0V43b8firkTLliCo5iNum357/",
417            "VQmdSEhXyag07yR/Kklg3H2fpbZQ3X7tdMMXW3FcWagfwWw9C4oGtdDM/Z1Lv23J",
418            "XDR9je8QV4OBGul+Jl8RfYx3kG94ZIfo8Qt0vP5hAoGARjAzdCGYz42NwaUk8n94",
419            "W2RuKHtTV9vtjaAbfPFbZoGkT7sXNJVlrA0C+9f+H9rOTM3mX59KrjmLVzde4Vhs",
420            "avWMShuK4vpAiDQLU7GyABvi5CR6Ld+AT+LSzxHhVe0ASOQPNCA2SOz3RQvgPi7R",
421            "GDgRMUB6cL3IRVzcR0dC6cY=",
422        )
423        .parse()
424        .unwrap();
425
426        let public_key: B64 = concat!(
427            "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArvcXfr5pCD6KhzXo7BWc",
428            "5Hdcbgp9U6hk0+wDYQBJ2yP8mlbd3GiN9JMFAtliE6BaTYLuxI9Mdk7XmDoKy63X",
429            "AuI8tUon5imL/792Wca3f3qrbZh9pOfPKWp7HkcByty1ZO8QPlEYUP24y4DzOfVd",
430            "LkdZfs9X5qKHiTxc+VklzTm3PSap4eORTQ/lP1GB10y0qJk5+44GRcSQSr3ku6ui",
431            "2re8AJ2GQhdnZz5oWaCb/kij5bQPBwBrIEBlgRdaeasVdR6wFJPJAQZxtqWo9MPK",
432            "eVDOkaQ3Qrryh+49S4rln3592/WeHYM5hO47DJr86ELcqcyCmksYas7xTqHfVfHS",
433            "XQIDAQAB",
434        )
435        .parse()
436        .unwrap();
437
438        let private_key = Pkcs8PrivateKeyBytes::from(private_key.as_bytes());
439        let private_key = PrivateKey::from_der(&private_key).unwrap();
440        let public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(&public_key)).unwrap();
441
442        let raw_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
443        #[expect(deprecated)]
444        let encrypted = UnsignedSharedKey::encapsulate_key_unsigned(&raw_key, &public_key).unwrap();
445        #[expect(deprecated)]
446        let decrypted = encrypted.decapsulate_key_unsigned(&private_key).unwrap();
447
448        assert_eq!(raw_key, decrypted);
449    }
450
451    #[test]
452    fn test_asymmetric_public_crypto_key_from_str() {
453        let public_key_b64 = concat!(
454            "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArvcXfr5pCD6KhzXo7BWc",
455            "5Hdcbgp9U6hk0+wDYQBJ2yP8mlbd3GiN9JMFAtliE6BaTYLuxI9Mdk7XmDoKy63X",
456            "AuI8tUon5imL/792Wca3f3qrbZh9pOfPKWp7HkcByty1ZO8QPlEYUP24y4DzOfVd",
457            "LkdZfs9X5qKHiTxc+VklzTm3PSap4eORTQ/lP1GB10y0qJk5+44GRcSQSr3ku6ui",
458            "2re8AJ2GQhdnZz5oWaCb/kij5bQPBwBrIEBlgRdaeasVdR6wFJPJAQZxtqWo9MPK",
459            "eVDOkaQ3Qrryh+49S4rln3592/WeHYM5hO47DJr86ELcqcyCmksYas7xTqHfVfHS",
460            "XQIDAQAB",
461        );
462
463        // Test FromStr
464        let parsed_key: PublicKey = public_key_b64.parse().expect("should parse");
465
466        // Verify the key can be converted back to DER and then to B64
467        let der = parsed_key.to_der().expect("should convert to DER");
468        let b64_str = B64::from(der.as_ref()).to_string();
469        assert_eq!(b64_str, public_key_b64);
470    }
471
472    #[test]
473    fn test_asymmetric_public_crypto_key_from_str_invalid() {
474        // Invalid base64
475        let result: Result<PublicKey, _> = "not-valid-base64!!!".parse();
476        assert!(result.is_err());
477
478        // Valid base64 but invalid key data
479        let result: Result<PublicKey, _> = "aGVsbG8gd29ybGQ=".parse();
480        assert!(result.is_err());
481    }
482
483    #[test]
484    fn test_asymmetric_public_crypto_key_serialize_deserialize() {
485        let public_key_b64 = concat!(
486            "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArvcXfr5pCD6KhzXo7BWc",
487            "5Hdcbgp9U6hk0+wDYQBJ2yP8mlbd3GiN9JMFAtliE6BaTYLuxI9Mdk7XmDoKy63X",
488            "AuI8tUon5imL/792Wca3f3qrbZh9pOfPKWp7HkcByty1ZO8QPlEYUP24y4DzOfVd",
489            "LkdZfs9X5qKHiTxc+VklzTm3PSap4eORTQ/lP1GB10y0qJk5+44GRcSQSr3ku6ui",
490            "2re8AJ2GQhdnZz5oWaCb/kij5bQPBwBrIEBlgRdaeasVdR6wFJPJAQZxtqWo9MPK",
491            "eVDOkaQ3Qrryh+49S4rln3592/WeHYM5hO47DJr86ELcqcyCmksYas7xTqHfVfHS",
492            "XQIDAQAB",
493        );
494
495        // Parse the key
496        let key: PublicKey = public_key_b64.parse().expect("should parse");
497
498        // Serialize to JSON
499        let serialized = serde_json::to_string(&key).expect("should serialize");
500        assert_eq!(serialized, format!("\"{}\"", public_key_b64));
501
502        // Deserialize from JSON
503        let deserialized: PublicKey =
504            serde_json::from_str(&serialized).expect("should deserialize");
505
506        // Verify the keys are equal by comparing their DER representations
507        assert_eq!(
508            key.to_der().expect("should convert to DER"),
509            deserialized.to_der().expect("should convert to DER")
510        );
511    }
512
513    #[test]
514    fn test_asymmetric_public_crypto_key_deserialize_invalid() {
515        // Invalid base64
516        let result: Result<PublicKey, _> = serde_json::from_str("\"not-valid-base64!!!\"");
517        assert!(result.is_err());
518
519        // Valid base64 but invalid key data
520        let result: Result<PublicKey, _> = serde_json::from_str("\"aGVsbG8gd29ybGQ=\"");
521        assert!(result.is_err());
522
523        // Not a string
524        let result: Result<PublicKey, _> = serde_json::from_str("123");
525        assert!(result.is_err());
526    }
527
528    // A fixed RSA-2048 key pair (PKCS#8 / SPKI base64), reused from the encrypt/decrypt test.
529    const RSA_PRIVATE_KEY_B64: &str = concat!(
530        "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu9xd+vmkIPoqH",
531        "NejsFZzkd1xuCn1TqGTT7ANhAEnbI/yaVt3caI30kwUC2WIToFpNgu7Ej0x2TteY",
532        "OgrLrdcC4jy1SifmKYv/v3ZZxrd/eqttmH2k588panseRwHK3LVk7xA+URhQ/bjL",
533        "gPM59V0uR1l+z1fmooeJPFz5WSXNObc9Jqnh45FND+U/UYHXTLSomTn7jgZFxJBK",
534        "veS7q6Lat7wAnYZCF2dnPmhZoJv+SKPltA8HAGsgQGWBF1p5qxV1HrAUk8kBBnG2",
535        "paj0w8p5UM6RpDdCuvKH7j1LiuWffn3b9Z4dgzmE7jsMmvzoQtypzIKaSxhqzvFO",
536        "od9V8dJdAgMBAAECggEAGGIYjOIB1rOKkDHP4ljXutI0mCRPl3FMDemiBeppoIfZ",
537        "G/Q3qpAKmndDt0Quwh/yfcNdvZhf1kwCCTWri/uPz5fSUIyDV3TaTRu0ZWoHaBVj",
538        "Hxylg+4HRZUQj+Vi50/PWr/jQmAAVMcrMfcoTl82q2ynmP/R1vM3EsXOCjTliv5B",
539        "XlMPRjj/9PDBH0dnnVcAPDOpflzOTL2f4HTFEMlmg9/tZBnd96J/cmfhjAv9XpFL",
540        "FBAFZzs5pz0rwCNSR8QZNonnK7pngVUlGDLORK58y84tGmxZhGdne3CtCWey/sJ4",
541        "7QF0Pe8YqWBU56926IY6DcSVBuQGZ6vMCNlU7J8D2QKBgQDXyh3t2TicM/n1QBLk",
542        "zLoGmVUmxUGziHgl2dnJiGDtyOAU3+yCorPgFaCie29s5qm4b0YEGxUxPIrRrEro",
543        "h0FfKn9xmr8CdmTPTcjJW1+M7bxxq7oBoU/QzKXgIHlpeCjjnvPJt0PcNkNTjCXv",
544        "shsrINh2rENoe/x79eEfM/N5eQKBgQDPkYSmYyALoNq8zq0A4BdR+F5lb5Fj5jBH",
545        "Jk68l6Uti+0hRbJ2d1tQTLkU+eCPQLGBl6fuc1i4K5FV7v14jWtRPdD7wxrkRi3j",
546        "ilqQwLBOU6Bj3FK4DvlLF+iYTuBWj2/KcxflXECmsjitKHLK6H7kFEiuJql+NAHU",
547        "U9EFXepLBQKBgQDQ+HCnZ1bFHiiP8m7Zl9EGlvK5SwlnPV9s+F1KJ4IGhCNM09UM",
548        "ZVfgR9F5yCONyIrPiyK40ylgtwqQJlOcf281I8irUXpsfg7+Gou5Q31y0r9NLUpC",
549        "Td8niyePtqMdGjouxD2+OHXFCd+FRxFt4IMi7vnxYr0csAVAXkqWlw7PsQKBgH/G",
550        "/PnQm7GM3BrOwAGB8dksJDAddkshMScblezTDYP0V43b8firkTLliCo5iNum357/",
551        "VQmdSEhXyag07yR/Kklg3H2fpbZQ3X7tdMMXW3FcWagfwWw9C4oGtdDM/Z1Lv23J",
552        "XDR9je8QV4OBGul+Jl8RfYx3kG94ZIfo8Qt0vP5hAoGARjAzdCGYz42NwaUk8n94",
553        "W2RuKHtTV9vtjaAbfPFbZoGkT7sXNJVlrA0C+9f+H9rOTM3mX59KrjmLVzde4Vhs",
554        "avWMShuK4vpAiDQLU7GyABvi5CR6Ld+AT+LSzxHhVe0ASOQPNCA2SOz3RQvgPi7R",
555        "GDgRMUB6cL3IRVzcR0dC6cY=",
556    );
557    const RSA_PUBLIC_KEY_B64: &str = concat!(
558        "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArvcXfr5pCD6KhzXo7BWc",
559        "5Hdcbgp9U6hk0+wDYQBJ2yP8mlbd3GiN9JMFAtliE6BaTYLuxI9Mdk7XmDoKy63X",
560        "AuI8tUon5imL/792Wca3f3qrbZh9pOfPKWp7HkcByty1ZO8QPlEYUP24y4DzOfVd",
561        "LkdZfs9X5qKHiTxc+VklzTm3PSap4eORTQ/lP1GB10y0qJk5+44GRcSQSr3ku6ui",
562        "2re8AJ2GQhdnZz5oWaCb/kij5bQPBwBrIEBlgRdaeasVdR6wFJPJAQZxtqWo9MPK",
563        "eVDOkaQ3Qrryh+49S4rln3592/WeHYM5hO47DJr86ELcqcyCmksYas7xTqHfVfHS",
564        "XQIDAQAB",
565    );
566
567    #[test]
568    fn test_thumbprint_private_matches_public() {
569        let private_key = PrivateKey::make(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
570        assert_eq!(
571            private_key.thumbprint().unwrap(),
572            private_key.to_public_key().thumbprint().unwrap()
573        );
574    }
575
576    #[test]
577    fn test_thumbprint_rsa_vector() {
578        let private_key_bytes: B64 = RSA_PRIVATE_KEY_B64.parse().unwrap();
579        let private_key =
580            PrivateKey::from_der(&Pkcs8PrivateKeyBytes::from(private_key_bytes.as_bytes()))
581                .unwrap();
582        let public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
583            RSA_PUBLIC_KEY_B64.parse::<B64>().unwrap().as_bytes(),
584        ))
585        .unwrap();
586
587        assert_eq!(
588            private_key.thumbprint().unwrap(),
589            public_key.thumbprint().unwrap()
590        );
591        assert_eq!(
592            public_key.thumbprint().unwrap().to_hex(),
593            "04fbcfa50c5805171304cc5b4794c25d77f7359d8a201828a5d7ef89162463aa"
594        );
595    }
596}