bitwarden_importers/importers/onepassword/access/
kdf.rs1use std::borrow::Cow;
4
5use data_encoding::BASE64URL_NOPAD;
6use hkdf::Hkdf;
7use icu_normalizer::DecomposingNormalizer;
8use sha2::{Digest, Sha256};
9
10use super::{account_key::AccountKey, error::OnePasswordError};
11
12pub(super) const MIN_PBKDF2_ITERATIONS: u32 = 10_000;
15
16pub(super) fn hkdf_sha256(method: &str, ikm: &[u8], salt: &[u8]) -> [u8; 32] {
18 let hk = Hkdf::<Sha256>::new(Some(salt), ikm);
19 let mut okm = [0u8; 32];
20 hk.expand(method.as_bytes(), &mut okm)
21 .expect("okm is a fixed 32 bytes, under HKDF's 255-block limit");
22 okm
23}
24
25pub(super) fn validate_pbes2(method: &str) -> Result<(), OnePasswordError> {
31 match matches!(method, "PBES2-HS256" | "PBES2g-HS256") {
32 true => Ok(()),
33 false => Err(OnePasswordError::Unsupported(format!(
34 "Method '{method}' is not supported"
35 ))),
36 }
37}
38
39pub(super) fn pbes2(password: &str, salt: &[u8], iterations: u32) -> [u8; 32] {
41 bitwarden_crypto::pbkdf2(password.as_bytes(), salt, iterations)
42}
43
44pub(super) fn normalize_password(password: &str) -> Cow<'_, str> {
45 DecomposingNormalizer::new_nfkd().normalize(password.trim())
46}
47
48pub(super) fn normalize_username(username: &str) -> String {
49 username.trim().to_lowercase()
50}
51
52pub(super) fn normalize_identity_username(username: &str) -> String {
54 DecomposingNormalizer::new_nfkd()
55 .normalize(username.trim())
56 .to_lowercase()
57}
58
59fn legacy_password(username: &str, password: &str) -> String {
61 let digest = match password.is_empty() {
62 true => String::new(),
63 false => BASE64URL_NOPAD.encode(&Sha256::digest(password.as_bytes())),
64 };
65 format!("{username}:{digest}")
66}
67
68pub(super) fn derive_master_key(
71 algorithm: &str,
72 iterations: u32,
73 salt: &[u8],
74 username: &str,
75 password: &str,
76 account_key: &AccountKey,
77) -> Result<[u8; 32], OnePasswordError> {
78 validate_pbes2(algorithm)?;
79
80 let username = normalize_username(username);
81
82 let is_legacy = algorithm.starts_with("PBES2-");
85
86 let k1;
87 let salt = if is_legacy {
88 salt
89 } else {
90 k1 = hkdf_sha256(algorithm, salt, username.as_bytes());
91 &k1
92 };
93
94 let password = if is_legacy {
95 legacy_password(&username, password)
96 } else {
97 normalize_password(password).into_owned()
98 };
99
100 let k2 = pbes2(&password, salt, iterations);
101
102 account_key.combine_with(&k2)
103}
104
105#[cfg(test)]
106mod tests {
107 use std::collections::BTreeMap;
108
109 use data_encoding::{BASE64URL_NOPAD, HEXLOWER};
110 use serde::Deserialize;
111
112 use super::*;
113
114 #[derive(Deserialize)]
116 struct Vectors {
117 params: VectorParams,
118 cases: Vec<Vector>,
119 }
120
121 #[derive(Deserialize)]
122 struct VectorParams {
123 username: String,
124 account_key: String,
125 salt_b64url: String,
126 iterations: u32,
127 }
128
129 #[derive(Deserialize)]
130 struct Vector {
131 name: String,
132 password: String,
133 password_hex: String,
134 normalized_hex: String,
135 keys: BTreeMap<String, String>,
136 }
137
138 fn vectors() -> Vectors {
139 serde_json::from_str(include_str!("fixtures/master-key-vectors.json"))
140 .expect("the vectors parse")
141 }
142
143 #[test]
144 fn vector_passwords_survived_the_trip_through_the_json_file() {
145 for case in vectors().cases {
146 assert_eq!(
147 HEXLOWER.encode(case.password.as_bytes()),
148 case.password_hex,
149 "{} was rewritten in the fixture",
150 case.name
151 );
152 }
153 }
154
155 #[test]
156 fn normalize_password_matches_the_web_client() {
157 for case in vectors().cases {
158 assert_eq!(
159 HEXLOWER.encode(normalize_password(&case.password).as_bytes()),
160 case.normalized_hex,
161 "{}",
162 case.name
163 );
164 }
165 }
166
167 fn derive_vector(params: &VectorParams, case: &Vector, algorithm: &str) -> [u8; 32] {
168 let salt = BASE64URL_NOPAD
169 .decode(params.salt_b64url.as_bytes())
170 .expect("valid salt");
171 let account_key = AccountKey::parse(¶ms.account_key).expect("valid account key");
172
173 derive_master_key(
174 algorithm,
175 params.iterations,
176 &salt,
177 ¶ms.username,
178 &case.password,
179 &account_key,
180 )
181 .expect("derivation succeeds")
182 }
183
184 #[test]
185 fn derive_master_key_matches_the_web_client() {
186 let Vectors { params, cases } = vectors();
187 for case in &cases {
188 assert!(!case.keys.is_empty(), "{} has no keys", case.name);
189 for (algorithm, expected) in &case.keys {
190 assert_eq!(
191 &HEXLOWER.encode(&derive_vector(¶ms, case, algorithm)),
192 expected,
193 "{} with {algorithm}",
194 case.name
195 );
196 }
197 }
198 }
199
200 #[test]
204 fn only_the_modern_prefix_is_blind_to_the_unicode_spelling() {
205 let Vectors { params, cases } = vectors();
206 let find = |name: &str| {
207 cases
208 .iter()
209 .find(|case| case.name == name)
210 .unwrap_or_else(|| panic!("{name} is in the fixture"))
211 };
212 let precomposed = find("precomposed_e_acute");
213 let decomposed = find("decomposed_e_acute");
214
215 assert_ne!(precomposed.password_hex, decomposed.password_hex);
216 for algorithm in precomposed.keys.keys() {
217 let one = derive_vector(¶ms, precomposed, algorithm);
218 let other = derive_vector(¶ms, decomposed, algorithm);
219 match algorithm.starts_with("PBES2g-") {
220 true => assert_eq!(one, other, "{algorithm}"),
221 false => assert_ne!(one, other, "{algorithm}"),
222 }
223 }
224 }
225
226 #[test]
227 fn normalize_username_trims_and_lowercases() {
228 assert_eq!(
229 normalize_username(" [email protected] \n"),
230 "[email protected]"
231 );
232 }
233
234 #[test]
236 fn normalize_identity_username_also_decomposes() {
237 assert_eq!(
238 HEXLOWER.encode(normalize_identity_username(" CAF\u{e9}@x.com ").as_bytes()),
239 HEXLOWER.encode("cafe\u{301}@x.com".as_bytes())
240 );
241 }
242
243 #[test]
244 fn hkdf_returns_derived_key() {
245 let derived = hkdf_sha256("PBES2g-HS256", b"ikm", b"salt");
246 assert_eq!(
247 BASE64URL_NOPAD.encode(&derived),
248 "UybCHXHHQRaFxUUR3G2ZO9CJ0H2eWJ1Ik_MpNQHrHdE"
249 );
250 }
251
252 #[test]
253 fn pbes2_returns_derived_key() {
254 assert_eq!(
255 BASE64URL_NOPAD.encode(&pbes2("password", b"salt", 100)),
256 "B-aZcYDPfxKQTwQQDUBdNIiP32KvbVBqDswjsZb-mdg"
257 );
258 }
259
260 #[test]
262 fn derive_master_key_throws_on_unsupported_method() {
263 let account_key =
264 AccountKey::parse("A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9").expect("valid account key");
265 for algorithm in ["PBES2-HS512", "PBES2g-HS512", "SRPg-4096", "Unknown", ""] {
266 let err = derive_master_key(algorithm, 100, b"salt", "user", "pw", &account_key)
267 .expect_err("unsupported");
268 assert!(matches!(err, OnePasswordError::Unsupported(_)));
269 assert!(err.to_string().contains("is not supported"));
270 }
271 }
272
273 #[test]
274 fn derive_master_key_returns_master_key() {
275 let salt = BASE64URL_NOPAD
276 .decode(b"i2enf0xq-XPKCFFf5UZqNQ")
277 .expect("valid salt");
278 let account_key =
279 AccountKey::parse("A3-RTN9SA-DY9445Y5FF96X6E7B5GPFA95R9").expect("valid account key");
280
281 let key = derive_master_key(
282 "PBES2g-HS256",
283 100000,
284 &salt,
285 "username",
286 "password",
287 &account_key,
288 )
289 .expect("derivation succeeds");
290
291 assert_eq!(
292 HEXLOWER.encode(&key),
293 "09f6cf6acc4f64f2ac6af5d912427253c4dd5e1a48dfc6bfea21df8f6d3a701e"
294 );
295 }
296}