Skip to main content

bitwarden_importers/importers/onepassword/access/
opdata.rs

1//! AES-256-GCM "opdata" envelope with the tag appended after the ciphertext.
2//!
3//! Built on the `aes-gcm` crate and validated below against the IEEE 802.1 GCM test vectors. The
4//! tag is the trailing 16 bytes of the ciphertext, exactly as `aes-gcm` lays it out.
5
6use aes_gcm::{
7    Aes256Gcm, KeyInit,
8    aead::{Aead, Nonce, Payload},
9};
10use data_encoding::BASE64URL_NOPAD;
11use zeroize::Zeroize;
12
13use super::{error::OnePasswordError, wire::EncryptedEnvelope};
14
15const ENCRYPTION_SCHEME: &str = "A256GCM";
16const CONTAINER_TYPE: &str = "b5+jwk+json";
17
18/// AES-256-GCM encrypt, returning `ciphertext || tag`.
19pub(super) fn encrypt(
20    key: &[u8],
21    plaintext: &[u8],
22    iv: &[u8],
23    aad: &[u8],
24) -> Result<Vec<u8>, OnePasswordError> {
25    let cipher = Aes256Gcm::new_from_slice(key)
26        .map_err(|_| OnePasswordError::Internal("the key must be 32 bytes long".into()))?;
27    let nonce = Nonce::<Aes256Gcm>::try_from(iv)
28        .map_err(|_| OnePasswordError::Internal("the iv must be 12 bytes long".into()))?;
29    cipher
30        .encrypt(
31            &nonce,
32            Payload {
33                msg: plaintext,
34                aad,
35            },
36        )
37        .map_err(|_| OnePasswordError::Internal("AES-GCM encryption failed".into()))
38}
39
40/// AES-256-GCM decrypt of `ciphertext || tag`.
41pub(super) fn decrypt(
42    key: &[u8],
43    ciphertext: &[u8],
44    iv: &[u8],
45    aad: &[u8],
46) -> Result<Vec<u8>, OnePasswordError> {
47    if ciphertext.len() < 16 {
48        return Err(OnePasswordError::Internal(
49            "the ciphertext must be at least 16 bytes long".into(),
50        ));
51    }
52    let cipher = Aes256Gcm::new_from_slice(key)
53        .map_err(|_| OnePasswordError::Internal("the key must be 32 bytes long".into()))?;
54    let nonce = Nonce::<Aes256Gcm>::try_from(iv)
55        .map_err(|_| OnePasswordError::Internal("the iv must be 12 bytes long".into()))?;
56    cipher
57        .decrypt(
58            &nonce,
59            Payload {
60                msg: ciphertext,
61                aad,
62            },
63        )
64        .map_err(|_| OnePasswordError::Internal("the auth tag doesn't match".into()))
65}
66
67/// A decoded envelope: base64 fields turned into bytes.
68///
69/// The envelope's container type (`cty`) is dropped: nothing dispatches on it.
70#[derive(Debug)]
71pub(super) struct Encrypted {
72    pub key_id: String,
73    pub scheme: String,
74    pub iv: Vec<u8>,
75    pub ciphertext: Vec<u8>,
76}
77
78impl Encrypted {
79    /// Decodes the base64 `iv`/`data` fields (the `iv` is optional).
80    pub(super) fn parse(envelope: &EncryptedEnvelope) -> Result<Encrypted, OnePasswordError> {
81        Ok(Encrypted {
82            key_id: envelope.kid.clone(),
83            scheme: envelope.enc.clone(),
84            iv: match &envelope.iv {
85                Some(iv) => decode64_loose(iv)?,
86                None => Vec::new(),
87            },
88            ciphertext: decode64_loose(&envelope.data)?,
89        })
90    }
91}
92
93/// A symmetric AES-256-GCM key identified by its kid.
94pub(super) struct AesKey {
95    pub id: String,
96    pub key: Vec<u8>,
97}
98
99impl Drop for AesKey {
100    fn drop(&mut self) {
101        self.key.zeroize();
102    }
103}
104
105impl AesKey {
106    pub(super) fn new(id: impl Into<String>, key: Vec<u8>) -> AesKey {
107        AesKey { id: id.into(), key }
108    }
109
110    /// Encrypts `plaintext` into a wire envelope using the given 12-byte IV and empty associated
111    /// data.
112    pub(super) fn encrypt(
113        &self,
114        plaintext: &[u8],
115        iv: &[u8],
116    ) -> Result<EncryptedEnvelope, OnePasswordError> {
117        let ciphertext = encrypt(&self.key, plaintext, iv, &[])?;
118        Ok(EncryptedEnvelope {
119            kid: self.id.clone(),
120            enc: ENCRYPTION_SCHEME.to_string(),
121            cty: CONTAINER_TYPE.to_string(),
122            iv: Some(BASE64URL_NOPAD.encode(iv)),
123            data: BASE64URL_NOPAD.encode(&ciphertext),
124        })
125    }
126
127    /// Decrypts an envelope encrypted for this key, with empty associated data.
128    pub(super) fn decrypt(&self, encrypted: &Encrypted) -> Result<Vec<u8>, OnePasswordError> {
129        if encrypted.key_id != self.id {
130            return Err(OnePasswordError::Internal("mismatching key id".into()));
131        }
132        if encrypted.scheme != ENCRYPTION_SCHEME {
133            return Err(OnePasswordError::Internal(format!(
134                "invalid encryption scheme '{}', expected '{ENCRYPTION_SCHEME}'",
135                encrypted.scheme
136            )));
137        }
138        decrypt(&self.key, &encrypted.ciphertext, &encrypted.iv, &[])
139    }
140}
141
142/// Decodes URL-safe, standard, or mixed base64 with or without padding.
143pub(super) fn decode64_loose(s: &str) -> Result<Vec<u8>, OnePasswordError> {
144    let normalized: String = s
145        .trim_end_matches('=')
146        .chars()
147        .map(|c| match c {
148            '-' => '+',
149            '_' => '/',
150            other => other,
151        })
152        .collect();
153    data_encoding::BASE64_NOPAD
154        .decode(normalized.as_bytes())
155        .map_err(|_| OnePasswordError::Parse)
156}
157
158#[cfg(test)]
159mod tests {
160    use data_encoding::HEXLOWER;
161
162    use super::*;
163
164    fn hex(s: &str) -> Vec<u8> {
165        HEXLOWER.decode(s.as_bytes()).expect("valid hex")
166    }
167
168    // Test vectors from
169    // http://www.ieee802.org/1/files/public/docs2011/bn-randall-test-vectors-0511-v1.pdf
170    struct Vector {
171        key: &'static str,
172        plaintext: &'static str,
173        iv: &'static str,
174        adata: &'static str,
175        ciphertext: &'static str,
176        tag: &'static str,
177    }
178
179    const VECTORS: &[Vector] = &[
180        Vector {
181            key: "e3c08a8f06c6e3ad95a70557b23f75483ce33021a9c72b7025666204c69c0b72",
182            plaintext: "",
183            iv: "12153524c0895e81b2c28465",
184            adata: "d609b1f056637a0d46df998d88e5222ab2c2846512153524c0895e8108000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340001",
185            ciphertext: "",
186            tag: "2f0bc5af409e06d609ea8b7d0fa5ea50",
187        },
188        Vector {
189            key: "e3c08a8f06c6e3ad95a70557b23f75483ce33021a9c72b7025666204c69c0b72",
190            plaintext: "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a0002",
191            iv: "12153524c0895e81b2c28465",
192            adata: "d609b1f056637a0d46df998d88e52e00b2c2846512153524c0895e81",
193            ciphertext: "e2006eb42f5277022d9b19925bc419d7a592666c925fe2ef718eb4e308efeaa7c5273b394118860a5be2a97f56ab7836",
194            tag: "5ca597cdbb3edb8d1a1151ea0af7b436",
195        },
196        Vector {
197            key: "691d3ee909d7f54167fd1ca0b5d769081f2bde1aee655fdbab80bd5295ae6be7",
198            plaintext: "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340004",
199            iv: "f0761e8dcd3d000176d457ed",
200            adata: "e20106d7cd0df0761e8dcd3d88e54c2a76d457ed",
201            ciphertext: "c1623f55730c93533097addad25664966125352b43adacbd61c5ef3ac90b5bee929ce4630ea79f6ce519",
202            tag: "12af39c2d1fdc2051f8b7b3c9d397ef2",
203        },
204    ];
205
206    #[test]
207    fn encrypt_returns_ciphertext() {
208        for v in VECTORS {
209            let out = encrypt(&hex(v.key), &hex(v.plaintext), &hex(v.iv), &hex(v.adata))
210                .expect("encrypt succeeds");
211            assert_eq!(out, hex(&format!("{}{}", v.ciphertext, v.tag)));
212        }
213    }
214
215    #[test]
216    fn decrypt_returns_plaintext() {
217        for v in VECTORS {
218            let ciphertext = hex(&format!("{}{}", v.ciphertext, v.tag));
219            let out = decrypt(&hex(v.key), &ciphertext, &hex(v.iv), &hex(v.adata))
220                .expect("decrypt succeeds");
221            assert_eq!(out, hex(v.plaintext));
222        }
223    }
224
225    #[test]
226    fn decrypt_throws_on_modified_ciphertext() {
227        let v = &VECTORS[1];
228        let mut ciphertext = hex(&format!("{}{}", v.ciphertext, v.tag));
229        ciphertext[0] ^= 1;
230        let err =
231            decrypt(&hex(v.key), &ciphertext, &hex(v.iv), &hex(v.adata)).expect_err("tampered");
232        assert!(err.to_string().contains("auth tag"));
233    }
234
235    #[test]
236    fn rejects_invalid_lengths() {
237        let msg = |r: Result<Vec<u8>, OnePasswordError>| r.expect_err("invalid").to_string();
238        assert!(msg(encrypt(&[0; 13], &[0; 16], &[0; 12], &[])).contains("key must"));
239        assert!(msg(encrypt(&[0; 32], &[0; 16], &[0; 13], &[])).contains("iv must"));
240        assert!(msg(decrypt(&[0; 32], &[0; 13], &[0; 12], &[])).contains("ciphertext must"));
241        assert!(msg(decrypt(&[0; 13], &[0; 16], &[0; 12], &[])).contains("key must"));
242        assert!(msg(decrypt(&[0; 32], &[0; 16], &[0; 13], &[])).contains("iv must"));
243    }
244
245    #[test]
246    fn decrypts_opdata_envelope() {
247        let master_key = hex("44c38e8fedb84a1ab5ba74ed98dde931f6500ae39c1d9c85e20a7268ab2074f0");
248        let key = AesKey::new("mp", master_key);
249
250        let envelope: EncryptedEnvelope =
251            serde_json::from_str(include_str!("fixtures/encrypted-aes-key.json"))
252                .expect("valid fixture");
253        let encrypted = Encrypted::parse(&envelope).expect("decodes envelope");
254
255        let plaintext = String::from_utf8(key.decrypt(&encrypted).expect("decrypts"))
256            .expect("plaintext is utf8");
257        assert!(plaintext.contains("szerdhg2ww2ahjo4ilz57x7cce"));
258    }
259}