lpgp 0.5.1 → 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 +8 -8
- 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 +749 -1068
- 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,1110 +260,774 @@ function startInteractiveMode() {
|
|
|
243
260
|
});
|
|
244
261
|
return encrypted;
|
|
245
262
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
263
|
+
// ---------- Decryption ----------
|
|
264
|
+
async function decryptWithSession(encryptedMessage) {
|
|
265
|
+
const message = await openpgp.readMessage({
|
|
266
|
+
armoredMessage: encryptedMessage,
|
|
267
|
+
});
|
|
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;
|
|
257
278
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const firstTry = await tryWith(getUnlockedPrivateKeys());
|
|
284
|
+
if (firstTry !== null) {
|
|
285
|
+
markKeyAsUsed(message);
|
|
286
|
+
return firstTry;
|
|
287
|
+
}
|
|
288
|
+
const keyIDs = message.getEncryptionKeyIDs();
|
|
289
|
+
if (keyIDs.length === 0) {
|
|
290
|
+
throw new Error('Message contains no recipient information');
|
|
291
|
+
}
|
|
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);
|
|
330
303
|
}
|
|
331
304
|
}
|
|
332
305
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
privateKey = await openpgp.decryptKey({
|
|
337
|
-
privateKey: await openpgp.readPrivateKey({
|
|
338
|
-
armoredKey: defaultKeypair.private_key,
|
|
339
|
-
config: weakKeyConfig,
|
|
340
|
-
}),
|
|
341
|
-
passphrase,
|
|
342
|
-
config: weakKeyConfig,
|
|
343
|
-
});
|
|
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');
|
|
344
309
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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;
|
|
319
|
+
}
|
|
350
320
|
}
|
|
351
|
-
|
|
352
|
-
armoredMessage: encryptedMessage,
|
|
353
|
-
});
|
|
354
|
-
const { data: decrypted } = await openpgp.decrypt({
|
|
355
|
-
message,
|
|
356
|
-
decryptionKeys: privateKey,
|
|
357
|
-
config: weakKeyConfig,
|
|
358
|
-
});
|
|
359
|
-
// Update last_used_at
|
|
360
|
-
db.update('keypair', { key: 'id', value: defaultKeypair.id }, { last_used_at: new Date().toISOString() });
|
|
361
|
-
return decrypted;
|
|
321
|
+
throw new Error('Could not decrypt with any of your matching keys');
|
|
362
322
|
}
|
|
363
|
-
function
|
|
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;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// ---------- Auto-save contact (silent) ----------
|
|
339
|
+
async function autoSaveContact(publicKeyArmored) {
|
|
364
340
|
try {
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
},
|
|
353
|
+
});
|
|
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
|
+
},
|
|
363
|
+
});
|
|
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.`);
|
|
367
380
|
}
|
|
368
381
|
catch {
|
|
369
|
-
|
|
382
|
+
// Silently skip on any failure
|
|
370
383
|
}
|
|
371
384
|
}
|
|
372
|
-
function
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (process.platform === 'darwin') {
|
|
382
|
-
editors.push({ name: 'TextEdit', command: 'open -e', available: true });
|
|
383
|
-
}
|
|
384
|
-
else if (process.platform === 'win32') {
|
|
385
|
-
editors.push({ name: 'Notepad', command: 'notepad', available: true });
|
|
386
|
-
}
|
|
387
|
-
// Check which editors are available
|
|
388
|
-
for (const editor of editors) {
|
|
389
|
-
if (editor.command.includes('open -e') || editor.command === 'notepad') {
|
|
390
|
-
editor.available = true; // TextEdit and Notepad are always available on their platforms
|
|
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
|
+
});
|
|
391
394
|
}
|
|
392
|
-
|
|
393
|
-
|
|
395
|
+
if (availableEditors.length > 0) {
|
|
396
|
+
inputChoices.push({
|
|
397
|
+
name: `${icons.editor} Open in an editor`,
|
|
398
|
+
value: 'editor',
|
|
399
|
+
});
|
|
394
400
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
async function readInlineMultilineInput(promptText) {
|
|
399
|
-
console.log(promptMessage(promptText));
|
|
400
|
-
console.log(colors.muted('(Type your message. Press Enter, then Ctrl+D to finish)\n'));
|
|
401
|
-
const rl = readline.createInterface({ input, output });
|
|
402
|
-
rl.setPrompt('');
|
|
403
|
-
const lines = [];
|
|
404
|
-
return new Promise((resolve) => {
|
|
405
|
-
rl.on('line', (line) => {
|
|
406
|
-
lines.push(line);
|
|
401
|
+
inputChoices.push({
|
|
402
|
+
name: `${icons.inline} Type inline ${colors.muted('(Ctrl+D when done)')}`,
|
|
403
|
+
value: 'inline',
|
|
407
404
|
});
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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.');
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
return { value: content };
|
|
427
|
+
}
|
|
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
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
417
485
|
}
|
|
418
|
-
async function
|
|
419
|
-
let
|
|
486
|
+
async function getRecipientFromPaste() {
|
|
487
|
+
let value;
|
|
420
488
|
try {
|
|
421
|
-
|
|
489
|
+
value = await readInlineMultiline("Paste the recipient's PGP PUBLIC KEY block:", '(Paste, then press Ctrl+D)');
|
|
422
490
|
}
|
|
423
|
-
catch {
|
|
424
|
-
|
|
425
|
-
|
|
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;
|
|
426
499
|
}
|
|
427
|
-
const keys = extractAllPublicKeys(
|
|
500
|
+
const keys = extractAllPublicKeys(value);
|
|
428
501
|
if (keys.length === 0) {
|
|
429
|
-
|
|
430
|
-
return
|
|
502
|
+
showError('No valid PGP public key block found.');
|
|
503
|
+
return null;
|
|
431
504
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
showWarning(`Skipping duplicate key: ${recipientName}`);
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
recipients.push({
|
|
446
|
-
name: recipientName,
|
|
447
|
-
publicKey,
|
|
448
|
-
});
|
|
449
|
-
showSuccess(`Added recipient: ${recipientName}`);
|
|
450
|
-
addedCount++;
|
|
451
|
-
}
|
|
452
|
-
catch (error) {
|
|
453
|
-
showError(`Failed to parse a key: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
454
|
-
}
|
|
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;
|
|
455
515
|
}
|
|
456
|
-
return addedCount;
|
|
457
516
|
}
|
|
458
517
|
async function selectMultipleRecipients() {
|
|
459
518
|
const recipients = [];
|
|
460
519
|
const contacts = db.select({ table: 'contact' });
|
|
461
|
-
const defaultKeypair =
|
|
462
|
-
|
|
463
|
-
function buildChoices() {
|
|
520
|
+
const defaultKeypair = keyManager.getDefaultKeypair();
|
|
521
|
+
while (true) {
|
|
464
522
|
const choices = [];
|
|
465
|
-
// Show current recipients count
|
|
466
523
|
if (recipients.length > 0) {
|
|
467
524
|
choices.push({
|
|
468
525
|
name: colors.primary(`── Current recipients: ${recipients.length} ──`),
|
|
469
|
-
value: 'show
|
|
526
|
+
value: 'show',
|
|
470
527
|
});
|
|
471
528
|
}
|
|
472
|
-
|
|
473
|
-
const selfAdded = recipients.some((r) => r.name === 'Myself');
|
|
474
|
-
if (defaultKeypair && !selfAdded) {
|
|
529
|
+
if (defaultKeypair && !recipients.some((r) => r.name === 'Myself')) {
|
|
475
530
|
choices.push({
|
|
476
|
-
name: `${icons.key} Add myself ${colors.muted('(so
|
|
531
|
+
name: `${icons.key} Add myself ${colors.muted('(so you can decrypt too)')}`,
|
|
477
532
|
value: 'self',
|
|
478
533
|
});
|
|
479
534
|
}
|
|
480
|
-
// Option to select from contacts
|
|
481
535
|
if (contacts.length > 0) {
|
|
482
536
|
choices.push({
|
|
483
|
-
name: `${icons.contact}
|
|
537
|
+
name: `${icons.contact} Pick from contacts ${colors.muted(`(${contacts.length})`)}`,
|
|
484
538
|
value: 'contacts',
|
|
485
539
|
});
|
|
486
540
|
}
|
|
487
|
-
|
|
488
|
-
choices.push({
|
|
489
|
-
name: `${icons.clipboard} Paste from clipboard ${colors.muted('(supports multiple keys)')}`,
|
|
490
|
-
value: 'clipboard',
|
|
491
|
-
});
|
|
492
|
-
choices.push({
|
|
493
|
-
name: `${icons.inline} Type/paste a single key`,
|
|
494
|
-
value: 'manual',
|
|
495
|
-
});
|
|
496
|
-
// Done or cancel
|
|
497
|
-
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(), {
|
|
498
542
|
name: recipients.length > 0
|
|
499
|
-
? `${icons.success} Done
|
|
543
|
+
? `${icons.success} Done`
|
|
500
544
|
: `${icons.back} Cancel`,
|
|
501
545
|
value: 'done',
|
|
502
546
|
});
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
let addMore = true;
|
|
506
|
-
while (addMore) {
|
|
507
|
-
const { addMethod } = await escapeablePrompt([
|
|
547
|
+
const { method } = await escapeablePrompt([
|
|
508
548
|
{
|
|
509
549
|
type: 'list',
|
|
510
|
-
name: '
|
|
550
|
+
name: 'method',
|
|
511
551
|
message: promptMessage('Add recipients:'),
|
|
512
|
-
choices
|
|
552
|
+
choices,
|
|
513
553
|
},
|
|
514
554
|
]);
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
else if (addMethod === 'show-recipients') {
|
|
519
|
-
// Show current recipients
|
|
555
|
+
if (method === 'done')
|
|
556
|
+
break;
|
|
557
|
+
if (method === 'show') {
|
|
520
558
|
console.log(colors.primary('\nCurrent recipients:'));
|
|
521
|
-
for (const r of recipients)
|
|
559
|
+
for (const r of recipients)
|
|
522
560
|
console.log(colors.muted(` • ${r.name}`));
|
|
523
|
-
}
|
|
524
561
|
console.log();
|
|
562
|
+
continue;
|
|
525
563
|
}
|
|
526
|
-
|
|
564
|
+
if (method === 'self') {
|
|
527
565
|
if (defaultKeypair) {
|
|
528
566
|
recipients.push({
|
|
529
567
|
name: 'Myself',
|
|
530
568
|
publicKey: defaultKeypair.public_key,
|
|
569
|
+
isNew: false,
|
|
531
570
|
});
|
|
532
|
-
showSuccess('Added yourself
|
|
571
|
+
showSuccess('Added yourself');
|
|
533
572
|
}
|
|
573
|
+
continue;
|
|
534
574
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const { selectedContacts } = await escapeablePrompt([
|
|
575
|
+
if (method === 'contacts') {
|
|
576
|
+
const { selected } = await escapeablePrompt([
|
|
538
577
|
{
|
|
539
578
|
type: 'checkbox',
|
|
540
|
-
name: '
|
|
541
|
-
message: promptMessage('Select contacts
|
|
579
|
+
name: 'selected',
|
|
580
|
+
message: promptMessage('Select contacts:'),
|
|
542
581
|
choices: contacts.map((c) => {
|
|
543
|
-
const
|
|
582
|
+
const already = recipients.some((r) => r.publicKey === c.public_key);
|
|
544
583
|
return {
|
|
545
|
-
name: `${c.name} <${c.email
|
|
584
|
+
name: `${c.name} <${c.email}>${already ? colors.muted(' (added)') : ''}`,
|
|
546
585
|
value: c.id,
|
|
547
|
-
|
|
548
|
-
disabled: alreadyAdded,
|
|
586
|
+
disabled: already,
|
|
549
587
|
};
|
|
550
588
|
}),
|
|
551
589
|
},
|
|
552
590
|
]);
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (contact) {
|
|
591
|
+
for (const id of selected) {
|
|
592
|
+
const c = contacts.find((x) => x.id === id);
|
|
593
|
+
if (c) {
|
|
557
594
|
recipients.push({
|
|
558
|
-
name:
|
|
559
|
-
|
|
595
|
+
name: formatMaskedRecipient({
|
|
596
|
+
name: c.name,
|
|
597
|
+
email: c.email,
|
|
598
|
+
fingerprint: c.fingerprint,
|
|
599
|
+
}),
|
|
600
|
+
publicKey: c.public_key,
|
|
601
|
+
isNew: false,
|
|
560
602
|
});
|
|
561
|
-
addedCount++;
|
|
562
603
|
}
|
|
563
604
|
}
|
|
564
|
-
|
|
565
|
-
showSuccess(`Added ${addedCount} contact${addedCount > 1 ? 's' : ''}`);
|
|
566
|
-
}
|
|
605
|
+
continue;
|
|
567
606
|
}
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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;
|
|
574
613
|
}
|
|
575
|
-
|
|
576
|
-
else if (addMethod === 'manual') {
|
|
577
|
-
const publicKey = await getRecipientPublicKey();
|
|
578
|
-
if (publicKey) {
|
|
614
|
+
for (const armored of keys) {
|
|
579
615
|
try {
|
|
580
|
-
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
recipients.push({
|
|
589
|
-
name: recipientName,
|
|
590
|
-
publicKey,
|
|
591
|
-
});
|
|
592
|
-
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;
|
|
593
622
|
}
|
|
623
|
+
recipients.push({ name, publicKey: armored, isNew: true });
|
|
624
|
+
showSuccess(`Added ${name}`);
|
|
594
625
|
}
|
|
595
626
|
catch (error) {
|
|
596
|
-
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}`);
|
|
597
641
|
}
|
|
598
642
|
}
|
|
599
643
|
}
|
|
600
644
|
}
|
|
601
645
|
return recipients;
|
|
602
646
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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;
|
|
610
654
|
}
|
|
611
|
-
|
|
612
|
-
|
|
655
|
+
let selected;
|
|
656
|
+
if (keypairs.length === 1) {
|
|
657
|
+
selected = keypairs[0];
|
|
613
658
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const { useClipboard } = await escapeablePrompt([
|
|
659
|
+
else {
|
|
660
|
+
const defaultId = keypairs.find((kp) => kp.is_default)?.id;
|
|
661
|
+
const { keypairId } = await escapeablePrompt([
|
|
618
662
|
{
|
|
619
|
-
type: '
|
|
620
|
-
name: '
|
|
621
|
-
message: '
|
|
622
|
-
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
|
+
],
|
|
623
675
|
},
|
|
624
676
|
]);
|
|
625
|
-
if (
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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 }];
|
|
629
717
|
}
|
|
630
718
|
}
|
|
719
|
+
catch {
|
|
720
|
+
// Bad key, fall through to picker
|
|
721
|
+
}
|
|
631
722
|
}
|
|
632
|
-
// If no
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
lines.push(line);
|
|
642
|
-
const content = lines.join('\n');
|
|
643
|
-
// Check if we have a complete key block and current line is empty
|
|
644
|
-
if (line.trim() === '' &&
|
|
645
|
-
content.includes('-----BEGIN PGP PUBLIC KEY BLOCK') &&
|
|
646
|
-
content.includes('-----END PGP PUBLIC KEY BLOCK')) {
|
|
647
|
-
rl.close();
|
|
648
|
-
resolve(content.trim());
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
rl.on('close', () => {
|
|
652
|
-
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',
|
|
653
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',
|
|
654
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
|
+
}
|
|
655
814
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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}`));
|
|
660
821
|
console.log();
|
|
661
|
-
return null;
|
|
662
822
|
}
|
|
663
|
-
//
|
|
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…');
|
|
664
890
|
try {
|
|
665
|
-
|
|
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();
|
|
666
898
|
console.log();
|
|
667
|
-
|
|
899
|
+
const copied = await writeClipboardSafe(plaintext);
|
|
900
|
+
if (copied)
|
|
901
|
+
showSuccess('Decrypted message copied to clipboard.');
|
|
668
902
|
console.log();
|
|
669
|
-
|
|
903
|
+
await pause();
|
|
670
904
|
}
|
|
671
905
|
catch (error) {
|
|
906
|
+
if (error instanceof EscapeError)
|
|
907
|
+
throw error;
|
|
908
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
672
909
|
console.log();
|
|
673
|
-
showError(`
|
|
674
|
-
|
|
910
|
+
showError(`Decryption failed: ${msg}`);
|
|
911
|
+
console.log();
|
|
912
|
+
await pause();
|
|
675
913
|
}
|
|
676
914
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
'open -e': 'Save: Cmd+S, then close the window',
|
|
686
|
-
notepad: 'Save: Ctrl+S, then close the window',
|
|
687
|
-
};
|
|
688
|
-
return instructions[editorCommand] || 'Save and close the editor when done';
|
|
689
|
-
}
|
|
690
|
-
function clearPassphraseCache() {
|
|
691
|
-
// Clear all cached passphrases from memory
|
|
692
|
-
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
|
+
]);
|
|
693
923
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (!db) {
|
|
697
|
-
db = await Db.init();
|
|
698
|
-
keyManager = new KeyManager(db);
|
|
699
|
-
}
|
|
924
|
+
// ---------- Main menu ----------
|
|
925
|
+
async function showMainMenu() {
|
|
700
926
|
printBanner();
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (!hasKeypair) {
|
|
704
|
-
console.log();
|
|
705
|
-
showWarning("No keypair found. Let's set up your first keypair.");
|
|
706
|
-
console.log();
|
|
707
|
-
await keyManager.setupFirstKeypair();
|
|
708
|
-
console.log();
|
|
709
|
-
showSuccess('Setup complete! You can now use the tool.');
|
|
710
|
-
console.log();
|
|
711
|
-
}
|
|
712
|
-
// Build menu choices
|
|
927
|
+
const defaultKp = keyManager.getDefaultKeypair();
|
|
928
|
+
printHomeStatus(defaultKp ? `${defaultKp.name} key` : null);
|
|
713
929
|
const menuChoices = [
|
|
714
|
-
{ name: `${icons.
|
|
930
|
+
{ name: `${icons.clipboard} Copy my public key`, value: 'copy' },
|
|
715
931
|
{ name: `${icons.decrypt} Decrypt a message`, value: 'decrypt' },
|
|
716
|
-
{ 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(),
|
|
717
936
|
];
|
|
718
|
-
// Check if installed globally and if update is available
|
|
719
|
-
const installedVersion = getInstalledVersion();
|
|
720
|
-
const latestVersion = getLatestVersion();
|
|
721
|
-
if (!installedVersion) {
|
|
722
|
-
// Not installed globally - offer to install
|
|
723
|
-
menuChoices.push(new inquirer.Separator());
|
|
724
|
-
menuChoices.push({
|
|
725
|
-
name: `${icons.add} Install lpgp globally ${colors.muted('(for offline use)')}`,
|
|
726
|
-
value: 'install',
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
else if (latestVersion &&
|
|
730
|
-
isOlderVersion(installedVersion, latestVersion)) {
|
|
731
|
-
// Installed but outdated - offer to update
|
|
732
|
-
menuChoices.push(new inquirer.Separator());
|
|
733
|
-
menuChoices.push({
|
|
734
|
-
name: `${icons.add} Update lpgp ${colors.muted(`(${installedVersion} → ${latestVersion})`)}`,
|
|
735
|
-
value: 'update',
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
menuChoices.push(new inquirer.Separator());
|
|
739
|
-
menuChoices.push(exitChoice());
|
|
740
937
|
const { action } = await escapeablePrompt([
|
|
741
938
|
{
|
|
742
939
|
type: 'list',
|
|
743
940
|
name: 'action',
|
|
744
|
-
message: promptMessage('
|
|
941
|
+
message: promptMessage('Choose an action'),
|
|
745
942
|
choices: menuChoices,
|
|
746
943
|
},
|
|
747
944
|
]);
|
|
748
945
|
if (action === 'exit') {
|
|
749
|
-
|
|
946
|
+
clearSession();
|
|
750
947
|
console.clear();
|
|
751
948
|
process.exit(0);
|
|
752
949
|
}
|
|
753
|
-
if (action === 'install' || action === 'update') {
|
|
754
|
-
await installOrUpdateGlobally(action === 'update');
|
|
755
|
-
await escapeablePrompt([
|
|
756
|
-
{
|
|
757
|
-
type: 'input',
|
|
758
|
-
name: 'continue',
|
|
759
|
-
message: promptMessage('Press Enter to continue...'),
|
|
760
|
-
},
|
|
761
|
-
]);
|
|
762
|
-
return main();
|
|
763
|
-
}
|
|
764
950
|
if (action === 'keys') {
|
|
765
951
|
await keyManager.showKeyManagementMenu();
|
|
766
|
-
return
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (action === 'copy') {
|
|
955
|
+
await actionCopyPublicKey();
|
|
956
|
+
return;
|
|
767
957
|
}
|
|
768
958
|
if (action === 'encrypt') {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const { recipient } = await escapeablePrompt([
|
|
772
|
-
{
|
|
773
|
-
type: 'list',
|
|
774
|
-
name: 'recipient',
|
|
775
|
-
message: promptMessage('Who do you want to encrypt this message for?'),
|
|
776
|
-
choices: [
|
|
777
|
-
{
|
|
778
|
-
name: `${icons.contact} Someone else ${colors.muted('(use their public key)')}`,
|
|
779
|
-
value: 'other',
|
|
780
|
-
},
|
|
781
|
-
{
|
|
782
|
-
name: `${icons.multiple} Multiple recipients`,
|
|
783
|
-
value: 'multiple',
|
|
784
|
-
},
|
|
785
|
-
{
|
|
786
|
-
name: `${icons.key} Myself ${colors.muted('(use my public key)')}`,
|
|
787
|
-
value: 'self',
|
|
788
|
-
},
|
|
789
|
-
new inquirer.Separator(),
|
|
790
|
-
mainMenuChoice(),
|
|
791
|
-
],
|
|
792
|
-
},
|
|
793
|
-
]);
|
|
794
|
-
if (recipient === 'back' || recipient === 'main-menu') {
|
|
795
|
-
return main();
|
|
796
|
-
}
|
|
797
|
-
let recipientPublicKeys = [];
|
|
798
|
-
let recipientNames = [];
|
|
799
|
-
let isNewContact = false;
|
|
800
|
-
// Handle multiple recipients
|
|
801
|
-
if (recipient === 'multiple') {
|
|
802
|
-
const recipients = await selectMultipleRecipients();
|
|
803
|
-
if (recipients.length === 0) {
|
|
804
|
-
console.log();
|
|
805
|
-
showError('No recipients selected. Aborting.');
|
|
806
|
-
console.log();
|
|
807
|
-
return main();
|
|
808
|
-
}
|
|
809
|
-
recipientPublicKeys = recipients.map((r) => r.publicKey);
|
|
810
|
-
recipientNames = recipients.map((r) => r.name);
|
|
811
|
-
// Show summary
|
|
812
|
-
console.log(colors.primary('\nEncrypting for the following recipients:'));
|
|
813
|
-
for (const name of recipientNames) {
|
|
814
|
-
console.log(colors.muted(` • ${name}`));
|
|
815
|
-
}
|
|
816
|
-
console.log();
|
|
817
|
-
}
|
|
818
|
-
else if (recipient === 'other') {
|
|
819
|
-
// Check if there are any saved contacts
|
|
820
|
-
const contacts = db.select({ table: 'contact' });
|
|
821
|
-
// Loop for recipient selection (allows going back from contacts submenu)
|
|
822
|
-
recipientLoop: while (true) {
|
|
823
|
-
// Build main menu choices
|
|
824
|
-
const recipientChoices = [];
|
|
825
|
-
if (contacts.length > 0) {
|
|
826
|
-
recipientChoices.push({
|
|
827
|
-
name: `${icons.contact} Saved contacts ${colors.muted(`(${contacts.length} available)`)}`,
|
|
828
|
-
value: 'saved-contacts',
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
recipientChoices.push({ name: `${icons.add} Use a new public key`, value: 'new' }, new inquirer.Separator(), mainMenuChoice());
|
|
832
|
-
const { recipientSource } = await escapeablePrompt([
|
|
833
|
-
{
|
|
834
|
-
type: 'list',
|
|
835
|
-
name: 'recipientSource',
|
|
836
|
-
message: promptMessage('How would you like to specify the recipient?'),
|
|
837
|
-
choices: recipientChoices,
|
|
838
|
-
},
|
|
839
|
-
]);
|
|
840
|
-
if (recipientSource === 'main' || recipientSource === 'main-menu') {
|
|
841
|
-
return main();
|
|
842
|
-
}
|
|
843
|
-
if (recipientSource === 'saved-contacts') {
|
|
844
|
-
// Show contacts submenu
|
|
845
|
-
const contactChoices = contacts.map((c) => ({
|
|
846
|
-
name: `${icons.contact} ${c.name} ${colors.muted(`<${c.email}>`)}`,
|
|
847
|
-
value: c.id,
|
|
848
|
-
}));
|
|
849
|
-
contactChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice(), new inquirer.Separator());
|
|
850
|
-
const { contactChoice } = await escapeablePrompt([
|
|
851
|
-
{
|
|
852
|
-
type: 'list',
|
|
853
|
-
name: 'contactChoice',
|
|
854
|
-
message: promptMessage('Select a contact:'),
|
|
855
|
-
choices: contactChoices,
|
|
856
|
-
},
|
|
857
|
-
]);
|
|
858
|
-
if (contactChoice === 'main' || contactChoice === 'main-menu') {
|
|
859
|
-
return main();
|
|
860
|
-
}
|
|
861
|
-
if (contactChoice === 'back') {
|
|
862
|
-
// Go back to recipient source selection
|
|
863
|
-
continue recipientLoop;
|
|
864
|
-
}
|
|
865
|
-
// Use saved contact
|
|
866
|
-
const selectedContact = contacts.find((c) => c.id === contactChoice);
|
|
867
|
-
if (selectedContact) {
|
|
868
|
-
recipientPublicKeys = [selectedContact.public_key];
|
|
869
|
-
break recipientLoop;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
else if (recipientSource === 'new') {
|
|
873
|
-
const publicKey = await getRecipientPublicKey();
|
|
874
|
-
if (!publicKey) {
|
|
875
|
-
console.log();
|
|
876
|
-
showError('Could not get recipient public key. Aborting.');
|
|
877
|
-
console.log();
|
|
878
|
-
return main();
|
|
879
|
-
}
|
|
880
|
-
recipientPublicKeys = [publicKey];
|
|
881
|
-
isNewContact = true;
|
|
882
|
-
break recipientLoop;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
// Detect available editors
|
|
887
|
-
const availableEditors = detectAvailableEditors();
|
|
888
|
-
let message;
|
|
889
|
-
// Loop for input method selection (allows going back from editor selection)
|
|
890
|
-
inputMethodLoop: while (true) {
|
|
891
|
-
// Ask for input method
|
|
892
|
-
const inputChoices = [];
|
|
893
|
-
// Always add clipboard option first
|
|
894
|
-
inputChoices.push({
|
|
895
|
-
name: `${icons.clipboard} Paste from clipboard`,
|
|
896
|
-
value: 'clipboard',
|
|
897
|
-
});
|
|
898
|
-
if (availableEditors.length > 0) {
|
|
899
|
-
inputChoices.push({ name: `${icons.editor} Use an editor`, value: 'editor' }, {
|
|
900
|
-
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
901
|
-
value: 'inline',
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
else {
|
|
905
|
-
inputChoices.push({
|
|
906
|
-
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
907
|
-
value: 'inline',
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
// Add main menu option
|
|
911
|
-
inputChoices.push(new inquirer.Separator(), mainMenuChoice());
|
|
912
|
-
const { inputMethod } = await escapeablePrompt([
|
|
913
|
-
{
|
|
914
|
-
type: 'list',
|
|
915
|
-
name: 'inputMethod',
|
|
916
|
-
message: promptMessage('How would you like to enter your message?'),
|
|
917
|
-
choices: inputChoices,
|
|
918
|
-
},
|
|
919
|
-
]);
|
|
920
|
-
if (inputMethod === 'back' || inputMethod === 'main-menu') {
|
|
921
|
-
return main();
|
|
922
|
-
}
|
|
923
|
-
if (inputMethod === 'clipboard') {
|
|
924
|
-
try {
|
|
925
|
-
message = await clipboardy.read();
|
|
926
|
-
if (!message || message.trim() === '') {
|
|
927
|
-
console.log();
|
|
928
|
-
showError('Clipboard is empty.');
|
|
929
|
-
console.log();
|
|
930
|
-
return main();
|
|
931
|
-
}
|
|
932
|
-
console.log();
|
|
933
|
-
showSuccess('Message loaded from clipboard');
|
|
934
|
-
console.log();
|
|
935
|
-
break inputMethodLoop;
|
|
936
|
-
}
|
|
937
|
-
catch (clipError) {
|
|
938
|
-
console.log();
|
|
939
|
-
showError(`Failed to read from clipboard: ${clipError}`);
|
|
940
|
-
return main();
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
else if (inputMethod === 'editor') {
|
|
944
|
-
// Let user choose editor
|
|
945
|
-
const editorChoices = availableEditors.map((e) => ({
|
|
946
|
-
name: `${icons.editor} ${e.name} ${colors.muted(`(${getEditorInstructions(e.command)})`)}`,
|
|
947
|
-
value: e.command,
|
|
948
|
-
}));
|
|
949
|
-
editorChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice());
|
|
950
|
-
const { selectedEditor } = await escapeablePrompt([
|
|
951
|
-
{
|
|
952
|
-
type: 'list',
|
|
953
|
-
name: 'selectedEditor',
|
|
954
|
-
message: promptMessage('Choose your editor:'),
|
|
955
|
-
choices: editorChoices,
|
|
956
|
-
},
|
|
957
|
-
]);
|
|
958
|
-
if (selectedEditor === 'back') {
|
|
959
|
-
// Re-ask for input method
|
|
960
|
-
continue inputMethodLoop;
|
|
961
|
-
}
|
|
962
|
-
if (selectedEditor === 'main-menu') {
|
|
963
|
-
return main();
|
|
964
|
-
}
|
|
965
|
-
// Set the EDITOR environment variable before opening inquirer editor
|
|
966
|
-
const originalEditor = process.env.EDITOR;
|
|
967
|
-
const originalVisual = process.env.VISUAL;
|
|
968
|
-
process.env.EDITOR = selectedEditor;
|
|
969
|
-
process.env.VISUAL = selectedEditor;
|
|
970
|
-
const editorName = availableEditors.find((e) => e.command === selectedEditor)
|
|
971
|
-
?.name || 'editor';
|
|
972
|
-
console.log(colors.muted('\nNote: The temp file is automatically deleted after encryption.\n'));
|
|
973
|
-
try {
|
|
974
|
-
const { editorInput } = await escapeablePrompt([
|
|
975
|
-
{
|
|
976
|
-
type: 'editor',
|
|
977
|
-
name: 'editorInput',
|
|
978
|
-
message: promptMessage(`Press Enter to open ${editorName}:`),
|
|
979
|
-
postfix: '.txt',
|
|
980
|
-
waitForUseInput: false,
|
|
981
|
-
},
|
|
982
|
-
]);
|
|
983
|
-
message = editorInput;
|
|
984
|
-
break inputMethodLoop;
|
|
985
|
-
}
|
|
986
|
-
finally {
|
|
987
|
-
// Restore original environment variables
|
|
988
|
-
if (originalEditor !== undefined) {
|
|
989
|
-
process.env.EDITOR = originalEditor;
|
|
990
|
-
}
|
|
991
|
-
else {
|
|
992
|
-
delete process.env.EDITOR;
|
|
993
|
-
}
|
|
994
|
-
if (originalVisual !== undefined) {
|
|
995
|
-
process.env.VISUAL = originalVisual;
|
|
996
|
-
}
|
|
997
|
-
else {
|
|
998
|
-
delete process.env.VISUAL;
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
else {
|
|
1003
|
-
message = await readInlineMultilineInput('Enter your message:');
|
|
1004
|
-
break inputMethodLoop;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
if (!message || message.trim() === '') {
|
|
1008
|
-
console.log();
|
|
1009
|
-
showError('No message provided. Aborting.');
|
|
1010
|
-
console.log();
|
|
1011
|
-
return main();
|
|
1012
|
-
}
|
|
1013
|
-
console.log();
|
|
1014
|
-
showLoading('Encrypting message...');
|
|
1015
|
-
console.log();
|
|
1016
|
-
const encrypted = await encryptMessage(message, recipientPublicKeys.length > 0 ? recipientPublicKeys : undefined);
|
|
1017
|
-
// Clear screen, show encrypted message, then clipboard status
|
|
1018
|
-
console.clear();
|
|
1019
|
-
printBanner();
|
|
1020
|
-
console.log(colors.successBold('Encrypted Message:\n'));
|
|
1021
|
-
printDivider();
|
|
1022
|
-
console.log(encrypted);
|
|
1023
|
-
printDivider();
|
|
1024
|
-
// Copy to clipboard and show status below the message
|
|
1025
|
-
try {
|
|
1026
|
-
await clipboardy.write(encrypted);
|
|
1027
|
-
console.log();
|
|
1028
|
-
showSuccess('Encrypted message copied to clipboard');
|
|
1029
|
-
console.log();
|
|
1030
|
-
}
|
|
1031
|
-
catch (clipError) {
|
|
1032
|
-
console.log();
|
|
1033
|
-
showWarning('Clipboard unavailable');
|
|
1034
|
-
console.log();
|
|
1035
|
-
}
|
|
1036
|
-
// Offer to save the contact if it's a new public key (single recipient only)
|
|
1037
|
-
const newPublicKey = recipientPublicKeys[0];
|
|
1038
|
-
if (isNewContact &&
|
|
1039
|
-
newPublicKey !== undefined &&
|
|
1040
|
-
recipientPublicKeys.length === 1) {
|
|
1041
|
-
const { saveContact } = await escapeablePrompt([
|
|
1042
|
-
{
|
|
1043
|
-
type: 'confirm',
|
|
1044
|
-
name: 'saveContact',
|
|
1045
|
-
message: promptMessage('Would you like to save this contact for future use?'),
|
|
1046
|
-
default: true,
|
|
1047
|
-
},
|
|
1048
|
-
]);
|
|
1049
|
-
if (saveContact) {
|
|
1050
|
-
try {
|
|
1051
|
-
// Extract key information
|
|
1052
|
-
const keyInfo = await extractPublicKeyInfo(newPublicKey);
|
|
1053
|
-
// Prompt for contact name
|
|
1054
|
-
const defaultName = (keyInfo.email || 'unknown').split('@')[0] || 'Contact';
|
|
1055
|
-
const answers = await escapeablePrompt([
|
|
1056
|
-
{
|
|
1057
|
-
type: 'input',
|
|
1058
|
-
name: 'contactName',
|
|
1059
|
-
message: promptMessage('Contact name:'),
|
|
1060
|
-
default: defaultName,
|
|
1061
|
-
validate: (input) => input.trim().length > 0 || 'Name cannot be empty',
|
|
1062
|
-
},
|
|
1063
|
-
]);
|
|
1064
|
-
const contactName = answers.contactName;
|
|
1065
|
-
// Check if contact already exists by fingerprint
|
|
1066
|
-
const existingContacts = db.select({
|
|
1067
|
-
table: 'contact',
|
|
1068
|
-
where: {
|
|
1069
|
-
key: 'fingerprint',
|
|
1070
|
-
compare: 'is',
|
|
1071
|
-
value: keyInfo.fingerprint,
|
|
1072
|
-
},
|
|
1073
|
-
});
|
|
1074
|
-
if (existingContacts.length > 0) {
|
|
1075
|
-
console.log();
|
|
1076
|
-
showWarning('This contact already exists.');
|
|
1077
|
-
console.log();
|
|
1078
|
-
}
|
|
1079
|
-
else {
|
|
1080
|
-
// Save the contact
|
|
1081
|
-
db.insert('contact', {
|
|
1082
|
-
name: contactName.trim(),
|
|
1083
|
-
email: keyInfo.email,
|
|
1084
|
-
fingerprint: keyInfo.fingerprint,
|
|
1085
|
-
public_key: newPublicKey,
|
|
1086
|
-
algorithm: keyInfo.algorithm,
|
|
1087
|
-
key_size: keyInfo.keySize,
|
|
1088
|
-
trusted: false,
|
|
1089
|
-
last_verified_at: null,
|
|
1090
|
-
notes: null,
|
|
1091
|
-
expires_at: keyInfo.expiresAt,
|
|
1092
|
-
revoked: false,
|
|
1093
|
-
});
|
|
1094
|
-
console.log();
|
|
1095
|
-
showSuccess(`Contact "${contactName}" saved successfully!`);
|
|
1096
|
-
console.log();
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
catch (error) {
|
|
1100
|
-
console.log();
|
|
1101
|
-
showError(`Failed to save contact: ${error instanceof Error ? error.message : error}`);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
catch (error) {
|
|
1107
|
-
// Re-throw escape errors to be handled by the main loop
|
|
1108
|
-
if (error instanceof EscapeError)
|
|
1109
|
-
throw error;
|
|
1110
|
-
console.log();
|
|
1111
|
-
showError(`Encryption failed: ${error instanceof Error ? error.message : error}`);
|
|
1112
|
-
}
|
|
959
|
+
await actionEncrypt();
|
|
960
|
+
return;
|
|
1113
961
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
const availableEditors = detectAvailableEditors();
|
|
1118
|
-
let encrypted;
|
|
1119
|
-
// Loop for input method selection (allows going back from editor selection)
|
|
1120
|
-
decryptInputLoop: while (true) {
|
|
1121
|
-
// Ask for input method
|
|
1122
|
-
const inputChoices = [];
|
|
1123
|
-
// Always add clipboard option first
|
|
1124
|
-
inputChoices.push({
|
|
1125
|
-
name: `${icons.clipboard} Paste from clipboard`,
|
|
1126
|
-
value: 'clipboard',
|
|
1127
|
-
});
|
|
1128
|
-
if (availableEditors.length > 0) {
|
|
1129
|
-
inputChoices.push({ name: `${icons.editor} Use an editor`, value: 'editor' }, {
|
|
1130
|
-
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
1131
|
-
value: 'inline',
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
else {
|
|
1135
|
-
inputChoices.push({
|
|
1136
|
-
name: `${icons.inline} Type inline ${colors.muted('(Enter, then Ctrl+D to finish)')}`,
|
|
1137
|
-
value: 'inline',
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
// Add main menu option
|
|
1141
|
-
inputChoices.push(new inquirer.Separator(), mainMenuChoice());
|
|
1142
|
-
const { inputMethod } = await escapeablePrompt([
|
|
1143
|
-
{
|
|
1144
|
-
type: 'list',
|
|
1145
|
-
name: 'inputMethod',
|
|
1146
|
-
message: promptMessage('How would you like to enter the encrypted message?'),
|
|
1147
|
-
choices: inputChoices,
|
|
1148
|
-
},
|
|
1149
|
-
]);
|
|
1150
|
-
if (inputMethod === 'back' || inputMethod === 'main-menu') {
|
|
1151
|
-
return main();
|
|
1152
|
-
}
|
|
1153
|
-
if (inputMethod === 'clipboard') {
|
|
1154
|
-
try {
|
|
1155
|
-
encrypted = await clipboardy.read();
|
|
1156
|
-
if (!encrypted || encrypted.trim() === '') {
|
|
1157
|
-
console.log();
|
|
1158
|
-
showError('Clipboard is empty.');
|
|
1159
|
-
console.log();
|
|
1160
|
-
return main();
|
|
1161
|
-
}
|
|
1162
|
-
console.log();
|
|
1163
|
-
showSuccess('Encrypted message loaded from clipboard');
|
|
1164
|
-
console.log();
|
|
1165
|
-
break decryptInputLoop;
|
|
1166
|
-
}
|
|
1167
|
-
catch (clipError) {
|
|
1168
|
-
console.log();
|
|
1169
|
-
showError(`Failed to read from clipboard: ${clipError}`);
|
|
1170
|
-
return main();
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
else if (inputMethod === 'editor') {
|
|
1174
|
-
// Let user choose editor
|
|
1175
|
-
const editorChoices = availableEditors.map((e) => ({
|
|
1176
|
-
name: `${icons.editor} ${e.name} ${colors.muted(`(${getEditorInstructions(e.command)})`)}`,
|
|
1177
|
-
value: e.command,
|
|
1178
|
-
}));
|
|
1179
|
-
editorChoices.push(new inquirer.Separator(), backChoice(), mainMenuChoice());
|
|
1180
|
-
const { selectedEditor } = await escapeablePrompt([
|
|
1181
|
-
{
|
|
1182
|
-
type: 'list',
|
|
1183
|
-
name: 'selectedEditor',
|
|
1184
|
-
message: promptMessage('Choose your editor:'),
|
|
1185
|
-
choices: editorChoices,
|
|
1186
|
-
},
|
|
1187
|
-
]);
|
|
1188
|
-
if (selectedEditor === 'back') {
|
|
1189
|
-
// Re-ask for input method
|
|
1190
|
-
continue decryptInputLoop;
|
|
1191
|
-
}
|
|
1192
|
-
if (selectedEditor === 'main-menu') {
|
|
1193
|
-
return main();
|
|
1194
|
-
}
|
|
1195
|
-
// Set the EDITOR environment variable before opening inquirer editor
|
|
1196
|
-
const originalEditor = process.env.EDITOR;
|
|
1197
|
-
const originalVisual = process.env.VISUAL;
|
|
1198
|
-
process.env.EDITOR = selectedEditor;
|
|
1199
|
-
process.env.VISUAL = selectedEditor;
|
|
1200
|
-
const editorName = availableEditors.find((e) => e.command === selectedEditor)
|
|
1201
|
-
?.name || 'editor';
|
|
1202
|
-
console.log(colors.muted('\nNote: The temp file is automatically deleted after decryption.\n'));
|
|
1203
|
-
try {
|
|
1204
|
-
const { editorInput } = await escapeablePrompt([
|
|
1205
|
-
{
|
|
1206
|
-
type: 'editor',
|
|
1207
|
-
name: 'editorInput',
|
|
1208
|
-
message: promptMessage(`Press Enter to open ${editorName}:`),
|
|
1209
|
-
postfix: '.txt',
|
|
1210
|
-
waitForUseInput: false,
|
|
1211
|
-
},
|
|
1212
|
-
]);
|
|
1213
|
-
encrypted = editorInput;
|
|
1214
|
-
break decryptInputLoop;
|
|
1215
|
-
}
|
|
1216
|
-
finally {
|
|
1217
|
-
// Restore original environment variables
|
|
1218
|
-
if (originalEditor !== undefined) {
|
|
1219
|
-
process.env.EDITOR = originalEditor;
|
|
1220
|
-
}
|
|
1221
|
-
else {
|
|
1222
|
-
delete process.env.EDITOR;
|
|
1223
|
-
}
|
|
1224
|
-
if (originalVisual !== undefined) {
|
|
1225
|
-
process.env.VISUAL = originalVisual;
|
|
1226
|
-
}
|
|
1227
|
-
else {
|
|
1228
|
-
delete process.env.VISUAL;
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
else {
|
|
1233
|
-
encrypted = await readInlineMultilineInput('Paste the encrypted message:');
|
|
1234
|
-
break decryptInputLoop;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
if (!encrypted || encrypted.trim() === '') {
|
|
1238
|
-
console.log();
|
|
1239
|
-
showError('No encrypted message provided. Aborting.');
|
|
1240
|
-
console.log();
|
|
1241
|
-
return main();
|
|
1242
|
-
}
|
|
1243
|
-
console.log();
|
|
1244
|
-
showLoading('Decrypting message...');
|
|
1245
|
-
console.log();
|
|
1246
|
-
const decrypted = await decryptMessage(encrypted);
|
|
1247
|
-
// Clear screen, show decrypted message, then clipboard status
|
|
1248
|
-
console.clear();
|
|
1249
|
-
printBanner();
|
|
1250
|
-
console.log(colors.successBold('Decrypted Message:\n'));
|
|
1251
|
-
printDivider();
|
|
1252
|
-
console.log(decrypted);
|
|
1253
|
-
printDivider();
|
|
1254
|
-
// Copy to clipboard and show status below the message
|
|
1255
|
-
try {
|
|
1256
|
-
await clipboardy.write(decrypted);
|
|
1257
|
-
console.log();
|
|
1258
|
-
showSuccess('Decrypted message copied to clipboard');
|
|
1259
|
-
console.log();
|
|
1260
|
-
}
|
|
1261
|
-
catch (clipError) {
|
|
1262
|
-
console.log();
|
|
1263
|
-
showWarning('Clipboard unavailable');
|
|
1264
|
-
console.log();
|
|
1265
|
-
}
|
|
1266
|
-
// Wait for user to press Enter before continuing
|
|
1267
|
-
await escapeablePrompt([
|
|
1268
|
-
{
|
|
1269
|
-
type: 'input',
|
|
1270
|
-
name: 'continue',
|
|
1271
|
-
message: colors.muted('Press Enter to continue...'),
|
|
1272
|
-
},
|
|
1273
|
-
]);
|
|
1274
|
-
}
|
|
1275
|
-
catch (error) {
|
|
1276
|
-
// Re-throw escape errors to be handled by the main loop
|
|
1277
|
-
if (error instanceof EscapeError)
|
|
1278
|
-
throw error;
|
|
1279
|
-
console.log();
|
|
1280
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1281
|
-
// Provide more helpful error messages for common issues
|
|
1282
|
-
if (errorMessage.includes('No decryption key packets found')) {
|
|
1283
|
-
showError('Decryption failed: This message was not encrypted for your current default key.');
|
|
1284
|
-
console.log();
|
|
1285
|
-
console.log(colors.muted(' Tip: Check Key Management to verify the correct keypair is set as default.'));
|
|
1286
|
-
}
|
|
1287
|
-
else {
|
|
1288
|
-
showError(`Decryption failed: ${errorMessage}`);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
962
|
+
if (action === 'decrypt') {
|
|
963
|
+
await actionDecrypt();
|
|
964
|
+
return;
|
|
1291
965
|
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
value: 'continue',
|
|
1302
|
-
},
|
|
1303
|
-
exitChoice(),
|
|
1304
|
-
],
|
|
1305
|
-
},
|
|
1306
|
-
]);
|
|
1307
|
-
if (nextAction === 'continue') {
|
|
1308
|
-
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);
|
|
1309
975
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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();
|
|
1313
986
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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();
|
|
1324
1000
|
while (true) {
|
|
1325
1001
|
try {
|
|
1326
|
-
await
|
|
1002
|
+
await showMainMenu();
|
|
1327
1003
|
}
|
|
1328
1004
|
catch (error) {
|
|
1329
1005
|
const e = error;
|
|
1330
|
-
// If escape was pressed, just restart the menu
|
|
1331
1006
|
if (error instanceof EscapeError ||
|
|
1332
1007
|
checkAndResetEscape() ||
|
|
1333
1008
|
e.message?.includes('prompt was closed')) {
|
|
1334
1009
|
continue;
|
|
1335
1010
|
}
|
|
1336
|
-
// Handle Ctrl+C gracefully (inquirer throws ExitPromptError)
|
|
1337
1011
|
if (e.message?.includes('force closed the prompt')) {
|
|
1338
|
-
|
|
1012
|
+
clearSession();
|
|
1339
1013
|
console.clear();
|
|
1340
1014
|
process.exit(0);
|
|
1341
1015
|
}
|
|
1342
|
-
|
|
1343
|
-
clearPassphraseCache();
|
|
1344
|
-
console.clear();
|
|
1016
|
+
clearSession();
|
|
1345
1017
|
showError(`Error: ${e.message || error}`);
|
|
1346
|
-
|
|
1018
|
+
await pause();
|
|
1347
1019
|
}
|
|
1348
1020
|
}
|
|
1349
1021
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
+
}
|
|
1352
1033
|
//# sourceMappingURL=pgp-tool.js.map
|