From b4c0a5b796b69737b037ae117288eabdcf763d5f Mon Sep 17 00:00:00 2001 From: Alexander Kalinin Date: Sun, 10 Oct 2021 11:54:07 +0100 Subject: [PATCH] 1.0 release (#14) * remove tldjs * refactor firefox cookies * refactor chrome * Update ChromeMacosCookieProvider.ts * lint * remove getIterations * refactor * Create ChromeMacosCookieProvider.unit.test.ts * Update ChromeMacosCookieProvider.unit.test.ts * Create ChromeWindowsCookieProvider.unit.test.ts * bump version to 1.0, add homepage link * Update package.json * 1.0 docs (#17) * docs update * Create browser_profiles.md * Update index.md * Create favicon.ico --- docs/favicon.ico | Bin 0 -> 67646 bytes docs/pages/browser_profiles.md | 50 ++++++++++ docs/pages/compatibility.md | 28 ++++++ docs/pages/fetching_cookies.md | 55 +++++++++++ docs/pages/index.md | 72 ++++---------- package.json | 13 ++- src/CookieProvider.ts | 6 ++ src/chrome/ChromeCookieDatabase.test.ts | 4 +- src/chrome/ChromeCookieDatabase.ts | 31 ++---- src/chrome/ChromeCookieRepository.ts | 11 +++ src/chrome/ChromeLinuxCookieProvider.ts | 39 ++++++++ src/chrome/ChromeMacosCookieProvider.ts | 39 ++++++++ .../ChromeMacosCookieProvider.unit.test.ts | 48 ++++++++++ src/chrome/ChromeWindowsCookieProvider.ts | 34 +++++++ .../ChromeWindowsCookieProvider.unit.test.ts | 44 +++++++++ src/chrome/getDerivedKey.ts | 2 +- src/chrome/index.ts | 90 ++++-------------- src/chrome/util.ts | 14 --- src/chrome/util.unit.test.ts | 42 +------- src/firefox/FirefoxCookieDatabase.test.ts | 4 +- src/firefox/FirefoxCookieDatabase.ts | 23 ++--- src/firefox/FirefoxCookieProvider.ts | 18 ++++ src/firefox/FirefoxCookieRepository.ts | 11 +++ src/firefox/index.ts | 64 +++---------- src/firefox/profiles.ts | 37 +++++++ src/index.ts | 14 +-- test/chrome/linux.unit.test.ts | 12 ++- test/firefox.unit.test.ts | 14 +-- yarn.lock | 12 --- 29 files changed, 523 insertions(+), 308 deletions(-) create mode 100644 docs/favicon.ico create mode 100644 docs/pages/browser_profiles.md create mode 100644 docs/pages/compatibility.md create mode 100644 docs/pages/fetching_cookies.md create mode 100644 src/CookieProvider.ts create mode 100644 src/chrome/ChromeCookieRepository.ts create mode 100644 src/chrome/ChromeLinuxCookieProvider.ts create mode 100644 src/chrome/ChromeMacosCookieProvider.ts create mode 100644 src/chrome/ChromeMacosCookieProvider.unit.test.ts create mode 100644 src/chrome/ChromeWindowsCookieProvider.ts create mode 100644 src/chrome/ChromeWindowsCookieProvider.unit.test.ts create mode 100644 src/firefox/FirefoxCookieProvider.ts create mode 100644 src/firefox/FirefoxCookieRepository.ts create mode 100644 src/firefox/profiles.ts diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fbcc7671876a32f91779168522c7e4e15c5fb93f GIT binary patch literal 67646 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 literal 0 HcmV?d00001 diff --git a/docs/pages/browser_profiles.md b/docs/pages/browser_profiles.md new file mode 100644 index 0000000..3ea31aa --- /dev/null +++ b/docs/pages/browser_profiles.md @@ -0,0 +1,50 @@ +--- +layout: default +title: Browser profiles +nav_order: 4 +description: Specifying browser profiles +permalink: /browser-profiles +--- + +# Browser profiles + +## Choosing browser profiles + +If you are utilising multiple browser profiles, you can specify which one to use in the function options. + +```js +import { getCookie, listCookies, Browser } from 'cookie-thief'; + +await getCookie({ + browser: Browser.Chrome, + cookieName: 'foo', + domain: '.github.com', + options: { + profile: 'SomeProfile', + }, +}); + +await listCookies({ + browser: Browser.Firefox, + options: { + profile: 'SomeProfile', + }, +}); +``` + +## Listing browser profiles + +If you want to programmatically list browser profiles, you can with `listProfiles`. + +```js +import { listProfiles, Browser } from 'cookie-thief'; + +const profiles: string[] = await listProfiles(Browser.Chrome); +``` + +## Default browser profiles + +If you do not specify a profile, a default profile name will be used. + +* Firefox: `default-release` +* Chrome: `Default` diff --git a/docs/pages/compatibility.md b/docs/pages/compatibility.md new file mode 100644 index 0000000..961d119 --- /dev/null +++ b/docs/pages/compatibility.md @@ -0,0 +1,28 @@ +--- +layout: default +title: Compatibility +nav_order: 2 +description: Compatible browsers and operation systems +permalink: /compatibility +--- + +## Compatibility + +### Supported Browsers + +* Google Chrome +* Firefox + +### Supported Operating Systems + +* MacOS +* Linux +* Windows + +## Limitations + +### MacOS + +On macOS, this package requires keychain access to access the Google Chrome encryption key. +You will get a dialogue popup requesting access. +Due to this popup, you cannot use this library completely headlessly to fetch cookies unless you run it once and click `Always Allow`. diff --git a/docs/pages/fetching_cookies.md b/docs/pages/fetching_cookies.md new file mode 100644 index 0000000..b66f2e7 --- /dev/null +++ b/docs/pages/fetching_cookies.md @@ -0,0 +1,55 @@ +--- +layout: default +title: Fetching Cookies +nav_order: 3 +description: Getting and listing cookies +permalink: /fetching-cookies +--- + +# Get Cookie + +`getCookie` can be used to try and find a cookie based on the domain and cookie name. +If a cookie is not found, the result will be `undefined`. + +```js +import { getCookie, Browser } from 'cookie-thief'; + +const cookie = await getCookie({ + browser: Browser.Chrome, // or Browser.Firefox + domain: '.reddit.com', + cookieName: 'loid', +}); + +/* +{ + name: 'loid', + value: 'the decrypted cookie value here', + domain: '.reddit.com', + path: '/', +} +*/ +``` + +# List Cookies + +`listCookies` can be used to list all cookies for a browser. +If no cookies are found you will get `[]`. + +```js +import { listCookies, Browser } from 'cookie-thief'; + +const cookies = await listCookies({ + browser: Browser.Chrome, // or Browser.Firefox +}) + +/* +[ + { + name: 'loid', + value: 'the decrypted cookie value here', + domain: '.reddit.com', + path: '/', + } +] +*/ +``` diff --git a/docs/pages/index.md b/docs/pages/index.md index 03bb8da..553a021 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -7,16 +7,15 @@ permalink: / --- # Cookie Thief -{: .no_toc} -1. TOC -{:toc} +![npm](https://img.shields.io/npm/v/cookie-thief) +![npm](https://img.shields.io/npm/dw/cookie-thief) +![npm bundle size](https://img.shields.io/bundlephobia/min/cookie-thief) +![npm bundle size](https://img.shields.io/bundlephobia/minzip/cookie-thief) +[![codecov](https://codecov.io/gh/Kalininator/cookie-thief/branch/master/graph/badge.svg?token=H0F1TIE0CY)](https://codecov.io/gh/Kalininator/cookie-thief) -## Compatibility - -Currently supports only Google Chrome and Firefox on MacOS, Linux, and Windows. - -In the future will hopefully expand to support other browsers. +A node.js library for extracting cookies from a browser installed on your system. +Inspired by [chrome-cookies-secure](https://github.com/bertrandom/chrome-cookies-secure). ## Installation @@ -31,23 +30,24 @@ yarn add cookie-thief ## Usage -### Google Chrome - ```javascript -const { getCookie, listCookies, Browser } = require('cookie-thief') +const { getCookie, listCookies, Browser, listBrowsers } = require('cookie-thief') // Get a cookie from chrome browser for domain .github.com, searching for cookie named 'dotcom_user' const cookie = await getCookie({ browser: Browser.Chrome, - url: 'https://github.com', + domain: '.github.com', cookieName: 'dotcom_user', - options: { - profile: 'Default', - }, }); console.log(cookie); -// Will be a string if cookie is successfully found +// Will be a Cookie if cookie is successfully found // Will be undefined if not found +//{ +// name: 'cookie name here', +// value: 'decrypted cookie content here', +// host: 'hostname of cookie here', +// path: 'path of cookie here' +//} const cookies = await listCookies({ browser: Browser.Chrome, @@ -63,42 +63,8 @@ console.log(cookies); // } //] -``` - -### Firefox - -```javascript -const { getCookie, Browser } = require('cookie-thief') - -// Get a cookie from chrome browser for domain .github.com, searching for cookie named 'dotcom_user' -const cookie = await getCookie({ - browser: Browser.Firefox, - url: 'https://github.com', - cookieName: 'dotcom_user', - options: { - profile: 'default-release', - }, -}); -console.log(cookie); -// Will be a string if cookie is successfully found -// Will be undefined if not found +const browsers = listBrowsers(); +console.log(browsers); +// [ Browser.Chrome, Browser.Firefox ] -const cookies = await listCookies({ - browser: Browser.Firefox, -}); -console.log(cookies); -// Array of cookies -//[ -// { -// name: 'cookie name here', -// value: 'decrypted cookie content here', -// host: 'hostname of cookie here', -// path: 'path of cookie here' -// } -//] ``` - -## Limitations - -### macOS -On macOS, this package requires keychain access to access the Google Chrome encryption key. You will get a dialogue popup requesting access. diff --git a/package.json b/package.json index 464f0d8..a737aec 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { "name": "cookie-thief", - "version": "0.7.0", + "version": "1.0.0", "description": "Steal browser cookies", "main": "./lib/index.js", "author": "Alex Kalinin (https://kalinin.uk)", + "homepage": "https://kalininator.github.io/cookie-thief/", + "bugs": { + "url": "https://github.com/Kalininator/cookie-thief/issues" + }, "license": "MIT", "files": [ "lib/", @@ -33,8 +37,8 @@ "publish-package": "npm run build && npm publish", "test": "jest", "test:watch": "jest --watch", - "clean:some": "rm -rf ./lib ./docs", - "clean:all": "rm -rf ./node_modules ./package-lock.json ./lib ./docs", + "clean:some": "rm -rf ./lib", + "clean:all": "rm -rf ./node_modules ./package-lock.json ./lib", "pr:lint": "./node_modules/eslint/bin/eslint.js 'src/**/*.ts'", "pr:test": "jest", "t": "ts-node testFile.ts" @@ -67,8 +71,7 @@ }, "dependencies": { "better-sqlite3": "^7.4.3", - "ini": "^2.0.0", - "tldjs": "^2.3.1" + "ini": "^2.0.0" }, "optionalDependencies": { "keytar": "^7.7.0", diff --git a/src/CookieProvider.ts b/src/CookieProvider.ts new file mode 100644 index 0000000..b39e71a --- /dev/null +++ b/src/CookieProvider.ts @@ -0,0 +1,6 @@ +import { Cookie } from './types'; + +export interface CookieProvider { + getCookie(domain: string, cookieName: string): Promise; + listCookies(): Promise; +} diff --git a/src/chrome/ChromeCookieDatabase.test.ts b/src/chrome/ChromeCookieDatabase.test.ts index beed1e3..d40c031 100644 --- a/src/chrome/ChromeCookieDatabase.test.ts +++ b/src/chrome/ChromeCookieDatabase.test.ts @@ -30,7 +30,7 @@ describe('ChromeCookieDatabase', () => { }); expect(getFn).toHaveBeenCalled(); expect(prepareFn).toHaveBeenCalledWith( - `SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies where host_key like '%.domain.com' and name like '%someCookie' ORDER BY LENGTH(path) DESC, creation_utc ASC`, + `SELECT host_key, path, name, encrypted_value FROM cookies where host_key like '%.domain.com' and name like '%someCookie' ORDER BY LENGTH(path) DESC, creation_utc ASC`, ); expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, { readonly: true, @@ -68,7 +68,7 @@ describe('ChromeCookieDatabase', () => { ]); expect(allFn).toHaveBeenCalled(); expect(prepareFn).toHaveBeenCalledWith( - `SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies`, + `SELECT host_key, path, name, encrypted_value FROM cookies`, ); expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, { readonly: true, diff --git a/src/chrome/ChromeCookieDatabase.ts b/src/chrome/ChromeCookieDatabase.ts index bba3240..2c7ad08 100644 --- a/src/chrome/ChromeCookieDatabase.ts +++ b/src/chrome/ChromeCookieDatabase.ts @@ -1,32 +1,13 @@ import sqlite from 'better-sqlite3'; +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; -type BooleanNumber = 0 | 1; +export class ChromeCookieDatabase implements ChromeCookieRepository { + constructor(private path: string) {} -export type ChromeCookie = { - host_key: string; - path: string; - is_secure: BooleanNumber; - expires_utc: number; - name: string; - value: string; - encrypted_value: Buffer; - creation_utc: number; - is_httponly: BooleanNumber; - has_expires: BooleanNumber; - is_persistent: BooleanNumber; -}; - -export class ChromeCookieDatabase { - path: string; - - constructor(path: string) { - this.path = path; - } - - findCookie(cookieName: string, domain: string): ChromeCookie { + findCookie(cookieName: string, domain: string): ChromeCookie | undefined { const db = sqlite(this.path, { readonly: true, fileMustExist: true }); const statement = db.prepare( - `SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies where host_key like '%${domain}' and name like '%${cookieName}' ORDER BY LENGTH(path) DESC, creation_utc ASC`, + `SELECT host_key, path, name, encrypted_value FROM cookies where host_key like '%${domain}' and name like '%${cookieName}' ORDER BY LENGTH(path) DESC, creation_utc ASC`, ); return statement.get(); } @@ -34,7 +15,7 @@ export class ChromeCookieDatabase { listCookies(): ChromeCookie[] { const db = sqlite(this.path, { readonly: true, fileMustExist: true }); const statement = db.prepare( - `SELECT host_key, path, is_secure, expires_utc, name, value, encrypted_value, creation_utc, is_httponly, has_expires, is_persistent FROM cookies`, + `SELECT host_key, path, name, encrypted_value FROM cookies`, ); const cookies: ChromeCookie[] = statement.all(); return cookies; diff --git a/src/chrome/ChromeCookieRepository.ts b/src/chrome/ChromeCookieRepository.ts new file mode 100644 index 0000000..e460dcc --- /dev/null +++ b/src/chrome/ChromeCookieRepository.ts @@ -0,0 +1,11 @@ +export type ChromeCookie = { + host_key: string; + path: string; + name: string; + encrypted_value: Buffer; +}; + +export interface ChromeCookieRepository { + findCookie(cookieName: string, domain: string): ChromeCookie | undefined; + listCookies(): ChromeCookie[]; +} diff --git a/src/chrome/ChromeLinuxCookieProvider.ts b/src/chrome/ChromeLinuxCookieProvider.ts new file mode 100644 index 0000000..62ddb2d --- /dev/null +++ b/src/chrome/ChromeLinuxCookieProvider.ts @@ -0,0 +1,39 @@ +import { CookieProvider } from '../CookieProvider'; +import { Cookie } from '../types'; +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { decrypt } from './decrypt'; +import { getLinuxDerivedKey } from './getDerivedKey'; + +const KEYLENGTH = 16; +const ITERATIONS = 1; + +async function decryptCookie(cookie: ChromeCookie): Promise { + const derivedKey = await getLinuxDerivedKey(KEYLENGTH, ITERATIONS); + return decrypt(derivedKey, cookie.encrypted_value, KEYLENGTH); +} + +async function toCookie(chromeCookie: ChromeCookie): Promise { + return { + value: await decryptCookie(chromeCookie), + host: chromeCookie.host_key, + path: chromeCookie.path, + name: chromeCookie.name, + }; +} + +export class ChromeLinuxCookieProvider implements CookieProvider { + constructor(private db: ChromeCookieRepository) {} + + async getCookie( + domain: string, + cookieName: string, + ): Promise { + const chromeCookie = this.db.findCookie(cookieName, domain); + if (!chromeCookie) return undefined; + return toCookie(chromeCookie); + } + + async listCookies(): Promise { + return Promise.all(this.db.listCookies().map(toCookie)); + } +} diff --git a/src/chrome/ChromeMacosCookieProvider.ts b/src/chrome/ChromeMacosCookieProvider.ts new file mode 100644 index 0000000..fa2523f --- /dev/null +++ b/src/chrome/ChromeMacosCookieProvider.ts @@ -0,0 +1,39 @@ +import { CookieProvider } from '../CookieProvider'; +import { Cookie } from '../types'; +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { decrypt } from './decrypt'; +import { getMacDerivedKey } from './getDerivedKey'; + +const KEYLENGTH = 16; +const ITERATIONS = 1003; + +async function decryptCookie(cookie: ChromeCookie): Promise { + const derivedKey = await getMacDerivedKey(KEYLENGTH, ITERATIONS); + return decrypt(derivedKey, cookie.encrypted_value, KEYLENGTH); +} + +async function toCookie(chromeCookie: ChromeCookie): Promise { + return { + value: await decryptCookie(chromeCookie), + host: chromeCookie.host_key, + path: chromeCookie.path, + name: chromeCookie.name, + }; +} + +export class ChromeMacosCookieProvider implements CookieProvider { + constructor(private db: ChromeCookieRepository) {} + + async getCookie( + domain: string, + cookieName: string, + ): Promise { + const chromeCookie = this.db.findCookie(cookieName, domain); + if (!chromeCookie) return undefined; + return toCookie(chromeCookie); + } + + async listCookies(): Promise { + return Promise.all(this.db.listCookies().map(toCookie)); + } +} diff --git a/src/chrome/ChromeMacosCookieProvider.unit.test.ts b/src/chrome/ChromeMacosCookieProvider.unit.test.ts new file mode 100644 index 0000000..3107fb1 --- /dev/null +++ b/src/chrome/ChromeMacosCookieProvider.unit.test.ts @@ -0,0 +1,48 @@ +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { ChromeMacosCookieProvider } from './ChromeMacosCookieProvider'; + +jest.mock('./decrypt', () => ({ + decrypt: jest.fn().mockResolvedValue('foo'), +})); + +jest.mock('./getDerivedKey', () => ({ + getMacDerivedKey: jest.fn().mockResolvedValue(Buffer.from('some_key')), +})); + +const fakeBuffer = Buffer.from('encrypted'); + +describe('chrome macos cookie provider', () => { + it('should fetch and decrypt cookie', async () => { + const mockFindCookie = jest + .fn() + .mockImplementation( + (cookieName: string, domain: string): ChromeCookie => { + return { + host_key: domain, + path: '/', + name: cookieName, + encrypted_value: fakeBuffer, + }; + }, + ); + const mockDb: ChromeCookieRepository = { + findCookie: mockFindCookie, + listCookies(): ChromeCookie[] { + throw new Error('Function not implemented.'); + }, + }; + + const provider = new ChromeMacosCookieProvider(mockDb); + + const cookie = await provider.getCookie('.test.com', 'some_cookie'); + + expect(cookie).toEqual({ + value: 'foo', + host: '.test.com', + path: '/', + name: 'some_cookie', + }); + + expect(mockFindCookie).toHaveBeenCalledWith('some_cookie', '.test.com'); + }); +}); diff --git a/src/chrome/ChromeWindowsCookieProvider.ts b/src/chrome/ChromeWindowsCookieProvider.ts new file mode 100644 index 0000000..d490cee --- /dev/null +++ b/src/chrome/ChromeWindowsCookieProvider.ts @@ -0,0 +1,34 @@ +import { CookieProvider } from '../CookieProvider'; +import { Cookie } from '../types'; +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { decryptWindows } from './decrypt'; + +async function decryptCookie(cookie: ChromeCookie): Promise { + return decryptWindows(cookie.encrypted_value); +} + +async function toCookie(chromeCookie: ChromeCookie): Promise { + return { + value: await decryptCookie(chromeCookie), + host: chromeCookie.host_key, + path: chromeCookie.path, + name: chromeCookie.name, + }; +} + +export class ChromeWindowsCookieProvider implements CookieProvider { + constructor(private db: ChromeCookieRepository) {} + + async getCookie( + domain: string, + cookieName: string, + ): Promise { + const chromeCookie = this.db.findCookie(cookieName, domain); + if (!chromeCookie) return undefined; + return toCookie(chromeCookie); + } + + async listCookies(): Promise { + return Promise.all(this.db.listCookies().map(toCookie)); + } +} diff --git a/src/chrome/ChromeWindowsCookieProvider.unit.test.ts b/src/chrome/ChromeWindowsCookieProvider.unit.test.ts new file mode 100644 index 0000000..94450d9 --- /dev/null +++ b/src/chrome/ChromeWindowsCookieProvider.unit.test.ts @@ -0,0 +1,44 @@ +import { ChromeCookie, ChromeCookieRepository } from './ChromeCookieRepository'; +import { ChromeWindowsCookieProvider } from './ChromeWindowsCookieProvider'; + +jest.mock('./decrypt', () => ({ + decryptWindows: jest.fn().mockResolvedValue('foo'), +})); + +const fakeBuffer = Buffer.from('encrypted'); + +describe('chrome windows cookie provider', () => { + it('should fetch and decrypt cookie', async () => { + const mockFindCookie = jest + .fn() + .mockImplementation( + (cookieName: string, domain: string): ChromeCookie => { + return { + host_key: domain, + path: '/', + name: cookieName, + encrypted_value: fakeBuffer, + }; + }, + ); + const mockDb: ChromeCookieRepository = { + findCookie: mockFindCookie, + listCookies(): ChromeCookie[] { + throw new Error('Function not implemented.'); + }, + }; + + const provider = new ChromeWindowsCookieProvider(mockDb); + + const cookie = await provider.getCookie('.test.com', 'some_cookie'); + + expect(cookie).toEqual({ + value: 'foo', + host: '.test.com', + path: '/', + name: 'some_cookie', + }); + + expect(mockFindCookie).toHaveBeenCalledWith('some_cookie', '.test.com'); + }); +}); diff --git a/src/chrome/getDerivedKey.ts b/src/chrome/getDerivedKey.ts index 537a273..ae38aef 100644 --- a/src/chrome/getDerivedKey.ts +++ b/src/chrome/getDerivedKey.ts @@ -6,7 +6,7 @@ const promisedPbkdf2 = promisify(crypto.pbkdf2); const SALT = 'saltysalt'; -async function getMacDerivedKey( +export async function getMacDerivedKey( keyLength: number, iterations: number, ): Promise { diff --git a/src/chrome/index.ts b/src/chrome/index.ts index c6799a5..13d6394 100644 --- a/src/chrome/index.ts +++ b/src/chrome/index.ts @@ -1,14 +1,14 @@ import { existsSync, readdirSync } from 'fs'; import { join } from 'path'; +import { CookieProvider } from '../CookieProvider'; import { Cookie } from '../types'; import { mergeDefaults } from '../utils'; import { ChromeCookieDatabase } from './ChromeCookieDatabase'; +import { ChromeLinuxCookieProvider } from './ChromeLinuxCookieProvider'; +import { ChromeMacosCookieProvider } from './ChromeMacosCookieProvider'; +import { ChromeWindowsCookieProvider } from './ChromeWindowsCookieProvider'; -import { decrypt, decryptWindows } from './decrypt'; -import { getDerivedKey } from './getDerivedKey'; -import { getDomain, getIterations, getCookiesPath, getPath } from './util'; - -const KEYLENGTH = 16; +import { getCookiesPath, getPath } from './util'; export interface GetChromeCookiesOptions { profile: string; @@ -18,36 +18,24 @@ const defaultOptions: GetChromeCookiesOptions = { profile: 'Default', }; -/** - * @deprecated Replaced by getCookie - */ +function getChromeCookieProvider(profile: string): CookieProvider { + const path = getCookiesPath(profile); + const db = new ChromeCookieDatabase(path); + if (process.platform === 'darwin') return new ChromeMacosCookieProvider(db); + if (process.platform === 'linux') return new ChromeLinuxCookieProvider(db); + if (process.platform === 'win32') return new ChromeWindowsCookieProvider(db); + + throw new Error(`Platform ${process.platform} is not supported`); +} + export async function getChromeCookie( - url: string, + domain: string, cookieName: string, options?: Partial, -): Promise { +): Promise { const config = mergeDefaults(defaultOptions, options); - const path = getCookiesPath(config.profile); - const domain = getDomain(url); - - const db = new ChromeCookieDatabase(path); - - // const cookie = tryGetCookie(path, domain, cookieName); - const cookie = db.findCookie(cookieName, domain); - - if (!cookie) return undefined; - - if (process.platform === 'darwin' || process.platform === 'linux') { - const iterations = getIterations(); - const derivedKey = await getDerivedKey(KEYLENGTH, iterations); - return decrypt(derivedKey, cookie.encrypted_value, KEYLENGTH); - } - - if (process.platform === 'win32') { - return decryptWindows(cookie.encrypted_value); - } - - throw new Error(`Platform ${process.platform} is not supported`); + const provider = getChromeCookieProvider(config.profile); + return provider.getCookie(domain, cookieName); } export async function listChromeProfiles(): Promise { @@ -61,42 +49,6 @@ export async function listChromeCookies( options?: Partial, ): Promise { const config = mergeDefaults(defaultOptions, options); - const path = getCookiesPath(config.profile); - const db = new ChromeCookieDatabase(path); - const cookies = db.listCookies(); - const decryptedCookies = await Promise.all( - cookies.map(async (cookie): Promise => { - if (cookie.value) - return { - name: cookie.name, - host: cookie.host_key, - path: cookie.path, - value: cookie.value, - }; - if (process.platform === 'darwin' || process.platform === 'linux') { - const iterations = getIterations(); - const derivedKey = await getDerivedKey(KEYLENGTH, iterations); - const value = decrypt(derivedKey, cookie.encrypted_value, KEYLENGTH); - return { - name: cookie.name, - host: cookie.host_key, - path: cookie.path, - value, - }; - } - - if (process.platform === 'win32') { - const value = decryptWindows(cookie.encrypted_value); - return { - name: cookie.name, - host: cookie.host_key, - path: cookie.path, - value, - }; - } - throw new Error('Failed to decrypt cookie'); - }), - ); - - return decryptedCookies; + const provider = getChromeCookieProvider(config.profile); + return provider.listCookies(); } diff --git a/src/chrome/util.ts b/src/chrome/util.ts index e3c91fd..c9ffc86 100644 --- a/src/chrome/util.ts +++ b/src/chrome/util.ts @@ -1,11 +1,4 @@ import { homedir } from 'os'; -import tld from 'tldjs'; - -export function getDomain(url: string): string { - const domain = tld.getDomain(url); - if (domain) return domain; - throw new Error(`Failed to extract domain from URL ${url}`); -} export function getPath(): string { if (process.platform === 'darwin') @@ -27,10 +20,3 @@ export function getCookiesPath(profile: string): string { throw new Error(`Platform ${process.platform} is not supported`); } - -export function getIterations(): number { - if (process.platform === 'darwin') return 1003; - if (process.platform === 'linux') return 1; - - throw new Error(`Platform ${process.platform} is not supported`); -} diff --git a/src/chrome/util.unit.test.ts b/src/chrome/util.unit.test.ts index 28221a7..1024789 100644 --- a/src/chrome/util.unit.test.ts +++ b/src/chrome/util.unit.test.ts @@ -1,25 +1,7 @@ import { homedir } from 'os'; -import { getDomain, getIterations, getCookiesPath, getPath } from './util'; +import { getCookiesPath, getPath } from './util'; import { mockPlatform, restorePlatform } from '../../test/util'; -describe('getDomain', () => { - it('should extract domain from github.com', () => { - expect(getDomain('https://github.com')).toEqual('github.com'); - }); - - it('should extract domain from https://some.web.site.com/some/page', () => { - expect(getDomain('https://some.web.site.com/some/page')).toEqual( - 'site.com', - ); - }); - - it('should fail to extract domain', () => { - expect(() => getDomain('foo')).toThrowError( - 'Failed to extract domain from URL foo', - ); - }); -}); - describe('getPath', () => { afterEach(() => { restorePlatform(); @@ -91,25 +73,3 @@ describe('getCookiesPath', () => { ); }); }); - -describe('getIterations', () => { - afterEach(restorePlatform); - - it('should get correct macos iterations', async () => { - mockPlatform('darwin'); - - expect(getIterations()).toEqual(1003); - }); - - it('should get correct linux iterations', async () => { - mockPlatform('linux'); - - expect(getIterations()).toEqual(1); - }); - - it('should throw if invalid os', () => { - mockPlatform('win32'); - - expect(() => getIterations()).toThrow('Platform win32 is not supported'); - }); -}); diff --git a/src/firefox/FirefoxCookieDatabase.test.ts b/src/firefox/FirefoxCookieDatabase.test.ts index d5395a3..8f43c86 100644 --- a/src/firefox/FirefoxCookieDatabase.test.ts +++ b/src/firefox/FirefoxCookieDatabase.test.ts @@ -17,10 +17,10 @@ describe('FirefoxCookieDatabase', () => { const cookie = db.findCookie('someCookie', '.domain.com'); - expect(cookie).toEqual('foo'); + expect(cookie).toEqual({ value: 'foo' }); expect(getFn).toHaveBeenCalled(); expect(prepareFn).toHaveBeenCalledWith( - "SELECT value from moz_cookies WHERE name like 'someCookie' AND host like '%.domain.com'", + "SELECT name, value, host, path from moz_cookies WHERE name like 'someCookie' AND host like '%.domain.com'", ); expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, { readonly: true, diff --git a/src/firefox/FirefoxCookieDatabase.ts b/src/firefox/FirefoxCookieDatabase.ts index 80c6b2f..d4da69c 100644 --- a/src/firefox/FirefoxCookieDatabase.ts +++ b/src/firefox/FirefoxCookieDatabase.ts @@ -1,27 +1,16 @@ import sqlite from 'better-sqlite3'; +import { FirefoxCookieRepository, MozCookie } from './FirefoxCookieRepository'; -type MozCookie = { - name: string; - value: string; - host: string; - path: string; -}; +export class FirefoxCookieDatabase implements FirefoxCookieRepository { + constructor(private path: string) {} -export class FirefoxCookieDatabase { - path: string; - - constructor(path: string) { - this.path = path; - } - - findCookie(cookieName: string, domain: string): string | undefined { + findCookie(cookieName: string, domain: string): MozCookie | undefined { const db = sqlite(this.path, { readonly: true, fileMustExist: true }); const statement = db.prepare( - `SELECT value from moz_cookies WHERE name like '${cookieName}' AND host like '%${domain}'`, + `SELECT name, value, host, path from moz_cookies WHERE name like '${cookieName}' AND host like '%${domain}'`, ); - const res = statement.get(); - return res?.value; + return statement.get(); } listCookies(): MozCookie[] { diff --git a/src/firefox/FirefoxCookieProvider.ts b/src/firefox/FirefoxCookieProvider.ts new file mode 100644 index 0000000..135a5d4 --- /dev/null +++ b/src/firefox/FirefoxCookieProvider.ts @@ -0,0 +1,18 @@ +import { CookieProvider } from '../CookieProvider'; +import { Cookie } from '../types'; +import { FirefoxCookieRepository } from './FirefoxCookieRepository'; + +export class FirefoxCookieProvider implements CookieProvider { + constructor(private db: FirefoxCookieRepository) {} + + async getCookie( + domain: string, + cookieName: string, + ): Promise { + return this.db.findCookie(cookieName, domain); + } + + async listCookies(): Promise { + return this.db.listCookies(); + } +} diff --git a/src/firefox/FirefoxCookieRepository.ts b/src/firefox/FirefoxCookieRepository.ts new file mode 100644 index 0000000..ec88a41 --- /dev/null +++ b/src/firefox/FirefoxCookieRepository.ts @@ -0,0 +1,11 @@ +export type MozCookie = { + name: string; + value: string; + host: string; + path: string; +}; + +export interface FirefoxCookieRepository { + findCookie(cookieName: string, domain: string): MozCookie | undefined; + listCookies(): MozCookie[]; +} diff --git a/src/firefox/index.ts b/src/firefox/index.ts index f9c7b84..6e20829 100644 --- a/src/firefox/index.ts +++ b/src/firefox/index.ts @@ -1,45 +1,9 @@ -import { readFileSync } from 'fs'; -import { homedir } from 'os'; import { join } from 'path'; -import * as ini from 'ini'; -import { getDomain } from 'tldjs'; import { mergeDefaults } from '../utils'; import { FirefoxCookieDatabase } from './FirefoxCookieDatabase'; import { Cookie } from '../types'; - -function getUserDirectory(): string { - switch (process.platform) { - case 'darwin': - return join(homedir(), '/Library/Application Support/Firefox'); - case 'linux': - return join(homedir(), '/.mozilla/firefox'); - case 'win32': - return join(process.env.APPDATA!, '/Mozilla/Firefox'); - default: - throw new Error(`Platform ${process.platform} is not supported`); - } -} - -type FirefoxProfile = { - Name: string; - IsRelative: number; - Path: string; - Default?: number; -}; - -function getProfiles(): FirefoxProfile[] { - const userDirectory = getUserDirectory(); - const fileData = readFileSync(join(userDirectory, 'profiles.ini'), { - encoding: 'utf8', - }); - - const iniData = ini.parse(fileData); - return Object.keys(iniData) - .filter((key) => { - return typeof key === 'string' && key.match(/^Profile/); - }) - .map((key) => iniData[key]); -} +import { getProfiles, getUserDirectory } from './profiles'; +import { FirefoxCookieProvider } from './FirefoxCookieProvider'; function getCookieFilePath(profile: string): string { const profiles = getProfiles(); @@ -60,28 +24,26 @@ export async function listFirefoxProfiles(): Promise { return getProfiles().map((p) => p.Name); } -/** - * @deprecated Replaced by getCookie - */ +function getFirefoxCookieProvider(profile: string): FirefoxCookieProvider { + const cookieFilePath = getCookieFilePath(profile); + const db = new FirefoxCookieDatabase(cookieFilePath); + return new FirefoxCookieProvider(db); +} export async function getFirefoxCookie( - url: string, + domain: string, cookieName: string, options?: Partial, -): Promise { +): Promise { const config = mergeDefaults(defaultOptions, options); - const domain = getDomain(url); - if (!domain) throw new Error('Could not extract domain from URL'); - const cookieFilePath = getCookieFilePath(config.profile); - const db = new FirefoxCookieDatabase(cookieFilePath); - return db.findCookie(cookieName, domain); + const cookieRepo = getFirefoxCookieProvider(config.profile); + return cookieRepo.getCookie(domain, cookieName); } export async function listFirefoxCookies( options?: Partial, ): Promise { const config = mergeDefaults(defaultOptions, options); - const cookieFilePath = getCookieFilePath(config.profile); - const db = new FirefoxCookieDatabase(cookieFilePath); - return db.listCookies(); + const cookieRepo = getFirefoxCookieProvider(config.profile); + return cookieRepo.listCookies(); } diff --git a/src/firefox/profiles.ts b/src/firefox/profiles.ts new file mode 100644 index 0000000..838c597 --- /dev/null +++ b/src/firefox/profiles.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'fs'; +import * as ini from 'ini'; +import { homedir } from 'os'; +import { join } from 'path'; + +export function getUserDirectory(): string { + switch (process.platform) { + case 'darwin': + return join(homedir(), '/Library/Application Support/Firefox'); + case 'linux': + return join(homedir(), '/.mozilla/firefox'); + case 'win32': + return join(process.env.APPDATA!, '/Mozilla/Firefox'); + default: + throw new Error(`Platform ${process.platform} is not supported`); + } +} +export type FirefoxProfile = { + Name: string; + IsRelative: number; + Path: string; + Default?: number; +}; + +export function getProfiles(): FirefoxProfile[] { + const userDirectory = getUserDirectory(); + const fileData = readFileSync(join(userDirectory, 'profiles.ini'), { + encoding: 'utf8', + }); + + const iniData = ini.parse(fileData); + return Object.keys(iniData) + .filter((key) => { + return typeof key === 'string' && key.match(/^Profile/); + }) + .map((key) => iniData[key]); +} diff --git a/src/index.ts b/src/index.ts index 74ce5f7..2a6d804 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,16 +5,16 @@ import { listChromeProfiles, } from './chrome'; import { - GetFirefoxCookieOptions, getFirefoxCookie, + GetFirefoxCookieOptions, listFirefoxCookies, listFirefoxProfiles, } from './firefox'; import { Cookie } from './types'; import { assertUnreachable } from './utils'; -export * from './chrome'; -export * from './firefox'; +// export * from './chrome'; +// export * from './firefox'; export * from './types'; export enum Browser { @@ -24,7 +24,7 @@ export enum Browser { interface BaseGetCookieConfig { browser: Browser; - url: string; + domain: string; cookieName: string; } @@ -44,12 +44,12 @@ export function listSupportedBrowsers(): Browser[] { export async function getCookie( config: GetFirefoxCookieConfig | GetChromeCookieConfig, -): Promise { +): Promise { switch (config.browser) { case Browser.Firefox: - return getFirefoxCookie(config.url, config.cookieName, config.options); + return getFirefoxCookie(config.domain, config.cookieName, config.options); case Browser.Chrome: - return getChromeCookie(config.url, config.cookieName, config.options); + return getChromeCookie(config.domain, config.cookieName, config.options); default: return assertUnreachable(config); } diff --git a/test/chrome/linux.unit.test.ts b/test/chrome/linux.unit.test.ts index d98bb86..3e6b76f 100644 --- a/test/chrome/linux.unit.test.ts +++ b/test/chrome/linux.unit.test.ts @@ -6,6 +6,9 @@ jest.mock('better-sqlite3', () => prepare: jest.fn().mockReturnValue({ get: jest.fn().mockReturnValue({ encrypted_value: Buffer.from('1MNujnd6tlf09xoB4tvBLQ==', 'base64'), + name: 'foo', + path: '/', + host_key: '.someUrl.com', }), all: jest.fn().mockReturnValue([ { @@ -35,11 +38,16 @@ describe('chrome - linux', () => { it('gets and decrypts linux cookie', async () => { const res = await getCookie({ browser: Browser.Chrome, - url: 'https://someUrl.com', + domain: '.someUrl.com', cookieName: 'foo', }); - expect(res).toEqual('bar'); + expect(res).toEqual({ + value: 'bar', + host: '.someUrl.com', + path: '/', + name: 'foo', + }); }); it('lists and decrypts linux cookies', async () => { diff --git a/test/firefox.unit.test.ts b/test/firefox.unit.test.ts index 3e9a4d6..3194636 100644 --- a/test/firefox.unit.test.ts +++ b/test/firefox.unit.test.ts @@ -66,10 +66,10 @@ describe('firefox get cookie', () => { expect( await getCookie({ browser: Browser.Firefox, - url: 'https://some.url', + domain: '.some.url', cookieName: 'some-cookie', }), - ).toEqual('foo'); + ).toEqual({ value: 'foo' }); expect(sqlite).toHaveBeenCalledWith( `${homedir()}/Library/Application Support/Firefox/Profiles/tfhz7h6q.default-release/cookies.sqlite`, { fileMustExist: true, readonly: true }, @@ -116,10 +116,10 @@ describe('firefox get cookie', () => { expect( await getCookie({ browser: Browser.Firefox, - url: 'https://some.url', + domain: '.some.url', cookieName: 'some-cookie', }), - ).toEqual('foo'); + ).toEqual({ value: 'foo' }); expect(sqlite).toHaveBeenCalledWith( `${homedir()}/.mozilla/firefox/Profiles/tfhz7h6q.default-release/cookies.sqlite`, { fileMustExist: true, readonly: true }, @@ -151,10 +151,10 @@ describe('firefox get cookie', () => { expect( await getCookie({ browser: Browser.Firefox, - url: 'https://some.url', + domain: '.some.url', cookieName: 'some-cookie', }), - ).toEqual('foo'); + ).toEqual({ value: 'foo' }); expect(sqlite).toHaveBeenCalledWith( `C:/foo/Mozilla/Firefox/Profiles/tfhz7h6q.default-release/cookies.sqlite`, { fileMustExist: true, readonly: true }, @@ -182,7 +182,7 @@ describe('firefox get cookie', () => { await expect( getCookie({ browser: Browser.Firefox, - url: 'https://someurl.com', + domain: '.some.url', cookieName: 'some-cookie', }), ).rejects.toThrow('Platform freebsd is not supported'); diff --git a/yarn.lock b/yarn.lock index 9e46821..d8b1f84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3522,11 +3522,6 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -4030,13 +4025,6 @@ throat@^6.0.1: resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== -tldjs@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tldjs/-/tldjs-2.3.1.tgz#cf09c3eb5d7403a9e214b7d65f3cf9651c0ab039" - integrity sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw== - dependencies: - punycode "^1.4.1" - tmpl@1.0.x: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"