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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Professional CLI styling for CryptoServe.
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent, enterprise-grade terminal output styling.
|
|
5
|
+
* Port of sdk/python/cryptoserve/_cli_style.py — zero dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { env, stdout } from 'node:process';
|
|
9
|
+
|
|
10
|
+
function supportsColor() {
|
|
11
|
+
if (env.NO_COLOR) return false;
|
|
12
|
+
if (env.FORCE_COLOR) return true;
|
|
13
|
+
if (!stdout.isTTY) return false;
|
|
14
|
+
if (process.platform === 'win32') {
|
|
15
|
+
return env.TERM === 'xterm' || !!env.ANSICON;
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ENABLED = supportsColor();
|
|
21
|
+
const e = (code) => ENABLED ? code : '';
|
|
22
|
+
|
|
23
|
+
export const Colors = {
|
|
24
|
+
RESET: e('\x1b[0m'),
|
|
25
|
+
BLACK: e('\x1b[30m'),
|
|
26
|
+
RED: e('\x1b[31m'),
|
|
27
|
+
GREEN: e('\x1b[32m'),
|
|
28
|
+
YELLOW: e('\x1b[33m'),
|
|
29
|
+
BLUE: e('\x1b[34m'),
|
|
30
|
+
MAGENTA: e('\x1b[35m'),
|
|
31
|
+
CYAN: e('\x1b[36m'),
|
|
32
|
+
WHITE: e('\x1b[37m'),
|
|
33
|
+
BRIGHT_BLACK: e('\x1b[90m'),
|
|
34
|
+
BRIGHT_RED: e('\x1b[91m'),
|
|
35
|
+
BRIGHT_GREEN: e('\x1b[92m'),
|
|
36
|
+
BRIGHT_YELLOW: e('\x1b[93m'),
|
|
37
|
+
BRIGHT_BLUE: e('\x1b[94m'),
|
|
38
|
+
BRIGHT_MAGENTA: e('\x1b[95m'),
|
|
39
|
+
BRIGHT_CYAN: e('\x1b[96m'),
|
|
40
|
+
BRIGHT_WHITE: e('\x1b[97m'),
|
|
41
|
+
BOLD: e('\x1b[1m'),
|
|
42
|
+
DIM: e('\x1b[2m'),
|
|
43
|
+
ITALIC: e('\x1b[3m'),
|
|
44
|
+
UNDERLINE: e('\x1b[4m'),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Style = {
|
|
48
|
+
SUCCESS: Colors.BRIGHT_GREEN,
|
|
49
|
+
ERROR: Colors.BRIGHT_RED,
|
|
50
|
+
WARNING: Colors.BRIGHT_YELLOW,
|
|
51
|
+
INFO: Colors.BRIGHT_BLUE,
|
|
52
|
+
HEADER: Colors.BRIGHT_CYAN + Colors.BOLD,
|
|
53
|
+
SUBHEADER: Colors.CYAN,
|
|
54
|
+
LABEL: Colors.BRIGHT_WHITE + Colors.BOLD,
|
|
55
|
+
VALUE: Colors.WHITE,
|
|
56
|
+
DIM: Colors.BRIGHT_BLACK,
|
|
57
|
+
ACCENT: Colors.BRIGHT_MAGENTA,
|
|
58
|
+
RESET: Colors.RESET,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const Box = {
|
|
62
|
+
TOP_LEFT: '╭',
|
|
63
|
+
TOP_RIGHT: '╮',
|
|
64
|
+
BOTTOM_LEFT: '╰',
|
|
65
|
+
BOTTOM_RIGHT: '╯',
|
|
66
|
+
HORIZONTAL: '─',
|
|
67
|
+
VERTICAL: '│',
|
|
68
|
+
T_DOWN: '┬',
|
|
69
|
+
T_UP: '┴',
|
|
70
|
+
T_RIGHT: '├',
|
|
71
|
+
T_LEFT: '┤',
|
|
72
|
+
CROSS: '┼',
|
|
73
|
+
DOUBLE_HORIZONTAL: '═',
|
|
74
|
+
DOUBLE_VERTICAL: '║',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Icons = {
|
|
78
|
+
SUCCESS: '+',
|
|
79
|
+
ERROR: 'x',
|
|
80
|
+
WARNING: '!',
|
|
81
|
+
INFO: '*',
|
|
82
|
+
PENDING: 'o',
|
|
83
|
+
IN_PROGRESS: '-',
|
|
84
|
+
ARROW_RIGHT: '>',
|
|
85
|
+
ARROW_LEFT: '<',
|
|
86
|
+
BULLET: '-',
|
|
87
|
+
STAR: '*',
|
|
88
|
+
LOCK: '[locked]',
|
|
89
|
+
UNLOCK: '[unlocked]',
|
|
90
|
+
KEY: '[key]',
|
|
91
|
+
SHIELD: '[secure]',
|
|
92
|
+
CHECK: '[x]',
|
|
93
|
+
ROCKET: '[deploy]',
|
|
94
|
+
CLOCK: '[time]',
|
|
95
|
+
LINK: '[link]',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function header(text, width = 60) {
|
|
99
|
+
const pad = width - 4;
|
|
100
|
+
const centered = text.length >= pad
|
|
101
|
+
? text.slice(0, pad)
|
|
102
|
+
: ' '.repeat(Math.floor((pad - text.length) / 2)) + text +
|
|
103
|
+
' '.repeat(Math.ceil((pad - text.length) / 2));
|
|
104
|
+
return [
|
|
105
|
+
`${Style.HEADER}${Box.TOP_LEFT}${Box.HORIZONTAL.repeat(width - 2)}${Box.TOP_RIGHT}${Style.RESET}`,
|
|
106
|
+
`${Style.HEADER}${Box.VERTICAL}${Style.RESET} ${centered} ${Style.HEADER}${Box.VERTICAL}${Style.RESET}`,
|
|
107
|
+
`${Style.HEADER}${Box.BOTTOM_LEFT}${Box.HORIZONTAL.repeat(width - 2)}${Box.BOTTOM_RIGHT}${Style.RESET}`,
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function compactHeader(command = '') {
|
|
112
|
+
if (command) {
|
|
113
|
+
return `\n${Style.HEADER}CRYPTOSERVE${Style.RESET} ${Style.DIM}›${Style.RESET} ${Style.LABEL}${command}${Style.RESET}\n`;
|
|
114
|
+
}
|
|
115
|
+
return `\n${Style.HEADER}CRYPTOSERVE${Style.RESET}\n`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function subheader(text, width = 60) {
|
|
119
|
+
const line = Box.HORIZONTAL.repeat(width);
|
|
120
|
+
return `\n${Style.SUBHEADER}${line}\n ${text}\n${line}${Style.RESET}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function section(title) {
|
|
124
|
+
return `\n${Style.LABEL}${title}${Style.RESET}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function divider(width = 60, char = '─') {
|
|
128
|
+
return `${Style.DIM}${char.repeat(width)}${Style.RESET}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function success(text) {
|
|
132
|
+
return `${Style.SUCCESS}${Icons.SUCCESS}${Style.RESET} ${text}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function error(text) {
|
|
136
|
+
return `${Style.ERROR}${Icons.ERROR}${Style.RESET} ${text}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function warning(text) {
|
|
140
|
+
return `${Style.WARNING}${Icons.WARNING}${Style.RESET} ${text}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function info(text) {
|
|
144
|
+
return `${Style.INFO}${Icons.INFO}${Style.RESET} ${text}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function dim(text) {
|
|
148
|
+
return `${Style.DIM}${text}${Style.RESET}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function bold(text) {
|
|
152
|
+
return `${Colors.BOLD}${text}${Style.RESET}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function labelValue(label, value, labelWidth = 20) {
|
|
156
|
+
return ` ${Style.LABEL}${label.padEnd(labelWidth)}${Style.RESET} ${Style.VALUE}${value}${Style.RESET}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function tableRow(columns, widths) {
|
|
160
|
+
const parts = columns.map((col, i) =>
|
|
161
|
+
String(col).padEnd(widths[i]).slice(0, widths[i])
|
|
162
|
+
);
|
|
163
|
+
return ` ${parts.join(' ')}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function tableHeader(columns, widths) {
|
|
167
|
+
const headerLine = tableRow(columns, widths);
|
|
168
|
+
const underline = ' ' + widths.map(w => Box.HORIZONTAL.repeat(w)).join(' ');
|
|
169
|
+
return `${Style.LABEL}${headerLine}${Style.RESET}\n${Style.DIM}${underline}${Style.RESET}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function progressBar(current, total, width = 30, showPercent = true) {
|
|
173
|
+
const percent = total === 0 ? 100 : Math.floor((current / total) * 100);
|
|
174
|
+
const filled = Math.floor((current / Math.max(total, 1)) * width);
|
|
175
|
+
const empty = width - filled;
|
|
176
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
177
|
+
const color = percent >= 80 ? Style.SUCCESS : percent >= 50 ? Style.WARNING : Style.ERROR;
|
|
178
|
+
return showPercent
|
|
179
|
+
? `${color}${bar}${Style.RESET} ${percent}%`
|
|
180
|
+
: `${color}${bar}${Style.RESET}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function statusBadge(status) {
|
|
184
|
+
const lower = status.toLowerCase();
|
|
185
|
+
if (['ready', 'active', 'success', 'healthy', 'ok'].includes(lower)) {
|
|
186
|
+
return `${Style.SUCCESS}● ${status}${Style.RESET}`;
|
|
187
|
+
}
|
|
188
|
+
if (['pending', 'waiting', 'in_progress'].includes(lower)) {
|
|
189
|
+
return `${Style.WARNING}○ ${status}${Style.RESET}`;
|
|
190
|
+
}
|
|
191
|
+
if (['error', 'failed', 'blocked'].includes(lower)) {
|
|
192
|
+
return `${Style.ERROR}● ${status}${Style.RESET}`;
|
|
193
|
+
}
|
|
194
|
+
return `${Style.DIM}○ ${status}${Style.RESET}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function codeBlock(code) {
|
|
198
|
+
const lines = code.trim().split('\n');
|
|
199
|
+
const formatted = [];
|
|
200
|
+
formatted.push(`${Style.DIM}┌${'─'.repeat(58)}┐${Style.RESET}`);
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
formatted.push(`${Style.DIM}│${Style.RESET} ${Style.ACCENT}${line.padEnd(56)}${Style.RESET} ${Style.DIM}│${Style.RESET}`);
|
|
203
|
+
}
|
|
204
|
+
formatted.push(`${Style.DIM}└${'─'.repeat(58)}┘${Style.RESET}`);
|
|
205
|
+
return formatted.join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function brandHeader() {
|
|
209
|
+
return `
|
|
210
|
+
${Style.HEADER}╭────────────────────────────────────────────────────────╮
|
|
211
|
+
│ │
|
|
212
|
+
│ ${Colors.BRIGHT_WHITE}CRYPTOSERVE${Style.HEADER} │
|
|
213
|
+
│ ${Style.DIM}Enterprise Cryptography Platform${Style.HEADER} │
|
|
214
|
+
│ │
|
|
215
|
+
╰────────────────────────────────────────────────────────╯${Style.RESET}
|
|
216
|
+
`;
|
|
217
|
+
}
|
package/lib/client.mjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for CryptoServe server API.
|
|
3
|
+
*
|
|
4
|
+
* Uses global fetch (Node 18+) — zero dependencies.
|
|
5
|
+
* Used only when connected to a server; all scanning/PQC/crypto work offline.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { loadToken, saveToken } from './credentials.mjs';
|
|
9
|
+
import { createServer } from 'node:http';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SERVER = 'https://localhost:8003';
|
|
12
|
+
const CALLBACK_PORT = 9876;
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// URL validation (SSRF protection)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const BLOCKED_HOSTS = [
|
|
19
|
+
'169.254.169.254', // AWS metadata
|
|
20
|
+
'metadata.google.internal', // GCP metadata
|
|
21
|
+
'100.100.100.200', // Alibaba metadata
|
|
22
|
+
'fd00::', // IPv6 link-local
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function validateServerUrl(url) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
29
|
+
throw new Error('Server URL must use http or https');
|
|
30
|
+
}
|
|
31
|
+
if (BLOCKED_HOSTS.some(h => parsed.hostname === h || parsed.hostname.includes(h))) {
|
|
32
|
+
throw new Error('Server URL points to a blocked address');
|
|
33
|
+
}
|
|
34
|
+
return parsed.toString().replace(/\/$/, '');
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if (e.message.includes('blocked') || e.message.includes('must use')) throw e;
|
|
37
|
+
throw new Error(`Invalid server URL: ${url}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// API client
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
export async function apiCall(method, path, body = null, options = {}) {
|
|
46
|
+
const creds = loadToken();
|
|
47
|
+
if (!creds) throw new Error('Not logged in. Run "cryptoserve login" first.');
|
|
48
|
+
|
|
49
|
+
const server = validateServerUrl(options.server || creds.server || DEFAULT_SERVER);
|
|
50
|
+
const url = `${server}${path}`;
|
|
51
|
+
|
|
52
|
+
const headers = {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
'Cookie': `session=${creds.token}`,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const fetchOpts = {
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
signal: AbortSignal.timeout(options.timeout || 30000),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (body) fetchOpts.body = JSON.stringify(body);
|
|
64
|
+
|
|
65
|
+
const response = await fetch(url, fetchOpts);
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
if (response.status === 401) {
|
|
69
|
+
throw new Error('Session expired. Run "cryptoserve login" again.');
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`API error ${response.status}: ${await response.text()}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return response.json();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function getStatus(server = null) {
|
|
78
|
+
const creds = loadToken();
|
|
79
|
+
const url = validateServerUrl(server || creds?.server || DEFAULT_SERVER);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const start = Date.now();
|
|
83
|
+
const response = await fetch(`${url}/health`, {
|
|
84
|
+
signal: AbortSignal.timeout(10000),
|
|
85
|
+
});
|
|
86
|
+
const latency = Date.now() - start;
|
|
87
|
+
const data = await response.json();
|
|
88
|
+
return { connected: true, latency, ...data };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return { connected: false, error: e.message };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Login flow (browser OAuth with localhost callback)
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export async function login(serverUrl = DEFAULT_SERVER) {
|
|
99
|
+
const server = validateServerUrl(serverUrl);
|
|
100
|
+
|
|
101
|
+
// Start local callback server
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const httpServer = createServer((req, res) => {
|
|
104
|
+
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
105
|
+
const token = url.searchParams.get('token') || url.searchParams.get('session');
|
|
106
|
+
|
|
107
|
+
if (token) {
|
|
108
|
+
saveToken(token, server);
|
|
109
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
110
|
+
res.end('<html><body><h2>Login successful</h2><p>You can close this tab.</p></body></html>');
|
|
111
|
+
httpServer.close();
|
|
112
|
+
resolve({ success: true, server });
|
|
113
|
+
} else {
|
|
114
|
+
res.writeHead(400);
|
|
115
|
+
res.end('Missing token');
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
httpServer.listen(CALLBACK_PORT, () => {
|
|
120
|
+
const authUrl = `${server}/auth/cli?redirect=http://localhost:${CALLBACK_PORT}/callback`;
|
|
121
|
+
console.log(`\nOpen this URL to log in:\n ${authUrl}\n`);
|
|
122
|
+
|
|
123
|
+
// Try to open browser
|
|
124
|
+
const { exec } = import('node:child_process').then(m => {
|
|
125
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
126
|
+
: process.platform === 'win32' ? 'start'
|
|
127
|
+
: 'xdg-open';
|
|
128
|
+
m.exec(`${cmd} "${authUrl}"`);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Timeout after 120 seconds
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
httpServer.close();
|
|
135
|
+
reject(new Error('Login timed out after 120 seconds'));
|
|
136
|
+
}, 120000);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context-aware algorithm resolver for the CryptoServe CLI.
|
|
3
|
+
*
|
|
4
|
+
* Port of the 5-layer context model from the CryptoServe backend
|
|
5
|
+
* (backend/app/core/algorithm_resolver.py + backend/app/schemas/context.py).
|
|
6
|
+
*
|
|
7
|
+
* Resolves context labels like "user-pii" to optimal algorithms from the
|
|
8
|
+
* set the CLI can actually execute: AES-256-GCM, AES-128-GCM, ChaCha20-Poly1305.
|
|
9
|
+
*
|
|
10
|
+
* Zero dependencies — uses only node:fs for loading .cryptoserve.json.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Enums / Constants
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
const SENSITIVITY = { critical: 'critical', high: 'high', medium: 'medium', low: 'low' };
|
|
21
|
+
|
|
22
|
+
const ADVERSARIES = {
|
|
23
|
+
opportunistic: 'opportunistic',
|
|
24
|
+
organized_crime: 'organized_crime',
|
|
25
|
+
nation_state: 'nation_state',
|
|
26
|
+
insider: 'insider',
|
|
27
|
+
quantum: 'quantum',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const USAGE = {
|
|
31
|
+
at_rest: 'at_rest',
|
|
32
|
+
in_transit: 'in_transit',
|
|
33
|
+
in_use: 'in_use',
|
|
34
|
+
streaming: 'streaming',
|
|
35
|
+
disk: 'disk',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const FREQUENCY = { high: 'high', medium: 'medium', low: 'low', rare: 'rare' };
|
|
39
|
+
|
|
40
|
+
// Executable algorithms — what the CLI can actually do
|
|
41
|
+
const EXECUTABLE_ALGORITHMS = {
|
|
42
|
+
'AES-256-GCM': { keyBits: 256, securityBits: 256, hwAccelerated: true, mode: 'gcm' },
|
|
43
|
+
'AES-128-GCM': { keyBits: 128, securityBits: 128, hwAccelerated: true, mode: 'gcm' },
|
|
44
|
+
'ChaCha20-Poly1305': { keyBits: 256, securityBits: 256, hwAccelerated: false, mode: 'stream' },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Sensitivity → minimum key bits
|
|
48
|
+
const SENSITIVITY_MIN_BITS = {
|
|
49
|
+
critical: 256,
|
|
50
|
+
high: 256,
|
|
51
|
+
medium: 128,
|
|
52
|
+
low: 128,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Sensitivity → key rotation recommendation (days)
|
|
56
|
+
const SENSITIVITY_ROTATION = {
|
|
57
|
+
critical: 30,
|
|
58
|
+
high: 90,
|
|
59
|
+
medium: 180,
|
|
60
|
+
low: 365,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Compliance framework constraints
|
|
64
|
+
const COMPLIANCE_CONSTRAINTS = {
|
|
65
|
+
'PCI-DSS': { minBits: 256, maxRotationDays: 90 },
|
|
66
|
+
'HIPAA': { minBits: 256, maxRotationDays: 90 },
|
|
67
|
+
'GDPR': { minBits: 128, maxRotationDays: 365 },
|
|
68
|
+
'SOX': { minBits: 256, maxRotationDays: 180 },
|
|
69
|
+
'OWASP': { minBits: 128, maxRotationDays: 365 },
|
|
70
|
+
'CCPA': { minBits: 128, maxRotationDays: 365 },
|
|
71
|
+
'NIST': { minBits: 256, maxRotationDays: 365 },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// =============================================================================
|
|
75
|
+
// Built-in presets (ported from backend/app/main.py seed contexts)
|
|
76
|
+
// =============================================================================
|
|
77
|
+
|
|
78
|
+
const BUILT_IN_CONTEXTS = {
|
|
79
|
+
'user-pii': {
|
|
80
|
+
displayName: 'User Personal Data',
|
|
81
|
+
description: 'Personally identifiable information that can identify an individual',
|
|
82
|
+
sensitivity: 'high',
|
|
83
|
+
compliance: ['GDPR'],
|
|
84
|
+
adversaries: ['organized_crime', 'nation_state'],
|
|
85
|
+
protectionYears: 20,
|
|
86
|
+
usage: 'at_rest',
|
|
87
|
+
frequency: 'high',
|
|
88
|
+
pii: true,
|
|
89
|
+
examples: ['email', 'SSN', 'phone number', 'home address', 'date of birth'],
|
|
90
|
+
},
|
|
91
|
+
'payment-data': {
|
|
92
|
+
displayName: 'Payment & Financial',
|
|
93
|
+
description: 'Payment card data and financial account information',
|
|
94
|
+
sensitivity: 'critical',
|
|
95
|
+
compliance: ['PCI-DSS'],
|
|
96
|
+
adversaries: ['organized_crime', 'insider'],
|
|
97
|
+
protectionYears: 7,
|
|
98
|
+
usage: 'at_rest',
|
|
99
|
+
frequency: 'high',
|
|
100
|
+
pci: true,
|
|
101
|
+
examples: ['credit card number', 'bank account', 'CVV', 'billing address'],
|
|
102
|
+
},
|
|
103
|
+
'session-tokens': {
|
|
104
|
+
displayName: 'Session & Auth Tokens',
|
|
105
|
+
description: 'Temporary authentication and session data',
|
|
106
|
+
sensitivity: 'medium',
|
|
107
|
+
compliance: ['OWASP'],
|
|
108
|
+
adversaries: ['opportunistic'],
|
|
109
|
+
protectionYears: 0.01,
|
|
110
|
+
usage: 'in_transit',
|
|
111
|
+
frequency: 'high',
|
|
112
|
+
examples: ['JWT tokens', 'session IDs', 'refresh tokens', 'API keys'],
|
|
113
|
+
},
|
|
114
|
+
'health-data': {
|
|
115
|
+
displayName: 'Health Information',
|
|
116
|
+
description: 'Protected health information and medical records',
|
|
117
|
+
sensitivity: 'critical',
|
|
118
|
+
compliance: ['HIPAA'],
|
|
119
|
+
adversaries: ['organized_crime', 'nation_state'],
|
|
120
|
+
protectionYears: 25,
|
|
121
|
+
usage: 'at_rest',
|
|
122
|
+
frequency: 'medium',
|
|
123
|
+
phi: true,
|
|
124
|
+
examples: ['diagnosis', 'prescriptions', 'medical history', 'insurance ID'],
|
|
125
|
+
},
|
|
126
|
+
'general': {
|
|
127
|
+
displayName: 'General Purpose',
|
|
128
|
+
description: 'General data without specific regulatory requirements',
|
|
129
|
+
sensitivity: 'medium',
|
|
130
|
+
compliance: [],
|
|
131
|
+
adversaries: ['opportunistic'],
|
|
132
|
+
protectionYears: 5,
|
|
133
|
+
usage: 'at_rest',
|
|
134
|
+
frequency: 'medium',
|
|
135
|
+
examples: ['internal IDs', 'configuration secrets', 'API responses'],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Context config defaults for custom contexts
|
|
140
|
+
const CONTEXT_DEFAULTS = {
|
|
141
|
+
sensitivity: 'medium',
|
|
142
|
+
compliance: [],
|
|
143
|
+
adversaries: ['opportunistic'],
|
|
144
|
+
protectionYears: 5,
|
|
145
|
+
usage: 'at_rest',
|
|
146
|
+
frequency: 'medium',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Custom context loader
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Load custom contexts from .cryptoserve.json in the given directory.
|
|
155
|
+
* Returns merged built-in + custom contexts.
|
|
156
|
+
*/
|
|
157
|
+
export function loadContexts(dir = process.cwd()) {
|
|
158
|
+
const contexts = { ...BUILT_IN_CONTEXTS };
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const configPath = resolve(dir, '.cryptoserve.json');
|
|
162
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
163
|
+
if (config.contexts && typeof config.contexts === 'object') {
|
|
164
|
+
for (const [name, userCtx] of Object.entries(config.contexts)) {
|
|
165
|
+
// Validate name format
|
|
166
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) continue;
|
|
167
|
+
// Merge with defaults
|
|
168
|
+
contexts[name] = { ...CONTEXT_DEFAULTS, ...userCtx, _custom: true };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch { /* no config file or parse error — use built-ins only */ }
|
|
172
|
+
|
|
173
|
+
return contexts;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// 5-Layer Algorithm Resolver
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Resolve a context name to an optimal algorithm and rationale.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} contextName - Context label (e.g. "user-pii")
|
|
184
|
+
* @param {string} [dir] - Directory to look for .cryptoserve.json
|
|
185
|
+
* @returns {{ algorithm, keyBits, context, factors[], alternatives[] }}
|
|
186
|
+
*/
|
|
187
|
+
export function resolveContext(contextName, dir = process.cwd()) {
|
|
188
|
+
const contexts = loadContexts(dir);
|
|
189
|
+
const ctx = contexts[contextName];
|
|
190
|
+
if (!ctx) {
|
|
191
|
+
return { error: `Unknown context: "${contextName}"`, validContexts: Object.keys(contexts) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const factors = [];
|
|
195
|
+
const alternatives = [];
|
|
196
|
+
|
|
197
|
+
// ---- Layer 1: Data Identity (Sensitivity) ----
|
|
198
|
+
const sensitivity = ctx.sensitivity || 'medium';
|
|
199
|
+
let minBits = SENSITIVITY_MIN_BITS[sensitivity] || 128;
|
|
200
|
+
factors.push(`Sensitivity: ${sensitivity.toUpperCase()} → ${minBits}-bit minimum`);
|
|
201
|
+
|
|
202
|
+
// ---- Layer 2: Regulatory Mapping (Compliance) ----
|
|
203
|
+
let rotationDays = SENSITIVITY_ROTATION[sensitivity] || 180;
|
|
204
|
+
const compliance = ctx.compliance || [];
|
|
205
|
+
for (const framework of compliance) {
|
|
206
|
+
const constraint = COMPLIANCE_CONSTRAINTS[framework];
|
|
207
|
+
if (constraint) {
|
|
208
|
+
minBits = Math.max(minBits, constraint.minBits);
|
|
209
|
+
rotationDays = Math.min(rotationDays, constraint.maxRotationDays);
|
|
210
|
+
factors.push(`Compliance: ${framework} → ${constraint.minBits}-bit min, ${constraint.maxRotationDays}-day rotation`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- Layer 3: Threat Model ----
|
|
215
|
+
const adversaries = ctx.adversaries || ['opportunistic'];
|
|
216
|
+
const protectionYears = ctx.protectionYears ?? 5;
|
|
217
|
+
|
|
218
|
+
if (adversaries.includes('nation_state')) {
|
|
219
|
+
minBits = Math.max(minBits, 256);
|
|
220
|
+
factors.push('Threat: Nation-state adversary → 256-bit enforced');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let quantumRisk = false;
|
|
224
|
+
if (adversaries.includes('quantum') || protectionYears > 10) {
|
|
225
|
+
quantumRisk = true;
|
|
226
|
+
minBits = Math.max(minBits, 256);
|
|
227
|
+
const reason = adversaries.includes('quantum')
|
|
228
|
+
? 'Quantum adversary specified'
|
|
229
|
+
: `Protection period ${protectionYears}yr exceeds quantum horizon`;
|
|
230
|
+
factors.push(`Quantum: ${reason} → 256-bit, PQC recommended`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---- Layer 4: Access Patterns ----
|
|
234
|
+
const usage = ctx.usage || 'at_rest';
|
|
235
|
+
const frequency = ctx.frequency || 'medium';
|
|
236
|
+
let preferHwAccel = false;
|
|
237
|
+
|
|
238
|
+
if (frequency === 'high') {
|
|
239
|
+
preferHwAccel = true;
|
|
240
|
+
factors.push('Access: High frequency → hardware acceleration preferred (AES-NI)');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (usage === 'streaming') {
|
|
244
|
+
factors.push('Usage: Streaming → stream cipher preferred');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---- Layer 5: Algorithm Selection ----
|
|
248
|
+
let algorithm;
|
|
249
|
+
|
|
250
|
+
if (usage === 'streaming' && minBits <= 256) {
|
|
251
|
+
// Streaming contexts prefer ChaCha20 (native stream cipher)
|
|
252
|
+
algorithm = 'ChaCha20-Poly1305';
|
|
253
|
+
factors.push('Selected: ChaCha20-Poly1305 (native stream cipher for streaming context)');
|
|
254
|
+
alternatives.push({ algorithm: 'AES-256-GCM', reason: 'If AES-NI hardware acceleration is available' });
|
|
255
|
+
} else if (minBits > 128) {
|
|
256
|
+
// 256-bit requirement
|
|
257
|
+
if (preferHwAccel) {
|
|
258
|
+
algorithm = 'AES-256-GCM';
|
|
259
|
+
factors.push('Selected: AES-256-GCM (256-bit, hardware accelerated, FIPS 197 + SP 800-38D)');
|
|
260
|
+
alternatives.push({ algorithm: 'ChaCha20-Poly1305', reason: 'Better on systems without AES-NI' });
|
|
261
|
+
} else {
|
|
262
|
+
// No strong hw-accel preference — still default to AES-256-GCM (most compatible)
|
|
263
|
+
algorithm = 'AES-256-GCM';
|
|
264
|
+
factors.push('Selected: AES-256-GCM (256-bit, widely supported, FIPS compliant)');
|
|
265
|
+
alternatives.push({ algorithm: 'ChaCha20-Poly1305', reason: 'Equal security, better without AES-NI' });
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
// 128-bit sufficient
|
|
269
|
+
if (preferHwAccel) {
|
|
270
|
+
algorithm = 'AES-128-GCM';
|
|
271
|
+
factors.push('Selected: AES-128-GCM (128-bit sufficient, fast with AES-NI)');
|
|
272
|
+
alternatives.push({ algorithm: 'AES-256-GCM', reason: 'If future-proofing or compliance requires 256-bit' });
|
|
273
|
+
alternatives.push({ algorithm: 'ChaCha20-Poly1305', reason: 'Better on systems without AES-NI' });
|
|
274
|
+
} else {
|
|
275
|
+
algorithm = 'AES-128-GCM';
|
|
276
|
+
factors.push('Selected: AES-128-GCM (128-bit sufficient for threat model)');
|
|
277
|
+
alternatives.push({ algorithm: 'ChaCha20-Poly1305', reason: 'Better performance without AES-NI' });
|
|
278
|
+
alternatives.push({ algorithm: 'AES-256-GCM', reason: 'If upgrading to 256-bit for future-proofing' });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (quantumRisk) {
|
|
283
|
+
factors.push('Note: Post-quantum migration recommended — use CryptoServe server for hybrid PQC');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
algorithm,
|
|
288
|
+
keyBits: EXECUTABLE_ALGORITHMS[algorithm].keyBits,
|
|
289
|
+
context: {
|
|
290
|
+
name: contextName,
|
|
291
|
+
displayName: ctx.displayName || contextName,
|
|
292
|
+
description: ctx.description || '',
|
|
293
|
+
sensitivity,
|
|
294
|
+
compliance,
|
|
295
|
+
adversaries,
|
|
296
|
+
protectionYears,
|
|
297
|
+
usage,
|
|
298
|
+
frequency,
|
|
299
|
+
pii: ctx.pii || false,
|
|
300
|
+
phi: ctx.phi || false,
|
|
301
|
+
pci: ctx.pci || false,
|
|
302
|
+
examples: ctx.examples || [],
|
|
303
|
+
custom: !!ctx._custom,
|
|
304
|
+
},
|
|
305
|
+
rotationDays,
|
|
306
|
+
quantumRisk,
|
|
307
|
+
factors,
|
|
308
|
+
alternatives,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* List all available contexts (built-in + custom).
|
|
314
|
+
*
|
|
315
|
+
* @param {string} [dir] - Directory to look for .cryptoserve.json
|
|
316
|
+
* @returns {Array<{ name, displayName, sensitivity, algorithm, compliance, custom }>}
|
|
317
|
+
*/
|
|
318
|
+
export function listContexts(dir = process.cwd()) {
|
|
319
|
+
const contexts = loadContexts(dir);
|
|
320
|
+
const result = [];
|
|
321
|
+
|
|
322
|
+
for (const [name, ctx] of Object.entries(contexts)) {
|
|
323
|
+
const resolved = resolveContext(name, dir);
|
|
324
|
+
result.push({
|
|
325
|
+
name,
|
|
326
|
+
displayName: ctx.displayName || name,
|
|
327
|
+
description: ctx.description || '',
|
|
328
|
+
sensitivity: ctx.sensitivity || 'medium',
|
|
329
|
+
algorithm: resolved.algorithm,
|
|
330
|
+
compliance: ctx.compliance || [],
|
|
331
|
+
custom: !!ctx._custom,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Re-export for CLI validation
|
|
339
|
+
export { BUILT_IN_CONTEXTS, EXECUTABLE_ALGORITHMS };
|