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.
@@ -1,115 +1,177 @@
1
- const zlib = require('zlib');
2
-
3
- /**
4
- * MIME types that are already compressed - no benefit from additional compression
5
- */
6
- const COMPRESSED_MIMES = new Set([
7
- // Archives
8
- 'application/zip',
9
- 'application/x-7z-compressed',
10
- 'application/x-rar-compressed',
11
- 'application/gzip',
12
- 'application/x-gzip',
13
- 'application/x-bzip2',
14
- 'application/x-xz',
15
- 'application/x-tar',
16
- 'application/x-lzip',
17
- 'application/x-lzma',
18
- 'application/zstd',
19
-
20
- // Images (lossy compressed)
21
- 'image/jpeg',
22
- 'image/png',
23
- 'image/gif',
24
- 'image/webp',
25
- 'image/avif',
26
- 'image/heic',
27
- 'image/heif',
28
- 'image/jxl',
29
-
30
- // Audio
31
- 'audio/mpeg', // mp3
32
- 'audio/ogg',
33
- 'audio/flac',
34
- 'audio/aac',
35
- 'audio/mp4',
36
- 'audio/x-m4a',
37
- 'audio/opus',
38
-
39
- // Video
40
- 'video/mp4',
41
- 'video/webm',
42
- 'video/x-matroska', // mkv
43
- 'video/quicktime', // mov
44
- 'video/x-msvideo', // avi
45
- 'video/mpeg',
46
-
47
- // Documents (OOXML are zip-based)
48
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
49
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // xlsx
50
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // pptx
51
- 'application/pdf',
52
- 'application/epub+zip',
53
- ]);
54
-
55
- /**
56
- * Check if a file type is already compressed
57
- * @param {string|null} mime - MIME type from file-type detection
58
- * @returns {boolean}
59
- */
60
- function isCompressedMime(mime) {
61
- return mime ? COMPRESSED_MIMES.has(mime) : false;
62
- }
63
-
64
- /**
65
- * Compress data using gzip
66
- * @param {Buffer} buffer - Data to compress
67
- * @returns {Promise<Buffer>} Compressed data
68
- */
69
- function compress(buffer) {
70
- return new Promise((resolve, reject) => {
71
- zlib.gzip(buffer, { level: 9 }, (err, result) => {
72
- if (err) reject(err);
73
- else resolve(result);
74
- });
75
- });
76
- }
77
-
78
- /**
79
- * Decompress gzip data
80
- * @param {Buffer} buffer - Compressed data
81
- * @returns {Promise<Buffer>} Decompressed data
82
- */
83
- function decompress(buffer) {
84
- return new Promise((resolve, reject) => {
85
- zlib.gunzip(buffer, (err, result) => {
86
- if (err) reject(err);
87
- else resolve(result);
88
- });
89
- });
90
- }
91
-
92
- /**
93
- * Create a streaming gzip compression transform
94
- * @returns {zlib.Gzip} Gzip transform stream
95
- */
96
- function createCompressStream() {
97
- return zlib.createGzip({ level: 9 });
98
- }
99
-
100
- /**
101
- * Create a streaming gunzip decompression transform
102
- * @returns {zlib.Gunzip} Gunzip transform stream
103
- */
104
- function createDecompressStream() {
105
- return zlib.createGunzip();
106
- }
107
-
108
- module.exports = {
109
- isCompressedMime,
110
- compress,
111
- decompress,
112
- createCompressStream,
113
- createDecompressStream,
114
- COMPRESSED_MIMES,
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
+ };