bitwarden_importers/importers/onepassword/access/
opdata.rs1use 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
18pub(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
40pub(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#[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 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
93pub(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 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 #[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 pub(super) fn decrypt(&self, encrypted: &Encrypted) -> Result<Vec<u8>, OnePasswordError> {
147 self.decrypt_with_aad(encrypted, &[])
148 }
149
150 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
169pub(super) fn decode64_loose(s: &str) -> Result<Vec<u8>, OnePasswordError> {
171 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 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}