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.
@@ -0,0 +1,323 @@
1
+ /**
2
+ * JavaScript/TypeScript crypto dependency and secret scanner.
3
+ *
4
+ * Scans projects for:
5
+ * 1. Cryptographic dependencies (package.json, imports, algorithm strings)
6
+ * 2. Hardcoded secrets (API keys, passwords — patterns from secretless-ai)
7
+ * 3. Certificate/key files (.pem, .key, .crt, .p12)
8
+ *
9
+ * Output matches the library inventory format used by pqc-engine.mjs.
10
+ * Zero dependencies — uses only node:fs and node:path.
11
+ */
12
+
13
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
14
+ import { join, relative, extname, basename } from 'node:path';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Known crypto packages → algorithm mappings
18
+ // ---------------------------------------------------------------------------
19
+
20
+ const CRYPTO_PACKAGES = {
21
+ 'crypto-js': { algorithms: ['AES', 'DES', '3DES', 'MD5', 'SHA-256', 'SHA-512', 'HMAC'], quantumRisk: 'low', category: 'symmetric' },
22
+ 'bcrypt': { algorithms: ['bcrypt'], quantumRisk: 'none', category: 'kdf' },
23
+ 'bcryptjs': { algorithms: ['bcrypt'], quantumRisk: 'none', category: 'kdf' },
24
+ 'jsonwebtoken': { algorithms: ['RS256', 'HS256', 'ES256'], quantumRisk: 'high', category: 'token' },
25
+ 'jose': { algorithms: ['RS256', 'ES256', 'EdDSA', 'AES-GCM'], quantumRisk: 'high', category: 'token' },
26
+ 'node-forge': { algorithms: ['RSA', 'AES', 'SHA-256', 'HMAC', 'TLS'], quantumRisk: 'high', category: 'tls' },
27
+ 'tweetnacl': { algorithms: ['X25519', 'Ed25519', 'XSalsa20'], quantumRisk: 'high', category: 'asymmetric' },
28
+ 'libsodium-wrappers': { algorithms: ['X25519', 'Ed25519', 'ChaCha20', 'AES-256-GCM'], quantumRisk: 'high', category: 'asymmetric' },
29
+ '@noble/curves': { algorithms: ['ECDSA', 'Ed25519', 'X25519'], quantumRisk: 'high', category: 'asymmetric' },
30
+ '@noble/hashes': { algorithms: ['SHA-256', 'SHA-512', 'SHA3', 'Blake2'], quantumRisk: 'low', category: 'hash' },
31
+ '@noble/post-quantum': { algorithms: ['ML-KEM', 'ML-DSA', 'SLH-DSA'], quantumRisk: 'none', category: 'pqc' },
32
+ 'openpgp': { algorithms: ['RSA', 'ECDSA', 'AES', 'SHA-256'], quantumRisk: 'high', category: 'asymmetric' },
33
+ 'elliptic': { algorithms: ['ECDSA', 'ECDHE', 'Ed25519'], quantumRisk: 'high', category: 'asymmetric' },
34
+ 'secp256k1': { algorithms: ['ECDSA'], quantumRisk: 'high', category: 'asymmetric' },
35
+ 'argon2': { algorithms: ['Argon2'], quantumRisk: 'none', category: 'kdf' },
36
+ 'scrypt': { algorithms: ['scrypt'], quantumRisk: 'none', category: 'kdf' },
37
+ 'pbkdf2': { algorithms: ['PBKDF2'], quantumRisk: 'none', category: 'kdf' },
38
+ 'tls': { algorithms: ['TLS', 'RSA', 'ECDSA'], quantumRisk: 'high', category: 'tls' },
39
+ 'ssh2': { algorithms: ['RSA', 'Ed25519', 'ECDSA', 'AES'], quantumRisk: 'high', category: 'asymmetric' },
40
+ 'node-rsa': { algorithms: ['RSA'], quantumRisk: 'high', category: 'asymmetric' },
41
+ };
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Import/require patterns to detect in source code
45
+ // ---------------------------------------------------------------------------
46
+
47
+ const IMPORT_PATTERNS = [
48
+ { pattern: /(?:require|from)\s*[('"`]node:crypto[)'"`]/g, lib: 'node:crypto' },
49
+ { pattern: /(?:require|from)\s*[('"`]crypto[)'"`]/g, lib: 'node:crypto' },
50
+ { pattern: /createCipheriv\s*\(/g, lib: 'node:crypto', detail: 'cipher' },
51
+ { pattern: /createDecipheriv\s*\(/g, lib: 'node:crypto', detail: 'cipher' },
52
+ { pattern: /createSign\s*\(/g, lib: 'node:crypto', detail: 'signature' },
53
+ { pattern: /createVerify\s*\(/g, lib: 'node:crypto', detail: 'signature' },
54
+ { pattern: /generateKeyPair(?:Sync)?\s*\(/g, lib: 'node:crypto', detail: 'keygen' },
55
+ { pattern: /scrypt(?:Sync)?\s*\(/g, lib: 'node:crypto', detail: 'kdf' },
56
+ { pattern: /pbkdf2(?:Sync)?\s*\(/g, lib: 'node:crypto', detail: 'kdf' },
57
+ { pattern: /createCipher\s*\(/g, lib: 'node:crypto', detail: 'DEPRECATED-no-iv' },
58
+ { pattern: /CryptoJS\./g, lib: 'crypto-js' },
59
+ { pattern: /forge\.\w+/g, lib: 'node-forge' },
60
+ { pattern: /nacl\.\w+/g, lib: 'tweetnacl' },
61
+ { pattern: /jwt\.(?:sign|verify|decode)\s*\(/g, lib: 'jsonwebtoken' },
62
+ ];
63
+
64
+ // Algorithm string literals to detect
65
+ const ALGO_LITERALS = [
66
+ { pattern: /['"`]aes-(?:256|128|192)-(?:gcm|cbc|ctr|ecb)['"`]/gi, algo: 'AES' },
67
+ { pattern: /['"`]chacha20-poly1305['"`]/gi, algo: 'ChaCha20' },
68
+ { pattern: /['"`]rsa-sha(?:256|384|512)['"`]/gi, algo: 'RSA' },
69
+ { pattern: /['"`]sha(?:256|384|512|1)['"`]/gi, algo: 'SHA-256' },
70
+ { pattern: /['"`](?:HS|RS|ES|PS)(?:256|384|512)['"`]/gi, algo: 'RS256' },
71
+ { pattern: /['"`]ed25519['"`]/gi, algo: 'Ed25519' },
72
+ { pattern: /minVersion:\s*['"`]TLSv1\.[0-3]['"`]/g, algo: 'TLS' },
73
+ { pattern: /['"`](?:md5|MD5)['"`]/g, algo: 'MD5' },
74
+ { pattern: /['"`](?:des|DES|3des|3DES|des-ede3)['"`]/gi, algo: 'DES' },
75
+ { pattern: /['"`](?:rc4|RC4)['"`]/gi, algo: 'RC4' },
76
+ ];
77
+
78
+ // Deprecated/weak patterns
79
+ const WEAK_PATTERNS = [
80
+ { pattern: /createCipher\s*\(\s*['"`]/g, issue: 'createCipher without IV (use createCipheriv)', severity: 'critical' },
81
+ { pattern: /['"`](?:md5|MD5)['"`]/g, issue: 'MD5 is cryptographically broken', severity: 'high' },
82
+ { pattern: /['"`](?:des|DES)['"`]/g, issue: 'DES has 56-bit keys (use AES)', severity: 'critical' },
83
+ { pattern: /['"`](?:rc4|RC4)['"`]/g, issue: 'RC4 is broken (use AES-GCM or ChaCha20)', severity: 'critical' },
84
+ { pattern: /['"`]aes-\d+-ecb['"`]/gi, issue: 'ECB mode leaks patterns (use GCM or CTR)', severity: 'high' },
85
+ { pattern: /['"`]aes-\d+-cbc['"`]/gi, issue: 'CBC mode is vulnerable to padding oracles (use GCM)', severity: 'medium' },
86
+ ];
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Hardcoded secret detection (borrowed from secretless-ai patterns)
90
+ // ---------------------------------------------------------------------------
91
+
92
+ const SECRET_PATTERNS = [
93
+ { id: 'anthropic', regex: /sk-ant-api\d{2}-[a-zA-Z0-9_-]{20,}/g, name: 'Anthropic API Key', envVar: 'ANTHROPIC_API_KEY' },
94
+ { id: 'openai-proj', regex: /sk-proj-[a-zA-Z0-9]{20,}/g, name: 'OpenAI Project Key', envVar: 'OPENAI_API_KEY' },
95
+ { id: 'openai-legacy', regex: /sk-[a-zA-Z0-9]{48,}/g, name: 'OpenAI Legacy Key', envVar: 'OPENAI_API_KEY' },
96
+ { id: 'aws-access', regex: /AKIA[0-9A-Z]{16}/g, name: 'AWS Access Key', envVar: 'AWS_ACCESS_KEY_ID' },
97
+ { id: 'github-pat', regex: /ghp_[a-zA-Z0-9]{36}/g, name: 'GitHub PAT', envVar: 'GITHUB_TOKEN' },
98
+ { id: 'github-fine', regex: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g, name: 'GitHub Fine-grained', envVar: 'GITHUB_TOKEN' },
99
+ { id: 'slack', regex: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24}/g, name: 'Slack Token', envVar: 'SLACK_TOKEN' },
100
+ { id: 'google', regex: /AIza[0-9A-Za-z_-]{35}/g, name: 'Google API Key', envVar: 'GOOGLE_API_KEY' },
101
+ { id: 'stripe', regex: /sk_live_[0-9a-zA-Z]{24,}/g, name: 'Stripe Secret Key', envVar: 'STRIPE_SECRET_KEY' },
102
+ { id: 'sendgrid', regex: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g, name: 'SendGrid Key', envVar: 'SENDGRID_API_KEY' },
103
+ { id: 'npm', regex: /npm_[a-zA-Z0-9]{36}/g, name: 'npm Token', envVar: 'NPM_TOKEN' },
104
+ { id: 'private-key', regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, name: 'Private Key', envVar: null },
105
+ ];
106
+
107
+ // File patterns for cert/key discovery
108
+ const CERT_EXTENSIONS = new Set(['.pem', '.key', '.crt', '.p12', '.pfx', '.jks', '.keystore']);
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // File walker
112
+ // ---------------------------------------------------------------------------
113
+
114
+ const SKIP_DIRS = new Set([
115
+ 'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
116
+ '.cache', '.nuxt', '.output', '.svelte-kit', '__pycache__',
117
+ 'vendor', '.venv', 'venv',
118
+ ]);
119
+
120
+ const SOURCE_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']);
121
+
122
+ function walkFiles(dir, maxFiles = 10000, maxBytes = 500 * 1024 * 1024) {
123
+ const files = [];
124
+ let totalBytes = 0;
125
+
126
+ function walk(currentDir) {
127
+ if (files.length >= maxFiles || totalBytes >= maxBytes) return;
128
+
129
+ let entries;
130
+ try { entries = readdirSync(currentDir, { withFileTypes: true }); }
131
+ catch { return; }
132
+
133
+ for (const entry of entries) {
134
+ if (files.length >= maxFiles || totalBytes >= maxBytes) return;
135
+
136
+ if (entry.isDirectory()) {
137
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
138
+ walk(join(currentDir, entry.name));
139
+ }
140
+ continue;
141
+ }
142
+
143
+ if (!entry.isFile()) continue;
144
+
145
+ const filePath = join(currentDir, entry.name);
146
+ try {
147
+ const stat = statSync(filePath);
148
+ if (stat.size > 1024 * 1024) continue; // Skip files >1MB
149
+ totalBytes += stat.size;
150
+ files.push(filePath);
151
+ } catch { continue; }
152
+ }
153
+ }
154
+
155
+ walk(dir);
156
+ return files;
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Scanner
161
+ // ---------------------------------------------------------------------------
162
+
163
+ export function scanProject(projectDir) {
164
+ const results = {
165
+ libraries: [],
166
+ secrets: [],
167
+ weakPatterns: [],
168
+ certFiles: [],
169
+ filesScanned: 0,
170
+ };
171
+
172
+ // 1. Scan package.json for crypto dependencies
173
+ const pkgPath = join(projectDir, 'package.json');
174
+ if (existsSync(pkgPath)) {
175
+ try {
176
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
177
+ const allDeps = {
178
+ ...(pkg.dependencies || {}),
179
+ ...(pkg.devDependencies || {}),
180
+ };
181
+
182
+ for (const [name, version] of Object.entries(allDeps)) {
183
+ if (name in CRYPTO_PACKAGES) {
184
+ const info = CRYPTO_PACKAGES[name];
185
+ results.libraries.push({
186
+ name,
187
+ version: version.replace(/^[\^~]/, ''),
188
+ algorithms: info.algorithms,
189
+ quantumRisk: info.quantumRisk,
190
+ category: info.category,
191
+ source: 'package.json',
192
+ });
193
+ }
194
+ }
195
+ } catch { /* invalid package.json */ }
196
+ }
197
+
198
+ // 2. Walk source files
199
+ const files = walkFiles(projectDir);
200
+ const seenImports = new Set();
201
+ const seenAlgos = new Set();
202
+
203
+ for (const filePath of files) {
204
+ const ext = extname(filePath);
205
+
206
+ // Check for cert/key files
207
+ if (CERT_EXTENSIONS.has(ext)) {
208
+ results.certFiles.push(relative(projectDir, filePath));
209
+ continue;
210
+ }
211
+
212
+ // Only scan source files for code patterns
213
+ if (!SOURCE_EXTENSIONS.has(ext)) continue;
214
+
215
+ results.filesScanned++;
216
+
217
+ let content;
218
+ try { content = readFileSync(filePath, 'utf-8'); }
219
+ catch { continue; }
220
+
221
+ const relPath = relative(projectDir, filePath);
222
+
223
+ // Detect imports/requires
224
+ for (const { pattern, lib, detail } of IMPORT_PATTERNS) {
225
+ pattern.lastIndex = 0;
226
+ if (pattern.test(content)) {
227
+ const key = `${lib}:${detail || ''}`;
228
+ if (!seenImports.has(key)) {
229
+ seenImports.add(key);
230
+ if (detail === 'DEPRECATED-no-iv') {
231
+ results.weakPatterns.push({
232
+ file: relPath,
233
+ issue: 'createCipher without IV (use createCipheriv)',
234
+ severity: 'critical',
235
+ });
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ // Detect algorithm string literals
242
+ for (const { pattern, algo } of ALGO_LITERALS) {
243
+ pattern.lastIndex = 0;
244
+ if (pattern.test(content) && !seenAlgos.has(algo)) {
245
+ seenAlgos.add(algo);
246
+ }
247
+ }
248
+
249
+ // Detect weak/deprecated patterns
250
+ for (const { pattern, issue, severity } of WEAK_PATTERNS) {
251
+ pattern.lastIndex = 0;
252
+ if (pattern.test(content)) {
253
+ results.weakPatterns.push({ file: relPath, issue, severity });
254
+ }
255
+ }
256
+
257
+ // Detect hardcoded secrets
258
+ for (const line of content.split('\n')) {
259
+ if (line.length > 4096) continue; // ReDoS protection
260
+ // Skip env var references
261
+ if (/\$\{[A-Z_]+\}/.test(line) || /process\.env\.[A-Z_]+/.test(line)) continue;
262
+
263
+ for (const { id, regex, name, envVar } of SECRET_PATTERNS) {
264
+ regex.lastIndex = 0;
265
+ if (regex.test(line)) {
266
+ results.secrets.push({
267
+ type: id,
268
+ name,
269
+ file: relPath,
270
+ envVar,
271
+ severity: 'critical',
272
+ });
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ // Add node:crypto as a library if imports were detected
279
+ if (seenImports.size > 0 || seenAlgos.size > 0) {
280
+ const nodeCryptoAlgos = [...seenAlgos];
281
+ if (seenImports.has('node:crypto:') || seenImports.has('node:crypto:cipher')) {
282
+ if (!nodeCryptoAlgos.includes('AES')) nodeCryptoAlgos.push('AES');
283
+ }
284
+ if (seenImports.has('node:crypto:signature')) {
285
+ if (!nodeCryptoAlgos.includes('RSA')) nodeCryptoAlgos.push('RSA');
286
+ }
287
+ if (seenImports.has('node:crypto:kdf')) {
288
+ nodeCryptoAlgos.push('scrypt');
289
+ }
290
+
291
+ if (nodeCryptoAlgos.length > 0) {
292
+ // Determine quantum risk based on detected algorithms
293
+ const hasAsymmetric = nodeCryptoAlgos.some(a =>
294
+ ['RSA', 'ECDSA', 'Ed25519', 'RS256', 'ES256', 'DH'].includes(a)
295
+ );
296
+ results.libraries.push({
297
+ name: 'node:crypto',
298
+ version: 'builtin',
299
+ algorithms: nodeCryptoAlgos,
300
+ quantumRisk: hasAsymmetric ? 'high' : 'low',
301
+ category: hasAsymmetric ? 'asymmetric' : 'symmetric',
302
+ source: 'source-code',
303
+ });
304
+ }
305
+ }
306
+
307
+ return results;
308
+ }
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // Format results as library inventory (for PQC engine input)
312
+ // ---------------------------------------------------------------------------
313
+
314
+ export function toLibraryInventory(scanResults) {
315
+ return scanResults.libraries.map(lib => ({
316
+ name: lib.name,
317
+ version: lib.version,
318
+ algorithms: lib.algorithms,
319
+ quantumRisk: lib.quantumRisk,
320
+ category: lib.category,
321
+ isDeprecated: false,
322
+ }));
323
+ }
package/lib/vault.mjs ADDED
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Encrypted local vault for secret management.
3
+ *
4
+ * Stores secrets in an AES-256-GCM encrypted JSON file at
5
+ * ~/.cryptoserve/vault.enc. Master password is used to derive the
6
+ * encryption key via scrypt.
7
+ *
8
+ * Zero dependencies — uses only node:crypto and node:fs.
9
+ *
10
+ * Commands:
11
+ * vault init — Create a new vault
12
+ * vault set K V — Store a secret
13
+ * vault get K — Retrieve a secret
14
+ * vault list — List secret names (not values)
15
+ * vault delete K — Remove a secret
16
+ * vault run -- CMD — Inject secrets as env vars, run command
17
+ * vault import F — Import .env file into vault
18
+ * vault export — Export as encrypted bundle
19
+ */
20
+
21
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { homedir } from 'node:os';
24
+ import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from 'node:crypto';
25
+ import { spawn } from 'node:child_process';
26
+
27
+ const CONFIG_DIR = join(homedir(), '.cryptoserve');
28
+ const VAULT_PATH = join(CONFIG_DIR, 'vault.enc');
29
+ const SALT_SIZE = 32;
30
+ const IV_SIZE = 12;
31
+ const TAG_SIZE = 16;
32
+ const SCRYPT_OPTS = { N: 2 ** 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Internal helpers
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function ensureConfigDir() {
39
+ if (!existsSync(CONFIG_DIR)) {
40
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
41
+ }
42
+ }
43
+
44
+ function deriveKey(password, salt) {
45
+ return scryptSync(password, salt, 32, SCRYPT_OPTS);
46
+ }
47
+
48
+ function encryptData(plaintext, password) {
49
+ const salt = randomBytes(SALT_SIZE);
50
+ const key = deriveKey(password, salt);
51
+ const iv = randomBytes(IV_SIZE);
52
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
53
+ const enc = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
54
+ const tag = cipher.getAuthTag();
55
+ return Buffer.concat([salt, iv, tag, enc]);
56
+ }
57
+
58
+ function decryptData(packed, password) {
59
+ if (packed.length < SALT_SIZE + IV_SIZE + TAG_SIZE + 1) {
60
+ throw new Error('Vault file is corrupted or empty');
61
+ }
62
+ const salt = packed.subarray(0, SALT_SIZE);
63
+ const iv = packed.subarray(SALT_SIZE, SALT_SIZE + IV_SIZE);
64
+ const tag = packed.subarray(SALT_SIZE + IV_SIZE, SALT_SIZE + IV_SIZE + TAG_SIZE);
65
+ const enc = packed.subarray(SALT_SIZE + IV_SIZE + TAG_SIZE);
66
+ const key = deriveKey(password, salt);
67
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
68
+ decipher.setAuthTag(tag);
69
+ return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf-8');
70
+ }
71
+
72
+ function loadVault(password, path = VAULT_PATH) {
73
+ if (!existsSync(path)) return null;
74
+ const packed = readFileSync(path);
75
+ const json = decryptData(packed, password);
76
+ return JSON.parse(json);
77
+ }
78
+
79
+ function saveVault(data, password, path = VAULT_PATH) {
80
+ ensureConfigDir();
81
+ const json = JSON.stringify(data, null, 2);
82
+ const encrypted = encryptData(json, password);
83
+ writeFileSync(path, encrypted, { mode: 0o600 });
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Public API
88
+ // ---------------------------------------------------------------------------
89
+
90
+ export function vaultExists(path = VAULT_PATH) {
91
+ return existsSync(path);
92
+ }
93
+
94
+ export function initVault(password, path = VAULT_PATH) {
95
+ if (existsSync(path)) {
96
+ throw new Error('Vault already exists. Use "vault reset" to recreate.');
97
+ }
98
+ const data = {
99
+ version: 1,
100
+ createdAt: new Date().toISOString(),
101
+ secrets: {},
102
+ };
103
+ saveVault(data, password, path);
104
+ return data;
105
+ }
106
+
107
+ export function setSecret(password, key, value, path = VAULT_PATH) {
108
+ const data = loadVault(password, path);
109
+ if (!data) throw new Error('Vault not found. Run "cryptoserve vault init" first.');
110
+ data.secrets[key] = {
111
+ value,
112
+ updatedAt: new Date().toISOString(),
113
+ };
114
+ saveVault(data, password, path);
115
+ }
116
+
117
+ export function getSecret(password, key, path = VAULT_PATH) {
118
+ const data = loadVault(password, path);
119
+ if (!data) throw new Error('Vault not found. Run "cryptoserve vault init" first.');
120
+ const entry = data.secrets[key];
121
+ if (!entry) return null;
122
+ return entry.value;
123
+ }
124
+
125
+ export function listSecrets(password, path = VAULT_PATH) {
126
+ const data = loadVault(password, path);
127
+ if (!data) throw new Error('Vault not found. Run "cryptoserve vault init" first.');
128
+ return Object.entries(data.secrets).map(([key, entry]) => ({
129
+ key,
130
+ updatedAt: entry.updatedAt,
131
+ }));
132
+ }
133
+
134
+ export function deleteSecret(password, key, path = VAULT_PATH) {
135
+ const data = loadVault(password, path);
136
+ if (!data) throw new Error('Vault not found. Run "cryptoserve vault init" first.');
137
+ if (!(key in data.secrets)) return false;
138
+ delete data.secrets[key];
139
+ saveVault(data, password, path);
140
+ return true;
141
+ }
142
+
143
+ export function resetVault(path = VAULT_PATH) {
144
+ if (existsSync(path)) unlinkSync(path);
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // vault run — inject secrets as env vars into a child process
149
+ // ---------------------------------------------------------------------------
150
+
151
+ export function vaultRun(password, command, args = [], path = VAULT_PATH) {
152
+ const data = loadVault(password, path);
153
+ if (!data) throw new Error('Vault not found. Run "cryptoserve vault init" first.');
154
+
155
+ const secretEnv = {};
156
+ for (const [key, entry] of Object.entries(data.secrets)) {
157
+ secretEnv[key] = entry.value;
158
+ }
159
+
160
+ return new Promise((resolve, reject) => {
161
+ const child = spawn(command, args, {
162
+ stdio: 'inherit',
163
+ env: { ...process.env, ...secretEnv },
164
+ });
165
+
166
+ child.on('close', code => resolve(code));
167
+ child.on('error', reject);
168
+ });
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // vault import — read .env file, store each key in vault
173
+ // ---------------------------------------------------------------------------
174
+
175
+ export function importEnvFile(password, envPath, path = VAULT_PATH) {
176
+ if (!existsSync(envPath)) throw new Error(`File not found: ${envPath}`);
177
+
178
+ const content = readFileSync(envPath, 'utf-8');
179
+ const lines = content.split('\n');
180
+ let imported = 0;
181
+
182
+ for (const line of lines) {
183
+ const trimmed = line.trim();
184
+ if (!trimmed || trimmed.startsWith('#')) continue;
185
+
186
+ // Parse KEY=VALUE (supports optional quotes)
187
+ const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
188
+ if (!match) continue;
189
+
190
+ const [, key, rawValue] = match;
191
+ // Strip surrounding quotes
192
+ let value = rawValue;
193
+ if ((value.startsWith('"') && value.endsWith('"')) ||
194
+ (value.startsWith("'") && value.endsWith("'"))) {
195
+ value = value.slice(1, -1);
196
+ }
197
+
198
+ setSecret(password, key, value, path);
199
+ imported++;
200
+ }
201
+
202
+ return imported;
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // vault export — create encrypted bundle
207
+ // ---------------------------------------------------------------------------
208
+
209
+ export function exportVault(password, exportPassword = null, path = VAULT_PATH) {
210
+ const data = loadVault(password, path);
211
+ if (!data) throw new Error('Vault not found.');
212
+
213
+ const json = JSON.stringify(data.secrets);
214
+ const pw = exportPassword || password;
215
+ return encryptData(json, pw).toString('base64');
216
+ }
217
+
218
+ export function importVaultBundle(password, bundle, importPassword = null, path = VAULT_PATH) {
219
+ const packed = Buffer.from(bundle, 'base64');
220
+ const pw = importPassword || password;
221
+ const json = decryptData(packed, pw);
222
+ const secrets = JSON.parse(json);
223
+
224
+ const data = loadVault(password, path) || {
225
+ version: 1,
226
+ createdAt: new Date().toISOString(),
227
+ secrets: {},
228
+ };
229
+
230
+ for (const [key, entry] of Object.entries(secrets)) {
231
+ data.secrets[key] = typeof entry === 'object' ? entry : {
232
+ value: entry,
233
+ updatedAt: new Date().toISOString(),
234
+ };
235
+ }
236
+
237
+ saveVault(data, password, path);
238
+ return Object.keys(secrets).length;
239
+ }
240
+
241
+ // Re-export for testing
242
+ export { encryptData as _encryptData, decryptData as _decryptData };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "cryptoserve",
3
+ "version": "0.1.0",
4
+ "description": "CryptoServe CLI - Cryptographic scanning, PQC analysis, encryption, and local key management",
5
+ "type": "module",
6
+ "bin": {
7
+ "cryptoserve": "./bin/cryptoserve.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "license": "Apache-2.0",
19
+ "keywords": [
20
+ "cryptography",
21
+ "pqc",
22
+ "post-quantum",
23
+ "encryption",
24
+ "security",
25
+ "scanner",
26
+ "keychain",
27
+ "vault"
28
+ ],
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/ecolibria/cryptoserve"
32
+ },
33
+ "scripts": {
34
+ "test": "node --test test/*.test.mjs"
35
+ }
36
+ }