Files
opsxshell/electron/services/crypto.ts
T
Logan.Li 514d62644e 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
2026-04-22 11:15:41 +08:00

181 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}