核心修复:
- ssh-service.ts: 新增 resolvePrivateKey(),将私钥文件路径(~/.ssh/id_rsa)解析为内容后传给 ssh2,
解决 'Cannot parse privateKey: Unsupported key format' 错误
- useTerminalConnection.ts + sessionHelper.ts: 根据 authType 过滤字段,password/agent 认证不再传 privateKey
- sessionStore.ts: addSession() 现在先调用 sessionCreate() 持久化到 SQLite,解决恢复标签'无可用连接信息'问题
- database-service.ts: updateSession() 补充 private_key_path 字段持久化
- SessionDialog.tsx: 默认 username='root'、privateKey='~/.ssh/id_rsa'
UI/UX:
- 语义化主题系统(app-theme.ts),强制深色模式,全局无边框设计
- macOS 标题栏支持双击最大化(window:toggleMaximize IPC)
- macOS 菜单栏('OpsXShell' 应用菜单 + Settings Cmd+,)
- WelcomeView 4卡片布局,移除 Settings 按钮
- TabBar/TabItem 背景色统一(bg-dark-800/900),消除白色边框线
架构优化:
- SFTP 生命周期修复(useSFTP.ts useRef + autoConnectAttempted guard)
- 连接池竞态修复,支持快速多标签连接
- IPC 接口规范化,安全错误处理
- Vite 代码分割优化
质量: lint零错误 | tsc零错误 | build成功 | 49 tests pass
92 lines
2.7 KiB
TypeScript
92 lines
2.7 KiB
TypeScript
import { app, safeStorage } from 'electron'
|
|
import path from 'path'
|
|
import fs from 'fs'
|
|
import crypto from 'crypto'
|
|
import { createLogger } from '../utils/logger'
|
|
|
|
const logger = createLogger('KeyService')
|
|
|
|
const KEY_FILE = 'encryption.key.enc'
|
|
const LEGACY_KEY_FILE = 'encryption.key'
|
|
|
|
export class KeyService {
|
|
private key: string | null = null
|
|
|
|
/**
|
|
* 获取或创建主密钥
|
|
* 使用 Electron safeStorage 加密存储,替代明文存储
|
|
*/
|
|
getMasterKey(): string {
|
|
if (this.key) return this.key
|
|
|
|
const userDataPath = app.getPath('userData')
|
|
const keyPath = path.join(userDataPath, KEY_FILE)
|
|
const legacyKeyPath = path.join(userDataPath, LEGACY_KEY_FILE)
|
|
|
|
// 1. 尝试读取新的加密格式密钥
|
|
if (fs.existsSync(keyPath)) {
|
|
try {
|
|
const encrypted = fs.readFileSync(keyPath)
|
|
this.key = safeStorage.decryptString(encrypted)
|
|
logger.info('Master key loaded from encrypted storage')
|
|
return this.key
|
|
} catch (error) {
|
|
logger.error('Failed to decrypt master key, generating new one:', error)
|
|
}
|
|
}
|
|
|
|
// 2. 迁移旧的明文密钥(向后兼容)
|
|
if (fs.existsSync(legacyKeyPath)) {
|
|
try {
|
|
this.key = fs.readFileSync(legacyKeyPath, 'utf8').trim()
|
|
// 迁移到加密存储
|
|
this.saveEncryptedKey(this.key)
|
|
// 安全删除旧文件(覆盖后删除)
|
|
this.secureDelete(legacyKeyPath)
|
|
logger.info('Migrated master key from plaintext to encrypted storage')
|
|
return this.key
|
|
} catch (error) {
|
|
logger.error('Failed to migrate legacy key:', error)
|
|
}
|
|
}
|
|
|
|
// 3. 生成新密钥
|
|
this.key = crypto.randomBytes(32).toString('hex')
|
|
this.saveEncryptedKey(this.key)
|
|
logger.info('Generated new master key with encrypted storage')
|
|
return this.key
|
|
}
|
|
|
|
// 使用 safeStorage 加密保存密钥
|
|
private saveEncryptedKey(key: string): void {
|
|
const keyPath = path.join(app.getPath('userData'), KEY_FILE)
|
|
const encrypted = safeStorage.encryptString(key)
|
|
fs.writeFileSync(keyPath, encrypted, { mode: 0o600 })
|
|
}
|
|
|
|
// 安全删除文件(先覆盖再删除)
|
|
private secureDelete(filePath: string): void {
|
|
try {
|
|
const stats = fs.statSync(filePath)
|
|
const size = stats.size
|
|
// 用随机数据覆盖
|
|
const randomData = crypto.randomBytes(size)
|
|
fs.writeFileSync(filePath, randomData)
|
|
// 再用零覆盖
|
|
fs.writeFileSync(filePath, Buffer.alloc(size, 0))
|
|
// 最后删除
|
|
fs.unlinkSync(filePath)
|
|
} catch (error) {
|
|
logger.error('Failed to securely delete file:', error)
|
|
}
|
|
}
|
|
|
|
// 清除密钥(登出时)
|
|
clearKey(): void {
|
|
this.key = null
|
|
}
|
|
}
|
|
|
|
// 全局单例
|
|
export const keyService = new KeyService()
|