hexpass 1.0.0 → 1.0.2
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/README.md +49 -10
- package/bin/hexpass.js +5 -0
- package/index.js +4 -198
- package/package.json +14 -4
- package/src/args.js +153 -0
- package/src/cli.js +103 -0
- package/src/generator.js +28 -0
- package/src/help.js +38 -0
- package/src/output.js +62 -0
package/README.md
CHANGED
|
@@ -21,15 +21,20 @@ npx hexpass
|
|
|
21
21
|
```
|
|
22
22
|
hexpass [length]
|
|
23
23
|
hexpass --bytes <n>
|
|
24
|
+
hexpass --count <n>
|
|
25
|
+
hexpass --env <NAME>
|
|
26
|
+
hexpass --json
|
|
24
27
|
hexpass --help
|
|
25
28
|
hexpass --version
|
|
26
|
-
hexpass --copy
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
- Default: `hexpass` outputs a 32-character hex string.
|
|
30
32
|
- `hexpass 64` outputs a 64-character string.
|
|
31
|
-
- `hexpass 33` supports odd lengths (generates extra byte and truncates).
|
|
32
|
-
- `hexpass --bytes 16` uses 16 random bytes (32 characters
|
|
33
|
+
- `hexpass 33` supports odd lengths (generates the necessary extra byte and truncates to the requested number of hex characters).
|
|
34
|
+
- `hexpass --bytes 16` uses 16 random bytes (32 hex characters).
|
|
35
|
+
- `hexpass 32 --count 5` generates 5 secrets, each on its own line.
|
|
36
|
+
- `hexpass 48 --env JWT_SECRET` outputs `JWT_SECRET=<secret>` format.
|
|
37
|
+
- `hexpass 32 --json` outputs a JSON object with `length`, `bytes`, and `hex` fields (single secret only).
|
|
33
38
|
|
|
34
39
|
## Options
|
|
35
40
|
|
|
@@ -37,30 +42,64 @@ hexpass --copy
|
|
|
37
42
|
| --- | --- |
|
|
38
43
|
| `length` | Desired hex string length (default 32, max 1024) |
|
|
39
44
|
| `--bytes <n>` | Generate from n bytes (output has n×2 characters) |
|
|
40
|
-
| `--
|
|
45
|
+
| `--count <n>`, `-n` | Generate n secrets (default 1, max 100) |
|
|
46
|
+
| `--env <NAME>`, `-e` | Output as `NAME=secret` instead of raw secret (only valid for single secret) |
|
|
47
|
+
| `--json`, `-j` | Output as JSON object (single secret only; incompatible with `--env` and `--copy`) |
|
|
48
|
+
| `--copy`, `-c` | Copy the generated hex string to the clipboard (single secret only) |
|
|
41
49
|
| `--help`, `-h` | Show usage information |
|
|
42
50
|
| `--version`, `-v` | Show version number |
|
|
43
51
|
|
|
52
|
+
### Notes on compatibility
|
|
53
|
+
|
|
54
|
+
- `--json` is only supported for single-secret output (`--count` must be 1).
|
|
55
|
+
- `--env` and `--json` are incompatible — use one or the other depending on whether you want an assignment string or structured data.
|
|
56
|
+
- `--copy` works only for single-secret output and is incompatible with `--json`.
|
|
57
|
+
|
|
44
58
|
## Examples
|
|
45
59
|
|
|
46
60
|
```
|
|
47
|
-
# Quickly generate a secret
|
|
48
|
-
hexpass
|
|
61
|
+
# Quickly generate a secret (default 32 chars)
|
|
62
|
+
hexpass
|
|
49
63
|
|
|
50
|
-
#
|
|
51
|
-
|
|
64
|
+
# Generate a longer secret
|
|
65
|
+
hexpass 64
|
|
52
66
|
|
|
53
|
-
#
|
|
67
|
+
# Use explicit byte count (generates n * 2 hex chars)
|
|
54
68
|
hexpass --bytes 32 > api-key.txt
|
|
55
69
|
|
|
56
|
-
# Generate and copy
|
|
70
|
+
# Generate and copy a secret (single secret)
|
|
57
71
|
hexpass 48 --copy
|
|
72
|
+
|
|
73
|
+
# Generate multiple secrets in one go
|
|
74
|
+
hexpass 32 --count 5
|
|
75
|
+
|
|
76
|
+
# Output as environment variable format
|
|
77
|
+
hexpass 48 --env JWT_SECRET
|
|
78
|
+
# Output: JWT_SECRET=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f
|
|
79
|
+
|
|
80
|
+
# Output as JSON for automation (single secret)
|
|
81
|
+
hexpass 32 --json
|
|
82
|
+
# Output: {"length":32,"bytes":16,"hex":"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"}
|
|
58
83
|
```
|
|
59
84
|
|
|
60
85
|
## Security
|
|
61
86
|
|
|
62
87
|
`hexpass` always uses `crypto.randomBytes()` from Node.js for cryptographically strong randomness. No weak sources like `Math.random()` are used.
|
|
63
88
|
|
|
89
|
+
## Testing
|
|
90
|
+
|
|
91
|
+
The repository includes a small CLI test script used during development. You can exercise common invocations with:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
npm test
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This runs several `hexpass` variants to validate behavior (lengths, bytes, `--count`, `--env`, and `--json`).
|
|
98
|
+
|
|
99
|
+
## Version
|
|
100
|
+
|
|
101
|
+
This README reflects the changes introduced in version 1.0.1 (modular refactor and new CLI options).
|
|
102
|
+
|
|
64
103
|
## License
|
|
65
104
|
|
|
66
105
|
MIT
|
package/bin/hexpass.js
ADDED
package/index.js
CHANGED
|
@@ -1,201 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
const { run } = require("./src/cli");
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const { version } = require("./package.json");
|
|
6
|
-
|
|
7
|
-
const DEFAULT_LENGTH = 32;
|
|
8
|
-
const MAX_LENGTH = 1024;
|
|
9
|
-
const MAX_BYTES = MAX_LENGTH / 2;
|
|
10
|
-
|
|
11
|
-
const args = process.argv.slice(2);
|
|
12
|
-
|
|
13
|
-
const parsed = parseArgs(args);
|
|
14
|
-
|
|
15
|
-
if (parsed.action === "help") {
|
|
16
|
-
printHelp();
|
|
17
|
-
process.exit(0);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (parsed.action === "version") {
|
|
21
|
-
console.log(version);
|
|
22
|
-
process.exit(0);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (parsed.bytes != null && parsed.lengthValue != null) {
|
|
26
|
-
exitWithError("Provide either a length or --bytes, not both.");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let charLength;
|
|
30
|
-
|
|
31
|
-
if (parsed.bytes != null) {
|
|
32
|
-
const byteLength = parseByteLength(parsed.bytes);
|
|
33
|
-
if (byteLength * 2 > MAX_LENGTH) {
|
|
34
|
-
exitWithError(
|
|
35
|
-
`Byte length exceeds maximum of ${MAX_BYTES} bytes (${MAX_LENGTH} characters).`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
charLength = byteLength * 2;
|
|
39
|
-
} else if (parsed.lengthValue != null) {
|
|
40
|
-
const length = parseCharacterLength(parsed.lengthValue);
|
|
41
|
-
if (length > MAX_LENGTH) {
|
|
42
|
-
exitWithError(`Length exceeds maximum of ${MAX_LENGTH} characters.`);
|
|
43
|
-
}
|
|
44
|
-
charLength = length;
|
|
45
|
-
} else {
|
|
46
|
-
charLength = DEFAULT_LENGTH;
|
|
3
|
+
if (require.main === module) {
|
|
4
|
+
run(process.argv.slice(2), process.stdout, process.stderr, process.exit);
|
|
47
5
|
}
|
|
48
6
|
|
|
49
|
-
|
|
50
|
-
process.stdout.write(`${password}\n`);
|
|
51
|
-
|
|
52
|
-
if (parsed.copy) {
|
|
53
|
-
try {
|
|
54
|
-
copyToClipboard(password);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
exitWithError(error.message || "Failed to copy to clipboard.");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function parseArgs(argv) {
|
|
61
|
-
let lengthValue = null;
|
|
62
|
-
let bytes = null;
|
|
63
|
-
let copy = false;
|
|
64
|
-
|
|
65
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
66
|
-
const arg = argv[i];
|
|
67
|
-
|
|
68
|
-
if (arg === "--help" || arg === "-h") {
|
|
69
|
-
return { action: "help" };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (arg === "--version" || arg === "-v") {
|
|
73
|
-
return { action: "version" };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (arg === "--bytes") {
|
|
77
|
-
if (bytes != null) {
|
|
78
|
-
exitWithError("The --bytes option can only be specified once.");
|
|
79
|
-
}
|
|
80
|
-
const next = argv[i + 1];
|
|
81
|
-
if (!next) {
|
|
82
|
-
exitWithError("Missing value for --bytes.");
|
|
83
|
-
}
|
|
84
|
-
bytes = next;
|
|
85
|
-
i += 1;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (arg === "--copy" || arg === "-c") {
|
|
90
|
-
copy = true;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (arg.startsWith("--")) {
|
|
95
|
-
exitWithError(`Unknown option: ${arg}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (lengthValue != null) {
|
|
99
|
-
exitWithError("Multiple length values provided.");
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
lengthValue = arg;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return { action: "generate", lengthValue, bytes, copy };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function parseCharacterLength(value) {
|
|
109
|
-
const num = Number(value);
|
|
110
|
-
if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
|
111
|
-
exitWithError(`Invalid length: '${value}'. Must be a positive integer.`);
|
|
112
|
-
}
|
|
113
|
-
if (num <= 0) {
|
|
114
|
-
exitWithError("Length must be a positive integer.");
|
|
115
|
-
}
|
|
116
|
-
return num;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function parseByteLength(value) {
|
|
120
|
-
const num = Number(value);
|
|
121
|
-
if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
|
122
|
-
exitWithError(
|
|
123
|
-
`Invalid byte length: '${value}'. Must be a positive integer.`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
if (num <= 0) {
|
|
127
|
-
exitWithError("Byte length must be a positive integer.");
|
|
128
|
-
}
|
|
129
|
-
return num;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function generateHex(charLength) {
|
|
133
|
-
const bytesNeeded = Math.ceil(charLength / 2);
|
|
134
|
-
const hex = crypto.randomBytes(bytesNeeded).toString("hex");
|
|
135
|
-
return hex.slice(0, charLength);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function printHelp() {
|
|
139
|
-
console.log(
|
|
140
|
-
`hexpass - Generate cryptographically secure hex passwords\n\n` +
|
|
141
|
-
`Usage:\n` +
|
|
142
|
-
` hexpass [length] Generate hex string with specified length (default: ${DEFAULT_LENGTH})\n` +
|
|
143
|
-
` hexpass --bytes <n> Generate from n random bytes (output has n*2 characters)\n\n` +
|
|
144
|
-
`Options:\n` +
|
|
145
|
-
` -h, --help Show this help message\n` +
|
|
146
|
-
` -v, --version Show version number\n` +
|
|
147
|
-
` -c, --copy Copy the generated hex string to clipboard\n` +
|
|
148
|
-
` --bytes <n> Specify length in bytes instead of characters\n\n` +
|
|
149
|
-
`Examples:\n` +
|
|
150
|
-
` hexpass # 32-character hex string\n` +
|
|
151
|
-
` hexpass 64 # 64-character hex string\n` +
|
|
152
|
-
` hexpass --bytes 32 # 64-character hex string (32 bytes)\n` +
|
|
153
|
-
` hexpass 48 --copy # Generate 48 chars and copy to clipboard\n\n` +
|
|
154
|
-
`Limits:\n` +
|
|
155
|
-
` Maximum output length is ${MAX_LENGTH} characters (${MAX_BYTES} bytes).\n\n` +
|
|
156
|
-
`Security:\n` +
|
|
157
|
-
` All passwords are generated using Node.js crypto.randomBytes()`
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function exitWithError(message) {
|
|
162
|
-
console.error(`Error: ${message}`);
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function copyToClipboard(text) {
|
|
167
|
-
const platform = process.platform;
|
|
168
|
-
|
|
169
|
-
if (platform === "darwin") {
|
|
170
|
-
runClipboardCommand("pbcopy", text);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (platform === "win32") {
|
|
175
|
-
runClipboardCommand("clip", text);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const linuxCommands = [
|
|
180
|
-
["xclip", ["-selection", "clipboard"]],
|
|
181
|
-
["xsel", ["--clipboard", "--input"]],
|
|
182
|
-
];
|
|
183
|
-
|
|
184
|
-
for (const [cmd, args] of linuxCommands) {
|
|
185
|
-
const result = spawnSync(cmd, args, { input: text, encoding: "utf8" });
|
|
186
|
-
if (!result.error && result.status === 0) {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
throw new Error(
|
|
192
|
-
"Clipboard copy not supported on this system. Install xclip or xsel."
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function runClipboardCommand(command, input) {
|
|
197
|
-
const result = spawnSync(command, [], { input, encoding: "utf8" });
|
|
198
|
-
if (result.error || result.status !== 0) {
|
|
199
|
-
throw new Error(`Failed to copy to clipboard using ${command}.`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
7
|
+
module.exports = { run };
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hexpass",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Generate cryptographically secure hexadecimal passwords and secrets",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"hexpass": "
|
|
7
|
+
"hexpass": "bin/hexpass.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node
|
|
10
|
+
"test": "node test.js"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|
|
13
13
|
"password",
|
|
@@ -19,12 +19,22 @@
|
|
|
19
19
|
"jwt",
|
|
20
20
|
"api-key"
|
|
21
21
|
],
|
|
22
|
-
"author": "",
|
|
22
|
+
"author": "Tejaswan Kalluri",
|
|
23
23
|
"license": "MIT",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/tejaswankalluri/hexpass.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/tejaswankalluri/hexpass/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/tejaswankalluri/hexpass#readme",
|
|
24
32
|
"engines": {
|
|
25
33
|
"node": ">=14.0.0"
|
|
26
34
|
},
|
|
27
35
|
"files": [
|
|
36
|
+
"bin/",
|
|
37
|
+
"src/",
|
|
28
38
|
"index.js",
|
|
29
39
|
"LICENSE",
|
|
30
40
|
"README.md"
|
package/src/args.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const DEFAULT_LENGTH = 32;
|
|
2
|
+
const MAX_LENGTH = 1024;
|
|
3
|
+
const MAX_BYTES = MAX_LENGTH / 2;
|
|
4
|
+
const MAX_COUNT = 100;
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
let lengthValue = null;
|
|
8
|
+
let bytes = null;
|
|
9
|
+
let copy = false;
|
|
10
|
+
let count = 1;
|
|
11
|
+
let envName = null;
|
|
12
|
+
let json = false;
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
15
|
+
const arg = argv[i];
|
|
16
|
+
|
|
17
|
+
if (arg === "--help" || arg === "-h") {
|
|
18
|
+
return { action: "help" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (arg === "--version" || arg === "-v") {
|
|
22
|
+
return { action: "version" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (arg === "--bytes") {
|
|
26
|
+
if (bytes != null) {
|
|
27
|
+
throw new Error("The --bytes option can only be specified once.");
|
|
28
|
+
}
|
|
29
|
+
const next = argv[i + 1];
|
|
30
|
+
if (!next || next.startsWith("-")) {
|
|
31
|
+
throw new Error("Missing value for --bytes.");
|
|
32
|
+
}
|
|
33
|
+
bytes = next;
|
|
34
|
+
i += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (arg === "--copy" || arg === "-c") {
|
|
39
|
+
copy = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (arg === "--count" || arg === "-n") {
|
|
44
|
+
if (count !== 1) {
|
|
45
|
+
throw new Error("The --count option can only be specified once.");
|
|
46
|
+
}
|
|
47
|
+
const next = argv[i + 1];
|
|
48
|
+
if (!next || next.startsWith("-")) {
|
|
49
|
+
throw new Error("Missing value for --count.");
|
|
50
|
+
}
|
|
51
|
+
const parsedCount = parseCount(next);
|
|
52
|
+
count = parsedCount;
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (arg === "--env" || arg === "-e") {
|
|
58
|
+
if (envName != null) {
|
|
59
|
+
throw new Error("The --env option can only be specified once.");
|
|
60
|
+
}
|
|
61
|
+
const next = argv[i + 1];
|
|
62
|
+
if (!next || next.startsWith("-")) {
|
|
63
|
+
throw new Error("Missing value for --env.");
|
|
64
|
+
}
|
|
65
|
+
envName = parseEnvName(next);
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (arg === "--json" || arg === "-j") {
|
|
71
|
+
json = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (arg.startsWith("--")) {
|
|
76
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (arg.startsWith("-")) {
|
|
80
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (lengthValue != null) {
|
|
84
|
+
throw new Error("Multiple length values provided.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
lengthValue = arg;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { action: "generate", lengthValue, bytes, copy, count, envName, json };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseCharacterLength(value) {
|
|
94
|
+
const num = Number(value);
|
|
95
|
+
if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
|
96
|
+
throw new Error(`Invalid length: '${value}'. Must be a positive integer.`);
|
|
97
|
+
}
|
|
98
|
+
if (num <= 0) {
|
|
99
|
+
throw new Error("Length must be a positive integer.");
|
|
100
|
+
}
|
|
101
|
+
return num;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseByteLength(value) {
|
|
105
|
+
const num = Number(value);
|
|
106
|
+
if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid byte length: '${value}'. Must be a positive integer.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (num <= 0) {
|
|
112
|
+
throw new Error("Byte length must be a positive integer.");
|
|
113
|
+
}
|
|
114
|
+
return num;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseCount(value) {
|
|
118
|
+
const num = Number(value);
|
|
119
|
+
if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Invalid count: '${value}'. Must be a positive integer.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (num <= 0) {
|
|
125
|
+
throw new Error("Count must be a positive integer.");
|
|
126
|
+
}
|
|
127
|
+
if (num > MAX_COUNT) {
|
|
128
|
+
throw new Error(`Count exceeds maximum of ${MAX_COUNT}.`);
|
|
129
|
+
}
|
|
130
|
+
return num;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseEnvName(value) {
|
|
134
|
+
if (!value || value.trim() === "") {
|
|
135
|
+
throw new Error("Environment variable name cannot be empty.");
|
|
136
|
+
}
|
|
137
|
+
if (value.includes(" ")) {
|
|
138
|
+
throw new Error("Environment variable name cannot contain spaces.");
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
DEFAULT_LENGTH,
|
|
145
|
+
MAX_LENGTH,
|
|
146
|
+
MAX_BYTES,
|
|
147
|
+
MAX_COUNT,
|
|
148
|
+
parseArgs,
|
|
149
|
+
parseCharacterLength,
|
|
150
|
+
parseByteLength,
|
|
151
|
+
parseCount,
|
|
152
|
+
parseEnvName
|
|
153
|
+
};
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const {
|
|
2
|
+
DEFAULT_LENGTH,
|
|
3
|
+
MAX_LENGTH,
|
|
4
|
+
MAX_BYTES,
|
|
5
|
+
parseArgs,
|
|
6
|
+
parseCharacterLength,
|
|
7
|
+
parseByteLength
|
|
8
|
+
} = require("./args");
|
|
9
|
+
const { generateSecrets, bytesUsed } = require("./generator");
|
|
10
|
+
const { formatPlain, formatJson, copyToClipboard } = require("./output");
|
|
11
|
+
const { getHelp } = require("./help");
|
|
12
|
+
const { version } = require("../package.json");
|
|
13
|
+
|
|
14
|
+
function run(argv, stdout, stderr, exit) {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = parseArgs(argv);
|
|
17
|
+
|
|
18
|
+
if (parsed.action === "help") {
|
|
19
|
+
stdout.write(getHelp() + "\n");
|
|
20
|
+
exit(0);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (parsed.action === "version") {
|
|
25
|
+
stdout.write(version + "\n");
|
|
26
|
+
exit(0);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (parsed.bytes != null && parsed.lengthValue != null) {
|
|
31
|
+
throw new Error("Provide either a length or --bytes, not both.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (parsed.count > 1 && parsed.envName != null) {
|
|
35
|
+
throw new Error("The --env option is only supported for single secret generation (count=1).");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (parsed.count > 1 && parsed.copy) {
|
|
39
|
+
throw new Error("Copy is only supported for single secret.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (parsed.json && parsed.count > 1) {
|
|
43
|
+
throw new Error("JSON mode is only supported for single secret generation (count=1).");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (parsed.json && parsed.envName != null) {
|
|
47
|
+
throw new Error("JSON mode is not compatible with --env. Use JSON output directly for structured data.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (parsed.json && parsed.copy) {
|
|
51
|
+
throw new Error("JSON mode is not compatible with --copy. Use JSON output for automation.");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let charLength;
|
|
55
|
+
let cachedByteLength = null;
|
|
56
|
+
|
|
57
|
+
if (parsed.bytes != null) {
|
|
58
|
+
cachedByteLength = parseByteLength(parsed.bytes);
|
|
59
|
+
if (cachedByteLength * 2 > MAX_LENGTH) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Byte length exceeds maximum of ${MAX_BYTES} bytes (${MAX_LENGTH} characters).`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
charLength = cachedByteLength * 2;
|
|
65
|
+
} else if (parsed.lengthValue != null) {
|
|
66
|
+
const length = parseCharacterLength(parsed.lengthValue);
|
|
67
|
+
if (length > MAX_LENGTH) {
|
|
68
|
+
throw new Error(`Length exceeds maximum of ${MAX_LENGTH} characters.`);
|
|
69
|
+
}
|
|
70
|
+
charLength = length;
|
|
71
|
+
} else {
|
|
72
|
+
charLength = DEFAULT_LENGTH;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const secrets = generateSecrets(charLength, parsed.count);
|
|
76
|
+
|
|
77
|
+
if (parsed.json) {
|
|
78
|
+
const bytes = bytesUsed(charLength, cachedByteLength);
|
|
79
|
+
const jsonOutput = formatJson(secrets[0], charLength, bytes);
|
|
80
|
+
stdout.write(jsonOutput + "\n");
|
|
81
|
+
} else {
|
|
82
|
+
const output = formatPlain(secrets, parsed.envName);
|
|
83
|
+
stdout.write(output + "\n");
|
|
84
|
+
|
|
85
|
+
if (parsed.copy) {
|
|
86
|
+
try {
|
|
87
|
+
copyToClipboard(secrets[0]);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(error.message || "Failed to copy to clipboard.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
exit(0);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
stderr.write(`Error: ${error.message}\n`);
|
|
97
|
+
exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
run
|
|
103
|
+
};
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
function generateSecrets(charLength, count) {
|
|
4
|
+
const secrets = [];
|
|
5
|
+
for (let i = 0; i < count; i += 1) {
|
|
6
|
+
const secret = generateHex(charLength);
|
|
7
|
+
secrets.push(secret);
|
|
8
|
+
}
|
|
9
|
+
return secrets;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function generateHex(charLength) {
|
|
13
|
+
const bytesNeeded = Math.ceil(charLength / 2);
|
|
14
|
+
const hex = crypto.randomBytes(bytesNeeded).toString("hex");
|
|
15
|
+
return hex.slice(0, charLength);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function bytesUsed(charLength, inputBytes) {
|
|
19
|
+
if (inputBytes != null) {
|
|
20
|
+
return inputBytes;
|
|
21
|
+
}
|
|
22
|
+
return Math.ceil(charLength / 2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
generateSecrets,
|
|
27
|
+
bytesUsed
|
|
28
|
+
};
|
package/src/help.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { DEFAULT_LENGTH, MAX_LENGTH, MAX_BYTES, MAX_COUNT } = require("./args");
|
|
2
|
+
|
|
3
|
+
function getHelp() {
|
|
4
|
+
return (
|
|
5
|
+
`hexpass - Generate cryptographically secure hex passwords\n\n` +
|
|
6
|
+
`Usage:\n` +
|
|
7
|
+
` hexpass [length] Generate hex string with specified length (default: ${DEFAULT_LENGTH})\n` +
|
|
8
|
+
` hexpass --bytes <n> Generate from n random bytes (output has n*2 characters)\n` +
|
|
9
|
+
` hexpass --count <n> Generate n secrets (default: 1, max: ${MAX_COUNT})\n` +
|
|
10
|
+
` hexpass --env <NAME> Output as NAME=secret instead of raw secret\n` +
|
|
11
|
+
` hexpass --json Output as JSON object (single secret only)\n\n` +
|
|
12
|
+
`Options:\n` +
|
|
13
|
+
` -h, --help Show this help message\n` +
|
|
14
|
+
` -v, --version Show version number\n` +
|
|
15
|
+
` -c, --copy Copy the generated hex string to clipboard (single secret only)\n` +
|
|
16
|
+
` -n, --count <n> Generate n secrets (default: 1, max: ${MAX_COUNT})\n` +
|
|
17
|
+
` -e, --env <NAME> Output as NAME=secret (single secret only)\n` +
|
|
18
|
+
` -j, --json Output as JSON object (single secret only, incompatible with --env and --copy)\n` +
|
|
19
|
+
` --bytes <n> Specify length in bytes instead of characters\n\n` +
|
|
20
|
+
`Examples:\n` +
|
|
21
|
+
` hexpass # 32-character hex string\n` +
|
|
22
|
+
` hexpass 64 # 64-character hex string\n` +
|
|
23
|
+
` hexpass --bytes 32 # 64-character hex string (32 bytes)\n` +
|
|
24
|
+
` hexpass 48 --copy # Generate 48 chars and copy to clipboard\n` +
|
|
25
|
+
` hexpass 32 --count 5 # Generate 5 secrets, each 32 characters\n` +
|
|
26
|
+
` hexpass 48 --env JWT_SECRET # Output as JWT_SECRET=<secret>\n` +
|
|
27
|
+
` hexpass 32 --json # Output as JSON with length, bytes, and hex fields\n\n` +
|
|
28
|
+
`Limits:\n` +
|
|
29
|
+
` Maximum output length is ${MAX_LENGTH} characters (${MAX_BYTES} bytes).\n` +
|
|
30
|
+
` Maximum count is ${MAX_COUNT} secrets.\n\n` +
|
|
31
|
+
`Security:\n` +
|
|
32
|
+
` All passwords are generated using Node.js crypto.randomBytes()`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
getHelp
|
|
38
|
+
};
|
package/src/output.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { spawnSync } = require("child_process");
|
|
2
|
+
|
|
3
|
+
function formatPlain(secrets, envName) {
|
|
4
|
+
const output = secrets.map(secret => {
|
|
5
|
+
if (envName != null) {
|
|
6
|
+
return `${envName}=${secret}`;
|
|
7
|
+
}
|
|
8
|
+
return secret;
|
|
9
|
+
}).join("\n");
|
|
10
|
+
return output;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatJson(secret, charLength, bytesValue) {
|
|
14
|
+
return JSON.stringify({
|
|
15
|
+
length: charLength,
|
|
16
|
+
bytes: bytesValue,
|
|
17
|
+
hex: secret
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function copyToClipboard(text) {
|
|
22
|
+
const platform = process.platform;
|
|
23
|
+
|
|
24
|
+
if (platform === "darwin") {
|
|
25
|
+
runClipboardCommand("pbcopy", text);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (platform === "win32") {
|
|
30
|
+
runClipboardCommand("clip", text);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const linuxCommands = [
|
|
35
|
+
["xclip", ["-selection", "clipboard"]],
|
|
36
|
+
["xsel", ["--clipboard", "--input"]],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const [cmd, args] of linuxCommands) {
|
|
40
|
+
const result = spawnSync(cmd, args, { input: text, encoding: "utf8" });
|
|
41
|
+
if (!result.error && result.status === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Clipboard copy not supported on this system. Install xclip or xsel."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runClipboardCommand(command, input) {
|
|
52
|
+
const result = spawnSync(command, [], { input, encoding: "utf8" });
|
|
53
|
+
if (result.error || result.status !== 0) {
|
|
54
|
+
throw new Error(`Failed to copy to clipboard using ${command}.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
formatPlain,
|
|
60
|
+
formatJson,
|
|
61
|
+
copyToClipboard
|
|
62
|
+
};
|