enkripsi-file 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/index.js +1064 -0
  4. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 ALDY
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # ๐Ÿ” enkripsi-file
2
+
3
+ > **Bulk File Encryption CLI** โ€” Enkripsi & dekripsi semua file di dalam folder secara massal menggunakan AES-256 via Node.js.
4
+
5
+ ```
6
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—
7
+ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•
8
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•
9
+ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ•”โ•
10
+ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘
11
+ โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•
12
+ โ”€โ”€โ”€ FileCrypt v1.0.0 โ€ข by ALDY
13
+ ```
14
+
15
+ ---
16
+
17
+ ## ๐Ÿ“ฆ Instalasi Global (via npm)
18
+
19
+ ```bash
20
+ npm install -g enkripsi-file
21
+ ```
22
+
23
+ Setelah install, langsung jalankan dari terminal mana saja:
24
+
25
+ ```bash
26
+ enkripsi-file
27
+ ```
28
+
29
+ Menu interaktif akan langsung muncul โ€” tidak perlu argumen apapun.
30
+
31
+ ---
32
+
33
+ ## โœจ Fitur Utama
34
+
35
+ | Fitur | Detail |
36
+ |---|---|
37
+ | **Menu interaktif** | Cukup ketik `enkripsi-file` โ€” tanpa hafal syntax |
38
+ | **Dua mode keamanan** | Easy (AES-256-CBC) & Hard/Military (AES-256-GCM) |
39
+ | **Authenticated Encryption** | Mode Hard: GCM + Auth Tag โ€” deteksi manipulasi file |
40
+ | **Anti Brute-Force** | Mode Hard: scrypt (N=65536, r=8, p=2) |
41
+ | **Stream-based** | Aman untuk file hingga 1 GB |
42
+ | **Auto-detect mode** | Saat dekripsi, mode terdeteksi dari magic bytes header |
43
+ | **Password tersembunyi** | Input password tidak terlihat di terminal |
44
+ | **Spinner animasi** | Progress realtime setiap file |
45
+ | **Cleanup parsial** | File rusak akibat error otomatis dihapus |
46
+
47
+ ---
48
+
49
+ ## ๐Ÿš€ Cara Pakai
50
+
51
+ ### Sekali ketik, langsung menu:
52
+ ```bash
53
+ enkripsi-file
54
+ ```
55
+
56
+ ### Atau dengan argumen (mode lama):
57
+ ```bash
58
+ enkripsi-file encrypt ./folder --mode hard
59
+ enkripsi-file decrypt ./folder
60
+ ```
61
+
62
+ ---
63
+
64
+ ## ๐Ÿ›ก๏ธ Mode Enkripsi
65
+
66
+ ### ๐ŸŸข EASY โ€” AES-256-CBC + PBKDF2
67
+ - Key derivation: PBKDF2-SHA512, 100.000 iterasi
68
+ - Header: `MAGIC(6) | SALT(32) | IV(16)` = 54 byte
69
+ - Cocok untuk: penggunaan umum, file banyak, kecepatan prioritas
70
+
71
+ ### ๐Ÿ”ด HARD โ€” AES-256-GCM + scrypt (Military Grade)
72
+ - Key derivation: scrypt (N=65536, r=8, p=2)
73
+ - Header: `MAGIC(6) | SALT(32) | IV(12) | AUTH_TAG(16)` = 66 byte
74
+ - Cocok untuk: dokumen sensitif, data keuangan/medis/hukum
75
+ - โš ๏ธ ~1โ€“3 detik/file (by design, bukan bug)
76
+
77
+ ---
78
+
79
+ ## ๐Ÿ“‹ Tampilan Terminal
80
+
81
+ ```
82
+ โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
83
+ โ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ•‘
84
+ โ•‘ ... โ•‘
85
+ โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
86
+ โ•‘ โ–ธ FileCrypt โ€” Bulk File Encryption CLI โ•‘
87
+ โ•‘ โ–ธ by ALDY ยท v1.0.0 โ•‘
88
+ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
89
+
90
+ ? Pilih aksi:
91
+ โฏ ๐Ÿ”’ ENKRIPSI โ€” Enkripsi semua file di dalam folder
92
+ ๐Ÿ”“ DEKRIPSI โ€” Dekripsi semua file .enc di dalam folder
93
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
94
+ ๐Ÿšช Keluar
95
+ ```
96
+
97
+ ---
98
+
99
+ ## โš™๏ธ Persyaratan
100
+
101
+ - Node.js **โ‰ฅ 16.0.0**
102
+ - npm
103
+
104
+ ---
105
+
106
+ ## ๐Ÿ“ Lisensi
107
+
108
+ MIT โ€” ยฉ 2024 ALDY
package/index.js ADDED
@@ -0,0 +1,1064 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
6
+ * โ•‘ FileCrypt โ€” Bulk File Encryption CLI โ•‘
7
+ * โ•‘ AES-256-CBC (Easy) ยท AES-256-GCM (Hard/Military Grade) โ•‘
8
+ * โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
9
+ *
10
+ * Struktur header file .enc:
11
+ *
12
+ * [EASY] MAGIC(6) | SALT(32) | IV(16) = 54 bytes header
13
+ * โ†’ Algoritma : AES-256-CBC
14
+ * โ†’ KDF : PBKDF2-SHA512, 100.000 iterasi
15
+ *
16
+ * [HARD] MAGIC(6) | SALT(32) | IV(12) | AUTH_TAG(16) = 66 bytes header
17
+ * โ†’ Algoritma : AES-256-GCM (Authenticated Encryption)
18
+ * โ†’ KDF : scrypt (N=65536, r=8, p=2)
19
+ * โ†’ Auth tag disimpan di header setelah enkripsi selesai
20
+ */
21
+
22
+ const { program } = require('commander');
23
+ const inquirer = require('inquirer');
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const crypto = require('crypto');
27
+ const { pipeline } = require('stream/promises');
28
+
29
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
30
+ // ยง CONSTANTS & CONFIGURATION
31
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
32
+
33
+ const MAX_FILE_SIZE = 1_073_741_824; // 1 GB
34
+
35
+ /** Magic bytes โ€” identifier mode enkripsi (6 bytes ASCII) */
36
+ const MAGIC = {
37
+ EASY: Buffer.from('FENC01'), // โ†’ AES-256-CBC + PBKDF2
38
+ HARD: Buffer.from('FENC02'), // โ†’ AES-256-GCM + scrypt
39
+ };
40
+
41
+ const CFG = {
42
+ easy: {
43
+ magic: MAGIC.EASY,
44
+ algorithm: 'aes-256-cbc',
45
+ saltSize: 32,
46
+ ivSize: 16,
47
+ keySize: 32,
48
+ iterations: 100_000,
49
+ digest: 'sha512',
50
+ // Header layout: MAGIC(6) + SALT(32) + IV(16) = 54 bytes total
51
+ headerSize: 6 + 32 + 16, // 54
52
+ saltOffset: 6,
53
+ ivOffset: 6 + 32, // 38
54
+ },
55
+ hard: {
56
+ magic: MAGIC.HARD,
57
+ algorithm: 'aes-256-gcm',
58
+ saltSize: 32,
59
+ ivSize: 12,
60
+ authTagSize: 16,
61
+ keySize: 32,
62
+ scryptN: 65536, // CPU/memory cost factor
63
+ scryptR: 8, // block size
64
+ scryptP: 2, // parallelization factor
65
+ // Header layout: MAGIC(6) + SALT(32) + IV(12) + AUTH_TAG(16) = 66 bytes total
66
+ headerSize: 6 + 32 + 12 + 16, // 66
67
+ saltOffset: 6,
68
+ ivOffset: 6 + 32, // 38
69
+ authTagOffset: 6 + 32 + 12, // 50
70
+ },
71
+ };
72
+
73
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
74
+ // ยง ANSI COLOR HELPERS
75
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
76
+
77
+ const C = {
78
+ reset: '\x1b[0m',
79
+ bold: '\x1b[1m',
80
+ dim: '\x1b[2m',
81
+ italic: '\x1b[3m',
82
+ underline: '\x1b[4m',
83
+ // Foreground
84
+ black: '\x1b[30m',
85
+ red: '\x1b[31m',
86
+ green: '\x1b[32m',
87
+ yellow: '\x1b[33m',
88
+ blue: '\x1b[34m',
89
+ magenta: '\x1b[35m',
90
+ cyan: '\x1b[36m',
91
+ white: '\x1b[37m',
92
+ gray: '\x1b[90m',
93
+ brightRed: '\x1b[91m',
94
+ brightGreen:'\x1b[92m',
95
+ brightYellow:'\x1b[93m',
96
+ brightBlue: '\x1b[94m',
97
+ brightMagenta:'\x1b[95m',
98
+ brightCyan: '\x1b[96m',
99
+ // Background
100
+ bgBlack: '\x1b[40m',
101
+ bgRed: '\x1b[41m',
102
+ bgGreen: '\x1b[42m',
103
+ bgYellow: '\x1b[43m',
104
+ bgBlue: '\x1b[44m',
105
+ bgMagenta: '\x1b[45m',
106
+ bgCyan: '\x1b[46m',
107
+ };
108
+
109
+ const color = (c, txt) => `${C[c] ?? ''}${txt}${C.reset}`;
110
+ const bold = (txt) => `${C.bold}${txt}${C.reset}`;
111
+ const dim = (txt) => `${C.dim}${txt}${C.reset}`;
112
+
113
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
114
+ // ยง VISUAL CONSTANTS & HELPERS
115
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
116
+
117
+ /** ASCII-art "ALDY" โ€” ditampilkan di banner utama & saat install */
118
+ const ALDY_ART = [
119
+ ' โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—',
120
+ 'โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•',
121
+ 'โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• ',
122
+ 'โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ•”โ• ',
123
+ 'โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ ',
124
+ 'โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• ',
125
+ ];
126
+
127
+ /** Lebar konten dalam box (karakter) */
128
+ const BOX_W = 62;
129
+
130
+ /** Buat satu baris box dengan padding */
131
+ function boxLine(content = '', align = 'left') {
132
+ const stripped = content.replace(/\x1b\[[0-9;]*m/g, ''); // hapus ANSI untuk hitung lebar
133
+ const pad = BOX_W - 2 - stripped.length;
134
+ if (align === 'center') {
135
+ const l = Math.floor(pad / 2);
136
+ const r = pad - l;
137
+ return `${C.cyan}โ•‘${C.reset}${' '.repeat(l)}${content}${' '.repeat(r)}${C.cyan}โ•‘${C.reset}`;
138
+ }
139
+ return `${C.cyan}โ•‘${C.reset} ${content}${' '.repeat(Math.max(0, pad - 1))}${C.cyan}โ•‘${C.reset}`;
140
+ }
141
+
142
+ const boxTop = () =>
143
+ `${C.cyan}โ•”${'โ•'.repeat(BOX_W - 2)}โ•—${C.reset}`;
144
+ const boxMid = () =>
145
+ `${C.cyan}โ• ${'โ•'.repeat(BOX_W - 2)}โ•ฃ${C.reset}`;
146
+ const boxSep = () =>
147
+ `${C.cyan}โ•Ÿ${'โ”€'.repeat(BOX_W - 2)}โ•ข${C.reset}`;
148
+ const boxBot = () =>
149
+ `${C.cyan}โ•š${'โ•'.repeat(BOX_W - 2)}โ•${C.reset}`;
150
+
151
+ /**
152
+ * Spinner sederhana โ€” tampilkan animasi saat operasi berjalan.
153
+ * @param {string} text - label di sebelah spinner
154
+ * @returns {{ stop(finalLine: string): void }}
155
+ */
156
+ function createSpinner(text) {
157
+ const frames = ['โ ‹','โ ™','โ น','โ ธ','โ ผ','โ ด','โ ฆ','โ ง','โ ‡','โ '];
158
+ let i = 0;
159
+ const iv = setInterval(() => {
160
+ process.stdout.write(`\r ${color('brightCyan', frames[i % frames.length])} ${color('dim', text)} `);
161
+ i++;
162
+ }, 80);
163
+ return {
164
+ stop(finalLine) {
165
+ clearInterval(iv);
166
+ process.stdout.write(`\r${finalLine}\n`);
167
+ },
168
+ };
169
+ }
170
+
171
+ /** Print watermark ALDY dengan warna gradien */
172
+ function printWatermark() {
173
+ const gradients = ['brightMagenta','magenta','brightBlue','blue','brightCyan','cyan'];
174
+ console.log('');
175
+ ALDY_ART.forEach((line, i) => {
176
+ console.log(` ${color(gradients[i % gradients.length], bold(line))}`);
177
+ });
178
+ console.log(` ${color('gray', dim('โ”€โ”€โ”€ by ALDY โ€ข FileCrypt v1.0.0 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'))}`);
179
+ console.log('');
180
+ }
181
+
182
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
183
+ // ยง LOW-LEVEL HELPERS
184
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
185
+
186
+ /**
187
+ * Baca N byte dari file di posisi tertentu (non-stream, untuk header).
188
+ * @param {string} filePath
189
+ * @param {number} offset - byte offset mulai baca
190
+ * @param {number} length - jumlah byte yang dibaca
191
+ * @returns {Promise<Buffer>}
192
+ */
193
+ async function readBytes(filePath, offset, length) {
194
+ const fh = await fs.promises.open(filePath, 'r');
195
+ const buf = Buffer.alloc(length);
196
+ const { bytesRead } = await fh.read(buf, 0, length, offset);
197
+ await fh.close();
198
+ if (bytesRead < length) {
199
+ throw new Error(`File terlalu kecil untuk dibaca sebagai file enkripsi (header tidak lengkap).`);
200
+ }
201
+ return buf;
202
+ }
203
+
204
+ /**
205
+ * Tulis buffer ke writeStream, kembalikan Promise.
206
+ * @param {fs.WriteStream} stream
207
+ * @param {Buffer} data
208
+ */
209
+ function writeAsync(stream, data) {
210
+ return new Promise((resolve, reject) => {
211
+ stream.write(data, (err) => (err ? reject(err) : resolve()));
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Tulis buffer ke posisi tertentu dalam file yang sudah ada (r+ mode).
217
+ * Digunakan untuk patch auth tag setelah enkripsi GCM selesai.
218
+ */
219
+ async function patchFileAt(filePath, offset, data) {
220
+ const fh = await fs.promises.open(filePath, 'r+');
221
+ await fh.write(data, 0, data.length, offset);
222
+ await fh.close();
223
+ }
224
+
225
+ /**
226
+ * Hapus file jika ada (cleanup output parsial saat error).
227
+ */
228
+ async function unlinkSafe(filePath) {
229
+ try { await fs.promises.unlink(filePath); } catch { /* abaikan */ }
230
+ }
231
+
232
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
233
+ // ยง KEY DERIVATION
234
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
235
+
236
+ /**
237
+ * Turunkan kunci dari password + salt.
238
+ * - mode 'easy': PBKDF2-SHA512, 100.000 iterasi
239
+ * - mode 'hard': scrypt(N=65536, r=8, p=2) โ†’ setara militer
240
+ *
241
+ * @param {'easy'|'hard'} mode
242
+ * @param {string} password
243
+ * @param {Buffer} salt
244
+ * @returns {Promise<Buffer>} 32-byte key
245
+ */
246
+ function deriveKey(mode, password, salt) {
247
+ return new Promise((resolve, reject) => {
248
+ if (mode === 'easy') {
249
+ crypto.pbkdf2(
250
+ password,
251
+ salt,
252
+ CFG.easy.iterations,
253
+ CFG.easy.keySize,
254
+ CFG.easy.digest,
255
+ (err, key) => (err ? reject(err) : resolve(key))
256
+ );
257
+ } else {
258
+ const { scryptN: N, scryptR: r, scryptP: p, keySize } = CFG.hard;
259
+ crypto.scrypt(
260
+ password,
261
+ salt,
262
+ keySize,
263
+ { N, r, p, maxmem: 128 * N * r * 2 },
264
+ (err, key) => (err ? reject(err) : resolve(key))
265
+ );
266
+ }
267
+ });
268
+ }
269
+
270
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
271
+ // ยง ENCRYPT โ€” EASY MODE (AES-256-CBC + PBKDF2)
272
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
273
+
274
+ /**
275
+ * Enkripsi satu file dengan AES-256-CBC + PBKDF2 via Streams.
276
+ * Header (54 byte): MAGIC | SALT | IV
277
+ *
278
+ * @param {string} filePath - path file sumber
279
+ * @param {string} password - password teks
280
+ * @returns {Promise<string>} - path file .enc
281
+ */
282
+ async function encryptEasy(filePath, password) {
283
+ const cfg = CFG.easy;
284
+ const salt = crypto.randomBytes(cfg.saltSize);
285
+ const iv = crypto.randomBytes(cfg.ivSize);
286
+ const key = await deriveKey('easy', password, salt);
287
+
288
+ const header = Buffer.concat([cfg.magic, salt, iv]);
289
+ const outPath = filePath + '.enc';
290
+ const writeStream = fs.createWriteStream(outPath);
291
+
292
+ // Tulis header dulu (54 byte)
293
+ await writeAsync(writeStream, header);
294
+
295
+ // Stream: readFile โ†’ AES-256-CBC cipher โ†’ writeFile
296
+ const cipher = crypto.createCipheriv(cfg.algorithm, key, iv);
297
+ await pipeline(fs.createReadStream(filePath), cipher, writeStream);
298
+
299
+ return outPath;
300
+ }
301
+
302
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
303
+ // ยง DECRYPT โ€” EASY MODE (AES-256-CBC + PBKDF2)
304
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
305
+
306
+ /**
307
+ * Dekripsi file .enc yang dibuat oleh encryptEasy().
308
+ *
309
+ * @param {string} encPath - path file .enc
310
+ * @param {string} password
311
+ * @returns {Promise<string>} - path file hasil dekripsi
312
+ */
313
+ async function decryptEasy(encPath, password) {
314
+ const cfg = CFG.easy;
315
+ const header = await readBytes(encPath, 0, cfg.headerSize);
316
+
317
+ // Validasi magic bytes
318
+ const magic = header.subarray(0, 6);
319
+ if (!magic.equals(cfg.magic)) {
320
+ throw new Error('Magic bytes tidak cocok โ€” file bukan enkripsi Easy mode.');
321
+ }
322
+
323
+ const salt = header.subarray(cfg.saltOffset, cfg.saltOffset + cfg.saltSize);
324
+ const iv = header.subarray(cfg.ivOffset, cfg.ivOffset + cfg.ivSize);
325
+ const key = await deriveKey('easy', password, salt);
326
+
327
+ const outPath = encPath.slice(0, -4); // buang ekstensi .enc
328
+ const decipher = crypto.createDecipheriv(cfg.algorithm, key, iv);
329
+
330
+ // Stream: readFile (mulai setelah header) โ†’ AES-256-CBC decipher โ†’ writeFile
331
+ await pipeline(
332
+ fs.createReadStream(encPath, { start: cfg.headerSize }),
333
+ decipher,
334
+ fs.createWriteStream(outPath)
335
+ );
336
+
337
+ return outPath;
338
+ }
339
+
340
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
341
+ // ยง ENCRYPT โ€” HARD MODE (AES-256-GCM + scrypt)
342
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
343
+
344
+ /**
345
+ * Enkripsi satu file dengan AES-256-GCM + scrypt via Streams.
346
+ * Header (66 byte): MAGIC | SALT | IV | AUTH_TAG
347
+ *
348
+ * Auth tag (16 byte) hanya tersedia setelah cipher selesai,
349
+ * sehingga ditulis ke header via patch (r+ mode) setelah pipeline.
350
+ *
351
+ * @param {string} filePath
352
+ * @param {string} password
353
+ * @returns {Promise<string>}
354
+ */
355
+ async function encryptHard(filePath, password) {
356
+ const cfg = CFG.hard;
357
+ const salt = crypto.randomBytes(cfg.saltSize);
358
+ const iv = crypto.randomBytes(cfg.ivSize);
359
+ const key = await deriveKey('hard', password, salt);
360
+
361
+ // Header dengan placeholder 16-byte untuk auth tag
362
+ const header = Buffer.concat([
363
+ cfg.magic,
364
+ salt,
365
+ iv,
366
+ Buffer.alloc(cfg.authTagSize), // โ† akan di-patch setelah cipher selesai
367
+ ]);
368
+
369
+ const outPath = filePath + '.enc';
370
+ const writeStream = fs.createWriteStream(outPath);
371
+
372
+ // Tulis header dulu (66 byte, auth tag masih 0x00)
373
+ await writeAsync(writeStream, header);
374
+
375
+ // Stream: readFile โ†’ AES-256-GCM cipher โ†’ writeFile
376
+ const cipher = crypto.createCipheriv(cfg.algorithm, key, iv);
377
+ await pipeline(fs.createReadStream(filePath), cipher, writeStream);
378
+ // โ†‘ writeStream sudah closed setelah pipeline selesai
379
+
380
+ // Ambil auth tag (tersedia setelah cipher.final() dipanggil internal pipeline)
381
+ const authTag = cipher.getAuthTag();
382
+
383
+ // Patch: tulis auth tag yang benar ke posisi authTagOffset dalam file
384
+ await patchFileAt(outPath, cfg.authTagOffset, authTag);
385
+
386
+ return outPath;
387
+ }
388
+
389
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
390
+ // ยง DECRYPT โ€” HARD MODE (AES-256-GCM + scrypt)
391
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
392
+
393
+ /**
394
+ * Dekripsi file .enc yang dibuat oleh encryptHard().
395
+ * GCM akan otomatis memverifikasi auth tag โ€” jika file corrupt
396
+ * atau password salah, pipeline akan throw error.
397
+ *
398
+ * @param {string} encPath
399
+ * @param {string} password
400
+ * @returns {Promise<string>}
401
+ */
402
+ async function decryptHard(encPath, password) {
403
+ const cfg = CFG.hard;
404
+ const header = await readBytes(encPath, 0, cfg.headerSize);
405
+
406
+ // Validasi magic bytes
407
+ const magic = header.subarray(0, 6);
408
+ if (!magic.equals(cfg.magic)) {
409
+ throw new Error('Magic bytes tidak cocok โ€” file bukan enkripsi Hard mode.');
410
+ }
411
+
412
+ const salt = header.subarray(cfg.saltOffset, cfg.saltOffset + cfg.saltSize);
413
+ const iv = header.subarray(cfg.ivOffset, cfg.ivOffset + cfg.ivSize);
414
+ const authTag = header.subarray(cfg.authTagOffset, cfg.authTagOffset + cfg.authTagSize);
415
+ const key = await deriveKey('hard', password, salt);
416
+
417
+ const outPath = encPath.slice(0, -4);
418
+ const decipher = crypto.createDecipheriv(cfg.algorithm, key, iv);
419
+ decipher.setAuthTag(authTag); // GCM akan verifikasi saat decipher.final()
420
+
421
+ // Stream: readFile (mulai setelah header) โ†’ AES-256-GCM decipher โ†’ writeFile
422
+ await pipeline(
423
+ fs.createReadStream(encPath, { start: cfg.headerSize }),
424
+ decipher,
425
+ fs.createWriteStream(outPath)
426
+ );
427
+
428
+ return outPath;
429
+ }
430
+
431
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
432
+ // ยง AUTO-DETECT MODE dari magic bytes
433
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
434
+
435
+ /**
436
+ * Deteksi mode enkripsi dari 6 byte pertama file.
437
+ * @param {string} encPath
438
+ * @returns {Promise<'easy'|'hard'|null>}
439
+ */
440
+ async function detectMode(encPath) {
441
+ const magic = await readBytes(encPath, 0, 6);
442
+ if (magic.equals(MAGIC.EASY)) return 'easy';
443
+ if (magic.equals(MAGIC.HARD)) return 'hard';
444
+ return null;
445
+ }
446
+
447
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
448
+ // ยง SCAN FOLDER
449
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
450
+
451
+ /**
452
+ * Pindai folder, kembalikan daftar file yang sesuai.
453
+ * - Encrypt: abaikan file .enc (sudah terenkripsi)
454
+ * - Decrypt: hanya ambil file .enc
455
+ *
456
+ * @param {string} folderPath
457
+ * @param {'encrypt'|'decrypt'} action
458
+ * @returns {Promise<string[]>}
459
+ */
460
+ async function scanFolder(folderPath, action) {
461
+ const entries = await fs.promises.readdir(folderPath, { withFileTypes: true });
462
+ const files = [];
463
+
464
+ for (const entry of entries) {
465
+ if (!entry.isFile()) continue;
466
+
467
+ if (action === 'encrypt' && entry.name.endsWith('.enc')) {
468
+ log('skip', `Lewati (sudah .enc): ${color('dim', entry.name)}`);
469
+ continue;
470
+ }
471
+ if (action === 'decrypt' && !entry.name.endsWith('.enc')) {
472
+ continue; // silent skip โ€” bukan target dekripsi
473
+ }
474
+
475
+ files.push(path.join(folderPath, entry.name));
476
+ }
477
+
478
+ return files;
479
+ }
480
+
481
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
482
+ // ยง PROCESS FILES (loop utama)
483
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
484
+
485
+ /**
486
+ * Proses semua file (enkripsi atau dekripsi) satu per satu.
487
+ * Menggunakan Streams agar memory-efficient bahkan untuk file mendekati 1 GB.
488
+ *
489
+ * @param {string[]} files
490
+ * @param {'encrypt'|'decrypt'} action
491
+ * @param {'easy'|'hard'|null} mode - null = auto-detect (untuk decrypt)
492
+ * @param {string} password
493
+ * @returns {Promise<{successCount,skippedCount,failedCount}>}
494
+ */
495
+ async function processFiles(files, action, mode, password) {
496
+ let successCount = 0, skippedCount = 0, failedCount = 0;
497
+
498
+ for (const filePath of files) {
499
+ const fileName = path.basename(filePath);
500
+
501
+ try {
502
+ // โ”€โ”€ Cek ukuran file (hanya saat enkripsi) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
503
+ if (action === 'encrypt') {
504
+ const { size } = await fs.promises.stat(filePath);
505
+ if (size > MAX_FILE_SIZE) {
506
+ const sizeMB = (size / 1_048_576).toFixed(1);
507
+ log('warn', `Lewati (${sizeMB} MB > 1 GB): ${color('yellow', fileName)}`);
508
+ skippedCount++;
509
+ continue;
510
+ }
511
+ }
512
+
513
+ // โ”€โ”€ Auto-detect mode saat dekripsi โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
514
+ let effectiveMode = mode;
515
+ if (action === 'decrypt') {
516
+ const detected = await detectMode(filePath);
517
+ if (!detected) {
518
+ log('warn', `Lewati (magic bytes tidak dikenali): ${color('yellow', fileName)}`);
519
+ skippedCount++;
520
+ continue;
521
+ }
522
+ effectiveMode = detected;
523
+ }
524
+
525
+ const icon = action === 'encrypt' ? '๐Ÿ”’' : '๐Ÿ”“';
526
+ const modeLabel = effectiveMode === 'hard'
527
+ ? color('brightRed', 'โ–ธ HARD')
528
+ : color('brightGreen', 'โ–ธ EASY');
529
+ const namePad = fileName.padEnd(36);
530
+
531
+ // Spinner selama operasi (berguna di mode Hard yang lambat)
532
+ const spinner = createSpinner(`${modeLabel} ${namePad}`);
533
+
534
+ let outPath;
535
+
536
+ // โ”€โ”€ Dispatch ke fungsi yang tepat โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
537
+ if (action === 'encrypt') {
538
+ outPath = effectiveMode === 'hard'
539
+ ? await encryptHard(filePath, password)
540
+ : await encryptEasy(filePath, password);
541
+ } else {
542
+ outPath = effectiveMode === 'hard'
543
+ ? await decryptHard(filePath, password)
544
+ : await decryptEasy(filePath, password);
545
+ }
546
+
547
+ spinner.stop(
548
+ ` ${icon} ${modeLabel} ${color('white', namePad)} ` +
549
+ `${color('brightGreen', 'โœ” OK')} ${color('gray', 'โ†’ ' + path.basename(outPath))}`
550
+ );
551
+ successCount++;
552
+
553
+ } catch (err) {
554
+ // Pastikan pindah baris setelah "..."
555
+ process.stdout.write('\n');
556
+
557
+ // โ”€โ”€ Terjemahkan kode error ke pesan yang informatif โ”€โ”€โ”€โ”€
558
+ let reason = err.message;
559
+ if (err.code === 'EACCES' || err.code === 'EPERM') {
560
+ reason = 'Permission denied โ€” tidak ada izin baca/tulis file.';
561
+ } else if (err.code === 'ENOENT') {
562
+ reason = 'File tidak ditemukan (mungkin sudah dihapus saat proses berjalan).';
563
+ } else if (
564
+ reason.includes('Unsupported state') ||
565
+ reason.includes('auth') ||
566
+ reason.includes('Authentication') ||
567
+ reason.includes('bad decrypt') ||
568
+ reason.includes('wrong final block')
569
+ ) {
570
+ reason = 'Otentikasi gagal โ€” password salah atau file telah dimanipulasi/corrupt.';
571
+ } else if (reason.includes('header tidak lengkap') || reason.includes('Magic bytes')) {
572
+ reason = 'Format tidak valid โ€” bukan file enkripsi FileCrypt.';
573
+ }
574
+
575
+ log('error', `Gagal memproses: ${color('bold', path.basename(filePath))}`);
576
+ console.error(` ${color('dim', 'โ†ณ')} ${color('red', reason)}`);
577
+ failedCount++;
578
+
579
+ // Bersihkan output parsial agar tidak meninggalkan file rusak
580
+ const partialOut = action === 'encrypt'
581
+ ? filePath + '.enc'
582
+ : filePath.slice(0, -4);
583
+ await unlinkSafe(partialOut);
584
+ }
585
+ }
586
+
587
+ return { successCount, skippedCount, failedCount };
588
+ }
589
+
590
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
591
+ // ยง LOGGING
592
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
593
+
594
+ function log(type, msg) {
595
+ const prefix = {
596
+ info: color('cyan', 'โ„น'),
597
+ warn: color('yellow', 'โš '),
598
+ error: color('red', 'โœ–'),
599
+ ok: color('green', 'โœ”'),
600
+ skip: color('gray', 'โญ'),
601
+ }[type] ?? 'โ€ข';
602
+ console.log(` ${prefix} ${msg}`);
603
+ }
604
+
605
+ function printBanner(action) {
606
+ const isEncrypt = action === 'encrypt';
607
+ const icon = isEncrypt ? '๐Ÿ”’' : '๐Ÿ”“';
608
+ const label = isEncrypt ? 'ENKRIPSI MASSAL' : 'DEKRIPSI MASSAL';
609
+ const algo = isEncrypt ? 'AES-256-CBC / AES-256-GCM' : 'Auto-Detect Mode';
610
+
611
+ console.log('');
612
+ console.log(boxTop());
613
+ ALDY_ART.forEach((line, i) => {
614
+ const gradients = ['brightMagenta','magenta','brightBlue','blue','brightCyan','cyan'];
615
+ const colored = `${C[gradients[i]]}${C.bold}${line}${C.reset}`;
616
+ console.log(boxLine(colored, 'center'));
617
+ });
618
+ console.log(boxLine(color('gray', dim('โ”€โ”€โ”€ FileCrypt v1.0.0 โ€ข by ALDY โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€')), 'center'));
619
+ console.log(boxSep());
620
+ console.log(boxLine(` ${icon} ${bold(label)}`, 'left'));
621
+ console.log(boxLine(` ${color('gray', `Algoritma : ${algo}`)}`, 'left'));
622
+ console.log(boxBot());
623
+ console.log('');
624
+ }
625
+
626
+ function printSummary(action, { successCount, skippedCount, failedCount }) {
627
+ const total = successCount + skippedCount + failedCount;
628
+ const word = action === 'encrypt' ? 'dienkripsi' : 'didekripsi';
629
+ const allOk = failedCount === 0;
630
+ const statusColor = allOk ? 'brightGreen' : 'brightYellow';
631
+ const statusIcon = allOk ? 'โœ…' : 'โš ๏ธ ';
632
+
633
+ console.log('');
634
+ console.log(boxTop());
635
+ console.log(boxLine(` ${statusIcon} ${bold(color(statusColor, 'Ringkasan Eksekusi'))}`, 'left'));
636
+ console.log(boxSep());
637
+
638
+ // Bar progress visual
639
+ const barTotal = 30;
640
+ const barFill = total > 0 ? Math.round((successCount / total) * barTotal) : 0;
641
+ const bar = color('brightGreen', 'โ–ˆ'.repeat(barFill)) +
642
+ color('gray', 'โ–‘'.repeat(barTotal - barFill));
643
+ const pct = total > 0 ? Math.round((successCount / total) * 100) : 0;
644
+ console.log(boxLine(` ${bar} ${bold(String(pct))}%`, 'left'));
645
+ console.log(boxSep());
646
+
647
+ const pad = (s, n) => String(s).padStart(n, ' ');
648
+ console.log(boxLine(` ${color('brightGreen','โœ”')} Berhasil ${word.padEnd(12)} : ${bold(color('brightGreen', pad(successCount,4)))} file`, 'left'));
649
+ if (skippedCount > 0)
650
+ console.log(boxLine(` ${color('yellow','โš ')} Dilewati : ${bold(color('yellow', pad(skippedCount,4)))} file`, 'left'));
651
+ if (failedCount > 0)
652
+ console.log(boxLine(` ${color('brightRed','โœ–')} Gagal : ${bold(color('brightRed', pad(failedCount,4)))} file`, 'left'));
653
+ console.log(boxSep());
654
+ console.log(boxLine(` ${color('gray','โ–ธ')} Total diproses : ${bold(pad(total,4))} file`, 'left'));
655
+ console.log(boxBot());
656
+ console.log('');
657
+ }
658
+
659
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
660
+ // ยง PROMPT HELPERS (Inquirer)
661
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
662
+
663
+ async function promptPassword(confirmRequired = false) {
664
+ const questions = [
665
+ {
666
+ type: 'password',
667
+ name: 'password',
668
+ message: 'Masukkan password:',
669
+ mask: '*',
670
+ validate: (v) =>
671
+ v.length >= 8
672
+ ? true
673
+ : color('red', 'Password minimal 8 karakter.'),
674
+ },
675
+ ];
676
+
677
+ if (confirmRequired) {
678
+ questions.push({
679
+ type: 'password',
680
+ name: 'confirm',
681
+ message: 'Konfirmasi password:',
682
+ mask: '*',
683
+ });
684
+ }
685
+
686
+ const ans = await inquirer.prompt(questions);
687
+
688
+ if (confirmRequired && ans.password !== ans.confirm) {
689
+ console.error(`\n ${color('red', 'โœ– Password tidak cocok! Operasi dibatalkan.')}\n`);
690
+ process.exit(1);
691
+ }
692
+
693
+ return ans.password;
694
+ }
695
+
696
+ async function promptMode(defaultMode) {
697
+ if (defaultMode === 'easy' || defaultMode === 'hard') return defaultMode;
698
+
699
+ const { mode } = await inquirer.prompt([
700
+ {
701
+ type: 'list',
702
+ name: 'mode',
703
+ message: 'Pilih mode enkripsi:',
704
+ choices: [
705
+ {
706
+ name: '๐ŸŸข EASY โ€” AES-256-CBC + PBKDF2-SHA512 (100k iter) โ€” Standar & Cepat',
707
+ value: 'easy',
708
+ },
709
+ {
710
+ name: '๐Ÿ”ด HARD โ€” AES-256-GCM + scrypt (N=65536, r=8, p=2) โ€” Militer / Anti Brute-Force',
711
+ value: 'hard',
712
+ },
713
+ ],
714
+ },
715
+ ]);
716
+
717
+ return mode;
718
+ }
719
+
720
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
721
+ // ยง VALIDATE FOLDER
722
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
723
+
724
+ async function resolveFolder(rawPath) {
725
+ const resolved = path.resolve(rawPath);
726
+ try {
727
+ const stat = await fs.promises.stat(resolved);
728
+ if (!stat.isDirectory()) throw new Error('Path bukan direktori.');
729
+ } catch (err) {
730
+ console.error(`\n ${color('red', 'โœ–')} Folder tidak valid: ${color('bold', resolved)}`);
731
+ console.error(` ${color('dim', 'โ†ณ')} ${err.message}\n`);
732
+ process.exit(1);
733
+ }
734
+ return resolved;
735
+ }
736
+
737
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
738
+ // ยง CLI DEFINITION (Commander)
739
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
740
+
741
+ program
742
+ .name('filecrypt')
743
+ .description('Enkripsi/dekripsi file massal dengan AES-256-CBC (Easy) atau AES-256-GCM (Hard)')
744
+ .version('1.0.0', '-v, --version', 'Tampilkan versi');
745
+
746
+ // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
747
+ // โ”‚ COMMAND: encrypt โ”‚
748
+ // โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
749
+ program
750
+ .command('encrypt <folder>')
751
+ .description('Enkripsi semua file di dalam <folder> (file .enc diabaikan)')
752
+ .option(
753
+ '-m, --mode <mode>',
754
+ 'Mode enkripsi: easy | hard (default: tanya interaktif)',
755
+ ''
756
+ )
757
+ .action(async (folder, opts) => {
758
+ printBanner('encrypt');
759
+
760
+ const folderPath = await resolveFolder(folder);
761
+ const mode = await promptMode(opts.mode);
762
+ const password = await promptPassword(true); // โ† konfirmasi wajib saat enkripsi
763
+
764
+ console.log(`\n ${color('bold', 'Konfigurasi:')}`)
765
+ console.log(` Folder : ${color('cyan', folderPath)}`);
766
+ console.log(` Mode : ${color('bold', mode.toUpperCase())}`);
767
+ console.log(` Algoritma : ${color('bold', mode === 'hard'
768
+ ? 'AES-256-GCM + scrypt (N=65536, r=8, p=2)'
769
+ : 'AES-256-CBC + PBKDF2-SHA512 (100.000 iter)')}`);
770
+
771
+ if (mode === 'hard') {
772
+ console.log(`\n ${color('yellow', 'โš  PERHATIAN:')} Mode Hard menggunakan scrypt dengan parameter berat.`);
773
+ console.log(` Proses derivasi kunci (~1-3 detik/file) adalah fitur keamanan, bukan bug.\n`);
774
+ } else {
775
+ console.log('');
776
+ }
777
+
778
+ const { confirmed } = await inquirer.prompt([{
779
+ type: 'confirm',
780
+ name: 'confirmed',
781
+ message: `Enkripsi semua file di folder ini?`,
782
+ default: false,
783
+ }]);
784
+
785
+ if (!confirmed) {
786
+ console.log(`\n ${color('yellow', 'Dibatalkan oleh pengguna.')}\n`);
787
+ process.exit(0);
788
+ }
789
+
790
+ console.log(`\n ${color('dim', 'โ”€'.repeat(54))}`);
791
+
792
+ const files = await scanFolder(folderPath, 'encrypt');
793
+
794
+ if (files.length === 0) {
795
+ console.log(`\n ${color('yellow', 'โš ')} Tidak ada file yang bisa dienkripsi di folder ini.\n`);
796
+ return;
797
+ }
798
+
799
+ console.log(`\n Ditemukan ${color('bold', String(files.length))} file untuk dienkripsi:\n`);
800
+ const stats = await processFiles(files, 'encrypt', mode, password);
801
+ printSummary('encrypt', stats);
802
+ });
803
+
804
+ // โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
805
+ // โ”‚ COMMAND: decrypt โ”‚
806
+ // โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
807
+ program
808
+ .command('decrypt <folder>')
809
+ .description('Dekripsi semua file .enc di dalam <folder> (mode terdeteksi otomatis)')
810
+ .action(async (folder) => {
811
+ printBanner('decrypt');
812
+
813
+ const folderPath = await resolveFolder(folder);
814
+
815
+ console.log(` ${color('bold', 'Konfigurasi:')}`);
816
+ console.log(` Folder : ${color('cyan', folderPath)}`);
817
+ console.log(` Mode : ${color('bold', 'AUTO-DETECT')} ${color('dim', '(dibaca dari magic bytes header)')}\n`);
818
+
819
+ const password = await promptPassword(false); // โ† tidak perlu konfirmasi saat dekripsi
820
+
821
+ console.log(`\n ${color('dim', 'โ”€'.repeat(54))}`);
822
+
823
+ const files = await scanFolder(folderPath, 'decrypt');
824
+
825
+ if (files.length === 0) {
826
+ console.log(`\n ${color('yellow', 'โš ')} Tidak ada file .enc yang ditemukan di folder ini.\n`);
827
+ return;
828
+ }
829
+
830
+ console.log(`\n Ditemukan ${color('bold', String(files.length))} file .enc untuk didekripsi:\n`);
831
+ const stats = await processFiles(files, 'decrypt', null, password);
832
+ printSummary('decrypt', stats);
833
+ });
834
+
835
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
836
+ // ยง INTERACTIVE STARTUP MENU (tanpa argumen)
837
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
838
+
839
+ /**
840
+ * Menu interaktif lengkap โ€” tampil saat `node index.js` dijalankan
841
+ * tanpa subcommand. Panduan langkah demi langkah, tanpa perlu hafal syntax.
842
+ */
843
+ async function runInteractiveMenu() {
844
+ // โ”€โ”€ Splash screen โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
845
+ console.clear();
846
+ console.log('');
847
+ console.log(boxTop());
848
+ ALDY_ART.forEach((line, i) => {
849
+ const gradients = ['brightMagenta','magenta','brightBlue','blue','brightCyan','cyan'];
850
+ const colored = `${C[gradients[i]]}${C.bold}${line}${C.reset}`;
851
+ console.log(boxLine(colored, 'center'));
852
+ });
853
+ console.log(boxSep());
854
+ console.log(boxLine(` ${color('gray', 'โ–ธ')} ${bold('FileCrypt')} ${color('gray','โ€”')} ${color('cyan','Bulk File Encryption CLI')}`, 'left'));
855
+ console.log(boxLine(` ${color('gray', 'โ–ธ')} ${color('gray','AES-256-CBC (Easy) ยท AES-256-GCM (Hard/Military)')}`, 'left'));
856
+ console.log(boxLine(` ${color('gray', 'โ–ธ')} ${color('gray','by')} ${bold(color('brightMagenta','ALDY'))} ${color('gray','ยท v1.0.0')}`, 'left'));
857
+ console.log(boxBot());
858
+ console.log('');
859
+
860
+ // โ”€โ”€ 1. Pilih aksi โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
861
+ const { action } = await inquirer.prompt([{
862
+ type: 'list',
863
+ name: 'action',
864
+ message: `${bold(color('brightCyan','Pilih aksi'))}:`,
865
+ choices: [
866
+ {
867
+ name: `${color('brightGreen','๐Ÿ”’')} ${bold('ENKRIPSI')} ${color('gray','โ”€')} Enkripsi semua file di dalam folder`,
868
+ value: 'encrypt',
869
+ },
870
+ {
871
+ name: `${color('brightBlue','๐Ÿ”“')} ${bold('DEKRIPSI')} ${color('gray','โ”€')} Dekripsi semua file .enc di dalam folder`,
872
+ value: 'decrypt',
873
+ },
874
+ new inquirer.Separator(color('gray', ' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€')),
875
+ {
876
+ name: `${color('gray','๐Ÿšช')} ${color('gray','Keluar')}`,
877
+ value: 'exit',
878
+ },
879
+ ],
880
+ }]);
881
+
882
+ if (action === 'exit') {
883
+ console.log('');
884
+ console.log(boxTop());
885
+ console.log(boxLine(` ${color('gray', '๐Ÿ‘‹')} ${bold(color('brightMagenta','Sampai jumpa, ALDY!'))}`, 'left'));
886
+ console.log(boxLine(` ${color('gray','Terima kasih telah menggunakan FileCrypt.')}`, 'left'));
887
+ console.log(boxBot());
888
+ console.log('');
889
+ process.exit(0);
890
+ }
891
+
892
+ // โ”€โ”€ 2. Masukkan path folder โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
893
+ console.log('');
894
+ const { folderRaw } = await inquirer.prompt([{
895
+ type: 'input',
896
+ name: 'folderRaw',
897
+ message: `${bold(color('brightCyan','Path folder target'))}:`,
898
+ validate: async (v) => {
899
+ if (!v.trim()) return color('brightRed', 'โœ– Path tidak boleh kosong.');
900
+ const resolved = path.resolve(v.trim());
901
+ try {
902
+ const stat = await fs.promises.stat(resolved);
903
+ if (!stat.isDirectory()) return color('brightRed', 'โœ– Path tersebut bukan direktori.');
904
+ return true;
905
+ } catch {
906
+ return color('brightRed', `โœ– Folder tidak ditemukan: ${resolved}`);
907
+ }
908
+ },
909
+ }]);
910
+
911
+ const folderPath = path.resolve(folderRaw.trim());
912
+
913
+ // โ”€โ”€ 3. Pilih mode (hanya saat enkripsi) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
914
+ let mode = null;
915
+ if (action === 'encrypt') {
916
+ console.log('');
917
+ const { selectedMode } = await inquirer.prompt([{
918
+ type: 'list',
919
+ name: 'selectedMode',
920
+ message: `${bold(color('brightCyan','Mode enkripsi'))}:`,
921
+ choices: [
922
+ {
923
+ name: [
924
+ `${color('brightGreen','๐ŸŸข')} ${bold('EASY')}`,
925
+ color('gray', 'โ”€'),
926
+ 'AES-256-CBC + PBKDF2-SHA512',
927
+ color('gray', '(Standar ยท Cepat)'),
928
+ ].join(' '),
929
+ value: 'easy',
930
+ },
931
+ {
932
+ name: [
933
+ `${color('brightRed','๐Ÿ”ด')} ${bold('HARD')}`,
934
+ color('gray', 'โ”€'),
935
+ 'AES-256-GCM + scrypt',
936
+ color('gray', '(Militer ยท Anti Brute-Force)'),
937
+ ].join(' '),
938
+ value: 'hard',
939
+ },
940
+ ],
941
+ }]);
942
+ mode = selectedMode;
943
+ }
944
+
945
+ // โ”€โ”€ 4. Tampilkan konfigurasi dalam box โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
946
+ console.log('');
947
+ console.log(boxTop());
948
+ console.log(boxLine(` ${color('gray','๐Ÿ“‹')} ${bold(color('brightCyan','Konfigurasi Operasi'))}`, 'left'));
949
+ console.log(boxSep());
950
+ console.log(boxLine(` ${color('gray','Folder :')} ${bold(color('brightCyan', folderPath))}`, 'left'));
951
+
952
+ if (action === 'encrypt') {
953
+ const modeColor = mode === 'hard' ? 'brightRed' : 'brightGreen';
954
+ const modeIcon = mode === 'hard' ? '๐Ÿ”ด' : '๐ŸŸข';
955
+ const algoStr = mode === 'hard'
956
+ ? 'AES-256-GCM + scrypt (N=65536, r=8, p=2)'
957
+ : 'AES-256-CBC + PBKDF2-SHA512 (100.000 iter)';
958
+ console.log(boxLine(` ${color('gray','Aksi :')} ${bold(color('brightGreen','ENKRIPSI'))}`, 'left'));
959
+ console.log(boxLine(` ${color('gray','Mode :')} ${modeIcon} ${bold(color(modeColor, mode.toUpperCase()))}`, 'left'));
960
+ console.log(boxLine(` ${color('gray','Algoritma :')} ${color('white', algoStr)}`, 'left'));
961
+
962
+ if (mode === 'hard') {
963
+ console.log(boxSep());
964
+ console.log(boxLine(` ${color('yellow','โš ')} ${bold('PERHATIAN')} โ€” scrypt memerlukan ~1-3 detik/file.`, 'left'));
965
+ console.log(boxLine(` ${color('gray',' Ini fitur keamanan (bukan bug).')}`, 'left'));
966
+ }
967
+ } else {
968
+ console.log(boxLine(` ${color('gray','Aksi :')} ${bold(color('brightBlue','DEKRIPSI'))}`, 'left'));
969
+ console.log(boxLine(` ${color('gray','Mode :')} ${color('white','AUTO-DETECT')} ${color('gray','(magic bytes)')}`, 'left'));
970
+ }
971
+
972
+ console.log(boxBot());
973
+ console.log('');
974
+
975
+ // โ”€โ”€ 5. Input password โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
976
+ const password = await promptPassword(action === 'encrypt');
977
+
978
+ // โ”€โ”€ 6. Konfirmasi akhir โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
979
+ console.log('');
980
+ const word = action === 'encrypt' ? 'Enkripsi' : 'Dekripsi';
981
+ const { confirmed } = await inquirer.prompt([{
982
+ type: 'confirm',
983
+ name: 'confirmed',
984
+ message: `${bold(color('brightYellow', `โšก ${word} semua file sekarang?`))}`,
985
+ default: false,
986
+ }]);
987
+
988
+ if (!confirmed) {
989
+ console.log('');
990
+ console.log(boxTop());
991
+ console.log(boxLine(` ${color('yellow','โš ')} ${bold('Dibatalkan oleh pengguna.')}`, 'left'));
992
+ console.log(boxBot());
993
+ console.log('');
994
+ process.exit(0);
995
+ }
996
+
997
+ // โ”€โ”€ 7. Header daftar file โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
998
+ console.log('');
999
+ console.log(color('cyan', ` ${'โ”€'.repeat(BOX_W - 2)}`));
1000
+
1001
+ // โ”€โ”€ 8. Scan & proses โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1002
+ const files = await scanFolder(folderPath, action);
1003
+
1004
+ if (files.length === 0) {
1005
+ const msg = action === 'encrypt'
1006
+ ? 'Tidak ada file yang bisa dienkripsi di folder ini.'
1007
+ : 'Tidak ada file .enc yang ditemukan di folder ini.';
1008
+ console.log('');
1009
+ console.log(boxTop());
1010
+ console.log(boxLine(` ${color('yellow','โš ')} ${msg}`, 'left'));
1011
+ console.log(boxBot());
1012
+ console.log('');
1013
+ return;
1014
+ }
1015
+
1016
+ const wordFile = action === 'encrypt' ? 'dienkripsi' : 'didekripsi';
1017
+ console.log(`\n ${color('gray','โ–ธ')} Ditemukan ${bold(color('brightCyan', String(files.length)))} file untuk ${wordFile}:\n`);
1018
+
1019
+ const stats = await processFiles(files, action, mode, password);
1020
+ printSummary(action, stats);
1021
+
1022
+ // โ”€โ”€ 9. Kembali ke menu? โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1023
+ const { again } = await inquirer.prompt([{
1024
+ type: 'confirm',
1025
+ name: 'again',
1026
+ message: `${bold(color('brightCyan','๐Ÿ”„ Kembali ke menu utama?'))}`,
1027
+ default: true,
1028
+ }]);
1029
+
1030
+ if (again) return runInteractiveMenu();
1031
+
1032
+ console.log('');
1033
+ console.log(boxTop());
1034
+ console.log(boxLine(` ${color('gray','๐Ÿ‘‹')} ${bold(color('brightMagenta','Sampai jumpa, ALDY!'))}`, 'left'));
1035
+ console.log(boxLine(` ${color('gray',' Semua operasi selesai.')}`, 'left'));
1036
+ console.log(boxBot());
1037
+ console.log('');
1038
+ }
1039
+
1040
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1041
+ // ยง ENTRY POINT
1042
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1043
+
1044
+ // Tangkap unhandled rejection agar tidak keluar tanpa pesan
1045
+ process.on('unhandledRejection', (err) => {
1046
+ console.error(`\n ${color('red', 'โœ– Unhandled Error:')} ${err.message}\n`);
1047
+ process.exit(1);
1048
+ });
1049
+
1050
+ // Jika dijalankan tanpa argumen โ†’ tampilkan menu interaktif
1051
+ if (process.argv.length <= 2) {
1052
+ runInteractiveMenu().catch((err) => {
1053
+ // Abaikan Ctrl+C (ExitPromptError dari inquirer)
1054
+ if (err.name === 'ExitPromptError' || err.message?.includes('User force closed')) {
1055
+ console.log(`\n ${color('dim', 'Dibatalkan. Sampai jumpa!')}\n`);
1056
+ process.exit(0);
1057
+ }
1058
+ console.error(`\n ${color('red', 'โœ– Error:')} ${err.message}\n`);
1059
+ process.exit(1);
1060
+ });
1061
+ } else {
1062
+ // Ada argumen โ†’ fallback ke Commander (syntax lama tetap bisa dipakai)
1063
+ program.parseAsync(process.argv);
1064
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "enkripsi-file",
3
+ "version": "1.0.0",
4
+ "description": "CLI enkripsi & dekripsi file massal โ€” AES-256-CBC (Easy) & AES-256-GCM (Hard/Military Grade)",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "enkripsi-file": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js",
11
+ "prepublishOnly": "node --check index.js"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "dependencies": {
19
+ "commander": "^11.1.0",
20
+ "inquirer": "^8.2.6"
21
+ },
22
+ "engines": {
23
+ "node": ">=16.0.0"
24
+ },
25
+ "keywords": [
26
+ "encryption",
27
+ "dekripsi",
28
+ "enkripsi",
29
+ "aes-256",
30
+ "gcm",
31
+ "cbc",
32
+ "scrypt",
33
+ "pbkdf2",
34
+ "cli",
35
+ "security",
36
+ "filecrypt",
37
+ "bulk-encryption",
38
+ "indonesia"
39
+ ],
40
+ "author": "ALDY",
41
+ "license": "MIT",
42
+ "preferGlobal": true
43
+ }