stegdoc 4.0.0 → 5.0.1
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/LICENSE +21 -21
- package/README.md +200 -214
- package/package.json +59 -59
- package/src/commands/decode.js +485 -343
- package/src/commands/encode.js +567 -449
- package/src/commands/info.js +118 -114
- package/src/commands/verify.js +207 -204
- package/src/index.js +89 -87
- package/src/lib/compression.js +177 -115
- package/src/lib/crypto.js +172 -172
- package/src/lib/decoy-generator.js +306 -306
- package/src/lib/docx-handler.js +587 -161
- package/src/lib/docx-templates.js +355 -0
- package/src/lib/file-handler.js +113 -113
- package/src/lib/file-utils.js +160 -150
- package/src/lib/interactive.js +190 -190
- package/src/lib/log-generator.js +764 -0
- package/src/lib/metadata.js +151 -122
- package/src/lib/streams.js +197 -197
- package/src/lib/utils.js +227 -227
- package/src/lib/xlsx-handler.js +597 -416
- package/src/lib/xml-utils.js +115 -115
package/src/lib/compression.js
CHANGED
|
@@ -1,115 +1,177 @@
|
|
|
1
|
-
const zlib = require('zlib');
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
1
|
+
const zlib = require('zlib');
|
|
2
|
+
|
|
3
|
+
// ─── Brotli Compression (v5+) ──────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Compress data using Brotli (best compression, used in v5+)
|
|
7
|
+
* @param {Buffer} buffer - Data to compress
|
|
8
|
+
* @returns {Promise<Buffer>} Compressed data
|
|
9
|
+
*/
|
|
10
|
+
function compressBrotli(buffer) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
zlib.brotliCompress(buffer, {
|
|
13
|
+
params: {
|
|
14
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY,
|
|
15
|
+
},
|
|
16
|
+
}, (err, result) => {
|
|
17
|
+
if (err) reject(err);
|
|
18
|
+
else resolve(result);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Decompress Brotli data
|
|
25
|
+
* @param {Buffer} buffer - Compressed data
|
|
26
|
+
* @returns {Promise<Buffer>} Decompressed data
|
|
27
|
+
*/
|
|
28
|
+
function decompressBrotli(buffer) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
zlib.brotliDecompress(buffer, (err, result) => {
|
|
31
|
+
if (err) reject(err);
|
|
32
|
+
else resolve(result);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a streaming Brotli compression transform
|
|
39
|
+
* @param {number} [quality] - Compression quality (0-11, default 11)
|
|
40
|
+
* @returns {zlib.BrotliCompress} Brotli transform stream
|
|
41
|
+
*/
|
|
42
|
+
function createBrotliCompressStream(quality) {
|
|
43
|
+
const q = quality !== undefined ? quality : zlib.constants.BROTLI_MAX_QUALITY;
|
|
44
|
+
return zlib.createBrotliCompress({
|
|
45
|
+
params: {
|
|
46
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: q,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a streaming Brotli decompression transform
|
|
53
|
+
* @returns {zlib.BrotliDecompress} Brotli transform stream
|
|
54
|
+
*/
|
|
55
|
+
function createBrotliDecompressStream() {
|
|
56
|
+
return zlib.createBrotliDecompress();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Gzip Compression (v3/v4 legacy) ───────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* MIME types that are already compressed - no benefit from additional compression
|
|
63
|
+
*/
|
|
64
|
+
const COMPRESSED_MIMES = new Set([
|
|
65
|
+
// Archives
|
|
66
|
+
'application/zip',
|
|
67
|
+
'application/x-7z-compressed',
|
|
68
|
+
'application/x-rar-compressed',
|
|
69
|
+
'application/gzip',
|
|
70
|
+
'application/x-gzip',
|
|
71
|
+
'application/x-bzip2',
|
|
72
|
+
'application/x-xz',
|
|
73
|
+
'application/x-tar',
|
|
74
|
+
'application/x-lzip',
|
|
75
|
+
'application/x-lzma',
|
|
76
|
+
'application/zstd',
|
|
77
|
+
|
|
78
|
+
// Images (lossy compressed)
|
|
79
|
+
'image/jpeg',
|
|
80
|
+
'image/png',
|
|
81
|
+
'image/gif',
|
|
82
|
+
'image/webp',
|
|
83
|
+
'image/avif',
|
|
84
|
+
'image/heic',
|
|
85
|
+
'image/heif',
|
|
86
|
+
'image/jxl',
|
|
87
|
+
|
|
88
|
+
// Audio
|
|
89
|
+
'audio/mpeg', // mp3
|
|
90
|
+
'audio/ogg',
|
|
91
|
+
'audio/flac',
|
|
92
|
+
'audio/aac',
|
|
93
|
+
'audio/mp4',
|
|
94
|
+
'audio/x-m4a',
|
|
95
|
+
'audio/opus',
|
|
96
|
+
|
|
97
|
+
// Video
|
|
98
|
+
'video/mp4',
|
|
99
|
+
'video/webm',
|
|
100
|
+
'video/x-matroska', // mkv
|
|
101
|
+
'video/quicktime', // mov
|
|
102
|
+
'video/x-msvideo', // avi
|
|
103
|
+
'video/mpeg',
|
|
104
|
+
|
|
105
|
+
// Documents (OOXML are zip-based)
|
|
106
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
|
|
107
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // xlsx
|
|
108
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // pptx
|
|
109
|
+
'application/pdf',
|
|
110
|
+
'application/epub+zip',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Check if a file type is already compressed
|
|
115
|
+
* @param {string|null} mime - MIME type from file-type detection
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function isCompressedMime(mime) {
|
|
119
|
+
return mime ? COMPRESSED_MIMES.has(mime) : false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Compress data using gzip
|
|
124
|
+
* @param {Buffer} buffer - Data to compress
|
|
125
|
+
* @returns {Promise<Buffer>} Compressed data
|
|
126
|
+
*/
|
|
127
|
+
function compress(buffer) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
zlib.gzip(buffer, { level: 9 }, (err, result) => {
|
|
130
|
+
if (err) reject(err);
|
|
131
|
+
else resolve(result);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Decompress gzip data
|
|
138
|
+
* @param {Buffer} buffer - Compressed data
|
|
139
|
+
* @returns {Promise<Buffer>} Decompressed data
|
|
140
|
+
*/
|
|
141
|
+
function decompress(buffer) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
zlib.gunzip(buffer, (err, result) => {
|
|
144
|
+
if (err) reject(err);
|
|
145
|
+
else resolve(result);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a streaming gzip compression transform
|
|
152
|
+
* @returns {zlib.Gzip} Gzip transform stream
|
|
153
|
+
*/
|
|
154
|
+
function createCompressStream() {
|
|
155
|
+
return zlib.createGzip({ level: 9 });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create a streaming gunzip decompression transform
|
|
160
|
+
* @returns {zlib.Gunzip} Gunzip transform stream
|
|
161
|
+
*/
|
|
162
|
+
function createDecompressStream() {
|
|
163
|
+
return zlib.createGunzip();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
isCompressedMime,
|
|
168
|
+
compress,
|
|
169
|
+
decompress,
|
|
170
|
+
createCompressStream,
|
|
171
|
+
createDecompressStream,
|
|
172
|
+
compressBrotli,
|
|
173
|
+
decompressBrotli,
|
|
174
|
+
createBrotliCompressStream,
|
|
175
|
+
createBrotliDecompressStream,
|
|
176
|
+
COMPRESSED_MIMES,
|
|
177
|
+
};
|
package/src/lib/crypto.js
CHANGED
|
@@ -1,172 +1,172 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
|
-
|
|
3
|
-
// AES-256-GCM Configuration
|
|
4
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
5
|
-
const KEY_LENGTH = 32; // 256 bits
|
|
6
|
-
const IV_LENGTH = 12; // 96 bits (recommended for GCM)
|
|
7
|
-
const SALT_LENGTH = 16; // 128 bits
|
|
8
|
-
const AUTH_TAG_LENGTH = 16; // 128 bits
|
|
9
|
-
const PBKDF2_ITERATIONS = 100000;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Derive a key from password using PBKDF2
|
|
13
|
-
* @param {string} password - User password
|
|
14
|
-
* @param {Buffer} salt - Salt for key derivation
|
|
15
|
-
* @returns {Buffer} Derived key
|
|
16
|
-
*/
|
|
17
|
-
function deriveKey(password, salt) {
|
|
18
|
-
return crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Encrypt plaintext using AES-256-GCM
|
|
23
|
-
* @param {string} plaintext - Data to encrypt
|
|
24
|
-
* @param {string} password - User password
|
|
25
|
-
* @returns {object} { ciphertext, iv, salt, authTag } - all as base64 strings
|
|
26
|
-
*/
|
|
27
|
-
function encrypt(plaintext, password) {
|
|
28
|
-
// Generate random salt and IV
|
|
29
|
-
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
30
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
31
|
-
|
|
32
|
-
// Derive key from password
|
|
33
|
-
const key = deriveKey(password, salt);
|
|
34
|
-
|
|
35
|
-
// Create cipher and encrypt
|
|
36
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
37
|
-
authTagLength: AUTH_TAG_LENGTH,
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
|
|
41
|
-
ciphertext += cipher.final('base64');
|
|
42
|
-
|
|
43
|
-
// Get authentication tag
|
|
44
|
-
const authTag = cipher.getAuthTag();
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
ciphertext,
|
|
48
|
-
iv: iv.toString('base64'),
|
|
49
|
-
salt: salt.toString('base64'),
|
|
50
|
-
authTag: authTag.toString('base64'),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Decrypt ciphertext using AES-256-GCM
|
|
56
|
-
* @param {string} ciphertext - Encrypted data (base64)
|
|
57
|
-
* @param {string} password - User password
|
|
58
|
-
* @param {string} ivBase64 - Initialization vector (base64)
|
|
59
|
-
* @param {string} saltBase64 - Salt used for key derivation (base64)
|
|
60
|
-
* @param {string} authTagBase64 - Authentication tag (base64)
|
|
61
|
-
* @returns {string} Decrypted plaintext
|
|
62
|
-
* @throws {Error} If decryption fails (wrong password or tampered data)
|
|
63
|
-
*/
|
|
64
|
-
function decrypt(ciphertext, password, ivBase64, saltBase64, authTagBase64) {
|
|
65
|
-
// Convert from base64
|
|
66
|
-
const iv = Buffer.from(ivBase64, 'base64');
|
|
67
|
-
const salt = Buffer.from(saltBase64, 'base64');
|
|
68
|
-
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
69
|
-
|
|
70
|
-
// Derive key from password
|
|
71
|
-
const key = deriveKey(password, salt);
|
|
72
|
-
|
|
73
|
-
// Create decipher
|
|
74
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
75
|
-
authTagLength: AUTH_TAG_LENGTH,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Set auth tag for verification
|
|
79
|
-
decipher.setAuthTag(authTag);
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
|
|
83
|
-
plaintext += decipher.final('utf8');
|
|
84
|
-
return plaintext;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
throw new Error('Decryption failed: Invalid password or corrupted data');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Pack encryption metadata into a single string for storage
|
|
92
|
-
* @param {object} encryptionData - { iv, salt, authTag }
|
|
93
|
-
* @returns {string} Packed string (iv:salt:authTag in base64)
|
|
94
|
-
*/
|
|
95
|
-
function packEncryptionMeta(encryptionData) {
|
|
96
|
-
return `${encryptionData.iv}:${encryptionData.salt}:${encryptionData.authTag}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Unpack encryption metadata from storage string
|
|
101
|
-
* @param {string} packed - Packed string (iv:salt:authTag)
|
|
102
|
-
* @returns {object} { iv, salt, authTag }
|
|
103
|
-
*/
|
|
104
|
-
function unpackEncryptionMeta(packed) {
|
|
105
|
-
const [iv, salt, authTag] = packed.split(':');
|
|
106
|
-
if (!iv || !salt || !authTag) {
|
|
107
|
-
throw new Error('Invalid encryption metadata format');
|
|
108
|
-
}
|
|
109
|
-
return { iv, salt, authTag };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Generate a random salt for key derivation (one per encode session)
|
|
114
|
-
* @returns {Buffer} Random salt
|
|
115
|
-
*/
|
|
116
|
-
function generateSalt() {
|
|
117
|
-
return crypto.randomBytes(SALT_LENGTH);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Create a streaming encrypt cipher for per-part encryption.
|
|
122
|
-
* Each call generates a unique IV. The salt should be shared across parts.
|
|
123
|
-
* Call getAuthTag() AFTER the cipher stream has ended (after .final()).
|
|
124
|
-
* @param {string} password - User password
|
|
125
|
-
* @param {Buffer} salt - Salt buffer (shared across parts in a session)
|
|
126
|
-
* @returns {object} { stream, iv, salt, getAuthTag }
|
|
127
|
-
*/
|
|
128
|
-
function createEncryptStream(password, salt) {
|
|
129
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
130
|
-
const key = deriveKey(password, salt);
|
|
131
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
132
|
-
authTagLength: AUTH_TAG_LENGTH,
|
|
133
|
-
});
|
|
134
|
-
return {
|
|
135
|
-
stream: cipher,
|
|
136
|
-
iv: iv.toString('base64'),
|
|
137
|
-
salt: salt.toString('base64'),
|
|
138
|
-
getAuthTag: () => cipher.getAuthTag().toString('base64'),
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Create a streaming decrypt decipher for per-part decryption.
|
|
144
|
-
* Auth tag is set immediately and verified on .final().
|
|
145
|
-
* @param {string} password - User password
|
|
146
|
-
* @param {string} ivBase64 - IV (base64)
|
|
147
|
-
* @param {string} saltBase64 - Salt (base64)
|
|
148
|
-
* @param {string} authTagBase64 - Auth tag (base64)
|
|
149
|
-
* @returns {crypto.Decipher} Decipher transform stream
|
|
150
|
-
*/
|
|
151
|
-
function createDecryptStream(password, ivBase64, saltBase64, authTagBase64) {
|
|
152
|
-
const iv = Buffer.from(ivBase64, 'base64');
|
|
153
|
-
const salt = Buffer.from(saltBase64, 'base64');
|
|
154
|
-
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
155
|
-
const key = deriveKey(password, salt);
|
|
156
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
157
|
-
authTagLength: AUTH_TAG_LENGTH,
|
|
158
|
-
});
|
|
159
|
-
decipher.setAuthTag(authTag);
|
|
160
|
-
return decipher;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
module.exports = {
|
|
164
|
-
encrypt,
|
|
165
|
-
decrypt,
|
|
166
|
-
deriveKey,
|
|
167
|
-
packEncryptionMeta,
|
|
168
|
-
unpackEncryptionMeta,
|
|
169
|
-
generateSalt,
|
|
170
|
-
createEncryptStream,
|
|
171
|
-
createDecryptStream,
|
|
172
|
-
};
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
// AES-256-GCM Configuration
|
|
4
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
5
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
6
|
+
const IV_LENGTH = 12; // 96 bits (recommended for GCM)
|
|
7
|
+
const SALT_LENGTH = 16; // 128 bits
|
|
8
|
+
const AUTH_TAG_LENGTH = 16; // 128 bits
|
|
9
|
+
const PBKDF2_ITERATIONS = 100000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Derive a key from password using PBKDF2
|
|
13
|
+
* @param {string} password - User password
|
|
14
|
+
* @param {Buffer} salt - Salt for key derivation
|
|
15
|
+
* @returns {Buffer} Derived key
|
|
16
|
+
*/
|
|
17
|
+
function deriveKey(password, salt) {
|
|
18
|
+
return crypto.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Encrypt plaintext using AES-256-GCM
|
|
23
|
+
* @param {string} plaintext - Data to encrypt
|
|
24
|
+
* @param {string} password - User password
|
|
25
|
+
* @returns {object} { ciphertext, iv, salt, authTag } - all as base64 strings
|
|
26
|
+
*/
|
|
27
|
+
function encrypt(plaintext, password) {
|
|
28
|
+
// Generate random salt and IV
|
|
29
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
30
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
31
|
+
|
|
32
|
+
// Derive key from password
|
|
33
|
+
const key = deriveKey(password, salt);
|
|
34
|
+
|
|
35
|
+
// Create cipher and encrypt
|
|
36
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
37
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let ciphertext = cipher.update(plaintext, 'utf8', 'base64');
|
|
41
|
+
ciphertext += cipher.final('base64');
|
|
42
|
+
|
|
43
|
+
// Get authentication tag
|
|
44
|
+
const authTag = cipher.getAuthTag();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
ciphertext,
|
|
48
|
+
iv: iv.toString('base64'),
|
|
49
|
+
salt: salt.toString('base64'),
|
|
50
|
+
authTag: authTag.toString('base64'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Decrypt ciphertext using AES-256-GCM
|
|
56
|
+
* @param {string} ciphertext - Encrypted data (base64)
|
|
57
|
+
* @param {string} password - User password
|
|
58
|
+
* @param {string} ivBase64 - Initialization vector (base64)
|
|
59
|
+
* @param {string} saltBase64 - Salt used for key derivation (base64)
|
|
60
|
+
* @param {string} authTagBase64 - Authentication tag (base64)
|
|
61
|
+
* @returns {string} Decrypted plaintext
|
|
62
|
+
* @throws {Error} If decryption fails (wrong password or tampered data)
|
|
63
|
+
*/
|
|
64
|
+
function decrypt(ciphertext, password, ivBase64, saltBase64, authTagBase64) {
|
|
65
|
+
// Convert from base64
|
|
66
|
+
const iv = Buffer.from(ivBase64, 'base64');
|
|
67
|
+
const salt = Buffer.from(saltBase64, 'base64');
|
|
68
|
+
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
69
|
+
|
|
70
|
+
// Derive key from password
|
|
71
|
+
const key = deriveKey(password, salt);
|
|
72
|
+
|
|
73
|
+
// Create decipher
|
|
74
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
75
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Set auth tag for verification
|
|
79
|
+
decipher.setAuthTag(authTag);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
|
|
83
|
+
plaintext += decipher.final('utf8');
|
|
84
|
+
return plaintext;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error('Decryption failed: Invalid password or corrupted data');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Pack encryption metadata into a single string for storage
|
|
92
|
+
* @param {object} encryptionData - { iv, salt, authTag }
|
|
93
|
+
* @returns {string} Packed string (iv:salt:authTag in base64)
|
|
94
|
+
*/
|
|
95
|
+
function packEncryptionMeta(encryptionData) {
|
|
96
|
+
return `${encryptionData.iv}:${encryptionData.salt}:${encryptionData.authTag}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Unpack encryption metadata from storage string
|
|
101
|
+
* @param {string} packed - Packed string (iv:salt:authTag)
|
|
102
|
+
* @returns {object} { iv, salt, authTag }
|
|
103
|
+
*/
|
|
104
|
+
function unpackEncryptionMeta(packed) {
|
|
105
|
+
const [iv, salt, authTag] = packed.split(':');
|
|
106
|
+
if (!iv || !salt || !authTag) {
|
|
107
|
+
throw new Error('Invalid encryption metadata format');
|
|
108
|
+
}
|
|
109
|
+
return { iv, salt, authTag };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generate a random salt for key derivation (one per encode session)
|
|
114
|
+
* @returns {Buffer} Random salt
|
|
115
|
+
*/
|
|
116
|
+
function generateSalt() {
|
|
117
|
+
return crypto.randomBytes(SALT_LENGTH);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a streaming encrypt cipher for per-part encryption.
|
|
122
|
+
* Each call generates a unique IV. The salt should be shared across parts.
|
|
123
|
+
* Call getAuthTag() AFTER the cipher stream has ended (after .final()).
|
|
124
|
+
* @param {string} password - User password
|
|
125
|
+
* @param {Buffer} salt - Salt buffer (shared across parts in a session)
|
|
126
|
+
* @returns {object} { stream, iv, salt, getAuthTag }
|
|
127
|
+
*/
|
|
128
|
+
function createEncryptStream(password, salt) {
|
|
129
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
130
|
+
const key = deriveKey(password, salt);
|
|
131
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
|
|
132
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
stream: cipher,
|
|
136
|
+
iv: iv.toString('base64'),
|
|
137
|
+
salt: salt.toString('base64'),
|
|
138
|
+
getAuthTag: () => cipher.getAuthTag().toString('base64'),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a streaming decrypt decipher for per-part decryption.
|
|
144
|
+
* Auth tag is set immediately and verified on .final().
|
|
145
|
+
* @param {string} password - User password
|
|
146
|
+
* @param {string} ivBase64 - IV (base64)
|
|
147
|
+
* @param {string} saltBase64 - Salt (base64)
|
|
148
|
+
* @param {string} authTagBase64 - Auth tag (base64)
|
|
149
|
+
* @returns {crypto.Decipher} Decipher transform stream
|
|
150
|
+
*/
|
|
151
|
+
function createDecryptStream(password, ivBase64, saltBase64, authTagBase64) {
|
|
152
|
+
const iv = Buffer.from(ivBase64, 'base64');
|
|
153
|
+
const salt = Buffer.from(saltBase64, 'base64');
|
|
154
|
+
const authTag = Buffer.from(authTagBase64, 'base64');
|
|
155
|
+
const key = deriveKey(password, salt);
|
|
156
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
|
|
157
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
158
|
+
});
|
|
159
|
+
decipher.setAuthTag(authTag);
|
|
160
|
+
return decipher;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
encrypt,
|
|
165
|
+
decrypt,
|
|
166
|
+
deriveKey,
|
|
167
|
+
packEncryptionMeta,
|
|
168
|
+
unpackEncryptionMeta,
|
|
169
|
+
generateSalt,
|
|
170
|
+
createEncryptStream,
|
|
171
|
+
createDecryptStream,
|
|
172
|
+
};
|