cipher-shield 1.0.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,268 @@
1
+ /**
2
+ * SmartLogger - Advanced Logging with Automatic Data Masking
3
+ *
4
+ * A secure logging utility that automatically masks sensitive data in logs.
5
+ * Recursively scans objects, arrays, and strings to protect sensitive information
6
+ * like passwords, tokens, and API keys.
7
+ *
8
+ * @module SmartLogger
9
+ * @version 1.0.0
10
+ * @author Omindu Dissanayaka
11
+ * @license MIT
12
+ */
13
+
14
+ class SmartLogger {
15
+ /**
16
+ * Creates a SmartLogger instance
17
+ *
18
+ * @param {Object} [options] - Configuration options
19
+ * @param {string[]} [options.maskKeys] - Additional keys to mask (case-insensitive)
20
+ * @param {string} [options.maskValue='********'] - Value to replace sensitive data with
21
+ * @param {boolean} [options.enableTimestamp=true] - Include timestamp in logs
22
+ * @param {boolean} [options.enableColors=true] - Enable colored output
23
+ */
24
+ constructor(options = {}) {
25
+ this.options = {
26
+ maskKeys: [
27
+ 'password', 'token', 'secret', 'apikey', 'api_key', 'apiKey',
28
+ 'creditcard', 'credit_card', 'cvv', 'auth', 'authorization',
29
+ 'bearer', 'session', 'cookie', 'private_key', 'privateKey'
30
+ ].concat(options.maskKeys || []),
31
+ maskValue: options.maskValue || '********',
32
+ enableTimestamp: options.enableTimestamp !== false,
33
+ enableColors: options.enableColors !== false,
34
+ ...options
35
+ };
36
+
37
+ this.colors = {
38
+ reset: '\x1b[0m',
39
+ bright: '\x1b[1m',
40
+ dim: '\x1b[2m',
41
+ red: '\x1b[31m',
42
+ green: '\x1b[32m',
43
+ yellow: '\x1b[33m',
44
+ blue: '\x1b[34m',
45
+ magenta: '\x1b[35m',
46
+ cyan: '\x1b[36m',
47
+ white: '\x1b[37m'
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Add additional keys to mask in logs
53
+ *
54
+ * @param {string|string[]} keys - Key(s) to add to the masking list
55
+ */
56
+ addMaskKey(keys) {
57
+ if (Array.isArray(keys)) {
58
+ this.options.maskKeys = this.options.maskKeys.concat(keys);
59
+ } else if (typeof keys === 'string') {
60
+ this.options.maskKeys.push(keys);
61
+ } else {
62
+ throw new Error('Keys must be a string or array of strings');
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Log information with data masking
68
+ *
69
+ * @param {*} message - Message or data to log
70
+ * @param {...*} args - Additional arguments
71
+ */
72
+ info(message, ...args) {
73
+ this._log('INFO', message, args, this.colors.green);
74
+ }
75
+
76
+ /**
77
+ * Log warnings with data masking
78
+ *
79
+ * @param {*} message - Message or data to log
80
+ * @param {...*} args - Additional arguments
81
+ */
82
+ warn(message, ...args) {
83
+ this._log('WARN', message, args, this.colors.yellow);
84
+ }
85
+
86
+ /**
87
+ * Log errors with data masking
88
+ *
89
+ * @param {*} message - Message or data to log
90
+ * @param {...*} args - Additional arguments
91
+ */
92
+ error(message, ...args) {
93
+ this._log('ERROR', message, args, this.colors.red);
94
+ }
95
+
96
+ /**
97
+ * Log debug information with data masking
98
+ *
99
+ * @param {*} message - Message or data to log
100
+ * @param {...*} args - Additional arguments
101
+ */
102
+ debug(message, ...args) {
103
+ this._log('DEBUG', message, args, this.colors.blue);
104
+ }
105
+
106
+ /**
107
+ * Log with custom level
108
+ *
109
+ * @param {string} level - Log level
110
+ * @param {*} message - Message or data to log
111
+ * @param {...*} args - Additional arguments
112
+ */
113
+ log(level, message, ...args) {
114
+ this._log(level.toUpperCase(), message, args, this.colors.white);
115
+ }
116
+
117
+ /**
118
+ * Internal logging method
119
+ *
120
+ * @private
121
+ * @param {string} level - Log level
122
+ * @param {*} message - Message or data to log
123
+ * @param {Array} args - Additional arguments
124
+ * @param {string} color - Color code for output
125
+ */
126
+ _log(level, message, args, color) {
127
+ const timestamp = this.options.enableTimestamp ?
128
+ `[${new Date().toISOString()}] ` : '';
129
+
130
+ const levelStr = this.options.enableColors ?
131
+ `${color}[${level}]${this.colors.reset}` : `[${level}]`;
132
+
133
+ const sanitizedMessage = this._sanitize(message);
134
+ const sanitizedArgs = args.map(arg => this._sanitize(arg));
135
+
136
+ let output = `${timestamp}${levelStr} ${sanitizedMessage}`;
137
+
138
+ if (sanitizedArgs.length > 0) {
139
+ output += ' ' + sanitizedArgs.map(arg =>
140
+ typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
141
+ ).join(' ');
142
+ }
143
+
144
+ console.log(output);
145
+ }
146
+
147
+ /**
148
+ * Sanitize data by masking sensitive information
149
+ *
150
+ * @private
151
+ * @param {*} data - Data to sanitize
152
+ * @returns {*} Sanitized data
153
+ */
154
+ _sanitize(data) {
155
+ if (data === null || data === undefined) {
156
+ return data;
157
+ }
158
+
159
+ if (typeof data === 'string') {
160
+ return this._sanitizeString(data);
161
+ }
162
+
163
+ if (typeof data === 'object') {
164
+ if (Array.isArray(data)) {
165
+ return data.map(item => this._sanitize(item));
166
+ }
167
+
168
+ return this._sanitizeObject(data);
169
+ }
170
+
171
+ return data;
172
+ }
173
+
174
+ /**
175
+ * Sanitize string data
176
+ *
177
+ * @private
178
+ * @param {string} str - String to sanitize
179
+ * @returns {string} Sanitized string
180
+ */
181
+ _sanitizeString(str) {
182
+ if (str.trim().startsWith('{') || str.trim().startsWith('[')) {
183
+ try {
184
+ const parsed = JSON.parse(str);
185
+ const sanitized = this._sanitize(parsed);
186
+ return JSON.stringify(sanitized);
187
+ } catch {
188
+ }
189
+ }
190
+
191
+ const sensitivePatterns = [
192
+ /password[\s]*[:=][\s]*[^\s]+/gi,
193
+ /token[\s]*[:=][\s]*[^\s]+/gi,
194
+ /secret[\s]*[:=][\s]*[^\s]+/gi,
195
+ /apikey[\s]*[:=][\s]*[^\s]+/gi,
196
+ /api_key[\s]*[:=][\s]*[^\s]+/gi,
197
+ /bearer[\s]+[^\s]+/gi,
198
+ /authorization[\s]*[:=][\s]*[^\s]+/gi
199
+ ];
200
+
201
+ let sanitized = str;
202
+ for (const pattern of sensitivePatterns) {
203
+ sanitized = sanitized.replace(pattern, (match) => {
204
+ const parts = match.split(/[:=\s]+/);
205
+ return parts[0] + ': ' + this.options.maskValue;
206
+ });
207
+ }
208
+
209
+ return sanitized;
210
+ }
211
+
212
+ /**
213
+ * Sanitize object data recursively
214
+ *
215
+ * @private
216
+ * @param {Object} obj - Object to sanitize
217
+ * @returns {Object} Sanitized object
218
+ */
219
+ _sanitizeObject(obj) {
220
+ if (obj === null || typeof obj !== 'object') {
221
+ return obj;
222
+ }
223
+
224
+ const sanitized = Array.isArray(obj) ? [] : {};
225
+
226
+ for (const [key, value] of Object.entries(obj)) {
227
+ const lowerKey = key.toLowerCase();
228
+
229
+ const shouldMask = this.options.maskKeys.some(maskKey =>
230
+ lowerKey.includes(maskKey.toLowerCase())
231
+ );
232
+
233
+ if (shouldMask) {
234
+ sanitized[key] = this.options.maskValue;
235
+ } else {
236
+ sanitized[key] = this._sanitize(value);
237
+ }
238
+ }
239
+
240
+ return sanitized;
241
+ }
242
+
243
+ /**
244
+ * Add custom keys to mask
245
+ *
246
+ * @param {string|string[]} keys - Keys to add to masking list
247
+ */
248
+ addMaskKeys(keys) {
249
+ const newKeys = Array.isArray(keys) ? keys : [keys];
250
+ this.options.maskKeys.push(...newKeys);
251
+ }
252
+
253
+ /**
254
+ * Remove keys from masking list
255
+ *
256
+ * @param {string|string[]} keys - Keys to remove from masking list
257
+ */
258
+ removeMaskKeys(keys) {
259
+ const keysToRemove = Array.isArray(keys) ? keys : [keys];
260
+ this.options.maskKeys = this.options.maskKeys.filter(maskKey =>
261
+ !keysToRemove.some(removeKey =>
262
+ maskKey.toLowerCase() === removeKey.toLowerCase()
263
+ )
264
+ );
265
+ }
266
+ }
267
+
268
+ module.exports = SmartLogger;
@@ -0,0 +1,345 @@
1
+ /**
2
+ * SSLManager - Automated SSL Certificate Management for Cipher Shield
3
+ *
4
+ * Handles automated SSL certificate generation and renewal using Let's Encrypt ACME protocol.
5
+ * Supports HTTP-01 challenge automation, secure certificate storage, and auto-renewal.
6
+ *
7
+ * @module SSLManager
8
+ * @version 1.0.0
9
+ * @author Omindu Dissanayaka
10
+ * @license MIT
11
+ */
12
+
13
+ const acme = require('acme-client');
14
+ const forge = require('node-forge');
15
+ const fs = require('fs').promises;
16
+ const path = require('path');
17
+ const crypto = require('crypto');
18
+
19
+ class SSLManager {
20
+ /**
21
+ * Creates an SSL Manager instance
22
+ *
23
+ * @param {Object} config - SSL configuration
24
+ * @param {string} config.email - Email for Let's Encrypt account registration
25
+ * @param {string[]} config.domains - Array of domains to obtain certificates for
26
+ * @param {boolean} [config.staging=true] - Use Let's Encrypt staging environment
27
+ * @param {string} [config.certDir='./ssl-certs'] - Directory to store certificates
28
+ * @param {Object} [config.expressApp] - Express app instance for HTTP-01 challenge
29
+ */
30
+ constructor(config) {
31
+ this.config = {
32
+ email: config.email,
33
+ domains: Array.isArray(config.domains) ? config.domains : [config.domains],
34
+ staging: config.staging !== false,
35
+ certDir: config.certDir || path.join(process.cwd(), 'ssl-certs'),
36
+ expressApp: config.expressApp,
37
+ ...config
38
+ };
39
+
40
+ this.client = null;
41
+ this.accountKey = null;
42
+ this.certificates = new Map();
43
+ this.renewalInterval = null;
44
+
45
+ this.directoryUrl = this.config.staging
46
+ ? acme.directory.letsencrypt.staging
47
+ : acme.directory.letsencrypt.production;
48
+
49
+ this.logger = {
50
+ info: (msg) => console.log(`[SSLManager] ${msg}`),
51
+ error: (msg) => console.error(`[SSLManager] ${msg}`),
52
+ warn: (msg) => console.warn(`[SSLManager] ${msg}`)
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Initialize SSL Manager and start certificate management
58
+ */
59
+ async initialize() {
60
+ try {
61
+ this.logger.info('Initializing SSL Manager...');
62
+
63
+ await this.ensureCertDirectory();
64
+ await this.initializeACMEClient();
65
+ await this.loadExistingCertificates();
66
+ this.startAutoRenewal();
67
+ await this.obtainCertificates();
68
+
69
+ this.logger.info('SSL Manager initialized successfully');
70
+ } catch (error) {
71
+ this.logger.error(`SSL Manager initialization failed: ${error.message}`);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Ensure certificate directory exists
78
+ */
79
+ async ensureCertDirectory() {
80
+ try {
81
+ await fs.access(this.config.certDir);
82
+ } catch {
83
+ await fs.mkdir(this.config.certDir, { recursive: true });
84
+ this.logger.info(`Created certificate directory: ${this.config.certDir}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Initialize ACME client with account registration
90
+ */
91
+ async initializeACMEClient() {
92
+ try {
93
+ this.accountKey = await this.getOrCreateAccountKey();
94
+
95
+ this.client = new acme.Client({
96
+ directoryUrl: this.directoryUrl,
97
+ accountKey: this.accountKey
98
+ });
99
+
100
+ this.logger.info('ACME client initialized');
101
+ } catch (error) {
102
+ this.logger.error(`ACME client initialization failed: ${error.message}`);
103
+ throw error;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get or create account private key
109
+ */
110
+ async getOrCreateAccountKey() {
111
+ const accountKeyPath = path.join(this.config.certDir, 'account-key.pem');
112
+
113
+ try {
114
+ const keyData = await fs.readFile(accountKeyPath, 'utf8');
115
+ this.logger.info('Loaded existing account key');
116
+ return keyData;
117
+ } catch {
118
+ this.logger.info('Generating new account key...');
119
+ const { privateKey } = crypto.generateKeyPairSync('ec', {
120
+ namedCurve: 'P-256',
121
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
122
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
123
+ });
124
+
125
+ await fs.writeFile(accountKeyPath, privateKey, { mode: 0o600 });
126
+ this.logger.info('Account key saved');
127
+ return privateKey;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Load existing certificates from disk
133
+ */
134
+ async loadExistingCertificates() {
135
+ for (const domain of this.config.domains) {
136
+ try {
137
+ const certData = await this.loadCertificate(domain);
138
+ if (certData) {
139
+ this.certificates.set(domain, certData);
140
+ this.logger.info(`Loaded existing certificate for ${domain}`);
141
+ }
142
+ } catch (error) {
143
+ this.logger.warn(`Could not load certificate for ${domain}: ${error.message}`);
144
+ }
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Load certificate data for a domain
150
+ */
151
+ async loadCertificate(domain) {
152
+ const certPath = path.join(this.config.certDir, `${domain}.pem`);
153
+ const keyPath = path.join(this.config.certDir, `${domain}-key.pem`);
154
+
155
+ try {
156
+ const [cert, key] = await Promise.all([
157
+ fs.readFile(certPath, 'utf8'),
158
+ fs.readFile(keyPath, 'utf8')
159
+ ]);
160
+
161
+ return { cert, key, domain };
162
+ } catch {
163
+ return null;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Obtain certificates for all configured domains
169
+ */
170
+ async obtainCertificates() {
171
+ for (const domain of this.config.domains) {
172
+ try {
173
+ const existingCert = this.certificates.get(domain);
174
+ if (existingCert && !this.isCertificateExpiringSoon(existingCert.cert)) {
175
+ this.logger.info(`Certificate for ${domain} is still valid`);
176
+ continue;
177
+ }
178
+
179
+ this.logger.info(`Obtaining certificate for ${domain}...`);
180
+ const certData = await this.obtainCertificate(domain);
181
+ this.certificates.set(domain, certData);
182
+ await this.saveCertificate(certData);
183
+
184
+ this.logger.info(`Certificate obtained and saved for ${domain}`);
185
+ } catch (error) {
186
+ this.logger.error(`Failed to obtain certificate for ${domain}: ${error.message}`);
187
+ throw error;
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Obtain certificate for a specific domain
194
+ */
195
+ async obtainCertificate(domain) {
196
+ try {
197
+ const domainKey = crypto.generateKeyPairSync('rsa', {
198
+ modulusLength: 2048,
199
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
200
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
201
+ });
202
+
203
+ const [key, csr] = await acme.crypto.createCsr({
204
+ commonName: domain,
205
+ altNames: [domain]
206
+ });
207
+
208
+ const challengeHandler = this.createChallengeHandler(domain);
209
+
210
+ const certificate = await this.client.auto({
211
+ csr,
212
+ email: this.config.email,
213
+ termsOfServiceAgreed: true,
214
+ challengeCreateFn: challengeHandler.create,
215
+ challengeRemoveFn: challengeHandler.remove
216
+ });
217
+
218
+ return {
219
+ cert: certificate,
220
+ key: key,
221
+ domain
222
+ };
223
+ } catch (error) {
224
+ this.logger.error(`Certificate obtain failed for ${domain}: ${error.message}`);
225
+ throw error;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Create HTTP-01 challenge handler
231
+ */
232
+ createChallengeHandler(domain) {
233
+ const challenges = new Map();
234
+
235
+ return {
236
+ create: async (authz, challenge, keyAuthorization) => {
237
+ if (challenge.type === 'http-01') {
238
+ const challengePath = `/.well-known/acme-challenge/${challenge.token}`;
239
+
240
+ challenges.set(challenge.token, keyAuthorization);
241
+
242
+ if (this.config.expressApp) {
243
+ this.config.expressApp.get(challengePath, (req, res) => {
244
+ const token = req.path.split('/').pop();
245
+ const auth = challenges.get(token);
246
+ if (auth) {
247
+ res.set('Content-Type', 'text/plain');
248
+ res.send(auth);
249
+ } else {
250
+ res.status(404).send('Challenge not found');
251
+ }
252
+ });
253
+
254
+ this.logger.info(`HTTP-01 challenge route setup for ${domain}: ${challengePath}`);
255
+ } else {
256
+ this.logger.warn('No Express app provided - HTTP-01 challenge will not be served automatically');
257
+ }
258
+ }
259
+ },
260
+
261
+ remove: async (authz, challenge) => {
262
+ if (challenge.type === 'http-01') {
263
+ challenges.delete(challenge.token);
264
+ this.logger.info(`HTTP-01 challenge removed for ${domain}`);
265
+ }
266
+ }
267
+ };
268
+ }
269
+
270
+ isCertificateExpiringSoon(certPem) {
271
+ try {
272
+ const cert = forge.pki.certificateFromPem(certPem);
273
+ const now = new Date();
274
+ const expiry = cert.validity.notAfter;
275
+ const daysUntilExpiry = (expiry - now) / (1000 * 60 * 60 * 24);
276
+
277
+ return daysUntilExpiry < 30;
278
+ } catch {
279
+ return true;
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Save certificate to disk
285
+ */
286
+ async saveCertificate(certData) {
287
+ const certPath = path.join(this.config.certDir, `${certData.domain}.pem`);
288
+ const keyPath = path.join(this.config.certDir, `${certData.domain}-key.pem`);
289
+
290
+ await Promise.all([
291
+ fs.writeFile(certPath, certData.cert, { mode: 0o644 }),
292
+ fs.writeFile(keyPath, certData.key, { mode: 0o600 })
293
+ ]);
294
+
295
+ this.logger.info(`Certificate saved for ${certData.domain}`);
296
+ }
297
+
298
+ /**
299
+ * Start auto-renewal process
300
+ */
301
+ startAutoRenewal() {
302
+ this.renewalInterval = setInterval(async () => {
303
+ try {
304
+ this.logger.info('Checking certificates for renewal...');
305
+ await this.obtainCertificates();
306
+ } catch (error) {
307
+ this.logger.error(`Auto-renewal check failed: ${error.message}`);
308
+ }
309
+ }, 24 * 60 * 60 * 1000);
310
+
311
+ this.logger.info('Auto-renewal process started (checks every 24 hours)');
312
+ }
313
+
314
+ stopAutoRenewal() {
315
+ if (this.renewalInterval) {
316
+ clearInterval(this.renewalInterval);
317
+ this.renewalInterval = null;
318
+ this.logger.info('Auto-renewal process stopped');
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Get certificate for domain
324
+ */
325
+ getCertificate(domain) {
326
+ return this.certificates.get(domain) || null;
327
+ }
328
+
329
+ /**
330
+ * Get all certificates
331
+ */
332
+ getAllCertificates() {
333
+ return Array.from(this.certificates.values());
334
+ }
335
+
336
+ /**
337
+ * Shutdown SSL Manager
338
+ */
339
+ async shutdown() {
340
+ this.stopAutoRenewal();
341
+ this.logger.info('SSL Manager shutdown complete');
342
+ }
343
+ }
344
+
345
+ module.exports = SSLManager;