cryptoserve 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/bin/cryptoserve.mjs +812 -0
- package/lib/cli-style.mjs +217 -0
- package/lib/client.mjs +138 -0
- package/lib/context-resolver.mjs +339 -0
- package/lib/credentials.mjs +67 -0
- package/lib/init.mjs +241 -0
- package/lib/keychain.mjs +303 -0
- package/lib/local-crypto.mjs +218 -0
- package/lib/pqc-engine.mjs +636 -0
- package/lib/scanner.mjs +323 -0
- package/lib/vault.mjs +242 -0
- package/package.json +36 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CryptoServe CLI — zero-dependency Node.js CLI.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* cryptoserve help
|
|
8
|
+
* cryptoserve version
|
|
9
|
+
* cryptoserve init [--insecure-storage]
|
|
10
|
+
* cryptoserve pqc [--profile P] [--format json] [--verbose]
|
|
11
|
+
* cryptoserve scan [path] [--format json]
|
|
12
|
+
* cryptoserve encrypt "text" [--context C | --algorithm A] [--password P]
|
|
13
|
+
* cryptoserve decrypt "blob" [--password P]
|
|
14
|
+
* cryptoserve encrypt --file in --output out [--context C | --algorithm A] [--password P]
|
|
15
|
+
* cryptoserve decrypt --file in --output out [--password P]
|
|
16
|
+
* cryptoserve hash-password [--algorithm scrypt|pbkdf2]
|
|
17
|
+
* cryptoserve context list | show NAME [--verbose] [--format json]
|
|
18
|
+
* cryptoserve vault init|set|get|list|delete|run|import|export
|
|
19
|
+
* cryptoserve login [--server URL]
|
|
20
|
+
* cryptoserve status
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
24
|
+
import { resolve, dirname, join } from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const PKG = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Arg parsing helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function getFlag(args, name) {
|
|
35
|
+
const idx = args.indexOf(name);
|
|
36
|
+
return idx !== -1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getOption(args, name, defaultValue = null) {
|
|
40
|
+
const idx = args.indexOf(name);
|
|
41
|
+
if (idx === -1 || idx + 1 >= args.length) return defaultValue;
|
|
42
|
+
return args[idx + 1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getPositional(args, optionsWithValues = ['--password', '--algorithm', '--profile', '--format', '--file', '--output', '--server', '--context']) {
|
|
46
|
+
const result = [];
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
if (args[i].startsWith('--')) {
|
|
49
|
+
// Skip the flag; if it takes a value, skip the next arg too
|
|
50
|
+
if (optionsWithValues.includes(args[i])) i++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
result.push(args[i]);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Commands
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
async function cmdHelp() {
|
|
63
|
+
const { compactHeader, dim, bold, info } = await import('../lib/cli-style.mjs');
|
|
64
|
+
console.log(compactHeader());
|
|
65
|
+
console.log(` ${bold('CryptoServe CLI')} v${PKG.version}`);
|
|
66
|
+
console.log(` ${dim('Cryptographic scanning, PQC analysis, encryption, and key management')}\n`);
|
|
67
|
+
console.log(` ${bold('Scanning & Analysis')}`);
|
|
68
|
+
console.log(` ${info('pqc [--profile P] [--format json]')} Post-quantum readiness analysis`);
|
|
69
|
+
console.log(` ${info('scan [path] [--format json]')} Scan project for crypto & secrets`);
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(` ${bold('Encryption')}`);
|
|
72
|
+
console.log(` ${info('encrypt "text" [--context C]')} Encrypt with context-aware algorithm selection`);
|
|
73
|
+
console.log(` ${info('encrypt "text" [--password P]')} Encrypt text (interactive password if omitted)`);
|
|
74
|
+
console.log(` ${info('decrypt "blob" [--password P]')} Decrypt text`);
|
|
75
|
+
console.log(` ${info('encrypt --file F --output O')} Encrypt file`);
|
|
76
|
+
console.log(` ${info('decrypt --file F --output O')} Decrypt file`);
|
|
77
|
+
console.log(` ${info('hash-password [--algorithm A]')} Hash a password (scrypt/pbkdf2)`);
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(` ${bold('Contexts')}`);
|
|
80
|
+
console.log(` ${info('context list')} List available encryption contexts`);
|
|
81
|
+
console.log(` ${info('context show NAME [--verbose]')} Show context details and resolved algorithm`);
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(` ${bold('Key Management')}`);
|
|
84
|
+
console.log(` ${info('init [--insecure-storage]')} Set up master key + AI tool protection`);
|
|
85
|
+
console.log(` ${info('vault init')} Create encrypted vault`);
|
|
86
|
+
console.log(` ${info('vault set KEY VALUE')} Store a secret`);
|
|
87
|
+
console.log(` ${info('vault get KEY')} Retrieve a secret`);
|
|
88
|
+
console.log(` ${info('vault list')} List stored secrets`);
|
|
89
|
+
console.log(` ${info('vault delete KEY')} Remove a secret`);
|
|
90
|
+
console.log(` ${info('vault run -- CMD [ARGS]')} Run command with secrets as env vars`);
|
|
91
|
+
console.log(` ${info('vault import .env')} Import .env file into vault`);
|
|
92
|
+
console.log();
|
|
93
|
+
console.log(` ${bold('Platform')}`);
|
|
94
|
+
console.log(` ${info('login [--server URL]')} Authenticate with CryptoServe server`);
|
|
95
|
+
console.log(` ${info('status')} Show configuration and server status`);
|
|
96
|
+
console.log(` ${info('version')} Show version`);
|
|
97
|
+
console.log(` ${info('help')} Show this help`);
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function cmdVersion() {
|
|
102
|
+
console.log(`cryptoserve ${PKG.version}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function cmdInit(args) {
|
|
106
|
+
const { compactHeader, success, info, warning, dim, labelValue } = await import('../lib/cli-style.mjs');
|
|
107
|
+
const { initProject } = await import('../lib/init.mjs');
|
|
108
|
+
|
|
109
|
+
console.log(compactHeader('init'));
|
|
110
|
+
|
|
111
|
+
const insecure = getFlag(args, '--insecure-storage');
|
|
112
|
+
const result = await initProject(process.cwd(), { insecureStorage: insecure });
|
|
113
|
+
|
|
114
|
+
// Key storage
|
|
115
|
+
if (result.keyStorage?.storage === 'keychain') {
|
|
116
|
+
console.log(success(`Master key stored in OS keychain (${result.keyStorage.platform})`));
|
|
117
|
+
} else if (result.keyStorage?.storage === 'encrypted-file') {
|
|
118
|
+
console.log(success(`Master key stored in encrypted file`));
|
|
119
|
+
} else if (result.keyStorage?.storage === 'plaintext-file') {
|
|
120
|
+
console.log(warning('Master key stored as plaintext (--insecure-storage)'));
|
|
121
|
+
} else if (result.keyStorage?.storage === 'existing') {
|
|
122
|
+
console.log(info('Master key already configured'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// AI tools
|
|
126
|
+
if (result.toolsDetected.length > 0) {
|
|
127
|
+
console.log(`\n ${dim('Detected AI tools:')}`);
|
|
128
|
+
for (const tool of result.toolsDetected) {
|
|
129
|
+
console.log(` ${success(tool)}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (result.toolsConfigured.length > 0) {
|
|
134
|
+
console.log(`\n ${dim('Configured protections:')}`);
|
|
135
|
+
for (const tool of result.toolsConfigured) {
|
|
136
|
+
console.log(` ${success(tool)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (result.filesCreated.length > 0) {
|
|
141
|
+
console.log(`\n ${dim('Created:')}`);
|
|
142
|
+
for (const f of result.filesCreated) console.log(` + ${f}`);
|
|
143
|
+
}
|
|
144
|
+
if (result.filesModified.length > 0) {
|
|
145
|
+
console.log(`\n ${dim('Modified:')}`);
|
|
146
|
+
for (const f of result.filesModified) console.log(` ~ ${f}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function cmdPqc(args) {
|
|
153
|
+
const { compactHeader, section, labelValue, tableHeader, tableRow, progressBar, statusBadge, divider, success, warning, error, info, dim, bold } = await import('../lib/cli-style.mjs');
|
|
154
|
+
const { analyzeOffline, DATA_PROFILES } = await import('../lib/pqc-engine.mjs');
|
|
155
|
+
|
|
156
|
+
const profile = getOption(args, '--profile', 'general');
|
|
157
|
+
const format = getOption(args, '--format', 'text');
|
|
158
|
+
const verbose = getFlag(args, '--verbose');
|
|
159
|
+
|
|
160
|
+
// Validate profile name
|
|
161
|
+
if (!DATA_PROFILES[profile]) {
|
|
162
|
+
const valid = Object.keys(DATA_PROFILES).join(', ');
|
|
163
|
+
if (format !== 'json') {
|
|
164
|
+
console.error(warning(`Unknown profile "${profile}", using default. Valid: ${valid}`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Use scanner results if available, otherwise use example libraries
|
|
169
|
+
let libraries = [];
|
|
170
|
+
try {
|
|
171
|
+
const { scanProject, toLibraryInventory } = await import('../lib/scanner.mjs');
|
|
172
|
+
const scanResults = scanProject(process.cwd());
|
|
173
|
+
libraries = toLibraryInventory(scanResults);
|
|
174
|
+
} catch { /* scanner not available, empty libraries */ }
|
|
175
|
+
|
|
176
|
+
const result = analyzeOffline(libraries, profile);
|
|
177
|
+
|
|
178
|
+
if (format === 'json') {
|
|
179
|
+
console.log(JSON.stringify(result, null, 2));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(compactHeader('pqc'));
|
|
184
|
+
|
|
185
|
+
// Data profile
|
|
186
|
+
console.log(section('Data Profile'));
|
|
187
|
+
console.log(labelValue('Profile', result.dataProfile.name));
|
|
188
|
+
console.log(labelValue('Protection needed', `${result.dataProfile.lifespanYears} years`));
|
|
189
|
+
console.log(labelValue('Urgency', result.dataProfile.urgency.toUpperCase()));
|
|
190
|
+
|
|
191
|
+
// Quantum readiness score
|
|
192
|
+
console.log(section('Quantum Readiness'));
|
|
193
|
+
console.log(` ${progressBar(result.quantumReadinessScore, 100)} ${bold(`${result.quantumReadinessScore}/100`)}`);
|
|
194
|
+
|
|
195
|
+
// SNDL assessment
|
|
196
|
+
const sndl = result.sndlAssessment;
|
|
197
|
+
console.log(section('SNDL Risk Assessment'));
|
|
198
|
+
console.log(labelValue('Risk level', statusBadge(sndl.riskLevel)));
|
|
199
|
+
console.log(labelValue('Vulnerable', sndl.vulnerable ? 'YES' : 'No'));
|
|
200
|
+
console.log(labelValue('Risk window', `${sndl.riskWindowYears} years`));
|
|
201
|
+
console.log(` ${dim(sndl.explanation)}`);
|
|
202
|
+
|
|
203
|
+
// KEM recommendations
|
|
204
|
+
if (result.kemRecommendations.length > 0) {
|
|
205
|
+
console.log(section('KEM Recommendations'));
|
|
206
|
+
console.log(tableHeader(['Algorithm', 'FIPS', 'Level', 'Score'], [20, 12, 14, 8]));
|
|
207
|
+
for (const rec of result.kemRecommendations) {
|
|
208
|
+
console.log(tableRow(
|
|
209
|
+
[rec.recommendedAlgorithm, rec.fipsStandard, rec.securityLevel, `${rec.score}%`],
|
|
210
|
+
[20, 12, 14, 8]
|
|
211
|
+
));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Signature recommendations
|
|
216
|
+
if (result.signatureRecommendations.length > 0) {
|
|
217
|
+
console.log(section('Signature Recommendations'));
|
|
218
|
+
console.log(tableHeader(['Algorithm', 'FIPS', 'Level', 'Score'], [20, 12, 14, 8]));
|
|
219
|
+
for (const rec of result.signatureRecommendations) {
|
|
220
|
+
console.log(tableRow(
|
|
221
|
+
[rec.recommendedAlgorithm, rec.fipsStandard, rec.securityLevel, `${rec.score}%`],
|
|
222
|
+
[20, 12, 14, 8]
|
|
223
|
+
));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Migration plan
|
|
228
|
+
if (result.migrationPlan.length > 0) {
|
|
229
|
+
console.log(section('Migration Plan'));
|
|
230
|
+
for (const step of result.migrationPlan) {
|
|
231
|
+
const icon = step.priority === 'CRITICAL' ? error(step.action)
|
|
232
|
+
: step.priority === 'HIGH' ? warning(step.action)
|
|
233
|
+
: info(step.action);
|
|
234
|
+
console.log(` ${step.step}. ${icon}`);
|
|
235
|
+
if (verbose) console.log(` ${dim(step.description)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Key findings
|
|
240
|
+
console.log(section('Key Findings'));
|
|
241
|
+
for (const finding of result.keyFindings) {
|
|
242
|
+
console.log(` ${info(finding)}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Next steps
|
|
246
|
+
console.log(section('Next Steps'));
|
|
247
|
+
for (const step of result.nextSteps) {
|
|
248
|
+
console.log(` ${success(step)}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Compliance (verbose only)
|
|
252
|
+
if (verbose && result.complianceReferences.length > 0) {
|
|
253
|
+
console.log(section('Compliance References'));
|
|
254
|
+
for (const ref of result.complianceReferences) {
|
|
255
|
+
console.log(labelValue(ref.framework, `${ref.authority} — ${ref.detail}`));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Threat timelines (verbose only)
|
|
260
|
+
if (verbose && Object.keys(result.threatTimelines).length > 0) {
|
|
261
|
+
console.log(section('Threat Timelines'));
|
|
262
|
+
console.log(tableHeader(['Algorithm', 'Min', 'Median', 'Max', 'Status'], [14, 6, 8, 6, 10]));
|
|
263
|
+
for (const [, t] of Object.entries(result.threatTimelines)) {
|
|
264
|
+
console.log(tableRow(
|
|
265
|
+
[t.algorithm, `${t.minYears}y`, `${t.medianYears}y`, `${t.maxYears}y`, t.status],
|
|
266
|
+
[14, 6, 8, 6, 10]
|
|
267
|
+
));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function cmdScan(args) {
|
|
275
|
+
const { compactHeader, section, tableHeader, tableRow, success, warning, error, info, dim, bold, labelValue } = await import('../lib/cli-style.mjs');
|
|
276
|
+
const { scanProject } = await import('../lib/scanner.mjs');
|
|
277
|
+
|
|
278
|
+
const positional = getPositional(args);
|
|
279
|
+
const scanDir = positional.length > 0 ? resolve(positional[0]) : process.cwd();
|
|
280
|
+
const format = getOption(args, '--format', 'text');
|
|
281
|
+
|
|
282
|
+
const results = scanProject(scanDir);
|
|
283
|
+
|
|
284
|
+
if (format === 'json') {
|
|
285
|
+
console.log(JSON.stringify(results, null, 2));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log(compactHeader('scan'));
|
|
290
|
+
console.log(labelValue('Directory', scanDir));
|
|
291
|
+
console.log(labelValue('Files scanned', String(results.filesScanned)));
|
|
292
|
+
|
|
293
|
+
// Crypto libraries
|
|
294
|
+
if (results.libraries.length > 0) {
|
|
295
|
+
console.log(section('Crypto Libraries'));
|
|
296
|
+
console.log(tableHeader(['Library', 'Version', 'Risk', 'Algorithms'], [22, 10, 10, 30]));
|
|
297
|
+
for (const lib of results.libraries) {
|
|
298
|
+
const riskColor = lib.quantumRisk === 'high' ? `\x1b[91m${lib.quantumRisk}\x1b[0m`
|
|
299
|
+
: lib.quantumRisk === 'none' ? `\x1b[92m${lib.quantumRisk}\x1b[0m`
|
|
300
|
+
: lib.quantumRisk;
|
|
301
|
+
console.log(tableRow(
|
|
302
|
+
[lib.name, lib.version, lib.quantumRisk, lib.algorithms.join(', ')],
|
|
303
|
+
[22, 10, 10, 30]
|
|
304
|
+
));
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
console.log(`\n ${dim('No crypto libraries detected')}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Hardcoded secrets
|
|
311
|
+
if (results.secrets.length > 0) {
|
|
312
|
+
console.log(section('Hardcoded Secrets'));
|
|
313
|
+
for (const s of results.secrets) {
|
|
314
|
+
console.log(` ${error(`[CRIT] ${s.name}`)}`);
|
|
315
|
+
console.log(` ${dim(s.file)}`);
|
|
316
|
+
if (s.envVar) console.log(` ${info(`Use $${s.envVar} instead`)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Weak patterns
|
|
321
|
+
if (results.weakPatterns.length > 0) {
|
|
322
|
+
console.log(section('Weak Crypto Patterns'));
|
|
323
|
+
for (const w of results.weakPatterns) {
|
|
324
|
+
const icon = w.severity === 'critical' ? error(w.issue) : warning(w.issue);
|
|
325
|
+
console.log(` ${icon}`);
|
|
326
|
+
console.log(` ${dim(w.file)}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Cert files
|
|
331
|
+
if (results.certFiles.length > 0) {
|
|
332
|
+
console.log(section('Certificate/Key Files'));
|
|
333
|
+
for (const f of results.certFiles) {
|
|
334
|
+
console.log(` ${info(f)}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Summary
|
|
339
|
+
console.log(section('Summary'));
|
|
340
|
+
console.log(labelValue('Libraries', String(results.libraries.length)));
|
|
341
|
+
console.log(labelValue('Secrets found', results.secrets.length > 0 ? error(String(results.secrets.length)) : success('0')));
|
|
342
|
+
console.log(labelValue('Weak patterns', results.weakPatterns.length > 0 ? warning(String(results.weakPatterns.length)) : success('0')));
|
|
343
|
+
console.log(labelValue('Cert/key files', String(results.certFiles.length)));
|
|
344
|
+
console.log();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function cmdEncrypt(args) {
|
|
348
|
+
const { promptPassword } = await import('../lib/keychain.mjs');
|
|
349
|
+
const { encryptString, encryptFile } = await import('../lib/local-crypto.mjs');
|
|
350
|
+
|
|
351
|
+
const file = getOption(args, '--file');
|
|
352
|
+
const output = getOption(args, '--output');
|
|
353
|
+
let password = getOption(args, '--password');
|
|
354
|
+
const contextName = getOption(args, '--context');
|
|
355
|
+
const verbose = getFlag(args, '--verbose');
|
|
356
|
+
let algorithm = getOption(args, '--algorithm', 'AES-256-GCM');
|
|
357
|
+
|
|
358
|
+
// Context-aware algorithm selection
|
|
359
|
+
if (contextName) {
|
|
360
|
+
const { resolveContext } = await import('../lib/context-resolver.mjs');
|
|
361
|
+
const resolved = resolveContext(contextName);
|
|
362
|
+
if (resolved.error) {
|
|
363
|
+
console.error(`${resolved.error}\nValid contexts: ${resolved.validContexts.join(', ')}`);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
algorithm = resolved.algorithm;
|
|
367
|
+
|
|
368
|
+
if (verbose) {
|
|
369
|
+
const { dim, success, labelValue } = await import('../lib/cli-style.mjs');
|
|
370
|
+
console.error(labelValue('Context', `${contextName} → ${algorithm}`));
|
|
371
|
+
for (const f of resolved.factors) console.error(` ${dim(f)}`);
|
|
372
|
+
console.error();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Interactive password prompt if not provided
|
|
377
|
+
if (!password) {
|
|
378
|
+
password = await promptPassword('Encryption password: ');
|
|
379
|
+
if (!password) { console.error('Password required.'); process.exit(1); }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (file) {
|
|
383
|
+
const outPath = output || file + '.enc';
|
|
384
|
+
encryptFile(file, outPath, password, algorithm, contextName || 'file');
|
|
385
|
+
console.log(`Encrypted: ${outPath}`);
|
|
386
|
+
} else {
|
|
387
|
+
const positional = getPositional(args);
|
|
388
|
+
const text = positional[0];
|
|
389
|
+
if (!text) { console.error('Provide text to encrypt or use --file.'); process.exit(1); }
|
|
390
|
+
console.log(encryptString(text, password, algorithm, contextName || 'cli'));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function cmdDecrypt(args) {
|
|
395
|
+
const { promptPassword } = await import('../lib/keychain.mjs');
|
|
396
|
+
const { decryptString, decryptFile } = await import('../lib/local-crypto.mjs');
|
|
397
|
+
|
|
398
|
+
const file = getOption(args, '--file');
|
|
399
|
+
const output = getOption(args, '--output');
|
|
400
|
+
let password = getOption(args, '--password');
|
|
401
|
+
|
|
402
|
+
if (!password) {
|
|
403
|
+
password = await promptPassword('Decryption password: ');
|
|
404
|
+
if (!password) { console.error('Password required.'); process.exit(1); }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
if (file) {
|
|
409
|
+
const outPath = output || file.replace(/\.enc$/, '.dec');
|
|
410
|
+
decryptFile(file, outPath, password);
|
|
411
|
+
console.log(`Decrypted: ${outPath}`);
|
|
412
|
+
} else {
|
|
413
|
+
const positional = getPositional(args);
|
|
414
|
+
const blob = positional[0];
|
|
415
|
+
if (!blob) { console.error('Provide encrypted text or use --file.'); process.exit(1); }
|
|
416
|
+
console.log(decryptString(blob, password));
|
|
417
|
+
}
|
|
418
|
+
} catch (e) {
|
|
419
|
+
console.error(`Decryption failed: ${e.message}`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function cmdHashPassword(args) {
|
|
425
|
+
const { promptPassword } = await import('../lib/keychain.mjs');
|
|
426
|
+
const { hashPassword } = await import('../lib/local-crypto.mjs');
|
|
427
|
+
|
|
428
|
+
const algorithm = getOption(args, '--algorithm', 'scrypt');
|
|
429
|
+
const positional = getPositional(args);
|
|
430
|
+
let password = positional[0];
|
|
431
|
+
|
|
432
|
+
if (!password) {
|
|
433
|
+
password = await promptPassword('Password to hash: ');
|
|
434
|
+
if (!password) { console.error('Password required.'); process.exit(1); }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
console.log(hashPassword(password, algorithm));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function cmdVault(args) {
|
|
441
|
+
const { compactHeader, success, error, warning, info, dim, labelValue, tableHeader, tableRow } = await import('../lib/cli-style.mjs');
|
|
442
|
+
const { promptPassword } = await import('../lib/keychain.mjs');
|
|
443
|
+
|
|
444
|
+
const subcommand = args[0];
|
|
445
|
+
const restArgs = args.slice(1);
|
|
446
|
+
|
|
447
|
+
if (!subcommand || subcommand === 'help') {
|
|
448
|
+
console.log(compactHeader('vault'));
|
|
449
|
+
console.log(' vault init Create new vault');
|
|
450
|
+
console.log(' vault set KEY VALUE Store a secret');
|
|
451
|
+
console.log(' vault get KEY Retrieve a secret');
|
|
452
|
+
console.log(' vault list List stored secrets');
|
|
453
|
+
console.log(' vault delete KEY Remove a secret');
|
|
454
|
+
console.log(' vault run -- CMD ARGS Run command with secrets as env vars');
|
|
455
|
+
console.log(' vault import .env Import .env file');
|
|
456
|
+
console.log(' vault export Export encrypted bundle');
|
|
457
|
+
console.log(' vault reset Delete vault');
|
|
458
|
+
console.log();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const vault = await import('../lib/vault.mjs');
|
|
463
|
+
|
|
464
|
+
if (subcommand === 'init') {
|
|
465
|
+
if (vault.vaultExists()) {
|
|
466
|
+
console.log(warning('Vault already exists.'));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const pw = await promptPassword('Set vault password: ');
|
|
470
|
+
const pw2 = await promptPassword('Confirm password: ');
|
|
471
|
+
if (pw !== pw2) { console.error('Passwords do not match.'); process.exit(1); }
|
|
472
|
+
vault.initVault(pw);
|
|
473
|
+
console.log(success('Vault created at ~/.cryptoserve/vault.enc'));
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (subcommand === 'reset') {
|
|
478
|
+
vault.resetVault();
|
|
479
|
+
console.log(success('Vault deleted.'));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// All other commands need the vault password
|
|
484
|
+
const pw = await promptPassword('Vault password: ');
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
switch (subcommand) {
|
|
488
|
+
case 'set': {
|
|
489
|
+
const key = restArgs[0];
|
|
490
|
+
let value = restArgs[1];
|
|
491
|
+
if (!key) { console.error('Usage: vault set KEY VALUE'); process.exit(1); }
|
|
492
|
+
if (!value) {
|
|
493
|
+
// Read from stdin if no value provided
|
|
494
|
+
value = await promptPassword(`Value for ${key}: `);
|
|
495
|
+
}
|
|
496
|
+
vault.setSecret(pw, key, value);
|
|
497
|
+
console.log(success(`Stored: ${key}`));
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
case 'get': {
|
|
501
|
+
const key = restArgs[0];
|
|
502
|
+
if (!key) { console.error('Usage: vault get KEY'); process.exit(1); }
|
|
503
|
+
const val = vault.getSecret(pw, key);
|
|
504
|
+
if (val === null) { console.error(`Not found: ${key}`); process.exit(1); }
|
|
505
|
+
console.log(val);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
case 'list': {
|
|
509
|
+
const secrets = vault.listSecrets(pw);
|
|
510
|
+
if (secrets.length === 0) {
|
|
511
|
+
console.log(dim(' Vault is empty'));
|
|
512
|
+
} else {
|
|
513
|
+
console.log(tableHeader(['Key', 'Updated'], [30, 24]));
|
|
514
|
+
for (const s of secrets) {
|
|
515
|
+
const ago = timeSince(new Date(s.updatedAt));
|
|
516
|
+
console.log(tableRow([s.key, ago], [30, 24]));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case 'delete': {
|
|
522
|
+
const key = restArgs[0];
|
|
523
|
+
if (!key) { console.error('Usage: vault delete KEY'); process.exit(1); }
|
|
524
|
+
if (vault.deleteSecret(pw, key)) {
|
|
525
|
+
console.log(success(`Deleted: ${key}`));
|
|
526
|
+
} else {
|
|
527
|
+
console.error(`Not found: ${key}`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
case 'run': {
|
|
533
|
+
const dashIdx = restArgs.indexOf('--');
|
|
534
|
+
const cmdArgs = dashIdx >= 0 ? restArgs.slice(dashIdx + 1) : restArgs;
|
|
535
|
+
if (cmdArgs.length === 0) {
|
|
536
|
+
console.error('Usage: vault run -- COMMAND [ARGS...]');
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
const exitCode = await vault.vaultRun(pw, cmdArgs[0], cmdArgs.slice(1));
|
|
540
|
+
process.exit(exitCode);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
case 'import': {
|
|
544
|
+
const envFile = restArgs[0] || '.env';
|
|
545
|
+
const count = vault.importEnvFile(pw, envFile);
|
|
546
|
+
console.log(success(`Imported ${count} secrets from ${envFile}`));
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
case 'export': {
|
|
550
|
+
const bundle = vault.exportVault(pw);
|
|
551
|
+
console.log(bundle);
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
default:
|
|
555
|
+
console.error(`Unknown vault command: ${subcommand}`);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
} catch (e) {
|
|
559
|
+
console.error(error(e.message));
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function cmdContext(args) {
|
|
565
|
+
const { compactHeader, section, labelValue, tableHeader, tableRow, success, warning, dim, bold, info, statusBadge } = await import('../lib/cli-style.mjs');
|
|
566
|
+
const { resolveContext, listContexts } = await import('../lib/context-resolver.mjs');
|
|
567
|
+
|
|
568
|
+
const subcommand = args[0];
|
|
569
|
+
const format = getOption(args, '--format', 'text');
|
|
570
|
+
const verbose = getFlag(args, '--verbose');
|
|
571
|
+
|
|
572
|
+
if (!subcommand || subcommand === 'list') {
|
|
573
|
+
const contexts = listContexts();
|
|
574
|
+
|
|
575
|
+
if (format === 'json') {
|
|
576
|
+
console.log(JSON.stringify(contexts, null, 2));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
console.log(compactHeader('contexts'));
|
|
581
|
+
console.log(tableHeader(['Context', 'Sensitivity', 'Algorithm', 'Compliance'], [20, 12, 20, 20]));
|
|
582
|
+
for (const ctx of contexts) {
|
|
583
|
+
const badge = ctx.custom ? dim('(custom)') : '';
|
|
584
|
+
console.log(tableRow(
|
|
585
|
+
[ctx.name, ctx.sensitivity, ctx.algorithm, ctx.compliance.join(', ') || '—'],
|
|
586
|
+
[20, 12, 20, 20]
|
|
587
|
+
));
|
|
588
|
+
}
|
|
589
|
+
console.log();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (subcommand === 'show') {
|
|
594
|
+
const name = args[1];
|
|
595
|
+
if (!name) { console.error('Usage: context show NAME [--verbose]'); process.exit(1); }
|
|
596
|
+
|
|
597
|
+
const resolved = resolveContext(name);
|
|
598
|
+
if (resolved.error) {
|
|
599
|
+
console.error(`${resolved.error}\nValid contexts: ${resolved.validContexts.join(', ')}`);
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (format === 'json') {
|
|
604
|
+
console.log(JSON.stringify(resolved, null, 2));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
console.log(compactHeader('context'));
|
|
609
|
+
|
|
610
|
+
// Identity
|
|
611
|
+
console.log(section(resolved.context.displayName));
|
|
612
|
+
console.log(labelValue('Context', resolved.context.name));
|
|
613
|
+
if (resolved.context.description) {
|
|
614
|
+
console.log(labelValue('Description', resolved.context.description));
|
|
615
|
+
}
|
|
616
|
+
if (resolved.context.custom) {
|
|
617
|
+
console.log(labelValue('Source', dim('custom (.cryptoserve.json)')));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Resolved algorithm
|
|
621
|
+
console.log(section('Resolved Algorithm'));
|
|
622
|
+
console.log(labelValue('Algorithm', bold(resolved.algorithm)));
|
|
623
|
+
console.log(labelValue('Key size', `${resolved.keyBits} bits`));
|
|
624
|
+
console.log(labelValue('Key rotation', `${resolved.rotationDays} days`));
|
|
625
|
+
if (resolved.quantumRisk) {
|
|
626
|
+
console.log(labelValue('Quantum risk', warning('PQC migration recommended')));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 5-layer summary
|
|
630
|
+
console.log(section('Context Layers'));
|
|
631
|
+
console.log(labelValue('1. Sensitivity', resolved.context.sensitivity.toUpperCase()));
|
|
632
|
+
|
|
633
|
+
const flags = [];
|
|
634
|
+
if (resolved.context.pii) flags.push('PII');
|
|
635
|
+
if (resolved.context.phi) flags.push('PHI');
|
|
636
|
+
if (resolved.context.pci) flags.push('PCI');
|
|
637
|
+
if (flags.length) console.log(labelValue(' Data flags', flags.join(', ')));
|
|
638
|
+
|
|
639
|
+
console.log(labelValue('2. Compliance', resolved.context.compliance.join(', ') || '—'));
|
|
640
|
+
console.log(labelValue('3. Threat model', resolved.context.adversaries.join(', ')));
|
|
641
|
+
console.log(labelValue(' Protection', `${resolved.context.protectionYears} years`));
|
|
642
|
+
console.log(labelValue('4. Access', `${resolved.context.frequency} frequency, ${resolved.context.usage}`));
|
|
643
|
+
|
|
644
|
+
// Examples
|
|
645
|
+
if (resolved.context.examples.length > 0) {
|
|
646
|
+
console.log(labelValue(' Examples', resolved.context.examples.join(', ')));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Verbose: full rationale
|
|
650
|
+
if (verbose) {
|
|
651
|
+
console.log(section('Resolution Rationale'));
|
|
652
|
+
for (const f of resolved.factors) {
|
|
653
|
+
console.log(` ${dim(f)}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (resolved.alternatives.length > 0) {
|
|
657
|
+
console.log(section('Alternatives'));
|
|
658
|
+
for (const alt of resolved.alternatives) {
|
|
659
|
+
console.log(` ${info(alt.algorithm)}`);
|
|
660
|
+
console.log(` ${dim(alt.reason)}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Usage hint
|
|
666
|
+
console.log(section('Usage'));
|
|
667
|
+
console.log(` ${dim(`cryptoserve encrypt "data" --context ${name} --password P`)}`);
|
|
668
|
+
console.log();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.error(`Unknown context command: ${subcommand}`);
|
|
673
|
+
console.error('Usage: context list | context show NAME');
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function cmdLogin(args) {
|
|
678
|
+
const { login } = await import('../lib/client.mjs');
|
|
679
|
+
const server = getOption(args, '--server', 'https://localhost:8003');
|
|
680
|
+
try {
|
|
681
|
+
await login(server);
|
|
682
|
+
console.log('Login successful.');
|
|
683
|
+
} catch (e) {
|
|
684
|
+
console.error(`Login failed: ${e.message}`);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
async function cmdStatus() {
|
|
690
|
+
const { compactHeader, section, labelValue, statusBadge, dim } = await import('../lib/cli-style.mjs');
|
|
691
|
+
const { loadToken, maskToken, parseJwtExpiry } = await import('../lib/credentials.mjs');
|
|
692
|
+
const { loadMasterKey, isKeychainAvailable } = await import('../lib/keychain.mjs');
|
|
693
|
+
const { vaultExists } = await import('../lib/vault.mjs');
|
|
694
|
+
const { getStatus } = await import('../lib/client.mjs');
|
|
695
|
+
|
|
696
|
+
console.log(compactHeader('status'));
|
|
697
|
+
|
|
698
|
+
// Key management
|
|
699
|
+
console.log(section('Key Management'));
|
|
700
|
+
const keychainOk = await isKeychainAvailable();
|
|
701
|
+
console.log(labelValue('OS keychain', keychainOk ? statusBadge('active') : statusBadge('unavailable')));
|
|
702
|
+
|
|
703
|
+
let hasKey = false;
|
|
704
|
+
try { hasKey = !!(await loadMasterKey()); } catch { /* no key */ }
|
|
705
|
+
console.log(labelValue('Master key', hasKey ? statusBadge('ready') : statusBadge('not configured')));
|
|
706
|
+
console.log(labelValue('Vault', vaultExists() ? statusBadge('ready') : statusBadge('not initialized')));
|
|
707
|
+
|
|
708
|
+
// Server connection
|
|
709
|
+
console.log(section('Server Connection'));
|
|
710
|
+
const creds = loadToken();
|
|
711
|
+
if (creds) {
|
|
712
|
+
console.log(labelValue('Server', creds.server));
|
|
713
|
+
console.log(labelValue('Token', maskToken(creds.token)));
|
|
714
|
+
const expiry = parseJwtExpiry(creds.token);
|
|
715
|
+
if (expiry) {
|
|
716
|
+
if (expiry.expired) {
|
|
717
|
+
console.log(labelValue('Expiry', statusBadge('expired')));
|
|
718
|
+
} else {
|
|
719
|
+
const mins = Math.floor(expiry.remainingMs / 60000);
|
|
720
|
+
console.log(labelValue('Expiry', `${mins} minutes remaining`));
|
|
721
|
+
}
|
|
722
|
+
if (expiry.subject) console.log(labelValue('Subject', expiry.subject));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const status = await getStatus();
|
|
726
|
+
console.log(labelValue('Connection', status.connected ? statusBadge('healthy') : statusBadge('error')));
|
|
727
|
+
if (status.latency) console.log(labelValue('Latency', `${status.latency}ms`));
|
|
728
|
+
} else {
|
|
729
|
+
console.log(labelValue('Status', dim('Not logged in')));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
console.log();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
// Utility
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
|
|
739
|
+
function timeSince(date) {
|
|
740
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
741
|
+
if (seconds < 60) return 'just now';
|
|
742
|
+
const minutes = Math.floor(seconds / 60);
|
|
743
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
744
|
+
const hours = Math.floor(minutes / 60);
|
|
745
|
+
if (hours < 24) return `${hours}h ago`;
|
|
746
|
+
const days = Math.floor(hours / 24);
|
|
747
|
+
return `${days}d ago`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ---------------------------------------------------------------------------
|
|
751
|
+
// Main router
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
|
|
754
|
+
const args = process.argv.slice(2);
|
|
755
|
+
const command = args[0];
|
|
756
|
+
const commandArgs = args.slice(1);
|
|
757
|
+
|
|
758
|
+
// Filter out the command from args for sub-parsers
|
|
759
|
+
// Strip flags that belong to the command router
|
|
760
|
+
const filteredArgs = commandArgs.filter(a => a !== command);
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
switch (command) {
|
|
764
|
+
case 'help':
|
|
765
|
+
case '--help':
|
|
766
|
+
case '-h':
|
|
767
|
+
case undefined:
|
|
768
|
+
await cmdHelp();
|
|
769
|
+
break;
|
|
770
|
+
case 'version':
|
|
771
|
+
case '--version':
|
|
772
|
+
case '-v':
|
|
773
|
+
await cmdVersion();
|
|
774
|
+
break;
|
|
775
|
+
case 'init':
|
|
776
|
+
await cmdInit(commandArgs);
|
|
777
|
+
break;
|
|
778
|
+
case 'pqc':
|
|
779
|
+
await cmdPqc(commandArgs);
|
|
780
|
+
break;
|
|
781
|
+
case 'scan':
|
|
782
|
+
await cmdScan(commandArgs);
|
|
783
|
+
break;
|
|
784
|
+
case 'encrypt':
|
|
785
|
+
await cmdEncrypt(commandArgs);
|
|
786
|
+
break;
|
|
787
|
+
case 'decrypt':
|
|
788
|
+
await cmdDecrypt(commandArgs);
|
|
789
|
+
break;
|
|
790
|
+
case 'hash-password':
|
|
791
|
+
await cmdHashPassword(commandArgs);
|
|
792
|
+
break;
|
|
793
|
+
case 'context':
|
|
794
|
+
await cmdContext(commandArgs);
|
|
795
|
+
break;
|
|
796
|
+
case 'vault':
|
|
797
|
+
await cmdVault(commandArgs);
|
|
798
|
+
break;
|
|
799
|
+
case 'login':
|
|
800
|
+
await cmdLogin(commandArgs);
|
|
801
|
+
break;
|
|
802
|
+
case 'status':
|
|
803
|
+
await cmdStatus();
|
|
804
|
+
break;
|
|
805
|
+
default:
|
|
806
|
+
console.error(`Unknown command: ${command}\nRun "cryptoserve help" for usage.`);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
} catch (e) {
|
|
810
|
+
console.error(`Error: ${e.message}`);
|
|
811
|
+
process.exit(1);
|
|
812
|
+
}
|