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, Zeroizing};
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    /// Encrypts `plaintext` into a wire envelope using the given 12-byte IV, authenticating `aad`.
128    #[cfg(test)]
129    pub(super) fn encrypt_with_aad(
130        &self,
131        plaintext: &[u8],
132        iv: &[u8],
133        aad: &[u8],
134    ) -> Result<EncryptedEnvelope, OnePasswordError> {
135        let ciphertext = encrypt(&self.key, plaintext, iv, aad)?;
136        Ok(EncryptedEnvelope {
137            kid: self.id.clone(),
138            enc: ENCRYPTION_SCHEME.to_string(),
139            cty: CONTAINER_TYPE.to_string(),
140            iv: Some(BASE64URL_NOPAD.encode(iv)),
141            data: BASE64URL_NOPAD.encode(&ciphertext),
142        })
143    }
144
145    /// Decrypts an envelope encrypted for this key, with empty associated data.
146    pub(super) fn decrypt(&self, encrypted: &Encrypted) -> Result<Vec<u8>, OnePasswordError> {
147        self.decrypt_with_aad(encrypted, &[])
148    }
149
150    /// Decrypts an envelope encrypted for this key, verifying `aad`.
151    pub(super) fn decrypt_with_aad(
152        &self,
153        encrypted: &Encrypted,
154        aad: &[u8],
155    ) -> Result<Vec<u8>, OnePasswordError> {
156        if encrypted.key_id != self.id {
157            return Err(OnePasswordError::Internal("mismatching key id".into()));
158        }
159        if encrypted.scheme != ENCRYPTION_SCHEME {
160            return Err(OnePasswordError::Internal(format!(
161                "invalid encryption scheme '{}', expected '{ENCRYPTION_SCHEME}'",
162                encrypted.scheme
163            )));
164        }
165        decrypt(&self.key, &encrypted.ciphertext, &encrypted.iv, aad)
166    }
167}
168
169/// Decodes URL-safe, standard, or mixed base64 with or without padding.
170pub(super) fn decode64_loose(s: &str) -> Result<Vec<u8>, OnePasswordError> {
171    // Wiped because the input is often key material. Sized up front so growing never leaves a copy
172    // behind.
173    let unpadded = s.trim_end_matches('=');
174    let mut normalized = Zeroizing::new(String::with_capacity(unpadded.len()));
175    normalized.extend(unpadded.chars().map(|c| match c {
176        '-' => '+',
177        '_' => '/',
178        other => other,
179    }));
180    data_encoding::BASE64_NOPAD
181        .decode(normalized.as_bytes())
182        .map_err(|_| OnePasswordError::Parse)
183}
184
185#[cfg(test)]
186mod tests {
187    use data_encoding::HEXLOWER;
188
189    use super::*;
190
191    fn hex(s: &str) -> Vec<u8> {
192        HEXLOWER.decode(s.as_bytes()).expect("valid hex")
193    }
194
195    // Test vectors from
196    // http://www.ieee802.org/1/files/public/docs2011/bn-randall-test-vectors-0511-v1.pdf
197    struct Vector {
198        key: &'static str,
199        plaintext: &'static str,
200        iv: &'static str,
201        adata: &'static str,
202        ciphertext: &'static str,
203        tag: &'static str,
204    }
205
206    const VECTORS: &[Vector] = &[
207        Vector {
208            key: "e3c08a8f06c6e3ad95a70557b23f75483ce33021a9c72b7025666204c69c0b72",
209            plaintext: "",
210            iv: "12153524c0895e81b2c28465",
211            adata: "d609b1f056637a0d46df998d88e5222ab2c2846512153524c0895e8108000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340001",
212            ciphertext: "",
213            tag: "2f0bc5af409e06d609ea8b7d0fa5ea50",
214        },
215        Vector {
216            key: "e3c08a8f06c6e3ad95a70557b23f75483ce33021a9c72b7025666204c69c0b72",
217            plaintext: "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a0002",
218            iv: "12153524c0895e81b2c28465",
219            adata: "d609b1f056637a0d46df998d88e52e00b2c2846512153524c0895e81",
220            ciphertext: "e2006eb42f5277022d9b19925bc419d7a592666c925fe2ef718eb4e308efeaa7c5273b394118860a5be2a97f56ab7836",
221            tag: "5ca597cdbb3edb8d1a1151ea0af7b436",
222        },
223        Vector {
224            key: "691d3ee909d7f54167fd1ca0b5d769081f2bde1aee655fdbab80bd5295ae6be7",
225            plaintext: "08000f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233340004",
226            iv: "f0761e8dcd3d000176d457ed",
227            adata: "e20106d7cd0df0761e8dcd3d88e54c2a76d457ed",
228            ciphertext: "c1623f55730c93533097addad25664966125352b43adacbd61c5ef3ac90b5bee929ce4630ea79f6ce519",
229            tag: "12af39c2d1fdc2051f8b7b3c9d397ef2",
230        },
231    ];
232
233    #[test]
234    fn encrypt_returns_ciphertext() {
235        for v in VECTORS {
236            let out = encrypt(&hex(v.key), &hex(v.plaintext), &hex(v.iv), &hex(v.adata))
237                .expect("encrypt succeeds");
238            assert_eq!(out, hex(&format!("{}{}", v.ciphertext, v.tag)));
239        }
240    }
241
242    #[test]
243    fn decrypt_returns_plaintext() {
244        for v in VECTORS {
245            let ciphertext = hex(&format!("{}{}", v.ciphertext, v.tag));
246            let out = decrypt(&hex(v.key), &ciphertext, &hex(v.iv), &hex(v.adata))
247                .expect("decrypt succeeds");
248            assert_eq!(out, hex(v.plaintext));
249        }
250    }
251
252    #[test]
253    fn decrypt_throws_on_modified_ciphertext() {
254        let v = &VECTORS[1];
255        let mut ciphertext = hex(&format!("{}{}", v.ciphertext, v.tag));
256        ciphertext[0] ^= 1;
257        let err =
258            decrypt(&hex(v.key), &ciphertext, &hex(v.iv), &hex(v.adata)).expect_err("tampered");
259        assert!(err.to_string().contains("auth tag"));
260    }
261
262    #[test]
263    fn rejects_invalid_lengths() {
264        let msg = |r: Result<Vec<u8>, OnePasswordError>| r.expect_err("invalid").to_string();
265        assert!(msg(encrypt(&[0; 13], &[0; 16], &[0; 12], &[])).contains("key must"));
266        assert!(msg(encrypt(&[0; 32], &[0; 16], &[0; 13], &[])).contains("iv must"));
267        assert!(msg(decrypt(&[0; 32], &[0; 13], &[0; 12], &[])).contains("ciphertext must"));
268        assert!(msg(decrypt(&[0; 13], &[0; 16], &[0; 12], &[])).contains("key must"));
269        assert!(msg(decrypt(&[0; 32], &[0; 16], &[0; 13], &[])).contains("iv must"));
270    }
271
272    #[test]
273    fn envelope_round_trips_with_aad() {
274        let key = AesKey::new("kid", vec![7; 32]);
275        let envelope = key
276            .encrypt_with_aad(b"secret", &[9; 12], b"context")
277            .expect("encrypts");
278        let encrypted = Encrypted::parse(&envelope).expect("decodes envelope");
279
280        assert_eq!(
281            key.decrypt_with_aad(&encrypted, b"context")
282                .expect("decrypts"),
283            b"secret"
284        );
285        assert!(key.decrypt_with_aad(&encrypted, b"other").is_err());
286        assert!(key.decrypt(&encrypted).is_err());
287    }
288
289    #[test]
290    fn decode64_loose_accepts_either_alphabet_with_or_without_padding() {
291        for input in ["+/+/", "-_-_", "+_-/"] {
292            assert_eq!(decode64_loose(input).expect("decodes"), [0xfb, 0xff, 0xbf]);
293        }
294        for input in ["+/8=", "-_8=", "-_8"] {
295            assert_eq!(decode64_loose(input).expect("decodes"), [0xfb, 0xff]);
296        }
297    }
298
299    #[test]
300    fn decrypts_opdata_envelope() {
301        let master_key = hex("44c38e8fedb84a1ab5ba74ed98dde931f6500ae39c1d9c85e20a7268ab2074f0");
302        let key = AesKey::new("mp", master_key);
303
304        let envelope: EncryptedEnvelope =
305            serde_json::from_str(include_str!("fixtures/encrypted-aes-key.json"))
306                .expect("valid fixture");
307        let encrypted = Encrypted::parse(&envelope).expect("decodes envelope");
308
309        let plaintext = String::from_utf8(key.decrypt(&encrypted).expect("decrypts"))
310            .expect("plaintext is utf8");
311        assert!(plaintext.contains("szerdhg2ww2ahjo4ilz57x7cce"));
312    }
313}