Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6892f11
feat(ssh): add FIDO2 security key (sk-*) support with native PIN/touc…
binaricat Jul 30, 2026
9514866
fix(ssh): harden FIDO agent lifecycle and jump-host SK auth
binaricat Jul 30, 2026
f21fcb1
fix(ssh): never kill the login agent when releasing FIDO agent
binaricat Jul 30, 2026
728e911
fix(ssh): avoid hanging utility workers on FIDO agent shutdown hooks
binaricat Jul 30, 2026
466bead
fix(ssh): wire FIDO askpass and owned agent across all auth surfaces
binaricat Jul 30, 2026
f70dff0
fix(ssh): detect real OpenSSH SK private PEMs via base64 decode
binaricat Jul 30, 2026
257d61d
fix(keychain): prefer SK material type over seeded ED25519 on import
binaricat Jul 30, 2026
cfc1cd1
test(keychain): fix ESM import in import-type structural test
binaricat Jul 30, 2026
9f495c3
feat(ssh): restore FIDO2 security key support (#2308)
cursoragent Aug 8, 2026
0c2e460
fix(ssh): harden FIDO cert, path peek, askpass leases, Win agent
cursoragent Aug 8, 2026
7ca835d
fix(ssh): release FIDO agent/askpass leases on all auth surfaces
cursoragent Aug 8, 2026
9a1b715
fix: address Codex review on PR #2823
netcatty-bot Aug 8, 2026
53e5295
fix: address Codex review on PR #2823
netcatty-bot Aug 8, 2026
f612e1f
fix: address Codex review on PR #2823
netcatty-bot Aug 8, 2026
a4d01a9
fix: address Codex review on PR #2823
netcatty-bot Aug 8, 2026
a93b58e
fix: address Codex review on PR #2823
netcatty-bot Aug 8, 2026
585d9bb
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
f3dd60e
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
3d44278
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
66e97d0
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
51b3b04
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
f84979d
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
9b1e4aa
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
cd53b18
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
5d0a0c6
fix: address Codex review on PR #2823
netcatty-bot Aug 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions application/app/AppHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,20 @@ export function handlePassphraseSkipImpl(getCtx: AppContextGetter, requestId: st
}
}

export function handleFidoPromptSubmitImpl(getCtx: AppContextGetter, requestId: string, response: string) {
const { netcattyBridge, setFidoPromptQueue } = getCtx();
const bridge = netcattyBridge.get();
void bridge?.respondFidoPrompt?.(requestId, response, false);
setFidoPromptQueue((prev: { requestId: string }[]) => prev.filter((r) => r.requestId !== requestId));
}

export function handleFidoPromptCancelImpl(getCtx: AppContextGetter, requestId: string) {
const { netcattyBridge, setFidoPromptQueue } = getCtx();
const bridge = netcattyBridge.get();
void bridge?.respondFidoPrompt?.(requestId, '', true);
setFidoPromptQueue((prev: { requestId: string }[]) => prev.filter((r) => r.requestId !== requestId));
}

export function createLocalTerminalWithCurrentShellImpl(getCtx: AppContextGetter) {
const { classifyLocalShellType, createLocalTerminal, discoveredShells, resolveShellSetting, terminalSettings } = getCtx();
{
Expand Down
55 changes: 54 additions & 1 deletion application/app/AppSideEffects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ import { toast } from '../../components/ui/toast';
import { VaultSection } from '../../components/VaultView';
import { KeyboardInteractiveRequest } from '../../components/KeyboardInteractiveModal';
import { PassphraseRequest } from '../../components/PassphraseModal';
import type { FidoPromptRequest } from '../../components/FidoPromptModal';
import { classifyLocalShellType } from '../../lib/localShell';
import { useDiscoveredShells, resolveShellSetting } from '../../lib/useDiscoveredShells';
import { Host, HostProtocol, KnownHost, SerialConfig, Snippet, SSHKey, TerminalSession } from '../../types';
import { resolveSnippetCommand } from '../../components/SnippetExecutionProvider';
import { isScriptSnippet } from '../../domain/snippetScript.ts';
import { useAppStartupEffects } from './useAppStartupEffects';
import { handleTrayJumpToSessionImpl, handleTrayTogglePortForwardImpl, handleTrayPanelConnectImpl, handleTrayPanelConnectRequestImpl, flushQueuedTrayPanelConnectHostsImpl, handleGlobalHotkeyKeyDownImpl, handleEscapeKeyDownImpl, handleKeyboardInteractiveSubmitImpl, handleKeyboardInteractiveCancelImpl, handlePassphraseSubmitImpl, handlePassphraseCancelImpl, handlePassphraseSkipImpl, createLocalTerminalWithCurrentShellImpl, splitSessionWithCurrentShellImpl, copySessionWithCurrentShellImpl, copyWorkspaceWithCurrentShellImpl, copySessionToNewWindowWithCurrentShellImpl, confirmIfBusyLocalTerminalImpl, closeTabsBatchImpl, executeHotkeyActionImpl, handleCreateLocalTerminalImpl, handleConnectToHostImpl, handleTerminalDataCaptureImpl, hasMultipleProtocolsImpl, handleHostConnectWithProtocolCheckImpl, handleProtocolSelectImpl, handleRootContextMenuImpl } from './AppHandlers';
import { handleTrayJumpToSessionImpl, handleTrayTogglePortForwardImpl, handleTrayPanelConnectImpl, handleTrayPanelConnectRequestImpl, flushQueuedTrayPanelConnectHostsImpl, handleGlobalHotkeyKeyDownImpl, handleEscapeKeyDownImpl, handleKeyboardInteractiveSubmitImpl, handleKeyboardInteractiveCancelImpl, handlePassphraseSubmitImpl, handlePassphraseCancelImpl, handlePassphraseSkipImpl, handleFidoPromptSubmitImpl, handleFidoPromptCancelImpl, createLocalTerminalWithCurrentShellImpl, splitSessionWithCurrentShellImpl, copySessionWithCurrentShellImpl, copyWorkspaceWithCurrentShellImpl, copySessionToNewWindowWithCurrentShellImpl, confirmIfBusyLocalTerminalImpl, closeTabsBatchImpl, executeHotkeyActionImpl, handleCreateLocalTerminalImpl, handleConnectToHostImpl, handleTerminalDataCaptureImpl, hasMultipleProtocolsImpl, handleHostConnectWithProtocolCheckImpl, handleProtocolSelectImpl, handleRootContextMenuImpl } from './AppHandlers';

type OpenSessionInNewWindowPayload = {
title?: string;
Expand Down Expand Up @@ -122,6 +123,8 @@ export function AppSideEffects() {
const [keyboardInteractiveQueue, setKeyboardInteractiveQueue] = useState<KeyboardInteractiveRequest[]>([]);
// Passphrase request queue for encrypted SSH keys
const [passphraseQueue, setPassphraseQueue] = useState<PassphraseRequest[]>([]);
// FIDO2 PIN / touch prompt queue (OpenSSH sk-*)
const [fidoPromptQueue, setFidoPromptQueue] = useState<FidoPromptRequest[]>([]);
const [deleteHostConfirm, setDeleteHostConfirm] = useState<{ hostId: string; name: string } | null>(null);
const [pendingNewWindowSession, setPendingNewWindowSession] = useState<OpenSessionInNewWindowPayload | null>(null);
const [pendingTrayPanelConnectHostIds, setPendingTrayPanelConnectHostIds] = useState<string[]>([]);
Expand Down Expand Up @@ -793,6 +796,50 @@ export function AppSideEffects() {
// Handle passphrase skip (skip this key, continue with others)
const handlePassphraseSkip = useCallback((requestId: string) => { return handlePassphraseSkipImpl(() => ({ netcattyBridge, requestId, setPassphraseQueue }), requestId); }, []);

// FIDO2 PIN / touch prompts from main-process askpass / sk-helper
useEffect(() => {
const bridge = netcattyBridge.get();
if (!bridge?.onFidoPromptRequest) return;
const unsubscribe = bridge.onFidoPromptRequest((request) => {
console.log('[App] FIDO prompt request:', request);
setFidoPromptQueue((prev) => [...prev, {
requestId: request.requestId,
kind: request.kind === 'touch' || request.kind === 'confirm' ? request.kind : 'pin',
message: request.message,
title: request.title,
keyName: request.keyName,
}]);
});
return () => { unsubscribe?.(); };
}, []);

const handleFidoPromptSubmit = useCallback((requestId: string, response: string) => {
return handleFidoPromptSubmitImpl(() => ({ netcattyBridge, requestId, setFidoPromptQueue }), requestId, response);
}, []);

const handleFidoPromptCancel = useCallback((requestId: string) => {
return handleFidoPromptCancelImpl(() => ({ netcattyBridge, requestId, setFidoPromptQueue }), requestId);
}, []);

useEffect(() => {
const bridge = netcattyBridge.get();
if (!bridge?.onFidoPromptTimeout) return;
const unsubscribe = bridge.onFidoPromptTimeout((event) => {
setFidoPromptQueue((prev) => prev.filter((r) => r.requestId !== event.requestId));
toast.error(t('fido.prompt.timeout'));
});
return () => { unsubscribe?.(); };
}, [t]);

useEffect(() => {
const bridge = netcattyBridge.get();
if (!bridge?.onFidoPromptCancelled) return;
const unsubscribe = bridge.onFidoPromptCancelled((event) => {
setFidoPromptQueue((prev) => prev.filter((r) => r.requestId !== event.requestId));
});
return () => { unsubscribe?.(); };
}, []);

// Handle passphrase timeout (request expired on backend)
useEffect(() => {
const bridge = netcattyBridge.get();
Expand Down Expand Up @@ -1670,6 +1717,8 @@ export function AppSideEffects() {
handlePassphraseCancel,
handlePassphraseSkip,
handlePassphraseSubmit,
handleFidoPromptCancel,
handleFidoPromptSubmit,
handleProtocolSelect,
handleRequestCloseEditorTabRef,
resolveEmptyVaultConflict,
Expand All @@ -1693,6 +1742,7 @@ export function AppSideEffects() {
portForwardingRules,
keyboardInteractiveQueue,
passphraseQueue,
fidoPromptQueue,
deleteHostConfirm,
vaultFocusRequest,
openNoteRequest,
Expand Down Expand Up @@ -1739,6 +1789,8 @@ export function AppSideEffects() {
handlePassphraseCancel,
handlePassphraseSkip,
handlePassphraseSubmit,
handleFidoPromptCancel,
handleFidoPromptSubmit,
handleProtocolSelect,
resolveEmptyVaultConflict,
handleCancelDeleteHost,
Expand All @@ -1754,6 +1806,7 @@ export function AppSideEffects() {
portForwardingRules,
keyboardInteractiveQueue,
passphraseQueue,
fidoPromptQueue,
deleteHostConfirm,
vaultFocusRequest,
openNoteRequest,
Expand Down
12 changes: 10 additions & 2 deletions application/app/AppView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { QuickScriptEditorDialog } from '../../components/scripts/QuickScriptEdi
import { AddToWorkspaceDialog } from '../../components/workspace/AddToWorkspaceDialog';
import { KeyboardInteractiveModal } from '../../components/KeyboardInteractiveModal';
import { PassphraseModal } from '../../components/PassphraseModal';
import { FidoPromptModal } from '../../components/FidoPromptModal';
import { UnsavedChangesProvider, promptUnsavedChanges } from '../../components/editor/UnsavedChangesDialog';
import { SnippetExecutionProvider } from '../../components/SnippetExecutionProvider';
import { Button } from '../../components/ui/button';
Expand Down Expand Up @@ -263,11 +264,11 @@ function AppViewInner({ domains }: AppViewProps) {
followAppTerminalTheme,
groupConfigs, handleAddKnownHost, handleConnectSerial, handleConnectToHost, handleCreateLocalTerminal, handleDefaultTerminalThemeChange, handleDeleteHost,
handleEndSessionDrag, handleFollowAppTerminalThemeChange, handleHostConnectWithProtocolCheck, handleHotkeyAction, handleKeyboardInteractiveCancel, handleKeyboardInteractiveSubmit,
handleOpenHostFromVaultNote, handleOpenQuickSwitcher, handleOpenSettings, handleOpenVaultHostFromChat, handleOpenVaultNoteFromChat, handleOpenVaultSectionFromChat, handleOpenVaultSnippetFromChat, handleRootContextMenu, handlePassphraseCancel, handlePassphraseSkip, handlePassphraseSubmit, handleProtocolSelect,
handleOpenHostFromVaultNote, handleOpenQuickSwitcher, handleOpenSettings, handleOpenVaultHostFromChat, handleOpenVaultNoteFromChat, handleOpenVaultSectionFromChat, handleOpenVaultSnippetFromChat, handleRootContextMenu, handlePassphraseCancel, handlePassphraseSkip, handlePassphraseSubmit, handleFidoPromptSubmit, handleFidoPromptCancel, handleProtocolSelect,
handleRequestCloseEditorTabRef, handleSessionStatusChange, handleSyncNowManual, handleTerminalDataCapture, handleUpdateHostFromTerminal,
hostById, hosts, terminalHosts, updateTerminalHosts, hotkeyScheme, identities, importOrReuseKey, isBroadcastEnabled, isCreateWorkspaceOpen, isMacClient, isQuickSwitcherOpen,
keyBindings, keyboardInteractiveQueue, keys, logViews, managedSources, navigateToSection, openLogView, openNoteRequest, orderedTabsWithEditors, orphanSessions,
passphraseQueue, protocolSelectHost, proxyProfiles, portForwardingRules, quickResults, quickSearch, removeSessionFromWorkspace, reorderWorkTabs, reorderWorkspaceSessions,
passphraseQueue, fidoPromptQueue, protocolSelectHost, proxyProfiles, portForwardingRules, quickResults, quickSearch, removeSessionFromWorkspace, reorderWorkTabs, reorderWorkspaceSessions,
resolveEmptyVaultConflict, resolveSessionAppearance, runSnippet, sessionLogsDir, sessionLogsEnabled, sessionLogsFormat, sessionLogsTimestampsEnabled, sessionRenameTarget, sshDebugLogsEnabled,
sessions, setActiveTabId, setDeepLinkHostDraft, setDraggingSessionId, setEditorWordWrap,
setNavigateToSection, setTerminalFontFamilyId, setTerminalFontSize, setVaultFocusRequest, updateSessionFontSize, updateSessionRestoreCwd, updateSessionDynamicTitle, updateSessionCodingCliProvider, clearSessionFontSizeOverride,
Expand Down Expand Up @@ -931,6 +932,13 @@ function AppViewInner({ domains }: AppViewProps) {
onSkip={handlePassphraseSkip}
/>

{/* FIDO2 PIN / touch presence (OpenSSH sk-*) */}
<FidoPromptModal
request={fidoPromptQueue?.[0] || null}
onSubmit={handleFidoPromptSubmit}
onCancel={handleFidoPromptCancel}
/>

{/* Empty vault vs cloud data confirmation dialog (#679).
This dialog intentionally cannot be dismissed — the user MUST
choose "Restore" or "Keep Empty" before the sync flow can
Expand Down
4 changes: 4 additions & 0 deletions application/app/appLocalUiStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Host, PortForwardingRule } from '../../domain/models';
import type { VaultSection } from '../../components/VaultView';
import type { KeyboardInteractiveRequest } from '../../components/KeyboardInteractiveModal';
import type { PassphraseRequest } from '../../components/PassphraseModal';
import type { FidoPromptRequest } from '../../components/FidoPromptModal';

type Listener = () => void;

Expand Down Expand Up @@ -31,6 +32,7 @@ export type AppLocalUiSnapshot = {
portForwardingRules: readonly PortForwardingRule[];
keyboardInteractiveQueue: readonly KeyboardInteractiveRequest[];
passphraseQueue: readonly PassphraseRequest[];
fidoPromptQueue: readonly FidoPromptRequest[];
deleteHostConfirm: { hostId: string; name: string } | null;
vaultFocusRequest: unknown;
openNoteRequest: unknown;
Expand All @@ -49,6 +51,7 @@ export const EMPTY_APP_LOCAL_UI: AppLocalUiSnapshot = Object.freeze({
portForwardingRules: Object.freeze([]) as readonly PortForwardingRule[],
keyboardInteractiveQueue: Object.freeze([]) as readonly KeyboardInteractiveRequest[],
passphraseQueue: Object.freeze([]) as readonly PassphraseRequest[],
fidoPromptQueue: Object.freeze([]) as readonly FidoPromptRequest[],
deleteHostConfirm: null,
vaultFocusRequest: null,
openNoteRequest: null,
Expand Down Expand Up @@ -81,6 +84,7 @@ class AppLocalUiStore {
&& prev.portForwardingRules === next.portForwardingRules
&& prev.keyboardInteractiveQueue === next.keyboardInteractiveQueue
&& prev.passphraseQueue === next.passphraseQueue
&& prev.fidoPromptQueue === next.fidoPromptQueue
&& prev.deleteHostConfirm === next.deleteHostConfirm
&& prev.vaultFocusRequest === next.vaultFocusRequest
&& prev.openNoteRequest === next.openNoteRequest
Expand Down
4 changes: 4 additions & 0 deletions application/app/hosts/DialogsHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,15 @@ export function DialogsHost() {
handlePassphraseCancel: handlers.handlePassphraseCancel,
handlePassphraseSkip: handlers.handlePassphraseSkip,
handlePassphraseSubmit: handlers.handlePassphraseSubmit,
handleFidoPromptCancel: handlers.handleFidoPromptCancel,
handleFidoPromptSubmit: handlers.handleFidoPromptSubmit,
handleProtocolSelect: handlers.handleProtocolSelect,
handleRequestCloseEditorTabRef: handlers.handleRequestCloseEditorTabRef,
isCreateWorkspaceOpen: local.isCreateWorkspaceOpen,
isQuickSwitcherOpen: local.isQuickSwitcherOpen,
keyboardInteractiveQueue: local.keyboardInteractiveQueue,
passphraseQueue: local.passphraseQueue,
fidoPromptQueue: local.fidoPromptQueue,
protocolSelectHost: local.protocolSelectHost,
quickResults,
quickSearch: local.quickSearch,
Expand All @@ -83,6 +86,7 @@ export function DialogsHost() {
local.isQuickSwitcherOpen,
local.keyboardInteractiveQueue,
local.passphraseQueue,
local.fidoPromptQueue,
local.protocolSelectHost,
local.quickSearch,
quickResults,
Expand Down
14 changes: 14 additions & 0 deletions application/i18n/locales/en/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,20 @@ export const enTerminalMessages: Messages = {
'keychain.field.publicKey': 'Public key',
'keychain.field.certificatePlaceholder': 'Certificate content (optional)',
'keychain.generate.keyType': 'Key type',
'keychain.generate.fidoHint': 'Requires a plugged-in FIDO2 security key and OpenSSH with libfido2. You may need to touch the key and enter its PIN.',
'fido.prompt.pinTitle': 'Security key PIN',
'fido.prompt.touchTitle': 'Touch your security key',
'fido.prompt.pinDesc': 'Enter the PIN for {keyName}.',
'fido.prompt.touchDesc': 'Touch or tap {keyName} to continue.',
'fido.prompt.pinLabel': 'PIN',
'fido.prompt.touchWaiting': 'Waiting for you to touch the security key…',
'fido.prompt.touchDone': 'I touched it',
'fido.prompt.timeout': 'FIDO prompt timed out. Try connecting again.',
'fido.error.opensshMissing': 'OpenSSH with FIDO/libfido2 is required. On macOS: brew install openssh libfido2.',
'fido.error.agentUnavailable': 'Could not start a FIDO-capable SSH agent.',
'keychain.generate.resident': 'Resident key (store on hardware)',
'keychain.generate.verifyRequired': 'Require PIN every use (verify-required)',
'keychain.action.loadResident': 'Load resident keys from security key',
'keychain.generate.keySize': 'Key size',
'keychain.generate.labelPlaceholder': 'Key label',
'keychain.generate.passphrasePlaceholder': 'Passphrase (optional)',
Expand Down
14 changes: 14 additions & 0 deletions application/i18n/locales/ru/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,20 @@ export const ruTerminalMessages: Messages = {
'keychain.field.publicKey': 'Публичный ключ',
'keychain.field.certificatePlaceholder': 'Содержимое сертификата (необязательно)',
'keychain.generate.keyType': 'Тип ключа',
'keychain.generate.fidoHint': 'Нужен подключённый FIDO2-ключ и OpenSSH с libfido2. Может потребоваться касание ключа и ввод PIN.',
'fido.prompt.pinTitle': 'PIN ключа безопасности',
'fido.prompt.touchTitle': 'Коснитесь ключа безопасности',
'fido.prompt.pinDesc': 'Введите PIN для {keyName}.',
'fido.prompt.touchDesc': 'Коснитесь {keyName}, чтобы продолжить.',
'fido.prompt.pinLabel': 'PIN',
'fido.prompt.touchWaiting': 'Ожидание касания ключа…',
'fido.prompt.touchDone': 'Я коснулся',
'fido.prompt.timeout': 'Время ожидания FIDO истекло. Попробуйте снова.',
'fido.error.opensshMissing': 'Нужен OpenSSH с FIDO/libfido2. macOS: brew install openssh libfido2.',
'fido.error.agentUnavailable': 'Не удалось запустить SSH agent с FIDO.',
'keychain.generate.resident': 'Resident-ключ (на устройстве)',
'keychain.generate.verifyRequired': 'Требовать PIN при каждом использовании',
'keychain.action.loadResident': 'Загрузить resident-ключи с устройства',
'keychain.generate.keySize': 'Размер ключа',
'keychain.generate.labelPlaceholder': 'Метка ключа',
'keychain.generate.passphrasePlaceholder': 'Парольная фраза (необязательно)',
Expand Down
14 changes: 14 additions & 0 deletions application/i18n/locales/zh-CN/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,20 @@ export const zhCNTerminalMessages: Messages = {
'keychain.field.publicKey': '公钥',
'keychain.field.certificatePlaceholder': '证书内容(可选)',
'keychain.generate.keyType': '密钥类型',
'keychain.generate.fidoHint': '需要插入 FIDO2 安全密钥,且系统 OpenSSH 需带 libfido2。生成时可能需要触摸密钥并输入 PIN。',
'fido.prompt.pinTitle': '安全密钥 PIN',
'fido.prompt.touchTitle': '请触摸安全密钥',
'fido.prompt.pinDesc': '请输入 {keyName} 的 PIN。',
'fido.prompt.touchDesc': '请触摸或轻触 {keyName} 以继续。',
'fido.prompt.pinLabel': 'PIN',
'fido.prompt.touchWaiting': '等待你触摸安全密钥…',
'fido.prompt.touchDone': '已触摸',
'fido.prompt.timeout': 'FIDO 提示已超时,请重新连接。',
'fido.error.opensshMissing': '需要带 FIDO/libfido2 的 OpenSSH。macOS:brew install openssh libfido2。',
'fido.error.agentUnavailable': '无法启动支持 FIDO 的 SSH agent。',
'keychain.generate.resident': '驻留密钥(保存在硬件上)',
'keychain.generate.verifyRequired': '每次使用需要 PIN(verify-required)',
'keychain.action.loadResident': '从安全密钥加载驻留密钥',
'keychain.generate.keySize': '密钥长度',
'keychain.generate.labelPlaceholder': '密钥 Label',
'keychain.generate.passphrasePlaceholder': 'Passphrase(可选)',
Expand Down
Loading