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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/index.js +1064 -0
- 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
|
+
}
|