Skip to main content

bitwarden_importers/importers/onepassword/access/
rsa.rs

1//! RSA-OAEP (SHA1/SHA256) decrypt from 1Password's JWK.
2//!
3//! This module shares its name with the external `rsa` crate, so inside the crate the dependency is
4//! reached as `::rsa`.
5
6use ::rsa::{BoxedUint, Oaep, RsaPrivateKey};
7use sha1::Sha1;
8use sha2::Sha256;
9
10use super::{
11    error::OnePasswordError,
12    opdata::{Encrypted, decode64_loose},
13    wire::RsaKeyJwk,
14};
15
16const OAEP_SHA1: &str = "RSA-OAEP";
17const OAEP_SHA256: &str = "RSA-OAEP-256";
18
19/// An RSA private key identified by its kid.
20pub(super) struct RsaKey {
21    pub id: String,
22    key: RsaPrivateKey,
23}
24
25impl RsaKey {
26    /// Builds the private key from the base64 JWK components.
27    pub(super) fn parse(jwk: &RsaKeyJwk) -> Result<RsaKey, OnePasswordError> {
28        let uint = |s: &str| -> Result<BoxedUint, OnePasswordError> {
29            Ok(BoxedUint::from_be_slice_vartime(&decode64_loose(s)?))
30        };
31
32        let key = RsaPrivateKey::from_components(
33            uint(&jwk.n)?,
34            uint(&jwk.e)?,
35            uint(&jwk.d)?,
36            vec![uint(&jwk.p)?, uint(&jwk.q)?],
37        )
38        .map_err(|_| OnePasswordError::Internal("invalid RSA key".into()))?;
39
40        Ok(RsaKey {
41            id: jwk.kid.clone(),
42            key,
43        })
44    }
45
46    /// Decrypts an envelope encrypted for this key, dispatching on the OAEP scheme.
47    pub(super) fn decrypt(&self, encrypted: &Encrypted) -> Result<Vec<u8>, OnePasswordError> {
48        if encrypted.key_id != self.id {
49            return Err(OnePasswordError::Internal("mismatching key id".into()));
50        }
51
52        let result = match encrypted.scheme.as_str() {
53            OAEP_SHA1 => self.key.decrypt(Oaep::<Sha1>::new(), &encrypted.ciphertext),
54            OAEP_SHA256 => self
55                .key
56                .decrypt(Oaep::<Sha256>::new(), &encrypted.ciphertext),
57            other => {
58                return Err(OnePasswordError::Internal(format!(
59                    "invalid encryption scheme '{other}'"
60                )));
61            }
62        };
63
64        result.map_err(|_| OnePasswordError::Decryption)
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use data_encoding::BASE64;
71
72    use super::*;
73
74    const SHA1_CIPHERTEXT: &str = "plF49e+3R0IpxBqWinosrPxWS8GdzKULvo4myIS1Gam5LCl1TmvvtntAiwOaL+/x8Ie7JApxksrpzrg9UAIaJeOJcoSzPA/hT4nn2jnglWLt+Dwz6RiEyQXhHqnyEOZ56RhNrVR8qKrnApUX2J/FWmrSYXQduIM2xbbx1LQwCGJJxCHp/pFf3Eb0fwtaw2AB5QEF5uTXOnOY+NYaPUJLKTX63uas+uPGUtdJP66WT15zHEK/WRx4ekafJvIjueSTaiceq+IVXc5niMzTMYvRb5rIEiNm3WSX7EteqaU9T46ytm9748ILQNeuGSjzIqhO4H7mO47/e8wdEh3WZk8Alg==";
75    const SHA256_CIPHERTEXT: &str = "R2wRx7neV9M/hMyWhr6heE43Q48xL+6lZuy9k03+G0FVPmXsVPRK4q7nWq6UDVwcj42nxMychMKfurCuecLEd+h5zum9Py9y6r702GnymQAl0ReM6NyjxW2m1YOp6zFVlqa69Tptn+ewOD1Fqr14yJTgVtcSJCKjQxI0ALrFst/tMvOjMFFtYPCsQ3oC0ka7kDnjbikOD0AL7Q6/19Nilr3C/TjQdNRC1Y3c5sKtyDZj++OkwgB2nac1V9IfLbpum5nqQim4UBOwE8f1axTDSYtKLJ31rr+z5bHxraUMzz96BnOmIzsZ2jj0fHrZBsBUs1L5Bg5XmGwHTz01z4HQ9A==";
76
77    fn key(fixture: &str) -> RsaKey {
78        let jwk: RsaKeyJwk = serde_json::from_str(fixture).expect("valid jwk");
79        RsaKey::parse(&jwk).expect("valid key")
80    }
81
82    fn envelope(key_id: &str, scheme: &str, ciphertext: Vec<u8>) -> Encrypted {
83        Encrypted {
84            key_id: key_id.into(),
85            scheme: scheme.into(),
86            iv: Vec::new(),
87            ciphertext,
88        }
89    }
90
91    #[test]
92    fn parses_sha1_key() {
93        assert_eq!(
94            key(include_str!("fixtures/rsa-key.json")).id,
95            "szerdhg2ww2ahjo4ilz57x7cce"
96        );
97    }
98
99    #[test]
100    fn parses_sha256_key() {
101        assert_eq!(
102            key(include_str!("fixtures/rsa-key-oaep-256.json")).id,
103            "sfaijsnbchbtznlar7mx6yrhae"
104        );
105    }
106
107    #[test]
108    fn decrypts_oaep_sha1() {
109        let key = key(include_str!("fixtures/rsa-key.json"));
110        let ciphertext = BASE64.decode(SHA1_CIPHERTEXT.as_bytes()).expect("base64");
111        let encrypted = envelope("szerdhg2ww2ahjo4ilz57x7cce", "RSA-OAEP", ciphertext);
112        let plain = key.decrypt(&encrypted).expect("decrypts");
113        assert_eq!(plain, b"All your base are belong to us");
114    }
115
116    #[test]
117    fn decrypts_oaep_sha256() {
118        let key = key(include_str!("fixtures/rsa-key-oaep-256.json"));
119        let ciphertext = BASE64.decode(SHA256_CIPHERTEXT.as_bytes()).expect("base64");
120        let encrypted = envelope("sfaijsnbchbtznlar7mx6yrhae", "RSA-OAEP-256", ciphertext);
121        let plain = key.decrypt(&encrypted).expect("decrypts");
122        assert_eq!(plain, b"All your base are belong to us");
123    }
124
125    #[test]
126    fn rejects_mismatching_key_id() {
127        let key = key(include_str!("fixtures/rsa-key.json"));
128        let encrypted = envelope("invalid-id", "RSA-OAEP", b"ciphertext".to_vec());
129        let err = key.decrypt(&encrypted).expect_err("mismatch");
130        assert!(err.to_string().contains("mismatching key id"));
131    }
132}