Skip to content

Commit

Permalink
add listCookies function (#12)
Browse files Browse the repository at this point in the history
* separate out firefox db logic

* list firefox cookies

* list chrome cookies

* generic listCookies

* abstract out chrome cookie database

* tests

* add chrome list cookies test

* add firefox list cookies test

* eslint

* bump

* Update README.md
  • Loading branch information
Kalininator committed Sep 26, 2021
1 parent f154131 commit dd60dc5
Show file tree
Hide file tree
Showing 13 changed files with 399 additions and 45 deletions.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ yarn add cookie-thief
### Google Chrome

```javascript
const { getCookie, Browser } = require('cookie-thief')
const { getCookie, listCookies, Browser } = require('cookie-thief')

// Get a cookie from chrome browser for domain .github.com, searching for cookie named 'dotcom_user'
const cookie = await getCookie({
Expand All @@ -41,6 +41,21 @@ const cookie = await getCookie({
console.log(cookie);
// Will be a string if cookie is successfully found
// Will be undefined if not found

const cookies = await listCookies({
browser: Browser.Chrome,
});
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'
// }
//]

```

### Firefox
Expand All @@ -60,6 +75,20 @@ const cookie = await getCookie({
console.log(cookie);
// Will be a string if cookie is successfully found
// Will be undefined if not found

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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cookie-thief",
"version": "0.5.0",
"version": "0.6.0",
"description": "Steal browser cookies",
"main": "./lib/index.js",
"author": "Alex Kalinin (https://kalinin.uk)",
Expand Down
78 changes: 78 additions & 0 deletions src/chrome/ChromeCookieDatabase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as sqlite from 'better-sqlite3';
import { ChromeCookieDatabase } from './ChromeCookieDatabase';

jest.mock('better-sqlite3');

describe('ChromeCookieDatabase', () => {
it('get single cookie', () => {
const getFn = jest.fn().mockReturnValue({
host_key: '.domain.com',
path: '/',
name: 'someCookie',
encrypted_value: 'encrypted_foo',
});
const prepareFn = jest.fn().mockReturnValue({ get: getFn });
(sqlite as unknown as jest.Mock).mockReturnValue({
prepare: prepareFn,
});

const path = '/some/path/db.sqlite';

const db = new ChromeCookieDatabase(path);

const cookie = db.findCookie('someCookie', '.domain.com');

expect(cookie).toEqual({
encrypted_value: 'encrypted_foo',
host_key: '.domain.com',
name: 'someCookie',
path: '/',
});
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`,
);
expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, {
readonly: true,
fileMustExist: true,
});
});

it('get all cookies', () => {
const allFn = jest.fn().mockReturnValue([
{
host_key: '.domain.com',
path: '/',
name: 'someCookie',
encrypted_value: 'encrypted_foo',
},
]);
const prepareFn = jest.fn().mockReturnValue({ all: allFn });
(sqlite as unknown as jest.Mock).mockReturnValue({
prepare: prepareFn,
});

const path = '/some/path/db.sqlite';

const db = new ChromeCookieDatabase(path);

const cookie = db.listCookies();

expect(cookie).toEqual([
{
encrypted_value: 'encrypted_foo',
host_key: '.domain.com',
name: 'someCookie',
path: '/',
},
]);
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`,
);
expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, {
readonly: true,
fileMustExist: true,
});
});
});
42 changes: 42 additions & 0 deletions src/chrome/ChromeCookieDatabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import sqlite from 'better-sqlite3';

type BooleanNumber = 0 | 1;

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 {
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`,
);
return statement.get();
}

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`,
);
const cookies: ChromeCookie[] = statement.all();
return cookies;
}
}
2 changes: 1 addition & 1 deletion src/chrome/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function decrypt(
let decoded = decipher.update(data);

const final = decipher.final();
final.copy(decoded, decoded.length - 1);
final.copy(decoded, decoded.length ? decoded.length - 1 : 0);

const padding = decoded[decoded.length - 1];
if (padding) {
Expand Down
80 changes: 50 additions & 30 deletions src/chrome/index.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,13 @@
import sqlite from 'better-sqlite3';
import { Cookie } from '../types';
import { mergeDefaults } from '../utils';
import { ChromeCookieDatabase } from './ChromeCookieDatabase';

import { decrypt, decryptWindows } from './decrypt';
import { getDerivedKey } from './getDerivedKey';
import { getDomain, getIterations, getPath } from './util';

const KEYLENGTH = 16;

type BooleanNumber = 0 | 1;

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;
};

function tryGetCookie(
path: string,
domain: string,
cookieName: string,
): ChromeCookie | undefined {
const db = sqlite(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`,
);
return statement.get();
}

export interface GetChromeCookiesOptions {
profile: string;
}
Expand All @@ -55,7 +28,10 @@ export async function getChromeCookie(
const path = getPath(config.profile);
const domain = getDomain(url);

const cookie = tryGetCookie(path, domain, cookieName);
const db = new ChromeCookieDatabase(path);

// const cookie = tryGetCookie(path, domain, cookieName);
const cookie = db.findCookie(cookieName, domain);

if (!cookie) return undefined;

Expand All @@ -71,3 +47,47 @@ export async function getChromeCookie(

throw new Error(`Platform ${process.platform} is not supported`);
}

export async function listChromeCookies(
options?: Partial<GetChromeCookiesOptions>,
): Promise<Cookie[]> {
const config = mergeDefaults(defaultOptions, options);
const path = getPath(config.profile);
const db = new ChromeCookieDatabase(path);
const cookies = db.listCookies();
const decryptedCookies = await Promise.all(
cookies.map(async (cookie): Promise<Cookie> => {
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;
}
54 changes: 54 additions & 0 deletions src/firefox/FirefoxCookieDatabase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as sqlite from 'better-sqlite3';
import { FirefoxCookieDatabase } from './FirefoxCookieDatabase';

jest.mock('better-sqlite3');

describe('FirefoxCookieDatabase', () => {
it('get single cookie', () => {
const getFn = jest.fn().mockReturnValue({ value: 'foo' });
const prepareFn = jest.fn().mockReturnValue({ get: getFn });
(sqlite as unknown as jest.Mock).mockReturnValue({
prepare: prepareFn,
});

const path = '/some/path/db.sqlite';

const db = new FirefoxCookieDatabase(path);

const cookie = db.findCookie('someCookie', '.domain.com');

expect(cookie).toEqual('foo');
expect(getFn).toHaveBeenCalled();
expect(prepareFn).toHaveBeenCalledWith(
"SELECT value from moz_cookies WHERE name like 'someCookie' AND host like '%.domain.com'",
);
expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, {
readonly: true,
fileMustExist: true,
});
});

it('get all cookies', () => {
const allFn = jest.fn().mockReturnValue([{ value: 'foo' }]);
const prepareFn = jest.fn().mockReturnValue({ all: allFn });
(sqlite as unknown as jest.Mock).mockReturnValue({
prepare: prepareFn,
});

const path = '/some/path/db.sqlite';

const db = new FirefoxCookieDatabase(path);

const cookie = db.listCookies();

expect(cookie).toEqual([{ value: 'foo' }]);
expect(allFn).toHaveBeenCalled();
expect(prepareFn).toHaveBeenCalledWith(
'SELECT name, value, host, path from moz_cookies',
);
expect(sqlite as unknown as jest.Mock).toHaveBeenCalledWith(path, {
readonly: true,
fileMustExist: true,
});
});
});
35 changes: 35 additions & 0 deletions src/firefox/FirefoxCookieDatabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import sqlite from 'better-sqlite3';

type MozCookie = {
name: string;
value: string;
host: string;
path: string;
};

export class FirefoxCookieDatabase {
path: string;

constructor(path: string) {
this.path = path;
}

findCookie(cookieName: string, domain: string): string | 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}'`,
);
const res = statement.get();
return res?.value;
}

listCookies(): MozCookie[] {
const db = sqlite(this.path, { readonly: true, fileMustExist: true });
const statement = db.prepare(
`SELECT name, value, host, path from moz_cookies`,
);
const res: MozCookie[] = statement.all();
return res;
}
}
Loading

0 comments on commit dd60dc5

Please sign in to comment.