i18ntk 2.4.0 → 2.5.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.
@@ -1,340 +0,0 @@
1
- const crypto = require('crypto');
2
- const fs = require('fs');
3
- const fsp = fs.promises;
4
- const path = require('path');
5
- const zlib = require('zlib');
6
- const { promisify } = require('util');
7
- const { existsSync, mkdirSync } = require('fs');
8
- const { EncryptionError, ValidationError } = require('./secure-errors');
9
-
10
- // Promisify functions
11
- const gzip = promisify(zlib.gzip);
12
- const gunzip = promisify(zlib.gunzip);
13
-
14
- // Constants
15
- const SALT_LENGTH = 32;
16
- const IV_LENGTH = 16;
17
- const KEY_LENGTH = 32;
18
- const ITERATIONS = 100000;
19
- const ALGORITHM = 'aes-256-gcm';
20
- const BACKUP_HEADER = 'I18NTK_BACKUP';
21
- const BACKUP_VERSION = 1;
22
-
23
- class SecureBackupManager {
24
- constructor(config = {}) {
25
- this.config = {
26
- backupDir: path.join(process.cwd(), 'backups'),
27
- maxBackups: 10,
28
- compress: true,
29
- ...config
30
- };
31
-
32
- // Create backup directory synchronously in constructor
33
- if (!existsSync(this.config.backupDir)) {
34
- mkdirSync(this.config.backupDir, { recursive: true });
35
- }
36
- }
37
-
38
- /**
39
- * Derive a key from a password using PBKDF2
40
- */
41
- async deriveKey(password, salt) {
42
- return new Promise((resolve, reject) => {
43
- crypto.pbkdf2(
44
- password,
45
- salt,
46
- ITERATIONS,
47
- KEY_LENGTH,
48
- 'sha512',
49
- (err, derivedKey) => {
50
- if (err) {
51
- reject(new EncryptionError('Key derivation failed', { error: err.message }));
52
- } else {
53
- resolve(derivedKey);
54
- }
55
- }
56
- );
57
- });
58
- }
59
-
60
- /**
61
- * Generate a random salt
62
- */
63
- generateSalt() {
64
- return crypto.randomBytes(SALT_LENGTH);
65
- }
66
-
67
- /**
68
- * Encrypt data with a password
69
- */
70
- async encryptData(data, password) {
71
- try {
72
- // Generate a random salt
73
- const salt = this.generateSalt();
74
-
75
- // Derive key from password
76
- const key = await this.deriveKey(password, salt);
77
-
78
- // Generate a random IV
79
- const iv = crypto.randomBytes(IV_LENGTH);
80
-
81
- // Create cipher
82
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
83
-
84
- // Encrypt the data
85
- let encrypted = cipher.update(data, 'utf8', 'hex');
86
- encrypted += cipher.final('hex');
87
-
88
- // Get the auth tag
89
- const authTag = cipher.getAuthTag();
90
-
91
- // Return the encrypted data with metadata
92
- return {
93
- encrypted,
94
- iv: iv.toString('hex'),
95
- salt: salt.toString('hex'),
96
- authTag: authTag.toString('hex'),
97
- algorithm: ALGORITHM,
98
- version: BACKUP_VERSION
99
- };
100
- } catch (error) {
101
- throw new EncryptionError('Encryption failed', { cause: error });
102
- }
103
- }
104
-
105
- /**
106
- * Decrypt data with a password
107
- */
108
- async decryptData(encryptedData, password) {
109
- try {
110
- // Parse the encrypted data
111
- const { encrypted, iv, salt, authTag, version } = encryptedData;
112
-
113
- // Validate version
114
- if (version !== BACKUP_VERSION) {
115
- throw new ValidationError('Unsupported backup version');
116
- }
117
-
118
- // Convert from hex
119
- const ivBuffer = Buffer.from(iv, 'hex');
120
- const saltBuffer = Buffer.from(salt, 'hex');
121
- const authTagBuffer = Buffer.from(authTag, 'hex');
122
-
123
- // Derive the key
124
- const key = await this.deriveKey(password, saltBuffer);
125
-
126
- // Create decipher
127
- const decipher = crypto.createDecipheriv(ALGORITHM, key, ivBuffer);
128
- decipher.setAuthTag(authTagBuffer);
129
-
130
- // Decrypt the data
131
- let decrypted = decipher.update(encrypted, 'hex', 'utf8');
132
- decrypted += decipher.final('utf8');
133
-
134
- return decrypted;
135
- } catch (error) {
136
- throw new EncryptionError('Decryption failed', { cause: error });
137
- }
138
- }
139
-
140
- /**
141
- * Create a secure backup
142
- */
143
- async createBackup(data, password, options = {}) {
144
- try {
145
- // Validate input
146
- if (!data) {
147
- throw new ValidationError('No data provided for backup');
148
- }
149
-
150
- if (!password) {
151
- throw new ValidationError('Password is required for backup');
152
- }
153
-
154
- // Stringify data if it's an object
155
- const dataString = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
156
-
157
- // Compress the data if enabled
158
- let processedData = dataString;
159
- if (this.config.compress) {
160
- processedData = await gzip(dataString);
161
- }
162
-
163
- // Encrypt the data
164
- const encryptedData = await this.encryptData(
165
- processedData.toString('base64'),
166
- password
167
- );
168
-
169
- // Add metadata
170
- const backupData = {
171
- header: BACKUP_HEADER,
172
- version: BACKUP_VERSION,
173
- timestamp: new Date().toISOString(),
174
- compressed: this.config.compress,
175
- ...encryptedData
176
- };
177
-
178
- // Create backup filename with timestamp
179
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
180
- const backupName = `backup-${timestamp}.i18ntk`;
181
- const backupPath = path.join(this.config.backupDir, backupName);
182
-
183
- // Write the backup file
184
- await fsp.writeFile(backupPath, JSON.stringify(backupData, null, 2), 'utf8');
185
-
186
- // Clean up old backups if we've exceeded the limit
187
- await this.cleanupOldBackups();
188
-
189
- return {
190
- success: true,
191
- backupPath,
192
- backupName,
193
- timestamp: backupData.timestamp,
194
- size: JSON.stringify(backupData).length
195
- };
196
- } catch (error) {
197
- throw new EncryptionError('Backup creation failed', { cause: error });
198
- }
199
- }
200
-
201
- /**
202
- * Restore a backup
203
- */
204
- async restoreBackup(backupPath, password) {
205
- try {
206
- // Read the backup file
207
- const backupData = JSON.parse(await fsp.readFile(backupPath, 'utf8'));
208
-
209
- // Validate the backup
210
- if (backupData.header !== BACKUP_HEADER) {
211
- throw new ValidationError('Invalid backup file');
212
- }
213
-
214
- // Decrypt the data
215
- const decryptedData = await this.decryptData(backupData, password);
216
-
217
- // Decompress if needed
218
- let processedData = Buffer.from(decryptedData, 'base64');
219
- if (backupData.compressed) {
220
- processedData = await gunzip(processedData);
221
- }
222
-
223
- // Parse the data if it's JSON
224
- try {
225
- return JSON.parse(processedData.toString('utf8'));
226
- } catch {
227
- return processedData.toString('utf8');
228
- }
229
- } catch (error) {
230
- throw new EncryptionError('Backup restoration failed', { cause: error });
231
- }
232
- }
233
-
234
- /**
235
- * List available backups
236
- */
237
- async listBackups() {
238
- try {
239
- // Read the backup directory
240
- const files = (await fsp.readdir(this.config.backupDir, { withFileTypes: true }))
241
- .filter(dirent => dirent.isFile())
242
- .map(dirent => dirent.name);
243
-
244
- // Filter for backup files
245
- const backupFiles = files.filter(file => file.endsWith('.i18ntk'));
246
-
247
- // Get file stats
248
- const backups = await Promise.all(
249
- backupFiles.map(async (file) => {
250
- const filePath = path.join(this.config.backupDir, file);
251
- const stat = await fsp.stat(filePath);
252
- if (stat.isDirectory()) {
253
- throw new Error('Backup path is a directory');
254
- }
255
- return {
256
- name: file,
257
- path: filePath,
258
- size: stat.size,
259
- createdAt: stat.birthtime,
260
- modifiedAt: stat.mtime
261
- };
262
- })
263
- );
264
-
265
- // Sort by creation date (newest first)
266
- return backups.sort((a, b) => b.createdAt - a.createdAt);
267
- } catch (error) {
268
- throw new Error(`Failed to list backups: ${error.message}`);
269
- }
270
- }
271
-
272
- /**
273
- * Clean up old backups
274
- */
275
- async cleanupOldBackups() {
276
- try {
277
- const backups = await this.listBackups();
278
-
279
- // If we haven't exceeded the limit, do nothing
280
- if (backups.length <= this.config.maxBackups) {
281
- return [];
282
- }
283
-
284
- // Sort by creation date (oldest first)
285
- const sortedBackups = [...backups].sort((a, b) => a.createdAt - b.createdAt);
286
-
287
- // Determine how many to delete
288
- const toDelete = sortedBackups.slice(0, backups.length - this.config.maxBackups);
289
-
290
- // Delete the old backups
291
- const deleted = [];
292
- for (const backup of toDelete) {
293
- try {
294
- await fsp.unlink(backup.path);
295
- deleted.push(backup.name);
296
- } catch (error) {
297
- console.error(`Failed to delete backup ${backup.name}:`, error);
298
- }
299
- }
300
-
301
- return deleted;
302
- } catch (error) {
303
- console.error('Failed to clean up old backups:', error);
304
- return [];
305
- }
306
- }
307
-
308
- /**
309
- * Verify a backup password
310
- */
311
- async verifyBackup(backupPath, password) {
312
- try {
313
- // Read the backup file
314
- const backupData = JSON.parse(await fsp.readFile(backupPath, 'utf8'));
315
-
316
- // Try to decrypt a small part to verify the password
317
- const testData = await this.decryptData(backupData, password);
318
-
319
- // If we got here, the password is correct
320
- return {
321
- valid: true,
322
- timestamp: backupData.timestamp,
323
- compressed: backupData.compressed
324
- };
325
- } catch (error) {
326
- // If decryption failed, the password is likely incorrect
327
- if (error instanceof EncryptionError) {
328
- return { valid: false, reason: 'Invalid password or corrupted backup' };
329
- }
330
-
331
- // For other errors, rethrow
332
- throw error;
333
- }
334
- }
335
- }
336
-
337
- module.exports = {
338
- SecureBackupManager,
339
- createBackupManager: (config) => new SecureBackupManager(config)
340
- };