mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
d65440ace7
- Add PassphraseModal component for interactive passphrase input - Add passphraseHandler bridge to manage passphrase requests/responses - Add sshAuthHelper for centralized SSH key decryption with passphrase support - Update sshBridge, sftpBridge, and portForwardingBridge to use new auth helper - Add passphrase-related IPC channels in preload and type definitions - Add i18n translations for passphrase modal UI (en/zh-CN) Co-Authored-By: Claude (gemini-claude-opus-4-5-thinking) <noreply@anthropic.com>
142 lines
4.1 KiB
JavaScript
142 lines
4.1 KiB
JavaScript
/**
|
|
* Passphrase Handler - Handles passphrase requests for encrypted SSH keys
|
|
* This module provides a mechanism to request passphrase input from the user
|
|
* when encountering encrypted default SSH keys in ~/.ssh
|
|
*/
|
|
|
|
// Passphrase request pending map
|
|
// Map of requestId -> { resolveCallback, rejectCallback, webContentsId, keyPath, createdAt, timeoutId }
|
|
const passphraseRequests = new Map();
|
|
|
|
// TTL for abandoned requests (2 minutes)
|
|
const REQUEST_TTL_MS = 2 * 60 * 1000;
|
|
|
|
/**
|
|
* Generate a unique request ID for passphrase requests
|
|
*/
|
|
function generateRequestId(prefix = 'pp') {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
}
|
|
|
|
/**
|
|
* Request passphrase from user via IPC
|
|
* @param {Object} sender - Electron webContents sender
|
|
* @param {string} keyPath - Path to the encrypted key
|
|
* @param {string} keyName - Name of the key (e.g., id_rsa)
|
|
* @param {string} [hostname] - Optional hostname for context
|
|
* @returns {Promise<{ passphrase?: string, cancelled?: boolean, skipped?: boolean } | null>}
|
|
*/
|
|
function requestPassphrase(sender, keyPath, keyName, hostname) {
|
|
return new Promise((resolve) => {
|
|
if (!sender || sender.isDestroyed()) {
|
|
console.warn('[Passphrase] Sender is destroyed, cannot request passphrase');
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
const requestId = generateRequestId();
|
|
|
|
// Set up TTL timeout to clean up abandoned requests
|
|
const timeoutId = setTimeout(() => {
|
|
const pending = passphraseRequests.get(requestId);
|
|
if (pending) {
|
|
console.warn(`[Passphrase] Request ${requestId} timed out after ${REQUEST_TTL_MS / 1000}s`);
|
|
passphraseRequests.delete(requestId);
|
|
|
|
// Notify renderer to close the modal
|
|
try {
|
|
if (!sender.isDestroyed()) {
|
|
sender.send('netcatty:passphrase-timeout', { requestId });
|
|
}
|
|
} catch (err) {
|
|
console.warn('[Passphrase] Failed to send timeout notification:', err.message);
|
|
}
|
|
|
|
resolve(null);
|
|
}
|
|
}, REQUEST_TTL_MS);
|
|
|
|
passphraseRequests.set(requestId, {
|
|
resolveCallback: resolve,
|
|
webContentsId: sender.id,
|
|
keyPath,
|
|
keyName,
|
|
createdAt: Date.now(),
|
|
timeoutId,
|
|
});
|
|
|
|
console.log(`[Passphrase] Requesting passphrase for ${keyName} (${requestId})`);
|
|
|
|
try {
|
|
sender.send('netcatty:passphrase-request', {
|
|
requestId,
|
|
keyPath,
|
|
keyName,
|
|
hostname,
|
|
});
|
|
} catch (err) {
|
|
console.error('[Passphrase] Failed to send passphrase request:', err);
|
|
passphraseRequests.delete(requestId);
|
|
clearTimeout(timeoutId);
|
|
resolve(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle passphrase response from renderer
|
|
*/
|
|
function handleResponse(_event, payload) {
|
|
const { requestId, passphrase, cancelled, skipped } = payload;
|
|
const pending = passphraseRequests.get(requestId);
|
|
|
|
if (!pending) {
|
|
console.warn(`[Passphrase] No pending request for ${requestId}`);
|
|
return { success: false, error: 'Request not found' };
|
|
}
|
|
|
|
// Clear the TTL timeout
|
|
if (pending.timeoutId) {
|
|
clearTimeout(pending.timeoutId);
|
|
}
|
|
|
|
passphraseRequests.delete(requestId);
|
|
|
|
if (cancelled) {
|
|
// User clicked Cancel - stop the entire passphrase flow
|
|
console.log(`[Passphrase] Request ${requestId} cancelled by user`);
|
|
pending.resolveCallback({ cancelled: true });
|
|
} else if (skipped) {
|
|
// User clicked Skip - skip this key but continue with others
|
|
console.log(`[Passphrase] Request ${requestId} skipped by user`);
|
|
pending.resolveCallback({ skipped: true });
|
|
} else {
|
|
console.log(`[Passphrase] Received passphrase for ${requestId}`);
|
|
pending.resolveCallback({ passphrase: passphrase || null });
|
|
}
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
/**
|
|
* Register IPC handler for passphrase responses
|
|
*/
|
|
function registerHandler(ipcMain) {
|
|
ipcMain.handle('netcatty:passphrase:respond', handleResponse);
|
|
}
|
|
|
|
/**
|
|
* Get pending requests (for debugging)
|
|
*/
|
|
function getRequests() {
|
|
return passphraseRequests;
|
|
}
|
|
|
|
module.exports = {
|
|
generateRequestId,
|
|
requestPassphrase,
|
|
handleResponse,
|
|
registerHandler,
|
|
getRequests,
|
|
};
|