win-cliauth 1.0.3 → 1.0.4

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/LICENSE CHANGED
@@ -1,27 +1,27 @@
1
- Copyright (c) <2022>, <Pedro Lucas Magalhães de Oliveira>
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without
5
- modification, are permitted provided that the following conditions are met:
6
-
7
- * Redistributions of source code must retain the above copyright notice, this
8
- list of conditions and the following disclaimer.
9
-
10
- * Redistributions in binary form must reproduce the above copyright notice,
11
- this list of conditions and the following disclaimer in the documentation
12
- and/or other materials provided with the distribution.
13
-
14
- * Neither the name of the copyright holder nor the names of its
15
- contributors may be used to endorse or promote products derived from
16
- this software without specific prior written permission.
17
-
18
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
1
+ Copyright (c) <2022>, <Pedro Lucas Magalhães de Oliveira>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
27
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,53 +1,78 @@
1
- # win-cliauth
2
-
3
- win-cliauth is a CLI-based OTP (One-Time Password) authenticator management tool for Windows.
4
-
5
- ## Installation
6
-
7
- You can install win-cliauth globally using npm:
8
-
9
- npm install -g win-cliauth
10
-
11
- ## Usage
12
-
13
- win-cliauth provides a command-line interface for managing OTP (One-Time Password) authenticators. It supports the following commands:
14
-
15
- - `add <name> <secret-key>`: Adds an authenticator with the given name and secret key.
16
- - `add <name> <secret-key> --replace`: Adds an authenticator, replacing an existing one with the same name.
17
- - `remove <name>`: Removes the authenticator with the specified name.
18
- - `get <name>`: Retrieves the token from the authenticator with the specified name.
19
- - `get <name> --steam`: Retrieves the token from the Steam authenticator with the specified name.
20
- - `get <name> --clipboard`: Retrieves the token and adds it to the clipboard.
21
- - `list`: Lists all registered authenticators.
22
- - `list --showsecret`: Lists all registered authenticators, including the secret keys.
23
-
24
- To use win-cliauth, open a terminal or command prompt and run one of the commands listed above.
25
-
26
- For detailed information on the available commands and options, you can run:
27
-
28
- win-cliauth --help
29
-
30
- ## Security
31
-
32
- - win-cliauth encrypts the secret keys using the `ConvertTo-SecureString` command in PowerShell before storing them on disk.
33
- - The secret keys are stored in an encrypted file in the user's data directory, protected by the user's operating system.
34
- - Please note that the encrypted file can only be read on the same Windows user account where it was created. It may not be readable on other user accounts or operating systems.
35
-
36
- ## Credits
37
-
38
- This project uses the following open-source libraries:
39
-
40
- - [otpauth](https://github.com/hectorm/otpauth) - One Time Password (HOTP/TOTP) library for Node.js, Deno, Bun and browsers.
41
- - [steam-totp](https://github.com/DoctorMcKay/node-steam-totp) - A Node.js library for generating Steam-compatible 2FA tokens.
42
- - [yargs](https://github.com/yargs/yargs) - A command-line argument parsing library for Node.js.
43
-
44
- Please refer to the respective library documentation for detailed information on their usage, licensing, and contributions.
45
-
46
-
47
- ## Contributing
48
-
49
- Contributions are welcome! If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on [GitHub](https://github.com/pedrolucasmag/win-cliauth).
50
-
51
- ## License
52
-
1
+ # win-cliauth
2
+
3
+ win-cliauth is a CLI-based OTP (One-Time Password) authenticator management tool for Windows.
4
+
5
+ ## Installation
6
+
7
+ You can install win-cliauth globally using npm:
8
+
9
+ npm install -g win-cliauth
10
+
11
+ ## Usage
12
+
13
+ win-cliauth provides a command-line interface for managing OTP (One-Time Password) authenticators. It supports the following commands:
14
+
15
+ - `add <name>`: Adds an authenticator, prompting for the secret key without echoing it (it can also be piped through stdin).
16
+ - `add <name> <secret-key>`: Same, with the secret on the command line (note: it stays in your shell history).
17
+ - `add <name> --replace`: Adds an authenticator, replacing an existing one with the same name.
18
+ - `add <name> --steam`: Adds a Steam Guard authenticator (see [Steam Guard](#steam-guard)).
19
+ - `add <name> --mafile <path>`: Imports a Steam Guard authenticator from a `.maFile`.
20
+ - `remove <name>`: Removes the authenticator with the specified name.
21
+ - `get <name>`: Retrieves the token from the authenticator with the specified name.
22
+ - `get <name> --clipboard`: Retrieves the token and adds it to the clipboard.
23
+ - `get <name> --sync`: Forces a fresh time sync before generating the token.
24
+ - `list`: Lists all registered authenticators.
25
+ - `list --showsecret`: Lists all registered authenticators, including the secret keys.
26
+
27
+ The secret key can be a base32 secret or an `otpauth://totp/...` URI (its digits, period and algorithm are respected).
28
+
29
+ Commands exit with a non-zero code when they fail (e.g. authenticator not found), so they can be used in scripts.
30
+
31
+ To use win-cliauth, open a terminal or command prompt and run one of the commands listed above.
32
+
33
+ For detailed information on the available commands and options, you can run:
34
+
35
+ win-cliauth --help
36
+
37
+ ## Steam Guard
38
+
39
+ Steam Guard authenticators can be imported from a `.maFile`:
40
+
41
+ ```
42
+ win-cliauth add steam --mafile path\to\account.maFile
43
+ ```
44
+
45
+ Encrypted `.maFile`s must be decrypted before importing. You can also add the secret directly with `add steam --steam`; it can be a base64 `shared_secret`, base32 secret, `steam://` value or `otpauth://` URI. Steam codes are synced against Steam's own time server.
46
+
47
+ Anyone with your `shared_secret` can generate your Steam Guard codes, so keep `.maFile`s private and don't leave them lying around after importing (see [Backups](#backups)).
48
+
49
+ ## Security
50
+
51
+ - Secret keys are encrypted with Windows DPAPI (`ConvertTo-SecureString` / `ConvertFrom-SecureString` in PowerShell) before being stored on disk. They are passed to PowerShell through stdin, never on the command line.
52
+ - The encrypted file lives in `%AppData%\win-cliauth\keys` and is written atomically; the previous version is kept as `keys.bak`.
53
+ - The encrypted file can only be read by the same Windows user account where it was created.
54
+ - Time is synced over HTTPS (Google for TOTP, Steam for Steam Guard) and the offset is cached for 6 hours in `%AppData%\win-cliauth\time.json`.
55
+
56
+ ## Backups
57
+
58
+ The vault is tied to your Windows user account: reinstalling Windows or switching accounts makes it unreadable, even if you copied the `keys` file. Before that happens, run `win-cliauth list --showsecret` and store the secrets somewhere safe (e.g. a password manager), along with each service's recovery codes, such as the Steam revocation code from your `.maFile`.
59
+
60
+ win-cliauth is provided without warranty; see the [license](LICENSE).
61
+
62
+ ## Credits
63
+
64
+ This project uses the following open-source libraries:
65
+
66
+ - [otpauth](https://github.com/hectorm/otpauth) - One Time Password (HOTP/TOTP) library for Node.js, Deno, Bun and browsers.
67
+ - [yargs](https://github.com/yargs/yargs) - A command-line argument parsing library for Node.js.
68
+
69
+ Please refer to the respective library documentation for detailed information on their usage, licensing, and contributions.
70
+
71
+
72
+ ## Contributing
73
+
74
+ Contributions are welcome! If you find any issues or have suggestions for improvements, please feel free to open an issue or submit a pull request on [GitHub](https://github.com/pedrolucasmag/win-cliauth).
75
+
76
+ ## License
77
+
53
78
  This project is licensed under the BSD-3-Clause License. See the LICENSE file for details.
package/dist/main.js CHANGED
@@ -7,21 +7,9 @@ All rights reserved.
7
7
  This source code is licensed under the BSD-style license found in the
8
8
  LICENSE file in the root directory of this source tree.
9
9
  */
10
- var _a, _b;
11
10
  Object.defineProperty(exports, "__esModule", { value: true });
12
11
  const commands_1 = require("./src/commands");
13
- const pshell_1 = require("./src/pshell");
14
- const childProcess = (0, pshell_1.decrypt)();
15
- let rawData = '';
16
- (_a = childProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => {
17
- rawData += chunk;
18
- });
19
- (_b = childProcess.stdout) === null || _b === void 0 ? void 0 : _b.on('end', () => {
20
- try {
21
- const svcs = JSON.parse(rawData);
22
- (0, commands_1.handleCommands)(svcs);
23
- }
24
- catch (error) {
25
- console.error('Error parsing JSON:', error);
26
- }
12
+ (0, commands_1.handleCommands)().catch((error) => {
13
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
14
+ process.exitCode = 1;
27
15
  });
package/dist/src/auth.js CHANGED
@@ -6,83 +6,94 @@ All rights reserved.
6
6
  This source code is licensed under the BSD-style license found in the
7
7
  LICENSE file in the root directory of this source tree.
8
8
  */
9
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
10
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
11
- return new (P || (P = Promise))(function (resolve, reject) {
12
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
13
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
14
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
15
- step((generator = generator.apply(thisArg, _arguments || [])).next());
16
- });
17
- };
18
9
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.listAuth = exports.removeAuth = exports.addAuth = exports.getAuth = void 0;
10
+ exports.getAuth = getAuth;
11
+ exports.addAuth = addAuth;
12
+ exports.removeAuth = removeAuth;
13
+ exports.listAuth = listAuth;
20
14
  const child_process_1 = require("child_process");
21
- const otpauth_1 = require("otpauth");
22
- const steam_totp_1 = require("steam-totp");
15
+ const promises_1 = require("fs/promises");
16
+ const otp_1 = require("./otp");
17
+ const prompt_1 = require("./prompt");
23
18
  const pshell_1 = require("./pshell");
24
- function synchronizeTime() {
25
- var _a;
26
- return __awaiter(this, void 0, void 0, function* () {
27
- (_a = AbortSignal.timeout) !== null && _a !== void 0 ? _a : (AbortSignal.timeout = function timeout(ms) {
28
- const ctrl = new AbortController();
29
- setTimeout(() => ctrl.abort(), ms);
30
- return ctrl.signal;
31
- });
32
- try {
33
- const response = yield fetch('http://www.google.com', { signal: AbortSignal.timeout(5000) });
34
- const responseHeaders = response.headers;
35
- const headerDate = responseHeaders.get('date');
36
- const serverTime = headerDate ? Date.parse(headerDate) : Date.now();
37
- const localTime = Date.now();
38
- const timeDifference = serverTime - localTime;
39
- return Date.now() + timeDifference;
40
- }
41
- catch (error) {
42
- return Date.now();
43
- }
44
- });
19
+ const time_1 = require("./time");
20
+ function parseEntry(entry) {
21
+ return typeof entry === 'string' ? { secret: entry, steam: false } : { secret: entry.secret, steam: !!entry.steam };
45
22
  }
46
- function getTOTP(key) {
47
- return __awaiter(this, void 0, void 0, function* () {
48
- const auth = new otpauth_1.TOTP({ secret: key, digits: 6, period: 30 });
49
- const timestamp = yield synchronizeTime();
50
- return auth.generate({ timestamp });
23
+ function notFound(name) {
24
+ console.error(`${name} not found.`);
25
+ process.exitCode = 1;
26
+ }
27
+ function copyToClipboard(text) {
28
+ return new Promise((resolve, reject) => {
29
+ const clip = (0, child_process_1.spawn)('clip');
30
+ clip.on('error', reject);
31
+ clip.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`clip exited with code ${code}`))));
32
+ clip.stdin.end(text);
51
33
  });
52
34
  }
53
- function getToken({ key, steam }) {
54
- return steam ? (0, steam_totp_1.generateAuthCode)(key) : getTOTP(key).then((code) => code);
35
+ async function readMaFile(path) {
36
+ const maFile = JSON.parse(await (0, promises_1.readFile)(path, 'utf8'));
37
+ if (!maFile?.shared_secret)
38
+ throw new Error('No shared_secret in maFile (if it is encrypted, decrypt it first).');
39
+ return maFile.shared_secret;
55
40
  }
56
- function getAuth({ objAuth, svc, clipboard, steam }) {
57
- return __awaiter(this, void 0, void 0, function* () {
58
- const skey = objAuth[`${svc}`];
59
- if (skey) {
60
- const token = yield getToken({ key: skey, steam: steam });
61
- if (clipboard)
62
- (0, child_process_1.spawn)('clip').stdin.end(token);
63
- return console.info(token);
64
- }
65
- return console.info(`${svc} not found.`);
66
- });
41
+ async function getAuth({ name, steam, clipboard, sync }) {
42
+ const objAuth = await (0, pshell_1.decrypt)();
43
+ if (!objAuth[name])
44
+ return notFound(name);
45
+ const entry = parseEntry(objAuth[name]);
46
+ const token = steam || entry.steam
47
+ ? (0, otp_1.steamCode)(entry.secret, await (0, time_1.syncedNow)('steam', sync))
48
+ : (0, otp_1.totpCode)(entry.secret, await (0, time_1.syncedNow)('totp', sync));
49
+ if (clipboard)
50
+ await copyToClipboard(token);
51
+ console.info(token);
67
52
  }
68
- exports.getAuth = getAuth;
69
- function addAuth({ objAuth, svc, sk, replace }) {
70
- if (objAuth[`${svc}`] && !replace)
71
- return console.info(`${svc} already exists, adds --replace to overwrite it.`);
72
- objAuth[`${svc}`] = String(sk);
73
- (0, pshell_1.encrypt)({ str: JSON.stringify(objAuth) });
74
- return console.info(`${svc} added!`);
53
+ async function addAuth({ name, secretKey, steam, mafile, replace }) {
54
+ let secret;
55
+ if (mafile) {
56
+ secret = await readMaFile(mafile);
57
+ steam = true;
58
+ }
59
+ else if (secretKey) {
60
+ secret = secretKey;
61
+ console.warn('Tip: omit <secret-key> to be prompted for it, so it does not end up in your shell history.');
62
+ }
63
+ else {
64
+ secret = await (0, prompt_1.readSecret)('Secret key: ');
65
+ }
66
+ if (!secret)
67
+ throw new Error('No secret key given.');
68
+ if (/^steam:\/\//i.test(secret))
69
+ steam = true;
70
+ // validates the secret before storing it
71
+ if (steam)
72
+ (0, otp_1.steamSecret)(secret);
73
+ else
74
+ (0, otp_1.totpCode)(secret, Date.now());
75
+ const objAuth = await (0, pshell_1.decrypt)();
76
+ if (objAuth[name] && !replace) {
77
+ console.error(`${name} already exists, add --replace to overwrite it.`);
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+ objAuth[name] = steam ? { secret, steam: true } : secret;
82
+ await (0, pshell_1.encrypt)(objAuth);
83
+ console.info(`${name} added!`);
75
84
  }
76
- exports.addAuth = addAuth;
77
- function removeAuth({ objAuth, svc }) {
78
- if (!objAuth[`${svc}`])
79
- return console.info(`${svc} not found.`);
80
- delete objAuth[`${svc}`];
81
- (0, pshell_1.encrypt)({ str: JSON.stringify(objAuth) });
82
- return console.info(`${svc} removed!`);
85
+ async function removeAuth({ name }) {
86
+ const objAuth = await (0, pshell_1.decrypt)();
87
+ if (!objAuth[name])
88
+ return notFound(name);
89
+ delete objAuth[name];
90
+ await (0, pshell_1.encrypt)(objAuth);
91
+ console.info(`${name} removed!`);
83
92
  }
84
- exports.removeAuth = removeAuth;
85
- function listAuth({ objAuth, showsecret }) {
86
- return showsecret ? console.table(objAuth) : console.table(Object.keys(objAuth));
93
+ async function listAuth({ showsecret }) {
94
+ const objAuth = await (0, pshell_1.decrypt)();
95
+ if (!showsecret)
96
+ return console.table(Object.keys(objAuth));
97
+ console.warn('Warning: secret keys are shown in plain text.');
98
+ console.table(Object.fromEntries(Object.entries(objAuth).map(([name, entry]) => [name, parseEntry(entry)])));
87
99
  }
88
- exports.listAuth = listAuth;
@@ -10,43 +10,42 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  return (mod && mod.__esModule) ? mod : { "default": mod };
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.handleCommands = void 0;
13
+ exports.handleCommands = handleCommands;
14
14
  const yargs_1 = __importDefault(require("yargs"));
15
15
  const helpers_1 = require("yargs/helpers");
16
16
  const auth_1 = require("./auth");
17
- function handleCommands(objAuth) {
18
- return (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
17
+ async function handleCommands() {
18
+ await (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
19
19
  .scriptName("win-cliauth")
20
- .command('add <name> <secret-key>', 'adds authenticator with given secret key.', (y) => y.options({
21
- 'replace': {
22
- description: "forces replacement of existing service.",
23
- require: false
24
- },
25
- }), (argv) => (0, auth_1.addAuth)({ objAuth, svc: argv.name, sk: argv.secretKey, replace: argv.replace }))
26
- .command('remove <name>', 'removes the given <name> authenticator.', {}, (argv) => (0, auth_1.removeAuth)({ objAuth, svc: argv.name }))
27
- .command('get <name>', 'gets the token from service <name> (for steam guard, add --steam)', (y) => y.options({
28
- 'steam': {
29
- description: "gets token from steam authenticator.",
30
- required: false
31
- },
32
- 'clipboard': {
33
- description: 'adds authenticator code to clipboard',
34
- required: false
35
- }
36
- }), (argv) => {
37
- (0, auth_1.getAuth)({ objAuth, svc: argv.name, clipboard: argv.clipboard, steam: argv.steam });
38
- })
20
+ .command('add <name> [secret-key]', 'adds authenticator with given secret key (prompts for it when omitted).', (y) => y
21
+ .positional('name', { type: 'string', demandOption: true })
22
+ .positional('secret-key', { type: 'string', description: 'base32 secret, otpauth:// URI, or Steam shared_secret' })
23
+ .options({
24
+ 'replace': { type: 'boolean', description: "forces replacement of existing service." },
25
+ 'steam': { type: 'boolean', description: "stores it as a Steam Guard authenticator." },
26
+ 'mafile': { type: 'string', description: "imports the Steam shared_secret from a .maFile." },
27
+ }), (argv) => (0, auth_1.addAuth)(argv))
28
+ .command('remove <name>', 'removes the given <name> authenticator.', (y) => y.positional('name', { type: 'string', demandOption: true }), (argv) => (0, auth_1.removeAuth)(argv))
29
+ .command('get <name>', 'gets the token from service <name>', (y) => y
30
+ .positional('name', { type: 'string', demandOption: true })
31
+ .options({
32
+ 'steam': { type: 'boolean', description: "gets token from steam authenticator." },
33
+ 'clipboard': { type: 'boolean', description: 'adds authenticator code to clipboard' },
34
+ 'sync': { type: 'boolean', description: 'forces a time re-sync instead of using the cached offset' },
35
+ }), (argv) => (0, auth_1.getAuth)(argv))
39
36
  .command('list', 'Prints out the list of authenticators.', (y) => y.options({
40
- 'showsecret': {
41
- description: "Prints out the list with secret keys",
42
- require: false
43
- },
44
- }), (argv) => (0, auth_1.listAuth)({ objAuth, showsecret: argv.showsecret }))
37
+ 'showsecret': { type: 'boolean', description: "Prints out the list with secret keys" },
38
+ }), (argv) => (0, auth_1.listAuth)(argv))
45
39
  .wrap(null)
46
- .showHelpOnFail(true)
47
40
  .demandCommand()
48
41
  .recommendCommands()
49
42
  .strict()
50
- .parse();
43
+ .fail((msg, err, y) => {
44
+ if (err)
45
+ throw err;
46
+ y.showHelp();
47
+ console.error(`\n${msg}`);
48
+ process.exit(1);
49
+ })
50
+ .parseAsync();
51
51
  }
52
- exports.handleCommands = handleCommands;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) <2022>, <Pedro Lucas Magalhães de Oliveira>
4
+ All rights reserved.
5
+
6
+ This source code is licensed under the BSD-style license found in the
7
+ LICENSE file in the root directory of this source tree.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.steamSecret = steamSecret;
11
+ exports.steamCode = steamCode;
12
+ exports.totpCode = totpCode;
13
+ const crypto_1 = require("crypto");
14
+ const otpauth_1 = require("otpauth");
15
+ const STEAM_CHARS = '23456789BCDFGHJKMNPQRTVWXY';
16
+ function parseUri(uri) {
17
+ const otp = otpauth_1.URI.parse(uri);
18
+ if (!(otp instanceof otpauth_1.TOTP))
19
+ throw new Error('Only TOTP (time-based) otpauth:// URIs are supported.');
20
+ return otp;
21
+ }
22
+ function fromBase32(str) {
23
+ return Buffer.from(otpauth_1.Secret.fromBase32(str.replace(/[\s-]/g, '').toUpperCase()).bytes);
24
+ }
25
+ /**
26
+ * Decodes a Steam Guard secret in any of the formats tools export it in:
27
+ * - base64 `shared_secret` (as in a .maFile)
28
+ * - 40-char hex
29
+ * - base32, `steam://BASE32` or an otpauth:// URI
30
+ */
31
+ function steamSecret(secret) {
32
+ const s = secret.trim();
33
+ let key;
34
+ if (/^otpauth:\/\//i.test(s))
35
+ key = Buffer.from(parseUri(s).secret.bytes);
36
+ else if (/^steam:\/\//i.test(s))
37
+ key = fromBase32(s.slice('steam://'.length));
38
+ else if (/^[0-9a-f]{40}$/i.test(s))
39
+ key = Buffer.from(s, 'hex');
40
+ else if (/^[A-Z2-7]{32}=*$/i.test(s.replace(/[\s-]/g, '')))
41
+ key = fromBase32(s.replace(/=+$/, ''));
42
+ else if (/^[A-Za-z0-9+/]+={0,2}$/.test(s))
43
+ key = Buffer.from(s, 'base64');
44
+ else
45
+ throw new Error('Invalid Steam secret: expected a base64 shared_secret, base32 secret or otpauth:// URI.');
46
+ if (key.length < 10)
47
+ throw new Error('Invalid Steam secret: decoded key is too short.');
48
+ return key;
49
+ }
50
+ function steamCode(secret, timestamp) {
51
+ const counter = Buffer.alloc(8);
52
+ counter.writeUInt32BE(Math.floor(timestamp / 1000 / 30), 4);
53
+ const hmac = (0, crypto_1.createHmac)('sha1', steamSecret(secret)).update(counter).digest();
54
+ const offset = hmac[19] & 0x0f;
55
+ let fullCode = hmac.readUInt32BE(offset) & 0x7fffffff;
56
+ let code = '';
57
+ for (let i = 0; i < 5; i++) {
58
+ code += STEAM_CHARS[fullCode % STEAM_CHARS.length];
59
+ fullCode = Math.floor(fullCode / STEAM_CHARS.length);
60
+ }
61
+ return code;
62
+ }
63
+ function totpCode(secret, timestamp) {
64
+ const s = secret.trim();
65
+ const totp = /^otpauth:\/\//i.test(s)
66
+ ? parseUri(s)
67
+ : new otpauth_1.TOTP({ secret: otpauth_1.Secret.fromBase32(s.replace(/[\s-]/g, '').replace(/=+$/, '').toUpperCase()), digits: 6, period: 30 });
68
+ return totp.generate({ timestamp });
69
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) <2022>, <Pedro Lucas Magalhães de Oliveira>
4
+ All rights reserved.
5
+
6
+ This source code is licensed under the BSD-style license found in the
7
+ LICENSE file in the root directory of this source tree.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.readSecret = readSecret;
11
+ async function readPiped() {
12
+ let data = '';
13
+ for await (const chunk of process.stdin)
14
+ data += chunk;
15
+ return data.split(/\r?\n/)[0].trim();
16
+ }
17
+ /** Reads a secret without echoing it (or the first line of piped stdin). */
18
+ function readSecret(prompt) {
19
+ const { stdin, stderr } = process;
20
+ if (!stdin.isTTY)
21
+ return readPiped();
22
+ return new Promise((resolve) => {
23
+ let value = '';
24
+ stderr.write(prompt);
25
+ stdin.setRawMode(true);
26
+ stdin.setEncoding('utf8');
27
+ stdin.resume();
28
+ const done = () => {
29
+ stdin.setRawMode(false);
30
+ stdin.pause();
31
+ stdin.off('data', onData);
32
+ stderr.write('\n');
33
+ };
34
+ const onData = (chunk) => {
35
+ for (const ch of chunk) {
36
+ if (ch === '\r' || ch === '\n') {
37
+ done();
38
+ return resolve(value.trim());
39
+ }
40
+ if (ch === '\u0003') {
41
+ done();
42
+ process.exit(130);
43
+ }
44
+ if (ch === '\u007f' || ch === '\b')
45
+ value = value.slice(0, -1);
46
+ else
47
+ value += ch;
48
+ }
49
+ };
50
+ stdin.on('data', onData);
51
+ });
52
+ }
@@ -7,33 +7,65 @@ This source code is licensed under the BSD-style license found in the
7
7
  LICENSE file in the root directory of this source tree.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.decrypt = exports.encrypt = void 0;
10
+ exports.encrypt = encrypt;
11
+ exports.decrypt = decrypt;
11
12
  const child_process_1 = require("child_process");
12
- function pshell({ cmd }) {
13
- return (0, child_process_1.spawn)('powershell.exe', ['/c', cmd], { stdio: 'pipe' });
13
+ // Secrets never appear in the script text (which is visible in process listings):
14
+ // they travel through stdin/stdout as base64-encoded UTF-8.
15
+ function pshell({ script, input = '' }) {
16
+ const encoded = Buffer.from(`$ErrorActionPreference = 'Stop'\n${script}`, 'utf16le').toString('base64');
17
+ return new Promise((resolve, reject) => {
18
+ const ps = (0, child_process_1.spawn)('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], { stdio: 'pipe', windowsHide: true });
19
+ let stdout = '';
20
+ let stderr = '';
21
+ ps.stdout.on('data', (chunk) => (stdout += chunk));
22
+ ps.stderr.on('data', (chunk) => (stderr += chunk));
23
+ ps.on('error', (err) => reject(new Error(`Could not start PowerShell: ${err.message}`)));
24
+ ps.on('close', (code) => {
25
+ if (code === 0)
26
+ return resolve(stdout.trim());
27
+ reject(new Error(stderr.trim() || `PowerShell exited with code ${code}`));
28
+ });
29
+ ps.stdin.end(input);
30
+ });
14
31
  }
15
- function encrypt({ str }) {
16
- return pshell({
17
- cmd: `
18
- $keyPath = "$($env:AppData)/win-cliauth/keys"
19
- if (!(Test-Path "$keyPath")) { New-Item -Path $keyPath -Force }
20
- $secureString = ConvertTo-SecureString '${str}' -AsPlainText -Force
21
- $encrypted = ConvertFrom-SecureString -SecureString $secureString
22
- $encrypted > $keyPath
23
- `
32
+ const KEY_PATH = `
33
+ $dir = Join-Path $env:AppData 'win-cliauth'
34
+ $keyPath = Join-Path $dir 'keys'
35
+ `;
36
+ async function encrypt(vault) {
37
+ await pshell({
38
+ input: Buffer.from(JSON.stringify(vault), 'utf8').toString('base64'),
39
+ script: `${KEY_PATH}
40
+ $plain = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([Console]::In.ReadToEnd().Trim()))
41
+ $secureString = ConvertTo-SecureString $plain -AsPlainText -Force
42
+ $encrypted = ConvertFrom-SecureString -SecureString $secureString
43
+ New-Item -ItemType Directory -Path $dir -Force | Out-Null
44
+ $tmp = "$keyPath.tmp"
45
+ [IO.File]::WriteAllText($tmp, $encrypted)
46
+ if (Test-Path $keyPath) { [IO.File]::Replace($tmp, $keyPath, "$keyPath.bak") }
47
+ else { [IO.File]::Move($tmp, $keyPath) }
48
+ `,
24
49
  });
25
50
  }
26
- exports.encrypt = encrypt;
27
- function decrypt() {
28
- return pshell({
29
- cmd: `
30
- $keyPath = "$($env:AppData)/win-cliauth/keys"
31
- if (!(Test-Path "$keyPath")) { '{}' }
32
- else {
33
- $secureString = Get-Content $keyPath | ConvertTo-SecureString
34
- $decrypted = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString))
35
- $decrypted
36
- }`
51
+ async function decrypt() {
52
+ const output = await pshell({
53
+ script: `${KEY_PATH}
54
+ if (Test-Path $keyPath) {
55
+ $secureString = (Get-Content $keyPath -Raw).Trim() | ConvertTo-SecureString
56
+ $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString)
57
+ try { $decrypted = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) }
58
+ finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
59
+ [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($decrypted))
60
+ }
61
+ `,
37
62
  });
63
+ if (!output)
64
+ return {};
65
+ try {
66
+ return JSON.parse(Buffer.from(output, 'base64').toString('utf8'));
67
+ }
68
+ catch {
69
+ throw new Error('The key file is corrupted (a backup may exist at %AppData%\\win-cliauth\\keys.bak).');
70
+ }
38
71
  }
39
- exports.decrypt = decrypt;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) <2022>, <Pedro Lucas Magalhães de Oliveira>
4
+ All rights reserved.
5
+
6
+ This source code is licensed under the BSD-style license found in the
7
+ LICENSE file in the root directory of this source tree.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.syncedNow = syncedNow;
11
+ const promises_1 = require("fs/promises");
12
+ const os_1 = require("os");
13
+ const path_1 = require("path");
14
+ const CACHE_PATH = (0, path_1.join)(process.env.APPDATA ?? (0, os_1.homedir)(), 'win-cliauth', 'time.json');
15
+ const MAX_AGE = 6 * 60 * 60 * 1000;
16
+ const TIMEOUT = 5000;
17
+ async function readCache() {
18
+ try {
19
+ return JSON.parse(await (0, promises_1.readFile)(CACHE_PATH, 'utf8'));
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ async function writeCache(cache) {
26
+ try {
27
+ await (0, promises_1.mkdir)((0, path_1.dirname)(CACHE_PATH), { recursive: true });
28
+ await (0, promises_1.writeFile)(CACHE_PATH, JSON.stringify(cache));
29
+ }
30
+ catch {
31
+ // the cache is an optimisation only
32
+ }
33
+ }
34
+ // Returns server time minus local time, in ms, measured at the request midpoint.
35
+ async function fetchOffset(source) {
36
+ const start = Date.now();
37
+ let serverTime;
38
+ if (source === 'steam') {
39
+ const response = await fetch('https://api.steampowered.com/ITwoFactorService/QueryTime/v1/', {
40
+ method: 'POST',
41
+ body: '',
42
+ signal: AbortSignal.timeout(TIMEOUT),
43
+ });
44
+ const json = (await response.json());
45
+ serverTime = Number(json?.response?.server_time) * 1000;
46
+ }
47
+ else {
48
+ const response = await fetch('https://www.google.com', { method: 'HEAD', signal: AbortSignal.timeout(TIMEOUT) });
49
+ serverTime = Date.parse(response.headers.get('date') ?? '');
50
+ }
51
+ if (!Number.isFinite(serverTime))
52
+ throw new Error('Invalid server time');
53
+ return serverTime - (start + Date.now()) / 2;
54
+ }
55
+ /**
56
+ * Current time corrected by the offset to a trusted HTTPS time source.
57
+ * The offset is cached for a few hours; `force` re-syncs. On network failure
58
+ * the last known offset (or the local clock) is used.
59
+ */
60
+ async function syncedNow(source, force = false) {
61
+ const cache = await readCache();
62
+ const cached = cache[source];
63
+ if (!force && cached && Date.now() - cached.checkedAt < MAX_AGE)
64
+ return Date.now() + cached.offset;
65
+ try {
66
+ const offset = await fetchOffset(source);
67
+ cache[source] = { offset, checkedAt: Date.now() };
68
+ await writeCache(cache);
69
+ return Date.now() + offset;
70
+ }
71
+ catch {
72
+ return Date.now() + (cached?.offset ?? 0);
73
+ }
74
+ }
package/package.json CHANGED
@@ -1,49 +1,56 @@
1
- {
2
- "name": "win-cliauth",
3
- "version": "1.0.3",
4
- "description": "win-cliauth is a CLI-based OTP (One-Time Password) authenticator management tool for Windows.",
5
- "author": "Pedro Lucas Magalhães <pedrolucasmag@gmail.com>",
6
- "license": "BSD-3-Clause",
7
- "scripts": {
8
- "build": "tsc",
9
- "release": "standard-version",
10
- "prepack": "tsc"
11
- },
12
- "bin": {
13
- "win-cliauth": "./dist/main.js"
14
- },
15
- "repository": "github:pedrolucasmag/win-cliauth",
16
- "files": [
17
- "dist",
18
- "LICENSE",
19
- "!dist/keys"
20
- ],
21
- "devDependencies": {
22
- "@commitlint/cli": "^17.7.1",
23
- "@commitlint/config-conventional": "^17.7.0",
24
- "@types/node": "^18.11.10",
25
- "@types/steam-totp": "^2.1.0",
26
- "@types/yargs": "^17.0.10",
27
- "@typescript-eslint/eslint-plugin": "^5.33.1",
28
- "@typescript-eslint/parser": "^5.33.1",
29
- "eslint": "^8.22.0",
30
- "husky": "^8.0.3"
31
- },
32
- "os": [
33
- "win32"
34
- ],
35
- "keywords": [
36
- "otp",
37
- "authenticator",
38
- "cli",
39
- "windows",
40
- "steamguard",
41
- "2fa",
42
- "totp"
43
- ],
44
- "dependencies": {
45
- "otpauth": "^9.1.4",
46
- "steam-totp": "^2.1.2",
47
- "yargs": "^17.5.1"
48
- }
49
- }
1
+ {
2
+ "name": "win-cliauth",
3
+ "version": "1.0.4",
4
+ "description": "win-cliauth is a CLI-based OTP (One-Time Password) authenticator management tool for Windows.",
5
+ "author": "Pedro Lucas Magalhães <pedrolucasmag@gmail.com>",
6
+ "license": "BSD-3-Clause",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "lint": "eslint . --ext .ts",
10
+ "test": "tsc && node --test \"dist/test/**/*.test.js\"",
11
+ "prepack": "tsc"
12
+ },
13
+ "bin": {
14
+ "win-cliauth": "dist/main.js"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/pedrolucasmag/win-cliauth.git"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "LICENSE",
23
+ "!dist/test"
24
+ ],
25
+ "devDependencies": {
26
+ "@commitlint/cli": "^17.7.1",
27
+ "@commitlint/config-conventional": "^17.7.0",
28
+ "@types/node": "^18.11.10",
29
+ "@types/yargs": "^17.0.10",
30
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
31
+ "@typescript-eslint/parser": "^8.0.0",
32
+ "eslint": "^8.57.0",
33
+ "husky": "^8.0.3",
34
+ "typescript": "^5.4.0"
35
+ },
36
+ "os": [
37
+ "win32"
38
+ ],
39
+ "keywords": [
40
+ "otp",
41
+ "authenticator",
42
+ "cli",
43
+ "windows",
44
+ "steamguard",
45
+ "2fa",
46
+ "totp",
47
+ "steam"
48
+ ],
49
+ "dependencies": {
50
+ "otpauth": "^9.1.4",
51
+ "yargs": "^17.5.1"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ }
56
+ }