spd-lib 1.2.2 → 1.2.4

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/src/index.js ADDED
@@ -0,0 +1,972 @@
1
+ const fs = require('fs')
2
+ const zlib = require('zlib');
3
+ const sodium = require('libsodium-wrappers');
4
+ const crypto = require('crypto');
5
+ const argon2 = require('argon2');
6
+
7
+
8
+ class SPD {
9
+
10
+
11
+ constructor() {
12
+ this.data = [];
13
+ this.keyPair; // Generate a key pair for encryption/decryption
14
+ this.userKey;
15
+ this.salt;
16
+ this.hash = 'sha3-512'
17
+ this.compression_level = 9
18
+
19
+ }
20
+
21
+ async init() {
22
+ await sodium.ready;
23
+
24
+ }
25
+ async changePasscode(oldPasscode, newPasscode) {
26
+ if (!oldPasscode || !newPasscode) {
27
+ throw new Error('Old and new passcodes must be provided.');
28
+ }
29
+
30
+ // Ensure sodium is ready
31
+ await sodium.ready;
32
+
33
+ // Step 1: Derive key from old passcode and check MAC
34
+ const { pqcKey: oldPQCKey, salt: currentSalt } = await this.convertPasscodeToPQCKeySalted(oldPasscode, this.salt);
35
+ const oldUserKey = oldPQCKey.privateKey;
36
+
37
+ // Rebuild MAC to verify old passcode
38
+ const spdPayload = { data: this.data, salt: Array.from(this.salt) };
39
+ const spdBuffer = Buffer.from(JSON.stringify(spdPayload), 'utf8');
40
+ const expectedMac = crypto.createHmac('sha512', oldUserKey).update(spdBuffer).digest();
41
+
42
+ const currentMac = this.generateMAC(spdBuffer, this.userKey);
43
+ if (!crypto.timingSafeEqual(expectedMac, currentMac)) {
44
+ throw new Error('Old passcode is incorrect or data integrity check failed.');
45
+ }
46
+
47
+ // Step 2: Decrypt everything with old key
48
+ const decryptedEntries = await Promise.all(
49
+ this.data.map(async dat => {
50
+ const decrypted = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
51
+ Buffer.from(dat.data),
52
+ Buffer.from(dat.nonce),
53
+ oldUserKey
54
+ );
55
+ const decompressed = zlib.inflateSync(decrypted);
56
+ const originalData = decompressed.toString('utf8');
57
+ return {
58
+ name: dat.dataName,
59
+ parsed: await this.CSTI(originalData, dat.dataType),
60
+ type: dat.dataType
61
+ };
62
+ })
63
+ );
64
+
65
+ // Step 3: Generate new salt + key
66
+ const { pqcKey: newPQCKey, salt: newSalt } = await this.convertPasscodeToPQCKey(newPasscode);
67
+ const newUserKey = newPQCKey.privateKey;
68
+
69
+ // Step 4: Re-encrypt data
70
+ this.salt = newSalt;
71
+ this.userKey = newUserKey;
72
+ this.data = [];
73
+
74
+ for (const entry of decryptedEntries) {
75
+ await this.addData(entry.name, entry.parsed);
76
+ }
77
+
78
+ // Done: data is now re-encrypted under the new key
79
+ }
80
+ generateMAC(dataBuffer, key, meta = {}) {
81
+ const macInput = Buffer.concat([
82
+ dataBuffer,
83
+ Buffer.from(meta.dataName || '', 'utf8'),
84
+ Buffer.from(meta.dataType || '', 'utf8')
85
+ ]);
86
+ return crypto.createHmac('sha512', key).update(macInput).digest();
87
+ }
88
+
89
+ checkPasscodeStrength(passcode) {
90
+ const minLength = 12;
91
+ const entropyChecks = [
92
+ /[a-z]/, // lowercase
93
+ /[A-Z]/, // uppercase
94
+ /[0-9]/, // digits
95
+ /[^A-Za-z0-9]/ // special characters
96
+ ];
97
+
98
+ if (typeof passcode !== 'string' || passcode.length < minLength) {
99
+ throw new Error(`Passcode must be at least ${minLength} characters long.`);
100
+ }
101
+
102
+ const failed = entropyChecks.filter(regex => !regex.test(passcode));
103
+ if (failed.length > 1) {
104
+ throw new Error('Passcode must contain at least three of the following: lowercase letters, uppercase letters, digits, special characters.');
105
+ }
106
+
107
+ return true;
108
+ }
109
+ #passcode
110
+ async setPassKey(passcode){
111
+ this.checkPasscodeStrength(passcode);
112
+ await sodium.ready;
113
+ await this.init()
114
+ const { pqcKey, salt } = await this.convertPasscodeToPQCKey(passcode);
115
+ const userKey = pqcKey.privateKey;
116
+ this.userKey = userKey;
117
+ this.salt = salt;
118
+ }
119
+
120
+
121
+ setHash(hash="sha3-512")
122
+ {
123
+ this.hash = hash
124
+ }
125
+ setCompressionLevel(level=9)
126
+ {
127
+ this.compression_level = level
128
+ }
129
+
130
+ getSodium(){
131
+ return sodium
132
+ }
133
+
134
+ sanitizeName(name) {
135
+ if (typeof name !== 'string') throw new Error('dataName must be a string');
136
+
137
+ // Normalize Unicode, lowercase, trim, and remove unsafe characters
138
+ return name
139
+ .normalize('NFKC') // Normalize full-width, diacritics, etc.
140
+ .trim()
141
+ .toLowerCase()
142
+ .replace(/[^a-z0-9_\-]/g, '_'); // Keep a-z, 0-9, underscore, dash
143
+ }
144
+
145
+
146
+ async addData(name, data) {
147
+
148
+
149
+ const dmap = await this.CITS(data)
150
+ await sodium.ready;
151
+ const dat = Buffer.from(dmap[0]);
152
+ const compressedData = zlib.deflateSync(dat,{
153
+ level:this.compression_level
154
+ });
155
+ const safeName = this.sanitizeName(name);
156
+
157
+ const nonce = sodium.randombytes_buf(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
158
+
159
+
160
+
161
+ const encryptedData = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
162
+ compressedData,
163
+ null,
164
+ null,
165
+ nonce,
166
+ this.userKey
167
+ )
168
+ //sodium.crypto_secretbox_easy(compressedData, nonce, this.userKey);
169
+ const hash = crypto.createHash(this.hash).update(encryptedData).digest('hex');
170
+
171
+
172
+ this.data.push({ dataName: name, nonce: Buffer.from(nonce), data: Buffer.from(encryptedData), hash, dataType: dmap[1] });
173
+ }
174
+
175
+
176
+
177
+ async addMany(dataItems) {
178
+ await sodium.ready;
179
+
180
+ const tasks = dataItems.map(async ({ name, data }) => {
181
+ const dmap = await this.CITS(data);
182
+ const dat = Buffer.from(dmap[0]);
183
+ const compressedData = zlib.deflateSync(dat, { level: this.compression_level });
184
+ const nonce = sodium.randombytes_buf(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
185
+
186
+ const encryptedData = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(compressedData, null, null, nonce, this.userKey);
187
+ const hash = crypto.createHash(this.hash).update(encryptedData).digest('hex');
188
+
189
+ return {
190
+ dataName: name,
191
+ nonce: Array.from(nonce),
192
+ data: Array.from(encryptedData),
193
+ hash,
194
+ dataType: dmap[1],
195
+ };
196
+ });
197
+
198
+ const results = await Promise.all(tasks);
199
+ this.data.push(...results);
200
+ }
201
+
202
+ clearCache(){
203
+ this.data = []
204
+ this.salt = null
205
+ this.userKey = null
206
+ }
207
+
208
+ async saveToFile(outputPath,passcode) {
209
+ if (
210
+ typeof outputPath !== 'string' ||
211
+ !outputPath.trim()
212
+ ) {
213
+ throw new Error('Invalid output path.');
214
+ }
215
+
216
+ const { encryptedSalt, saltNonce, wrapSalt } = await SPD.encryptSalt(this.salt, passcode);
217
+
218
+ const spdPayload = {
219
+ data: this.data,
220
+ encryptedSalt,
221
+ saltNonce,
222
+ wrapSalt,
223
+ version:24
224
+ };
225
+
226
+ const spdDataJSON = JSON.stringify(spdPayload);
227
+ const spdBuffer = Buffer.from(spdDataJSON, 'utf8');
228
+ const mac = this.generateMAC(spdBuffer, this.userKey, {
229
+ dataName: '', // Entire dataset, use blank or static label
230
+ dataType: 'application/json' // Or whatever label you want for top-level
231
+ });
232
+
233
+
234
+
235
+ const finalObject = {
236
+ payload: Buffer.from(spdBuffer),
237
+ mac: Buffer.from(mac),
238
+
239
+ };
240
+
241
+ const finalBuffer = zlib.deflateSync(Buffer.from(JSON.stringify(finalObject)), {
242
+ level: this.compression_level
243
+ });
244
+
245
+ fs.writeFileSync(outputPath, finalBuffer, { mode: 0o600 });
246
+ }
247
+
248
+
249
+ static async loadFromFile(spdPath, passcode,hash='sha3-512',compression_level=9) {
250
+ try{
251
+ if (!spdPath || typeof spdPath !== 'string' || !spdPath.trim() || !passcode || typeof passcode !== 'string' || !passcode.trim()) {
252
+ throw new Error('Invalid SPD path or passcode.')
253
+ }
254
+
255
+ await sodium.ready;
256
+
257
+ const compressedSpdData = fs.readFileSync(spdPath);
258
+ const spdData = zlib.inflateSync(compressedSpdData,{level:compression_level}).toString('utf8');
259
+
260
+ const { payload,mac } = JSON.parse(spdData);
261
+ const spdBuffer = Buffer.from(payload);
262
+ const t = new SPD()
263
+
264
+
265
+ const { data, encryptedSalt, saltNonce, wrapSalt,version } = JSON.parse(Buffer.from(payload).toString("utf-8"));
266
+ if (typeof version !== 'number' || version !== 24) {
267
+ throw new Error(`Unsupported SPD version: ${version}`);
268
+ }
269
+ const salt = await SPD.decryptSalt(encryptedSalt, saltNonce, wrapSalt, passcode);
270
+ const { pqcKey } = await t.convertPasscodeToPQCKey(passcode, new Uint8Array(salt));
271
+ const pbk = pqcKey.privateKey;
272
+ const expectedMac = crypto.createHmac('sha512', pbk).update(Buffer.concat([
273
+ spdBuffer,
274
+ Buffer.from('', 'utf8'),
275
+ Buffer.from('application/json', 'utf8')
276
+ ])).digest();
277
+
278
+ if (!crypto.timingSafeEqual(Buffer.from(mac), expectedMac)) {
279
+ throw new Error('MAC verification failed — data may be tampered with.');
280
+ }
281
+
282
+
283
+ const spd = new SPD();
284
+ spd.setHash(hash)
285
+ spd.setCompressionLevel(compression_level)
286
+ spd.userKey = pbk;
287
+ spd.keyPair = {
288
+ publicKey: pbk.publicKey
289
+ };
290
+ spd.data = data.map(dat => ({
291
+ dataName: dat.dataName,
292
+ nonce: Buffer.from(dat.nonce),
293
+ data: Buffer.from(dat.data),
294
+ hash: dat.hash,
295
+ dataType: dat.dataType
296
+ }));
297
+ await Promise.all(spd.data.map(dat => {
298
+ const calculatedHash = crypto.createHash(hash).update(Buffer.from(dat.data)).digest('hex');
299
+ if (calculatedHash !== dat.hash) {
300
+ throw new Error(`Data integrity check failed for ${dat.dataName}`);
301
+ }
302
+ }));
303
+
304
+ return spd;
305
+ }catch(err){
306
+ throw new Error(err)
307
+ }
308
+
309
+ }
310
+
311
+ async extractData() {
312
+ await sodium.ready;
313
+
314
+ const tasks = this.data.map(async dat => {
315
+ try {
316
+ const decryptedData = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null,dat.data,null,dat.nonce, this.userKey);
317
+ const decompressedData = zlib.inflateSync(decryptedData, { level: this.compression_level });
318
+ const dt = decompressedData.toString('utf8');
319
+ const parsed = await this.CSTI(dt, dat.dataType);
320
+ return [dat.dataName, parsed];
321
+ } catch (err) {
322
+ throw new Error(`Failed to process ${dat.dataName}: ${err.message}`);
323
+ }
324
+ });
325
+
326
+ const results = await Promise.all(tasks);
327
+ return Object.fromEntries(results);
328
+ }
329
+
330
+
331
+
332
+
333
+ static async derivePBK(passcode, salt) {
334
+ if (!salt || !(salt instanceof Uint8Array) || salt.length !== 16) {
335
+ throw new Error('Invalid salt.');
336
+ }
337
+ if (!passcode || typeof passcode !== 'string' || !salt || !(salt instanceof Uint8Array) || salt.length !== 16) {
338
+ throw new Error('Invalid passcode or salt.');
339
+ }
340
+
341
+
342
+ // 2. Hash again with SHA-512 to get deterministic binary seed material
343
+ const derivedKey = await argon2.hash(passcode, {
344
+ salt: Buffer.from(salt),
345
+ type: argon2.argon2id,
346
+ raw: true, // <- raw output as Buffer
347
+ memoryCost: 2 ** 16, // 64 MB
348
+ timeCost: 5,
349
+ parallelism: 1,
350
+ hashLength: sodium.crypto_kx_SEEDBYTES // usually 32 bytes
351
+ });
352
+ // 3. Slice securely to get the 32-byte seed for libsodium
353
+ const seed = derivedKey.slice(0, sodium.crypto_kx_SEEDBYTES);
354
+
355
+ return { pbk: seed, salt };
356
+ }
357
+
358
+ async saveData(passcode="") {
359
+ const { encryptedSalt, saltNonce, wrapSalt } = await SPD.encryptSalt(this.salt, passcode);
360
+ const spdPayload = {
361
+ data: this.data,
362
+ encryptedSalt,
363
+ saltNonce,
364
+ wrapSalt,
365
+ version: 24
366
+ };
367
+
368
+ const spdDataJSON = JSON.stringify(spdPayload);
369
+ const spdBuffer = Buffer.from(spdDataJSON, 'utf8');
370
+ const mac = this.generateMAC(spdBuffer, this.userKey, {
371
+ dataName: '', // Entire dataset, use blank or static label
372
+ dataType: 'application/json' // Or whatever label you want for top-level
373
+ });
374
+
375
+ const finalObject = {
376
+ payload: Buffer.from(spdBuffer),
377
+ mac: Buffer.from(mac),
378
+ };
379
+
380
+ const compressedSpdData = zlib.deflateSync(Buffer.from(JSON.stringify(finalObject)), {
381
+ level: this.compression_level
382
+ });
383
+
384
+ return compressedSpdData.toString('base64');
385
+ }
386
+ static async decryptSalt(encryptedSalt, saltNonce, wrapSalt, passcode) {
387
+
388
+ const { pbk } = await SPD.derivePBK(passcode, new Uint8Array(wrapSalt));
389
+ await sodium.ready;
390
+
391
+ const salt = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null,
392
+ new Uint8Array(encryptedSalt),
393
+ null,
394
+ new Uint8Array(saltNonce),
395
+ new Uint8Array(pbk)
396
+ );
397
+ if (!salt) throw new Error('Failed to decrypt salt.');
398
+ return salt;
399
+ }
400
+
401
+ static async encryptSalt(salt, passcode) {
402
+ const wrapSalt = crypto.getRandomValues(new Uint8Array(16));
403
+ const { pbk } = await SPD.derivePBK(passcode, wrapSalt);
404
+ await sodium.ready;
405
+
406
+ const nonce = sodium.randombytes_buf(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
407
+ const encryptedSalt = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(salt, null,null, nonce, new Uint8Array(pbk));
408
+
409
+ return {
410
+ encryptedSalt: Array.from(encryptedSalt),
411
+ saltNonce: Array.from(nonce),
412
+ wrapSalt: Array.from(wrapSalt)
413
+ };
414
+ }
415
+
416
+
417
+ static async loadFromString(spdData, passcode,hash='sha3-512',compression_level=9) {
418
+ try{
419
+
420
+ if (!spdData || typeof spdData !== 'string' || !spdData.trim() || !passcode || typeof passcode !== 'string' || !passcode.trim()) {
421
+ throw new Error('Invalid SPD path or passcode.')
422
+ }
423
+
424
+ await sodium.ready;
425
+ const spdDataBuffer = Buffer.from(spdData, 'base64');
426
+ const spdData2 = zlib.inflateSync(spdDataBuffer,{level:compression_level}).toString('utf8');
427
+ const { payload, mac } = JSON.parse(spdData2);
428
+ const { data, encryptedSalt, saltNonce, wrapSalt,version } = JSON.parse(Buffer.from(payload).toString("utf-8"));
429
+ const salt = await SPD.decryptSalt(encryptedSalt, saltNonce, wrapSalt, passcode);
430
+ if (typeof version !== 'number' || version !== 24) {
431
+ throw new Error(`Unsupported SPD version: ${version}`);
432
+ }
433
+ const spdBuffer = Buffer.from(payload);
434
+ const t = new SPD()
435
+
436
+ const { pqcKey } = await t.convertPasscodeToPQCKey(passcode, new Uint8Array(salt));
437
+ const pbk = pqcKey.privateKey;
438
+
439
+
440
+ const expectedMac = crypto.createHmac('sha512', pbk).update(Buffer.concat([
441
+ spdBuffer,
442
+ Buffer.from('', 'utf8'),
443
+ Buffer.from('application/json', 'utf8')
444
+ ])).digest();
445
+
446
+ if (!crypto.timingSafeEqual(Buffer.from(mac), expectedMac)) {
447
+ throw new Error('MAC verification failed — data may be tampered with.');
448
+ }
449
+
450
+ const spd = new SPD();
451
+ spd.setHash(hash)
452
+ spd.setCompressionLevel(compression_level)
453
+ spd.userKey = pbk;
454
+ spd.keyPair = {
455
+ publicKey: pbk.publicKey
456
+ };
457
+ spd.data = data.map(dat => ({
458
+ dataName: dat.dataName,
459
+ nonce: Buffer.from(dat.nonce),
460
+ data: Buffer.from(dat.data),
461
+ hash: dat.hash,
462
+ dataType: dat.dataType
463
+ }));
464
+ await Promise.all(spd.data.map(dat => {
465
+ const calculatedHash = crypto.createHash(hash).update(Buffer.from(dat.data)).digest('hex');
466
+ if (calculatedHash !== dat.hash) {
467
+ throw new Error(`Data integrity check failed for ${dat.dataName}`);
468
+ }
469
+ }));
470
+ return spd;
471
+
472
+ }catch(err){
473
+ throw new Error(err)
474
+ }
475
+
476
+
477
+ }
478
+
479
+ static toBase64(buffer) {
480
+ return Buffer.from(buffer).toString('base64');
481
+ }
482
+
483
+ static fromBase64(str) {
484
+ return new Uint8Array(Buffer.from(str, 'base64'));
485
+ }
486
+
487
+
488
+ async convertPasscodeToPQCKey(passcode, salt = crypto.getRandomValues(new Uint8Array(16))) {
489
+ if (typeof passcode !== 'string' || passcode.length < 8)
490
+ throw new Error('Invalid passcode.');
491
+ const { pbk } = await SPD.derivePBK(passcode, salt);
492
+ await this.init();
493
+ const keyPair = sodium.crypto_kx_seed_keypair(pbk);
494
+ sodium.memzero(pbk);
495
+ return { pqcKey: { privateKey: keyPair.privateKey }, salt };
496
+ }
497
+ async TDT(data) {
498
+ const classTypeMap = {
499
+ '[object Array]': 'Array',
500
+ '[object Uint8Array]': 'Uint8Array',
501
+ '[object Uint16Array]': 'Uint16Array',
502
+ '[object Uint32Array]': 'Uint32Array',
503
+ '[object BigInt64Array]': 'BigInt64Array',
504
+ '[object BigUint64Array]': 'BigUint64Array',
505
+ '[object Float32Array]': 'Float32Array',
506
+ '[object Float64Array]': 'Float64Array',
507
+ '[object Map]': 'Map',
508
+ '[object Set]': 'Set',
509
+ '[object Date]': 'Date',
510
+ '[object RegExp]': 'RegExp',
511
+ '[object Error]': 'Error'
512
+ };
513
+
514
+ const objectType = Object.prototype.toString.call(data);
515
+ const mappedType = classTypeMap[objectType];
516
+ return mappedType;
517
+ }
518
+ async isNumArr(dataType) {
519
+ if(dataType === 'Uint8Array' || dataType === 'Uint16Array' || dataType === 'Uint32Array' || dataType === 'BigInt64Array' || dataType === 'BigUint64Array' || dataType === 'Float32Array' || dataType === 'Float64Array'){
520
+ return true;
521
+ }
522
+ }
523
+ async isSWM(dataType) {
524
+ if(dataType === 'Map' || dataType === 'Set' || dataType === 'WeakMap' || dataType === 'WeakSet'){
525
+ return true;
526
+ }
527
+ }
528
+ async isDRE(dataType) {
529
+ if(dataType === 'Date' || dataType === 'RegExp' || dataType === 'Error'){
530
+ return true;
531
+ }
532
+ }
533
+ async CITS(data) {
534
+ const dataType = typeof data;
535
+ if (dataType === 'string' || dataType === 'number' || dataType === 'boolean') {
536
+ return [data.toString(), dataType];
537
+ }
538
+ if(typeof data === 'object'){
539
+ const type = await this.TDT(data)
540
+ if(type === 'Array'){
541
+ return [JSON.stringify(data),'Array'];
542
+ }
543
+ if(await this.isNumArr(type)){
544
+ return [JSON.stringify(Array.from(data)),type];
545
+ }
546
+ if(await this.isSWM(type)){
547
+
548
+ return [JSON.stringify([...data]),type];
549
+ }
550
+ if(await this.isDRE(type)){
551
+ return [data.toString(),type];
552
+ }
553
+ return [JSON.stringify(data), typeof data];
554
+ }
555
+ }
556
+ async CSTI(data, type) {
557
+ try {
558
+ switch (type) {
559
+ case 'string':
560
+ if (typeof data !== 'string') throw new Error('Expected string');
561
+ return data;
562
+
563
+ case 'number':
564
+ const num = parseFloat(data);
565
+ if (isNaN(num)) throw new Error('Invalid number');
566
+ return num;
567
+
568
+ case 'boolean':
569
+ if (data !== 'true' && data !== 'false') throw new Error('Invalid boolean');
570
+ return data === 'true';
571
+
572
+ case 'Array':
573
+ case 'object':
574
+ const obj = JSON.parse(data);
575
+ if (typeof obj !== 'object' || obj === null) throw new Error('Invalid object');
576
+ return obj;
577
+
578
+ case 'Uint8Array':
579
+ return new Uint8Array(JSON.parse(data));
580
+
581
+ case 'Uint16Array':
582
+ return new Uint16Array(JSON.parse(data));
583
+
584
+ case 'Uint32Array':
585
+ return new Uint32Array(JSON.parse(data));
586
+
587
+ case 'BigInt64Array':
588
+ return new BigInt64Array(JSON.parse(data));
589
+
590
+ case 'BigUint64Array':
591
+ return new BigUint64Array(JSON.parse(data));
592
+
593
+ case 'Float32Array':
594
+ return new Float32Array(JSON.parse(data));
595
+
596
+ case 'Float64Array':
597
+ return new Float64Array(JSON.parse(data));
598
+
599
+ case 'Map':
600
+ return new Map(JSON.parse(data));
601
+
602
+ case 'Set':
603
+ return new Set(JSON.parse(data));
604
+
605
+ case 'Date':
606
+ const d = new Date(data);
607
+ if (isNaN(d.getTime())) throw new Error('Invalid Date');
608
+ return d;
609
+
610
+ case 'RegExp':
611
+ return new RegExp(data);
612
+
613
+ case 'Error':
614
+ return new Error(data);
615
+
616
+ default:
617
+ throw new Error(`Unknown or unsupported type: ${type}`);
618
+ }
619
+ } catch (err) {
620
+ throw new Error(`Failed to restore data of type "${type}": ${err.message}`);
621
+ }
622
+ }
623
+
624
+ }
625
+
626
+ class SPD_Legacy {
627
+
628
+
629
+ constructor() {
630
+ this.data = [];
631
+ this.keyPair; // Generate a key pair for encryption/decryption
632
+ this.userKey;
633
+ this.salt;
634
+ this.init();
635
+ }
636
+ async init() {
637
+ await sodium.ready;
638
+ this.keyPair = sodium.crypto_box_keypair()
639
+ }
640
+ async setPassKey(passcode){
641
+ await sodium.ready;
642
+ this.init()
643
+ const { pqcKey, salt } = await new SPD_Legacy().convertPasscodeToPQCKey(passcode);
644
+ const userKey = pqcKey.publicKey;
645
+ this.userKey = userKey;
646
+ this.salt = salt;
647
+ }
648
+
649
+ async addData(name, data) {
650
+
651
+
652
+ const dmap = await this.CITS(data)
653
+ await sodium.ready;
654
+ const dat = Buffer.from(dmap[0]);
655
+ const compressedData = zlib.deflateSync(dat,{
656
+ level:9
657
+ });
658
+ const nonce = sodium.randombytes_buf(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
659
+ const encryptedData = sodium.crypto_secretbox_easy(compressedData, nonce, this.userKey);
660
+ const hash = crypto.createHash('sha256').update(encryptedData).digest('hex');
661
+ this.data.push({ dataName: name, nonce: Array.from(nonce), data: Array.from(encryptedData), hash, dataType: dmap[1] });
662
+ }
663
+
664
+ saveToFile(outputPath) {
665
+ if (!outputPath || typeof outputPath !== 'string' || !outputPath.trim() || !this.salt || !(this.salt instanceof Uint8Array) || this.salt.length !== 16) {
666
+ throw new Error('Invalid output path or salt.');
667
+ }
668
+
669
+ const spdData = JSON.stringify({ data: this.data, salt: Array.from(this.salt) });
670
+ const compressedSpdData = zlib.deflateSync(spdData,{
671
+ level:9
672
+ });
673
+ fs.writeFileSync(outputPath, compressedSpdData, { mode: 0o600 });
674
+ }
675
+
676
+
677
+ static async loadFromFile(spdPath, passcode) {
678
+ if (
679
+ !spdPath || typeof spdPath !== 'string' || !spdPath.trim() ||
680
+ !passcode || typeof passcode !== 'string' || !passcode.trim()
681
+ ) {
682
+ throw new Error('Invalid SPD path or passcode.');
683
+ }
684
+
685
+ await sodium.ready;
686
+
687
+ try {
688
+ const compressedSpdData = fs.readFileSync(spdPath);
689
+ const spdData = zlib.inflateSync(compressedSpdData, { level: 9 }).toString('utf8');
690
+ const { data, salt } = JSON.parse(spdData);
691
+
692
+ const legacyInstance = new SPD_Legacy();
693
+ const { pqcKey } = await legacyInstance.convertPasscodeToPQCKeySalted(passcode, new Uint8Array(salt));
694
+ const pbk = pqcKey.publicKey;
695
+
696
+ const spd = new SPD_Legacy();
697
+ spd.userKey = pbk;
698
+ spd.keyPair = { publicKey: pbk.publicKey };
699
+
700
+ spd.data = data.map(dat => ({
701
+ dataName: dat.dataName,
702
+ nonce: Buffer.from(dat.nonce),
703
+ data: Buffer.from(dat.data),
704
+ hash: dat.hash,
705
+ dataType: dat.dataType
706
+ }));
707
+
708
+ // ✅ Run hash validation in parallel
709
+ const validationTasks = spd.data.map(dat => {
710
+ return new Promise((resolve, reject) => {
711
+ const calculatedHash = crypto.createHash('sha256').update(dat.data).digest('hex');
712
+ if (calculatedHash !== dat.hash) {
713
+ reject(new Error(`Data integrity check failed for ${dat.dataName}`));
714
+ } else {
715
+ resolve();
716
+ }
717
+ });
718
+ });
719
+
720
+ await Promise.all(validationTasks);
721
+
722
+ return spd;
723
+
724
+ } catch (err) {
725
+ throw new Error(`Failed to load SPD file: ${err.message}`);
726
+ }
727
+ }
728
+
729
+
730
+ async extractData() {
731
+ await sodium.ready;
732
+
733
+ try {
734
+ const extractionTasks = this.data.map(async dat => {
735
+ try {
736
+ const decryptedData = sodium.crypto_secretbox_open_easy(dat.data, dat.nonce, this.userKey);
737
+ const decompressedData = zlib.inflateSync(decryptedData, { level: 9 });
738
+ const dt = decompressedData.toString('utf8');
739
+ const parsed = await this.CSTI(dt, dat.dataType);
740
+ return [dat.dataName, parsed];
741
+ } catch (err) {
742
+ throw new Error(`Failed to process ${dat.dataName}: ${err.message}`);
743
+ }
744
+ });
745
+
746
+ const results = await Promise.all(extractionTasks);
747
+ return Object.fromEntries(results);
748
+
749
+ } catch (err) {
750
+ throw new Error(`Data extraction failed: ${err.message}`);
751
+ }
752
+ }
753
+
754
+
755
+
756
+
757
+ static async derivePBK(passcode, salt) {
758
+ if (!passcode || typeof passcode !== 'string' || !passcode.trim() || !salt || !(salt instanceof Uint8Array) || salt.length !== 16) {
759
+ throw new Error('Invalid passcode or salt.');
760
+ }
761
+
762
+ return new Promise((resolve, reject) => {
763
+ crypto.pbkdf2(passcode, salt, 100000, 32, 'sha256', (err, derivedKey) => {
764
+ if (err) {
765
+ reject(err);
766
+ } else {
767
+ resolve({ pbk: derivedKey, salt: salt });
768
+ }
769
+ });
770
+ });
771
+ }
772
+
773
+ saveData() {
774
+ const spdData = JSON.stringify({ data: this.data, salt: Array.from(this.salt) });
775
+ const compressedSpdData = zlib.deflateSync(spdData,{level:9});
776
+ return compressedSpdData;
777
+ }
778
+
779
+ static async loadFromString(spdData, passcode) {
780
+ if (!spdData || typeof spdData !== 'string' || !spdData.trim() ||
781
+ !passcode || typeof passcode !== 'string' || !passcode.trim()) {
782
+ throw new Error('Invalid SPD data or passcode.');
783
+ }
784
+
785
+ await sodium.ready;
786
+
787
+ try {
788
+ const spdDataBuffer = Buffer.from(spdData, 'base64');
789
+ const decompressed = zlib.inflateSync(spdDataBuffer, { level: 9 }).toString('utf8');
790
+ const { data, salt } = JSON.parse(decompressed);
791
+
792
+ const legacyInstance = new SPD_Legacy();
793
+ const { pqcKey } = await legacyInstance.convertPasscodeToPQCKeySalted(passcode, new Uint8Array(salt));
794
+ const pbk = pqcKey.publicKey;
795
+
796
+ const spd = new SPD_Legacy();
797
+ spd.userKey = pbk;
798
+ spd.keyPair = { publicKey: pbk.publicKey };
799
+
800
+ // Deserialize data
801
+ spd.data = data.map(dat => ({
802
+ dataName: dat.dataName,
803
+ nonce: Buffer.from(dat.nonce),
804
+ data: Buffer.from(dat.data),
805
+ hash: dat.hash,
806
+ dataType: dat.dataType
807
+ }));
808
+
809
+ // ✅ Validate hashes in parallel
810
+ const validationTasks = spd.data.map(dat => {
811
+ return new Promise((resolve, reject) => {
812
+ const calculatedHash = crypto.createHash('sha256').update(dat.data).digest('hex');
813
+ if (calculatedHash !== dat.hash) {
814
+ reject(new Error(`Data integrity check failed for ${dat.dataName}`));
815
+ } else {
816
+ resolve();
817
+ }
818
+ });
819
+ });
820
+
821
+ await Promise.all(validationTasks);
822
+
823
+ return spd;
824
+ } catch (err) {
825
+ throw new Error(`Failed to load SPD data: ${err.message}`);
826
+ }
827
+ }
828
+
829
+
830
+ async convertPasscodeToPQCKeySalted(passcode, salt) {
831
+ if (!passcode || typeof passcode !== 'string' || !passcode.trim() || passcode.length < 8 || !salt || !(salt instanceof Uint8Array) || salt.length !== 16) {
832
+ throw new Error('Invalid passcode or salt.');
833
+ }
834
+
835
+ const { pbk } = await SPD.derivePBK(passcode, salt);
836
+ await sodium.ready;
837
+ const keyPair = sodium.crypto_kx_seed_keypair(pbk)
838
+ return { pqcKey: { publicKey: keyPair.publicKey }, salt };
839
+ }
840
+
841
+ async convertPasscodeToPQCKey(passcode) {
842
+ if (!passcode || typeof passcode !== 'string' || !passcode.trim() || passcode.length < 8) {
843
+ throw new Error('Invalid passcode.');
844
+ }
845
+
846
+ const { pbk, salt } = await SPD.derivePBK(passcode, crypto.getRandomValues(new Uint8Array(16)));
847
+ await sodium.ready;
848
+ const keyPair = sodium.crypto_kx_seed_keypair(pbk.slice(0, sodium.crypto_kx_SEEDBYTES));
849
+ return { pqcKey: { publicKey: keyPair.publicKey }, salt };
850
+ }
851
+ async TDT(data) {
852
+ const classTypeMap = {
853
+ '[object Array]': 'Array',
854
+ '[object Uint8Array]': 'Uint8Array',
855
+ '[object Uint16Array]': 'Uint16Array',
856
+ '[object Uint32Array]': 'Uint32Array',
857
+ '[object BigInt64Array]': 'BigInt64Array',
858
+ '[object BigUint64Array]': 'BigUint64Array',
859
+ '[object Float32Array]': 'Float32Array',
860
+ '[object Float64Array]': 'Float64Array',
861
+ '[object Map]': 'Map',
862
+ '[object Set]': 'Set',
863
+ '[object Date]': 'Date',
864
+ '[object RegExp]': 'RegExp',
865
+ '[object Error]': 'Error'
866
+ };
867
+
868
+ const objectType = Object.prototype.toString.call(data);
869
+ const mappedType = classTypeMap[objectType];
870
+ return mappedType;
871
+ }
872
+ async isNumArr(dataType) {
873
+ if(dataType === 'Uint8Array' || dataType === 'Uint16Array' || dataType === 'Uint32Array' || dataType === 'BigInt64Array' || dataType === 'BigUint64Array' || dataType === 'Float32Array' || dataType === 'Float64Array'){
874
+ return true;
875
+ }
876
+ }
877
+ async isSWM(dataType) {
878
+ if(dataType === 'Map' || dataType === 'Set' || dataType === 'WeakMap' || dataType === 'WeakSet'){
879
+ return true;
880
+ }
881
+ }
882
+ async isDRE(dataType) {
883
+ if(dataType === 'Date' || dataType === 'RegExp' || dataType === 'Error'){
884
+ return true;
885
+ }
886
+ }
887
+ async CITS(data) {
888
+ const dataType = typeof data;
889
+ if (dataType === 'string' || dataType === 'number' || dataType === 'boolean') {
890
+ return [data.toString(), dataType];
891
+ }
892
+ if(typeof data === 'object'){
893
+ const type = await this.TDT(data)
894
+ if(type === 'Array'){
895
+ return [JSON.stringify(data),'Array'];
896
+ }
897
+ if(await this.isNumArr(type)){
898
+ return [JSON.stringify(Array.from(data)),type];
899
+ }
900
+ if(await this.isSWM(type)){
901
+
902
+ return [JSON.stringify([...data]),type];
903
+ }
904
+ if(await this.isDRE(type)){
905
+ return [data.toString(),type];
906
+ }
907
+ return [JSON.stringify(data), typeof data];
908
+ }
909
+ }
910
+ async CSTI(data,type) {
911
+ if(type === 'string') {
912
+ return data
913
+ }
914
+ if(type === 'number'){
915
+ return parseFloat(data);
916
+ }
917
+ if(type === 'boolean'){
918
+ return (data === 'true')&&(data !== 'false');
919
+ }
920
+ if(type === 'object' || type === 'Array'){
921
+ return JSON.parse(data);
922
+ }
923
+ if(type === 'Uint8Array'){
924
+ return new Uint8Array(JSON.parse(data));
925
+ }
926
+ if(type === 'Uint16Array'){
927
+ return new Uint16Array(JSON.parse(data));
928
+ }
929
+ if(type === 'Uint32Array'){
930
+
931
+ return new Uint32Array(JSON.parse(data));
932
+ }
933
+ if(type === 'BigInt64Array'){
934
+ return new BigInt64Array(JSON.parse(data));
935
+ }
936
+ if(type === 'BigUint64Array'){
937
+ return new BigUint64Array(JSON.parse(data));
938
+ }
939
+ if(type === 'Float32Array'){
940
+ return new Float32Array(JSON.parse(data));
941
+ }
942
+ if(type === 'Float64Array'){
943
+ return new Float64Array(JSON.parse(data));
944
+ }
945
+ if(type === 'Map'){
946
+ return new Map(JSON.parse(data));
947
+ }
948
+ if(type === 'Set'){
949
+ return new Set(JSON.parse(data));
950
+ }
951
+ if(type === 'WeakMap'){
952
+ return new WeakMap(JSON.parse(data));
953
+ }
954
+ if(type === 'WeakSet'){
955
+ return new WeakSet(JSON.parse(data));
956
+ }
957
+ if(type === 'Date'){
958
+ return new Date(data);
959
+ }
960
+ if(type === 'RegExp'){
961
+ return new RegExp(data);
962
+ }
963
+ if(type === 'Error'){
964
+ return new Error(data);
965
+ }
966
+ }
967
+ }
968
+
969
+ module.exports = {
970
+ SPD,
971
+ SPD_LEG:SPD_Legacy
972
+ };