@mherod/get-cookie 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,24 +1,56 @@
1
1
  {
2
2
  "name": "@mherod/get-cookie",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Node.js module for querying a local user's Chrome cookie",
5
- "main": "index.js",
6
- "bin": "cli.js",
5
+ "source": "src/index.js",
6
+ "bin": "dist/cli.js",
7
+ "main": "dist/main.js",
8
+ "module": "dist/module.js",
7
9
  "publishConfig": {
8
10
  "access": "public"
9
11
  },
10
12
  "scripts": {
13
+ "clean": "git clean -fXd",
14
+ "build": "parcel build",
11
15
  "test": "jest",
12
- "installGlobal": "rm -rf /usr/local/bin/get-cookie ; npm install --force --global ."
16
+ "postbuild": "npm run install:global",
17
+ "install:global": "npm install --force --global .",
18
+ "postinstall:global": "find $NVM_BIN -exec chmod +x {} \\; || true",
19
+ "prettier": "prettier --write 'src/{**/*,*}.{js,json}'"
20
+ },
21
+ "targets": {
22
+ "main": {
23
+ "source": "src/index.js",
24
+ "distDir": "dist",
25
+ "engines": {
26
+ "node": "16"
27
+ }
28
+ },
29
+ "cli": {
30
+ "source": "src/cli.js",
31
+ "distDir": "dist",
32
+ "engines": {
33
+ "node": "16"
34
+ }
35
+ }
36
+ },
37
+ "@parcel/transformer-js": {
38
+ "inlineFS": false,
39
+ "inlineEnvironment": false
40
+ },
41
+ "engines": {
42
+ "node": "16"
13
43
  },
14
44
  "keywords": [],
15
45
  "author": "Matthew Herod",
16
46
  "license": "ISC",
17
47
  "dependencies": {
18
48
  "jsonwebtoken": "^8.5.1",
49
+ "lodash": "^4.17.21",
19
50
  "sqlite3": "^5.0.8"
20
51
  },
21
52
  "devDependencies": {
22
- "jest": "^28.1.0"
53
+ "jest": "^28.1.0",
54
+ "prettier": "^2.7.1"
23
55
  }
24
56
  }
package/src/cli.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { version } from "../package.json";
4
+ import { env } from "./global";
5
+ const { getCookie } = require("./index");
6
+
7
+ const argv = process.argv;
8
+ if (argv) {
9
+ if (argv.length > 2) {
10
+ if (argv.includes("--version") || argv.includes("-v")) {
11
+ console.log(version);
12
+ return;
13
+ }
14
+
15
+ const name = argv[2];
16
+
17
+ let domain;
18
+ if (argv[3] != null && argv[3].indexOf(".") > -1) {
19
+ domain = argv[3];
20
+ } else {
21
+ domain = "%";
22
+ }
23
+
24
+ if (argv.includes("--require-jwt")) {
25
+ env.REQUIRE_JWT = "true";
26
+ }
27
+ if (argv.includes("--verbose")) {
28
+ env.VERBOSE = "true";
29
+ }
30
+ if (argv.includes("--chrome-only")) {
31
+ env.CHROME_ONLY = "true";
32
+ }
33
+ if (argv.includes("--firefox-only")) {
34
+ env.FIREFOX_ONLY = "true";
35
+ }
36
+ if (argv.includes("--ignore-expired")) {
37
+ env.IGNORE_EXPIRED = "true";
38
+ }
39
+
40
+ if (env.VERBOSE) {
41
+ console.log("Verbose mode", argv);
42
+ }
43
+
44
+ getCookie({
45
+ name: name,
46
+ domain: domain,
47
+ requireJwt: env.REQUIRE_JWT === "true",
48
+ })
49
+ .then((cookie) => {
50
+ if (typeof cookie === "string") {
51
+ console.log(cookie);
52
+ }
53
+ })
54
+ .catch((err) => {
55
+ console.error(err);
56
+ });
57
+ }
58
+ }
package/src/global.js ADDED
@@ -0,0 +1,3 @@
1
+ const { merge } = require("lodash");
2
+ export const env = {};
3
+ merge(env, process.env);
package/src/index.js ADDED
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import crypto from "crypto";
5
+ import { env } from "./global";
6
+
7
+ if (process.platform !== "darwin") {
8
+ throw new Error("This script only works on macOS");
9
+ }
10
+
11
+ const {
12
+ execSimple,
13
+ toStringOrNull,
14
+ doSqliteQuery1,
15
+ toStringValue,
16
+ } = require("./utils");
17
+ const jsonwebtoken = require("jsonwebtoken");
18
+
19
+ function isValidJwt(token) {
20
+ if (typeof token !== "string") {
21
+ return false;
22
+ }
23
+ try {
24
+ const result = jsonwebtoken.decode(token, { complete: true });
25
+ if (env.VERBOSE) {
26
+ console.log(result);
27
+ }
28
+ return true;
29
+ } catch (err) {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ *
36
+ * @returns {Promise<string>}
37
+ */
38
+ async function getChromePassword() {
39
+ return execSimple(
40
+ 'security find-generic-password -w -s "Chrome Safe Storage"'
41
+ );
42
+ }
43
+
44
+ /**
45
+ *
46
+ * @param params
47
+ * @returns {Promise<string>}
48
+ */
49
+ async function getCookie(params) {
50
+ if (env.CHROME_ONLY) {
51
+ if (env.VERBOSE) {
52
+ console.log("chrome only");
53
+ }
54
+ return getChromeCookie(params)
55
+ .then((cookie) => {
56
+ if (cookie && cookie.length > 0) {
57
+ return toStringValue(cookie);
58
+ }
59
+ })
60
+ .catch((err) => {
61
+ if (env.VERBOSE) {
62
+ console.error(err);
63
+ }
64
+ });
65
+ } else if (env.FIREFOX_ONLY) {
66
+ if (env.VERBOSE) {
67
+ console.log("firefox only");
68
+ }
69
+ return getFirefoxCookie(params)
70
+ .then((b) => b.toString("utf8"))
71
+ .then((cookie) => {
72
+ if (cookie && cookie.length > 0) {
73
+ return toStringValue(cookie);
74
+ } else {
75
+ console.log("No cookie found");
76
+ }
77
+ })
78
+ .catch((err) => {
79
+ if (env.VERBOSE) {
80
+ console.error("Error getting Firefox cookie", err);
81
+ }
82
+ });
83
+ } else {
84
+ return await getChromeCookie(params)
85
+ .catch((err) => {
86
+ if (env.VERBOSE) {
87
+ console.error("Error getting Chrome cookie", err);
88
+ }
89
+ })
90
+ .then((r) => {
91
+ if (typeof r === "string" && r.trim().length > 0) {
92
+ return r;
93
+ } else {
94
+ return getFirefoxCookie(params).catch((err) => {
95
+ if (env.VERBOSE) {
96
+ console.error("Error getting Firefox cookie", err);
97
+ }
98
+ });
99
+ }
100
+ })
101
+ .catch((e) => {
102
+ if (env.VERBOSE) {
103
+ console.error("Error getting Chrome or Firefox cookie", e);
104
+ }
105
+ })
106
+ .then(toStringValue);
107
+ }
108
+ }
109
+
110
+ /**
111
+ *
112
+ * @param name
113
+ * @param domain
114
+ * @returns {Promise<Buffer>}
115
+ */
116
+ async function getFirefoxCookie({ name, domain }) {
117
+ if (name && typeof name !== "string") {
118
+ throw new Error("name must be a string");
119
+ }
120
+ if (domain && typeof domain !== "string") {
121
+ throw new Error("domain must be a string");
122
+ }
123
+ const [file] = await findAllFiles({
124
+ path: `${env.HOME}/Library/Application Support/Firefox/Profiles`,
125
+ name: "cookies.sqlite",
126
+ });
127
+ if (file && !fs.existsSync(file)) {
128
+ throw new Error(`File ${file} does not exist`);
129
+ }
130
+ if (env.VERBOSE) {
131
+ console.log(`Trying Firefox cookie ${name} for domain ${domain}`);
132
+ }
133
+ let sql;
134
+ sql = "SELECT value FROM moz_cookies";
135
+ if (typeof name === "string" || typeof domain === "string") {
136
+ sql += ` WHERE `;
137
+ if (typeof name === "string") {
138
+ sql += `name = '${name}'`;
139
+ if (typeof domain === "string") {
140
+ sql += ` AND `;
141
+ }
142
+ }
143
+ if (typeof domain === "string") {
144
+ sql += `host LIKE '${domain}';`;
145
+ }
146
+ }
147
+ return await doSqliteQuery1(file, sql).then((rows) => {
148
+ return rows.map(toStringOrNull).find((v) => v !== null);
149
+ });
150
+ }
151
+
152
+ const defaultChromeRoot = `${env.HOME}/Library/Application Support/Google/Chrome`;
153
+ const defaultChromeCookies = `${defaultChromeRoot}/Default/Cookies`;
154
+
155
+ async function getEncryptedChromeCookie({
156
+ name,
157
+ domain,
158
+ file = defaultChromeCookies,
159
+ }) {
160
+ if (name && typeof name !== "string") {
161
+ throw new Error("name must be a string");
162
+ }
163
+ if (domain && typeof domain !== "string") {
164
+ throw new Error("domain must be a string");
165
+ }
166
+ if (file && typeof file !== "string") {
167
+ throw new Error("file must be a string");
168
+ }
169
+ if (!fs.existsSync(file)) {
170
+ throw new Error(`File ${file} does not exist`);
171
+ }
172
+ if (env.VERBOSE) {
173
+ console.log(
174
+ `Trying Chrome (at ${file
175
+ .split("/")
176
+ .slice(-3)
177
+ .join("/")}) cookie ${name} for domain ${domain}`
178
+ );
179
+ }
180
+ let sql;
181
+ sql = `SELECT encrypted_value FROM cookies`;
182
+ if (typeof name === "string" || typeof domain === "string") {
183
+ sql += ` WHERE `;
184
+ if (typeof name === "string") {
185
+ sql += `name = '${name}'`;
186
+ if (typeof domain === "string") {
187
+ sql += ` AND `;
188
+ }
189
+ }
190
+ if (typeof domain === "string") {
191
+ sql += `host_key LIKE '${domain}';`;
192
+ }
193
+ }
194
+ return await doSqliteQuery1(file, sql);
195
+ }
196
+
197
+ /**
198
+ *
199
+ * @param {string} password
200
+ * @param {Buffer} encryptedData
201
+ * @returns {Promise<string>}
202
+ */
203
+ async function decrypt(password, encryptedData) {
204
+ if (typeof password !== "string") {
205
+ throw new Error("password must be a string: " + password);
206
+ }
207
+ let encryptedData1;
208
+ encryptedData1 = encryptedData;
209
+ if (encryptedData1 == null || typeof encryptedData1 !== "object") {
210
+ throw new Error("encryptedData must be a object: " + encryptedData1);
211
+ }
212
+ if (!(encryptedData1 instanceof Buffer)) {
213
+ if (Array.isArray(encryptedData1) && encryptedData1[0] instanceof Buffer) {
214
+ [encryptedData1] = encryptedData1;
215
+ if (env.VERBOSE) {
216
+ console.log(
217
+ `encryptedData is an array of buffers, selected first: ${encryptedData1}`
218
+ );
219
+ }
220
+ } else {
221
+ throw new Error("encryptedData must be a Buffer: " + encryptedData1);
222
+ }
223
+ encryptedData1 = Buffer.from(encryptedData1);
224
+ }
225
+ if (env.VERBOSE) {
226
+ console.log(`Trying to decrypt with password ${password}`);
227
+ }
228
+ return new Promise((resolve, reject) => {
229
+ crypto.pbkdf2(password, "saltysalt", 1003, 16, "sha1", (error, buffer) => {
230
+ try {
231
+ if (error) {
232
+ if (env.VERBOSE) {
233
+ console.log("Error doing pbkdf2", error);
234
+ }
235
+ reject(error);
236
+ return;
237
+ }
238
+
239
+ if (buffer.length !== 16) {
240
+ if (env.VERBOSE) {
241
+ console.log(
242
+ "Error doing pbkdf2, buffer length is not 16",
243
+ buffer.length
244
+ );
245
+ }
246
+ reject(new Error("Buffer length is not 16"));
247
+ return;
248
+ }
249
+
250
+ const iv = new Buffer.from(new Array(17).join(" "), "binary");
251
+ const decipher = crypto.createDecipheriv("aes-128-cbc", buffer, iv);
252
+ decipher.setAutoPadding(false);
253
+
254
+ if (encryptedData1 && encryptedData1.slice) {
255
+ encryptedData1 = encryptedData1.slice(3);
256
+ }
257
+
258
+ if (encryptedData1.length % 16 !== 0) {
259
+ if (env.VERBOSE) {
260
+ console.log(
261
+ "Error doing pbkdf2, encryptedData length is not a multiple of 16",
262
+ encryptedData1.length
263
+ );
264
+ }
265
+ reject(new Error("encryptedData length is not a multiple of 16"));
266
+ return;
267
+ }
268
+
269
+ let decoded = decipher.update(encryptedData1);
270
+ try {
271
+ decipher.final("utf-8");
272
+ } catch (e) {
273
+ if (env.VERBOSE) {
274
+ console.log("Error doing decipher.final()", e);
275
+ }
276
+ reject(e);
277
+ return;
278
+ }
279
+
280
+ let padding = decoded[decoded.length - 1];
281
+ if (padding) {
282
+ decoded = decoded.slice(0, 0 - padding);
283
+ }
284
+ // noinspection JSCheckFunctionSignatures
285
+ decoded = decoded.toString("utf8");
286
+ resolve(decoded);
287
+ } catch (e) {
288
+ reject(e);
289
+ }
290
+ });
291
+ });
292
+ }
293
+
294
+ /**
295
+ *
296
+ * @param {string|undefined} name
297
+ * @param {string} domain
298
+ * @param {boolean} requireJwt
299
+ * @returns {Promise<string>}
300
+ */
301
+ async function getChromeCookie({ name, domain = "%", requireJwt = false }) {
302
+ if (name && typeof name !== "string") {
303
+ throw new Error("name must be a string");
304
+ }
305
+ if (typeof domain !== "string") {
306
+ throw new Error("domain must be a string");
307
+ }
308
+ const encryptedDataItems = await findAllFiles({
309
+ path: defaultChromeRoot,
310
+ name: "Cookies",
311
+ })
312
+ .then((files) => {
313
+ const promises = files.map((file) => {
314
+ return getEncryptedChromeCookie({
315
+ name: name,
316
+ domain: domain,
317
+ file: file,
318
+ }).catch((e) => {
319
+ if (env.VERBOSE) {
320
+ console.log("Error getting encrypted cookie", e);
321
+ }
322
+ return [];
323
+ });
324
+ });
325
+ return Promise.all(promises)
326
+ .then((results) => results.flat())
327
+ .then((results) => {
328
+ if (env.VERBOSE) {
329
+ console.log("getEncryptedChromeCookie results", results);
330
+ }
331
+ return results.filter((result) => {
332
+ return result;
333
+ });
334
+ });
335
+ })
336
+ .catch((error) => {
337
+ if (env.VERBOSE) {
338
+ console.log("error", error);
339
+ }
340
+ return [];
341
+ });
342
+ const password = await getChromePassword();
343
+ if (env.VERBOSE) {
344
+ console.log("encryptedDataItems", encryptedDataItems);
345
+ }
346
+ const decrypted = encryptedDataItems
347
+ .filter((encryptedData) => {
348
+ return encryptedData != null && encryptedData.length > 0;
349
+ })
350
+ .map(async (encryptedData) => {
351
+ if (env.VERBOSE) {
352
+ console.log("Received encrypted", encryptedData);
353
+ }
354
+ let decrypted;
355
+ try {
356
+ decrypted = await decrypt(password, encryptedData);
357
+ } catch (e) {
358
+ if (env.VERBOSE) {
359
+ console.log("Error decrypting cookie", e);
360
+ }
361
+ return null;
362
+ }
363
+ if (decrypted) {
364
+ if (env.VERBOSE) {
365
+ console.log("Decrypted", decrypted);
366
+ }
367
+ return decrypted;
368
+ }
369
+ return null;
370
+ });
371
+ const results = await Promise.all(decrypted);
372
+ if (env.VERBOSE) {
373
+ console.log("results", results);
374
+ }
375
+ return results.map(toStringOrNull).find((result) => {
376
+ return (
377
+ typeof result === "string" &&
378
+ result.length > 0 &&
379
+ (requireJwt === false || isValidJwt(result))
380
+ );
381
+ });
382
+ }
383
+
384
+ /**
385
+ *
386
+ * @param path
387
+ * @param name
388
+ * @param rootSegments
389
+ * @param maxDepth
390
+ * @returns {Promise<[string]>}
391
+ */
392
+ async function findAllFiles({ path, name, maxDepth = 2 }) {
393
+ if (typeof path !== "string") {
394
+ throw new Error("path must be a string");
395
+ }
396
+ if (typeof name !== "string") {
397
+ throw new Error("name must be a string");
398
+ }
399
+
400
+ const rootSegments = path.split("/").length;
401
+
402
+ if (env.VERBOSE) {
403
+ console.log(`Searching for ${name} in ${path}`);
404
+ }
405
+ const files = [];
406
+ let readdirSync;
407
+ try {
408
+ readdirSync = fs.readdirSync(path);
409
+ } catch (e) {
410
+ if (env.VERBOSE) {
411
+ console.log(`Error reading ${path}`, e);
412
+ }
413
+ return files;
414
+ }
415
+ for (const file of readdirSync) {
416
+ const filePath = path + "/" + file;
417
+ let stat;
418
+ try {
419
+ stat = fs.statSync(filePath);
420
+ } catch (e) {
421
+ if (env.VERBOSE) {
422
+ console.error(`Error getting stat for ${filePath}`, e);
423
+ }
424
+ continue;
425
+ }
426
+ if (stat.isDirectory()) {
427
+ if (filePath.split("/").length < rootSegments + maxDepth) {
428
+ try {
429
+ const subFiles = await findAllFiles({
430
+ path: filePath,
431
+ name: name,
432
+ rootSegments: rootSegments,
433
+ maxDepth: 2,
434
+ });
435
+ files.push(...subFiles);
436
+ } catch (e) {
437
+ if (env.VERBOSE) {
438
+ console.error(e);
439
+ }
440
+ }
441
+ }
442
+ } else if (file === name) {
443
+ files.push(filePath);
444
+ }
445
+ }
446
+ if (env.VERBOSE) {
447
+ if (files.length > 0) {
448
+ console.log(`Found ${files.length} ${name} files`);
449
+ console.log(files);
450
+ }
451
+ }
452
+ return files;
453
+ }
454
+
455
+ // noinspection JSUnusedGlobalSymbols
456
+ module.exports = {
457
+ getDecryptedCookie: getChromeCookie,
458
+ getChromeCookie,
459
+ getFirefoxCookie,
460
+ getCookie,
461
+ decrypt,
462
+ };