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