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 +26 -26
- package/README.md +77 -52
- package/dist/main.js +3 -15
- package/dist/src/auth.js +80 -69
- package/dist/src/commands.js +29 -30
- package/dist/src/otp.js +69 -0
- package/dist/src/prompt.js +52 -0
- package/dist/src/pshell.js +56 -24
- package/dist/src/time.js +74 -0
- package/package.json +56 -49
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
|
|
16
|
-
- `add <name> <secret-key
|
|
17
|
-
- `
|
|
18
|
-
- `
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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
|
|
22
|
-
const
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
return
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
54
|
-
|
|
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({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
if (!objAuth[
|
|
79
|
-
return
|
|
80
|
-
delete objAuth[
|
|
81
|
-
(0, pshell_1.encrypt)(
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
package/dist/src/commands.js
CHANGED
|
@@ -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 =
|
|
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(
|
|
18
|
-
|
|
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>
|
|
21
|
-
'
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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;
|
package/dist/src/otp.js
ADDED
|
@@ -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
|
+
}
|
package/dist/src/pshell.js
CHANGED
|
@@ -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.
|
|
10
|
+
exports.encrypt = encrypt;
|
|
11
|
+
exports.decrypt = decrypt;
|
|
11
12
|
const child_process_1 = require("child_process");
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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;
|
package/dist/src/time.js
ADDED
|
@@ -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.
|
|
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
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"@
|
|
27
|
-
"@
|
|
28
|
-
"@
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
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
|
+
}
|