├── .github ├── codeowners ├── funding.yml └── workflows │ └── cd.yml ├── havelock.js ├── crypto.js ├── browser.js ├── browsers ├── chrome-beta.js ├── chrome-dev.js ├── chrome-canary.js ├── chromium.js ├── brave.js └── chrome.js ├── test.js ├── cli ├── cli.js └── actions.js ├── license.md ├── crypto ├── win32.js ├── darwin.js └── linux.js ├── package.json ├── .gitignore ├── readme.md └── explorer.js /.github/codeowners: -------------------------------------------------------------------------------- 1 | * @phoqe -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: phoqe 2 | -------------------------------------------------------------------------------- /havelock.js: -------------------------------------------------------------------------------- 1 | const explorer = require("./explorer"); 2 | const browser = require("./browser"); 3 | const crypto = require("./crypto"); 4 | 5 | exports.explorer = explorer; 6 | exports.browser = browser; 7 | exports.crypto = crypto; 8 | -------------------------------------------------------------------------------- /crypto.js: -------------------------------------------------------------------------------- 1 | if (process.platform === "win32") { 2 | module.exports = require("./crypto/win32"); 3 | 4 | return; 5 | } 6 | 7 | if (process.platform === "darwin") { 8 | module.exports = require("./crypto/darwin"); 9 | 10 | return; 11 | } 12 | 13 | if (process.platform === "linux") { 14 | module.exports = require("./crypto/linux"); 15 | 16 | return; 17 | } 18 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | // Chromium 2 | exports.chromium = require("./browsers/chromium"); 3 | 4 | // Google Chrome 5 | exports.chrome = require("./browsers/chrome"); 6 | exports.chromeBeta = require("./browsers/chrome-beta"); 7 | exports.chromeDev = require("./browsers/chrome-dev"); 8 | exports.chromeCanary = require("./browsers/chrome-canary"); 9 | 10 | // Brave 11 | exports.brave = require("./browsers/brave"); 12 | -------------------------------------------------------------------------------- /browsers/chrome-beta.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | exports.id = "chrome-beta"; 4 | exports.name = "Google Chrome Beta"; 5 | exports.keychain = { 6 | service: "Chrome Safe Storage", 7 | account: "Chrome", 8 | }; 9 | 10 | exports.userDataDirectoryPath = () => { 11 | let userDataDirectoryPath; 12 | 13 | if (process.platform === "linux") { 14 | userDataDirectoryPath = path.join( 15 | process.env.HOME, 16 | ".config", 17 | "google-chrome-beta" 18 | ); 19 | } 20 | 21 | return userDataDirectoryPath; 22 | }; 23 | -------------------------------------------------------------------------------- /browsers/chrome-dev.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | exports.id = "chrome-dev"; 4 | exports.name = "Google Chrome Dev"; 5 | exports.keychain = { 6 | service: "Chrome Safe Storage", 7 | account: "Chrome", 8 | }; 9 | 10 | exports.userDataDirectoryPath = () => { 11 | let userDataDirectoryPath; 12 | 13 | if (process.platform === "linux") { 14 | userDataDirectoryPath = path.join( 15 | process.env.HOME, 16 | ".config", 17 | "google-chrome-unstable" 18 | ); 19 | } 20 | 21 | return userDataDirectoryPath; 22 | }; 23 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const havelock = require("./havelock"); 2 | 3 | const explorer = havelock.explorer; 4 | const browser = havelock.browser; 5 | const crypto = havelock.crypto; 6 | 7 | explorer 8 | .dataFromUddFile(browser.chrome, "Default", "Login Data", "logins") 9 | .then((logins) => { 10 | logins.forEach((login) => { 11 | crypto 12 | .decrypt(browser.chrome, login.password_value) 13 | .then((value) => { 14 | console.log(value); 15 | }) 16 | .catch((reason) => { 17 | console.error(reason); 18 | }); 19 | }); 20 | }) 21 | .catch((reason) => { 22 | console.error(reason); 23 | }); 24 | -------------------------------------------------------------------------------- /browsers/chrome-canary.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | exports.id = "chrome-canary"; 4 | exports.name = "Google Chrome Canary"; 5 | exports.keychain = { 6 | service: "Chrome Safe Storage", 7 | account: "Chrome", 8 | }; 9 | 10 | exports.userDataDirectoryPath = () => { 11 | let userDataDirectoryPath; 12 | 13 | if (process.platform === "win32") { 14 | userDataDirectoryPath = path.join( 15 | process.env.LOCALAPPDATA, 16 | "Google", 17 | "Chrome SxS", 18 | "User Data" 19 | ); 20 | } 21 | 22 | if (process.platform === "darwin") { 23 | userDataDirectoryPath = path.join( 24 | process.env.HOME, 25 | "Library", 26 | "Application Support", 27 | "Google", 28 | "Chrome Canary" 29 | ); 30 | } 31 | 32 | return userDataDirectoryPath; 33 | }; 34 | -------------------------------------------------------------------------------- /browsers/chromium.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | exports.id = "chromium"; 4 | exports.name = "Chromium"; 5 | exports.keychain = { 6 | service: "Chromium Safe Storage", 7 | account: "Chromium", 8 | }; 9 | 10 | exports.userDataDirectoryPath = () => { 11 | let userDataDirectoryPath; 12 | 13 | if (process.platform === "win32") { 14 | userDataDirectoryPath = path.join( 15 | process.env.LOCALAPPDATA, 16 | "Chromium", 17 | "User Data" 18 | ); 19 | } 20 | 21 | if (process.platform === "darwin") { 22 | userDataDirectoryPath = path.join( 23 | process.env.HOME, 24 | "Library", 25 | "Application Support", 26 | "Chromium" 27 | ); 28 | } 29 | 30 | if (process.platform === "linux") { 31 | userDataDirectoryPath = path.join(process.env.HOME, ".config", "chromium"); 32 | } 33 | 34 | return userDataDirectoryPath; 35 | }; 36 | -------------------------------------------------------------------------------- /browsers/brave.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | exports.id = "brave"; 4 | exports.name = "Brave"; 5 | exports.keychain = { 6 | service: "Brave Safe Storage", 7 | account: "Brave", 8 | }; 9 | 10 | exports.userDataDirectoryPath = () => { 11 | let userDataDirectoryPath; 12 | 13 | if (process.platform === "win32") { 14 | userDataDirectoryPath = path.join( 15 | process.env.LOCALAPPDATA, 16 | "BraveSoftware", 17 | "Brave-Browser", 18 | "User Data" 19 | ); 20 | } 21 | 22 | if (process.platform === "darwin") { 23 | userDataDirectoryPath = path.join( 24 | process.env.HOME, 25 | "Library", 26 | "Application Support", 27 | "BraveSoftware", 28 | "Brave-Browser" 29 | ); 30 | } 31 | 32 | if (process.platform === "linux") { 33 | userDataDirectoryPath = path.join(process.env.HOME, ".config", "brave"); 34 | } 35 | 36 | return userDataDirectoryPath; 37 | }; 38 | -------------------------------------------------------------------------------- /browsers/chrome.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | exports.id = "chrome"; 4 | exports.name = "Google Chrome"; 5 | exports.keychain = { 6 | service: "Chrome Safe Storage", 7 | account: "Chrome", 8 | }; 9 | 10 | exports.userDataDirectoryPath = () => { 11 | let userDataDirectoryPath; 12 | 13 | if (process.platform === "win32") { 14 | userDataDirectoryPath = path.join( 15 | process.env.LOCALAPPDATA, 16 | "Google", 17 | "Chrome", 18 | "User Data" 19 | ); 20 | } 21 | 22 | if (process.platform === "darwin") { 23 | userDataDirectoryPath = path.join( 24 | process.env.HOME, 25 | "Library", 26 | "Application Support", 27 | "Google", 28 | "Chrome" 29 | ); 30 | } 31 | 32 | if (process.platform === "linux") { 33 | userDataDirectoryPath = path.join( 34 | process.env.HOME, 35 | ".config", 36 | "google-chrome" 37 | ); 38 | } 39 | 40 | return userDataDirectoryPath; 41 | }; 42 | -------------------------------------------------------------------------------- /cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { program } = require("commander"); 4 | 5 | const package = require("../package.json"); 6 | const actions = require("./actions"); 7 | 8 | /** 9 | * Configuration 10 | */ 11 | 12 | program.name(package.name); 13 | program.version(package.version); 14 | program.description(package.description); 15 | 16 | /** 17 | * Options 18 | */ 19 | 20 | program.option("-t, --tabular", "present data of interest in a table", false); 21 | program.option("-f, --file", "write requested data to file", false); 22 | program.option("-d, --decrypt", "decrypt fields known to be encrypted", false); 23 | 24 | /** 25 | * Actions 26 | */ 27 | 28 | program 29 | .command("logins [profile]") 30 | .alias("accounts") 31 | .action((browser, profile) => actions.logins(browser, profile)); 32 | 33 | program 34 | .command("cookies [profile]") 35 | .action((browser, profile) => actions.cookies(browser, profile)); 36 | 37 | program 38 | .command("urls [profile]") 39 | .alias("history") 40 | .action((browser, profile) => actions.urls(browser, profile)); 41 | 42 | program.parse(process.argv); 43 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | **Copyright © 2021 Linus Långberg** 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v1 17 | 18 | - id: yarn-cache-directory-path 19 | run: echo "::set-output name=dir::$(yarn cache dir)" 20 | 21 | - uses: actions/cache@v1 22 | id: yarn-cache 23 | with: 24 | path: ${{ steps.yarn-cache-directory-path.outputs.dir }} 25 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-yarn- 28 | 29 | - name: Install dependencies 30 | run: yarn install 31 | 32 | - name: Release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | GIT_AUTHOR_NAME: Linus Långberg 37 | GIT_AUTHOR_EMAIL: git@phoqe.dev 38 | GIT_COMMITTER_NAME: phoqe 39 | GIT_COMITTER_EMAIL: git@phoqe.dev 40 | run: npx semantic-release 41 | -------------------------------------------------------------------------------- /crypto/win32.js: -------------------------------------------------------------------------------- 1 | // Core modules 2 | const crypto = require("crypto"); 3 | 4 | // Third-party modules 5 | const hkdf = require("futoin-hkdf"); 6 | 7 | // Password Versions 8 | const PASSWORD_V10 = "v10"; 9 | 10 | // Encryption Key 11 | const KEY_SECRET = "peanuts"; 12 | const KEY_LEN = 256 / 8; // 32 bytes 13 | const KEY_SALT = "salt"; 14 | const KEY_INFO = "info"; 15 | const KEY_HASH = "sha-256"; 16 | 17 | const NONCE_LEN = 96 / 8; // 12 bytes 18 | const DEC_ALGO = "aes-256-gcm"; 19 | 20 | /** 21 | * 22 | * @returns {Buffer} 23 | */ 24 | const createEncryptionKey = () => { 25 | return hkdf(KEY_SECRET, KEY_LEN, { 26 | salt: KEY_SALT, 27 | info: KEY_INFO, 28 | hash: KEY_HASH, 29 | }); 30 | }; 31 | 32 | /** 33 | * 34 | * @param {string} ciphertext 35 | * @returns {string} 36 | */ 37 | const decryptWithDpApi = (ciphertext) => {}; 38 | 39 | /** 40 | * 41 | * @param {object} browser 42 | * @param {Buffer} data 43 | * @returns {Promise} 44 | */ 45 | exports.decrypt = (browser, data) => { 46 | return new Promise((resolve, reject) => { 47 | if (!browser) { 48 | reject(new TypeError("No browser.")); 49 | 50 | return; 51 | } 52 | 53 | if (!data) { 54 | reject(new TypeError("No data.")); 55 | 56 | return; 57 | } 58 | 59 | const ciphertext = data.toString(); 60 | 61 | if (ciphertext.startsWith(PASSWORD_V10)) { 62 | // TODO: Unprotect data. 63 | 64 | return; 65 | } 66 | 67 | const key = createEncryptionKey(); 68 | const nonce = ciphertext.substring(PASSWORD_V10.length - 1, NONCE_LEN); 69 | const rawCiphertext = ciphertext.substring( 70 | NONCE_LEN + PASSWORD_V10.length - 1 71 | ); 72 | const decipher = crypto.createDecipheriv(DEC_ALGO, key, iv); 73 | }); 74 | }; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "havelock", 3 | "version": "2.4.0", 4 | "description": "Extract accounts, cookies, and history from Chromium-based web browsers.", 5 | "keywords": [ 6 | "chromium", 7 | "chrome", 8 | "google", 9 | "web", 10 | "browser", 11 | "password", 12 | "cookies", 13 | "history" 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/phoqe/havelock/issues", 17 | "email": "phoqe@phoqe.dev" 18 | }, 19 | "license": "MIT", 20 | "main": "havelock.js", 21 | "author": { 22 | "name": "Linus Långberg", 23 | "email": "phoqe@phoqe.dev", 24 | "url": "https://phoqe.me" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/phoqe/havelock.git" 29 | }, 30 | "private": false, 31 | "dependencies": { 32 | "commander": "^7.2.0", 33 | "futoin-hkdf": "^1.3.3", 34 | "keytar": "^7.2.0", 35 | "prettier": "2.2.1", 36 | "sqlite3": "^5.0.0" 37 | }, 38 | "scripts": { 39 | "format": "prettier --write --ignore-unknown ." 40 | }, 41 | "devDependencies": { 42 | "@semantic-release/git": "^9.0.0", 43 | "husky": "^4.2.1", 44 | "lint-staged": "^11.0.0", 45 | "semantic-release": "^17.4.3" 46 | }, 47 | "husky": { 48 | "hooks": { 49 | "pre-commit": "lint-staged" 50 | } 51 | }, 52 | "bin": { 53 | "havelock": "./cli/cli.js" 54 | }, 55 | "release": { 56 | "tagFormat": "${version}", 57 | "plugins": [ 58 | "@semantic-release/commit-analyzer", 59 | "@semantic-release/release-notes-generator", 60 | "@semantic-release/npm", 61 | "@semantic-release/git", 62 | [ 63 | "@semantic-release/github", 64 | { 65 | "successComment": false, 66 | "failComment": false, 67 | "failTitle": false, 68 | "labels": false, 69 | "releasedLabels": false 70 | } 71 | ] 72 | ] 73 | }, 74 | "lint-staged": { 75 | "*": [ 76 | "prettier --write --ignore-unknown" 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crypto/darwin.js: -------------------------------------------------------------------------------- 1 | // Core modules 2 | const crypto = require("crypto"); 3 | 4 | // Third-party modules 5 | const keytar = require("keytar"); 6 | 7 | // Password Versions 8 | const PASSWORD_V10 = "v10"; 9 | 10 | // Encryption Key 11 | const KEY_SALT = "saltysalt"; 12 | const KEY_ITERS = 1003; 13 | const KEY_LEN_BYTES = 16; 14 | const KEY_DIGEST = "sha1"; 15 | 16 | // Decipher 17 | const DEC_ALGO = "aes-128-cbc"; 18 | 19 | // Initialization Vector 20 | const IV_BLOCK_SIZE = 16; 21 | 22 | /** 23 | * Retrieve the password used in Chromium's cryptography logic, i.e., when encrypting and decrypting strings. 24 | * The password is stored in a form of key storage solution. 25 | * 26 | * @param {object} browser Decides which service and account to use in the key storage solution. 27 | * @returns {Promise} Resolved with the password string or rejected with an error. 28 | */ 29 | const getPassword = (browser) => { 30 | return new Promise((resolve, reject) => { 31 | if (!browser) { 32 | reject(); 33 | 34 | return; 35 | } 36 | 37 | keytar 38 | .getPassword(browser.keychain.service, browser.keychain.account) 39 | .then((password) => { 40 | resolve(password); 41 | }) 42 | .catch((reason) => { 43 | reject(reason); 44 | }); 45 | }); 46 | }; 47 | 48 | /** 49 | * Create an encryption key for the specified `browser`. 50 | * 51 | * @param {object} browser 52 | * @param {string} version 53 | * @returns {Promise} 54 | */ 55 | const createEncryptionKey = (browser) => { 56 | return new Promise((resolve, reject) => { 57 | if (!browser) { 58 | reject(); 59 | 60 | return; 61 | } 62 | 63 | getPassword(browser) 64 | .then((password) => { 65 | if (!password) { 66 | reject(); 67 | 68 | return; 69 | } 70 | 71 | crypto.pbkdf2( 72 | password, 73 | KEY_SALT, 74 | KEY_ITERS, 75 | KEY_LEN_BYTES, 76 | KEY_DIGEST, 77 | (err, derivedKey) => { 78 | if (err) { 79 | reject(err); 80 | 81 | return; 82 | } 83 | 84 | resolve(derivedKey); 85 | } 86 | ); 87 | }) 88 | .catch((reason) => { 89 | reject(reason); 90 | }); 91 | }); 92 | }; 93 | 94 | /** 95 | * Decrypt data from a buffer to a plaintext string using the defined algorithm. 96 | * 97 | * @param {object} browser 98 | * @param {Buffer} data 99 | * @returns {Promise} 100 | */ 101 | exports.decrypt = (browser, data) => { 102 | return new Promise((resolve, reject) => { 103 | if (!browser || !data) { 104 | reject(); 105 | 106 | return; 107 | } 108 | 109 | // Check that the incoming data was encrypted and with what version. 110 | // Credit card numbers are current legacy unencrypted data at the time of writing. 111 | // So false match with prefix won't happen. 112 | if (data.toString().indexOf(PASSWORD_V10) !== 0) { 113 | // If the prefix is not found then we'll assume we're dealing with old data. 114 | // It's saved as clear text and we'll return it directly. 115 | resolve(data); 116 | 117 | return; 118 | } 119 | 120 | createEncryptionKey(browser) 121 | .then((encryptionKey) => { 122 | if (!encryptionKey) { 123 | reject(); 124 | 125 | return; 126 | } 127 | 128 | const iv = Buffer.alloc(IV_BLOCK_SIZE, "20", "hex"); 129 | const decipher = crypto.createDecipheriv(DEC_ALGO, encryptionKey, iv); 130 | const ciphertext = Buffer.from(data).toString("base64"); 131 | const rawCiphertext = ciphertext.substring(PASSWORD_V10.length + 1); 132 | let plaintext = decipher.update(rawCiphertext, "base64", "utf8"); 133 | 134 | plaintext += decipher.final("utf8"); 135 | 136 | resolve(plaintext); 137 | }) 138 | .catch((reason) => { 139 | reject(reason); 140 | }); 141 | }); 142 | }; 143 | -------------------------------------------------------------------------------- /crypto/linux.js: -------------------------------------------------------------------------------- 1 | // Core modules 2 | const crypto = require("crypto"); 3 | 4 | // Third-party modules 5 | const keytar = require("keytar"); 6 | 7 | // Password Versions 8 | const PASSWORD_V10 = "v10"; 9 | const PASSWORD_V11 = "v11"; 10 | 11 | // Encryption Key 12 | const KEY_SALT = "saltysalt"; 13 | const KEY_ITERS = 1; 14 | const KEY_LEN_BYTES = 16; 15 | const KEY_DIGEST = "sha1"; 16 | 17 | // Decipher 18 | const DEC_ALGO = "aes-128-cbc"; 19 | 20 | // Initialization Vector 21 | const IV_BLOCK_SIZE = 16; 22 | 23 | /** 24 | * Retrieve the password used in Chromium's cryptography logic, i.e., when encrypting and decrypting strings. 25 | * The password is stored in a form of key storage solution. 26 | * 27 | * @param {object} browser Decides which service and account to use in the key storage solution. 28 | * @param {string} version Determines whether we should retrieve the password through the key storage solution or just use the hardcoded password. 29 | * @returns {Promise} Resolved with the password string or rejected with an error. 30 | */ 31 | const getPassword = (browser, version) => { 32 | return new Promise((resolve, reject) => { 33 | if (!browser) { 34 | reject(new TypeError("No browser.")); 35 | 36 | return; 37 | } 38 | 39 | if (!version) { 40 | reject(new TypeError("No version.")); 41 | 42 | return; 43 | } 44 | 45 | if (version === PASSWORD_V10) { 46 | resolve("peanuts"); 47 | 48 | return; 49 | } 50 | 51 | if (version === PASSWORD_V11) { 52 | const service = browser.keychain.service; 53 | const account = browser.keychain.account; 54 | 55 | keytar 56 | .getPassword(service, account) 57 | .then((password) => { 58 | resolve(password); 59 | }) 60 | .catch((reason) => { 61 | reject(reason); 62 | }); 63 | } 64 | }); 65 | }; 66 | 67 | /** 68 | * Create an encryption key for the specified `browser`. 69 | * 70 | * @param {object} browser 71 | * @param {string} version 72 | * @returns {Promise} 73 | */ 74 | const createEncryptionKey = (browser, version) => { 75 | return new Promise((resolve, reject) => { 76 | if (!browser) { 77 | reject(new TypeError("No browser.")); 78 | 79 | return; 80 | } 81 | 82 | if (!version) { 83 | reject(new TypeError("No version.")); 84 | 85 | return; 86 | } 87 | 88 | getPassword(browser, version) 89 | .then((password) => { 90 | if (!password) { 91 | reject(new TypeError("No password.")); 92 | 93 | return; 94 | } 95 | 96 | crypto.pbkdf2( 97 | password, 98 | KEY_SALT, 99 | KEY_ITERS, 100 | KEY_LEN_BYTES, 101 | KEY_DIGEST, 102 | (error, key) => { 103 | if (error) { 104 | reject(error); 105 | 106 | return; 107 | } 108 | 109 | resolve(key); 110 | } 111 | ); 112 | }) 113 | .catch((reason) => { 114 | reject(reason); 115 | }); 116 | }); 117 | }; 118 | 119 | /** 120 | * Decrypt data from a buffer to a plaintext string using the defined algorithm. 121 | * 122 | * @param {object} browser 123 | * @param {Buffer} data 124 | * @returns {Promise} 125 | */ 126 | exports.decrypt = (browser, data) => { 127 | return new Promise((resolve, reject) => { 128 | if (!browser) { 129 | reject(new TypeError("No browser.")); 130 | 131 | return; 132 | } 133 | 134 | if (!data) { 135 | reject(new TypeError("No data.")); 136 | 137 | return; 138 | } 139 | 140 | const dataString = data.toString(); 141 | let version; 142 | 143 | // Check that the incoming data was encrypted and with what version. 144 | // Credit card numbers are current legacy unencrypted data at the time of writing. 145 | // So false match with prefix won't happen. 146 | if (dataString.startsWith(PASSWORD_V10)) { 147 | version = PASSWORD_V10; 148 | } else if (dataString.startsWith(PASSWORD_V11)) { 149 | version = PASSWORD_V11; 150 | } else { 151 | // If the prefix is not found then we'll assume we're dealing with old data. 152 | // It's saved as clear text and we'll return it directly. 153 | resolve(data); 154 | 155 | return; 156 | } 157 | 158 | createEncryptionKey(browser, version) 159 | .then((key) => { 160 | if (!key) { 161 | reject(new TypeError("No key.")); 162 | 163 | return; 164 | } 165 | 166 | const iv = Buffer.alloc(IV_BLOCK_SIZE, "20", "hex"); 167 | const decipher = crypto.createDecipheriv(DEC_ALGO, key, iv); 168 | const ciphertext = Buffer.from(data).toString("base64"); 169 | const rawCiphertext = ciphertext.substring(version.length + 1); 170 | let plaintext = decipher.update(rawCiphertext, "base64", "utf8"); 171 | 172 | plaintext += decipher.final("utf8"); 173 | 174 | resolve(plaintext); 175 | }) 176 | .catch((reason) => { 177 | reject(reason); 178 | }); 179 | }); 180 | }; 181 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,macos,webstorm,visualstudiocode 3 | # Edit at https://www.gitignore.io/?templates=node,macos,webstorm,visualstudiocode 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Node ### 34 | # Logs 35 | logs 36 | *.log 37 | npm-debug.log* 38 | yarn-debug.log* 39 | yarn-error.log* 40 | lerna-debug.log* 41 | 42 | # Diagnostic reports (https://nodejs.org/api/report.html) 43 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 44 | 45 | # Runtime data 46 | pids 47 | *.pid 48 | *.seed 49 | *.pid.lock 50 | 51 | # Directory for instrumented libs generated by jscoverage/JSCover 52 | lib-cov 53 | 54 | # Coverage directory used by tools like istanbul 55 | coverage 56 | *.lcov 57 | 58 | # nyc test coverage 59 | .nyc_output 60 | 61 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 62 | .grunt 63 | 64 | # Bower dependency directory (https://bower.io/) 65 | bower_components 66 | 67 | # node-waf configuration 68 | .lock-wscript 69 | 70 | # Compiled binary addons (https://nodejs.org/api/addons.html) 71 | build/Release 72 | 73 | # Dependency directories 74 | node_modules/ 75 | jspm_packages/ 76 | 77 | # TypeScript v1 declaration files 78 | typings/ 79 | 80 | # TypeScript cache 81 | *.tsbuildinfo 82 | 83 | # Optional npm cache directory 84 | .npm 85 | 86 | # Optional eslint cache 87 | .eslintcache 88 | 89 | # Optional REPL history 90 | .node_repl_history 91 | 92 | # Output of 'npm pack' 93 | *.tgz 94 | 95 | # Yarn Integrity file 96 | .yarn-integrity 97 | 98 | # dotenv environment variables file 99 | .env 100 | .env.test 101 | 102 | # parcel-bundler cache (https://parceljs.org/) 103 | .cache 104 | 105 | # next.js build output 106 | .next 107 | 108 | # nuxt.js build output 109 | .nuxt 110 | 111 | # rollup.js default build output 112 | dist/ 113 | 114 | # Uncomment the public line if your project uses Gatsby 115 | # https://nextjs.org/blog/next-9-1#public-directory-support 116 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 117 | # public 118 | 119 | # Storybook build outputs 120 | .out 121 | .storybook-out 122 | 123 | # vuepress build output 124 | .vuepress/dist 125 | 126 | # Serverless directories 127 | .serverless/ 128 | 129 | # FuseBox cache 130 | .fusebox/ 131 | 132 | # DynamoDB Local files 133 | .dynamodb/ 134 | 135 | # Temporary folders 136 | tmp/ 137 | temp/ 138 | 139 | ### VisualStudioCode ### 140 | .vscode/* 141 | !.vscode/settings.json 142 | !.vscode/tasks.json 143 | !.vscode/launch.json 144 | !.vscode/extensions.json 145 | 146 | ### VisualStudioCode Patch ### 147 | # Ignore all local history of files 148 | .history 149 | 150 | ### WebStorm ### 151 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 152 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 153 | 154 | # User-specific stuff 155 | .idea/**/workspace.xml 156 | .idea/**/tasks.xml 157 | .idea/**/usage.statistics.xml 158 | .idea/**/dictionaries 159 | .idea/**/shelf 160 | 161 | # Generated files 162 | .idea/**/contentModel.xml 163 | 164 | # Sensitive or high-churn files 165 | .idea/**/dataSources/ 166 | .idea/**/dataSources.ids 167 | .idea/**/dataSources.local.xml 168 | .idea/**/sqlDataSources.xml 169 | .idea/**/dynamic.xml 170 | .idea/**/uiDesigner.xml 171 | .idea/**/dbnavigator.xml 172 | 173 | # Gradle 174 | .idea/**/gradle.xml 175 | .idea/**/libraries 176 | 177 | # Gradle and Maven with auto-import 178 | # When using Gradle or Maven with auto-import, you should exclude module files, 179 | # since they will be recreated, and may cause churn. Uncomment if using 180 | # auto-import. 181 | # .idea/modules.xml 182 | # .idea/*.iml 183 | # .idea/modules 184 | # *.iml 185 | # *.ipr 186 | 187 | # CMake 188 | cmake-build-*/ 189 | 190 | # Mongo Explorer plugin 191 | .idea/**/mongoSettings.xml 192 | 193 | # File-based project format 194 | *.iws 195 | 196 | # IntelliJ 197 | out/ 198 | 199 | # mpeltonen/sbt-idea plugin 200 | .idea_modules/ 201 | 202 | # JIRA plugin 203 | atlassian-ide-plugin.xml 204 | 205 | # Cursive Clojure plugin 206 | .idea/replstate.xml 207 | 208 | # Crashlytics plugin (for Android Studio and IntelliJ) 209 | com_crashlytics_export_strings.xml 210 | crashlytics.properties 211 | crashlytics-build.properties 212 | fabric.properties 213 | 214 | # Editor-based Rest Client 215 | .idea/httpRequests 216 | 217 | # Android studio 3.1+ serialized cache file 218 | .idea/caches/build_file_checksums.ser 219 | 220 | ### WebStorm Patch ### 221 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 222 | 223 | # *.iml 224 | # modules.xml 225 | # .idea/misc.xml 226 | # *.ipr 227 | 228 | # Sonarlint plugin 229 | .idea/**/sonarlint/ 230 | 231 | # SonarQube Plugin 232 | .idea/**/sonarIssues.xml 233 | 234 | # Markdown Navigator plugin 235 | .idea/**/markdown-navigator.xml 236 | .idea/**/markdown-navigator/ 237 | 238 | # End of https://www.gitignore.io/api/node,macos,webstorm,visualstudiocode 239 | 240 | logins.json 241 | urls.json 242 | cookies.json -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Havelock 2 | 3 | Extract and decrypt accounts, cookies, and history from web browsers based on Chromium. Havelock was initially developed as part of a remote administration tool for harvesting accounts from a computer and sending them to a remote endpoint securely. It’s now available as an API in JavaScript and a standalone CLI. 4 | 5 | ## Verified Web Browsers 6 | 7 | Every web browser using the same storage mechanism for user data is supported. These are the verified web browsers: 8 | 9 | | Name | API | Platform(s) | 10 | | -------------------- | -------------- | --------------------- | 11 | | Chromium | `chromium` | Windows, macOS, Linux | 12 | | Google Chrome Stable | `chrome` | Windows, macOS, Linux | 13 | | Google Chrome Beta | `chromeBeta` | Linux | 14 | | Google Chrome Dev | `chromeDev` | Linux | 15 | | Google Chrome Canary | `chromeCanary` | Windows, macOS | 16 | | Brave Stable | `brave` | Windows, macOS, Linux | 17 | 18 | ### Adding a browser 19 | 20 | Feel free to add support for more browsers through a Pull Request. To get started, take a look at the existing browser definitions in [`/browsers`](browsers). The gist of adding a browser is simple. You need to figure out the Keychain credentials and provide a path resolution that works on Windows, macOS, and Linux. 21 | 22 | ## String Decryption 23 | 24 | You can decrypt strings retrieved from your web browser using Havelock. 25 | 26 | | Platform | Algorithm | Supported | Source | 27 | | -------- | ----------- | --------- | ----------------------------------------------------------------------------------------------------------------------- | 28 | | Windows | AES-256-GCM | No | [`os_crypt_win.cc`](https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_win.cc) | 29 | | macOS | AES-128-CBC | Yes | [`os_crypt_mac.mm`](https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm) | 30 | | Linux | AES-128-CBC | Yes | [`os_crypt_linux.cc`](https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_linux.cc) | 31 | 32 | ## API 33 | 34 | The Havelock API is available in JavaScript. You can only use it from a Node.js environment. 35 | 36 | ### Installation 37 | 38 | Havelock is available as `havelock` in npm. Use your favorite package manager to install it for your Node.js project: 39 | 40 | ```sh 41 | yarn add havelock 42 | ``` 43 | 44 | ### Usage 45 | 46 | Using the Havelock API is quick and easy. 47 | 48 | #### Extracting data 49 | 50 | Here’s an example of retrieving data from the `logins` table in the `Login Data` file of the `Default` profile in Google Chrome: 51 | 52 | ```js 53 | const havelock = require("havelock"); 54 | 55 | const explorer = havelock.explorer; 56 | const browser = havelock.browser; 57 | 58 | explorer 59 | .dataFromUserDataDirectoryFile(browser.chrome, "Default", "Login Data", "logins") 60 | .then((logins) => { 61 | console.info(logins); 62 | }) 63 | .catch((reason) => { 64 | console.error(reason); 65 | }); 66 | ``` 67 | 68 | There are also shorthands available for interesting files. You can achieve the same result using this shorter function: 69 | 70 | ```js 71 | explorer 72 | .logins(browser.chrome, "Default") 73 | .then((logins) => { 74 | console.log(logins); 75 | }) 76 | .catch((reason) => { 77 | console.error(reason); 78 | }); 79 | ``` 80 | 81 | #### Decrypting data 82 | 83 | Havelock can decrypt passwords and credit cards numbers. Here’s an example of decrypting a password from the `logins` table in the `Login Data` file of the `Default` profile of Google Chrome: 84 | 85 | ```js 86 | const crypto = havelock.crypto; 87 | 88 | explorer 89 | .dataFromUserDataDirectoryFile(browser.chrome, "Default", "Login Data", "logins") 90 | .then((logins) => { 91 | logins.forEach((login) => { 92 | crypto 93 | .decrypt(browser.chrome, login.password_value) 94 | .then((value) => { 95 | console.log(value); 96 | }) 97 | .catch((reason) => { 98 | console.error(reason); 99 | }); 100 | }); 101 | }) 102 | .catch((reason) => { 103 | console.error(reason); 104 | }); 105 | ``` 106 | 107 | ## CLI 108 | 109 | Havelock is also available as a standalone CLI. It can be used separately to execute commands on the local machine. 110 | 111 | ### Installation 112 | 113 | The Havelock CLI can be included by using your favorite package manager to install it globally: 114 | 115 | ```sh 116 | yarn global add havelock 117 | ``` 118 | 119 | ### Usage 120 | 121 | The command `havelock` should now be available globally throughout your system. You can see the commands and options with: 122 | 123 | ```sh 124 | havelock --help 125 | ``` 126 | 127 | ### Extracting data 128 | 129 | You can retrieve your logins from the default profile in Google Chrome with: 130 | 131 | ```sh 132 | havelock logins chrome default 133 | ``` 134 | 135 | If you want a more filtered version of the output, i.e. interesting data points, you can use the option `-t`: 136 | 137 | ```sh 138 | havelock logins chrome default -t 139 | ``` 140 | 141 | ### Decrypting data 142 | 143 | Use the option `-d` if you want to decrypt fields known to be encrypted. 144 | 145 | ## Attribution 146 | 147 | Thank you to David Sheldrick ([ds300](https://github.com/ds300)) for passing on the package name. 148 | 149 | ## License 150 | 151 | MIT 152 | -------------------------------------------------------------------------------- /explorer.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const os = require("os"); 4 | const sqlite3 = require("sqlite3"); 5 | 6 | const package = require("./package.json"); 7 | 8 | const TEMP_DIR_PREFIX = `${package.name}-`; 9 | 10 | /** 11 | * Copy data file to a temporary directory and try reading the file again. 12 | * This function can be used to mitigate `SQLITE_BUSY` errors by moving them 13 | * from where they were being used at the time. 14 | * 15 | * @param {string} filePath Path to the original data file being read by a browser. 16 | * @param {string} table Table that was being accessed at the time of the error. 17 | */ 18 | const dataFromTempFile = (filePath, table) => { 19 | return new Promise((resolve, reject) => { 20 | if (!filePath) { 21 | reject(new TypeError("No file path.")); 22 | 23 | return; 24 | } 25 | 26 | const prefix = path.join(os.tmpdir(), TEMP_DIR_PREFIX); 27 | 28 | fs.mkdtemp(prefix, (error, dirPath) => { 29 | if (error) { 30 | reject(error); 31 | 32 | return; 33 | } 34 | 35 | const fileName = path.basename(filePath); 36 | const destPath = path.join(dirPath, fileName); 37 | 38 | fs.copyFile(filePath, destPath, (error) => { 39 | if (error) { 40 | reject(error); 41 | 42 | return; 43 | } 44 | 45 | exports 46 | .dataFromFile(filePath, table) 47 | .then((rows) => { 48 | resolve(rows); 49 | }) 50 | .catch((reason) => { 51 | reject(reason); 52 | }); 53 | }); 54 | }); 55 | }); 56 | }; 57 | 58 | /** 59 | * Extract rows from a given table using a database and a file path for error handling. 60 | * 61 | * @param {sqlite3.Database} db An open SQLite database, preferably from a file. 62 | * @param {string} table Table to use when selecting the rows. 63 | * @param {string} filePath Path to the file being read. 64 | * @returns {Promise} 65 | */ 66 | const rowsFromTable = (db, table, filePath) => { 67 | return new Promise((resolve, reject) => { 68 | db.all(`SELECT * FROM ${table}`, (error, rows) => { 69 | if (error) { 70 | if (error.code === "SQLITE_BUSY") { 71 | dataFromTempFile(filePath, table) 72 | .then((rows) => { 73 | resolve(rows); 74 | }) 75 | .catch((reason) => { 76 | reject(reason); 77 | }); 78 | 79 | return; 80 | } 81 | 82 | reject(error); 83 | 84 | return; 85 | } 86 | 87 | resolve(rows); 88 | }); 89 | }); 90 | }; 91 | 92 | /** 93 | * Extracts data from an SQLite file located at `filePath` using the SQL statement `SELECT * FROM table`. 94 | * 95 | * @param filePath {string} The path to an SQLite file. 96 | * @param table {string} The table to use in the `SELECT` statement, i.e. the table to extract data from. 97 | * @returns {Promise} The rows selected from `table`. 98 | */ 99 | exports.dataFromFile = (filePath, table) => { 100 | return new Promise((resolve, reject) => { 101 | if (!filePath) { 102 | reject(new Error("No file path provided.")); 103 | 104 | return; 105 | } 106 | 107 | fs.access(filePath, fs.constants.R_OK, (error) => { 108 | if (error) { 109 | reject(error); 110 | 111 | return; 112 | } 113 | 114 | const db = new sqlite3.Database( 115 | filePath, 116 | sqlite3.OPEN_READONLY, 117 | (error) => { 118 | if (error) { 119 | reject(error); 120 | 121 | return; 122 | } 123 | 124 | rowsFromTable(db, table, filePath) 125 | .then((rows) => { 126 | resolve(rows); 127 | }) 128 | .catch((reason) => { 129 | reject(reason); 130 | }); 131 | } 132 | ); 133 | }); 134 | }); 135 | }; 136 | 137 | /** 138 | * Short-hand for `dataFromFile()` that combines the parameters to a path. 139 | * 140 | * @param browser {object} A Havelock browser object, e.g. `browser.chromium`. 141 | * @param profile {string} A profile where the user data of interest resides. 142 | * @param file {string} The SQLite file to extract data from. 143 | * @param table {string} The table to use in the `SELECT` statement, i.e. the table to extract data from. 144 | * @returns {Promise} The rows selected from `table`. 145 | */ 146 | exports.dataFromUddFile = (browser, profile, file, table) => { 147 | if (!browser || !profile || !file || !table) { 148 | return; 149 | } 150 | 151 | const filePath = path.join(browser.userDataDirectoryPath(), profile, file); 152 | 153 | return exports.dataFromFile(filePath, table); 154 | }; 155 | 156 | /** 157 | * Short-hand for `dataFromFile()` that combines the parameters to a path to the `Login Data` file using the table `logins`. 158 | * 159 | * @param browser {object} A Havelock browser object, e.g. `browser.chromium`. 160 | * @param profile {string} A profile where the user data of interest resides. 161 | * @returns {Promise} The rows selected from `logins`. 162 | */ 163 | exports.logins = (browser, profile) => { 164 | if (!browser || !profile) { 165 | return; 166 | } 167 | 168 | const filePath = path.join( 169 | browser.userDataDirectoryPath(), 170 | profile, 171 | "Login Data" 172 | ); 173 | 174 | return exports.dataFromFile(filePath, "logins"); 175 | }; 176 | 177 | /** 178 | * Short-hand for `dataFromFile()` that combines the parameters to a path to the `Cookies` file using the table `cookies`. 179 | * 180 | * @param browser {object} A Havelock browser object, e.g. `browser.chromium`. 181 | * @param profile {string} A profile where the user data of interest resides. 182 | * @returns {Promise} The rows selected from `cookies`. 183 | */ 184 | exports.cookies = (browser, profile) => { 185 | if (!browser || !profile) { 186 | return; 187 | } 188 | 189 | const filePath = path.join( 190 | browser.userDataDirectoryPath(), 191 | profile, 192 | "Cookies" 193 | ); 194 | 195 | return exports.dataFromFile(filePath, "cookies"); 196 | }; 197 | 198 | /** 199 | * Short-hand for `dataFromFile()` that combines the parameters to a path to the `History` file using the table `urls`. 200 | * 201 | * @param browser {object} A Havelock browser object, e.g. `browser.chromium`. 202 | * @param profile {string} A profile where the user data of interest resides. 203 | * @returns {Promise} The rows selected from `urls`. 204 | */ 205 | exports.urls = (browser, profile) => { 206 | if (!browser || !profile) { 207 | return; 208 | } 209 | 210 | const filePath = path.join( 211 | browser.userDataDirectoryPath(), 212 | profile, 213 | "History" 214 | ); 215 | 216 | return exports.dataFromFile(filePath, "urls"); 217 | }; 218 | -------------------------------------------------------------------------------- /cli/actions.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { program } = require("commander"); 4 | const prettier = require("prettier"); 5 | const havelock = require("../havelock"); 6 | const commander = require("commander"); 7 | 8 | const explorer = havelock.explorer; 9 | 10 | /** 11 | * Show an optional error message and exits the program with an error code. 12 | * 13 | * @param {string} message Message to show before exiting. Will be sent to `stderr`. 14 | * @param {any[]} optionalParams Any additional params to help the user. 15 | */ 16 | const error = (message = null, ...optionalParams) => { 17 | if (message) { 18 | console.error(message, ...optionalParams); 19 | } 20 | 21 | process.exit(1); 22 | }; 23 | 24 | /** 25 | * Show an optional message and exits the program normally. 26 | * 27 | * @param {string} message Message to show before exiting. Will be sent to `stdout`. 28 | * @param {any[]} optionalParams Any additional params to help the user. 29 | */ 30 | const success = (message = null, ...optionalParams) => { 31 | if (message) { 32 | console.info(message, ...optionalParams); 33 | } 34 | 35 | process.exit(0); 36 | }; 37 | 38 | /** 39 | * Instead of printing the data to `stdout` you can use this method to write the data to a file. 40 | * 41 | * @param {string} fileName The name of the file to write the `data` to. Do not include the file extension, it will use `.json` by default. 42 | * @param {any} data The data to write to the file. It should be convertable using `JSON.stringify()`. 43 | * 44 | * @returns {Promise} Promise resolved with the complete file path or rejected with a `NodeJS.ErrnoException`. 45 | */ 46 | const writeToFile = (fileName, data) => { 47 | return new Promise((resolve, reject) => { 48 | const filePath = path.join(process.cwd(), fileName + ".json"); 49 | const json = JSON.stringify(data); 50 | const fmtJson = prettier.format(json, { parser: "json" }); 51 | 52 | fs.writeFile(filePath, fmtJson, { encoding: "utf8" }, (err) => { 53 | if (err) { 54 | reject(err); 55 | 56 | return; 57 | } 58 | 59 | resolve(filePath); 60 | }); 61 | }); 62 | }; 63 | 64 | /** 65 | * Print array of `data` in a table structure. 66 | * Supply `type` to use typical data points. 67 | * 68 | * @param {string} type Type of data, e.g., `logins` or `urls`. 69 | * @param {object[]} data Data to structure in a table. 70 | */ 71 | const tabular = (type, data) => { 72 | switch (type) { 73 | case "logins": 74 | console.table(data, ["origin_url", "username_value", "password_value"]); 75 | break; 76 | 77 | case "cookies": 78 | console.table(data, ["host_key", "name", "encrypted_value"]); 79 | break; 80 | 81 | case "urls": 82 | console.table(data, ["url", "title"]); 83 | break; 84 | 85 | default: 86 | console.table(data); 87 | break; 88 | } 89 | }; 90 | 91 | /** 92 | * Return the encrypted field for a type of data, e.g., for `logins` it's `password_value`. 93 | * 94 | * @param {string} type 95 | * @returns {string} 96 | */ 97 | const encryptedFieldForType = (type) => { 98 | switch (type) { 99 | case "logins": 100 | return "password_value"; 101 | case "cookies": 102 | return "encrypted_value"; 103 | default: 104 | return null; 105 | } 106 | }; 107 | 108 | /** 109 | * Decrypt specified `rows` and return them in the same format but with the decrypted value. 110 | * 111 | * @param {object[]} rows Rows to decrypt. Must adhere to correct fields. 112 | * @param {object} browser Browser to decrypt from. 113 | * @param {string} type Type of data to decrypt. 114 | * @returns {Promise} 115 | */ 116 | const decrypt = (rows, browser, type) => { 117 | return new Promise((resolve, reject) => { 118 | if (!rows) { 119 | reject(new TypeError("No rows.")); 120 | 121 | return; 122 | } 123 | 124 | const decs = []; 125 | 126 | rows.forEach((row) => { 127 | const encField = encryptedFieldForType(type); 128 | 129 | decs.push( 130 | havelock.crypto.decrypt(browser, row[encField]).then((plaintext) => { 131 | return { 132 | ...row, 133 | [encField]: plaintext, 134 | }; 135 | }) 136 | ); 137 | }); 138 | 139 | Promise.all(decs) 140 | .then((decRows) => { 141 | resolve(decRows); 142 | }) 143 | .catch((reason) => { 144 | reject(reason); 145 | }); 146 | }); 147 | }; 148 | 149 | /** 150 | * Print `data` in multiple formats. 151 | * Format is deduced from `opts`. 152 | * 153 | * @param {string} type Type of data to print. Used to determine interesting data points. 154 | * @param {Array} data Data to print. Must be an `Array`. 155 | * @param {commander.OptionValues} opts Program options used to determine format type. 156 | * @param {object} browser 157 | * @returns 158 | */ 159 | const printData = (type, data, opts, browser) => { 160 | return new Promise((resolve, reject) => { 161 | if (!data.length) { 162 | reject(); 163 | 164 | return; 165 | } 166 | 167 | if (opts.decrypt) { 168 | decrypt(data, browser, type) 169 | .then((rows) => { 170 | if (opts.tabular) { 171 | tabular(type, rows); 172 | 173 | resolve(); 174 | 175 | return; 176 | } 177 | 178 | if (opts.file) { 179 | writeToFile(type, rows) 180 | .then((filePath) => { 181 | resolve(filePath); 182 | }) 183 | .catch((reason) => { 184 | reject(reason); 185 | }); 186 | 187 | return; 188 | } 189 | 190 | console.info(rows); 191 | 192 | resolve(); 193 | }) 194 | .catch((reason) => { 195 | reject(reason); 196 | }); 197 | 198 | return; 199 | } 200 | 201 | if (opts.tabular) { 202 | tabular(type, data); 203 | 204 | resolve(); 205 | 206 | return; 207 | } 208 | 209 | if (opts.file) { 210 | writeToFile(type, data) 211 | .then((filePath) => { 212 | resolve(filePath); 213 | }) 214 | .catch((reason) => { 215 | reject(reason); 216 | }); 217 | 218 | return; 219 | } 220 | 221 | console.info(data); 222 | 223 | resolve(); 224 | }); 225 | }; 226 | 227 | exports.logins = (browser, profile = "Default") => { 228 | const opts = program.opts(); 229 | 230 | browser = havelock.browser[browser]; 231 | 232 | if (!browser) { 233 | error( 234 | "Couldn't convert the specified browser to a verified Havelock browser." 235 | ); 236 | 237 | return; 238 | } 239 | 240 | explorer 241 | .logins(browser, profile) 242 | .then((logins) => { 243 | printData("logins", logins, opts, browser) 244 | .then((filePath) => { 245 | if (filePath) { 246 | success(filePath); 247 | 248 | return; 249 | } 250 | 251 | success(); 252 | }) 253 | .catch((reason) => { 254 | if (reason) { 255 | error("Failed to write data to file.", reason); 256 | 257 | return; 258 | } 259 | 260 | error("No data."); 261 | }); 262 | }) 263 | .catch((reason) => { 264 | error("Failed to retrieve logins from data file.", reason); 265 | }); 266 | }; 267 | 268 | exports.cookies = (browser, profile = "Default") => { 269 | const opts = program.opts(); 270 | 271 | browser = havelock.browser[browser]; 272 | 273 | if (!browser) { 274 | error( 275 | "Couldn't convert the specified browser to a verified Havelock browser." 276 | ); 277 | 278 | return; 279 | } 280 | 281 | explorer 282 | .cookies(browser, profile) 283 | .then((cookies) => { 284 | printData("cookies", cookies, opts, browser) 285 | .then((filePath) => { 286 | if (filePath) { 287 | success(filePath); 288 | 289 | return; 290 | } 291 | 292 | success(); 293 | }) 294 | .catch((reason) => { 295 | if (reason) { 296 | error("Failed to write data to file.", reason); 297 | 298 | return; 299 | } 300 | 301 | error("No data."); 302 | }); 303 | }) 304 | .catch((reason) => { 305 | error("Failed to retrieve cookies from data file.", reason); 306 | }); 307 | }; 308 | 309 | exports.urls = (browser, profile = "Default") => { 310 | const opts = program.opts(); 311 | 312 | browser = havelock.browser[browser]; 313 | 314 | if (!browser) { 315 | error( 316 | "Couldn't convert the specified browser to a verified Havelock browser." 317 | ); 318 | 319 | return; 320 | } 321 | 322 | explorer 323 | .urls(browser, profile) 324 | .then((urls) => { 325 | printData("urls", urls, opts, browser) 326 | .then((filePath) => { 327 | if (filePath) { 328 | success(filePath); 329 | 330 | return; 331 | } 332 | 333 | success(); 334 | }) 335 | .catch((reason) => { 336 | if (reason) { 337 | error("Failed to write data to file.", reason); 338 | 339 | return; 340 | } 341 | 342 | error("No data."); 343 | }); 344 | }) 345 | .catch((reason) => { 346 | error("Failed to retrieve URLs from data file.", reason); 347 | }); 348 | }; 349 | --------------------------------------------------------------------------------