tlc-claude-code 1.4.8 → 1.4.9

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.
Files changed (169) hide show
  1. package/package.json +1 -1
  2. package/server/index.js +229 -14
  3. package/server/lib/compliance/control-mapper.js +401 -0
  4. package/server/lib/compliance/control-mapper.test.js +117 -0
  5. package/server/lib/compliance/evidence-linker.js +296 -0
  6. package/server/lib/compliance/evidence-linker.test.js +121 -0
  7. package/server/lib/compliance/gdpr-checklist.js +416 -0
  8. package/server/lib/compliance/gdpr-checklist.test.js +131 -0
  9. package/server/lib/compliance/hipaa-checklist.js +277 -0
  10. package/server/lib/compliance/hipaa-checklist.test.js +101 -0
  11. package/server/lib/compliance/iso27001-checklist.js +287 -0
  12. package/server/lib/compliance/iso27001-checklist.test.js +99 -0
  13. package/server/lib/compliance/multi-framework-reporter.js +284 -0
  14. package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
  15. package/server/lib/compliance/pci-dss-checklist.js +214 -0
  16. package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
  17. package/server/lib/compliance/trust-centre.js +187 -0
  18. package/server/lib/compliance/trust-centre.test.js +93 -0
  19. package/server/lib/dashboard/api-server.js +155 -0
  20. package/server/lib/dashboard/api-server.test.js +155 -0
  21. package/server/lib/dashboard/health-api.js +199 -0
  22. package/server/lib/dashboard/health-api.test.js +122 -0
  23. package/server/lib/dashboard/notes-api.js +234 -0
  24. package/server/lib/dashboard/notes-api.test.js +134 -0
  25. package/server/lib/dashboard/router-api.js +176 -0
  26. package/server/lib/dashboard/router-api.test.js +132 -0
  27. package/server/lib/dashboard/tasks-api.js +289 -0
  28. package/server/lib/dashboard/tasks-api.test.js +161 -0
  29. package/server/lib/dashboard/tlc-introspection.js +197 -0
  30. package/server/lib/dashboard/tlc-introspection.test.js +138 -0
  31. package/server/lib/dashboard/version-api.js +222 -0
  32. package/server/lib/dashboard/version-api.test.js +112 -0
  33. package/server/lib/dashboard/websocket-server.js +104 -0
  34. package/server/lib/dashboard/websocket-server.test.js +118 -0
  35. package/server/lib/deploy/branch-classifier.js +163 -0
  36. package/server/lib/deploy/branch-classifier.test.js +164 -0
  37. package/server/lib/deploy/deployment-approval.js +299 -0
  38. package/server/lib/deploy/deployment-approval.test.js +296 -0
  39. package/server/lib/deploy/deployment-audit.js +374 -0
  40. package/server/lib/deploy/deployment-audit.test.js +307 -0
  41. package/server/lib/deploy/deployment-executor.js +335 -0
  42. package/server/lib/deploy/deployment-executor.test.js +329 -0
  43. package/server/lib/deploy/deployment-rules.js +163 -0
  44. package/server/lib/deploy/deployment-rules.test.js +188 -0
  45. package/server/lib/deploy/rollback-manager.js +379 -0
  46. package/server/lib/deploy/rollback-manager.test.js +321 -0
  47. package/server/lib/deploy/security-gates.js +236 -0
  48. package/server/lib/deploy/security-gates.test.js +222 -0
  49. package/server/lib/k8s/gitops-config.js +188 -0
  50. package/server/lib/k8s/gitops-config.test.js +59 -0
  51. package/server/lib/k8s/helm-generator.js +196 -0
  52. package/server/lib/k8s/helm-generator.test.js +59 -0
  53. package/server/lib/k8s/kustomize-generator.js +176 -0
  54. package/server/lib/k8s/kustomize-generator.test.js +58 -0
  55. package/server/lib/k8s/network-policy.js +114 -0
  56. package/server/lib/k8s/network-policy.test.js +53 -0
  57. package/server/lib/k8s/pod-security.js +114 -0
  58. package/server/lib/k8s/pod-security.test.js +55 -0
  59. package/server/lib/k8s/rbac-generator.js +132 -0
  60. package/server/lib/k8s/rbac-generator.test.js +57 -0
  61. package/server/lib/k8s/resource-manager.js +172 -0
  62. package/server/lib/k8s/resource-manager.test.js +60 -0
  63. package/server/lib/k8s/secrets-encryption.js +168 -0
  64. package/server/lib/k8s/secrets-encryption.test.js +49 -0
  65. package/server/lib/monitoring/alert-manager.js +238 -0
  66. package/server/lib/monitoring/alert-manager.test.js +106 -0
  67. package/server/lib/monitoring/health-check.js +226 -0
  68. package/server/lib/monitoring/health-check.test.js +176 -0
  69. package/server/lib/monitoring/incident-manager.js +230 -0
  70. package/server/lib/monitoring/incident-manager.test.js +98 -0
  71. package/server/lib/monitoring/log-aggregator.js +147 -0
  72. package/server/lib/monitoring/log-aggregator.test.js +89 -0
  73. package/server/lib/monitoring/metrics-collector.js +337 -0
  74. package/server/lib/monitoring/metrics-collector.test.js +172 -0
  75. package/server/lib/monitoring/status-page.js +214 -0
  76. package/server/lib/monitoring/status-page.test.js +105 -0
  77. package/server/lib/monitoring/uptime-monitor.js +194 -0
  78. package/server/lib/monitoring/uptime-monitor.test.js +109 -0
  79. package/server/lib/network/fail2ban-config.js +294 -0
  80. package/server/lib/network/fail2ban-config.test.js +275 -0
  81. package/server/lib/network/firewall-manager.js +252 -0
  82. package/server/lib/network/firewall-manager.test.js +254 -0
  83. package/server/lib/network/geoip-filter.js +282 -0
  84. package/server/lib/network/geoip-filter.test.js +264 -0
  85. package/server/lib/network/rate-limiter.js +229 -0
  86. package/server/lib/network/rate-limiter.test.js +293 -0
  87. package/server/lib/network/request-validator.js +351 -0
  88. package/server/lib/network/request-validator.test.js +345 -0
  89. package/server/lib/network/security-headers.js +251 -0
  90. package/server/lib/network/security-headers.test.js +283 -0
  91. package/server/lib/network/tls-config.js +210 -0
  92. package/server/lib/network/tls-config.test.js +248 -0
  93. package/server/lib/security/auth-security.js +369 -0
  94. package/server/lib/security/auth-security.test.js +448 -0
  95. package/server/lib/security/cis-benchmark.js +152 -0
  96. package/server/lib/security/cis-benchmark.test.js +137 -0
  97. package/server/lib/security/compose-templates.js +312 -0
  98. package/server/lib/security/compose-templates.test.js +229 -0
  99. package/server/lib/security/container-runtime.js +456 -0
  100. package/server/lib/security/container-runtime.test.js +503 -0
  101. package/server/lib/security/cors-validator.js +278 -0
  102. package/server/lib/security/cors-validator.test.js +310 -0
  103. package/server/lib/security/crypto-utils.js +253 -0
  104. package/server/lib/security/crypto-utils.test.js +409 -0
  105. package/server/lib/security/dockerfile-linter.js +459 -0
  106. package/server/lib/security/dockerfile-linter.test.js +483 -0
  107. package/server/lib/security/dockerfile-templates.js +278 -0
  108. package/server/lib/security/dockerfile-templates.test.js +164 -0
  109. package/server/lib/security/error-sanitizer.js +426 -0
  110. package/server/lib/security/error-sanitizer.test.js +331 -0
  111. package/server/lib/security/headers-generator.js +368 -0
  112. package/server/lib/security/headers-generator.test.js +398 -0
  113. package/server/lib/security/image-scanner.js +83 -0
  114. package/server/lib/security/image-scanner.test.js +106 -0
  115. package/server/lib/security/input-validator.js +352 -0
  116. package/server/lib/security/input-validator.test.js +330 -0
  117. package/server/lib/security/network-policy.js +174 -0
  118. package/server/lib/security/network-policy.test.js +164 -0
  119. package/server/lib/security/output-encoder.js +237 -0
  120. package/server/lib/security/output-encoder.test.js +276 -0
  121. package/server/lib/security/path-validator.js +359 -0
  122. package/server/lib/security/path-validator.test.js +293 -0
  123. package/server/lib/security/query-builder.js +421 -0
  124. package/server/lib/security/query-builder.test.js +318 -0
  125. package/server/lib/security/secret-detector.js +290 -0
  126. package/server/lib/security/secret-detector.test.js +354 -0
  127. package/server/lib/security/secrets-validator.js +137 -0
  128. package/server/lib/security/secrets-validator.test.js +120 -0
  129. package/server/lib/security-testing/dast-runner.js +154 -0
  130. package/server/lib/security-testing/dast-runner.test.js +62 -0
  131. package/server/lib/security-testing/dependency-scanner.js +172 -0
  132. package/server/lib/security-testing/dependency-scanner.test.js +64 -0
  133. package/server/lib/security-testing/pentest-runner.js +230 -0
  134. package/server/lib/security-testing/pentest-runner.test.js +60 -0
  135. package/server/lib/security-testing/sast-runner.js +136 -0
  136. package/server/lib/security-testing/sast-runner.test.js +62 -0
  137. package/server/lib/security-testing/secret-scanner.js +153 -0
  138. package/server/lib/security-testing/secret-scanner.test.js +66 -0
  139. package/server/lib/security-testing/security-gate.js +216 -0
  140. package/server/lib/security-testing/security-gate.test.js +115 -0
  141. package/server/lib/security-testing/security-reporter.js +303 -0
  142. package/server/lib/security-testing/security-reporter.test.js +114 -0
  143. package/server/lib/standards/audit-checker.js +546 -0
  144. package/server/lib/standards/audit-checker.test.js +415 -0
  145. package/server/lib/standards/cleanup-executor.js +452 -0
  146. package/server/lib/standards/cleanup-executor.test.js +293 -0
  147. package/server/lib/standards/refactor-stepper.js +425 -0
  148. package/server/lib/standards/refactor-stepper.test.js +298 -0
  149. package/server/lib/standards/standards-injector.js +167 -0
  150. package/server/lib/standards/standards-injector.test.js +232 -0
  151. package/server/lib/user-management.test.js +284 -0
  152. package/server/lib/vps/backup-manager.js +157 -0
  153. package/server/lib/vps/backup-manager.test.js +59 -0
  154. package/server/lib/vps/caddy-config.js +159 -0
  155. package/server/lib/vps/caddy-config.test.js +48 -0
  156. package/server/lib/vps/compose-orchestrator.js +219 -0
  157. package/server/lib/vps/compose-orchestrator.test.js +50 -0
  158. package/server/lib/vps/database-config.js +208 -0
  159. package/server/lib/vps/database-config.test.js +47 -0
  160. package/server/lib/vps/deploy-script.js +211 -0
  161. package/server/lib/vps/deploy-script.test.js +53 -0
  162. package/server/lib/vps/secrets-manager.js +148 -0
  163. package/server/lib/vps/secrets-manager.test.js +58 -0
  164. package/server/lib/vps/server-hardening.js +174 -0
  165. package/server/lib/vps/server-hardening.test.js +70 -0
  166. package/server/package-lock.json +19 -0
  167. package/server/package.json +1 -0
  168. package/server/templates/CLAUDE.md +37 -0
  169. package/server/templates/CODING-STANDARDS.md +408 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Crypto Utilities Module
3
+ *
4
+ * Secure cryptographic primitives.
5
+ * Addresses OWASP A02: Cryptographic Failures
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+
10
+ /**
11
+ * Custom error for deprecated algorithm usage
12
+ */
13
+ export class DeprecatedAlgorithmError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'DeprecatedAlgorithmError';
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Deprecated algorithms that should not be used for security
22
+ */
23
+ const DEPRECATED_ALGORITHMS = ['md5', 'sha1', 'md4', 'ripemd160'];
24
+
25
+ /**
26
+ * Generate cryptographically secure random bytes
27
+ * @param {number} length - Number of bytes to generate
28
+ * @returns {Buffer} Random bytes
29
+ */
30
+ export function randomBytes(length) {
31
+ if (length <= 0) {
32
+ throw new Error('Length must be positive');
33
+ }
34
+ return crypto.randomBytes(length);
35
+ }
36
+
37
+ /**
38
+ * Generate a random string in specified encoding
39
+ * @param {number} bytes - Number of bytes of randomness
40
+ * @param {string} encoding - Output encoding (hex, base64, base64url, alphanumeric)
41
+ * @returns {string} Random string
42
+ */
43
+ export function randomString(bytes, encoding = 'hex') {
44
+ const buffer = randomBytes(bytes);
45
+
46
+ switch (encoding) {
47
+ case 'hex':
48
+ return buffer.toString('hex');
49
+ case 'base64':
50
+ return buffer.toString('base64');
51
+ case 'base64url':
52
+ return buffer.toString('base64url');
53
+ case 'alphanumeric': {
54
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
55
+ let result = '';
56
+ for (let i = 0; i < bytes; i++) {
57
+ result += chars[buffer[i] % chars.length];
58
+ }
59
+ return result;
60
+ }
61
+ default:
62
+ return buffer.toString('hex');
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Encrypt data using AES-256-GCM
68
+ * @param {string|Buffer} plaintext - Data to encrypt
69
+ * @param {Buffer} key - 32-byte encryption key
70
+ * @param {Object} options - Encryption options
71
+ * @returns {string} Base64-encoded ciphertext with IV and auth tag
72
+ */
73
+ export function encrypt(plaintext, key, options = {}) {
74
+ const { aad } = options;
75
+
76
+ if (key.length !== 32) {
77
+ throw new Error('Key must be 32 bytes for AES-256');
78
+ }
79
+
80
+ // Generate random 12-byte IV
81
+ const iv = crypto.randomBytes(12);
82
+
83
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
84
+
85
+ if (aad) {
86
+ cipher.setAAD(Buffer.from(aad));
87
+ }
88
+
89
+ const plaintextBuffer = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext);
90
+ const encrypted = Buffer.concat([cipher.update(plaintextBuffer), cipher.final()]);
91
+ const authTag = cipher.getAuthTag();
92
+
93
+ // Format: IV (12 bytes) + ciphertext + authTag (16 bytes)
94
+ const combined = Buffer.concat([iv, encrypted, authTag]);
95
+ return combined.toString('base64');
96
+ }
97
+
98
+ /**
99
+ * Decrypt data encrypted with AES-256-GCM
100
+ * @param {string} ciphertext - Base64-encoded ciphertext
101
+ * @param {Buffer} key - 32-byte decryption key
102
+ * @param {Object} options - Decryption options
103
+ * @returns {string|Buffer} Decrypted data
104
+ */
105
+ export function decrypt(ciphertext, key, options = {}) {
106
+ const { aad, encoding = 'utf8' } = options;
107
+
108
+ if (key.length !== 32) {
109
+ throw new Error('Key must be 32 bytes for AES-256');
110
+ }
111
+
112
+ const combined = Buffer.from(ciphertext, 'base64');
113
+
114
+ // Extract IV (first 12 bytes), ciphertext, and authTag (last 16 bytes)
115
+ const iv = combined.subarray(0, 12);
116
+ const authTag = combined.subarray(combined.length - 16);
117
+ const encrypted = combined.subarray(12, combined.length - 16);
118
+
119
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
120
+ decipher.setAuthTag(authTag);
121
+
122
+ if (aad) {
123
+ decipher.setAAD(Buffer.from(aad));
124
+ }
125
+
126
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
127
+
128
+ return encoding === 'buffer' ? decrypted : decrypted.toString(encoding);
129
+ }
130
+
131
+ /**
132
+ * Sign data with HMAC
133
+ * @param {string|Buffer} data - Data to sign
134
+ * @param {Buffer} key - Signing key
135
+ * @param {Object} options - HMAC options
136
+ * @returns {string} Hex-encoded signature
137
+ */
138
+ export function hmacSign(data, key, options = {}) {
139
+ const { algorithm = 'sha256', purpose = 'security' } = options;
140
+
141
+ // Check for deprecated algorithms
142
+ if (DEPRECATED_ALGORITHMS.includes(algorithm.toLowerCase()) && purpose !== 'checksum') {
143
+ throw new DeprecatedAlgorithmError(`Algorithm ${algorithm} is deprecated for security purposes`);
144
+ }
145
+
146
+ const hmac = crypto.createHmac(algorithm, key);
147
+ hmac.update(data);
148
+ return hmac.digest('hex');
149
+ }
150
+
151
+ /**
152
+ * Verify HMAC signature
153
+ * @param {string|Buffer} data - Original data
154
+ * @param {string} signature - Signature to verify
155
+ * @param {Buffer} key - Signing key
156
+ * @param {Object} options - HMAC options
157
+ * @returns {boolean} True if signature is valid
158
+ */
159
+ export function hmacVerify(data, signature, key, options = {}) {
160
+ const { algorithm = 'sha256' } = options;
161
+
162
+ const expected = hmacSign(data, key, { ...options, purpose: 'security' });
163
+ return constantTimeCompare(Buffer.from(expected), Buffer.from(signature));
164
+ }
165
+
166
+ /**
167
+ * Derive a key from password or input key material
168
+ * @param {string|Buffer} input - Password or key material
169
+ * @param {Buffer} salt - Salt for derivation
170
+ * @param {Object} options - Derivation options
171
+ * @returns {Buffer} Derived key
172
+ */
173
+ export function deriveKey(input, salt, options = {}) {
174
+ const {
175
+ algorithm = 'pbkdf2',
176
+ keyLength = 32,
177
+ iterations = 100000, // OWASP minimum
178
+ info = '',
179
+ } = options;
180
+
181
+ const inputBuffer = Buffer.isBuffer(input) ? input : Buffer.from(input);
182
+
183
+ if (algorithm === 'pbkdf2') {
184
+ return crypto.pbkdf2Sync(inputBuffer, salt, iterations, keyLength, 'sha256');
185
+ }
186
+
187
+ if (algorithm === 'hkdf') {
188
+ const derived = crypto.hkdfSync('sha256', inputBuffer, salt, Buffer.from(info), keyLength);
189
+ return Buffer.from(derived);
190
+ }
191
+
192
+ throw new Error(`Unknown algorithm: ${algorithm}`);
193
+ }
194
+
195
+ /**
196
+ * Constant-time buffer comparison
197
+ * @param {Buffer} a - First buffer
198
+ * @param {Buffer} b - Second buffer
199
+ * @returns {boolean} True if buffers are equal
200
+ */
201
+ export function constantTimeCompare(a, b) {
202
+ if (!Buffer.isBuffer(a)) a = Buffer.from(a);
203
+ if (!Buffer.isBuffer(b)) b = Buffer.from(b);
204
+
205
+ if (a.length !== b.length) {
206
+ // Do dummy comparison to maintain constant time
207
+ crypto.timingSafeEqual(a, a);
208
+ return false;
209
+ }
210
+
211
+ return crypto.timingSafeEqual(a, b);
212
+ }
213
+
214
+ /**
215
+ * Generate an asymmetric key pair
216
+ * @param {string} type - Key type (rsa, ec)
217
+ * @param {Object} options - Key generation options
218
+ * @returns {Promise<{publicKey: string, privateKey: string}>} Key pair
219
+ */
220
+ export async function generateKeyPair(type, options = {}) {
221
+ return new Promise((resolve, reject) => {
222
+ if (type === 'rsa') {
223
+ const { modulusLength = 2048 } = options;
224
+
225
+ if (modulusLength < 2048) {
226
+ reject(new Error('RSA key size must be at least 2048 bits'));
227
+ return;
228
+ }
229
+
230
+ crypto.generateKeyPair('rsa', {
231
+ modulusLength,
232
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
233
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
234
+ }, (err, publicKey, privateKey) => {
235
+ if (err) reject(err);
236
+ else resolve({ publicKey, privateKey });
237
+ });
238
+ } else if (type === 'ec') {
239
+ const { namedCurve = 'P-256' } = options;
240
+
241
+ crypto.generateKeyPair('ec', {
242
+ namedCurve,
243
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
244
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
245
+ }, (err, publicKey, privateKey) => {
246
+ if (err) reject(err);
247
+ else resolve({ publicKey, privateKey });
248
+ });
249
+ } else {
250
+ reject(new Error(`Unknown key type: ${type}`));
251
+ }
252
+ });
253
+ }
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Crypto Utilities Tests
3
+ *
4
+ * Tests for secure cryptographic primitives.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ randomBytes,
10
+ randomString,
11
+ encrypt,
12
+ decrypt,
13
+ hmacSign,
14
+ hmacVerify,
15
+ deriveKey,
16
+ constantTimeCompare,
17
+ generateKeyPair,
18
+ DeprecatedAlgorithmError,
19
+ } from './crypto-utils.js';
20
+
21
+ describe('crypto-utils', () => {
22
+ describe('randomBytes', () => {
23
+ it('generates specified number of bytes', () => {
24
+ const bytes = randomBytes(32);
25
+
26
+ expect(bytes).toBeInstanceOf(Buffer);
27
+ expect(bytes.length).toBe(32);
28
+ });
29
+
30
+ it('generates different bytes each call', () => {
31
+ const bytes1 = randomBytes(32);
32
+ const bytes2 = randomBytes(32);
33
+
34
+ expect(bytes1.equals(bytes2)).toBe(false);
35
+ });
36
+
37
+ it('generates cryptographically random bytes', () => {
38
+ // Statistical test: bytes should have reasonable entropy
39
+ const bytes = randomBytes(1000);
40
+ const counts = new Array(256).fill(0);
41
+
42
+ for (const byte of bytes) {
43
+ counts[byte]++;
44
+ }
45
+
46
+ // Each byte value should appear roughly 1000/256 ≈ 4 times
47
+ // With random data, standard deviation is about sqrt(1000 * (1/256) * (255/256)) ≈ 2
48
+ // So values should be within roughly 4 ± 6 most of the time
49
+ const avg = 1000 / 256;
50
+ const inRange = counts.filter(c => c >= 0 && c <= avg * 3).length;
51
+
52
+ expect(inRange).toBeGreaterThan(200); // Most values in reasonable range
53
+ });
54
+
55
+ it('rejects zero or negative length', () => {
56
+ expect(() => randomBytes(0)).toThrow();
57
+ expect(() => randomBytes(-1)).toThrow();
58
+ });
59
+ });
60
+
61
+ describe('randomString', () => {
62
+ it('generates hex string of correct length', () => {
63
+ const str = randomString(16, 'hex');
64
+
65
+ expect(str).toMatch(/^[a-f0-9]{32}$/); // 16 bytes = 32 hex chars
66
+ });
67
+
68
+ it('generates base64 string', () => {
69
+ const str = randomString(16, 'base64');
70
+
71
+ expect(str).toMatch(/^[A-Za-z0-9+/]+=*$/);
72
+ });
73
+
74
+ it('generates base64url string', () => {
75
+ const str = randomString(16, 'base64url');
76
+
77
+ expect(str).toMatch(/^[A-Za-z0-9_-]+$/);
78
+ });
79
+
80
+ it('generates alphanumeric string', () => {
81
+ const str = randomString(32, 'alphanumeric');
82
+
83
+ expect(str).toMatch(/^[A-Za-z0-9]{32}$/);
84
+ });
85
+
86
+ it('generates unique strings', () => {
87
+ const strings = new Set();
88
+ for (let i = 0; i < 100; i++) {
89
+ strings.add(randomString(16, 'hex'));
90
+ }
91
+
92
+ expect(strings.size).toBe(100);
93
+ });
94
+ });
95
+
96
+ describe('encrypt / decrypt (AES-256-GCM)', () => {
97
+ it('roundtrips encryption correctly', () => {
98
+ const key = randomBytes(32);
99
+ const plaintext = 'Hello, World!';
100
+
101
+ const encrypted = encrypt(plaintext, key);
102
+ const decrypted = decrypt(encrypted, key);
103
+
104
+ expect(decrypted).toBe(plaintext);
105
+ });
106
+
107
+ it('handles binary data', () => {
108
+ const key = randomBytes(32);
109
+ const plaintext = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd]);
110
+
111
+ const encrypted = encrypt(plaintext, key);
112
+ const decrypted = decrypt(encrypted, key, { encoding: 'buffer' });
113
+
114
+ expect(decrypted.equals(plaintext)).toBe(true);
115
+ });
116
+
117
+ it('produces different ciphertext for same plaintext', () => {
118
+ const key = randomBytes(32);
119
+ const plaintext = 'Same message';
120
+
121
+ const encrypted1 = encrypt(plaintext, key);
122
+ const encrypted2 = encrypt(plaintext, key);
123
+
124
+ expect(encrypted1).not.toBe(encrypted2);
125
+ });
126
+
127
+ it('fails decryption with wrong key', () => {
128
+ const key1 = randomBytes(32);
129
+ const key2 = randomBytes(32);
130
+ const plaintext = 'Secret message';
131
+
132
+ const encrypted = encrypt(plaintext, key1);
133
+
134
+ expect(() => decrypt(encrypted, key2)).toThrow();
135
+ });
136
+
137
+ it('fails decryption with tampered ciphertext', () => {
138
+ const key = randomBytes(32);
139
+ const plaintext = 'Secret message';
140
+
141
+ const encrypted = encrypt(plaintext, key);
142
+
143
+ // Tamper with the ciphertext
144
+ const tampered = Buffer.from(encrypted, 'base64');
145
+ tampered[20] ^= 0xff;
146
+ const tamperedStr = tampered.toString('base64');
147
+
148
+ expect(() => decrypt(tamperedStr, key)).toThrow();
149
+ });
150
+
151
+ it('fails decryption with tampered auth tag', () => {
152
+ const key = randomBytes(32);
153
+ const plaintext = 'Secret message';
154
+
155
+ const encrypted = encrypt(plaintext, key);
156
+
157
+ // Tamper with the auth tag (last 16 bytes)
158
+ const tampered = Buffer.from(encrypted, 'base64');
159
+ tampered[tampered.length - 1] ^= 0xff;
160
+ const tamperedStr = tampered.toString('base64');
161
+
162
+ expect(() => decrypt(tamperedStr, key)).toThrow();
163
+ });
164
+
165
+ it('rejects key of wrong length', () => {
166
+ const shortKey = randomBytes(16); // Should be 32 for AES-256
167
+ const plaintext = 'Message';
168
+
169
+ expect(() => encrypt(plaintext, shortKey)).toThrow();
170
+ });
171
+
172
+ it('supports additional authenticated data', () => {
173
+ const key = randomBytes(32);
174
+ const plaintext = 'Secret';
175
+ const aad = 'context-data';
176
+
177
+ const encrypted = encrypt(plaintext, key, { aad });
178
+ const decrypted = decrypt(encrypted, key, { aad });
179
+
180
+ expect(decrypted).toBe(plaintext);
181
+ });
182
+
183
+ it('fails with wrong additional authenticated data', () => {
184
+ const key = randomBytes(32);
185
+ const plaintext = 'Secret';
186
+
187
+ const encrypted = encrypt(plaintext, key, { aad: 'correct' });
188
+
189
+ expect(() => decrypt(encrypted, key, { aad: 'wrong' })).toThrow();
190
+ });
191
+ });
192
+
193
+ describe('hmacSign / hmacVerify', () => {
194
+ it('signs data with HMAC-SHA256', () => {
195
+ const key = randomBytes(32);
196
+ const data = 'Message to sign';
197
+
198
+ const signature = hmacSign(data, key);
199
+
200
+ expect(signature).toMatch(/^[a-f0-9]{64}$/); // SHA256 = 64 hex chars
201
+ });
202
+
203
+ it('verifies valid signature', () => {
204
+ const key = randomBytes(32);
205
+ const data = 'Message to sign';
206
+
207
+ const signature = hmacSign(data, key);
208
+ const isValid = hmacVerify(data, signature, key);
209
+
210
+ expect(isValid).toBe(true);
211
+ });
212
+
213
+ it('rejects invalid signature', () => {
214
+ const key = randomBytes(32);
215
+ const data = 'Message to sign';
216
+
217
+ const signature = hmacSign(data, key);
218
+ const isValid = hmacVerify(data, signature + 'x', key);
219
+
220
+ expect(isValid).toBe(false);
221
+ });
222
+
223
+ it('rejects signature with wrong key', () => {
224
+ const key1 = randomBytes(32);
225
+ const key2 = randomBytes(32);
226
+ const data = 'Message to sign';
227
+
228
+ const signature = hmacSign(data, key1);
229
+ const isValid = hmacVerify(data, signature, key2);
230
+
231
+ expect(isValid).toBe(false);
232
+ });
233
+
234
+ it('rejects modified data', () => {
235
+ const key = randomBytes(32);
236
+
237
+ const signature = hmacSign('Original message', key);
238
+ const isValid = hmacVerify('Modified message', signature, key);
239
+
240
+ expect(isValid).toBe(false);
241
+ });
242
+
243
+ it('supports SHA-512', () => {
244
+ const key = randomBytes(32);
245
+ const data = 'Message';
246
+
247
+ const signature = hmacSign(data, key, { algorithm: 'sha512' });
248
+
249
+ expect(signature).toMatch(/^[a-f0-9]{128}$/); // SHA512 = 128 hex chars
250
+ });
251
+ });
252
+
253
+ describe('deriveKey', () => {
254
+ it('derives key with PBKDF2', () => {
255
+ const password = 'user-password';
256
+ const salt = randomBytes(16);
257
+
258
+ const key = deriveKey(password, salt, { algorithm: 'pbkdf2' });
259
+
260
+ expect(key).toBeInstanceOf(Buffer);
261
+ expect(key.length).toBe(32);
262
+ });
263
+
264
+ it('produces consistent keys', () => {
265
+ const password = 'user-password';
266
+ const salt = randomBytes(16);
267
+
268
+ const key1 = deriveKey(password, salt);
269
+ const key2 = deriveKey(password, salt);
270
+
271
+ expect(key1.equals(key2)).toBe(true);
272
+ });
273
+
274
+ it('produces different keys with different salts', () => {
275
+ const password = 'user-password';
276
+
277
+ const key1 = deriveKey(password, randomBytes(16));
278
+ const key2 = deriveKey(password, randomBytes(16));
279
+
280
+ expect(key1.equals(key2)).toBe(false);
281
+ });
282
+
283
+ it('supports HKDF', () => {
284
+ const inputKey = randomBytes(32);
285
+ const salt = randomBytes(16);
286
+ const info = 'context';
287
+
288
+ const derivedKey = deriveKey(inputKey, salt, {
289
+ algorithm: 'hkdf',
290
+ info,
291
+ });
292
+
293
+ expect(derivedKey).toBeInstanceOf(Buffer);
294
+ expect(derivedKey.length).toBe(32);
295
+ });
296
+
297
+ it('uses sufficient iterations for PBKDF2', () => {
298
+ const password = 'password';
299
+ const salt = randomBytes(16);
300
+
301
+ // Should use at least 100,000 iterations (OWASP recommendation)
302
+ const key = deriveKey(password, salt, { algorithm: 'pbkdf2' });
303
+
304
+ // We can't directly check iterations, but we can check it takes time
305
+ expect(key).toBeDefined();
306
+ });
307
+ });
308
+
309
+ describe('constantTimeCompare', () => {
310
+ it('returns true for equal buffers', () => {
311
+ const a = Buffer.from('same-value');
312
+ const b = Buffer.from('same-value');
313
+
314
+ expect(constantTimeCompare(a, b)).toBe(true);
315
+ });
316
+
317
+ it('returns false for different buffers', () => {
318
+ const a = Buffer.from('value-a');
319
+ const b = Buffer.from('value-b');
320
+
321
+ expect(constantTimeCompare(a, b)).toBe(false);
322
+ });
323
+
324
+ it('returns false for different length buffers', () => {
325
+ const a = Buffer.from('short');
326
+ const b = Buffer.from('much-longer');
327
+
328
+ expect(constantTimeCompare(a, b)).toBe(false);
329
+ });
330
+
331
+ it('has constant time regardless of difference position', () => {
332
+ const base = Buffer.from('abcdefghijklmnopqrstuvwxyz');
333
+ const diffStart = Buffer.from('Xbcdefghijklmnopqrstuvwxyz');
334
+ const diffEnd = Buffer.from('abcdefghijklmnopqrstuvwxyZ');
335
+
336
+ const times1 = [];
337
+ const times2 = [];
338
+
339
+ for (let i = 0; i < 1000; i++) {
340
+ const start1 = process.hrtime.bigint();
341
+ constantTimeCompare(base, diffStart);
342
+ const end1 = process.hrtime.bigint();
343
+ times1.push(Number(end1 - start1));
344
+
345
+ const start2 = process.hrtime.bigint();
346
+ constantTimeCompare(base, diffEnd);
347
+ const end2 = process.hrtime.bigint();
348
+ times2.push(Number(end2 - start2));
349
+ }
350
+
351
+ const avg1 = times1.reduce((a, b) => a + b) / times1.length;
352
+ const avg2 = times2.reduce((a, b) => a + b) / times2.length;
353
+ const ratio = Math.max(avg1, avg2) / Math.min(avg1, avg2);
354
+
355
+ // Times should be very similar (within 50%)
356
+ expect(ratio).toBeLessThan(1.5);
357
+ });
358
+ });
359
+
360
+ describe('deprecated algorithm rejection', () => {
361
+ it('rejects MD5 for security purposes', () => {
362
+ expect(() => {
363
+ hmacSign('data', randomBytes(16), { algorithm: 'md5' });
364
+ }).toThrow(DeprecatedAlgorithmError);
365
+ });
366
+
367
+ it('rejects SHA1 for security purposes', () => {
368
+ expect(() => {
369
+ hmacSign('data', randomBytes(16), { algorithm: 'sha1' });
370
+ }).toThrow(DeprecatedAlgorithmError);
371
+ });
372
+
373
+ it('allows MD5 when explicitly marked as non-security', () => {
374
+ // For checksums, not security
375
+ const result = hmacSign('data', randomBytes(16), {
376
+ algorithm: 'md5',
377
+ purpose: 'checksum',
378
+ });
379
+
380
+ expect(result).toBeDefined();
381
+ });
382
+ });
383
+
384
+ describe('generateKeyPair', () => {
385
+ it('generates RSA key pair', async () => {
386
+ const { publicKey, privateKey } = await generateKeyPair('rsa', {
387
+ modulusLength: 2048,
388
+ });
389
+
390
+ expect(publicKey).toContain('BEGIN PUBLIC KEY');
391
+ expect(privateKey).toContain('BEGIN PRIVATE KEY');
392
+ });
393
+
394
+ it('generates EC key pair', async () => {
395
+ const { publicKey, privateKey } = await generateKeyPair('ec', {
396
+ namedCurve: 'P-256',
397
+ });
398
+
399
+ expect(publicKey).toBeDefined();
400
+ expect(privateKey).toBeDefined();
401
+ });
402
+
403
+ it('rejects weak RSA key sizes', async () => {
404
+ await expect(
405
+ generateKeyPair('rsa', { modulusLength: 1024 })
406
+ ).rejects.toThrow();
407
+ });
408
+ });
409
+ });