1use std::collections::HashMap;
10use std::num::NonZeroU32;
11
12use anyhow::{Context, Result, anyhow, bail};
13use base64::Engine;
14use bitwarden_crypto::{
15 BitwardenLegacyKeyBytes, EncString, HashPurpose, Kdf, KeyDecryptable, KeyEncryptable,
16 MasterKey, OctetStreamBytes,
17};
18use hmac::{Hmac, KeyInit, Mac};
19use rsa::pkcs8::DecodePrivateKey;
20use sha2::{Digest, Sha256};
21use zeroize::Zeroizing;
22
23use crate::vault::VaultError;
24use crate::vault::models::{Prelogin, Profile, TokenResponse};
25
26pub use bitwarden_crypto::SymmetricCryptoKey;
29
30pub struct Unlocked {
36 pub user_key: SymmetricCryptoKey,
37 pub private_key: Option<rsa::RsaPrivateKey>,
38 pub org_keys: HashMap<String, SymmetricCryptoKey>,
39 pub failed_orgs: Vec<String>,
43 pub user_key_fingerprint: String,
46}
47
48impl Unlocked {
49 fn base_key<'a>(
54 &'a self,
55 cipher: &serde_json::Value,
56 ) -> Result<&'a SymmetricCryptoKey, VaultError> {
57 match cipher.get("organizationId").and_then(serde_json::Value::as_str) {
58 Some(org_id) => {
59 self.org_keys.get(org_id).ok_or_else(|| VaultError::MissingKey(org_id.to_owned()))
60 }
61 None => Ok(&self.user_key),
62 }
63 }
64
65 pub fn decrypt_item_str(
67 &self,
68 cipher: &serde_json::Value,
69 enc: &str,
70 ) -> Result<String, VaultError> {
71 let key = cipher_key(cipher, self.base_key(cipher)?)?;
72 decrypt_str(enc, &key).map_err(VaultError::Crypto)
73 }
74
75 pub fn encrypt_item_str(
77 &self,
78 cipher: &serde_json::Value,
79 value: &str,
80 ) -> Result<String, VaultError> {
81 let key = cipher_key(cipher, self.base_key(cipher)?)?;
82 encrypt_str(value, &key).map_err(VaultError::Crypto)
83 }
84
85 pub fn decrypt_user_str(&self, enc: &str) -> Result<String> {
87 decrypt_str(enc, &self.user_key)
88 }
89
90 pub fn encrypt_user_str(&self, value: &str) -> Result<String> {
92 encrypt_str(value, &self.user_key)
93 }
94
95 pub fn prepare_attachment(
98 &self,
99 cipher: &serde_json::Value,
100 name: &str,
101 data: &[u8],
102 ) -> Result<(String, String, Vec<u8>), VaultError> {
103 let item_key = cipher_key(cipher, self.base_key(cipher)?)?;
104 let attachment_key = make_symmetric_key();
105 let wrapped = wrap_symmetric_key(&attachment_key, &item_key)?;
106 let encrypted_name = encrypt_str(name, &item_key)?;
107 let blob = encrypt_attachment_blob(data, &attachment_key)?;
108 Ok((wrapped, encrypted_name, blob))
109 }
110
111 pub fn open_attachment(
114 &self,
115 cipher: &serde_json::Value,
116 wrapped_key: &str,
117 blob: &[u8],
118 ) -> Result<Vec<u8>, VaultError> {
119 let item_key = cipher_key(cipher, self.base_key(cipher)?)?;
120 let key = unwrap_attachment_key(wrapped_key, &item_key, self.private_key.as_ref())?;
121 decrypt_attachment_blob(blob, &key).map_err(VaultError::Crypto)
122 }
123}
124
125pub fn kdf_from_prelogin(pre: &Prelogin) -> Result<Kdf> {
128 let nz = |value: u32, name: &str| {
129 NonZeroU32::new(value).ok_or_else(|| anyhow!("prelogin returned {name} = 0"))
130 };
131 match pre.kdf {
132 0 => Ok(Kdf::PBKDF2 { iterations: nz(pre.kdf_iterations, "kdfIterations")? }),
133 1 => Ok(Kdf::Argon2id {
134 iterations: nz(pre.kdf_iterations, "kdfIterations")?,
135 memory: nz(
138 pre.kdf_memory.context("prelogin returned kdf = 1 without kdfMemory")?,
139 "kdfMemory",
140 )?,
141 parallelism: nz(
142 pre.kdf_parallelism.context("prelogin returned kdf = 1 without kdfParallelism")?,
143 "kdfParallelism",
144 )?,
145 }),
146 other => bail!("prelogin returned unknown kdf type {other}"),
147 }
148}
149
150pub fn kdf_label(pre: &Prelogin) -> String {
152 match pre.kdf {
153 0 => format!("PBKDF2-HMAC-SHA256, iterations={}", pre.kdf_iterations),
154 1 => format!(
155 "Argon2id, iterations={}, memory={} MiB, parallelism={}",
156 pre.kdf_iterations,
157 pre.kdf_memory.unwrap_or(0),
158 pre.kdf_parallelism.unwrap_or(0)
159 ),
160 other => format!("unknown({other})"),
161 }
162}
163
164pub fn derive_master_key(password: &str, email: &str, pre: &Prelogin) -> Result<MasterKey> {
166 let kdf = kdf_from_prelogin(pre)?;
167 MasterKey::derive(password, email, &kdf)
168 .map_err(|e| anyhow!("master key derivation failed: {e}"))
169}
170
171pub fn master_password_hash(master_key: &MasterKey, password: &str) -> String {
173 String::from(
174 master_key.derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization),
175 )
176}
177
178pub fn unlock(
180 token: &TokenResponse,
181 master_key: &MasterKey,
182 profile: &Profile,
183) -> Result<Unlocked> {
184 let wrapped_user_key = wrapped_user_key(token, profile)?;
185 let user_key: EncString =
186 wrapped_user_key.parse().map_err(|e| anyhow!("wrapped user key did not parse: {e}"))?;
187 let user_key = master_key.decrypt_user_key(user_key)?;
188 with_user_key(token, user_key, profile, &wrapped_user_key)
189}
190
191pub fn unlock_with_user_key(
200 token: &TokenResponse,
201 user_key: &str,
202 profile: &Profile,
203) -> Result<Unlocked> {
204 let user_key = SymmetricCryptoKey::try_from(user_key.to_owned())
205 .map_err(|e| anyhow!("released user key did not parse: {e}"))?;
206 let wrapped_user_key = wrapped_user_key(token, profile)?;
207 with_user_key(token, user_key, profile, &wrapped_user_key)
208}
209
210fn wrapped_user_key(token: &TokenResponse, profile: &Profile) -> Result<String> {
214 token
215 .key
216 .clone()
217 .filter(|key| !key.is_empty())
218 .or_else(|| profile.key.clone().filter(|key| !key.is_empty()))
219 .context("token response and sync profile both lack the wrapped user key")
220}
221
222fn with_user_key(
224 token: &TokenResponse,
225 user_key: SymmetricCryptoKey,
226 profile: &Profile,
227 wrapped_user_key: &str,
228) -> Result<Unlocked> {
229 let user_key_fingerprint = sha256_hex(wrapped_user_key.as_bytes());
230
231 let private_key_enc = token
234 .private_key
235 .clone()
236 .filter(|key| !key.is_empty())
237 .or_else(|| profile.private_key.clone().filter(|key| !key.is_empty()));
238 let private_key = match private_key_enc {
239 Some(enc) => {
240 let der = Zeroizing::new(
241 decrypt_bytes(&enc, &user_key).context("decrypting the private key")?,
242 );
243 Some(parse_private_key(&der)?)
244 }
245 None => None,
246 };
247
248 let (org_keys, failed_orgs) = org_keys(private_key.as_ref(), profile);
249
250 Ok(Unlocked { user_key, private_key, org_keys, failed_orgs, user_key_fingerprint })
251}
252
253fn org_keys(
257 private_key: Option<&rsa::RsaPrivateKey>,
258 profile: &Profile,
259) -> (HashMap<String, SymmetricCryptoKey>, Vec<String>) {
260 let mut keys = HashMap::new();
261 let mut failed = Vec::new();
262 let Some(private) = private_key else {
263 return (keys, profile.organizations.iter().map(|org| org.id.clone()).collect());
265 };
266 for org in &profile.organizations {
267 let Some(key) = org.key.as_deref().filter(|key| !key.is_empty()) else {
268 failed.push(org.id.clone());
269 continue;
270 };
271 match unwrap_org_key(key, private) {
272 Ok(unwrapped) => {
273 keys.insert(org.id.clone(), unwrapped);
274 }
275 Err(error) => {
276 tracing::warn!(
277 target: "forge_connectors::vault",
278 event_name = "vault_org_key_unwrap_failed",
279 org_id = %org.id,
280 error = %error,
281 "an organization key was not unwrapped",
282 );
283 failed.push(org.id.clone());
284 }
285 }
286 }
287 (keys, failed)
288}
289
290pub fn unwrap_org_key(enc: &str, private: &rsa::RsaPrivateKey) -> Result<SymmetricCryptoKey> {
293 let (kind, rest) = enc.split_once('.').context("org key has no EncString header")?;
294 match kind {
295 "4" => {}
296 "6" => bail!("org key is EncString type 6 (OAEP-SHA1 + HMAC); only type 4 is unwrapped"),
297 other => bail!("org key is EncString type {other}, not an RSA-OAEP wrap"),
298 }
299 let parts: Vec<&str> = rest.split('|').collect();
300 let data = parts.first().ok_or_else(|| anyhow!("org key has no ciphertext part"))?;
301 rsa_oaep_unwrap(data, private, "org key")
302}
303
304fn rsa_oaep_unwrap(
305 data_b64: &str,
306 private: &rsa::RsaPrivateKey,
307 what: &str,
308) -> Result<SymmetricCryptoKey> {
309 let ciphertext = base64::engine::general_purpose::STANDARD
310 .decode(data_b64)
311 .with_context(|| format!("{what} ciphertext is not base64"))?;
312 let decrypted = Zeroizing::new(
313 private
314 .decrypt(rsa::Oaep::<sha1::Sha1>::new(), &ciphertext)
315 .map_err(|e| anyhow!("RSA-OAEP unwrap failed: {e}"))?,
316 );
317 let legacy = BitwardenLegacyKeyBytes::from(decrypted.to_vec());
318 SymmetricCryptoKey::try_from(&legacy)
319 .map_err(|e| anyhow!("unwrapped {what} is not a symmetric key: {e}"))
320}
321
322pub fn enc_string_type(enc: &str) -> Option<u8> {
325 enc.split_once('.').and_then(|(kind, _)| kind.parse::<u8>().ok())
326}
327
328pub fn unwrap_attachment_key(
335 enc: &str,
336 item_key: &SymmetricCryptoKey,
337 private_key: Option<&rsa::RsaPrivateKey>,
338) -> Result<SymmetricCryptoKey, VaultError> {
339 let (kind, rest) = enc.split_once('.').context("attachment key has no EncString header")?;
340 match kind {
341 "0" | "1" | "2" => {
342 let parsed: EncString =
343 enc.parse().map_err(|e| anyhow!("attachment key EncString parse failed: {e}"))?;
344 let raw = Zeroizing::<Vec<u8>>::new(
345 parsed
346 .decrypt_with_key(item_key)
347 .map_err(|e| anyhow!("attachment key decrypt failed: {e}"))?,
348 );
349 let legacy = BitwardenLegacyKeyBytes::from(raw.to_vec());
350 SymmetricCryptoKey::try_from(&legacy).map_err(|e| {
351 VaultError::Crypto(anyhow!("unwrapped attachment key is not a symmetric key: {e}"))
352 })
353 }
354 "4" | "6" => {
355 let private_key = private_key.ok_or(VaultError::MissingPrivateKey)?;
358 let parts: Vec<&str> = rest.split('|').collect();
359 let data = parts.first().context("attachment key has no ciphertext part")?;
362 if kind == "6" {
363 let mac = parts.get(1).context("attachment key type 6 has no MAC part")?;
364 verify_type6_mac(data, mac, item_key)?;
365 }
366 rsa_oaep_unwrap(data, private_key, "attachment key").map_err(VaultError::Crypto)
367 }
368 other => Err(VaultError::Crypto(anyhow!(
369 "attachment key is EncString type {other}; no unwrap implemented"
370 ))),
371 }
372}
373
374fn verify_type6_mac(data_b64: &str, mac_b64: &str, item_key: &SymmetricCryptoKey) -> Result<()> {
375 let decode = |value: &str, what: &str| {
376 base64::engine::general_purpose::STANDARD
377 .decode(value)
378 .with_context(|| format!("{what} is not base64"))
379 };
380 let data = decode(data_b64, "type-6 ciphertext")?;
381 let mac = decode(mac_b64, "type-6 MAC")?;
382 let encoded = item_key.to_encoded();
383 let mac_key = encoded.as_ref().get(32..64).context("item key has no MAC half")?;
384 let mut hmac =
385 Hmac::<sha2::Sha256>::new_from_slice(mac_key).map_err(|e| anyhow!("HMAC init: {e}"))?;
386 hmac.update(&data);
387 hmac.verify_slice(&mac).map_err(|_| anyhow!("attachment key type-6 MAC did not verify"))
388}
389
390pub fn decrypt_str(enc: &str, key: &SymmetricCryptoKey) -> Result<String> {
391 let parsed: EncString = enc.parse().map_err(|e| anyhow!("EncString parse failed: {e}"))?;
392 parsed.decrypt_with_key(key).map_err(|e| anyhow!("EncString decrypt failed: {e}"))
393}
394
395pub fn decrypt_bytes(enc: &str, key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
396 let parsed: EncString = enc.parse().map_err(|e| anyhow!("EncString parse failed: {e}"))?;
397 parsed.decrypt_with_key(key).map_err(|e| anyhow!("EncString decrypt failed: {e}"))
398}
399
400fn parse_private_key(bytes: &[u8]) -> Result<rsa::RsaPrivateKey> {
404 if let Ok(text) = std::str::from_utf8(bytes) {
405 if text.trim_start().starts_with("-----BEGIN") {
406 return rsa::RsaPrivateKey::from_pkcs8_pem(text)
407 .context("parsing the private key as PEM");
408 }
409 if let Ok(der) = base64::engine::general_purpose::STANDARD.decode(text.trim())
410 && let Ok(key) = rsa::RsaPrivateKey::from_pkcs8_der(&Zeroizing::new(der))
411 {
412 return Ok(key);
413 }
414 }
415 rsa::RsaPrivateKey::from_pkcs8_der(bytes).context("parsing the private key as PKCS#8 DER")
416}
417
418pub fn cipher_key(
421 cipher: &serde_json::Value,
422 base: &SymmetricCryptoKey,
423) -> Result<SymmetricCryptoKey> {
424 match cipher.get("key").and_then(|v| v.as_str()) {
425 Some(enc) if !enc.is_empty() => {
426 let parsed: EncString =
427 enc.parse().map_err(|e| anyhow!("cipher key EncString parse failed: {e}"))?;
428 let raw = Zeroizing::<Vec<u8>>::new(
429 parsed
430 .decrypt_with_key(base)
431 .map_err(|e| anyhow!("cipher key decrypt failed: {e}"))?,
432 );
433 let legacy = BitwardenLegacyKeyBytes::from(raw.to_vec());
434 SymmetricCryptoKey::try_from(&legacy)
435 .map_err(|e| anyhow!("cipher key is not a symmetric key: {e}"))
436 }
437 _ => Ok(base.clone()),
438 }
439}
440
441pub fn wrap_symmetric_key(
442 key: &SymmetricCryptoKey,
443 wrapping: &SymmetricCryptoKey,
444) -> Result<String> {
445 let bytes = BitwardenLegacyKeyBytes::from(key.to_encoded().as_ref().to_vec());
446 (&bytes)
447 .encrypt_with_key(wrapping)
448 .map(|enc| enc.to_string())
449 .map_err(|e| anyhow!("wrapping a symmetric key failed: {e}"))
450}
451
452pub fn unwrap_symmetric_key(
453 enc: &str,
454 wrapping: &SymmetricCryptoKey,
455) -> Result<SymmetricCryptoKey> {
456 let parsed: EncString =
457 enc.parse().map_err(|e| anyhow!("wrapped key EncString parse failed: {e}"))?;
458 let raw = Zeroizing::<Vec<u8>>::new(
459 parsed
460 .decrypt_with_key(wrapping)
461 .map_err(|e| anyhow!("wrapped key decrypt failed: {e}"))?,
462 );
463 let legacy = BitwardenLegacyKeyBytes::from(raw.to_vec());
464 SymmetricCryptoKey::try_from(&legacy)
465 .map_err(|e| anyhow!("unwrapped key is not a symmetric key: {e}"))
466}
467
468pub fn encrypt_str(value: &str, key: &SymmetricCryptoKey) -> Result<String> {
469 value
470 .encrypt_with_key(key)
471 .map(|enc| enc.to_string())
472 .map_err(|e| anyhow!("EncString encrypt failed: {e}"))
473}
474
475pub struct Totp {
477 pub code: String,
478 pub period: u64,
479 pub remaining: u64,
480}
481
482pub fn totp_from_uri(uri: &str, now: std::time::SystemTime) -> Result<Totp> {
486 let (secret, digits, period, algorithm) = if uri.trim_start().starts_with("otpauth://") {
487 let parsed = url::Url::parse(uri.trim()).context("parsing the otpauth URI")?;
488 let pairs: std::collections::HashMap<_, _> = parsed.query_pairs().into_owned().collect();
489 let secret = pairs.get("secret").cloned().context("otpauth URI has no secret")?;
490 let digits = pairs.get("digits").and_then(|v| v.parse().ok()).unwrap_or(6u32);
491 let period = pairs.get("period").and_then(|v| v.parse().ok()).unwrap_or(30u64);
492 let algorithm =
493 pairs.get("algorithm").map_or_else(|| "SHA1".to_owned(), |v| v.to_uppercase());
494 (secret, digits, period, algorithm)
495 } else {
496 (uri.trim().to_owned(), 6u32, 30u64, "SHA1".to_owned())
497 };
498 if !(6..=8).contains(&digits) {
501 bail!("TOTP digits out of range: {digits}");
502 }
503 if period == 0 {
504 bail!("TOTP period is zero");
505 }
506 let key = decode_base32(&secret)?;
507 let unix =
508 now.duration_since(std::time::UNIX_EPOCH).context("system clock is before 1970")?.as_secs();
509 let counter = unix / period;
510 let remaining = period - (unix % period);
511
512 let digest = match algorithm.as_str() {
513 "SHA1" => {
514 let mut mac =
515 Hmac::<sha1::Sha1>::new_from_slice(&key).map_err(|e| anyhow!("HMAC init: {e}"))?;
516 mac.update(&counter.to_be_bytes());
517 mac.finalize().into_bytes().to_vec()
518 }
519 "SHA256" => {
520 let mut mac = Hmac::<sha2::Sha256>::new_from_slice(&key)
521 .map_err(|e| anyhow!("HMAC init: {e}"))?;
522 mac.update(&counter.to_be_bytes());
523 mac.finalize().into_bytes().to_vec()
524 }
525 "SHA512" => {
526 let mut mac = Hmac::<sha2::Sha512>::new_from_slice(&key)
527 .map_err(|e| anyhow!("HMAC init: {e}"))?;
528 mac.update(&counter.to_be_bytes());
529 mac.finalize().into_bytes().to_vec()
530 }
531 other => bail!("unsupported TOTP algorithm: {other}"),
532 };
533
534 let offset = usize::from(digest[digest.len() - 1] & 0x0f);
535 let binary = (u32::from(digest[offset] & 0x7f) << 24)
536 | (u32::from(digest[offset + 1]) << 16)
537 | (u32::from(digest[offset + 2]) << 8)
538 | u32::from(digest[offset + 3]);
539 let code = binary % 10u32.pow(digits);
540 Ok(Totp { code: format!("{code:0width$}", width = digits as usize), period, remaining })
541}
542
543fn decode_base32(secret: &str) -> Result<Vec<u8>> {
544 let cleaned: String = secret
545 .chars()
546 .filter(|c| !c.is_whitespace() && *c != '=')
547 .collect::<String>()
548 .to_uppercase();
549 data_encoding::BASE32_NOPAD.decode(cleaned.as_bytes()).context("TOTP secret is not base32")
550}
551
552pub fn encrypt_attachment_blob(data: &[u8], key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
555 let typed = OctetStreamBytes::from(data.to_vec());
556 let encrypted =
557 (&typed).encrypt_with_key(key).map_err(|e| anyhow!("attachment encrypt failed: {e}"))?;
558 wire_from_enc_string(&encrypted)
559}
560
561fn wire_from_enc_string(encrypted: &EncString) -> Result<Vec<u8>> {
562 match encrypted {
563 EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data } => {
564 let mut out = Vec::with_capacity(49 + data.len());
565 out.push(0x02);
566 out.extend_from_slice(iv);
567 out.extend_from_slice(mac);
568 out.extend_from_slice(data);
569 Ok(out)
570 }
571 _ => bail!("attachment encryption did not produce an AES-256-CBC-HMAC string"),
572 }
573}
574
575pub fn decrypt_attachment_blob(wire: &[u8], key: &SymmetricCryptoKey) -> Result<Vec<u8>> {
577 if wire.first() != Some(&0x02) {
578 bail!("unknown attachment format header (expected 0x02)");
579 }
580 if wire.len() < 49 + 16 {
581 bail!("attachment blob is too short to be valid");
582 }
583 let iv: [u8; 16] = wire[1..17].try_into().map_err(|_| anyhow!("bad IV length"))?;
584 let mac: [u8; 32] = wire[17..49].try_into().map_err(|_| anyhow!("bad MAC length"))?;
585 let data = wire[49..].to_vec();
586 let encrypted = EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data };
587 encrypted.decrypt_with_key(key).map_err(|e| anyhow!("attachment decrypt failed: {e}"))
588}
589
590pub fn sha256_hex(data: &[u8]) -> String {
591 data_encoding::HEXLOWER.encode(&Sha256::digest(data))
592}
593
594pub fn make_symmetric_key() -> SymmetricCryptoKey {
597 SymmetricCryptoKey::make(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac)
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603 use bitwarden_crypto::SymmetricKeyAlgorithm;
604 use rsa::pkcs8::EncodePrivateKey;
605
606 #[test]
609 fn master_password_hash_matches_the_reference_vector() {
610 let pre =
611 Prelogin { kdf: 0, kdf_iterations: 100_000, kdf_memory: None, kdf_parallelism: None };
612 let master_key = derive_master_key("asdfasdf", "test@bitwarden.com", &pre).unwrap();
613 assert_eq!(
614 master_password_hash(&master_key, "asdfasdf"),
615 "wmyadRMyBZOH7P/a/ucTCbSghKgdzDpPqUnu/DAVtSw="
616 );
617 }
618
619 #[test]
620 fn email_is_trimmed_and_lowercased_as_the_salt() {
621 let pre =
622 Prelogin { kdf: 0, kdf_iterations: 100_000, kdf_memory: None, kdf_parallelism: None };
623 let a = derive_master_key("asdfasdf", "test@bitwarden.com", &pre).unwrap();
624 let b = derive_master_key("asdfasdf", " TEST@bitwarden.com ", &pre).unwrap();
625 assert_eq!(master_password_hash(&a, "asdfasdf"), master_password_hash(&b, "asdfasdf"));
626 }
627
628 #[test]
629 fn unknown_kdf_is_a_named_error() {
630 let pre = Prelogin { kdf: 7, kdf_iterations: 1, kdf_memory: None, kdf_parallelism: None };
631 assert!(derive_master_key("x", "y", &pre).is_err());
632 }
633
634 #[test]
636 fn unlock_parses_a_private_key_stored_as_raw_der() {
637 let pre =
638 Prelogin { kdf: 0, kdf_iterations: 100_000, kdf_memory: None, kdf_parallelism: None };
639 let master_key = derive_master_key("asdfasdf", "test@bitwarden.com", &pre).unwrap();
640 let (user_key, wrapped_user_key) = master_key.make_user_key().unwrap();
641 let user_key = user_key.0;
642
643 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 1024).unwrap();
644 let der = rsa_key.to_pkcs8_der().unwrap();
645 let der_bytes = BitwardenLegacyKeyBytes::from(der.as_bytes().to_vec());
646 let wrapped_private_key = (&der_bytes).encrypt_with_key(&user_key).unwrap().to_string();
647
648 let token = TokenResponse {
649 access_token: "token".to_owned(),
650 refresh_token: None,
651 key: Some(wrapped_user_key.to_string()),
652 private_key: Some(wrapped_private_key),
653 };
654 let unlocked = unlock(&token, &master_key, &Profile::default()).unwrap();
655 assert!(unlocked.private_key.is_some());
656 }
657
658 #[test]
662 fn unlock_with_a_released_user_key_matches_the_password_unlock() {
663 let pre =
664 Prelogin { kdf: 0, kdf_iterations: 100_000, kdf_memory: None, kdf_parallelism: None };
665 let master_key = derive_master_key("asdfasdf", "test@bitwarden.com", &pre).unwrap();
666 let (user_key, wrapped_user_key) = master_key.make_user_key().unwrap();
667 let user_key = user_key.0;
668
669 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 1024).unwrap();
670 let der = rsa_key.to_pkcs8_der().unwrap();
671 let der_bytes = BitwardenLegacyKeyBytes::from(der.as_bytes().to_vec());
672 let wrapped_private_key = (&der_bytes).encrypt_with_key(&user_key).unwrap().to_string();
673 let token = TokenResponse {
674 access_token: "token".to_owned(),
675 refresh_token: None,
676 key: Some(wrapped_user_key.to_string()),
677 private_key: Some(wrapped_private_key),
678 };
679
680 let from_password =
681 unlock(&token, &master_key, &Profile::default()).expect("the password unlock");
682 let released = user_key.to_base64().to_string();
683 let from_release = unlock_with_user_key(&token, &released, &Profile::default())
684 .expect("the release unlock");
685
686 assert_eq!(from_release.user_key, from_password.user_key, "the same user key");
687 assert_eq!(
688 from_release.user_key_fingerprint, from_password.user_key_fingerprint,
689 "the same wrapped-key fingerprint, which the rotation check reads",
690 );
691 assert!(from_release.private_key.is_some(), "the private key unwraps from the release");
692 assert_eq!(from_release.failed_orgs, from_password.failed_orgs);
693 }
694
695 #[test]
698 fn a_released_user_key_that_is_not_a_key_is_refused() {
699 let token = TokenResponse {
700 access_token: "t".to_owned(),
701 refresh_token: None,
702 key: Some("2.x|y|z".to_owned()),
703 private_key: None,
704 };
705 assert!(unlock_with_user_key(&token, "not a key", &Profile::default()).is_err());
706 }
707
708 #[test]
712 fn a_mismatched_but_valid_user_key_is_refused() {
713 let pre =
714 Prelogin { kdf: 0, kdf_iterations: 100_000, kdf_memory: None, kdf_parallelism: None };
715 let account = derive_master_key("asdfasdf", "test@bitwarden.com", &pre).unwrap();
716 let (user_key, wrapped_user_key) = account.make_user_key().unwrap();
717 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 1024).unwrap();
718 let der = rsa_key.to_pkcs8_der().unwrap();
719 let der_bytes = BitwardenLegacyKeyBytes::from(der.as_bytes().to_vec());
720 let wrapped_private_key = (&der_bytes).encrypt_with_key(&user_key.0).unwrap().to_string();
721 let token = TokenResponse {
722 access_token: "token".to_owned(),
723 refresh_token: None,
724 key: Some(wrapped_user_key.to_string()),
725 private_key: Some(wrapped_private_key),
726 };
727
728 let elsewhere = derive_master_key("another password", "other@example.com", &pre).unwrap();
729 let (wrong, _) = elsewhere.make_user_key().unwrap();
730 let refused =
731 unlock_with_user_key(&token, &wrong.0.to_base64().to_string(), &Profile::default());
732
733 assert!(refused.is_err(), "a valid key from another account is refused");
734 }
735
736 #[test]
739 fn an_empty_token_private_key_falls_back_to_the_profile_key() {
740 let pre =
741 Prelogin { kdf: 0, kdf_iterations: 100_000, kdf_memory: None, kdf_parallelism: None };
742 let master_key = derive_master_key("asdfasdf", "test@bitwarden.com", &pre).unwrap();
743 let (user_key, wrapped_user_key) = master_key.make_user_key().unwrap();
744 let user_key = user_key.0;
745
746 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 1024).unwrap();
747 let der = rsa_key.to_pkcs8_der().unwrap();
748 let der_bytes = BitwardenLegacyKeyBytes::from(der.as_bytes().to_vec());
749 let wrapped_private_key = (&der_bytes).encrypt_with_key(&user_key).unwrap().to_string();
750
751 let token = TokenResponse {
752 access_token: "token".to_owned(),
753 refresh_token: None,
754 key: Some(wrapped_user_key.to_string()),
755 private_key: Some(String::new()),
756 };
757 let profile = Profile { private_key: Some(wrapped_private_key), ..Profile::default() };
758
759 let unlocked = unlock(&token, &master_key, &profile).expect("the profile key is used");
760
761 assert!(unlocked.private_key.is_some(), "the profile private key was unwrapped");
762 }
763
764 #[test]
767 fn org_key_unwrap_round_trips_a_type_4_wrap() {
768 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 2048).unwrap();
769 let public_key = rsa_key.to_public_key();
770 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
771 let encoded = key.to_encoded();
772 let ciphertext = public_key
773 .encrypt(&mut bitwarden_random::rng(), rsa::Oaep::<sha1::Sha1>::new(), encoded.as_ref())
774 .unwrap();
775
776 let data = base64::engine::general_purpose::STANDARD.encode(ciphertext);
777 let unwrapped = unwrap_org_key(&format!("4.{data}"), &rsa_key).unwrap();
778
779 assert_eq!(unwrapped, key);
780 }
781
782 #[test]
783 fn org_key_of_a_non_rsa_type_is_refused() {
784 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 1024).unwrap();
785 let err = unwrap_org_key("2.abc|def|ghi", &rsa_key).unwrap_err().to_string();
786 assert!(err.contains("type 2"), "unexpected error: {err}");
787 }
788
789 #[test]
790 fn totp_matches_the_rfc_6238_sha1_vector() {
791 let at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(59);
794 let totp = totp_from_uri(
795 "otpauth://totp/spike?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&digits=8&period=30",
796 at,
797 )
798 .unwrap();
799 assert_eq!(totp.code, "94287082");
800 assert_eq!(totp.period, 30);
801 assert_eq!(totp.remaining, 1);
802 }
803
804 #[test]
805 fn totp_rejects_a_non_base32_secret() {
806 let at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(59);
807 assert!(totp_from_uri("otpauth://totp/x?secret=not!base32", at).is_err());
808 }
809
810 #[test]
813 fn totp_rejects_a_digit_count_outside_six_to_eight() {
814 let at = std::time::UNIX_EPOCH + std::time::Duration::from_secs(59);
815 let uri = |digits: u32| {
816 format!("otpauth://totp/x?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&digits={digits}")
817 };
818 let overflow = totp_from_uri(&uri(10), at).err().expect("ten digits must not overflow");
819 assert!(
820 overflow.to_string().contains("digits out of range"),
821 "the refusal names the bound: {overflow}"
822 );
823 assert!(totp_from_uri(&uri(5), at).is_err());
824 assert!(totp_from_uri(&uri(6), at).is_ok());
825 assert!(totp_from_uri(&uri(8), at).is_ok());
826 }
827
828 #[test]
831 fn argon2id_requires_its_parameters() {
832 let missing =
833 Prelogin { kdf: 1, kdf_iterations: 3, kdf_memory: None, kdf_parallelism: Some(4) };
834 assert!(derive_master_key("x", "y", &missing).is_err());
835 let missing_parallelism =
836 Prelogin { kdf: 1, kdf_iterations: 3, kdf_memory: Some(64), kdf_parallelism: None };
837 assert!(derive_master_key("x", "y", &missing_parallelism).is_err());
838 let complete =
839 Prelogin { kdf: 1, kdf_iterations: 3, kdf_memory: Some(64), kdf_parallelism: Some(4) };
840 assert!(derive_master_key("x", "y", &complete).is_ok());
841 }
842
843 #[test]
846 fn an_empty_token_key_falls_back_to_the_profile_key() {
847 let pre =
848 Prelogin { kdf: 0, kdf_iterations: 100_000, kdf_memory: None, kdf_parallelism: None };
849 let master_key = derive_master_key("asdfasdf", "test@bitwarden.com", &pre).unwrap();
850 let (user_key, wrapped_user_key) = master_key.make_user_key().unwrap();
851 drop(user_key);
852
853 let token = TokenResponse {
854 access_token: "token".to_owned(),
855 refresh_token: None,
856 key: Some(String::new()),
857 private_key: None,
858 };
859 let profile = Profile { key: Some(wrapped_user_key.to_string()), ..Profile::default() };
860
861 let unlocked = unlock(&token, &master_key, &profile).expect("the profile key is used");
862
863 assert!(unlocked.failed_orgs.is_empty());
864 }
865
866 #[test]
869 fn an_organization_cipher_without_its_key_is_refused() {
870 let unlocked = Unlocked {
871 user_key: SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac),
872 private_key: None,
873 org_keys: HashMap::new(),
874 failed_orgs: vec!["org-1".to_owned()],
875 user_key_fingerprint: String::new(),
876 };
877 let org_cipher = serde_json::json!({ "organizationId": "org-1", "name": "x" });
878
879 let error = unlocked.decrypt_item_str(&org_cipher, "2.aGk=|ZGF0YQ==|bWFj").unwrap_err();
880
881 assert!(matches!(&error, VaultError::MissingKey(org_id) if org_id == "org-1"), "{error}");
882
883 let personal = serde_json::json!({ "name": "x" });
884 let enc = encrypt_str("hello", &unlocked.user_key).unwrap();
885 assert_eq!(unlocked.decrypt_item_str(&personal, &enc).unwrap(), "hello");
886 }
887
888 #[test]
891 fn a_failed_org_key_is_recorded_by_id() {
892 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 1024).unwrap();
893 let profile = Profile {
894 organizations: vec![crate::vault::models::Organization {
895 id: "org-1".to_owned(),
896 key: Some("2.aGk=|ZGF0YQ==|bWFj".to_owned()),
897 name: None,
898 }],
899 ..Profile::default()
900 };
901
902 let (keys, failed) = org_keys(Some(&rsa_key), &profile);
903
904 assert!(keys.is_empty());
905 assert_eq!(failed, vec!["org-1"]);
906 }
907
908 #[test]
909 fn attachment_blob_round_trips_and_rejects_tampering() {
910 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
911 let data = b"vault attachment contents, round trip";
912 let wire = encrypt_attachment_blob(data, &key).unwrap();
913 assert_eq!(wire[0], 0x02);
914 assert_eq!(decrypt_attachment_blob(&wire, &key).unwrap(), data);
915
916 let mut tampered = wire.clone();
917 let last = tampered.len() - 1;
918 tampered[last] ^= 0xff;
919 assert!(decrypt_attachment_blob(&tampered, &key).is_err());
920 }
921
922 #[test]
925 fn attachment_wire_layout_is_discriminator_iv_mac_ciphertext() {
926 let key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
927 let data = b"layout";
928 let typed = OctetStreamBytes::from(data.to_vec());
929 let encrypted = (&typed).encrypt_with_key(&key).unwrap();
930 let EncString::Aes256Cbc_HmacSha256_B64 { iv, mac, data: ciphertext } = &encrypted else {
931 panic!("expected an AES-256-CBC-HMAC EncString");
932 };
933 let mut expected = vec![0x02];
934 expected.extend_from_slice(iv);
935 expected.extend_from_slice(mac);
936 expected.extend_from_slice(ciphertext);
937
938 assert_eq!(wire_from_enc_string(&encrypted).unwrap(), expected);
939 assert_eq!(&expected[1..17], iv.as_slice());
940 assert_eq!(&expected[17..49], mac.as_slice());
941
942 let live_shape = encrypt_attachment_blob(&[b'x'; 47], &key).unwrap();
943 assert_eq!(live_shape.len(), 97);
944 assert_eq!(live_shape[0], 0x02);
945 assert_eq!(decrypt_attachment_blob(&live_shape, &key).unwrap(), vec![b'x'; 47]);
946 }
947
948 #[test]
949 fn attachment_key_unwraps_when_aes_wrapped_as_type_2() {
950 let item_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
951 let attachment_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
952 let wrapped = wrap_symmetric_key(&attachment_key, &item_key).unwrap();
953 let unwrapped = unwrap_attachment_key(&wrapped, &item_key, None).unwrap();
954 assert_eq!(unwrapped, attachment_key);
955 }
956
957 #[test]
960 fn attachment_key_unwraps_when_rsa_wrapped_as_type_4() {
961 use base64::Engine;
962 let item_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
963 let attachment_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
964 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 2048).unwrap();
965 let public = rsa_key.to_public_key();
966 let encoded = attachment_key.to_encoded();
967 let ciphertext = public
968 .encrypt(&mut bitwarden_random::rng(), rsa::Oaep::<sha1::Sha1>::new(), encoded.as_ref())
969 .unwrap();
970 let data = base64::engine::general_purpose::STANDARD.encode(ciphertext);
971 let wrapped = format!("4.{data}");
972
973 let unwrapped = unwrap_attachment_key(&wrapped, &item_key, Some(&rsa_key)).unwrap();
974 assert_eq!(unwrapped, attachment_key);
975 }
976
977 #[test]
980 fn attachment_key_unwraps_when_rsa_wrapped_as_type_6_and_rejects_a_bad_mac() {
981 use base64::Engine;
982 use hmac::{Hmac, KeyInit, Mac};
983
984 let item_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
985 let attachment_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
986 let rsa_key = rsa::RsaPrivateKey::new(&mut bitwarden_random::rng(), 2048).unwrap();
987 let public = rsa_key.to_public_key();
988 let encoded = attachment_key.to_encoded();
989 let ciphertext = public
990 .encrypt(&mut bitwarden_random::rng(), rsa::Oaep::<sha1::Sha1>::new(), encoded.as_ref())
991 .unwrap();
992 let composite = item_key.to_encoded();
993 let mut hmac = Hmac::<sha2::Sha256>::new_from_slice(&composite.as_ref()[32..64]).unwrap();
994 hmac.update(&ciphertext);
995 let mac = hmac.finalize().into_bytes();
996 let engine = base64::engine::general_purpose::STANDARD;
997 let wrapped = format!("6.{}|{}", engine.encode(&ciphertext), engine.encode(mac));
998
999 let unwrapped = unwrap_attachment_key(&wrapped, &item_key, Some(&rsa_key)).unwrap();
1000 assert_eq!(unwrapped, attachment_key);
1001
1002 let mut bad_mac = mac.to_vec();
1003 bad_mac[0] ^= 0xff;
1004 let bad = format!("6.{}|{}", engine.encode(&ciphertext), engine.encode(bad_mac));
1005 let error = unwrap_attachment_key(&bad, &item_key, Some(&rsa_key))
1006 .expect_err("a tampered MAC is refused");
1007 assert!(
1008 error.to_string().contains("MAC did not verify"),
1009 "the refusal names its own reason: {error}"
1010 );
1011 }
1012
1013 #[test]
1016 fn an_rsa_wrapped_attachment_key_without_a_private_key_is_named() {
1017 let item_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac);
1018
1019 let error = unwrap_attachment_key("4.aGk=", &item_key, None).unwrap_err();
1020
1021 assert!(matches!(error, VaultError::MissingPrivateKey), "{error}");
1022 }
1023
1024 #[test]
1025 fn enc_string_type_reads_the_header_byte_only() {
1026 assert_eq!(enc_string_type("2.aGk=|ZGF0YQ==|bWFj"), Some(2));
1027 assert_eq!(enc_string_type("4.aGk=|ZGF0YQ=="), Some(4));
1028 assert_eq!(enc_string_type("not-an-encstring"), None);
1029 }
1030
1031 #[test]
1032 fn sha256_hex_matches_the_known_digest_of_abc() {
1033 assert_eq!(
1034 sha256_hex(b"abc"),
1035 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
1036 );
1037 }
1038}