yamlock 0.2.1 → 0.2.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/README.md +16 -0
- package/dist/cli/cli.js +129 -15
- package/dist/utils/config.js +25 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,10 +47,26 @@ YAMLOCK_KEY="super-secret" yamlock encrypt config.yaml
|
|
|
47
47
|
|
|
48
48
|
# Decrypt values in place using explicit key/algorithm flags
|
|
49
49
|
yamlock decrypt settings.json --key "super-secret" --algorithm aes-256-cbc
|
|
50
|
+
|
|
51
|
+
# Encrypt only selected fields into a new file
|
|
52
|
+
yamlock encrypt config.json --key "$YAMLOCK_KEY" --paths "db.password,api.token" --output config.secure.json
|
|
53
|
+
|
|
54
|
+
# Inspect CLI metadata
|
|
55
|
+
yamlock version
|
|
56
|
+
yamlock algorithms
|
|
57
|
+
|
|
58
|
+
# Generate a random key for YAMLOCK_KEY
|
|
59
|
+
yamlock keygen --length 64 --format base64
|
|
50
60
|
```
|
|
51
61
|
|
|
52
62
|
The CLI detects YAML (`.yaml`/`.yml`) and JSON extensions automatically and writes the file back in the same format.
|
|
53
63
|
|
|
64
|
+
Options of note:
|
|
65
|
+
- `--output <file>` writes the result to a separate file instead of overwriting the input.
|
|
66
|
+
- `--paths <path1,path2>` targets only the specified fields (dot/bracket notation like `db.password` or `users[0].token`).
|
|
67
|
+
- Command `keygen` produces a random key and shows how to store it (shell export or `.env`).
|
|
68
|
+
- Commands `version` and `algorithms` print the installed CLI version and the list of supported ciphers respectively.
|
|
69
|
+
|
|
54
70
|
### Node.js API
|
|
55
71
|
|
|
56
72
|
```js
|
package/dist/cli/cli.js
CHANGED
|
@@ -2,27 +2,45 @@
|
|
|
2
2
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { resolve, extname } from 'node:path';
|
|
4
4
|
import { exit } from 'node:process';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
5
7
|
|
|
6
8
|
import yaml from 'js-yaml';
|
|
7
9
|
|
|
8
10
|
import { processConfig } from '../utils/config.js';
|
|
11
|
+
import { listSupportedAlgorithms } from '../crypto/utils.js';
|
|
9
12
|
|
|
10
|
-
const
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const packageJson = require('../../package.json');
|
|
15
|
+
|
|
16
|
+
const BANNER = `
|
|
11
17
|
░█░█░█▀█░█▄░▄█░█░░░█▀█░█▀▀░█░█░
|
|
12
18
|
░░█░░█▀█░█░▀░█░█░░░█░█░█░░░█▀▄░
|
|
13
|
-
|
|
19
|
+
░░▀░░▀░▀░▀░░░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░`;
|
|
20
|
+
|
|
21
|
+
function getHelpText() {
|
|
22
|
+
return `${BANNER}
|
|
23
|
+
Version: ${packageJson.version}
|
|
14
24
|
|
|
15
25
|
Usage:
|
|
16
26
|
yamlock <command> [options]
|
|
17
27
|
|
|
18
28
|
Commands:
|
|
19
|
-
encrypt <file>
|
|
20
|
-
decrypt <file>
|
|
29
|
+
encrypt <file> Encrypt string values in the given YAML/JSON file.
|
|
30
|
+
decrypt <file> Decrypt string values in the given YAML/JSON file.
|
|
31
|
+
version Print the yamlock CLI version.
|
|
32
|
+
algorithms Print the list of supported cipher algorithms.
|
|
33
|
+
keygen Generate a random YAMLOCK_KEY.
|
|
21
34
|
|
|
22
35
|
Options:
|
|
23
36
|
-k, --key <value> Encryption key (or use YAMLOCK_KEY env).
|
|
24
37
|
-a, --algorithm <value> Cipher algorithm (default: aes-256-cbc).
|
|
38
|
+
-o, --output <file> Write the result to a different file (otherwise overwrites the input file).
|
|
39
|
+
-p, --paths <p1,p2> Comma-separated list of field paths to process (dot/bracket notation).
|
|
40
|
+
--length <bytes> (keygen) Number of random bytes to generate (default: 32).
|
|
41
|
+
--format <hex|base64> (keygen) Output format (default: base64).
|
|
25
42
|
`;
|
|
43
|
+
}
|
|
26
44
|
|
|
27
45
|
function print(message) {
|
|
28
46
|
console.log(message);
|
|
@@ -63,15 +81,35 @@ function writeConfigFile(filePath, format, data) {
|
|
|
63
81
|
writeFileSync(filePath, `${serialized}\n`, 'utf8');
|
|
64
82
|
}
|
|
65
83
|
|
|
84
|
+
function parsePaths(value) {
|
|
85
|
+
if (!value) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return String(value)
|
|
90
|
+
.split(',')
|
|
91
|
+
.map((segment) => segment.trim())
|
|
92
|
+
.filter((segment) => segment.length > 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
66
95
|
function parseArgs(argv) {
|
|
67
96
|
const args = argv.slice(2);
|
|
68
97
|
const result = {
|
|
69
98
|
command: args[0],
|
|
70
|
-
file:
|
|
99
|
+
file: undefined,
|
|
71
100
|
options: {}
|
|
72
101
|
};
|
|
73
102
|
|
|
74
|
-
|
|
103
|
+
let index = 1;
|
|
104
|
+
const potentialFile = args[1];
|
|
105
|
+
if (potentialFile && !potentialFile.startsWith('-')) {
|
|
106
|
+
result.file = potentialFile;
|
|
107
|
+
index = 2;
|
|
108
|
+
} else {
|
|
109
|
+
index = 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (let i = index; i < args.length; i += 1) {
|
|
75
113
|
const arg = args[i];
|
|
76
114
|
const next = args[i + 1];
|
|
77
115
|
|
|
@@ -81,17 +119,79 @@ function parseArgs(argv) {
|
|
|
81
119
|
} else if (arg === '-a' || arg === '--algorithm') {
|
|
82
120
|
result.options.algorithm = next;
|
|
83
121
|
i += 1;
|
|
122
|
+
} else if (arg === '-o' || arg === '--output') {
|
|
123
|
+
result.options.output = next;
|
|
124
|
+
i += 1;
|
|
125
|
+
} else if (arg === '-p' || arg === '--paths') {
|
|
126
|
+
result.options.paths = parsePaths(next);
|
|
127
|
+
i += 1;
|
|
128
|
+
} else if (arg === '--length') {
|
|
129
|
+
result.options.length = next;
|
|
130
|
+
i += 1;
|
|
131
|
+
} else if (arg === '--format') {
|
|
132
|
+
result.options.format = next;
|
|
133
|
+
i += 1;
|
|
84
134
|
}
|
|
85
135
|
}
|
|
86
136
|
|
|
87
137
|
return result;
|
|
88
138
|
}
|
|
89
139
|
|
|
140
|
+
function generateRandomKey(length, format) {
|
|
141
|
+
const size = Number.isFinite(length) && length > 0 ? Math.floor(length) : 32;
|
|
142
|
+
const buffer = randomBytes(size);
|
|
143
|
+
if (format === 'hex') {
|
|
144
|
+
return buffer.toString('hex');
|
|
145
|
+
}
|
|
146
|
+
return buffer.toString('base64');
|
|
147
|
+
}
|
|
148
|
+
|
|
90
149
|
export async function runCli(argv = process.argv) {
|
|
91
150
|
const { command, file, options } = parseArgs(argv);
|
|
92
151
|
|
|
93
|
-
if (!command
|
|
94
|
-
print(
|
|
152
|
+
if (!command) {
|
|
153
|
+
print(getHelpText().trim());
|
|
154
|
+
return exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (command === 'version') {
|
|
158
|
+
print(`yamlock ${packageJson.version}`);
|
|
159
|
+
return exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (command === 'algorithms') {
|
|
163
|
+
const algorithms = listSupportedAlgorithms();
|
|
164
|
+
print('Supported algorithms:');
|
|
165
|
+
algorithms.forEach((name) => print(`- ${name}`));
|
|
166
|
+
return exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (command === 'keygen') {
|
|
170
|
+
const desiredLength = options.length ? Number(options.length) : 32;
|
|
171
|
+
const normalizedFormat = (options.format ?? 'base64').toLowerCase();
|
|
172
|
+
|
|
173
|
+
if (!Number.isFinite(desiredLength) || desiredLength <= 0) {
|
|
174
|
+
printError('Key length must be a positive number.');
|
|
175
|
+
return exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!['base64', 'hex'].includes(normalizedFormat)) {
|
|
179
|
+
printError('Key format must be either "base64" or "hex".');
|
|
180
|
+
return exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const keyValue = generateRandomKey(desiredLength, normalizedFormat);
|
|
184
|
+
print(`Generated key (${normalizedFormat}, ${Math.floor(desiredLength)} bytes of entropy):`);
|
|
185
|
+
print(keyValue);
|
|
186
|
+
print('\nStore it securely, e.g.');
|
|
187
|
+
print(` export YAMLOCK_KEY="${keyValue}"`);
|
|
188
|
+
print(' # or place in an .env file as YAMLOCK_KEY=your-key');
|
|
189
|
+
return exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!file) {
|
|
193
|
+
printError('A file path is required for this command.');
|
|
194
|
+
print(getHelpText().trim());
|
|
95
195
|
return exit(1);
|
|
96
196
|
}
|
|
97
197
|
|
|
@@ -110,23 +210,37 @@ export async function runCli(argv = process.argv) {
|
|
|
110
210
|
return exit(1);
|
|
111
211
|
}
|
|
112
212
|
|
|
213
|
+
const outputPath = options.output
|
|
214
|
+
? resolve(process.cwd(), options.output)
|
|
215
|
+
: absolutePath;
|
|
216
|
+
|
|
113
217
|
try {
|
|
114
218
|
if (command === 'encrypt') {
|
|
115
|
-
const result = processConfig(config.data, {
|
|
116
|
-
|
|
117
|
-
|
|
219
|
+
const result = processConfig(config.data, {
|
|
220
|
+
mode: 'encrypt',
|
|
221
|
+
key,
|
|
222
|
+
algorithm: options.algorithm,
|
|
223
|
+
paths: options.paths
|
|
224
|
+
});
|
|
225
|
+
writeConfigFile(outputPath, config.format, result);
|
|
226
|
+
print(`Encrypted values in ${outputPath === absolutePath ? file : options.output}`);
|
|
118
227
|
return exit(0);
|
|
119
228
|
}
|
|
120
229
|
|
|
121
230
|
if (command === 'decrypt') {
|
|
122
|
-
const result = processConfig(config.data, {
|
|
123
|
-
|
|
124
|
-
|
|
231
|
+
const result = processConfig(config.data, {
|
|
232
|
+
mode: 'decrypt',
|
|
233
|
+
key,
|
|
234
|
+
algorithm: options.algorithm,
|
|
235
|
+
paths: options.paths
|
|
236
|
+
});
|
|
237
|
+
writeConfigFile(outputPath, config.format, result);
|
|
238
|
+
print(`Decrypted values in ${outputPath === absolutePath ? file : options.output}`);
|
|
125
239
|
return exit(0);
|
|
126
240
|
}
|
|
127
241
|
|
|
128
242
|
printError(`Unknown command: ${command}`);
|
|
129
|
-
print(
|
|
243
|
+
print(getHelpText().trim());
|
|
130
244
|
return exit(1);
|
|
131
245
|
} catch (error) {
|
|
132
246
|
printError(`Operation failed: ${error.message}`);
|
package/dist/utils/config.js
CHANGED
|
@@ -15,18 +15,33 @@ const MODES = {
|
|
|
15
15
|
* @param {string|Buffer} options.key
|
|
16
16
|
* @param {string|object} [options.algorithm]
|
|
17
17
|
* @param {object} [options.algorithmOptions]
|
|
18
|
+
* @param {string[]} [options.paths]
|
|
18
19
|
* @param {Array<string|number>} [options.parentPath]
|
|
19
20
|
* @returns {Object|Array}
|
|
20
21
|
*/
|
|
21
|
-
export function processConfig(node,
|
|
22
|
+
export function processConfig(node, options) {
|
|
22
23
|
if (typeof node !== 'object' || node === null) {
|
|
23
24
|
throw new Error('processConfig expects a non-null object or array.');
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
const mode = options.mode;
|
|
26
28
|
if (mode !== MODES.ENCRYPT && mode !== MODES.DECRYPT) {
|
|
27
29
|
throw new Error(`Unknown processConfig mode: ${mode}`);
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
const normalizedPaths = Array.isArray(options.paths) && options.paths.length > 0
|
|
33
|
+
? new Set(options.paths.map((path) => String(path).trim()).filter(Boolean))
|
|
34
|
+
: null;
|
|
35
|
+
|
|
36
|
+
return traverseConfig(node, {
|
|
37
|
+
...options,
|
|
38
|
+
mode,
|
|
39
|
+
parentPath: options.parentPath ?? [],
|
|
40
|
+
normalizedPaths
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function traverseConfig(node, { mode, key, algorithm, algorithmOptions, parentPath, normalizedPaths }) {
|
|
30
45
|
const isArrayNode = Array.isArray(node);
|
|
31
46
|
const result = isArrayNode ? [] : {};
|
|
32
47
|
const cryptoOptions = algorithmOptions ?? algorithm;
|
|
@@ -37,12 +52,13 @@ export function processConfig(node, { mode, key, algorithm, algorithmOptions, pa
|
|
|
37
52
|
const currentPath = buildPath(parentPath, segment);
|
|
38
53
|
|
|
39
54
|
if (value !== null && typeof value === 'object') {
|
|
40
|
-
result[targetKey] =
|
|
55
|
+
result[targetKey] = traverseConfig(value, {
|
|
41
56
|
mode,
|
|
42
57
|
key,
|
|
43
58
|
algorithm: cryptoOptions,
|
|
44
59
|
algorithmOptions: cryptoOptions,
|
|
45
|
-
parentPath: [...parentPath, segment]
|
|
60
|
+
parentPath: [...parentPath, segment],
|
|
61
|
+
normalizedPaths
|
|
46
62
|
});
|
|
47
63
|
return;
|
|
48
64
|
}
|
|
@@ -52,6 +68,12 @@ export function processConfig(node, { mode, key, algorithm, algorithmOptions, pa
|
|
|
52
68
|
return;
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
const shouldProcess = !normalizedPaths || normalizedPaths.has(currentPath);
|
|
72
|
+
if (!shouldProcess) {
|
|
73
|
+
result[targetKey] = value;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
if (mode === MODES.ENCRYPT) {
|
|
56
78
|
result[targetKey] = encryptValue(value, key, currentPath, cryptoOptions);
|
|
57
79
|
} else {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yamlock",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"author": "PAVEL TKACHEV",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"author": "PAVEL TKACHEV (phoenixweiss) <mail@phoenixweiss.me>",
|
|
5
5
|
"description": "Value-level encryption for YAML/JSON configuration files with CLI + Node.js APIs.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|