核心修复:
- 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
181 lines
5.5 KiB
TypeScript
181 lines
5.5 KiB
TypeScript
import crypto from 'crypto'
|
||
import { promisify } from 'util'
|
||
|
||
const ALGORITHM = 'aes-256-gcm'
|
||
const KEY_LENGTH = 32
|
||
const IV_LENGTH = 16
|
||
const _TAG_LENGTH = 16
|
||
const SALT_LENGTH = 32
|
||
// OWASP 2023 建议至少 600,000 次(针对 SHA256)
|
||
const ITERATIONS = 600000
|
||
const CRYPTO_VERSION = 'v1'
|
||
|
||
// 异步 PBKDF2 避免阻塞事件循环
|
||
const pbkdf2Async = promisify(crypto.pbkdf2)
|
||
|
||
/**
|
||
* 从密码异步派生密钥
|
||
* 使用异步版本避免阻塞 Electron 主进程事件循环
|
||
*/
|
||
async function deriveKeyAsync(password: string, salt: Buffer): Promise<Buffer> {
|
||
return pbkdf2Async(password, salt, ITERATIONS, KEY_LENGTH, 'sha512')
|
||
}
|
||
|
||
/**
|
||
* 从密码同步派生密钥(仅在启动等必要时使用)
|
||
*/
|
||
function deriveKeySync(password: string, salt: Buffer): Buffer {
|
||
return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha512')
|
||
}
|
||
|
||
// 生成随机密钥(用于主密钥)
|
||
export function generateMasterKey(): string {
|
||
return crypto.randomBytes(KEY_LENGTH).toString('hex')
|
||
}
|
||
|
||
/**
|
||
* 加密数据
|
||
* 格式: version:salt:iv:tag:encrypted (全部 hex)
|
||
* version 用于未来加密方案升级时的安全迁移
|
||
*/
|
||
export function encrypt(plaintext: string, password: string): string {
|
||
const salt = crypto.randomBytes(SALT_LENGTH)
|
||
const key = deriveKeySync(password, salt)
|
||
const iv = crypto.randomBytes(IV_LENGTH)
|
||
|
||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
|
||
const tag = cipher.getAuthTag()
|
||
|
||
// 立即清零密钥内存
|
||
key.fill(0)
|
||
|
||
// 格式: version:salt:iv:tag:encrypted (全部 hex)
|
||
return [
|
||
CRYPTO_VERSION,
|
||
salt.toString('hex'),
|
||
iv.toString('hex'),
|
||
tag.toString('hex'),
|
||
encrypted.toString('hex'),
|
||
].join(':')
|
||
}
|
||
|
||
/**
|
||
* 异步加密(推荐,不阻塞事件循环)
|
||
*/
|
||
export async function encryptAsync(plaintext: string, password: string): Promise<string> {
|
||
const salt = crypto.randomBytes(SALT_LENGTH)
|
||
const key = await deriveKeyAsync(password, salt)
|
||
const iv = crypto.randomBytes(IV_LENGTH)
|
||
|
||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
|
||
const tag = cipher.getAuthTag()
|
||
|
||
// 立即清零密钥内存
|
||
key.fill(0)
|
||
|
||
return [
|
||
CRYPTO_VERSION,
|
||
salt.toString('hex'),
|
||
iv.toString('hex'),
|
||
tag.toString('hex'),
|
||
encrypted.toString('hex'),
|
||
].join(':')
|
||
}
|
||
|
||
/**
|
||
* 解密数据
|
||
* 支持带版本标识的新格式和不带版本标识的旧格式(向后兼容)
|
||
*/
|
||
export function decrypt(ciphertext: string, password: string): string {
|
||
try {
|
||
const parts = ciphertext.split(':')
|
||
|
||
// 判断格式:新格式有5部分(含version),旧格式有4部分
|
||
let saltHex: string, ivHex: string, tagHex: string, encryptedHex: string
|
||
|
||
if (parts.length === 5) {
|
||
// 新格式: version:salt:iv:tag:encrypted
|
||
[, saltHex, ivHex, tagHex, encryptedHex] = parts
|
||
} else if (parts.length === 4) {
|
||
// 旧格式: salt:iv:tag:encrypted(向后兼容)
|
||
[saltHex, ivHex, tagHex, encryptedHex] = parts
|
||
} else {
|
||
throw new Error('Invalid ciphertext format')
|
||
}
|
||
|
||
if (!saltHex || !ivHex || !tagHex || !encryptedHex) {
|
||
throw new Error('Invalid ciphertext format')
|
||
}
|
||
|
||
const salt = Buffer.from(saltHex, 'hex')
|
||
const iv = Buffer.from(ivHex, 'hex')
|
||
const tag = Buffer.from(tagHex, 'hex')
|
||
const encrypted = Buffer.from(encryptedHex, 'hex')
|
||
|
||
const key = deriveKeySync(password, salt)
|
||
|
||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
|
||
decipher.setAuthTag(tag)
|
||
|
||
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()])
|
||
// 清零密钥内存
|
||
key.fill(0)
|
||
return decrypted.toString('utf8')
|
||
} catch (error) {
|
||
throw new Error('Decryption failed: ' + (error as Error).message)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 简单加密(使用主密钥,不经过 PBKDF2)
|
||
* 用于内部数据加密,密钥长度必须正确
|
||
*/
|
||
export function simpleEncrypt(plaintext: string, key: string): string {
|
||
const iv = crypto.randomBytes(IV_LENGTH)
|
||
const keyBuffer = Buffer.from(key, 'hex')
|
||
|
||
// 验证密钥长度
|
||
if (keyBuffer.length !== KEY_LENGTH) {
|
||
throw new Error(`Invalid key length: expected ${KEY_LENGTH} bytes, got ${keyBuffer.length}`)
|
||
}
|
||
|
||
const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv)
|
||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
|
||
const tag = cipher.getAuthTag()
|
||
|
||
return iv.toString('hex') + ':' + tag.toString('hex') + ':' + encrypted.toString('hex')
|
||
}
|
||
|
||
/**
|
||
* 简单解密
|
||
*/
|
||
export function simpleDecrypt(ciphertext: string, key: string): string {
|
||
try {
|
||
const [ivHex, tagHex, encryptedHex] = ciphertext.split(':')
|
||
|
||
if (!ivHex || !tagHex || !encryptedHex) {
|
||
throw new Error('Invalid ciphertext format')
|
||
}
|
||
|
||
const iv = Buffer.from(ivHex, 'hex')
|
||
const tag = Buffer.from(tagHex, 'hex')
|
||
const encrypted = Buffer.from(encryptedHex, 'hex')
|
||
const keyBuffer = Buffer.from(key, 'hex')
|
||
|
||
// 验证密钥长度
|
||
if (keyBuffer.length !== KEY_LENGTH) {
|
||
throw new Error(`Invalid key length: expected ${KEY_LENGTH} bytes, got ${keyBuffer.length}`)
|
||
}
|
||
|
||
const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv)
|
||
decipher.setAuthTag(tag)
|
||
|
||
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()])
|
||
return decrypted.toString('utf8')
|
||
} catch (error) {
|
||
throw new Error('Decryption failed: ' + (error as Error).message)
|
||
}
|
||
}
|