edge-cookies-secure
Version:
Extract encrypted Google Chrome and Microsoft Edge cookies for a url on a Mac, Linux or Windows
531 lines (403 loc) • 14.1 kB
JavaScript
/*
* Copyright (c) 2015, Yahoo! Inc. All rights reserved.
* Copyrights licensed under the MIT License.
* See the accompanying LICENSE file for terms.
*/
const sqlite3 = require('sqlite3');
const tld = require('tldjs');
const tough = require('tough-cookie');
const int = require('int');
const url = require('url');
const crypto = require('crypto');
const os = require('os');
const fs = require('fs');
let dpapi,
ITERATIONS,
dbClosed = false;
const KEYLENGTH = 16
const SALT = 'saltysalt'
// Decryption based on http://n8henrie.com/2014/05/decrypt-chrome-cookies-with-python/
// Inspired by https://www.npmjs.org/package/chrome-cookies
function decrypt(key, encryptedData) {
let decipher,
decoded,
final,
padding,
iv = new Buffer.from(new Array(KEYLENGTH + 1).join(' '), 'binary');
decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
decipher.setAutoPadding(false);
encryptedData = encryptedData.slice(3);
decoded = decipher.update(encryptedData);
final = decipher.final();
final.copy(decoded, decoded.length - 1);
padding = decoded[decoded.length - 1];
if (padding) {
decoded = decoded.slice(32, decoded.length - padding);
}
return decoded.toString('utf8');
}
function getDerivedKey(browser, callback) {
let keytar,
password;
if (process.platform === 'darwin') {
keytar = require('keytar');
const keychainService = browser === 'edge' ? 'Microsoft Edge Safe Storage' : 'Chrome Safe Storage';
const keychainAccount = browser === 'edge' ? 'Microsoft Edge' : 'Chrome';
keytar.getPassword(keychainService, keychainAccount).then(function(password) {
crypto.pbkdf2(password, SALT, ITERATIONS, KEYLENGTH, 'sha1', callback);
});
} else if (process.platform === 'linux') {
password = 'peanuts';
crypto.pbkdf2(password, SALT, ITERATIONS, KEYLENGTH, 'sha1', callback);
} else if (process.platform === 'win32') {
// On Windows, the crypto is managed entirely by the OS. We never see the keys.
dpapi = require('win-dpapi');
callback(null, null);
}
}
const pathIdentifiers = ['/', '\\'];
const isPathFormat = (profileOrPath) =>
profileOrPath &&
pathIdentifiers.some(pathIdentifier => profileOrPath.includes(pathIdentifier));
/**
* Set the iteration count per platform
*/
const setIterations = () => {
if (process.platform === 'darwin') {
ITERATIONS = 1003;
}
if (process.platform === 'linux') {
ITERATIONS = 1;
}
}
const caterForCookiesInPath = (path) => {
const cookiesFileName = 'Cookies'
const includesCookies = path.slice(-cookiesFileName.length) === cookiesFileName
if (includesCookies) {
return path;
}
if (process.platform === 'darwin' || process.platform === 'linux') {
return path.concat(`/${cookiesFileName}`)
}
if (process.platform === 'win32') {
return path.concat(`\\${cookiesFileName}`)
}
return path
}
/**
* Converts profileOrPath argument into a path
*/
const getPath = (profileOrPath, browser = 'chrome') => {
if (isPathFormat(profileOrPath)) {
const path = caterForCookiesInPath(profileOrPath)
if (!fs.existsSync(path)) {
throw new Error(`Path: ${path} not found`);
}
return path
}
const defaultProfile = 'Default';
const profile = profileOrPath || defaultProfile;
if (process.platform === 'darwin') {
if (browser === 'edge') {
return process.env.HOME + `/Library/Application Support/Microsoft Edge/${profile}/Cookies`;
} else {
return process.env.HOME + `/Library/Application Support/Google/Chrome/${profile}/Cookies`;
}
}
if (process.platform === 'linux') {
if (browser === 'edge') {
return process.env.HOME + `/.config/microsoft-edge/${profile}/Cookies`;
} else {
return process.env.HOME + `/.config/google-chrome/${profile}/Cookies`;
}
}
if (process.platform === 'win32') {
let path;
if (browser === 'edge') {
path = os.homedir() + `\\AppData\\Local\\Microsoft\\Edge\\User Data\\${profile}\\Network\\Cookies`;
// Windows has two potential locations for Edge
if (fs.existsSync(path)) {
return path;
}
return os.homedir() + `\\AppData\\Local\\Microsoft\\Edge\\User Data\\${profile}\\Cookies`;
} else {
path = os.homedir() + `\\AppData\\Local\\Google\\Chrome\\User Data\\${profile}\\Network\\Cookies`;
// Windows has two potential locations for Chrome
if (fs.existsSync(path)) {
return path;
}
return os.homedir() + `\\AppData\\Local\\Google\\Chrome\\User Data\\${profile}\\Cookies`;
}
}
return new Error('Only Mac, Windows, and Linux are supported.');
}
const getLocalStatePath = (browser = 'chrome') => {
if (process.platform === 'win32') {
if (browser === 'edge') {
return os.homedir() + '/AppData/Local/Microsoft/Edge/User Data/Local State';
} else {
return os.homedir() + '/AppData/Local/Google/Chrome/User Data/Local State';
}
} else if (process.platform === 'darwin') {
if (browser === 'edge') {
return process.env.HOME + '/Library/Application Support/Microsoft Edge/Local State';
} else {
return process.env.HOME + '/Library/Application Support/Google/Chrome/Local State';
}
} else if (process.platform === 'linux') {
if (browser === 'edge') {
return process.env.HOME + '/.config/microsoft-edge/Local State';
} else {
return process.env.HOME + '/.config/google-chrome/Local State';
}
}
}
// Chromium stores its timestamps in sqlite on the Mac using the Windows Gregorian epoch
// https://github.com/adobe/chromium/blob/master/base/time_mac.cc#L29
// This converts it to a UNIX timestamp
function convertChromiumTimestampToUnix(timestamp) {
return int(timestamp.toString()).sub('11644473600000000').div(1000000);
}
function convertRawToNetscapeCookieFileFormat(cookies, domain) {
let out = ''
cookies.forEach(function (cookie, index) {
out += cookie.host_key + '\t';
out += ((cookie.host_key === '.' + domain) ? 'TRUE' : 'FALSE') + '\t';
out += cookie.path + '\t';
out += (cookie.is_secure ? 'TRUE' : 'FALSE') + '\t';
if (cookie.has_expires) {
out += convertChromiumTimestampToUnix(cookie.expires_utc).toString() + '\t';
} else {
out += '0' + '\t';
}
out += cookie.name + '\t';
out += cookie.value + '\t';
if (cookies.length > index + 1) {
out += '\n';
}
});
return out;
}
function convertRawToHeader(cookies) {
let out = ''
cookies.forEach(function (cookie, index) {
out += cookie.name + '=' + cookie.value;
if (cookies.length > index + 1) {
out += '; ';
}
});
return out;
}
function convertRawToJar(cookies, uri) {
const jar = new tough.CookieJar()
cookies.forEach(({ name, value }) => {
jar.setCookieSync(`${name}=${value}`, uri);
});
return { _jar: jar };
}
function convertRawToSetCookieStrings(cookies) {
const strings = [];
cookies.forEach(function(cookie) {
let out = '';
const dateExpires = new Date(convertChromiumTimestampToUnix(cookie.expires_utc) * 1000);
out += cookie.name + '=' + cookie.value + '; ';
out += 'expires=' + tough.formatDate(dateExpires) + '; ';
out += 'domain=' + cookie.host_key + '; ';
out += 'path=' + cookie.path;
if (cookie.is_secure) {
out += '; Secure';
}
if (cookie.is_httponly) {
out += '; HttpOnly';
}
strings.push(out);
});
return strings;
}
function convertRawToPuppeteerState(cookies) {
const puppeteerCookies = cookies.map(function(cookie) {
const newCookieObject = {
name: cookie.name,
value: cookie.value,
expires: cookie.expires_utc,
domain: cookie.host_key,
path: cookie.path
}
if (cookie.is_secure) {
newCookieObject['Secure'] = true
}
if (cookie.is_httponly) {
newCookieObject['HttpOnly'] = true
}
return newCookieObject
})
return puppeteerCookies;
}
function convertRawToObject(cookies) {
const out = {};
cookies.forEach(function (cookie) {
out[cookie.name] = cookie.value;
});
return out;
}
function decryptAES256GCM(key, enc, nonce, tag) {
const algorithm = 'aes-256-gcm';
const decipher = crypto.createDecipheriv(algorithm, key, nonce);
decipher.setAuthTag(tag);
let str = decipher.update(enc,'base64','utf8');
str += decipher.final('utf-8');
return str;
}
const getOutput = (format, validCookies, domain, uri) => {
switch (format) {
case 'curl':
return convertRawToNetscapeCookieFileFormat(validCookies, domain);
case 'jar':
return convertRawToJar(validCookies, uri);
case 'set-cookie':
return convertRawToSetCookieStrings(validCookies);
case 'header':
return convertRawToHeader(validCookies);
case 'puppeteer':
return convertRawToPuppeteerState(validCookies)
case 'object':
/* falls through */
default:
return convertRawToObject(validCookies);
}
}
/*
Possible formats:
curl - Netscape HTTP Cookie File contents usable by curl and wget http://curl.haxx.se/docs/http-cookies.html
set-cookie - Array of set-cookie strings
header - "cookie" header string
puppeteer - array of cookie objects that can be loaded straight into puppeteer setCookie(...)
object - key/value of name/value pairs, overlapping names are overwritten
*/
/**
* @param {*} uri - the site to retrieve cookies for
* @param {*} format - the format you want the cookies returned in
* @param {*} callback -
* @param {*} profileOrPath - if empty will use the 'Default' profile in default Chrome location; if specified can be an alternative profile name e.g. 'Profile 1' or an absolute path to an alternative user-data-dir
* @param {*} browser - which browser to extract cookies from ('chrome' or 'edge'), defaults to 'chrome'
*/
const getCookies = async (uri, format, callback, profileOrPath, browser = 'chrome') => {
setIterations();
const path = getPath(profileOrPath, browser);
if (path instanceof Error) {
const error = path;
return callback(error);
}
db = new sqlite3.Database(path);
if (format instanceof Function) {
callback = format;
format = null;
}
const parsedUrl = url.parse(uri);
if (!parsedUrl.protocol || !parsedUrl.hostname) {
return callback(new Error('Could not parse URI, format should be http://www.example.com/path/'));
}
if (dbClosed) {
db = new sqlite3.Database(path);
dbClosed = false;
}
getDerivedKey(browser, function (err, derivedKey) {
if (err) {
return callback(err);
}
db.serialize(function () {
const cookies = [];
const domain = tld.getDomain(uri);
if (!domain) {
return callback(new Error('Could not parse domain from URI, format should be http://www.example.com/path/'));
}
// ORDER BY tries to match sort order specified in
// RFC 6265 - Section 5.4, step 2
// http://tools.ietf.org/html/rfc6265#section-5.4
db.each(
"SELECT host_key, path, is_secure, expires_utc, name, value, hex(encrypted_value) as encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies where host_key like '%" + domain + "' ORDER BY LENGTH(path) DESC, creation_utc ASC",
function (err, cookie) {
let encryptedValue;
if (err) {
return callback(err);
}
if (cookie.value === '' && cookie.encrypted_value.length > 0) {
encryptedValue = Buffer.from(cookie.encrypted_value, 'hex');
if (process.platform === 'win32') {
if (encryptedValue[0] == 0x01 && encryptedValue[1] == 0x00 && encryptedValue[2] == 0x00 && encryptedValue[3] == 0x00){
cookie.value = dpapi.unprotectData(encryptedValue, null, 'CurrentUser').toString('utf-8');
} else if (encryptedValue[0] == 0x76 && encryptedValue[1] == 0x31 && encryptedValue[2] == 0x30 ){
const localStatePath = getLocalStatePath(browser);
localState = JSON.parse(fs.readFileSync(localStatePath));
b64encodedKey = localState.os_crypt.encrypted_key;
encryptedKey = new Buffer.from(b64encodedKey,'base64');
key = dpapi.unprotectData(encryptedKey.slice(5, encryptedKey.length), null, 'CurrentUser');
nonce = encryptedValue.slice(3, 15);
tag = encryptedValue.slice(encryptedValue.length - 16, encryptedValue.length);
encryptedValue = encryptedValue.slice(15, encryptedValue.length - 16);
cookie.value = decryptAES256GCM(key, encryptedValue, nonce, tag).toString('utf-8');
}
} else {
cookie.value = decrypt(derivedKey, encryptedValue);
}
delete cookie.encrypted_value;
}
cookies.push(cookie);
},
function () {
let host = parsedUrl.hostname,
path = parsedUrl.path,
isSecure = parsedUrl.protocol.match('https');
let validCookies = cookies.filter(function (cookie) {
if (cookie.is_secure && !isSecure) {
return false;
}
if (!tough.domainMatch(host, cookie.host_key, true)) {
return false;
}
if (!tough.pathMatch(path, cookie.path)) {
return false;
}
return true;
});
const filteredCookies = [];
const keys = {};
validCookies.reverse().forEach(function (cookie) {
if (typeof keys[cookie.name] === 'undefined') {
filteredCookies.push(cookie);
keys[cookie.name] = true;
}
});
const reversedCookies = filteredCookies.reverse();
const output = getOutput(format, reversedCookies, domain, uri)
db.close(function(err) {
if (!err) {
dbClosed = true;
}
return callback(null, output);
});
});
});
});
};
/**
* Promise wrapper for the main callback function
* @param {*} uri - the site to retrieve cookies for
* @param {*} format - the format you want the cookies returned in
* @param {*} profileOrPath - if empty will use the 'Default' profile in default Chrome location; if specified can be an alternative profile name e.g. 'Profile 1' or an absolute path to an alternative user-data-dir
* @param {*} browser - which browser to extract cookies from ('chrome' or 'edge'), defaults to 'chrome'
*/
const getCookiesPromised = async (uri, format, profileOrPath, browser = 'chrome') => {
return new Promise((resolve, reject) => {
getCookies(uri, format, function(err, cookies) {
if (err) {
return reject(err)
}
resolve(cookies)
}, profileOrPath, browser)
})
}
module.exports = {
getCookies,
getCookiesPromised
};