v2.0.0: 主题系统+SSH私钥解析修复+会话持久化+连接池优化+UI打磨
核心修复:
- 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
This commit is contained in:
1 parent
37e65be823
commit
514d62644e
77 files changed
+2563
-931
No files matched your search
+100
-15
@@ -1,15 +1,31 @@
|
||||
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 _TAG_LENGTH = 16
|
||||
const SALT_LENGTH = 32
|
||||
const ITERATIONS = 100000
|
||||
// OWASP 2023 建议至少 600,000 次(针对 SHA256)
|
||||
const ITERATIONS = 600000
|
||||
const CRYPTO_VERSION = 'v1'
|
||||
|
||||
// 从密码派生密钥
|
||||
function deriveKey(password: string, salt: Buffer): Buffer {
|
||||
return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha256')
|
||||
// 异步 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')
|
||||
}
|
||||
|
||||
// 生成随机密钥(用于主密钥)
|
||||
@@ -17,18 +33,26 @@ 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 = deriveKey(password, salt)
|
||||
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()
|
||||
|
||||
// 格式: salt:iv:tag:encrypted (全部 hex)
|
||||
// 立即清零密钥内存
|
||||
key.fill(0)
|
||||
|
||||
// 格式: version:salt:iv:tag:encrypted (全部 hex)
|
||||
return [
|
||||
CRYPTO_VERSION,
|
||||
salt.toString('hex'),
|
||||
iv.toString('hex'),
|
||||
tag.toString('hex'),
|
||||
@@ -36,10 +60,50 @@ export function encrypt(plaintext: string, password: string): string {
|
||||
].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 [saltHex, ivHex, tagHex, encryptedHex] = ciphertext.split(':')
|
||||
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')
|
||||
@@ -50,22 +114,32 @@ export function decrypt(ciphertext: string, password: string): string {
|
||||
const tag = Buffer.from(tagHex, 'hex')
|
||||
const encrypted = Buffer.from(encryptedHex, 'hex')
|
||||
|
||||
const key = deriveKey(password, salt)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 简单加密(不带 salt,用于可逆加密)
|
||||
/**
|
||||
* 简单加密(使用主密钥,不经过 PBKDF2)
|
||||
* 用于内部数据加密,密钥长度必须正确
|
||||
*/
|
||||
export function simpleEncrypt(plaintext: string, key: string): string {
|
||||
const iv = crypto.randomBytes(IV_LENGTH)
|
||||
const keyBuffer = Buffer.from(key, 'hex').slice(0, KEY_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()])
|
||||
@@ -74,15 +148,26 @@ export function simpleEncrypt(plaintext: string, key: string): string {
|
||||
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').slice(0, KEY_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 decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv)
|
||||
decipher.setAuthTag(tag)
|
||||
|
||||
Reference in new issue
Block a user