lpgp 0.5.0 → 0.6.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/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +16 -9
- package/dist/cli-commands.js.map +1 -1
- package/dist/decrypt.d.ts.map +1 -1
- package/dist/decrypt.js +1 -0
- package/dist/decrypt.js.map +1 -1
- package/dist/encrypt.d.ts.map +1 -1
- package/dist/encrypt.js +1 -0
- package/dist/encrypt.js.map +1 -1
- package/dist/inline-editor.d.ts +2 -0
- package/dist/inline-editor.d.ts.map +1 -0
- package/dist/inline-editor.js +196 -0
- package/dist/inline-editor.js.map +1 -0
- package/dist/key-manager.d.ts +0 -6
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/key-manager.js +51 -33
- package/dist/key-manager.js.map +1 -1
- package/dist/key-utils.d.ts +16 -0
- package/dist/key-utils.d.ts.map +1 -1
- package/dist/key-utils.js +39 -0
- package/dist/key-utils.js.map +1 -1
- package/dist/passphrase-store.d.ts +6 -0
- package/dist/passphrase-store.d.ts.map +1 -0
- package/dist/passphrase-store.js +89 -0
- package/dist/passphrase-store.js.map +1 -0
- package/dist/pgp-tool.js +750 -1050
- package/dist/pgp-tool.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +22 -1
- package/dist/prompts.js.map +1 -1
- package/dist/ui.d.ts +1 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +31 -11
- package/dist/ui.js.map +1 -1
- package/dist/version-check.d.ts +6 -0
- package/dist/version-check.d.ts.map +1 -0
- package/dist/version-check.js +91 -0
- package/dist/version-check.js.map +1 -0
- package/package.json +1 -1
package/dist/pgp-tool.js
CHANGED
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
import * as openpgp from 'openpgp';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
|
-
import * as readline from 'readline';
|
|
6
|
-
import { stdin as input, stdout as output } from 'process';
|
|
7
5
|
import clipboardy from 'clipboardy';
|
|
8
6
|
import { Command } from 'commander';
|
|
9
7
|
import { Db } from './db.js';
|
|
10
8
|
import { KeyManager } from './key-manager.js';
|
|
11
|
-
import { extractPublicKeyInfo } from './key-utils.js';
|
|
9
|
+
import { extractPublicKeyInfo, formatMaskedRecipient } from './key-utils.js';
|
|
12
10
|
import { escapeablePrompt, enableGlobalEscape, checkAndResetEscape, EscapeError, } from './prompts.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { getCachedPassphrase, cachePassphrase, } from './passphrase-store.js';
|
|
12
|
+
import { readInlineMultiline } from './inline-editor.js';
|
|
13
|
+
import { colors, icons, printBanner, printDivider, printHomeStatus, showSuccess, showError, showWarning, showInfo, showLoading, promptMessage, mainMenuChoice, backChoice, exitChoice, } from './ui.js';
|
|
15
14
|
import { generateCommand, exportPublicCommand, listKeysCommand, encryptCommand, decryptCommand, } from './cli-commands.js';
|
|
16
15
|
import { readFileSync } from 'fs';
|
|
17
16
|
import { dirname, join } from 'path';
|
|
18
17
|
import { fileURLToPath } from 'url';
|
|
19
|
-
// Get package version
|
|
20
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
19
|
const __dirname = dirname(__filename);
|
|
22
20
|
function getPackageVersion() {
|
|
@@ -29,17 +27,13 @@ function getPackageVersion() {
|
|
|
29
27
|
return '0.0.0';
|
|
30
28
|
}
|
|
31
29
|
}
|
|
32
|
-
// Helper: Collect multiple --to values
|
|
33
30
|
function collect(value, previous) {
|
|
34
31
|
return previous.concat([value]);
|
|
35
32
|
}
|
|
36
|
-
// Check if running in CLI mode (has subcommand arguments)
|
|
37
33
|
function isCLIMode() {
|
|
38
|
-
// If we have arguments beyond the script name that look like commands
|
|
39
34
|
const args = process.argv.slice(2);
|
|
40
35
|
if (args.length === 0)
|
|
41
36
|
return false;
|
|
42
|
-
// Check if first arg is a known command or starts with -
|
|
43
37
|
const commands = [
|
|
44
38
|
'generate',
|
|
45
39
|
'export-public',
|
|
@@ -53,7 +47,6 @@ function isCLIMode() {
|
|
|
53
47
|
];
|
|
54
48
|
return commands.some((cmd) => args[0] === cmd || args[0]?.startsWith('-'));
|
|
55
49
|
}
|
|
56
|
-
// Set up CLI commands
|
|
57
50
|
function setupCLI() {
|
|
58
51
|
const program = new Command()
|
|
59
52
|
.name('lpgp')
|
|
@@ -94,148 +87,172 @@ function setupCLI() {
|
|
|
94
87
|
.action(decryptCommand);
|
|
95
88
|
program.parse();
|
|
96
89
|
}
|
|
97
|
-
// Run CLI mode if arguments provided
|
|
98
90
|
if (isCLIMode()) {
|
|
99
91
|
setupCLI();
|
|
100
92
|
}
|
|
101
93
|
else {
|
|
102
|
-
// Interactive mode - continue with existing behavior
|
|
103
94
|
startInteractiveMode();
|
|
104
95
|
}
|
|
105
96
|
function startInteractiveMode() {
|
|
106
|
-
// Config to allow weak keys like DSA (not recommended for production)
|
|
107
97
|
const weakKeyConfig = {
|
|
108
98
|
rejectPublicKeyAlgorithms: new Set(),
|
|
109
99
|
rejectHashAlgorithms: new Set(),
|
|
110
100
|
rejectMessageHashAlgorithms: new Set(),
|
|
111
101
|
rejectCurves: new Set(),
|
|
102
|
+
allowMissingKeyFlags: true,
|
|
112
103
|
};
|
|
113
|
-
// Database and key manager (initialized in main())
|
|
114
104
|
let db;
|
|
115
105
|
let keyManager;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Get installed version of lpgp (null if not installed)
|
|
119
|
-
function getInstalledVersion() {
|
|
106
|
+
const unlockedKeys = new Map();
|
|
107
|
+
function checkEditorAvailable(command) {
|
|
120
108
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
encoding: 'utf-8',
|
|
124
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
125
|
-
});
|
|
126
|
-
// Get the installed version
|
|
127
|
-
const version = execSync('npm list -g lpgp --json 2>/dev/null', {
|
|
128
|
-
encoding: 'utf-8',
|
|
129
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
130
|
-
});
|
|
131
|
-
const parsed = JSON.parse(version);
|
|
132
|
-
return parsed.dependencies?.lpgp?.version || null;
|
|
109
|
+
execSync(`which ${command}`, { stdio: 'ignore' });
|
|
110
|
+
return true;
|
|
133
111
|
}
|
|
134
112
|
catch {
|
|
135
|
-
return
|
|
113
|
+
return false;
|
|
136
114
|
}
|
|
137
115
|
}
|
|
138
|
-
|
|
139
|
-
|
|
116
|
+
function detectAvailableEditors() {
|
|
117
|
+
const editors = [
|
|
118
|
+
{ name: 'VS Code', command: 'code', available: false },
|
|
119
|
+
{ name: 'Neovim', command: 'nvim', available: false },
|
|
120
|
+
{ name: 'Vim', command: 'vim', available: false },
|
|
121
|
+
{ name: 'Nano', command: 'nano', available: false },
|
|
122
|
+
{ name: 'Emacs', command: 'emacs', available: false },
|
|
123
|
+
];
|
|
124
|
+
if (process.platform === 'darwin') {
|
|
125
|
+
editors.push({ name: 'TextEdit', command: 'open -e', available: true });
|
|
126
|
+
}
|
|
127
|
+
else if (process.platform === 'win32') {
|
|
128
|
+
editors.push({ name: 'Notepad', command: 'notepad', available: true });
|
|
129
|
+
}
|
|
130
|
+
for (const editor of editors) {
|
|
131
|
+
if (editor.command.includes('open -e') || editor.command === 'notepad') {
|
|
132
|
+
editor.available = true;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
editor.available = checkEditorAvailable(editor.command);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return editors.filter((e) => e.available);
|
|
139
|
+
}
|
|
140
|
+
function getEditorInstructions(editorCommand) {
|
|
141
|
+
const instructions = {
|
|
142
|
+
nano: 'Save: Ctrl+O, then Enter. Exit: Ctrl+X',
|
|
143
|
+
vim: 'Save and exit: :wq | Cancel: :q!',
|
|
144
|
+
nvim: 'Save and exit: :wq | Cancel: :q!',
|
|
145
|
+
code: 'Save: Cmd/Ctrl+S, then close the editor tab',
|
|
146
|
+
emacs: 'Save: Ctrl+X Ctrl+S | Exit: Ctrl+X Ctrl+C',
|
|
147
|
+
'open -e': 'Save: Cmd+S, then close the window',
|
|
148
|
+
notepad: 'Save: Ctrl+S, then close the window',
|
|
149
|
+
};
|
|
150
|
+
return instructions[editorCommand] || 'Save and close the editor when done';
|
|
151
|
+
}
|
|
152
|
+
// ---------- Clipboard / key extraction ----------
|
|
153
|
+
function extractAllPublicKeys(content) {
|
|
154
|
+
const keyRegex = /-----BEGIN PGP PUBLIC KEY BLOCK-----[\s\S]*?-----END PGP PUBLIC KEY BLOCK-----/g;
|
|
155
|
+
return content.match(keyRegex) ?? [];
|
|
156
|
+
}
|
|
157
|
+
async function readClipboardSafe() {
|
|
140
158
|
try {
|
|
141
|
-
|
|
142
|
-
encoding: 'utf-8',
|
|
143
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
144
|
-
});
|
|
145
|
-
return result.trim();
|
|
159
|
+
return await clipboardy.read();
|
|
146
160
|
}
|
|
147
161
|
catch {
|
|
148
162
|
return null;
|
|
149
163
|
}
|
|
150
164
|
}
|
|
151
|
-
|
|
152
|
-
function detectPackageManager() {
|
|
165
|
+
async function writeClipboardSafe(content) {
|
|
153
166
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
});
|
|
157
|
-
return 'pnpm';
|
|
167
|
+
await clipboardy.write(content);
|
|
168
|
+
return true;
|
|
158
169
|
}
|
|
159
170
|
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// ---------- Session: unlock keys ----------
|
|
175
|
+
async function unlockKeypair(keypair, options = {}) {
|
|
176
|
+
const existing = unlockedKeys.get(keypair.fingerprint);
|
|
177
|
+
if (existing)
|
|
178
|
+
return existing;
|
|
179
|
+
const privateKey = await openpgp.readPrivateKey({
|
|
180
|
+
armoredKey: keypair.private_key,
|
|
181
|
+
config: weakKeyConfig,
|
|
182
|
+
});
|
|
183
|
+
if (!keypair.passphrase_protected) {
|
|
184
|
+
unlockedKeys.set(keypair.fingerprint, privateKey);
|
|
185
|
+
return privateKey;
|
|
186
|
+
}
|
|
187
|
+
const cached = getCachedPassphrase(keypair.fingerprint);
|
|
188
|
+
if (cached) {
|
|
160
189
|
try {
|
|
161
|
-
|
|
162
|
-
|
|
190
|
+
const unlocked = await openpgp.decryptKey({
|
|
191
|
+
privateKey,
|
|
192
|
+
passphrase: cached,
|
|
193
|
+
config: weakKeyConfig,
|
|
163
194
|
});
|
|
164
|
-
|
|
195
|
+
unlockedKeys.set(keypair.fingerprint, unlocked);
|
|
196
|
+
return unlocked;
|
|
165
197
|
}
|
|
166
198
|
catch {
|
|
167
|
-
|
|
199
|
+
if (options.silent)
|
|
200
|
+
return null;
|
|
201
|
+
showWarning(`Saved passphrase for "${keypair.name}" is invalid.`);
|
|
168
202
|
}
|
|
169
203
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
function isOlderVersion(v1, v2) {
|
|
173
|
-
const p1 = v1.split('.').map(Number);
|
|
174
|
-
const p2 = v2.split('.').map(Number);
|
|
175
|
-
for (let i = 0; i < 3; i++) {
|
|
176
|
-
if ((p1[i] || 0) < (p2[i] || 0))
|
|
177
|
-
return true;
|
|
178
|
-
if ((p1[i] || 0) > (p2[i] || 0))
|
|
179
|
-
return false;
|
|
204
|
+
else if (options.silent) {
|
|
205
|
+
return null;
|
|
180
206
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
execSync(cmd, { stdio: 'inherit' });
|
|
197
|
-
console.log();
|
|
198
|
-
if (isUpdate) {
|
|
199
|
-
showSuccess('lpgp updated successfully!');
|
|
207
|
+
let attempts = 0;
|
|
208
|
+
while (attempts < 3) {
|
|
209
|
+
attempts++;
|
|
210
|
+
const { passphrase } = await escapeablePrompt([
|
|
211
|
+
{
|
|
212
|
+
type: 'password',
|
|
213
|
+
name: 'passphrase',
|
|
214
|
+
message: promptMessage(`Passphrase for "${keypair.name}":`),
|
|
215
|
+
mask: '*',
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
if (!passphrase) {
|
|
219
|
+
showWarning('Cancelled.');
|
|
220
|
+
return null;
|
|
200
221
|
}
|
|
201
|
-
|
|
202
|
-
|
|
222
|
+
try {
|
|
223
|
+
const unlocked = await openpgp.decryptKey({
|
|
224
|
+
privateKey,
|
|
225
|
+
passphrase,
|
|
226
|
+
config: weakKeyConfig,
|
|
227
|
+
});
|
|
228
|
+
cachePassphrase(keypair.fingerprint, passphrase);
|
|
229
|
+
unlockedKeys.set(keypair.fingerprint, unlocked);
|
|
230
|
+
showSuccess('Passphrase saved locally — you won\'t be asked again.');
|
|
231
|
+
return unlocked;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
showError('Incorrect passphrase. Try again.');
|
|
203
235
|
}
|
|
204
|
-
console.log();
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
console.log();
|
|
209
|
-
showError(`Failed to ${isUpdate ? 'update' : 'install'}. You may need to run with sudo:`);
|
|
210
|
-
console.log(colors.muted(` sudo ${cmd}`));
|
|
211
|
-
console.log();
|
|
212
|
-
return false;
|
|
213
236
|
}
|
|
237
|
+
return null;
|
|
214
238
|
}
|
|
215
|
-
async function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
// Use default keypair's public key (encrypt to self)
|
|
226
|
-
const defaultKeypair = await keyManager.getDefaultKeypair();
|
|
227
|
-
if (!defaultKeypair) {
|
|
228
|
-
throw new Error('No default keypair found. Please set up a keypair first.');
|
|
239
|
+
async function unlockAllCached() {
|
|
240
|
+
const keypairs = db.select({ table: 'keypair' });
|
|
241
|
+
for (const kp of keypairs) {
|
|
242
|
+
if (kp.passphrase_protected) {
|
|
243
|
+
await unlockKeypair(kp, { silent: true });
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
await unlockKeypair(kp);
|
|
229
247
|
}
|
|
230
|
-
publicKeys = [
|
|
231
|
-
await openpgp.readKey({
|
|
232
|
-
armoredKey: defaultKeypair.public_key,
|
|
233
|
-
config: weakKeyConfig,
|
|
234
|
-
}),
|
|
235
|
-
];
|
|
236
|
-
// Update last_used_at
|
|
237
|
-
db.update('keypair', { key: 'id', value: defaultKeypair.id }, { last_used_at: new Date().toISOString() });
|
|
238
248
|
}
|
|
249
|
+
}
|
|
250
|
+
function getUnlockedPrivateKeys() {
|
|
251
|
+
return Array.from(unlockedKeys.values());
|
|
252
|
+
}
|
|
253
|
+
// ---------- Encryption ----------
|
|
254
|
+
async function encryptForKeys(message, publicKeysArmored) {
|
|
255
|
+
const publicKeys = await Promise.all(publicKeysArmored.map((key) => openpgp.readKey({ armoredKey: key, config: weakKeyConfig })));
|
|
239
256
|
const encrypted = await openpgp.encrypt({
|
|
240
257
|
message: await openpgp.createMessage({ text: message }),
|
|
241
258
|
encryptionKeys: publicKeys,
|
|
@@ -243,1091 +260,774 @@ function startInteractiveMode() {
|
|
|
243
260
|
});
|
|
244
261
|
return encrypted;
|
|
245
262
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!defaultKeypair) {
|
|
249
|
-
throw new Error('No default keypair found. Please set up a keypair first.');
|
|
250
|
-
}
|
|
251
|
-
// Check if passphrase is cached for this keypair
|
|
252
|
-
let passphrase = '';
|
|
253
|
-
if (defaultKeypair.passphrase_protected) {
|
|
254
|
-
if (passphraseCache.has(defaultKeypair.id)) {
|
|
255
|
-
// Use session-cached passphrase
|
|
256
|
-
passphrase = passphraseCache.get(defaultKeypair.id);
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
// Check if passphrase is stored in system keychain
|
|
260
|
-
const storedPassphrase = await getStoredPassphrase(defaultKeypair.fingerprint);
|
|
261
|
-
if (storedPassphrase) {
|
|
262
|
-
// Validate the stored passphrase
|
|
263
|
-
try {
|
|
264
|
-
await openpgp.decryptKey({
|
|
265
|
-
privateKey: await openpgp.readPrivateKey({
|
|
266
|
-
armoredKey: defaultKeypair.private_key,
|
|
267
|
-
config: weakKeyConfig,
|
|
268
|
-
}),
|
|
269
|
-
passphrase: storedPassphrase,
|
|
270
|
-
config: weakKeyConfig,
|
|
271
|
-
});
|
|
272
|
-
// Stored passphrase is valid, use it
|
|
273
|
-
passphrase = storedPassphrase;
|
|
274
|
-
passphraseCache.set(defaultKeypair.id, passphrase);
|
|
275
|
-
console.log(colors.muted('Using passphrase from system keychain'));
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
// Stored passphrase is invalid (key may have changed), prompt for new one
|
|
279
|
-
showWarning('Stored passphrase is invalid. Please enter your passphrase.');
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// If we still don't have a valid passphrase, prompt for it
|
|
283
|
-
if (!passphrase) {
|
|
284
|
-
const { passphraseInput } = await escapeablePrompt([
|
|
285
|
-
{
|
|
286
|
-
type: 'password',
|
|
287
|
-
name: 'passphraseInput',
|
|
288
|
-
message: promptMessage('Enter your private key passphrase:'),
|
|
289
|
-
mask: '*',
|
|
290
|
-
},
|
|
291
|
-
]);
|
|
292
|
-
passphrase = passphraseInput;
|
|
293
|
-
// Validate the passphrase by attempting to decrypt the key
|
|
294
|
-
try {
|
|
295
|
-
await openpgp.decryptKey({
|
|
296
|
-
privateKey: await openpgp.readPrivateKey({
|
|
297
|
-
armoredKey: defaultKeypair.private_key,
|
|
298
|
-
config: weakKeyConfig,
|
|
299
|
-
}),
|
|
300
|
-
passphrase,
|
|
301
|
-
config: weakKeyConfig,
|
|
302
|
-
});
|
|
303
|
-
// If successful, cache the passphrase in session
|
|
304
|
-
passphraseCache.set(defaultKeypair.id, passphrase);
|
|
305
|
-
// Ask if user wants to save passphrase to system keychain
|
|
306
|
-
const alreadyStored = await hasStoredPassphrase(defaultKeypair.fingerprint);
|
|
307
|
-
if (!alreadyStored) {
|
|
308
|
-
const { saveToKeychain } = await escapeablePrompt([
|
|
309
|
-
{
|
|
310
|
-
type: 'confirm',
|
|
311
|
-
name: 'saveToKeychain',
|
|
312
|
-
message: promptMessage('Save passphrase to system keychain?'),
|
|
313
|
-
default: false,
|
|
314
|
-
},
|
|
315
|
-
]);
|
|
316
|
-
if (saveToKeychain) {
|
|
317
|
-
const saved = await storePassphrase(defaultKeypair.fingerprint, passphrase);
|
|
318
|
-
if (saved) {
|
|
319
|
-
showSuccess('Passphrase saved to system keychain');
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
showWarning('Could not save to keychain (may not be available on this system)');
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
throw new Error('Incorrect passphrase');
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
const privateKey = await openpgp.decryptKey({
|
|
334
|
-
privateKey: await openpgp.readPrivateKey({
|
|
335
|
-
armoredKey: defaultKeypair.private_key,
|
|
336
|
-
config: weakKeyConfig,
|
|
337
|
-
}),
|
|
338
|
-
passphrase,
|
|
339
|
-
config: weakKeyConfig,
|
|
340
|
-
});
|
|
263
|
+
// ---------- Decryption ----------
|
|
264
|
+
async function decryptWithSession(encryptedMessage) {
|
|
341
265
|
const message = await openpgp.readMessage({
|
|
342
266
|
armoredMessage: encryptedMessage,
|
|
343
267
|
});
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
268
|
+
const tryWith = async (keys) => {
|
|
269
|
+
if (keys.length === 0)
|
|
270
|
+
return null;
|
|
271
|
+
try {
|
|
272
|
+
const { data } = await openpgp.decrypt({
|
|
273
|
+
message,
|
|
274
|
+
decryptionKeys: keys,
|
|
275
|
+
config: weakKeyConfig,
|
|
276
|
+
});
|
|
277
|
+
return data;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const firstTry = await tryWith(getUnlockedPrivateKeys());
|
|
284
|
+
if (firstTry !== null) {
|
|
285
|
+
markKeyAsUsed(message);
|
|
286
|
+
return firstTry;
|
|
357
287
|
}
|
|
358
|
-
|
|
359
|
-
|
|
288
|
+
const keyIDs = message.getEncryptionKeyIDs();
|
|
289
|
+
if (keyIDs.length === 0) {
|
|
290
|
+
throw new Error('Message contains no recipient information');
|
|
360
291
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
292
|
+
const allKeypairs = db.select({ table: 'keypair' });
|
|
293
|
+
const matching = [];
|
|
294
|
+
const seen = new Set();
|
|
295
|
+
for (const keyID of keyIDs) {
|
|
296
|
+
const idHex = keyID.toHex().toUpperCase();
|
|
297
|
+
for (const kp of allKeypairs) {
|
|
298
|
+
if (seen.has(kp.id))
|
|
299
|
+
continue;
|
|
300
|
+
if (kp.fingerprint.toUpperCase().endsWith(idHex)) {
|
|
301
|
+
matching.push(kp);
|
|
302
|
+
seen.add(kp.id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
373
305
|
}
|
|
374
|
-
|
|
375
|
-
|
|
306
|
+
const locked = matching.filter((kp) => !unlockedKeys.has(kp.fingerprint));
|
|
307
|
+
if (locked.length === 0) {
|
|
308
|
+
throw new Error('This message was not encrypted for any of your keys');
|
|
376
309
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
310
|
+
for (const kp of locked) {
|
|
311
|
+
showInfo(`Message is encrypted for "${kp.name}" — unlocking…`);
|
|
312
|
+
const unlocked = await unlockKeypair(kp);
|
|
313
|
+
if (!unlocked)
|
|
314
|
+
continue;
|
|
315
|
+
const result = await tryWith([unlocked]);
|
|
316
|
+
if (result !== null) {
|
|
317
|
+
markKeyAsUsed(message);
|
|
318
|
+
return result;
|
|
381
319
|
}
|
|
382
|
-
|
|
383
|
-
|
|
320
|
+
}
|
|
321
|
+
throw new Error('Could not decrypt with any of your matching keys');
|
|
322
|
+
}
|
|
323
|
+
function markKeyAsUsed(message) {
|
|
324
|
+
const keyIDs = message.getEncryptionKeyIDs();
|
|
325
|
+
if (keyIDs.length === 0)
|
|
326
|
+
return;
|
|
327
|
+
const allKeypairs = db.select({ table: 'keypair' });
|
|
328
|
+
for (const keyID of keyIDs) {
|
|
329
|
+
const idHex = keyID.toHex().toUpperCase();
|
|
330
|
+
const match = allKeypairs.find((kp) => kp.fingerprint.toUpperCase().endsWith(idHex) &&
|
|
331
|
+
unlockedKeys.has(kp.fingerprint));
|
|
332
|
+
if (match) {
|
|
333
|
+
db.update('keypair', { key: 'id', value: match.id }, { last_used_at: new Date().toISOString() });
|
|
334
|
+
return;
|
|
384
335
|
}
|
|
385
336
|
}
|
|
386
|
-
return editors.filter((e) => e.available);
|
|
387
337
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
338
|
+
// ---------- Auto-save contact (silent) ----------
|
|
339
|
+
async function autoSaveContact(publicKeyArmored) {
|
|
340
|
+
try {
|
|
341
|
+
const keyInfo = await extractPublicKeyInfo(publicKeyArmored);
|
|
342
|
+
if (!keyInfo.name || keyInfo.name === 'Unknown')
|
|
343
|
+
return;
|
|
344
|
+
if (!keyInfo.email || keyInfo.email === 'unknown@example.com')
|
|
345
|
+
return;
|
|
346
|
+
const existing = db.select({
|
|
347
|
+
table: 'contact',
|
|
348
|
+
where: {
|
|
349
|
+
key: 'fingerprint',
|
|
350
|
+
compare: 'is',
|
|
351
|
+
value: keyInfo.fingerprint,
|
|
352
|
+
},
|
|
397
353
|
});
|
|
398
|
-
|
|
399
|
-
|
|
354
|
+
if (existing.length > 0)
|
|
355
|
+
return;
|
|
356
|
+
const ownKey = db.select({
|
|
357
|
+
table: 'keypair',
|
|
358
|
+
where: {
|
|
359
|
+
key: 'fingerprint',
|
|
360
|
+
compare: 'is',
|
|
361
|
+
value: keyInfo.fingerprint,
|
|
362
|
+
},
|
|
400
363
|
});
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
364
|
+
if (ownKey.length > 0)
|
|
365
|
+
return;
|
|
366
|
+
db.insert('contact', {
|
|
367
|
+
name: keyInfo.name,
|
|
368
|
+
email: keyInfo.email,
|
|
369
|
+
fingerprint: keyInfo.fingerprint,
|
|
370
|
+
public_key: publicKeyArmored,
|
|
371
|
+
algorithm: keyInfo.algorithm,
|
|
372
|
+
key_size: keyInfo.keySize,
|
|
373
|
+
trusted: false,
|
|
374
|
+
last_verified_at: null,
|
|
375
|
+
notes: null,
|
|
376
|
+
expires_at: keyInfo.expiresAt,
|
|
377
|
+
revoked: false,
|
|
378
|
+
});
|
|
379
|
+
showInfo(`Saved "${keyInfo.name}" to contacts.`);
|
|
412
380
|
}
|
|
413
381
|
catch {
|
|
414
|
-
|
|
415
|
-
return 0;
|
|
416
|
-
}
|
|
417
|
-
const keys = extractAllPublicKeys(clipboardContent);
|
|
418
|
-
if (keys.length === 0) {
|
|
419
|
-
showWarning('No public keys found in clipboard');
|
|
420
|
-
return 0;
|
|
382
|
+
// Silently skip on any failure
|
|
421
383
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
384
|
+
}
|
|
385
|
+
async function chooseInputMethod(promptText, skipClipboard = false) {
|
|
386
|
+
const availableEditors = detectAvailableEditors();
|
|
387
|
+
while (true) {
|
|
388
|
+
const inputChoices = [];
|
|
389
|
+
if (!skipClipboard) {
|
|
390
|
+
inputChoices.push({
|
|
391
|
+
name: `${icons.clipboard} Paste from clipboard`,
|
|
392
|
+
value: 'clipboard',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (availableEditors.length > 0) {
|
|
396
|
+
inputChoices.push({
|
|
397
|
+
name: `${icons.editor} Open in an editor`,
|
|
398
|
+
value: 'editor',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
inputChoices.push({
|
|
402
|
+
name: `${icons.inline} Type inline ${colors.muted('(Ctrl+D when done)')}`,
|
|
403
|
+
value: 'inline',
|
|
404
|
+
});
|
|
405
|
+
const choices = [
|
|
406
|
+
...inputChoices,
|
|
407
|
+
new inquirer.Separator(),
|
|
408
|
+
mainMenuChoice(),
|
|
409
|
+
];
|
|
410
|
+
const { inputMethod } = await escapeablePrompt([
|
|
411
|
+
{
|
|
412
|
+
type: 'list',
|
|
413
|
+
name: 'inputMethod',
|
|
414
|
+
message: promptMessage(promptText),
|
|
415
|
+
choices,
|
|
416
|
+
},
|
|
417
|
+
]);
|
|
418
|
+
if (inputMethod === 'main-menu')
|
|
419
|
+
return { cancelled: true };
|
|
420
|
+
if (inputMethod === 'clipboard') {
|
|
421
|
+
const content = await readClipboardSafe();
|
|
422
|
+
if (!content || content.trim() === '') {
|
|
423
|
+
showError('Clipboard is empty.');
|
|
433
424
|
continue;
|
|
434
425
|
}
|
|
435
|
-
|
|
436
|
-
name: recipientName,
|
|
437
|
-
publicKey,
|
|
438
|
-
});
|
|
439
|
-
showSuccess(`Added recipient: ${recipientName}`);
|
|
440
|
-
addedCount++;
|
|
426
|
+
return { value: content };
|
|
441
427
|
}
|
|
442
|
-
|
|
443
|
-
|
|
428
|
+
if (inputMethod === 'editor') {
|
|
429
|
+
const editorChoices = availableEditors.map((e) => ({
|
|
430
|
+
name: `${icons.editor} ${e.name} ${colors.muted(`(${getEditorInstructions(e.command)})`)}`,
|
|
431
|
+
value: e.command,
|
|
432
|
+
}));
|
|
433
|
+
editorChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice());
|
|
434
|
+
const { selectedEditor } = await escapeablePrompt([
|
|
435
|
+
{
|
|
436
|
+
type: 'list',
|
|
437
|
+
name: 'selectedEditor',
|
|
438
|
+
message: promptMessage('Choose your editor:'),
|
|
439
|
+
choices: editorChoices,
|
|
440
|
+
},
|
|
441
|
+
]);
|
|
442
|
+
if (selectedEditor === 'back')
|
|
443
|
+
continue;
|
|
444
|
+
if (selectedEditor === 'main-menu')
|
|
445
|
+
return { cancelled: true };
|
|
446
|
+
const originalEditor = process.env.EDITOR;
|
|
447
|
+
const originalVisual = process.env.VISUAL;
|
|
448
|
+
process.env.EDITOR = selectedEditor;
|
|
449
|
+
process.env.VISUAL = selectedEditor;
|
|
450
|
+
try {
|
|
451
|
+
const { editorInput } = await escapeablePrompt([
|
|
452
|
+
{
|
|
453
|
+
type: 'editor',
|
|
454
|
+
name: 'editorInput',
|
|
455
|
+
message: promptMessage('Press Enter to open editor:'),
|
|
456
|
+
postfix: '.txt',
|
|
457
|
+
waitForUseInput: false,
|
|
458
|
+
},
|
|
459
|
+
]);
|
|
460
|
+
return { value: editorInput ?? '' };
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
if (originalEditor !== undefined)
|
|
464
|
+
process.env.EDITOR = originalEditor;
|
|
465
|
+
else
|
|
466
|
+
delete process.env.EDITOR;
|
|
467
|
+
if (originalVisual !== undefined)
|
|
468
|
+
process.env.VISUAL = originalVisual;
|
|
469
|
+
else
|
|
470
|
+
delete process.env.VISUAL;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (inputMethod === 'inline') {
|
|
474
|
+
try {
|
|
475
|
+
const value = await readInlineMultiline('Type your message:');
|
|
476
|
+
return { value };
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
if (error instanceof EscapeError)
|
|
480
|
+
return { cancelled: true };
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
444
483
|
}
|
|
445
484
|
}
|
|
446
|
-
|
|
485
|
+
}
|
|
486
|
+
async function getRecipientFromPaste() {
|
|
487
|
+
let value;
|
|
488
|
+
try {
|
|
489
|
+
value = await readInlineMultiline("Paste the recipient's PGP PUBLIC KEY block:", '(Paste, then press Ctrl+D)');
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
if (error instanceof EscapeError)
|
|
493
|
+
return null;
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
if (!value.includes('BEGIN PGP PUBLIC KEY BLOCK')) {
|
|
497
|
+
showError('No PGP public key found in the input.');
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
const keys = extractAllPublicKeys(value);
|
|
501
|
+
if (keys.length === 0) {
|
|
502
|
+
showError('No valid PGP public key block found.');
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const armored = keys[0];
|
|
507
|
+
await openpgp.readKey({ armoredKey: armored, config: weakKeyConfig });
|
|
508
|
+
const info = await extractPublicKeyInfo(armored);
|
|
509
|
+
const name = formatMaskedRecipient(info);
|
|
510
|
+
return { name, publicKey: armored, isNew: true };
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
showError(`Failed to read public key: ${error instanceof Error ? error.message : error}`);
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
447
516
|
}
|
|
448
517
|
async function selectMultipleRecipients() {
|
|
449
518
|
const recipients = [];
|
|
450
519
|
const contacts = db.select({ table: 'contact' });
|
|
451
|
-
const defaultKeypair =
|
|
452
|
-
|
|
453
|
-
function buildChoices() {
|
|
520
|
+
const defaultKeypair = keyManager.getDefaultKeypair();
|
|
521
|
+
while (true) {
|
|
454
522
|
const choices = [];
|
|
455
|
-
// Show current recipients count
|
|
456
523
|
if (recipients.length > 0) {
|
|
457
524
|
choices.push({
|
|
458
525
|
name: colors.primary(`── Current recipients: ${recipients.length} ──`),
|
|
459
|
-
value: 'show
|
|
526
|
+
value: 'show',
|
|
460
527
|
});
|
|
461
528
|
}
|
|
462
|
-
|
|
463
|
-
const selfAdded = recipients.some((r) => r.name === 'Myself');
|
|
464
|
-
if (defaultKeypair && !selfAdded) {
|
|
529
|
+
if (defaultKeypair && !recipients.some((r) => r.name === 'Myself')) {
|
|
465
530
|
choices.push({
|
|
466
|
-
name: `${icons.key} Add myself ${colors.muted('(so
|
|
531
|
+
name: `${icons.key} Add myself ${colors.muted('(so you can decrypt too)')}`,
|
|
467
532
|
value: 'self',
|
|
468
533
|
});
|
|
469
534
|
}
|
|
470
|
-
// Option to select from contacts
|
|
471
535
|
if (contacts.length > 0) {
|
|
472
536
|
choices.push({
|
|
473
|
-
name: `${icons.contact}
|
|
537
|
+
name: `${icons.contact} Pick from contacts ${colors.muted(`(${contacts.length})`)}`,
|
|
474
538
|
value: 'contacts',
|
|
475
539
|
});
|
|
476
540
|
}
|
|
477
|
-
|
|
478
|
-
choices.push({
|
|
479
|
-
name: `${icons.clipboard} Paste from clipboard ${colors.muted('(supports multiple keys)')}`,
|
|
480
|
-
value: 'clipboard',
|
|
481
|
-
});
|
|
482
|
-
choices.push({
|
|
483
|
-
name: `${icons.inline} Type/paste a single key`,
|
|
484
|
-
value: 'manual',
|
|
485
|
-
});
|
|
486
|
-
// Done or cancel
|
|
487
|
-
choices.push({
|
|
541
|
+
choices.push({ name: `${icons.clipboard} Add from clipboard`, value: 'clipboard' }, { name: `${icons.inline} Paste a public key`, value: 'paste' }, new inquirer.Separator(), {
|
|
488
542
|
name: recipients.length > 0
|
|
489
|
-
? `${icons.success} Done
|
|
543
|
+
? `${icons.success} Done`
|
|
490
544
|
: `${icons.back} Cancel`,
|
|
491
545
|
value: 'done',
|
|
492
546
|
});
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
let addMore = true;
|
|
496
|
-
while (addMore) {
|
|
497
|
-
const { addMethod } = await escapeablePrompt([
|
|
547
|
+
const { method } = await escapeablePrompt([
|
|
498
548
|
{
|
|
499
549
|
type: 'list',
|
|
500
|
-
name: '
|
|
550
|
+
name: 'method',
|
|
501
551
|
message: promptMessage('Add recipients:'),
|
|
502
|
-
choices
|
|
552
|
+
choices,
|
|
503
553
|
},
|
|
504
554
|
]);
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
else if (addMethod === 'show-recipients') {
|
|
509
|
-
// Show current recipients
|
|
555
|
+
if (method === 'done')
|
|
556
|
+
break;
|
|
557
|
+
if (method === 'show') {
|
|
510
558
|
console.log(colors.primary('\nCurrent recipients:'));
|
|
511
|
-
for (const r of recipients)
|
|
559
|
+
for (const r of recipients)
|
|
512
560
|
console.log(colors.muted(` • ${r.name}`));
|
|
513
|
-
}
|
|
514
561
|
console.log();
|
|
562
|
+
continue;
|
|
515
563
|
}
|
|
516
|
-
|
|
564
|
+
if (method === 'self') {
|
|
517
565
|
if (defaultKeypair) {
|
|
518
566
|
recipients.push({
|
|
519
567
|
name: 'Myself',
|
|
520
568
|
publicKey: defaultKeypair.public_key,
|
|
569
|
+
isNew: false,
|
|
521
570
|
});
|
|
522
|
-
showSuccess('Added yourself
|
|
571
|
+
showSuccess('Added yourself');
|
|
523
572
|
}
|
|
573
|
+
continue;
|
|
524
574
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const { selectedContacts } = await escapeablePrompt([
|
|
575
|
+
if (method === 'contacts') {
|
|
576
|
+
const { selected } = await escapeablePrompt([
|
|
528
577
|
{
|
|
529
578
|
type: 'checkbox',
|
|
530
|
-
name: '
|
|
531
|
-
message: promptMessage('Select contacts
|
|
579
|
+
name: 'selected',
|
|
580
|
+
message: promptMessage('Select contacts:'),
|
|
532
581
|
choices: contacts.map((c) => {
|
|
533
|
-
const
|
|
582
|
+
const already = recipients.some((r) => r.publicKey === c.public_key);
|
|
534
583
|
return {
|
|
535
|
-
name: `${c.name} <${c.email
|
|
584
|
+
name: `${c.name} <${c.email}>${already ? colors.muted(' (added)') : ''}`,
|
|
536
585
|
value: c.id,
|
|
537
|
-
|
|
538
|
-
disabled: alreadyAdded,
|
|
586
|
+
disabled: already,
|
|
539
587
|
};
|
|
540
588
|
}),
|
|
541
589
|
},
|
|
542
590
|
]);
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (contact) {
|
|
591
|
+
for (const id of selected) {
|
|
592
|
+
const c = contacts.find((x) => x.id === id);
|
|
593
|
+
if (c) {
|
|
547
594
|
recipients.push({
|
|
548
|
-
name:
|
|
549
|
-
|
|
595
|
+
name: formatMaskedRecipient({
|
|
596
|
+
name: c.name,
|
|
597
|
+
email: c.email,
|
|
598
|
+
fingerprint: c.fingerprint,
|
|
599
|
+
}),
|
|
600
|
+
publicKey: c.public_key,
|
|
601
|
+
isNew: false,
|
|
550
602
|
});
|
|
551
|
-
addedCount++;
|
|
552
603
|
}
|
|
553
604
|
}
|
|
554
|
-
|
|
555
|
-
showSuccess(`Added ${addedCount} contact${addedCount > 1 ? 's' : ''}`);
|
|
556
|
-
}
|
|
605
|
+
continue;
|
|
557
606
|
}
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
607
|
+
if (method === 'clipboard') {
|
|
608
|
+
const content = await readClipboardSafe();
|
|
609
|
+
const keys = content ? extractAllPublicKeys(content) : [];
|
|
610
|
+
if (keys.length === 0) {
|
|
611
|
+
showWarning('No public keys found in clipboard.');
|
|
612
|
+
continue;
|
|
564
613
|
}
|
|
565
|
-
|
|
566
|
-
else if (addMethod === 'manual') {
|
|
567
|
-
const publicKey = await getRecipientPublicKey();
|
|
568
|
-
if (publicKey) {
|
|
614
|
+
for (const armored of keys) {
|
|
569
615
|
try {
|
|
570
|
-
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
recipients.push({
|
|
579
|
-
name: recipientName,
|
|
580
|
-
publicKey,
|
|
581
|
-
});
|
|
582
|
-
showSuccess(`Added recipient: ${recipientName}`);
|
|
616
|
+
await openpgp.readKey({ armoredKey: armored, config: weakKeyConfig });
|
|
617
|
+
const info = await extractPublicKeyInfo(armored);
|
|
618
|
+
const name = formatMaskedRecipient(info);
|
|
619
|
+
if (recipients.some((r) => r.publicKey === armored)) {
|
|
620
|
+
showWarning(`Skipping duplicate: ${name}`);
|
|
621
|
+
continue;
|
|
583
622
|
}
|
|
623
|
+
recipients.push({ name, publicKey: armored, isNew: true });
|
|
624
|
+
showSuccess(`Added ${name}`);
|
|
584
625
|
}
|
|
585
626
|
catch (error) {
|
|
586
|
-
showError(
|
|
627
|
+
showError(`Skipped invalid key: ${error}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (method === 'paste') {
|
|
633
|
+
const r = await getRecipientFromPaste();
|
|
634
|
+
if (r) {
|
|
635
|
+
if (recipients.some((x) => x.publicKey === r.publicKey)) {
|
|
636
|
+
showWarning('Already added.');
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
recipients.push(r);
|
|
640
|
+
showSuccess(`Added ${r.name}`);
|
|
587
641
|
}
|
|
588
642
|
}
|
|
589
643
|
}
|
|
590
644
|
}
|
|
591
645
|
return recipients;
|
|
592
646
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
647
|
+
// ---------- Actions ----------
|
|
648
|
+
async function actionCopyPublicKey() {
|
|
649
|
+
const keypairs = db.select({ table: 'keypair' });
|
|
650
|
+
if (keypairs.length === 0) {
|
|
651
|
+
showError('No keypairs found. Create one from the key management menu.');
|
|
652
|
+
await pause();
|
|
653
|
+
return;
|
|
600
654
|
}
|
|
601
|
-
|
|
602
|
-
|
|
655
|
+
let selected;
|
|
656
|
+
if (keypairs.length === 1) {
|
|
657
|
+
selected = keypairs[0];
|
|
603
658
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const { useClipboard } = await escapeablePrompt([
|
|
659
|
+
else {
|
|
660
|
+
const defaultId = keypairs.find((kp) => kp.is_default)?.id;
|
|
661
|
+
const { keypairId } = await escapeablePrompt([
|
|
608
662
|
{
|
|
609
|
-
type: '
|
|
610
|
-
name: '
|
|
611
|
-
message: '
|
|
612
|
-
default:
|
|
663
|
+
type: 'list',
|
|
664
|
+
name: 'keypairId',
|
|
665
|
+
message: promptMessage('Which public key would you like to copy?'),
|
|
666
|
+
default: defaultId,
|
|
667
|
+
choices: [
|
|
668
|
+
...keypairs.map((kp) => ({
|
|
669
|
+
name: `${icons.key} ${kp.name}${kp.is_default ? ` ${colors.muted('(default)')}` : ''}`,
|
|
670
|
+
value: kp.id,
|
|
671
|
+
})),
|
|
672
|
+
new inquirer.Separator(),
|
|
673
|
+
mainMenuChoice(),
|
|
674
|
+
],
|
|
613
675
|
},
|
|
614
676
|
]);
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
677
|
+
if (keypairId === 'main-menu')
|
|
678
|
+
return;
|
|
679
|
+
const found = keypairs.find((kp) => kp.id === keypairId);
|
|
680
|
+
if (!found)
|
|
681
|
+
return;
|
|
682
|
+
selected = found;
|
|
683
|
+
}
|
|
684
|
+
const ok = await writeClipboardSafe(selected.public_key);
|
|
685
|
+
console.log();
|
|
686
|
+
if (ok) {
|
|
687
|
+
showSuccess(`Public key for "${selected.name}" copied to clipboard.`);
|
|
688
|
+
console.log(colors.muted(` Fingerprint: ${selected.fingerprint.slice(-16)}`));
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
showError('Could not write to clipboard.');
|
|
692
|
+
}
|
|
693
|
+
console.log();
|
|
694
|
+
await pause();
|
|
695
|
+
}
|
|
696
|
+
async function actionEncrypt() {
|
|
697
|
+
let recipients = [];
|
|
698
|
+
// 1. Auto-detect a public key in clipboard
|
|
699
|
+
const clipboard = await readClipboardSafe();
|
|
700
|
+
const clipboardKeys = clipboard ? extractAllPublicKeys(clipboard) : [];
|
|
701
|
+
if (clipboardKeys.length === 1) {
|
|
702
|
+
const armored = clipboardKeys[0];
|
|
703
|
+
try {
|
|
704
|
+
await openpgp.readKey({ armoredKey: armored, config: weakKeyConfig });
|
|
705
|
+
const info = await extractPublicKeyInfo(armored);
|
|
706
|
+
const masked = formatMaskedRecipient(info);
|
|
707
|
+
const { useClipboard } = await escapeablePrompt([
|
|
708
|
+
{
|
|
709
|
+
type: 'confirm',
|
|
710
|
+
name: 'useClipboard',
|
|
711
|
+
message: promptMessage(`Encrypt for ${colors.successBold(masked)}?`),
|
|
712
|
+
default: true,
|
|
713
|
+
},
|
|
714
|
+
]);
|
|
715
|
+
if (useClipboard) {
|
|
716
|
+
recipients = [{ name: masked, publicKey: armored, isNew: true }];
|
|
619
717
|
}
|
|
620
718
|
}
|
|
719
|
+
catch {
|
|
720
|
+
// Bad key, fall through to picker
|
|
721
|
+
}
|
|
621
722
|
}
|
|
622
|
-
// If no
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
lines.push(line);
|
|
632
|
-
const content = lines.join('\n');
|
|
633
|
-
// Check if we have a complete key block and current line is empty
|
|
634
|
-
if (line.trim() === '' &&
|
|
635
|
-
content.includes('-----BEGIN PGP PUBLIC KEY BLOCK') &&
|
|
636
|
-
content.includes('-----END PGP PUBLIC KEY BLOCK')) {
|
|
637
|
-
rl.close();
|
|
638
|
-
resolve(content.trim());
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
rl.on('close', () => {
|
|
642
|
-
resolve(lines.join('\n'));
|
|
723
|
+
// 2. If no clipboard recipient, show picker
|
|
724
|
+
if (recipients.length === 0) {
|
|
725
|
+
const contacts = db.select({ table: 'contact' });
|
|
726
|
+
const defaultKeypair = keyManager.getDefaultKeypair();
|
|
727
|
+
const choices = [];
|
|
728
|
+
if (contacts.length > 0) {
|
|
729
|
+
choices.push({
|
|
730
|
+
name: `${icons.contact} A saved contact ${colors.muted(`(${contacts.length})`)}`,
|
|
731
|
+
value: 'contact',
|
|
643
732
|
});
|
|
733
|
+
}
|
|
734
|
+
choices.push({
|
|
735
|
+
name: `${icons.inline} Paste a public key`,
|
|
736
|
+
value: 'paste',
|
|
737
|
+
});
|
|
738
|
+
choices.push({
|
|
739
|
+
name: `${icons.multiple} Multiple recipients`,
|
|
740
|
+
value: 'multi',
|
|
644
741
|
});
|
|
742
|
+
if (defaultKeypair) {
|
|
743
|
+
choices.push({
|
|
744
|
+
name: `${icons.key} Myself`,
|
|
745
|
+
value: 'self',
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
choices.push(new inquirer.Separator(), mainMenuChoice());
|
|
749
|
+
const { recipient } = await escapeablePrompt([
|
|
750
|
+
{
|
|
751
|
+
type: 'list',
|
|
752
|
+
name: 'recipient',
|
|
753
|
+
message: promptMessage('Who do you want to encrypt for?'),
|
|
754
|
+
choices,
|
|
755
|
+
},
|
|
756
|
+
]);
|
|
757
|
+
if (recipient === 'main-menu')
|
|
758
|
+
return;
|
|
759
|
+
if (recipient === 'contact') {
|
|
760
|
+
const { contactId } = await escapeablePrompt([
|
|
761
|
+
{
|
|
762
|
+
type: 'list',
|
|
763
|
+
name: 'contactId',
|
|
764
|
+
message: promptMessage('Select contact:'),
|
|
765
|
+
choices: [
|
|
766
|
+
...contacts.map((c) => ({
|
|
767
|
+
name: `${icons.contact} ${c.name} ${colors.muted(`<${c.email}>`)}`,
|
|
768
|
+
value: c.id,
|
|
769
|
+
})),
|
|
770
|
+
new inquirer.Separator(),
|
|
771
|
+
backChoice(),
|
|
772
|
+
],
|
|
773
|
+
},
|
|
774
|
+
]);
|
|
775
|
+
if (contactId === 'back')
|
|
776
|
+
return actionEncrypt();
|
|
777
|
+
const c = contacts.find((x) => x.id === contactId);
|
|
778
|
+
if (!c)
|
|
779
|
+
return;
|
|
780
|
+
recipients = [
|
|
781
|
+
{
|
|
782
|
+
name: formatMaskedRecipient({
|
|
783
|
+
name: c.name,
|
|
784
|
+
email: c.email,
|
|
785
|
+
fingerprint: c.fingerprint,
|
|
786
|
+
}),
|
|
787
|
+
publicKey: c.public_key,
|
|
788
|
+
isNew: false,
|
|
789
|
+
},
|
|
790
|
+
];
|
|
791
|
+
}
|
|
792
|
+
else if (recipient === 'paste') {
|
|
793
|
+
const r = await getRecipientFromPaste();
|
|
794
|
+
if (!r)
|
|
795
|
+
return;
|
|
796
|
+
recipients = [r];
|
|
797
|
+
}
|
|
798
|
+
else if (recipient === 'multi') {
|
|
799
|
+
recipients = await selectMultipleRecipients();
|
|
800
|
+
if (recipients.length === 0)
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
else if (recipient === 'self') {
|
|
804
|
+
if (defaultKeypair) {
|
|
805
|
+
recipients = [
|
|
806
|
+
{
|
|
807
|
+
name: 'Myself',
|
|
808
|
+
publicKey: defaultKeypair.public_key,
|
|
809
|
+
isNew: false,
|
|
810
|
+
},
|
|
811
|
+
];
|
|
812
|
+
}
|
|
813
|
+
}
|
|
645
814
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
815
|
+
if (recipients.length === 0)
|
|
816
|
+
return;
|
|
817
|
+
if (recipients.length > 1) {
|
|
818
|
+
console.log(colors.primary('\nEncrypting for:'));
|
|
819
|
+
for (const r of recipients)
|
|
820
|
+
console.log(colors.muted(` • ${r.name}`));
|
|
650
821
|
console.log();
|
|
651
|
-
return null;
|
|
652
822
|
}
|
|
653
|
-
//
|
|
823
|
+
// 3. Get message
|
|
824
|
+
const result = await chooseInputMethod('How would you like to enter your message?');
|
|
825
|
+
if ('cancelled' in result)
|
|
826
|
+
return;
|
|
827
|
+
const message = result.value;
|
|
828
|
+
if (!message || message.trim() === '') {
|
|
829
|
+
showError('No message provided.');
|
|
830
|
+
await pause();
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
// 4. Encrypt
|
|
834
|
+
showLoading('Encrypting…');
|
|
835
|
+
const encrypted = await encryptForKeys(message, recipients.map((r) => r.publicKey));
|
|
836
|
+
// 5. Display + copy
|
|
837
|
+
console.clear();
|
|
838
|
+
printBanner();
|
|
839
|
+
console.log(colors.successBold('Encrypted Message:\n'));
|
|
840
|
+
printDivider();
|
|
841
|
+
console.log(encrypted);
|
|
842
|
+
printDivider();
|
|
843
|
+
console.log();
|
|
844
|
+
const copied = await writeClipboardSafe(encrypted);
|
|
845
|
+
if (copied) {
|
|
846
|
+
showSuccess('Encrypted message copied to clipboard.');
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
showWarning('Clipboard unavailable.');
|
|
850
|
+
}
|
|
851
|
+
// 6. Auto-save new contacts (silent if name available)
|
|
852
|
+
for (const r of recipients) {
|
|
853
|
+
if (r.isNew) {
|
|
854
|
+
await autoSaveContact(r.publicKey);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
console.log();
|
|
858
|
+
await pause();
|
|
859
|
+
}
|
|
860
|
+
async function actionDecrypt() {
|
|
861
|
+
let encrypted = null;
|
|
862
|
+
// 1. Auto-detect clipboard
|
|
863
|
+
const clipboard = await readClipboardSafe();
|
|
864
|
+
if (clipboard && clipboard.includes('BEGIN PGP MESSAGE')) {
|
|
865
|
+
const { useClipboard } = await escapeablePrompt([
|
|
866
|
+
{
|
|
867
|
+
type: 'confirm',
|
|
868
|
+
name: 'useClipboard',
|
|
869
|
+
message: promptMessage('Encrypted message detected in clipboard. Use it?'),
|
|
870
|
+
default: true,
|
|
871
|
+
},
|
|
872
|
+
]);
|
|
873
|
+
if (useClipboard)
|
|
874
|
+
encrypted = clipboard;
|
|
875
|
+
}
|
|
876
|
+
// 2. Fall back to input method picker
|
|
877
|
+
if (!encrypted) {
|
|
878
|
+
const result = await chooseInputMethod('How would you like to enter the encrypted message?');
|
|
879
|
+
if ('cancelled' in result)
|
|
880
|
+
return;
|
|
881
|
+
encrypted = result.value;
|
|
882
|
+
}
|
|
883
|
+
if (!encrypted || !encrypted.includes('BEGIN PGP MESSAGE')) {
|
|
884
|
+
showError('No PGP message found.');
|
|
885
|
+
await pause();
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
// 3. Decrypt
|
|
889
|
+
showLoading('Decrypting…');
|
|
654
890
|
try {
|
|
655
|
-
|
|
891
|
+
const plaintext = await decryptWithSession(encrypted);
|
|
892
|
+
console.clear();
|
|
893
|
+
printBanner();
|
|
894
|
+
console.log(colors.successBold('Decrypted Message:\n'));
|
|
895
|
+
printDivider();
|
|
896
|
+
console.log(plaintext);
|
|
897
|
+
printDivider();
|
|
656
898
|
console.log();
|
|
657
|
-
|
|
899
|
+
const copied = await writeClipboardSafe(plaintext);
|
|
900
|
+
if (copied)
|
|
901
|
+
showSuccess('Decrypted message copied to clipboard.');
|
|
658
902
|
console.log();
|
|
659
|
-
|
|
903
|
+
await pause();
|
|
660
904
|
}
|
|
661
905
|
catch (error) {
|
|
906
|
+
if (error instanceof EscapeError)
|
|
907
|
+
throw error;
|
|
908
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
662
909
|
console.log();
|
|
663
|
-
showError(`
|
|
664
|
-
|
|
910
|
+
showError(`Decryption failed: ${msg}`);
|
|
911
|
+
console.log();
|
|
912
|
+
await pause();
|
|
665
913
|
}
|
|
666
914
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
'open -e': 'Save: Cmd+S, then close the window',
|
|
676
|
-
notepad: 'Save: Ctrl+S, then close the window',
|
|
677
|
-
};
|
|
678
|
-
return instructions[editorCommand] || 'Save and close the editor when done';
|
|
679
|
-
}
|
|
680
|
-
function clearPassphraseCache() {
|
|
681
|
-
// Clear all cached passphrases from memory
|
|
682
|
-
passphraseCache.clear();
|
|
915
|
+
async function pause() {
|
|
916
|
+
await escapeablePrompt([
|
|
917
|
+
{
|
|
918
|
+
type: 'input',
|
|
919
|
+
name: 'continue',
|
|
920
|
+
message: colors.muted('Press Enter to continue…'),
|
|
921
|
+
},
|
|
922
|
+
]);
|
|
683
923
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (!db) {
|
|
687
|
-
db = await Db.init();
|
|
688
|
-
keyManager = new KeyManager(db);
|
|
689
|
-
}
|
|
924
|
+
// ---------- Main menu ----------
|
|
925
|
+
async function showMainMenu() {
|
|
690
926
|
printBanner();
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (!hasKeypair) {
|
|
694
|
-
console.log();
|
|
695
|
-
showWarning("No keypair found. Let's set up your first keypair.");
|
|
696
|
-
console.log();
|
|
697
|
-
await keyManager.setupFirstKeypair();
|
|
698
|
-
console.log();
|
|
699
|
-
showSuccess('Setup complete! You can now use the tool.');
|
|
700
|
-
console.log();
|
|
701
|
-
}
|
|
702
|
-
// Build menu choices
|
|
927
|
+
const defaultKp = keyManager.getDefaultKeypair();
|
|
928
|
+
printHomeStatus(defaultKp ? `${defaultKp.name} key` : null);
|
|
703
929
|
const menuChoices = [
|
|
704
|
-
{ name: `${icons.
|
|
930
|
+
{ name: `${icons.clipboard} Copy my public key`, value: 'copy' },
|
|
705
931
|
{ name: `${icons.decrypt} Decrypt a message`, value: 'decrypt' },
|
|
706
|
-
{ name: `${icons.
|
|
932
|
+
{ name: `${icons.encrypt} Encrypt a message`, value: 'encrypt' },
|
|
933
|
+
new inquirer.Separator(colors.muted(' ─────────')),
|
|
934
|
+
{ name: `${icons.key} Manage keys & contacts`, value: 'keys' },
|
|
935
|
+
exitChoice(),
|
|
707
936
|
];
|
|
708
|
-
// Check if installed globally and if update is available
|
|
709
|
-
const installedVersion = getInstalledVersion();
|
|
710
|
-
const latestVersion = getLatestVersion();
|
|
711
|
-
if (!installedVersion) {
|
|
712
|
-
// Not installed globally - offer to install
|
|
713
|
-
menuChoices.push(new inquirer.Separator());
|
|
714
|
-
menuChoices.push({
|
|
715
|
-
name: `${icons.add} Install lpgp globally ${colors.muted('(for offline use)')}`,
|
|
716
|
-
value: 'install',
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
else if (latestVersion &&
|
|
720
|
-
isOlderVersion(installedVersion, latestVersion)) {
|
|
721
|
-
// Installed but outdated - offer to update
|
|
722
|
-
menuChoices.push(new inquirer.Separator());
|
|
723
|
-
menuChoices.push({
|
|
724
|
-
name: `${icons.add} Update lpgp ${colors.muted(`(${installedVersion} → ${latestVersion})`)}`,
|
|
725
|
-
value: 'update',
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
menuChoices.push(new inquirer.Separator());
|
|
729
|
-
menuChoices.push(exitChoice());
|
|
730
937
|
const { action } = await escapeablePrompt([
|
|
731
938
|
{
|
|
732
939
|
type: 'list',
|
|
733
940
|
name: 'action',
|
|
734
|
-
message: promptMessage('
|
|
941
|
+
message: promptMessage('Choose an action'),
|
|
735
942
|
choices: menuChoices,
|
|
736
943
|
},
|
|
737
944
|
]);
|
|
738
945
|
if (action === 'exit') {
|
|
739
|
-
|
|
946
|
+
clearSession();
|
|
740
947
|
console.clear();
|
|
741
948
|
process.exit(0);
|
|
742
949
|
}
|
|
743
|
-
if (action === 'install' || action === 'update') {
|
|
744
|
-
await installOrUpdateGlobally(action === 'update');
|
|
745
|
-
await escapeablePrompt([
|
|
746
|
-
{
|
|
747
|
-
type: 'input',
|
|
748
|
-
name: 'continue',
|
|
749
|
-
message: promptMessage('Press Enter to continue...'),
|
|
750
|
-
},
|
|
751
|
-
]);
|
|
752
|
-
return main();
|
|
753
|
-
}
|
|
754
950
|
if (action === 'keys') {
|
|
755
951
|
await keyManager.showKeyManagementMenu();
|
|
756
|
-
return
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (action === 'copy') {
|
|
955
|
+
await actionCopyPublicKey();
|
|
956
|
+
return;
|
|
757
957
|
}
|
|
758
958
|
if (action === 'encrypt') {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const { recipient } = await escapeablePrompt([
|
|
762
|
-
{
|
|
763
|
-
type: 'list',
|
|
764
|
-
name: 'recipient',
|
|
765
|
-
message: promptMessage('Who do you want to encrypt this message for?'),
|
|
766
|
-
choices: [
|
|
767
|
-
{
|
|
768
|
-
name: `${icons.contact} Someone else ${colors.muted('(use their public key)')}`,
|
|
769
|
-
value: 'other',
|
|
770
|
-
},
|
|
771
|
-
{
|
|
772
|
-
name: `${icons.multiple} Multiple recipients`,
|
|
773
|
-
value: 'multiple',
|
|
774
|
-
},
|
|
775
|
-
{
|
|
776
|
-
name: `${icons.key} Myself ${colors.muted('(use my public key)')}`,
|
|
777
|
-
value: 'self',
|
|
778
|
-
},
|
|
779
|
-
new inquirer.Separator(),
|
|
780
|
-
mainMenuChoice(),
|
|
781
|
-
],
|
|
782
|
-
},
|
|
783
|
-
]);
|
|
784
|
-
if (recipient === 'back' || recipient === 'main-menu') {
|
|
785
|
-
return main();
|
|
786
|
-
}
|
|
787
|
-
let recipientPublicKeys = [];
|
|
788
|
-
let recipientNames = [];
|
|
789
|
-
let isNewContact = false;
|
|
790
|
-
// Handle multiple recipients
|
|
791
|
-
if (recipient === 'multiple') {
|
|
792
|
-
const recipients = await selectMultipleRecipients();
|
|
793
|
-
if (recipients.length === 0) {
|
|
794
|
-
console.log();
|
|
795
|
-
showError('No recipients selected. Aborting.');
|
|
796
|
-
console.log();
|
|
797
|
-
return main();
|
|
798
|
-
}
|
|
799
|
-
recipientPublicKeys = recipients.map((r) => r.publicKey);
|
|
800
|
-
recipientNames = recipients.map((r) => r.name);
|
|
801
|
-
// Show summary
|
|
802
|
-
console.log(colors.primary('\nEncrypting for the following recipients:'));
|
|
803
|
-
for (const name of recipientNames) {
|
|
804
|
-
console.log(colors.muted(` • ${name}`));
|
|
805
|
-
}
|
|
806
|
-
console.log();
|
|
807
|
-
}
|
|
808
|
-
else if (recipient === 'other') {
|
|
809
|
-
// Check if there are any saved contacts
|
|
810
|
-
const contacts = db.select({ table: 'contact' });
|
|
811
|
-
// Loop for recipient selection (allows going back from contacts submenu)
|
|
812
|
-
recipientLoop: while (true) {
|
|
813
|
-
// Build main menu choices
|
|
814
|
-
const recipientChoices = [];
|
|
815
|
-
if (contacts.length > 0) {
|
|
816
|
-
recipientChoices.push({
|
|
817
|
-
name: `${icons.contact} Saved contacts ${colors.muted(`(${contacts.length} available)`)}`,
|
|
818
|
-
value: 'saved-contacts',
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
recipientChoices.push({ name: `${icons.add} Use a new public key`, value: 'new' }, new inquirer.Separator(), mainMenuChoice());
|
|
822
|
-
const { recipientSource } = await escapeablePrompt([
|
|
823
|
-
{
|
|
824
|
-
type: 'list',
|
|
825
|
-
name: 'recipientSource',
|
|
826
|
-
message: promptMessage('How would you like to specify the recipient?'),
|
|
827
|
-
choices: recipientChoices,
|
|
828
|
-
},
|
|
829
|
-
]);
|
|
830
|
-
if (recipientSource === 'main' || recipientSource === 'main-menu') {
|
|
831
|
-
return main();
|
|
832
|
-
}
|
|
833
|
-
if (recipientSource === 'saved-contacts') {
|
|
834
|
-
// Show contacts submenu
|
|
835
|
-
const contactChoices = contacts.map((c) => ({
|
|
836
|
-
name: `${icons.contact} ${c.name} ${colors.muted(`<${c.email}>`)}`,
|
|
837
|
-
value: c.id,
|
|
838
|
-
}));
|
|
839
|
-
contactChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice(), new inquirer.Separator());
|
|
840
|
-
const { contactChoice } = await escapeablePrompt([
|
|
841
|
-
{
|
|
842
|
-
type: 'list',
|
|
843
|
-
name: 'contactChoice',
|
|
844
|
-
message: promptMessage('Select a contact:'),
|
|
845
|
-
choices: contactChoices,
|
|
846
|
-
},
|
|
847
|
-
]);
|
|
848
|
-
if (contactChoice === 'main' || contactChoice === 'main-menu') {
|
|
849
|
-
return main();
|
|
850
|
-
}
|
|
851
|
-
if (contactChoice === 'back') {
|
|
852
|
-
// Go back to recipient source selection
|
|
853
|
-
continue recipientLoop;
|
|
854
|
-
}
|
|
855
|
-
// Use saved contact
|
|
856
|
-
const selectedContact = contacts.find((c) => c.id === contactChoice);
|
|
857
|
-
if (selectedContact) {
|
|
858
|
-
recipientPublicKeys = [selectedContact.public_key];
|
|
859
|
-
break recipientLoop;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
else if (recipientSource === 'new') {
|
|
863
|
-
const publicKey = await getRecipientPublicKey();
|
|
864
|
-
if (!publicKey) {
|
|
865
|
-
console.log();
|
|
866
|
-
showError('Could not get recipient public key. Aborting.');
|
|
867
|
-
console.log();
|
|
868
|
-
return main();
|
|
869
|
-
}
|
|
870
|
-
recipientPublicKeys = [publicKey];
|
|
871
|
-
isNewContact = true;
|
|
872
|
-
break recipientLoop;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
// Detect available editors
|
|
877
|
-
const availableEditors = detectAvailableEditors();
|
|
878
|
-
let message;
|
|
879
|
-
// Loop for input method selection (allows going back from editor selection)
|
|
880
|
-
inputMethodLoop: while (true) {
|
|
881
|
-
// Ask for input method
|
|
882
|
-
const inputChoices = [];
|
|
883
|
-
// Always add clipboard option first
|
|
884
|
-
inputChoices.push({
|
|
885
|
-
name: `${icons.clipboard} Paste from clipboard`,
|
|
886
|
-
value: 'clipboard',
|
|
887
|
-
});
|
|
888
|
-
if (availableEditors.length > 0) {
|
|
889
|
-
inputChoices.push({ name: `${icons.editor} Use an editor`, value: 'editor' }, {
|
|
890
|
-
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
891
|
-
value: 'inline',
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
else {
|
|
895
|
-
inputChoices.push({
|
|
896
|
-
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
897
|
-
value: 'inline',
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
// Add main menu option
|
|
901
|
-
inputChoices.push(new inquirer.Separator(), mainMenuChoice());
|
|
902
|
-
const { inputMethod } = await escapeablePrompt([
|
|
903
|
-
{
|
|
904
|
-
type: 'list',
|
|
905
|
-
name: 'inputMethod',
|
|
906
|
-
message: promptMessage('How would you like to enter your message?'),
|
|
907
|
-
choices: inputChoices,
|
|
908
|
-
},
|
|
909
|
-
]);
|
|
910
|
-
if (inputMethod === 'back' || inputMethod === 'main-menu') {
|
|
911
|
-
return main();
|
|
912
|
-
}
|
|
913
|
-
if (inputMethod === 'clipboard') {
|
|
914
|
-
try {
|
|
915
|
-
message = await clipboardy.read();
|
|
916
|
-
if (!message || message.trim() === '') {
|
|
917
|
-
console.log();
|
|
918
|
-
showError('Clipboard is empty.');
|
|
919
|
-
console.log();
|
|
920
|
-
return main();
|
|
921
|
-
}
|
|
922
|
-
console.log();
|
|
923
|
-
showSuccess('Message loaded from clipboard');
|
|
924
|
-
console.log();
|
|
925
|
-
break inputMethodLoop;
|
|
926
|
-
}
|
|
927
|
-
catch (clipError) {
|
|
928
|
-
console.log();
|
|
929
|
-
showError(`Failed to read from clipboard: ${clipError}`);
|
|
930
|
-
return main();
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
else if (inputMethod === 'editor') {
|
|
934
|
-
// Let user choose editor
|
|
935
|
-
const editorChoices = availableEditors.map((e) => ({
|
|
936
|
-
name: `${icons.editor} ${e.name} ${colors.muted(`(${getEditorInstructions(e.command)})`)}`,
|
|
937
|
-
value: e.command,
|
|
938
|
-
}));
|
|
939
|
-
editorChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice());
|
|
940
|
-
const { selectedEditor } = await escapeablePrompt([
|
|
941
|
-
{
|
|
942
|
-
type: 'list',
|
|
943
|
-
name: 'selectedEditor',
|
|
944
|
-
message: promptMessage('Choose your editor:'),
|
|
945
|
-
choices: editorChoices,
|
|
946
|
-
},
|
|
947
|
-
]);
|
|
948
|
-
if (selectedEditor === 'back') {
|
|
949
|
-
// Re-ask for input method
|
|
950
|
-
continue inputMethodLoop;
|
|
951
|
-
}
|
|
952
|
-
if (selectedEditor === 'main-menu') {
|
|
953
|
-
return main();
|
|
954
|
-
}
|
|
955
|
-
// Set the EDITOR environment variable before opening inquirer editor
|
|
956
|
-
const originalEditor = process.env.EDITOR;
|
|
957
|
-
const originalVisual = process.env.VISUAL;
|
|
958
|
-
process.env.EDITOR = selectedEditor;
|
|
959
|
-
process.env.VISUAL = selectedEditor;
|
|
960
|
-
const editorName = availableEditors.find((e) => e.command === selectedEditor)
|
|
961
|
-
?.name || 'editor';
|
|
962
|
-
console.log(colors.muted('\nNote: The temp file is automatically deleted after encryption.\n'));
|
|
963
|
-
try {
|
|
964
|
-
const { editorInput } = await escapeablePrompt([
|
|
965
|
-
{
|
|
966
|
-
type: 'editor',
|
|
967
|
-
name: 'editorInput',
|
|
968
|
-
message: promptMessage(`Press Enter to open ${editorName}:`),
|
|
969
|
-
postfix: '.txt',
|
|
970
|
-
waitForUseInput: false,
|
|
971
|
-
},
|
|
972
|
-
]);
|
|
973
|
-
message = editorInput;
|
|
974
|
-
break inputMethodLoop;
|
|
975
|
-
}
|
|
976
|
-
finally {
|
|
977
|
-
// Restore original environment variables
|
|
978
|
-
if (originalEditor !== undefined) {
|
|
979
|
-
process.env.EDITOR = originalEditor;
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
delete process.env.EDITOR;
|
|
983
|
-
}
|
|
984
|
-
if (originalVisual !== undefined) {
|
|
985
|
-
process.env.VISUAL = originalVisual;
|
|
986
|
-
}
|
|
987
|
-
else {
|
|
988
|
-
delete process.env.VISUAL;
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
else {
|
|
993
|
-
message = await readInlineMultilineInput('Enter your message:');
|
|
994
|
-
break inputMethodLoop;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
if (!message || message.trim() === '') {
|
|
998
|
-
console.log();
|
|
999
|
-
showError('No message provided. Aborting.');
|
|
1000
|
-
console.log();
|
|
1001
|
-
return main();
|
|
1002
|
-
}
|
|
1003
|
-
console.log();
|
|
1004
|
-
showLoading('Encrypting message...');
|
|
1005
|
-
console.log();
|
|
1006
|
-
const encrypted = await encryptMessage(message, recipientPublicKeys.length > 0 ? recipientPublicKeys : undefined);
|
|
1007
|
-
// Clear screen, show encrypted message, then clipboard status
|
|
1008
|
-
console.clear();
|
|
1009
|
-
printBanner();
|
|
1010
|
-
console.log(colors.successBold('Encrypted Message:\n'));
|
|
1011
|
-
printDivider();
|
|
1012
|
-
console.log(encrypted);
|
|
1013
|
-
printDivider();
|
|
1014
|
-
// Copy to clipboard and show status below the message
|
|
1015
|
-
try {
|
|
1016
|
-
await clipboardy.write(encrypted);
|
|
1017
|
-
console.log();
|
|
1018
|
-
showSuccess('Encrypted message copied to clipboard');
|
|
1019
|
-
console.log();
|
|
1020
|
-
}
|
|
1021
|
-
catch (clipError) {
|
|
1022
|
-
console.log();
|
|
1023
|
-
showWarning('Clipboard unavailable');
|
|
1024
|
-
console.log();
|
|
1025
|
-
}
|
|
1026
|
-
// Offer to save the contact if it's a new public key (single recipient only)
|
|
1027
|
-
const newPublicKey = recipientPublicKeys[0];
|
|
1028
|
-
if (isNewContact &&
|
|
1029
|
-
newPublicKey !== undefined &&
|
|
1030
|
-
recipientPublicKeys.length === 1) {
|
|
1031
|
-
const { saveContact } = await escapeablePrompt([
|
|
1032
|
-
{
|
|
1033
|
-
type: 'confirm',
|
|
1034
|
-
name: 'saveContact',
|
|
1035
|
-
message: promptMessage('Would you like to save this contact for future use?'),
|
|
1036
|
-
default: true,
|
|
1037
|
-
},
|
|
1038
|
-
]);
|
|
1039
|
-
if (saveContact) {
|
|
1040
|
-
try {
|
|
1041
|
-
// Extract key information
|
|
1042
|
-
const keyInfo = await extractPublicKeyInfo(newPublicKey);
|
|
1043
|
-
// Prompt for contact name
|
|
1044
|
-
const defaultName = (keyInfo.email || 'unknown').split('@')[0] || 'Contact';
|
|
1045
|
-
const answers = await escapeablePrompt([
|
|
1046
|
-
{
|
|
1047
|
-
type: 'input',
|
|
1048
|
-
name: 'contactName',
|
|
1049
|
-
message: promptMessage('Contact name:'),
|
|
1050
|
-
default: defaultName,
|
|
1051
|
-
validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
|
|
1052
|
-
},
|
|
1053
|
-
]);
|
|
1054
|
-
const contactName = answers.contactName;
|
|
1055
|
-
// Check if contact already exists by fingerprint
|
|
1056
|
-
const existingContacts = db.select({
|
|
1057
|
-
table: 'contact',
|
|
1058
|
-
where: {
|
|
1059
|
-
key: 'fingerprint',
|
|
1060
|
-
compare: 'is',
|
|
1061
|
-
value: keyInfo.fingerprint,
|
|
1062
|
-
},
|
|
1063
|
-
});
|
|
1064
|
-
if (existingContacts.length > 0) {
|
|
1065
|
-
console.log();
|
|
1066
|
-
showWarning('This contact already exists.');
|
|
1067
|
-
console.log();
|
|
1068
|
-
}
|
|
1069
|
-
else {
|
|
1070
|
-
// Save the contact
|
|
1071
|
-
db.insert('contact', {
|
|
1072
|
-
name: contactName.trim(),
|
|
1073
|
-
email: keyInfo.email,
|
|
1074
|
-
fingerprint: keyInfo.fingerprint,
|
|
1075
|
-
public_key: newPublicKey,
|
|
1076
|
-
algorithm: keyInfo.algorithm,
|
|
1077
|
-
key_size: keyInfo.keySize,
|
|
1078
|
-
trusted: false,
|
|
1079
|
-
last_verified_at: null,
|
|
1080
|
-
notes: null,
|
|
1081
|
-
expires_at: keyInfo.expiresAt,
|
|
1082
|
-
revoked: false,
|
|
1083
|
-
});
|
|
1084
|
-
console.log();
|
|
1085
|
-
showSuccess(`Contact "${contactName}" saved successfully!`);
|
|
1086
|
-
console.log();
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
catch (error) {
|
|
1090
|
-
console.log();
|
|
1091
|
-
showError(`Failed to save contact: ${error instanceof Error ? error.message : error}`);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
catch (error) {
|
|
1097
|
-
// Re-throw escape errors to be handled by the main loop
|
|
1098
|
-
if (error instanceof EscapeError)
|
|
1099
|
-
throw error;
|
|
1100
|
-
console.log();
|
|
1101
|
-
showError(`Encryption failed: ${error instanceof Error ? error.message : error}`);
|
|
1102
|
-
}
|
|
959
|
+
await actionEncrypt();
|
|
960
|
+
return;
|
|
1103
961
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const availableEditors = detectAvailableEditors();
|
|
1108
|
-
let encrypted;
|
|
1109
|
-
// Loop for input method selection (allows going back from editor selection)
|
|
1110
|
-
decryptInputLoop: while (true) {
|
|
1111
|
-
// Ask for input method
|
|
1112
|
-
const inputChoices = [];
|
|
1113
|
-
// Always add clipboard option first
|
|
1114
|
-
inputChoices.push({
|
|
1115
|
-
name: `${icons.clipboard} Paste from clipboard`,
|
|
1116
|
-
value: 'clipboard',
|
|
1117
|
-
});
|
|
1118
|
-
if (availableEditors.length > 0) {
|
|
1119
|
-
inputChoices.push({ name: `${icons.editor} Use an editor`, value: 'editor' }, {
|
|
1120
|
-
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
1121
|
-
value: 'inline',
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
else {
|
|
1125
|
-
inputChoices.push({
|
|
1126
|
-
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
1127
|
-
value: 'inline',
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
// Add main menu option
|
|
1131
|
-
inputChoices.push(new inquirer.Separator(), mainMenuChoice());
|
|
1132
|
-
const { inputMethod } = await escapeablePrompt([
|
|
1133
|
-
{
|
|
1134
|
-
type: 'list',
|
|
1135
|
-
name: 'inputMethod',
|
|
1136
|
-
message: promptMessage('How would you like to enter the encrypted message?'),
|
|
1137
|
-
choices: inputChoices,
|
|
1138
|
-
},
|
|
1139
|
-
]);
|
|
1140
|
-
if (inputMethod === 'back' || inputMethod === 'main-menu') {
|
|
1141
|
-
return main();
|
|
1142
|
-
}
|
|
1143
|
-
if (inputMethod === 'clipboard') {
|
|
1144
|
-
try {
|
|
1145
|
-
encrypted = await clipboardy.read();
|
|
1146
|
-
if (!encrypted || encrypted.trim() === '') {
|
|
1147
|
-
console.log();
|
|
1148
|
-
showError('Clipboard is empty.');
|
|
1149
|
-
console.log();
|
|
1150
|
-
return main();
|
|
1151
|
-
}
|
|
1152
|
-
console.log();
|
|
1153
|
-
showSuccess('Encrypted message loaded from clipboard');
|
|
1154
|
-
console.log();
|
|
1155
|
-
break decryptInputLoop;
|
|
1156
|
-
}
|
|
1157
|
-
catch (clipError) {
|
|
1158
|
-
console.log();
|
|
1159
|
-
showError(`Failed to read from clipboard: ${clipError}`);
|
|
1160
|
-
return main();
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
else if (inputMethod === 'editor') {
|
|
1164
|
-
// Let user choose editor
|
|
1165
|
-
const editorChoices = availableEditors.map((e) => ({
|
|
1166
|
-
name: `${icons.editor} ${e.name} ${colors.muted(`(${getEditorInstructions(e.command)})`)}`,
|
|
1167
|
-
value: e.command,
|
|
1168
|
-
}));
|
|
1169
|
-
editorChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice());
|
|
1170
|
-
const { selectedEditor } = await escapeablePrompt([
|
|
1171
|
-
{
|
|
1172
|
-
type: 'list',
|
|
1173
|
-
name: 'selectedEditor',
|
|
1174
|
-
message: promptMessage('Choose your editor:'),
|
|
1175
|
-
choices: editorChoices,
|
|
1176
|
-
},
|
|
1177
|
-
]);
|
|
1178
|
-
if (selectedEditor === 'back') {
|
|
1179
|
-
// Re-ask for input method
|
|
1180
|
-
continue decryptInputLoop;
|
|
1181
|
-
}
|
|
1182
|
-
if (selectedEditor === 'main-menu') {
|
|
1183
|
-
return main();
|
|
1184
|
-
}
|
|
1185
|
-
// Set the EDITOR environment variable before opening inquirer editor
|
|
1186
|
-
const originalEditor = process.env.EDITOR;
|
|
1187
|
-
const originalVisual = process.env.VISUAL;
|
|
1188
|
-
process.env.EDITOR = selectedEditor;
|
|
1189
|
-
process.env.VISUAL = selectedEditor;
|
|
1190
|
-
const editorName = availableEditors.find((e) => e.command === selectedEditor)
|
|
1191
|
-
?.name || 'editor';
|
|
1192
|
-
console.log(colors.muted('\nNote: The temp file is automatically deleted after decryption.\n'));
|
|
1193
|
-
try {
|
|
1194
|
-
const { editorInput } = await escapeablePrompt([
|
|
1195
|
-
{
|
|
1196
|
-
type: 'editor',
|
|
1197
|
-
name: 'editorInput',
|
|
1198
|
-
message: promptMessage(`Press Enter to open ${editorName}:`),
|
|
1199
|
-
postfix: '.txt',
|
|
1200
|
-
waitForUseInput: false,
|
|
1201
|
-
},
|
|
1202
|
-
]);
|
|
1203
|
-
encrypted = editorInput;
|
|
1204
|
-
break decryptInputLoop;
|
|
1205
|
-
}
|
|
1206
|
-
finally {
|
|
1207
|
-
// Restore original environment variables
|
|
1208
|
-
if (originalEditor !== undefined) {
|
|
1209
|
-
process.env.EDITOR = originalEditor;
|
|
1210
|
-
}
|
|
1211
|
-
else {
|
|
1212
|
-
delete process.env.EDITOR;
|
|
1213
|
-
}
|
|
1214
|
-
if (originalVisual !== undefined) {
|
|
1215
|
-
process.env.VISUAL = originalVisual;
|
|
1216
|
-
}
|
|
1217
|
-
else {
|
|
1218
|
-
delete process.env.VISUAL;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
encrypted = await readInlineMultilineInput('Paste the encrypted message:');
|
|
1224
|
-
break decryptInputLoop;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
if (!encrypted || encrypted.trim() === '') {
|
|
1228
|
-
console.log();
|
|
1229
|
-
showError('No encrypted message provided. Aborting.');
|
|
1230
|
-
console.log();
|
|
1231
|
-
return main();
|
|
1232
|
-
}
|
|
1233
|
-
console.log();
|
|
1234
|
-
showLoading('Decrypting message...');
|
|
1235
|
-
console.log();
|
|
1236
|
-
const decrypted = await decryptMessage(encrypted);
|
|
1237
|
-
// Clear screen, show decrypted message, then clipboard status
|
|
1238
|
-
console.clear();
|
|
1239
|
-
printBanner();
|
|
1240
|
-
console.log(colors.successBold('Decrypted Message:\n'));
|
|
1241
|
-
printDivider();
|
|
1242
|
-
console.log(decrypted);
|
|
1243
|
-
printDivider();
|
|
1244
|
-
// Copy to clipboard and show status below the message
|
|
1245
|
-
try {
|
|
1246
|
-
await clipboardy.write(decrypted);
|
|
1247
|
-
console.log();
|
|
1248
|
-
showSuccess('Decrypted message copied to clipboard');
|
|
1249
|
-
console.log();
|
|
1250
|
-
}
|
|
1251
|
-
catch (clipError) {
|
|
1252
|
-
console.log();
|
|
1253
|
-
showWarning('Clipboard unavailable');
|
|
1254
|
-
console.log();
|
|
1255
|
-
}
|
|
1256
|
-
// Wait for user to press Enter before continuing
|
|
1257
|
-
await escapeablePrompt([
|
|
1258
|
-
{
|
|
1259
|
-
type: 'input',
|
|
1260
|
-
name: 'continue',
|
|
1261
|
-
message: colors.muted('Press Enter to continue...'),
|
|
1262
|
-
},
|
|
1263
|
-
]);
|
|
1264
|
-
}
|
|
1265
|
-
catch (error) {
|
|
1266
|
-
// Re-throw escape errors to be handled by the main loop
|
|
1267
|
-
if (error instanceof EscapeError)
|
|
1268
|
-
throw error;
|
|
1269
|
-
console.log();
|
|
1270
|
-
showError(`Decryption failed: ${error instanceof Error ? error.message : error}`);
|
|
1271
|
-
}
|
|
962
|
+
if (action === 'decrypt') {
|
|
963
|
+
await actionDecrypt();
|
|
964
|
+
return;
|
|
1272
965
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
value: 'continue',
|
|
1283
|
-
},
|
|
1284
|
-
exitChoice(),
|
|
1285
|
-
],
|
|
1286
|
-
},
|
|
1287
|
-
]);
|
|
1288
|
-
if (nextAction === 'continue') {
|
|
1289
|
-
await main();
|
|
966
|
+
}
|
|
967
|
+
function clearSession() {
|
|
968
|
+
unlockedKeys.clear();
|
|
969
|
+
}
|
|
970
|
+
// ---------- Bootstrap ----------
|
|
971
|
+
async function main() {
|
|
972
|
+
if (!db) {
|
|
973
|
+
db = await Db.init();
|
|
974
|
+
keyManager = new KeyManager(db);
|
|
1290
975
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
976
|
+
printBanner();
|
|
977
|
+
const hasKeypair = keyManager.hasDefaultKeypair();
|
|
978
|
+
if (!hasKeypair) {
|
|
979
|
+
console.log();
|
|
980
|
+
showWarning("No keypair found. Let's set up your first keypair.");
|
|
981
|
+
console.log();
|
|
982
|
+
await keyManager.setupFirstKeypair();
|
|
983
|
+
console.log();
|
|
984
|
+
showSuccess('Setup complete!');
|
|
985
|
+
console.log();
|
|
1294
986
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
987
|
+
// Unlock default key up front if not already cached
|
|
988
|
+
const defaultKp = keyManager.getDefaultKeypair();
|
|
989
|
+
if (defaultKp &&
|
|
990
|
+
defaultKp.passphrase_protected &&
|
|
991
|
+
!unlockedKeys.has(defaultKp.fingerprint)) {
|
|
992
|
+
const unlocked = await unlockKeypair(defaultKp);
|
|
993
|
+
if (!unlocked) {
|
|
994
|
+
showWarning('No passphrase provided. Decryption will be unavailable.');
|
|
995
|
+
console.log();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Try to unlock everything else from cache (silent)
|
|
999
|
+
await unlockAllCached();
|
|
1305
1000
|
while (true) {
|
|
1306
1001
|
try {
|
|
1307
|
-
await
|
|
1002
|
+
await showMainMenu();
|
|
1308
1003
|
}
|
|
1309
1004
|
catch (error) {
|
|
1310
1005
|
const e = error;
|
|
1311
|
-
// If escape was pressed, just restart the menu
|
|
1312
1006
|
if (error instanceof EscapeError ||
|
|
1313
1007
|
checkAndResetEscape() ||
|
|
1314
1008
|
e.message?.includes('prompt was closed')) {
|
|
1315
1009
|
continue;
|
|
1316
1010
|
}
|
|
1317
|
-
// Handle Ctrl+C gracefully (inquirer throws ExitPromptError)
|
|
1318
1011
|
if (e.message?.includes('force closed the prompt')) {
|
|
1319
|
-
|
|
1012
|
+
clearSession();
|
|
1320
1013
|
console.clear();
|
|
1321
1014
|
process.exit(0);
|
|
1322
1015
|
}
|
|
1323
|
-
|
|
1324
|
-
clearPassphraseCache();
|
|
1325
|
-
console.clear();
|
|
1016
|
+
clearSession();
|
|
1326
1017
|
showError(`Error: ${e.message || error}`);
|
|
1327
|
-
|
|
1018
|
+
await pause();
|
|
1328
1019
|
}
|
|
1329
1020
|
}
|
|
1330
1021
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1022
|
+
process.on('SIGINT', () => {
|
|
1023
|
+
clearSession();
|
|
1024
|
+
console.clear();
|
|
1025
|
+
process.exit(0);
|
|
1026
|
+
});
|
|
1027
|
+
enableGlobalEscape();
|
|
1028
|
+
main().catch((error) => {
|
|
1029
|
+
showError(`Fatal: ${error instanceof Error ? error.message : error}`);
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1333
1033
|
//# sourceMappingURL=pgp-tool.js.map
|