enc-x 1.1.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/README.DEV.md ADDED
@@ -0,0 +1,256 @@
1
+ # enc-x — Developer Guide
2
+
3
+ Everything you need to work on, extend, or contribute to `enc-x`.
4
+
5
+ ---
6
+
7
+ ## Prerequisites
8
+
9
+ - Node.js 18+
10
+ - npm 9+
11
+
12
+ ---
13
+
14
+ ## Getting Started
15
+
16
+ ```bash
17
+ git clone <repo>
18
+ cd enc-x
19
+ npm install
20
+ npm run build
21
+ ```
22
+
23
+ Link the CLI globally so you can test it like a real install:
24
+
25
+ ```bash
26
+ npm link
27
+ enc-x --help
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Scripts
33
+
34
+ | Script | Description |
35
+ | ----------------------- | ------------------------------------------ |
36
+ | `npm run build` | Compile `src/` → `dist/` via tsup |
37
+ | `npm run dev` | Watch mode — rebuilds on file change |
38
+ | `npm run typecheck` | Type-check without emitting output |
39
+ | `npm test` | Run all Jest tests (serial, `--runInBand`) |
40
+ | `npm run test:coverage` | Run tests and generate coverage report |
41
+
42
+ ---
43
+
44
+ ## Project Structure
45
+
46
+ ```
47
+ enc-x/
48
+ ├── src/
49
+ │ ├── index.ts # CLI entry — commander setup, command registration
50
+ │ ├── cli/
51
+ │ │ ├── ui.ts # chalk helpers (info/success/error/warn) + progress bar
52
+ │ │ └── commands/
53
+ │ │ ├── encrypt.ts # `enc-x enc` handler — validates input, drives encryptFile()
54
+ │ │ └── decrypt.ts # `enc-x dec` handler — validates input, drives decryptFile()
55
+ │ ├── crypto/
56
+ │ │ ├── constants.ts # All algorithm params and header layout constants
57
+ │ │ ├── keys.ts # generateSalt(), generateIV(), deriveKey() (PBKDF2)
58
+ │ │ ├── header.ts # serializeHeader(), parseHeader(), AUTH_TAG_OFFSET
59
+ │ │ ├── encrypt.ts # encryptFile() — streaming AES-256-GCM encrypt
60
+ │ │ └── decrypt.ts # decryptFile() — streaming AES-256-GCM decrypt
61
+ │ └── utils/
62
+ │ ├── mime.ts # Extension → MIME type lookup (no external deps)
63
+ │ ├── progress.ts # Passthrough Transform stream for byte progress tracking
64
+ │ └── format.ts # formatBytes(), formatDuration()
65
+ ├── src/__tests__/
66
+ │ ├── crypto/
67
+ │ │ ├── constants.test.ts
68
+ │ │ ├── keys.test.ts
69
+ │ │ ├── header.test.ts
70
+ │ │ └── encrypt-decrypt.test.ts
71
+ │ └── utils/
72
+ │ ├── format.test.ts
73
+ │ ├── mime.test.ts
74
+ │ └── progress.test.ts
75
+ ├── dist/ # Compiled output (gitignored)
76
+ ├── jest.config.ts
77
+ ├── tsconfig.json # Main TypeScript config
78
+ ├── tsconfig.test.json # Extends tsconfig.json — adds jest types for ts-jest
79
+ ├── tsup.config.ts # Build config — CJS only, shebang banner, @ alias
80
+ ├── package.json
81
+ ├── .gitignore
82
+ ├── README.md # User-facing docs
83
+ └── README.DEV.md # This file
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Architecture
89
+
90
+ ### Encrypted File Format
91
+
92
+ Every `.enc-x` file is structured as a binary header followed by raw AES-256-GCM ciphertext:
93
+
94
+ ```
95
+ Offset Size Field
96
+ ────── ────── ─────────────────────────────────
97
+ 0 4 Magic: "ENCX" (0x45 0x4E 0x43 0x58)
98
+ 4 1 Version: 0x01
99
+ 5 32 Salt (random, generated per file)
100
+ 37 12 IV (random, generated per file)
101
+ 49 16 GCM Auth Tag (patched after encryption)
102
+ 65 2 Metadata JSON byte length (uint16 BE)
103
+ 67 N Metadata JSON (UTF-8)
104
+ 67+N ... AES-256-GCM ciphertext
105
+ ```
106
+
107
+ The metadata JSON looks like:
108
+
109
+ ```json
110
+ { "name": "video.mp4", "mime": "video/mp4", "size": 104857600 }
111
+ ```
112
+
113
+ ### Auth Tag Patching
114
+
115
+ GCM auth tags are only available _after_ the cipher stream is finalized. To avoid buffering the entire ciphertext in memory, `encryptFile()` uses a two-pass approach:
116
+
117
+ 1. Write the header with 16 zero bytes in the auth tag slot
118
+ 2. Stream plaintext → cipher → output file
119
+ 3. Call `cipher.getAuthTag()` and `fd.write()` to patch the tag back at `AUTH_TAG_OFFSET` (byte 49)
120
+
121
+ This keeps memory usage flat regardless of file size.
122
+
123
+ ### Streaming Pipeline
124
+
125
+ ```
126
+ encryptFile:
127
+ createReadStream(input)
128
+ → [optional progress Transform]
129
+ → createCipheriv (AES-256-GCM)
130
+ → createWriteStream(output) ← header already written
131
+ → patch auth tag at offset 49
132
+
133
+ decryptFile:
134
+ createReadStream(input, { start: headerSize })
135
+ → [optional progress Transform]
136
+ → createDecipheriv (AES-256-GCM) ← auth tag set before pipeline
137
+ → createWriteStream(output)
138
+ ```
139
+
140
+ `pipeline()` from `node:stream/promises` is used throughout — it handles backpressure and cleans up streams on error automatically.
141
+
142
+ ### Key Derivation
143
+
144
+ ```
145
+ password + salt (32 bytes random)
146
+ → PBKDF2-SHA512, 100,000 iterations
147
+ → 32-byte AES key
148
+ ```
149
+
150
+ A new salt is generated for every encryption, so the same password produces a different key each time.
151
+
152
+ ---
153
+
154
+ ## TypeScript Config
155
+
156
+ Two tsconfig files are used:
157
+
158
+ | File | Purpose |
159
+ | -------------------- | ------------------------------------------------------------------------------------------------------ |
160
+ | `tsconfig.json` | Main config — `moduleResolution: bundler`, used by tsup and `tsc --noEmit` |
161
+ | `tsconfig.test.json` | Extends main — switches to `CommonJS` + `moduleResolution: node` for ts-jest, adds `"jest"` to `types` |
162
+
163
+ The `@/*` path alias maps to `./src/*` in both configs and in tsup's esbuild options.
164
+
165
+ `"ignoreDeprecations": "6.0"` is set in `tsconfig.test.json` because TypeScript 6 internally sets `pathsBasePath` when `paths` is used, which triggers a deprecation warning in ts-jest's DTS builder.
166
+
167
+ ---
168
+
169
+ ## Testing
170
+
171
+ Tests live in `src/__tests__/` and mirror the `src/` structure.
172
+
173
+ ```bash
174
+ npm test # run all tests
175
+ npm run test:coverage # with coverage report → coverage/
176
+ ```
177
+
178
+ ### Test suites
179
+
180
+ | Suite | Tests | What it covers |
181
+ | ------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------- |
182
+ | `constants.test.ts` | 11 | Security parameter values, magic bytes, header size math |
183
+ | `keys.test.ts` | 9 | Salt/IV uniqueness, PBKDF2 determinism, key sensitivity |
184
+ | `header.test.ts` | 14 | Serialize/parse round-trips, bad magic, bad version, corrupted JSON, unicode filenames |
185
+ | `encrypt-decrypt.test.ts` | 20 | Full round-trips (text, binary, unicode, empty, 1 MB), wrong password, partial file cleanup, progress callbacks, cross-password rejection |
186
+ | `format.test.ts` | 11 | formatBytes and formatDuration edge cases |
187
+ | `mime.test.ts` | 12 | All MIME categories, unknown extensions, case-insensitivity, full paths |
188
+ | `progress.test.ts` | 5 | Passthrough integrity, cumulative byte counts, empty stream |
189
+
190
+ Integration tests (`encrypt-decrypt.test.ts`) use real Node.js crypto with real temp files — no mocks. Each test gets a fresh `os.tmpdir()` subdirectory via `beforeEach` / `afterEach`.
191
+
192
+ ### Timeouts
193
+
194
+ PBKDF2 with 100,000 iterations takes ~50–200ms per call. Tests that run multiple encrypt/decrypt cycles set explicit timeouts (15s–60s) to avoid flakiness on slow CI machines.
195
+
196
+ ---
197
+
198
+ ## Build
199
+
200
+ tsup compiles to a single CJS bundle at `dist/index.js`:
201
+
202
+ ```bash
203
+ npm run build
204
+ ```
205
+
206
+ Key tsup options:
207
+
208
+ - `format: ['cjs']` — CLI binary, no ESM needed
209
+ - `banner: { js: '#!/usr/bin/env node' }` — shebang injected by tsup (not in source)
210
+ - `dts: false` — no type declarations for a CLI
211
+ - `clean: true` — wipes `dist/` before each build
212
+ - `esbuildOptions.alias` — resolves `@/` → `./src/` at bundle time
213
+
214
+ ---
215
+
216
+ ## Adding a New Command
217
+
218
+ 1. Create `src/cli/commands/<name>.ts` with a `run<Name>()` async function
219
+ 2. Register it in `src/index.ts` using `program.command(...).action(...)`
220
+ 3. Add tests in `src/__tests__/`
221
+
222
+ ---
223
+
224
+ ## Dependencies
225
+
226
+ ### Runtime
227
+
228
+ | Package | Version | Purpose |
229
+ | -------------- | ------- | --------------------- |
230
+ | `commander` | 14.x | CLI argument parsing |
231
+ | `chalk` | 5.x | Terminal color output |
232
+ | `cli-progress` | 3.x | Progress bar |
233
+
234
+ No third-party crypto dependencies — all encryption uses Node.js built-in `node:crypto`.
235
+
236
+ ### Dev
237
+
238
+ | Package | Version | Purpose |
239
+ | --------------------- | ------- | ----------------------------- |
240
+ | `typescript` | 6.x | Type checking |
241
+ | `tsup` | 8.x | Bundler (esbuild-based) |
242
+ | `jest` | 30.x | Test runner |
243
+ | `ts-jest` | 29.x | TypeScript transform for Jest |
244
+ | `@types/node` | 22.x | Node.js type definitions |
245
+ | `@types/jest` | 30.x | Jest type definitions |
246
+ | `@types/cli-progress` | 3.x | cli-progress type definitions |
247
+
248
+ ---
249
+
250
+ ## Contributing
251
+
252
+ 1. Fork and create a feature branch
253
+ 2. Make changes — keep the streaming-first, no-external-crypto principles
254
+ 3. Add or update tests — all cases should be covered
255
+ 4. Run `npm run typecheck && npm test` — both must pass clean
256
+ 5. Open a PR with a clear description of what changed and why
package/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # enc-x
2
+
3
+ Encrypt and decrypt any file with a password. Fast, secure, works on files of any size.
4
+
5
+ The command automatically detects whether to encrypt or decrypt based on the file extension.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g enc-x
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Encrypt
16
+
17
+ ```bash
18
+ enc-x <file> -k <password>
19
+ ```
20
+
21
+ ```bash
22
+ enc-x video.mp4 -k "mypassword"
23
+ # → video.mp4.enc-x
24
+ ```
25
+
26
+ ### Decrypt
27
+
28
+ ```bash
29
+ enc-x <file.enc-x> -k <password>
30
+ ```
31
+
32
+ ```bash
33
+ enc-x video.mp4.enc-x -k "mypassword"
34
+ # → video.mp4
35
+ ```
36
+
37
+ ### Custom output path
38
+
39
+ ```bash
40
+ enc-x report.pdf -k "pass" -o /secure/report.pdf.enc-x
41
+ enc-x report.pdf.enc-x -k "pass" -o ./report.pdf
42
+ ```
43
+
44
+ ## Options
45
+
46
+ | Option | Short | Description |
47
+ | ------------------ | ----- | ------------------- |
48
+ | `--key <password>` | `-k` | Password (required) |
49
+ | `--output <path>` | `-o` | Custom output path |
50
+ | `--help` | `-h` | Show help |
51
+ | `--version` | `-v` | Show version |
52
+
53
+ ## Wrong password
54
+
55
+ If the password is wrong, the output file is deleted and you'll see:
56
+
57
+ ```
58
+ ✖ Invalid password or corrupted file
59
+ ```
60
+
61
+ ## How it works
62
+
63
+ - Pass any file → encrypts it and adds `.enc-x` extension
64
+ - Pass a `.enc-x` file → decrypts it and restores the original filename
65
+ - Encryption: **AES-256-GCM**
66
+ - Key derivation: **PBKDF2-SHA512**, 100,000 iterations, random salt per file
67
+ - Streaming I/O — no memory issues with large files
package/dist/index.js ADDED
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var commander = require('commander');
5
+ var chalk = require('chalk');
6
+ var fs = require('fs');
7
+ var path3 = require('path');
8
+ var crypto = require('crypto');
9
+ var promises = require('stream/promises');
10
+ var stream = require('stream');
11
+ var cliProgress = require('cli-progress');
12
+
13
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
+
15
+ var chalk__default = /*#__PURE__*/_interopDefault(chalk);
16
+ var path3__default = /*#__PURE__*/_interopDefault(path3);
17
+ var cliProgress__default = /*#__PURE__*/_interopDefault(cliProgress);
18
+
19
+ // src/crypto/constants.ts
20
+ var ALGORITHM = "aes-256-gcm";
21
+ var KEY_LENGTH = 32;
22
+ var IV_LENGTH = 12;
23
+ var SALT_LENGTH = 32;
24
+ var AUTH_TAG_LENGTH = 16;
25
+ var PBKDF2_ITERATIONS = 1e5;
26
+ var PBKDF2_DIGEST = "sha512";
27
+ var FILE_EXTENSION = ".enc-x";
28
+ var MAGIC = Buffer.from([69, 78, 67, 88]);
29
+ var VERSION = 1;
30
+ function generateSalt() {
31
+ return crypto.randomBytes(SALT_LENGTH);
32
+ }
33
+ function generateIV(length) {
34
+ return crypto.randomBytes(length);
35
+ }
36
+ function deriveKey(password, salt) {
37
+ return new Promise((resolve, reject) => {
38
+ crypto.pbkdf2(
39
+ password,
40
+ salt,
41
+ PBKDF2_ITERATIONS,
42
+ KEY_LENGTH,
43
+ PBKDF2_DIGEST,
44
+ (err, key) => {
45
+ if (err) reject(err);
46
+ else resolve(key);
47
+ }
48
+ );
49
+ });
50
+ }
51
+
52
+ // src/crypto/header.ts
53
+ function serializeHeader(salt, iv, metadata, authTag = Buffer.alloc(AUTH_TAG_LENGTH)) {
54
+ const metaJson = Buffer.from(JSON.stringify(metadata), "utf8");
55
+ const metaLen = Buffer.alloc(2);
56
+ metaLen.writeUInt16BE(metaJson.length, 0);
57
+ return Buffer.concat([
58
+ MAGIC,
59
+ Buffer.from([VERSION]),
60
+ salt,
61
+ iv,
62
+ authTag,
63
+ metaLen,
64
+ metaJson
65
+ ]);
66
+ }
67
+ function parseHeader(buf) {
68
+ let offset = 0;
69
+ const magic = buf.subarray(offset, offset + 4);
70
+ offset += 4;
71
+ if (!magic.equals(MAGIC)) {
72
+ throw new Error("Not a valid .enc-x file (bad magic number)");
73
+ }
74
+ const version = buf[offset++];
75
+ if (version !== VERSION) {
76
+ throw new Error(`Unsupported .enc-x version: ${version}`);
77
+ }
78
+ const salt = Buffer.from(buf.subarray(offset, offset + SALT_LENGTH));
79
+ offset += SALT_LENGTH;
80
+ const iv = Buffer.from(buf.subarray(offset, offset + IV_LENGTH));
81
+ offset += IV_LENGTH;
82
+ const authTag = Buffer.from(buf.subarray(offset, offset + AUTH_TAG_LENGTH));
83
+ offset += AUTH_TAG_LENGTH;
84
+ const metaLen = buf.readUInt16BE(offset);
85
+ offset += 2;
86
+ const metaJson = buf.subarray(offset, offset + metaLen);
87
+ offset += metaLen;
88
+ let metadata;
89
+ try {
90
+ metadata = JSON.parse(metaJson.toString("utf8"));
91
+ } catch {
92
+ throw new Error("Corrupted .enc-x file (invalid metadata)");
93
+ }
94
+ return { salt, iv, authTag, metadata, totalSize: offset };
95
+ }
96
+ var AUTH_TAG_OFFSET = 4 + 1 + SALT_LENGTH + IV_LENGTH;
97
+ var MIME_MAP = {
98
+ // Video
99
+ mp4: "video/mp4",
100
+ mkv: "video/x-matroska",
101
+ avi: "video/x-msvideo",
102
+ mov: "video/quicktime",
103
+ webm: "video/webm",
104
+ // Audio
105
+ mp3: "audio/mpeg",
106
+ wav: "audio/wav",
107
+ flac: "audio/flac",
108
+ aac: "audio/aac",
109
+ ogg: "audio/ogg",
110
+ // Images
111
+ jpg: "image/jpeg",
112
+ jpeg: "image/jpeg",
113
+ png: "image/png",
114
+ gif: "image/gif",
115
+ webp: "image/webp",
116
+ svg: "image/svg+xml",
117
+ // Documents
118
+ pdf: "application/pdf",
119
+ doc: "application/msword",
120
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
121
+ xls: "application/vnd.ms-excel",
122
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
123
+ ppt: "application/vnd.ms-powerpoint",
124
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
125
+ // Archives
126
+ zip: "application/zip",
127
+ tar: "application/x-tar",
128
+ gz: "application/gzip",
129
+ "7z": "application/x-7z-compressed",
130
+ rar: "application/vnd.rar",
131
+ // Text / code
132
+ txt: "text/plain",
133
+ json: "application/json",
134
+ xml: "application/xml",
135
+ html: "text/html",
136
+ css: "text/css",
137
+ js: "application/javascript",
138
+ ts: "application/typescript",
139
+ // Executables / binaries
140
+ exe: "application/x-msdownload",
141
+ dmg: "application/x-apple-diskimage",
142
+ iso: "application/x-iso9660-image"
143
+ };
144
+ function getMimeType(filePath) {
145
+ const ext = path3__default.default.extname(filePath).replace(".", "").toLowerCase();
146
+ return MIME_MAP[ext] ?? "application/octet-stream";
147
+ }
148
+ function createProgressStream(total, onProgress) {
149
+ let processed = 0;
150
+ return new stream.Transform({
151
+ transform(chunk, _encoding, callback) {
152
+ processed += chunk.length;
153
+ onProgress(processed, total);
154
+ callback(null, chunk);
155
+ }
156
+ });
157
+ }
158
+
159
+ // src/crypto/encrypt.ts
160
+ async function encryptFile(opts) {
161
+ const { inputPath, password } = opts;
162
+ const stat = await fs.promises.stat(inputPath);
163
+ const originalSize = stat.size;
164
+ const originalName = path3__default.default.basename(inputPath);
165
+ const mime = getMimeType(inputPath);
166
+ const salt = generateSalt();
167
+ const iv = generateIV(IV_LENGTH);
168
+ const key = await deriveKey(password, salt);
169
+ const metadata = {
170
+ name: originalName,
171
+ mime,
172
+ size: originalSize
173
+ };
174
+ const headerBuf = serializeHeader(salt, iv, metadata);
175
+ const outputPath = opts.outputPath ?? inputPath + FILE_EXTENSION;
176
+ const writeStream = fs.createWriteStream(outputPath);
177
+ await new Promise((resolve, reject) => {
178
+ writeStream.write(headerBuf, (err) => err ? reject(err) : resolve());
179
+ });
180
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
181
+ const readStream = fs.createReadStream(inputPath);
182
+ const progressStream = opts.onProgress ? createProgressStream(originalSize, opts.onProgress) : null;
183
+ const source = progressStream ? readStream.pipe(progressStream) : readStream;
184
+ await promises.pipeline(source, cipher, writeStream);
185
+ const authTag = cipher.getAuthTag();
186
+ const fd = await fs.promises.open(outputPath, "r+");
187
+ try {
188
+ await fd.write(authTag, 0, AUTH_TAG_LENGTH, AUTH_TAG_OFFSET);
189
+ } finally {
190
+ await fd.close();
191
+ }
192
+ return { outputPath, originalSize };
193
+ }
194
+
195
+ // src/utils/format.ts
196
+ function formatBytes(bytes) {
197
+ if (bytes === 0) return "0 B";
198
+ const units = ["B", "KB", "MB", "GB", "TB"];
199
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
200
+ const value = bytes / Math.pow(1024, i);
201
+ return `${value.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
202
+ }
203
+ function formatDuration(ms) {
204
+ if (ms < 1e3) return `${ms}ms`;
205
+ const s = ms / 1e3;
206
+ if (s < 60) return `${s.toFixed(1)}s`;
207
+ const m = Math.floor(s / 60);
208
+ const rem = (s % 60).toFixed(0).padStart(2, "0");
209
+ return `${m}m ${rem}s`;
210
+ }
211
+
212
+ // src/cli/ui.ts
213
+ var ui = {
214
+ info: (msg) => console.log(chalk__default.default.cyan("\u2139"), msg),
215
+ success: (msg) => console.log(chalk__default.default.green("\u2714"), msg),
216
+ error: (msg) => console.error(chalk__default.default.red("\u2716"), chalk__default.default.red(msg)),
217
+ warn: (msg) => console.warn(chalk__default.default.yellow("\u26A0"), chalk__default.default.yellow(msg)),
218
+ dim: (msg) => console.log(chalk__default.default.dim(msg))
219
+ };
220
+ function createProgressBar(label) {
221
+ const bar = new cliProgress__default.default.SingleBar(
222
+ {
223
+ format: `${chalk__default.default.cyan(label)} ${chalk__default.default.cyan("{bar}")} {percentage}% | {value_fmt} / {total_fmt} | ETA: {eta}s`,
224
+ barCompleteChar: "\u2588",
225
+ barIncompleteChar: "\u2591",
226
+ hideCursor: true,
227
+ clearOnComplete: false,
228
+ stopOnComplete: true
229
+ },
230
+ cliProgress__default.default.Presets.shades_classic
231
+ );
232
+ return {
233
+ start(total) {
234
+ bar.start(total, 0, {
235
+ value_fmt: formatBytes(0),
236
+ total_fmt: formatBytes(total)
237
+ });
238
+ },
239
+ update(value, total) {
240
+ bar.update(value, {
241
+ value_fmt: formatBytes(value),
242
+ total_fmt: formatBytes(total)
243
+ });
244
+ },
245
+ stop() {
246
+ bar.stop();
247
+ }
248
+ };
249
+ }
250
+
251
+ // src/cli/commands/encrypt.ts
252
+ async function runEncrypt(filePath, opts) {
253
+ const inputPath = path3__default.default.resolve(filePath);
254
+ if (!fs.existsSync(inputPath)) {
255
+ ui.error(`File not found: ${filePath}`);
256
+ process.exit(1);
257
+ }
258
+ if (inputPath.endsWith(FILE_EXTENSION)) {
259
+ ui.warn(
260
+ "Input file already has .enc-x extension \u2014 it may already be encrypted."
261
+ );
262
+ }
263
+ const outputPath = opts.output ?? inputPath + FILE_EXTENSION;
264
+ ui.info(`Encrypting ${chalk__default.default.bold(path3__default.default.basename(inputPath))}`);
265
+ ui.dim(` Output : ${outputPath}`);
266
+ ui.dim(` Cipher : AES-256-GCM | KDF: PBKDF2-SHA512 (100,000 iterations)`);
267
+ const bar = createProgressBar("Encrypting");
268
+ let barStarted = false;
269
+ const start = Date.now();
270
+ try {
271
+ const result = await encryptFile({
272
+ inputPath,
273
+ password: opts.key,
274
+ outputPath,
275
+ onProgress(processed, total) {
276
+ if (!barStarted) {
277
+ bar.start(total);
278
+ barStarted = true;
279
+ }
280
+ bar.update(processed, total);
281
+ }
282
+ });
283
+ if (barStarted) bar.stop();
284
+ const elapsed = Date.now() - start;
285
+ ui.success(
286
+ `Done in ${formatDuration(elapsed)} \u2014 ${chalk__default.default.bold(path3__default.default.basename(result.outputPath))} (${formatBytes(result.originalSize)})`
287
+ );
288
+ } catch (err) {
289
+ if (barStarted) bar.stop();
290
+ const msg = err instanceof Error ? err.message : String(err);
291
+ ui.error(`Encryption failed: ${msg}`);
292
+ process.exit(1);
293
+ }
294
+ }
295
+ var HEADER_READ_LIMIT = 4096;
296
+ async function decryptFile(opts) {
297
+ const { inputPath, password } = opts;
298
+ const stat = await fs.promises.stat(inputPath);
299
+ const totalSize = stat.size;
300
+ const fd = await fs.promises.open(inputPath, "r");
301
+ const headerBuf = Buffer.alloc(HEADER_READ_LIMIT);
302
+ const { bytesRead } = await fd.read(headerBuf, 0, HEADER_READ_LIMIT, 0);
303
+ await fd.close();
304
+ const header = parseHeader(headerBuf.subarray(0, bytesRead));
305
+ const { salt, iv, authTag, metadata, totalSize: headerSize } = header;
306
+ const key = await deriveKey(password, salt);
307
+ const outputDir = path3__default.default.dirname(inputPath);
308
+ const outputPath = opts.outputPath ?? path3__default.default.join(outputDir, metadata.name);
309
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
310
+ decipher.setAuthTag(authTag);
311
+ const ciphertextSize = totalSize - headerSize;
312
+ const readStream = fs.createReadStream(inputPath, { start: headerSize });
313
+ const progressStream = opts.onProgress ? createProgressStream(ciphertextSize, opts.onProgress) : null;
314
+ const writeStream = fs.createWriteStream(outputPath);
315
+ const source = progressStream ? readStream.pipe(progressStream) : readStream;
316
+ try {
317
+ await promises.pipeline(source, decipher, writeStream);
318
+ } catch (err) {
319
+ await fs.promises.unlink(outputPath).catch(() => {
320
+ });
321
+ const msg = err instanceof Error ? err.message : String(err);
322
+ if (msg.includes("Unsupported state") || msg.includes("bad decrypt") || msg.includes("auth") || msg.includes("ERR_CRYPTO")) {
323
+ throw new Error("Invalid password or corrupted file");
324
+ }
325
+ throw err;
326
+ }
327
+ return { outputPath, originalName: metadata.name };
328
+ }
329
+
330
+ // src/cli/commands/decrypt.ts
331
+ async function runDecrypt(filePath, opts) {
332
+ const inputPath = path3__default.default.resolve(filePath);
333
+ if (!fs.existsSync(inputPath)) {
334
+ ui.error(`File not found: ${filePath}`);
335
+ process.exit(1);
336
+ }
337
+ if (!inputPath.endsWith(FILE_EXTENSION)) {
338
+ ui.warn(
339
+ `File does not have a ${FILE_EXTENSION} extension \u2014 it may not be an encrypted file.`
340
+ );
341
+ }
342
+ ui.info(`Decrypting ${chalk__default.default.bold(path3__default.default.basename(inputPath))}`);
343
+ const bar = createProgressBar("Decrypting");
344
+ let barStarted = false;
345
+ const start = Date.now();
346
+ try {
347
+ const result = await decryptFile({
348
+ inputPath,
349
+ password: opts.key,
350
+ outputPath: opts.output,
351
+ onProgress(processed, total) {
352
+ if (!barStarted) {
353
+ bar.start(total);
354
+ barStarted = true;
355
+ }
356
+ bar.update(processed, total);
357
+ }
358
+ });
359
+ if (barStarted) bar.stop();
360
+ const elapsed = Date.now() - start;
361
+ ui.success(
362
+ `Done in ${formatDuration(elapsed)} \u2014 restored ${chalk__default.default.bold(result.originalName)}`
363
+ );
364
+ ui.dim(` Output : ${result.outputPath}`);
365
+ } catch (err) {
366
+ if (barStarted) bar.stop();
367
+ const msg = err instanceof Error ? err.message : String(err);
368
+ if (msg === "Invalid password or corrupted file") {
369
+ ui.error("Invalid password or corrupted file");
370
+ } else {
371
+ ui.error(`Decryption failed: ${msg}`);
372
+ }
373
+ process.exit(1);
374
+ }
375
+ }
376
+
377
+ // src/index.ts
378
+ var program = new commander.Command();
379
+ program.name("enc-x").description(
380
+ chalk__default.default.cyan("enc-x") + " \u2014 secure AES-256-GCM file encryption & decryption"
381
+ ).version("1.1.1", "-v, --version").argument("<file>", "file to encrypt or decrypt (auto-detected by extension)").requiredOption("-k, --key <password>", "password").option("-o, --output <path>", "custom output path").action(async (file, opts) => {
382
+ if (file.endsWith(FILE_EXTENSION)) {
383
+ await runDecrypt(file, opts);
384
+ } else {
385
+ await runEncrypt(file, opts);
386
+ }
387
+ });
388
+ program.parse(process.argv);
389
+ //# sourceMappingURL=index.js.map
390
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/crypto/constants.ts","../src/crypto/keys.ts","../src/crypto/header.ts","../src/utils/mime.ts","../src/utils/progress.ts","../src/crypto/encrypt.ts","../src/utils/format.ts","../src/cli/ui.ts","../src/cli/commands/encrypt.ts","../src/crypto/decrypt.ts","../src/cli/commands/decrypt.ts","../src/index.ts"],"names":["randomBytes","pbkdf2","path","Transform","fsp","createWriteStream","createCipheriv","createReadStream","pipeline","chalk","cliProgress","existsSync","createDecipheriv","Command"],"mappings":"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","file":"index.js","sourcesContent":["// Encryption constants\nexport const ALGORITHM = \"aes-256-gcm\" as const;\nexport const KEY_LENGTH = 32; // 256 bits\nexport const IV_LENGTH = 12; // 96 bits — recommended for GCM\nexport const SALT_LENGTH = 32; // 256 bits\nexport const AUTH_TAG_LENGTH = 16; // 128 bits — GCM auth tag\nexport const PBKDF2_ITERATIONS = 100_000;\nexport const PBKDF2_DIGEST = \"sha512\" as const;\nexport const FILE_EXTENSION = \".enc-x\";\n\n// Header layout (binary, written at start of .enc-x file):\n//\n// [4 bytes] magic number 0x454E4358 (\"ENCX\")\n// [1 byte] version 0x01\n// [32 bytes] salt\n// [12 bytes] IV\n// [16 bytes] GCM auth tag (written after encryption, patched back)\n// [2 bytes] metadata JSON length (uint16 BE)\n// [N bytes] metadata JSON (UTF-8)\n//\nexport const MAGIC = Buffer.from([0x45, 0x4e, 0x43, 0x58]); // \"ENCX\"\nexport const VERSION = 0x01;\n\nexport const HEADER_FIXED_SIZE =\n 4 + // magic\n 1 + // version\n SALT_LENGTH +\n IV_LENGTH +\n AUTH_TAG_LENGTH +\n 2; // metadata length prefix\n","import { randomBytes, pbkdf2 } from \"node:crypto\";\nimport {\n KEY_LENGTH,\n SALT_LENGTH,\n PBKDF2_ITERATIONS,\n PBKDF2_DIGEST,\n} from \"@/crypto/constants\";\n\n/**\n * Generate a cryptographically secure random salt.\n */\nexport function generateSalt(): Buffer {\n return randomBytes(SALT_LENGTH);\n}\n\n/**\n * Generate a cryptographically secure random IV.\n */\nexport function generateIV(length: number): Buffer {\n return randomBytes(length);\n}\n\n/**\n * Derive a 256-bit key from a password + salt using PBKDF2-SHA512.\n */\nexport function deriveKey(password: string, salt: Buffer): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n pbkdf2(\n password,\n salt,\n PBKDF2_ITERATIONS,\n KEY_LENGTH,\n PBKDF2_DIGEST,\n (err, key) => {\n if (err) reject(err);\n else resolve(key);\n },\n );\n });\n}\n","import {\n MAGIC,\n VERSION,\n SALT_LENGTH,\n IV_LENGTH,\n AUTH_TAG_LENGTH,\n} from \"@/crypto/constants\";\n\nexport interface FileMetadata {\n name: string;\n mime: string;\n size?: number;\n}\n\nexport interface EncxHeader {\n salt: Buffer;\n iv: Buffer;\n authTag: Buffer;\n metadata: FileMetadata;\n /** Total byte length of the header (fixed + variable JSON) */\n totalSize: number;\n}\n\n/**\n * Serialize the header into a Buffer.\n * Auth tag is written as 16 zero bytes and must be patched after encryption.\n */\nexport function serializeHeader(\n salt: Buffer,\n iv: Buffer,\n metadata: FileMetadata,\n authTag: Buffer = Buffer.alloc(AUTH_TAG_LENGTH),\n): Buffer {\n const metaJson = Buffer.from(JSON.stringify(metadata), \"utf8\");\n const metaLen = Buffer.alloc(2);\n metaLen.writeUInt16BE(metaJson.length, 0);\n\n return Buffer.concat([\n MAGIC,\n Buffer.from([VERSION]),\n salt,\n iv,\n authTag,\n metaLen,\n metaJson,\n ]);\n}\n\n/**\n * Parse a header from the start of an .enc-x file buffer.\n * Throws if the magic number or version is invalid.\n */\nexport function parseHeader(buf: Buffer): EncxHeader {\n let offset = 0;\n\n // Magic\n const magic = buf.subarray(offset, offset + 4);\n offset += 4;\n if (!magic.equals(MAGIC)) {\n throw new Error(\"Not a valid .enc-x file (bad magic number)\");\n }\n\n // Version\n const version = buf[offset++];\n if (version !== VERSION) {\n throw new Error(`Unsupported .enc-x version: ${version}`);\n }\n\n // Salt\n const salt = Buffer.from(buf.subarray(offset, offset + SALT_LENGTH));\n offset += SALT_LENGTH;\n\n // IV\n const iv = Buffer.from(buf.subarray(offset, offset + IV_LENGTH));\n offset += IV_LENGTH;\n\n // Auth tag\n const authTag = Buffer.from(buf.subarray(offset, offset + AUTH_TAG_LENGTH));\n offset += AUTH_TAG_LENGTH;\n\n // Metadata length\n const metaLen = buf.readUInt16BE(offset);\n offset += 2;\n\n // Metadata JSON\n const metaJson = buf.subarray(offset, offset + metaLen);\n offset += metaLen;\n\n let metadata: FileMetadata;\n try {\n metadata = JSON.parse(metaJson.toString(\"utf8\")) as FileMetadata;\n } catch {\n throw new Error(\"Corrupted .enc-x file (invalid metadata)\");\n }\n\n return { salt, iv, authTag, metadata, totalSize: offset };\n}\n\n/**\n * Patch the auth tag into an already-written header in a file.\n * The auth tag sits at a fixed offset after magic(4) + version(1) + salt(32) + iv(12).\n */\nexport const AUTH_TAG_OFFSET = 4 + 1 + SALT_LENGTH + IV_LENGTH;\n","import path from \"node:path\";\n\n// Lightweight extension → MIME map (no external deps)\nconst MIME_MAP: Record<string, string> = {\n // Video\n mp4: \"video/mp4\",\n mkv: \"video/x-matroska\",\n avi: \"video/x-msvideo\",\n mov: \"video/quicktime\",\n webm: \"video/webm\",\n // Audio\n mp3: \"audio/mpeg\",\n wav: \"audio/wav\",\n flac: \"audio/flac\",\n aac: \"audio/aac\",\n ogg: \"audio/ogg\",\n // Images\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n svg: \"image/svg+xml\",\n // Documents\n pdf: \"application/pdf\",\n doc: \"application/msword\",\n docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n xls: \"application/vnd.ms-excel\",\n xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n ppt: \"application/vnd.ms-powerpoint\",\n pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n // Archives\n zip: \"application/zip\",\n tar: \"application/x-tar\",\n gz: \"application/gzip\",\n \"7z\": \"application/x-7z-compressed\",\n rar: \"application/vnd.rar\",\n // Text / code\n txt: \"text/plain\",\n json: \"application/json\",\n xml: \"application/xml\",\n html: \"text/html\",\n css: \"text/css\",\n js: \"application/javascript\",\n ts: \"application/typescript\",\n // Executables / binaries\n exe: \"application/x-msdownload\",\n dmg: \"application/x-apple-diskimage\",\n iso: \"application/x-iso9660-image\",\n};\n\nexport function getMimeType(filePath: string): string {\n const ext = path.extname(filePath).replace(\".\", \"\").toLowerCase();\n return MIME_MAP[ext] ?? \"application/octet-stream\";\n}\n","import { Transform, type TransformCallback } from \"node:stream\";\n\n/**\n * A passthrough Transform stream that tracks bytes flowing through it\n * and calls onProgress(bytesProcessed, total).\n */\nexport function createProgressStream(\n total: number,\n onProgress: (bytesProcessed: number, total: number) => void,\n): Transform {\n let processed = 0;\n\n return new Transform({\n transform(\n chunk: Buffer,\n _encoding: BufferEncoding,\n callback: TransformCallback,\n ) {\n processed += chunk.length;\n onProgress(processed, total);\n callback(null, chunk);\n },\n });\n}\n","import { createCipheriv } from \"node:crypto\";\nimport { createReadStream, createWriteStream, promises as fsp } from \"node:fs\";\nimport { pipeline } from \"node:stream/promises\";\nimport { Transform } from \"node:stream\";\nimport path from \"node:path\";\nimport {\n ALGORITHM,\n IV_LENGTH,\n FILE_EXTENSION,\n AUTH_TAG_LENGTH,\n} from \"@/crypto/constants\";\nimport { generateSalt, generateIV, deriveKey } from \"@/crypto/keys\";\nimport {\n serializeHeader,\n AUTH_TAG_OFFSET,\n type FileMetadata,\n} from \"@/crypto/header\";\nimport { getMimeType } from \"@/utils/mime\";\nimport { createProgressStream } from \"@/utils/progress\";\n\nexport interface EncryptOptions {\n inputPath: string;\n password: string;\n outputPath?: string;\n onProgress?: (bytesProcessed: number, total: number) => void;\n}\n\nexport interface EncryptResult {\n outputPath: string;\n originalSize: number;\n}\n\nexport async function encryptFile(\n opts: EncryptOptions,\n): Promise<EncryptResult> {\n const { inputPath, password } = opts;\n\n // Stat the input file\n const stat = await fsp.stat(inputPath);\n const originalSize = stat.size;\n const originalName = path.basename(inputPath);\n const mime = getMimeType(inputPath);\n\n // Derive key\n const salt = generateSalt();\n const iv = generateIV(IV_LENGTH);\n const key = await deriveKey(password, salt);\n\n // Build metadata\n const metadata: FileMetadata = {\n name: originalName,\n mime,\n size: originalSize,\n };\n\n // Write placeholder header (auth tag = 16 zero bytes, patched later)\n const headerBuf = serializeHeader(salt, iv, metadata);\n\n // Output path\n const outputPath = opts.outputPath ?? inputPath + FILE_EXTENSION;\n const writeStream = createWriteStream(outputPath);\n\n // Write header first\n await new Promise<void>((resolve, reject) => {\n writeStream.write(headerBuf, (err) => (err ? reject(err) : resolve()));\n });\n\n // Create cipher\n const cipher = createCipheriv(ALGORITHM, key, iv);\n\n // Stream: input → [progress] → cipher → output\n const readStream = createReadStream(inputPath);\n const progressStream = opts.onProgress\n ? createProgressStream(originalSize, opts.onProgress)\n : null;\n\n const source = progressStream ? readStream.pipe(progressStream) : readStream;\n\n await pipeline(source as NodeJS.ReadableStream, cipher, writeStream);\n\n // Retrieve and patch auth tag back into the file header\n const authTag = cipher.getAuthTag();\n const fd = await fsp.open(outputPath, \"r+\");\n try {\n await fd.write(authTag, 0, AUTH_TAG_LENGTH, AUTH_TAG_OFFSET);\n } finally {\n await fd.close();\n }\n\n return { outputPath, originalSize };\n}\n","/**\n * Format bytes into a human-readable string.\n */\nexport function formatBytes(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(1024));\n const value = bytes / Math.pow(1024, i);\n return `${value.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;\n}\n\n/**\n * Format milliseconds into a human-readable duration.\n */\nexport function formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n const s = ms / 1000;\n if (s < 60) return `${s.toFixed(1)}s`;\n const m = Math.floor(s / 60);\n const rem = (s % 60).toFixed(0).padStart(2, \"0\");\n return `${m}m ${rem}s`;\n}\n","import chalk from \"chalk\";\nimport cliProgress from \"cli-progress\";\nimport { formatBytes } from \"@/utils/format\";\n\nexport const ui = {\n info: (msg: string) => console.log(chalk.cyan(\"ℹ\"), msg),\n success: (msg: string) => console.log(chalk.green(\"✔\"), msg),\n error: (msg: string) => console.error(chalk.red(\"✖\"), chalk.red(msg)),\n warn: (msg: string) => console.warn(chalk.yellow(\"⚠\"), chalk.yellow(msg)),\n dim: (msg: string) => console.log(chalk.dim(msg)),\n};\n\nexport function createProgressBar(label: string) {\n const bar = new cliProgress.SingleBar(\n {\n format:\n `${chalk.cyan(label)} ${chalk.cyan(\"{bar}\")} {percentage}%` +\n ` | {value_fmt} / {total_fmt} | ETA: {eta}s`,\n barCompleteChar: \"█\",\n barIncompleteChar: \"░\",\n hideCursor: true,\n clearOnComplete: false,\n stopOnComplete: true,\n },\n cliProgress.Presets.shades_classic,\n );\n\n return {\n start(total: number) {\n bar.start(total, 0, {\n value_fmt: formatBytes(0),\n total_fmt: formatBytes(total),\n });\n },\n update(value: number, total: number) {\n bar.update(value, {\n value_fmt: formatBytes(value),\n total_fmt: formatBytes(total),\n });\n },\n stop() {\n bar.stop();\n },\n };\n}\n","import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport chalk from \"chalk\";\nimport { encryptFile } from \"@/crypto/encrypt\";\nimport { FILE_EXTENSION } from \"@/crypto/constants\";\nimport { ui, createProgressBar } from \"@/cli/ui\";\nimport { formatBytes, formatDuration } from \"@/utils/format\";\n\nexport interface EncryptCommandOptions {\n key: string;\n output?: string;\n}\n\nexport async function runEncrypt(\n filePath: string,\n opts: EncryptCommandOptions,\n): Promise<void> {\n const inputPath = path.resolve(filePath);\n\n if (!existsSync(inputPath)) {\n ui.error(`File not found: ${filePath}`);\n process.exit(1);\n }\n\n if (inputPath.endsWith(FILE_EXTENSION)) {\n ui.warn(\n \"Input file already has .enc-x extension — it may already be encrypted.\",\n );\n }\n\n const outputPath = opts.output ?? inputPath + FILE_EXTENSION;\n\n ui.info(`Encrypting ${chalk.bold(path.basename(inputPath))}`);\n ui.dim(` Output : ${outputPath}`);\n ui.dim(` Cipher : AES-256-GCM | KDF: PBKDF2-SHA512 (100,000 iterations)`);\n\n const bar = createProgressBar(\"Encrypting\");\n let barStarted = false;\n const start = Date.now();\n\n try {\n const result = await encryptFile({\n inputPath,\n password: opts.key,\n outputPath,\n onProgress(processed, total) {\n if (!barStarted) {\n bar.start(total);\n barStarted = true;\n }\n bar.update(processed, total);\n },\n });\n\n if (barStarted) bar.stop();\n\n const elapsed = Date.now() - start;\n ui.success(\n `Done in ${formatDuration(elapsed)} — ` +\n `${chalk.bold(path.basename(result.outputPath))} ` +\n `(${formatBytes(result.originalSize)})`,\n );\n } catch (err: unknown) {\n if (barStarted) bar.stop();\n const msg = err instanceof Error ? err.message : String(err);\n ui.error(`Encryption failed: ${msg}`);\n process.exit(1);\n }\n}\n","import { createDecipheriv } from \"node:crypto\";\nimport { createReadStream, createWriteStream, promises as fsp } from \"node:fs\";\nimport { pipeline } from \"node:stream/promises\";\nimport { PassThrough } from \"node:stream\";\nimport path from \"node:path\";\nimport { ALGORITHM } from \"@/crypto/constants\";\nimport { deriveKey } from \"@/crypto/keys\";\nimport { parseHeader } from \"@/crypto/header\";\nimport { createProgressStream } from \"@/utils/progress\";\n\nexport interface DecryptOptions {\n inputPath: string;\n password: string;\n outputPath?: string;\n onProgress?: (bytesProcessed: number, total: number) => void;\n}\n\nexport interface DecryptResult {\n outputPath: string;\n originalName: string;\n}\n\n// How many bytes to read upfront to parse the header\n// Max header = fixed(67) + 2(len) + 65535(max JSON) — in practice ~200 bytes\nconst HEADER_READ_LIMIT = 4096;\n\nexport async function decryptFile(\n opts: DecryptOptions,\n): Promise<DecryptResult> {\n const { inputPath, password } = opts;\n\n const stat = await fsp.stat(inputPath);\n const totalSize = stat.size;\n\n // Read enough bytes to parse the header\n const fd = await fsp.open(inputPath, \"r\");\n const headerBuf = Buffer.alloc(HEADER_READ_LIMIT);\n const { bytesRead } = await fd.read(headerBuf, 0, HEADER_READ_LIMIT, 0);\n await fd.close();\n\n const header = parseHeader(headerBuf.subarray(0, bytesRead));\n const { salt, iv, authTag, metadata, totalSize: headerSize } = header;\n\n // Derive key from password + salt stored in file\n const key = await deriveKey(password, salt);\n\n // Determine output path\n const outputDir = path.dirname(inputPath);\n const outputPath = opts.outputPath ?? path.join(outputDir, metadata.name);\n\n // Create decipher and set auth tag\n const decipher = createDecipheriv(ALGORITHM, key, iv);\n decipher.setAuthTag(authTag);\n\n // Stream ciphertext (skip header bytes) → decipher → output file\n const ciphertextSize = totalSize - headerSize;\n const readStream = createReadStream(inputPath, { start: headerSize });\n\n const progressStream = opts.onProgress\n ? createProgressStream(ciphertextSize, opts.onProgress)\n : null;\n\n const writeStream = createWriteStream(outputPath);\n\n const source = progressStream ? readStream.pipe(progressStream) : readStream;\n\n try {\n await pipeline(source as NodeJS.ReadableStream, decipher, writeStream);\n } catch (err: unknown) {\n // Clean up partial output on auth failure\n await fsp.unlink(outputPath).catch(() => {});\n\n const msg = err instanceof Error ? err.message : String(err);\n if (\n msg.includes(\"Unsupported state\") ||\n msg.includes(\"bad decrypt\") ||\n msg.includes(\"auth\") ||\n msg.includes(\"ERR_CRYPTO\")\n ) {\n throw new Error(\"Invalid password or corrupted file\");\n }\n throw err;\n }\n\n return { outputPath, originalName: metadata.name };\n}\n","import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport chalk from \"chalk\";\nimport { decryptFile } from \"@/crypto/decrypt\";\nimport { FILE_EXTENSION } from \"@/crypto/constants\";\nimport { ui, createProgressBar } from \"@/cli/ui\";\nimport { formatDuration } from \"@/utils/format\";\n\nexport interface DecryptCommandOptions {\n key: string;\n output?: string;\n}\n\nexport async function runDecrypt(\n filePath: string,\n opts: DecryptCommandOptions,\n): Promise<void> {\n const inputPath = path.resolve(filePath);\n\n if (!existsSync(inputPath)) {\n ui.error(`File not found: ${filePath}`);\n process.exit(1);\n }\n\n if (!inputPath.endsWith(FILE_EXTENSION)) {\n ui.warn(\n `File does not have a ${FILE_EXTENSION} extension — it may not be an encrypted file.`,\n );\n }\n\n ui.info(`Decrypting ${chalk.bold(path.basename(inputPath))}`);\n\n const bar = createProgressBar(\"Decrypting\");\n let barStarted = false;\n const start = Date.now();\n\n try {\n const result = await decryptFile({\n inputPath,\n password: opts.key,\n outputPath: opts.output,\n onProgress(processed, total) {\n if (!barStarted) {\n bar.start(total);\n barStarted = true;\n }\n bar.update(processed, total);\n },\n });\n\n if (barStarted) bar.stop();\n\n const elapsed = Date.now() - start;\n ui.success(\n `Done in ${formatDuration(elapsed)} — ` +\n `restored ${chalk.bold(result.originalName)}`,\n );\n ui.dim(` Output : ${result.outputPath}`);\n } catch (err: unknown) {\n if (barStarted) bar.stop();\n const msg = err instanceof Error ? err.message : String(err);\n\n if (msg === \"Invalid password or corrupted file\") {\n ui.error(\"Invalid password or corrupted file\");\n } else {\n ui.error(`Decryption failed: ${msg}`);\n }\n process.exit(1);\n }\n}\n","import { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { runEncrypt } from \"@/cli/commands/encrypt\";\nimport { runDecrypt } from \"@/cli/commands/decrypt\";\nimport { FILE_EXTENSION } from \"@/crypto/constants\";\n\nconst program = new Command();\n\nprogram\n .name(\"enc-x\")\n .description(\n chalk.cyan(\"enc-x\") + \" — secure AES-256-GCM file encryption & decryption\",\n )\n .version(\"1.1.1\", \"-v, --version\")\n .argument(\"<file>\", \"file to encrypt or decrypt (auto-detected by extension)\")\n .requiredOption(\"-k, --key <password>\", \"password\")\n .option(\"-o, --output <path>\", \"custom output path\")\n .action(async (file: string, opts: { key: string; output?: string }) => {\n if (file.endsWith(FILE_EXTENSION)) {\n await runDecrypt(file, opts);\n } else {\n await runEncrypt(file, opts);\n }\n });\n\nprogram.parse(process.argv);\n"]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "enc-x",
3
+ "version": "1.1.1",
4
+ "description": "Secure AES-256-GCM file encryption & decryption CLI",
5
+ "bin": {
6
+ "enc-x": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsup",
13
+ "dev": "tsup --watch",
14
+ "typecheck": "tsc --noEmit",
15
+ "test": "jest --runInBand",
16
+ "test:coverage": "jest --runInBand --coverage"
17
+ },
18
+ "dependencies": {
19
+ "chalk": "5.6.2",
20
+ "cli-progress": "3.12.0",
21
+ "commander": "14.0.3"
22
+ },
23
+ "devDependencies": {
24
+ "@types/cli-progress": "3.11.6",
25
+ "@types/jest": "^30.0.0",
26
+ "@types/node": "22.15.3",
27
+ "jest": "^30.3.0",
28
+ "ts-jest": "^29.4.9",
29
+ "tsup": "8.5.1",
30
+ "typescript": "6.0.2"
31
+ }
32
+ }