lpgp 0.2.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 +52 -0
- package/README.md +218 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -0
- package/dist/db.d.ts +80 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +284 -0
- package/dist/db.js.map +1 -0
- package/dist/decrypt.d.ts +2 -0
- package/dist/decrypt.d.ts.map +1 -0
- package/dist/decrypt.js +25 -0
- package/dist/decrypt.js.map +1 -0
- package/dist/encrypt.d.ts +2 -0
- package/dist/encrypt.d.ts.map +1 -0
- package/dist/encrypt.js +18 -0
- package/dist/encrypt.js.map +1 -0
- package/dist/key-manager.d.ts +119 -0
- package/dist/key-manager.d.ts.map +1 -0
- package/dist/key-manager.js +1235 -0
- package/dist/key-manager.js.map +1 -0
- package/dist/key-utils.d.ts +47 -0
- package/dist/key-utils.d.ts.map +1 -0
- package/dist/key-utils.js +199 -0
- package/dist/key-utils.js.map +1 -0
- package/dist/keychain.d.ts +22 -0
- package/dist/keychain.d.ts.map +1 -0
- package/dist/keychain.js +73 -0
- package/dist/keychain.js.map +1 -0
- package/dist/pgp-tool.d.ts +3 -0
- package/dist/pgp-tool.d.ts.map +1 -0
- package/dist/pgp-tool.js +1061 -0
- package/dist/pgp-tool.js.map +1 -0
- package/dist/prompts.d.ts +11 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +109 -0
- package/dist/prompts.js.map +1 -0
- package/dist/schema.sql +86 -0
- package/dist/system-keys.d.ts +32 -0
- package/dist/system-keys.d.ts.map +1 -0
- package/dist/system-keys.js +123 -0
- package/dist/system-keys.js.map +1 -0
- package/dist/ui.d.ts +94 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +175 -0
- package/dist/ui.js.map +1 -0
- package/package.json +56 -0
package/dist/pgp-tool.js
ADDED
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as openpgp from 'openpgp';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import * as readline from 'readline';
|
|
6
|
+
import { stdin as input, stdout as output } from 'process';
|
|
7
|
+
import clipboardy from 'clipboardy';
|
|
8
|
+
import { Db } from './db.js';
|
|
9
|
+
import { KeyManager } from './key-manager.js';
|
|
10
|
+
import { extractPublicKeyInfo } from './key-utils.js';
|
|
11
|
+
import { escapeablePrompt, enableGlobalEscape, checkAndResetEscape, EscapeError, } from './prompts.js';
|
|
12
|
+
import { getStoredPassphrase, storePassphrase, hasStoredPassphrase, } from './keychain.js';
|
|
13
|
+
import { colors, icons, printBanner, printDivider, showSuccess, showError, showWarning, showLoading, promptMessage, mainMenuChoice, backChoice, exitChoice, } from './ui.js';
|
|
14
|
+
// Config to allow weak keys like DSA (not recommended for production)
|
|
15
|
+
const weakKeyConfig = {
|
|
16
|
+
rejectPublicKeyAlgorithms: new Set(),
|
|
17
|
+
rejectHashAlgorithms: new Set(),
|
|
18
|
+
rejectMessageHashAlgorithms: new Set(),
|
|
19
|
+
rejectCurves: new Set(),
|
|
20
|
+
};
|
|
21
|
+
// Initialize database and key manager
|
|
22
|
+
const db = new Db();
|
|
23
|
+
const keyManager = new KeyManager(db);
|
|
24
|
+
// Session passphrase cache - stores passphrases by keypair ID
|
|
25
|
+
const passphraseCache = new Map();
|
|
26
|
+
async function encryptMessage(message, publicKeysArmored) {
|
|
27
|
+
let publicKeys;
|
|
28
|
+
if (publicKeysArmored) {
|
|
29
|
+
// Use provided public key(s)
|
|
30
|
+
const keysArray = Array.isArray(publicKeysArmored) ? publicKeysArmored : [publicKeysArmored];
|
|
31
|
+
publicKeys = await Promise.all(keysArray.map((key) => openpgp.readKey({ armoredKey: key, config: weakKeyConfig })));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Use default keypair's public key (encrypt to self)
|
|
35
|
+
const defaultKeypair = await keyManager.getDefaultKeypair();
|
|
36
|
+
if (!defaultKeypair) {
|
|
37
|
+
throw new Error('No default keypair found. Please set up a keypair first.');
|
|
38
|
+
}
|
|
39
|
+
publicKeys = [await openpgp.readKey({ armoredKey: defaultKeypair.public_key, config: weakKeyConfig })];
|
|
40
|
+
// Update last_used_at
|
|
41
|
+
db.update('keypair', { key: 'id', value: defaultKeypair.id }, { last_used_at: new Date().toISOString() });
|
|
42
|
+
}
|
|
43
|
+
const encrypted = await openpgp.encrypt({
|
|
44
|
+
message: await openpgp.createMessage({ text: message }),
|
|
45
|
+
encryptionKeys: publicKeys,
|
|
46
|
+
config: weakKeyConfig,
|
|
47
|
+
});
|
|
48
|
+
return encrypted;
|
|
49
|
+
}
|
|
50
|
+
async function decryptMessage(encryptedMessage) {
|
|
51
|
+
const defaultKeypair = await keyManager.getDefaultKeypair();
|
|
52
|
+
if (!defaultKeypair) {
|
|
53
|
+
throw new Error('No default keypair found. Please set up a keypair first.');
|
|
54
|
+
}
|
|
55
|
+
// Check if passphrase is cached for this keypair
|
|
56
|
+
let passphrase = '';
|
|
57
|
+
if (defaultKeypair.passphrase_protected) {
|
|
58
|
+
if (passphraseCache.has(defaultKeypair.id)) {
|
|
59
|
+
// Use session-cached passphrase
|
|
60
|
+
passphrase = passphraseCache.get(defaultKeypair.id);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Check if passphrase is stored in system keychain
|
|
64
|
+
const storedPassphrase = await getStoredPassphrase(defaultKeypair.fingerprint);
|
|
65
|
+
if (storedPassphrase) {
|
|
66
|
+
// Validate the stored passphrase
|
|
67
|
+
try {
|
|
68
|
+
await openpgp.decryptKey({
|
|
69
|
+
privateKey: await openpgp.readPrivateKey({ armoredKey: defaultKeypair.private_key, config: weakKeyConfig }),
|
|
70
|
+
passphrase: storedPassphrase,
|
|
71
|
+
config: weakKeyConfig,
|
|
72
|
+
});
|
|
73
|
+
// Stored passphrase is valid, use it
|
|
74
|
+
passphrase = storedPassphrase;
|
|
75
|
+
passphraseCache.set(defaultKeypair.id, passphrase);
|
|
76
|
+
console.log(colors.muted('Using passphrase from system keychain'));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Stored passphrase is invalid (key may have changed), prompt for new one
|
|
80
|
+
showWarning('Stored passphrase is invalid. Please enter your passphrase.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// If we still don't have a valid passphrase, prompt for it
|
|
84
|
+
if (!passphrase) {
|
|
85
|
+
const { passphraseInput } = await escapeablePrompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'password',
|
|
88
|
+
name: 'passphraseInput',
|
|
89
|
+
message: promptMessage('Enter your private key passphrase:'),
|
|
90
|
+
mask: '*',
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
passphrase = passphraseInput;
|
|
94
|
+
// Validate the passphrase by attempting to decrypt the key
|
|
95
|
+
try {
|
|
96
|
+
await openpgp.decryptKey({
|
|
97
|
+
privateKey: await openpgp.readPrivateKey({ armoredKey: defaultKeypair.private_key, config: weakKeyConfig }),
|
|
98
|
+
passphrase,
|
|
99
|
+
config: weakKeyConfig,
|
|
100
|
+
});
|
|
101
|
+
// If successful, cache the passphrase in session
|
|
102
|
+
passphraseCache.set(defaultKeypair.id, passphrase);
|
|
103
|
+
// Ask if user wants to save passphrase to system keychain
|
|
104
|
+
const alreadyStored = await hasStoredPassphrase(defaultKeypair.fingerprint);
|
|
105
|
+
if (!alreadyStored) {
|
|
106
|
+
const { saveToKeychain } = await escapeablePrompt([
|
|
107
|
+
{
|
|
108
|
+
type: 'confirm',
|
|
109
|
+
name: 'saveToKeychain',
|
|
110
|
+
message: promptMessage('Save passphrase to system keychain?'),
|
|
111
|
+
default: false,
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
if (saveToKeychain) {
|
|
115
|
+
const saved = await storePassphrase(defaultKeypair.fingerprint, passphrase);
|
|
116
|
+
if (saved) {
|
|
117
|
+
showSuccess('Passphrase saved to system keychain');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
showWarning('Could not save to keychain (may not be available on this system)');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
throw new Error('Incorrect passphrase');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const privateKey = await openpgp.decryptKey({
|
|
132
|
+
privateKey: await openpgp.readPrivateKey({ armoredKey: defaultKeypair.private_key, config: weakKeyConfig }),
|
|
133
|
+
passphrase,
|
|
134
|
+
config: weakKeyConfig,
|
|
135
|
+
});
|
|
136
|
+
const message = await openpgp.readMessage({
|
|
137
|
+
armoredMessage: encryptedMessage,
|
|
138
|
+
});
|
|
139
|
+
const { data: decrypted } = await openpgp.decrypt({
|
|
140
|
+
message,
|
|
141
|
+
decryptionKeys: privateKey,
|
|
142
|
+
config: weakKeyConfig,
|
|
143
|
+
});
|
|
144
|
+
// Update last_used_at
|
|
145
|
+
db.update('keypair', { key: 'id', value: defaultKeypair.id }, { last_used_at: new Date().toISOString() });
|
|
146
|
+
return decrypted;
|
|
147
|
+
}
|
|
148
|
+
function checkEditorAvailable(command) {
|
|
149
|
+
try {
|
|
150
|
+
execSync(`which ${command}`, { stdio: 'ignore' });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function detectAvailableEditors() {
|
|
158
|
+
const editors = [
|
|
159
|
+
{ name: 'VS Code', command: 'code', available: false },
|
|
160
|
+
{ name: 'Neovim', command: 'nvim', available: false },
|
|
161
|
+
{ name: 'Vim', command: 'vim', available: false },
|
|
162
|
+
{ name: 'Nano', command: 'nano', available: false },
|
|
163
|
+
{ name: 'Emacs', command: 'emacs', available: false },
|
|
164
|
+
];
|
|
165
|
+
// Check platform specific editors
|
|
166
|
+
if (process.platform === 'darwin') {
|
|
167
|
+
editors.push({ name: 'TextEdit', command: 'open -e', available: true });
|
|
168
|
+
}
|
|
169
|
+
else if (process.platform === 'win32') {
|
|
170
|
+
editors.push({ name: 'Notepad', command: 'notepad', available: true });
|
|
171
|
+
}
|
|
172
|
+
// Check which editors are available
|
|
173
|
+
for (const editor of editors) {
|
|
174
|
+
if (editor.command.includes('open -e') || editor.command === 'notepad') {
|
|
175
|
+
editor.available = true; // TextEdit and Notepad are always available on their platforms
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
editor.available = checkEditorAvailable(editor.command);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return editors.filter((e) => e.available);
|
|
182
|
+
}
|
|
183
|
+
async function readInlineMultilineInput(promptText) {
|
|
184
|
+
console.log(promptMessage(promptText));
|
|
185
|
+
console.log(colors.muted('(Type your message. Press Enter, then Ctrl+D to finish)\n'));
|
|
186
|
+
const rl = readline.createInterface({ input, output });
|
|
187
|
+
rl.setPrompt('');
|
|
188
|
+
const lines = [];
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
rl.on('line', (line) => {
|
|
191
|
+
lines.push(line);
|
|
192
|
+
});
|
|
193
|
+
rl.on('close', () => {
|
|
194
|
+
resolve(lines.join('\n'));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function extractAllPublicKeys(content) {
|
|
199
|
+
const keyRegex = /-----BEGIN PGP PUBLIC KEY BLOCK-----[\s\S]*?-----END PGP PUBLIC KEY BLOCK-----/g;
|
|
200
|
+
const matches = content.match(keyRegex);
|
|
201
|
+
return matches || [];
|
|
202
|
+
}
|
|
203
|
+
async function addKeysFromClipboard(recipients) {
|
|
204
|
+
let clipboardContent = '';
|
|
205
|
+
try {
|
|
206
|
+
clipboardContent = await clipboardy.read();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
showWarning('Could not access clipboard');
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
const keys = extractAllPublicKeys(clipboardContent);
|
|
213
|
+
if (keys.length === 0) {
|
|
214
|
+
showWarning('No public keys found in clipboard');
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
let addedCount = 0;
|
|
218
|
+
for (const publicKey of keys) {
|
|
219
|
+
try {
|
|
220
|
+
// Validate the key
|
|
221
|
+
await openpgp.readKey({ armoredKey: publicKey, config: weakKeyConfig });
|
|
222
|
+
const keyInfo = await extractPublicKeyInfo(publicKey);
|
|
223
|
+
const recipientName = keyInfo.email || keyInfo.fingerprint?.slice(-8) || 'Unknown';
|
|
224
|
+
// Check for duplicates
|
|
225
|
+
const isDuplicate = recipients.some((r) => r.publicKey === publicKey);
|
|
226
|
+
if (isDuplicate) {
|
|
227
|
+
showWarning(`Skipping duplicate key: ${recipientName}`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
recipients.push({
|
|
231
|
+
name: recipientName,
|
|
232
|
+
publicKey,
|
|
233
|
+
});
|
|
234
|
+
showSuccess(`Added recipient: ${recipientName}`);
|
|
235
|
+
addedCount++;
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
showError(`Failed to parse a key: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return addedCount;
|
|
242
|
+
}
|
|
243
|
+
async function selectMultipleRecipients() {
|
|
244
|
+
const recipients = [];
|
|
245
|
+
const contacts = db.select({ table: 'contact' });
|
|
246
|
+
const defaultKeypair = await keyManager.getDefaultKeypair();
|
|
247
|
+
// Build the menu choices
|
|
248
|
+
function buildChoices() {
|
|
249
|
+
const choices = [];
|
|
250
|
+
// Show current recipients count
|
|
251
|
+
if (recipients.length > 0) {
|
|
252
|
+
choices.push({
|
|
253
|
+
name: colors.primary(`── Current recipients: ${recipients.length} ──`),
|
|
254
|
+
value: 'show-recipients',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// Option to add self (if not already added)
|
|
258
|
+
const selfAdded = recipients.some((r) => r.name === 'Myself');
|
|
259
|
+
if (defaultKeypair && !selfAdded) {
|
|
260
|
+
choices.push({
|
|
261
|
+
name: `${icons.key} Add myself ${colors.muted('(so I can also decrypt)')}`,
|
|
262
|
+
value: 'self',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
// Option to select from contacts
|
|
266
|
+
if (contacts.length > 0) {
|
|
267
|
+
choices.push({
|
|
268
|
+
name: `${icons.contact} Select from saved contacts ${colors.muted(`(${contacts.length} available)`)}`,
|
|
269
|
+
value: 'contacts',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// Clipboard and manual options
|
|
273
|
+
choices.push({
|
|
274
|
+
name: `${icons.clipboard} Paste from clipboard ${colors.muted('(supports multiple keys)')}`,
|
|
275
|
+
value: 'clipboard',
|
|
276
|
+
});
|
|
277
|
+
choices.push({
|
|
278
|
+
name: `${icons.inline} Type/paste a single key`,
|
|
279
|
+
value: 'manual',
|
|
280
|
+
});
|
|
281
|
+
// Done or cancel
|
|
282
|
+
choices.push({
|
|
283
|
+
name: recipients.length > 0 ? `${icons.success} Done adding recipients` : `${icons.back} Cancel`,
|
|
284
|
+
value: 'done',
|
|
285
|
+
});
|
|
286
|
+
return choices;
|
|
287
|
+
}
|
|
288
|
+
let addMore = true;
|
|
289
|
+
while (addMore) {
|
|
290
|
+
const { addMethod } = await escapeablePrompt([
|
|
291
|
+
{
|
|
292
|
+
type: 'list',
|
|
293
|
+
name: 'addMethod',
|
|
294
|
+
message: promptMessage('Add recipients:'),
|
|
295
|
+
choices: buildChoices(),
|
|
296
|
+
},
|
|
297
|
+
]);
|
|
298
|
+
if (addMethod === 'done') {
|
|
299
|
+
addMore = false;
|
|
300
|
+
}
|
|
301
|
+
else if (addMethod === 'show-recipients') {
|
|
302
|
+
// Show current recipients
|
|
303
|
+
console.log(colors.primary('\nCurrent recipients:'));
|
|
304
|
+
for (const r of recipients) {
|
|
305
|
+
console.log(colors.muted(` • ${r.name}`));
|
|
306
|
+
}
|
|
307
|
+
console.log();
|
|
308
|
+
}
|
|
309
|
+
else if (addMethod === 'self') {
|
|
310
|
+
if (defaultKeypair) {
|
|
311
|
+
recipients.push({
|
|
312
|
+
name: 'Myself',
|
|
313
|
+
publicKey: defaultKeypair.public_key,
|
|
314
|
+
});
|
|
315
|
+
showSuccess('Added yourself as a recipient');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else if (addMethod === 'contacts') {
|
|
319
|
+
// Show contacts as a checkbox
|
|
320
|
+
const { selectedContacts } = await escapeablePrompt([
|
|
321
|
+
{
|
|
322
|
+
type: 'checkbox',
|
|
323
|
+
name: 'selectedContacts',
|
|
324
|
+
message: promptMessage('Select contacts (space to toggle, enter to confirm):'),
|
|
325
|
+
choices: contacts.map((c) => {
|
|
326
|
+
const alreadyAdded = recipients.some((r) => r.publicKey === c.public_key);
|
|
327
|
+
return {
|
|
328
|
+
name: `${c.name} <${c.email || 'no email'}>${alreadyAdded ? colors.muted(' (already added)') : ''}`,
|
|
329
|
+
value: c.id,
|
|
330
|
+
checked: false,
|
|
331
|
+
disabled: alreadyAdded,
|
|
332
|
+
};
|
|
333
|
+
}),
|
|
334
|
+
},
|
|
335
|
+
]);
|
|
336
|
+
let addedCount = 0;
|
|
337
|
+
for (const contactId of selectedContacts) {
|
|
338
|
+
const contact = contacts.find((c) => c.id === contactId);
|
|
339
|
+
if (contact) {
|
|
340
|
+
recipients.push({
|
|
341
|
+
name: `${contact.name} <${contact.email || 'no email'}>`,
|
|
342
|
+
publicKey: contact.public_key,
|
|
343
|
+
});
|
|
344
|
+
addedCount++;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (addedCount > 0) {
|
|
348
|
+
showSuccess(`Added ${addedCount} contact${addedCount > 1 ? 's' : ''}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (addMethod === 'clipboard') {
|
|
352
|
+
const added = await addKeysFromClipboard(recipients);
|
|
353
|
+
if (added > 0) {
|
|
354
|
+
console.log();
|
|
355
|
+
showSuccess(`Added ${added} recipient${added > 1 ? 's' : ''} from clipboard`);
|
|
356
|
+
console.log();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (addMethod === 'manual') {
|
|
360
|
+
const publicKey = await getRecipientPublicKey();
|
|
361
|
+
if (publicKey) {
|
|
362
|
+
try {
|
|
363
|
+
const keyInfo = await extractPublicKeyInfo(publicKey);
|
|
364
|
+
const recipientName = keyInfo.email || keyInfo.fingerprint?.slice(-8) || 'Unknown';
|
|
365
|
+
// Check for duplicates
|
|
366
|
+
const isDuplicate = recipients.some((r) => r.publicKey === publicKey);
|
|
367
|
+
if (isDuplicate) {
|
|
368
|
+
showWarning('This recipient is already in the list');
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
recipients.push({
|
|
372
|
+
name: recipientName,
|
|
373
|
+
publicKey,
|
|
374
|
+
});
|
|
375
|
+
showSuccess(`Added recipient: ${recipientName}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
showError('Failed to parse public key');
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return recipients;
|
|
385
|
+
}
|
|
386
|
+
async function getRecipientPublicKey() {
|
|
387
|
+
// Check clipboard for public key
|
|
388
|
+
let clipboardContent = '';
|
|
389
|
+
let hasPublicKeyInClipboard = false;
|
|
390
|
+
try {
|
|
391
|
+
clipboardContent = await clipboardy.read();
|
|
392
|
+
hasPublicKeyInClipboard = clipboardContent.includes('BEGIN PGP PUBLIC KEY BLOCK');
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
// Clipboard not available, continue without it
|
|
396
|
+
}
|
|
397
|
+
let publicKey = '';
|
|
398
|
+
// If public key found in clipboard, ask if user wants to use it
|
|
399
|
+
if (hasPublicKeyInClipboard) {
|
|
400
|
+
const { useClipboard } = await escapeablePrompt([
|
|
401
|
+
{
|
|
402
|
+
type: 'confirm',
|
|
403
|
+
name: 'useClipboard',
|
|
404
|
+
message: 'Public key detected in clipboard. Use it?',
|
|
405
|
+
default: true,
|
|
406
|
+
},
|
|
407
|
+
]);
|
|
408
|
+
if (useClipboard) {
|
|
409
|
+
const publicMatch = clipboardContent.match(/-----BEGIN PGP PUBLIC KEY BLOCK-----[\s\S]*?-----END PGP PUBLIC KEY BLOCK-----/);
|
|
410
|
+
if (publicMatch) {
|
|
411
|
+
publicKey = publicMatch[0];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// If no key from clipboard, prompt for input
|
|
416
|
+
if (!publicKey) {
|
|
417
|
+
console.log(promptMessage('\nPaste the recipient\'s PGP PUBLIC key:'));
|
|
418
|
+
console.log(colors.muted('(Press Enter to finish, or press Enter then Ctrl+D)\n'));
|
|
419
|
+
const rl = readline.createInterface({ input, output });
|
|
420
|
+
rl.setPrompt('');
|
|
421
|
+
const lines = [];
|
|
422
|
+
publicKey = await new Promise((resolve) => {
|
|
423
|
+
rl.on('line', (line) => {
|
|
424
|
+
lines.push(line);
|
|
425
|
+
const content = lines.join('\n');
|
|
426
|
+
// Check if we have a complete key block and current line is empty
|
|
427
|
+
if (line.trim() === '' &&
|
|
428
|
+
content.includes('-----BEGIN PGP PUBLIC KEY BLOCK') &&
|
|
429
|
+
content.includes('-----END PGP PUBLIC KEY BLOCK')) {
|
|
430
|
+
rl.close();
|
|
431
|
+
resolve(content.trim());
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
rl.on('close', () => {
|
|
435
|
+
resolve(lines.join('\n'));
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
// Validate public key format
|
|
440
|
+
if (!publicKey.includes('BEGIN PGP PUBLIC KEY BLOCK')) {
|
|
441
|
+
console.log();
|
|
442
|
+
showError('Invalid public key format');
|
|
443
|
+
console.log();
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
// Try to read the key to validate it
|
|
447
|
+
try {
|
|
448
|
+
await openpgp.readKey({ armoredKey: publicKey, config: weakKeyConfig });
|
|
449
|
+
console.log();
|
|
450
|
+
showSuccess('Valid public key');
|
|
451
|
+
console.log();
|
|
452
|
+
return publicKey;
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
console.log();
|
|
456
|
+
showError(`Failed to read public key: ${error instanceof Error ? error.message : error}`);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
// printBanner is imported from ui.ts
|
|
461
|
+
function getEditorInstructions(editorCommand) {
|
|
462
|
+
const instructions = {
|
|
463
|
+
'nano': 'Save: Ctrl+O, then Enter. Exit: Ctrl+X',
|
|
464
|
+
'vim': 'Save and exit: :wq | Cancel: :q!',
|
|
465
|
+
'nvim': 'Save and exit: :wq | Cancel: :q!',
|
|
466
|
+
'code': 'Save: Cmd/Ctrl+S, then close the editor tab',
|
|
467
|
+
'emacs': 'Save: Ctrl+X Ctrl+S | Exit: Ctrl+X Ctrl+C',
|
|
468
|
+
'open -e': 'Save: Cmd+S, then close the window',
|
|
469
|
+
'notepad': 'Save: Ctrl+S, then close the window',
|
|
470
|
+
};
|
|
471
|
+
return instructions[editorCommand] || 'Save and close the editor when done';
|
|
472
|
+
}
|
|
473
|
+
function clearPassphraseCache() {
|
|
474
|
+
// Clear all cached passphrases from memory
|
|
475
|
+
passphraseCache.clear();
|
|
476
|
+
}
|
|
477
|
+
async function main() {
|
|
478
|
+
printBanner();
|
|
479
|
+
// Check for default keypair on first run
|
|
480
|
+
const hasKeypair = await keyManager.hasDefaultKeypair();
|
|
481
|
+
if (!hasKeypair) {
|
|
482
|
+
console.log();
|
|
483
|
+
showWarning('No keypair found. Let\'s set up your first keypair.');
|
|
484
|
+
console.log();
|
|
485
|
+
await keyManager.setupFirstKeypair();
|
|
486
|
+
console.log();
|
|
487
|
+
showSuccess('Setup complete! You can now use the tool.');
|
|
488
|
+
console.log();
|
|
489
|
+
}
|
|
490
|
+
const { action } = await escapeablePrompt([
|
|
491
|
+
{
|
|
492
|
+
type: 'list',
|
|
493
|
+
name: 'action',
|
|
494
|
+
message: promptMessage('What would you like to do?'),
|
|
495
|
+
choices: [
|
|
496
|
+
{ name: `${icons.encrypt} Encrypt a message`, value: 'encrypt' },
|
|
497
|
+
{ name: `${icons.decrypt} Decrypt a message`, value: 'decrypt' },
|
|
498
|
+
{ name: `${icons.key} Manage keys`, value: 'keys' },
|
|
499
|
+
new inquirer.Separator(),
|
|
500
|
+
exitChoice(),
|
|
501
|
+
],
|
|
502
|
+
},
|
|
503
|
+
]);
|
|
504
|
+
if (action === 'exit') {
|
|
505
|
+
clearPassphraseCache();
|
|
506
|
+
console.clear();
|
|
507
|
+
process.exit(0);
|
|
508
|
+
}
|
|
509
|
+
if (action === 'keys') {
|
|
510
|
+
await keyManager.showKeyManagementMenu();
|
|
511
|
+
return main();
|
|
512
|
+
}
|
|
513
|
+
if (action === 'encrypt') {
|
|
514
|
+
try {
|
|
515
|
+
// Ask who to encrypt for
|
|
516
|
+
const { recipient } = await escapeablePrompt([
|
|
517
|
+
{
|
|
518
|
+
type: 'list',
|
|
519
|
+
name: 'recipient',
|
|
520
|
+
message: promptMessage('Who do you want to encrypt this message for?'),
|
|
521
|
+
choices: [
|
|
522
|
+
{ name: `${icons.contact} Someone else ${colors.muted('(use their public key)')}`, value: 'other' },
|
|
523
|
+
{ name: `${icons.multiple} Multiple recipients`, value: 'multiple' },
|
|
524
|
+
{ name: `${icons.key} Myself ${colors.muted('(use my public key)')}`, value: 'self' },
|
|
525
|
+
new inquirer.Separator(),
|
|
526
|
+
mainMenuChoice(),
|
|
527
|
+
],
|
|
528
|
+
},
|
|
529
|
+
]);
|
|
530
|
+
if (recipient === 'back' || recipient === 'main-menu') {
|
|
531
|
+
return main();
|
|
532
|
+
}
|
|
533
|
+
let recipientPublicKeys = [];
|
|
534
|
+
let recipientNames = [];
|
|
535
|
+
let isNewContact = false;
|
|
536
|
+
// Handle multiple recipients
|
|
537
|
+
if (recipient === 'multiple') {
|
|
538
|
+
const recipients = await selectMultipleRecipients();
|
|
539
|
+
if (recipients.length === 0) {
|
|
540
|
+
console.log();
|
|
541
|
+
showError('No recipients selected. Aborting.');
|
|
542
|
+
console.log();
|
|
543
|
+
return main();
|
|
544
|
+
}
|
|
545
|
+
recipientPublicKeys = recipients.map((r) => r.publicKey);
|
|
546
|
+
recipientNames = recipients.map((r) => r.name);
|
|
547
|
+
// Show summary
|
|
548
|
+
console.log(colors.primary('\nEncrypting for the following recipients:'));
|
|
549
|
+
for (const name of recipientNames) {
|
|
550
|
+
console.log(colors.muted(` • ${name}`));
|
|
551
|
+
}
|
|
552
|
+
console.log();
|
|
553
|
+
}
|
|
554
|
+
else if (recipient === 'other') {
|
|
555
|
+
// Check if there are any saved contacts
|
|
556
|
+
const contacts = db.select({ table: 'contact' });
|
|
557
|
+
// Loop for recipient selection (allows going back from contacts submenu)
|
|
558
|
+
recipientLoop: while (true) {
|
|
559
|
+
// Build main menu choices
|
|
560
|
+
const recipientChoices = [];
|
|
561
|
+
if (contacts.length > 0) {
|
|
562
|
+
recipientChoices.push({
|
|
563
|
+
name: `${icons.contact} Saved contacts ${colors.muted(`(${contacts.length} available)`)}`,
|
|
564
|
+
value: 'saved-contacts',
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
recipientChoices.push({ name: `${icons.add} Use a new public key`, value: 'new' }, new inquirer.Separator(), mainMenuChoice());
|
|
568
|
+
const { recipientSource } = await escapeablePrompt([
|
|
569
|
+
{
|
|
570
|
+
type: 'list',
|
|
571
|
+
name: 'recipientSource',
|
|
572
|
+
message: promptMessage('How would you like to specify the recipient?'),
|
|
573
|
+
choices: recipientChoices,
|
|
574
|
+
},
|
|
575
|
+
]);
|
|
576
|
+
if (recipientSource === 'main' || recipientSource === 'main-menu') {
|
|
577
|
+
return main();
|
|
578
|
+
}
|
|
579
|
+
if (recipientSource === 'saved-contacts') {
|
|
580
|
+
// Show contacts submenu
|
|
581
|
+
const contactChoices = contacts.map((c) => ({
|
|
582
|
+
name: `${icons.contact} ${c.name} ${colors.muted(`<${c.email}>`)}`,
|
|
583
|
+
value: c.id,
|
|
584
|
+
}));
|
|
585
|
+
contactChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice(), new inquirer.Separator());
|
|
586
|
+
const { contactChoice } = await escapeablePrompt([
|
|
587
|
+
{
|
|
588
|
+
type: 'list',
|
|
589
|
+
name: 'contactChoice',
|
|
590
|
+
message: promptMessage('Select a contact:'),
|
|
591
|
+
choices: contactChoices,
|
|
592
|
+
},
|
|
593
|
+
]);
|
|
594
|
+
if (contactChoice === 'main' || contactChoice === 'main-menu') {
|
|
595
|
+
return main();
|
|
596
|
+
}
|
|
597
|
+
if (contactChoice === 'back') {
|
|
598
|
+
// Go back to recipient source selection
|
|
599
|
+
continue recipientLoop;
|
|
600
|
+
}
|
|
601
|
+
// Use saved contact
|
|
602
|
+
const selectedContact = contacts.find((c) => c.id === contactChoice);
|
|
603
|
+
if (selectedContact) {
|
|
604
|
+
recipientPublicKeys = [selectedContact.public_key];
|
|
605
|
+
break recipientLoop;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else if (recipientSource === 'new') {
|
|
609
|
+
const publicKey = await getRecipientPublicKey();
|
|
610
|
+
if (!publicKey) {
|
|
611
|
+
console.log();
|
|
612
|
+
showError('Could not get recipient public key. Aborting.');
|
|
613
|
+
console.log();
|
|
614
|
+
return main();
|
|
615
|
+
}
|
|
616
|
+
recipientPublicKeys = [publicKey];
|
|
617
|
+
isNewContact = true;
|
|
618
|
+
break recipientLoop;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Detect available editors
|
|
623
|
+
const availableEditors = detectAvailableEditors();
|
|
624
|
+
let message;
|
|
625
|
+
// Loop for input method selection (allows going back from editor selection)
|
|
626
|
+
inputMethodLoop: while (true) {
|
|
627
|
+
// Ask for input method
|
|
628
|
+
const inputChoices = [];
|
|
629
|
+
// Always add clipboard option first
|
|
630
|
+
inputChoices.push({
|
|
631
|
+
name: `${icons.clipboard} Paste from clipboard`,
|
|
632
|
+
value: 'clipboard',
|
|
633
|
+
});
|
|
634
|
+
if (availableEditors.length > 0) {
|
|
635
|
+
inputChoices.push({ name: `${icons.editor} Use an editor`, value: 'editor' }, { name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`, value: 'inline' });
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
inputChoices.push({
|
|
639
|
+
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
640
|
+
value: 'inline',
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
// Add main menu option
|
|
644
|
+
inputChoices.push(new inquirer.Separator(), mainMenuChoice());
|
|
645
|
+
const { inputMethod } = await escapeablePrompt([
|
|
646
|
+
{
|
|
647
|
+
type: 'list',
|
|
648
|
+
name: 'inputMethod',
|
|
649
|
+
message: promptMessage('How would you like to enter your message?'),
|
|
650
|
+
choices: inputChoices,
|
|
651
|
+
},
|
|
652
|
+
]);
|
|
653
|
+
if (inputMethod === 'back' || inputMethod === 'main-menu') {
|
|
654
|
+
return main();
|
|
655
|
+
}
|
|
656
|
+
if (inputMethod === 'clipboard') {
|
|
657
|
+
try {
|
|
658
|
+
message = await clipboardy.read();
|
|
659
|
+
if (!message || message.trim() === '') {
|
|
660
|
+
console.log();
|
|
661
|
+
showError('Clipboard is empty.');
|
|
662
|
+
console.log();
|
|
663
|
+
return main();
|
|
664
|
+
}
|
|
665
|
+
console.log();
|
|
666
|
+
showSuccess('Message loaded from clipboard');
|
|
667
|
+
console.log();
|
|
668
|
+
break inputMethodLoop;
|
|
669
|
+
}
|
|
670
|
+
catch (clipError) {
|
|
671
|
+
console.log();
|
|
672
|
+
showError(`Failed to read from clipboard: ${clipError}`);
|
|
673
|
+
return main();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else if (inputMethod === 'editor') {
|
|
677
|
+
// Let user choose editor
|
|
678
|
+
const editorChoices = availableEditors.map((e) => ({
|
|
679
|
+
name: `${icons.editor} ${e.name} ${colors.muted(`(${getEditorInstructions(e.command)})`)}`,
|
|
680
|
+
value: e.command,
|
|
681
|
+
}));
|
|
682
|
+
editorChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice());
|
|
683
|
+
const { selectedEditor } = await escapeablePrompt([
|
|
684
|
+
{
|
|
685
|
+
type: 'list',
|
|
686
|
+
name: 'selectedEditor',
|
|
687
|
+
message: promptMessage('Choose your editor:'),
|
|
688
|
+
choices: editorChoices,
|
|
689
|
+
},
|
|
690
|
+
]);
|
|
691
|
+
if (selectedEditor === 'back') {
|
|
692
|
+
// Re-ask for input method
|
|
693
|
+
continue inputMethodLoop;
|
|
694
|
+
}
|
|
695
|
+
if (selectedEditor === 'main-menu') {
|
|
696
|
+
return main();
|
|
697
|
+
}
|
|
698
|
+
// Set the EDITOR environment variable before opening inquirer editor
|
|
699
|
+
const originalEditor = process.env.EDITOR;
|
|
700
|
+
const originalVisual = process.env.VISUAL;
|
|
701
|
+
process.env.EDITOR = selectedEditor;
|
|
702
|
+
process.env.VISUAL = selectedEditor;
|
|
703
|
+
const editorName = availableEditors.find((e) => e.command === selectedEditor)?.name || 'editor';
|
|
704
|
+
console.log(colors.muted('\nNote: The temp file is automatically deleted after encryption.\n'));
|
|
705
|
+
try {
|
|
706
|
+
const { editorInput } = await escapeablePrompt([
|
|
707
|
+
{
|
|
708
|
+
type: 'editor',
|
|
709
|
+
name: 'editorInput',
|
|
710
|
+
message: promptMessage(`Press Enter to open ${editorName}:`),
|
|
711
|
+
postfix: '.txt',
|
|
712
|
+
waitForUseInput: false,
|
|
713
|
+
},
|
|
714
|
+
]);
|
|
715
|
+
message = editorInput;
|
|
716
|
+
break inputMethodLoop;
|
|
717
|
+
}
|
|
718
|
+
finally {
|
|
719
|
+
// Restore original environment variables
|
|
720
|
+
if (originalEditor !== undefined) {
|
|
721
|
+
process.env.EDITOR = originalEditor;
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
delete process.env.EDITOR;
|
|
725
|
+
}
|
|
726
|
+
if (originalVisual !== undefined) {
|
|
727
|
+
process.env.VISUAL = originalVisual;
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
delete process.env.VISUAL;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
message = await readInlineMultilineInput('Enter your message:');
|
|
736
|
+
break inputMethodLoop;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (!message || message.trim() === '') {
|
|
740
|
+
console.log();
|
|
741
|
+
showError('No message provided. Aborting.');
|
|
742
|
+
console.log();
|
|
743
|
+
return main();
|
|
744
|
+
}
|
|
745
|
+
console.log();
|
|
746
|
+
showLoading('Encrypting message...');
|
|
747
|
+
console.log();
|
|
748
|
+
const encrypted = await encryptMessage(message, recipientPublicKeys.length > 0 ? recipientPublicKeys : undefined);
|
|
749
|
+
// Clear screen, show encrypted message, then clipboard status
|
|
750
|
+
console.clear();
|
|
751
|
+
printBanner();
|
|
752
|
+
console.log(colors.successBold('Encrypted Message:\n'));
|
|
753
|
+
printDivider();
|
|
754
|
+
console.log(encrypted);
|
|
755
|
+
printDivider();
|
|
756
|
+
// Copy to clipboard and show status below the message
|
|
757
|
+
try {
|
|
758
|
+
await clipboardy.write(encrypted);
|
|
759
|
+
console.log();
|
|
760
|
+
showSuccess('Encrypted message copied to clipboard');
|
|
761
|
+
console.log();
|
|
762
|
+
}
|
|
763
|
+
catch (clipError) {
|
|
764
|
+
console.log();
|
|
765
|
+
showWarning('Clipboard unavailable');
|
|
766
|
+
console.log();
|
|
767
|
+
}
|
|
768
|
+
// Offer to save the contact if it's a new public key (single recipient only)
|
|
769
|
+
const newPublicKey = recipientPublicKeys[0];
|
|
770
|
+
if (isNewContact && newPublicKey !== undefined && recipientPublicKeys.length === 1) {
|
|
771
|
+
const { saveContact } = await escapeablePrompt([
|
|
772
|
+
{
|
|
773
|
+
type: 'confirm',
|
|
774
|
+
name: 'saveContact',
|
|
775
|
+
message: promptMessage('Would you like to save this contact for future use?'),
|
|
776
|
+
default: true,
|
|
777
|
+
},
|
|
778
|
+
]);
|
|
779
|
+
if (saveContact) {
|
|
780
|
+
try {
|
|
781
|
+
// Extract key information
|
|
782
|
+
const keyInfo = await extractPublicKeyInfo(newPublicKey);
|
|
783
|
+
// Prompt for contact name
|
|
784
|
+
const defaultName = (keyInfo.email || 'unknown').split('@')[0] || 'Contact';
|
|
785
|
+
const answers = await escapeablePrompt([
|
|
786
|
+
{
|
|
787
|
+
type: 'input',
|
|
788
|
+
name: 'contactName',
|
|
789
|
+
message: promptMessage('Contact name:'),
|
|
790
|
+
default: defaultName,
|
|
791
|
+
validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
|
|
792
|
+
},
|
|
793
|
+
]);
|
|
794
|
+
const contactName = answers.contactName;
|
|
795
|
+
// Check if contact already exists by fingerprint
|
|
796
|
+
const existingContacts = db.select({
|
|
797
|
+
table: 'contact',
|
|
798
|
+
where: { key: 'fingerprint', compare: 'is', value: keyInfo.fingerprint },
|
|
799
|
+
});
|
|
800
|
+
if (existingContacts.length > 0) {
|
|
801
|
+
console.log();
|
|
802
|
+
showWarning('This contact already exists.');
|
|
803
|
+
console.log();
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
// Save the contact
|
|
807
|
+
db.insert('contact', {
|
|
808
|
+
name: contactName.trim(),
|
|
809
|
+
email: keyInfo.email,
|
|
810
|
+
fingerprint: keyInfo.fingerprint,
|
|
811
|
+
public_key: newPublicKey,
|
|
812
|
+
algorithm: keyInfo.algorithm,
|
|
813
|
+
key_size: keyInfo.keySize,
|
|
814
|
+
trusted: false,
|
|
815
|
+
last_verified_at: null,
|
|
816
|
+
notes: null,
|
|
817
|
+
expires_at: keyInfo.expiresAt,
|
|
818
|
+
revoked: false,
|
|
819
|
+
});
|
|
820
|
+
console.log();
|
|
821
|
+
showSuccess(`Contact "${contactName}" saved successfully!`);
|
|
822
|
+
console.log();
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
console.log();
|
|
827
|
+
showError(`Failed to save contact: ${error instanceof Error ? error.message : error}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
// Re-throw escape errors to be handled by the main loop
|
|
834
|
+
if (error instanceof EscapeError)
|
|
835
|
+
throw error;
|
|
836
|
+
console.log();
|
|
837
|
+
showError(`Encryption failed: ${error instanceof Error ? error.message : error}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
else if (action === 'decrypt') {
|
|
841
|
+
try {
|
|
842
|
+
// Detect available editors
|
|
843
|
+
const availableEditors = detectAvailableEditors();
|
|
844
|
+
let encrypted;
|
|
845
|
+
// Loop for input method selection (allows going back from editor selection)
|
|
846
|
+
decryptInputLoop: while (true) {
|
|
847
|
+
// Ask for input method
|
|
848
|
+
const inputChoices = [];
|
|
849
|
+
// Always add clipboard option first
|
|
850
|
+
inputChoices.push({
|
|
851
|
+
name: `${icons.clipboard} Paste from clipboard`,
|
|
852
|
+
value: 'clipboard',
|
|
853
|
+
});
|
|
854
|
+
if (availableEditors.length > 0) {
|
|
855
|
+
inputChoices.push({ name: `${icons.editor} Use an editor`, value: 'editor' }, { name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`, value: 'inline' });
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
inputChoices.push({
|
|
859
|
+
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
860
|
+
value: 'inline',
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
// Add main menu option
|
|
864
|
+
inputChoices.push(new inquirer.Separator(), mainMenuChoice());
|
|
865
|
+
const { inputMethod } = await escapeablePrompt([
|
|
866
|
+
{
|
|
867
|
+
type: 'list',
|
|
868
|
+
name: 'inputMethod',
|
|
869
|
+
message: promptMessage('How would you like to enter the encrypted message?'),
|
|
870
|
+
choices: inputChoices,
|
|
871
|
+
},
|
|
872
|
+
]);
|
|
873
|
+
if (inputMethod === 'back' || inputMethod === 'main-menu') {
|
|
874
|
+
return main();
|
|
875
|
+
}
|
|
876
|
+
if (inputMethod === 'clipboard') {
|
|
877
|
+
try {
|
|
878
|
+
encrypted = await clipboardy.read();
|
|
879
|
+
if (!encrypted || encrypted.trim() === '') {
|
|
880
|
+
console.log();
|
|
881
|
+
showError('Clipboard is empty.');
|
|
882
|
+
console.log();
|
|
883
|
+
return main();
|
|
884
|
+
}
|
|
885
|
+
console.log();
|
|
886
|
+
showSuccess('Encrypted message loaded from clipboard');
|
|
887
|
+
console.log();
|
|
888
|
+
break decryptInputLoop;
|
|
889
|
+
}
|
|
890
|
+
catch (clipError) {
|
|
891
|
+
console.log();
|
|
892
|
+
showError(`Failed to read from clipboard: ${clipError}`);
|
|
893
|
+
return main();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
else if (inputMethod === 'editor') {
|
|
897
|
+
// Let user choose editor
|
|
898
|
+
const editorChoices = availableEditors.map((e) => ({
|
|
899
|
+
name: `${icons.editor} ${e.name} ${colors.muted(`(${getEditorInstructions(e.command)})`)}`,
|
|
900
|
+
value: e.command,
|
|
901
|
+
}));
|
|
902
|
+
editorChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice());
|
|
903
|
+
const { selectedEditor } = await escapeablePrompt([
|
|
904
|
+
{
|
|
905
|
+
type: 'list',
|
|
906
|
+
name: 'selectedEditor',
|
|
907
|
+
message: promptMessage('Choose your editor:'),
|
|
908
|
+
choices: editorChoices,
|
|
909
|
+
},
|
|
910
|
+
]);
|
|
911
|
+
if (selectedEditor === 'back') {
|
|
912
|
+
// Re-ask for input method
|
|
913
|
+
continue decryptInputLoop;
|
|
914
|
+
}
|
|
915
|
+
if (selectedEditor === 'main-menu') {
|
|
916
|
+
return main();
|
|
917
|
+
}
|
|
918
|
+
// Set the EDITOR environment variable before opening inquirer editor
|
|
919
|
+
const originalEditor = process.env.EDITOR;
|
|
920
|
+
const originalVisual = process.env.VISUAL;
|
|
921
|
+
process.env.EDITOR = selectedEditor;
|
|
922
|
+
process.env.VISUAL = selectedEditor;
|
|
923
|
+
const editorName = availableEditors.find((e) => e.command === selectedEditor)?.name || 'editor';
|
|
924
|
+
console.log(colors.muted('\nNote: The temp file is automatically deleted after decryption.\n'));
|
|
925
|
+
try {
|
|
926
|
+
const { editorInput } = await escapeablePrompt([
|
|
927
|
+
{
|
|
928
|
+
type: 'editor',
|
|
929
|
+
name: 'editorInput',
|
|
930
|
+
message: promptMessage(`Press Enter to open ${editorName}:`),
|
|
931
|
+
postfix: '.txt',
|
|
932
|
+
waitForUseInput: false,
|
|
933
|
+
},
|
|
934
|
+
]);
|
|
935
|
+
encrypted = editorInput;
|
|
936
|
+
break decryptInputLoop;
|
|
937
|
+
}
|
|
938
|
+
finally {
|
|
939
|
+
// Restore original environment variables
|
|
940
|
+
if (originalEditor !== undefined) {
|
|
941
|
+
process.env.EDITOR = originalEditor;
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
delete process.env.EDITOR;
|
|
945
|
+
}
|
|
946
|
+
if (originalVisual !== undefined) {
|
|
947
|
+
process.env.VISUAL = originalVisual;
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
delete process.env.VISUAL;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
encrypted = await readInlineMultilineInput('Paste the encrypted message:');
|
|
956
|
+
break decryptInputLoop;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (!encrypted || encrypted.trim() === '') {
|
|
960
|
+
console.log();
|
|
961
|
+
showError('No encrypted message provided. Aborting.');
|
|
962
|
+
console.log();
|
|
963
|
+
return main();
|
|
964
|
+
}
|
|
965
|
+
console.log();
|
|
966
|
+
showLoading('Decrypting message...');
|
|
967
|
+
console.log();
|
|
968
|
+
const decrypted = await decryptMessage(encrypted);
|
|
969
|
+
// Clear screen, show decrypted message, then clipboard status
|
|
970
|
+
console.clear();
|
|
971
|
+
printBanner();
|
|
972
|
+
console.log(colors.successBold('Decrypted Message:\n'));
|
|
973
|
+
printDivider();
|
|
974
|
+
console.log(decrypted);
|
|
975
|
+
printDivider();
|
|
976
|
+
// Copy to clipboard and show status below the message
|
|
977
|
+
try {
|
|
978
|
+
await clipboardy.write(decrypted);
|
|
979
|
+
console.log();
|
|
980
|
+
showSuccess('Decrypted message copied to clipboard');
|
|
981
|
+
console.log();
|
|
982
|
+
}
|
|
983
|
+
catch (clipError) {
|
|
984
|
+
console.log();
|
|
985
|
+
showWarning('Clipboard unavailable');
|
|
986
|
+
console.log();
|
|
987
|
+
}
|
|
988
|
+
// Wait for user to press Enter before continuing
|
|
989
|
+
await escapeablePrompt([
|
|
990
|
+
{
|
|
991
|
+
type: 'input',
|
|
992
|
+
name: 'continue',
|
|
993
|
+
message: colors.muted('Press Enter to continue...'),
|
|
994
|
+
},
|
|
995
|
+
]);
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
// Re-throw escape errors to be handled by the main loop
|
|
999
|
+
if (error instanceof EscapeError)
|
|
1000
|
+
throw error;
|
|
1001
|
+
console.log();
|
|
1002
|
+
showError(`Decryption failed: ${error instanceof Error ? error.message : error}`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
// Ask if user wants to continue
|
|
1006
|
+
const { nextAction } = await escapeablePrompt([
|
|
1007
|
+
{
|
|
1008
|
+
type: 'list',
|
|
1009
|
+
name: 'nextAction',
|
|
1010
|
+
message: promptMessage('What would you like to do next?'),
|
|
1011
|
+
choices: [
|
|
1012
|
+
{ name: `${icons.loop} Perform another operation`, value: 'continue' },
|
|
1013
|
+
exitChoice(),
|
|
1014
|
+
],
|
|
1015
|
+
},
|
|
1016
|
+
]);
|
|
1017
|
+
if (nextAction === 'continue') {
|
|
1018
|
+
await main();
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
clearPassphraseCache();
|
|
1022
|
+
console.clear();
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
// Graceful exit on Ctrl+C
|
|
1026
|
+
process.on('SIGINT', () => {
|
|
1027
|
+
clearPassphraseCache();
|
|
1028
|
+
console.clear();
|
|
1029
|
+
process.exit(0);
|
|
1030
|
+
});
|
|
1031
|
+
// Enable global escape key handling and run menu in a loop
|
|
1032
|
+
enableGlobalEscape();
|
|
1033
|
+
async function runApp() {
|
|
1034
|
+
while (true) {
|
|
1035
|
+
try {
|
|
1036
|
+
await main();
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
const e = error;
|
|
1040
|
+
// If escape was pressed, just restart the menu
|
|
1041
|
+
if (error instanceof EscapeError ||
|
|
1042
|
+
checkAndResetEscape() ||
|
|
1043
|
+
e.message?.includes('prompt was closed')) {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
// Handle Ctrl+C gracefully (inquirer throws ExitPromptError)
|
|
1047
|
+
if (e.message?.includes('force closed the prompt')) {
|
|
1048
|
+
clearPassphraseCache();
|
|
1049
|
+
console.clear();
|
|
1050
|
+
process.exit(0);
|
|
1051
|
+
}
|
|
1052
|
+
// Handle other errors
|
|
1053
|
+
clearPassphraseCache();
|
|
1054
|
+
console.clear();
|
|
1055
|
+
showError(`Error: ${e.message || error}`);
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
runApp();
|
|
1061
|
+
//# sourceMappingURL=pgp-tool.js.map
|