按 Claude Design 定稿(归档在 docs/design/)重做 UI,并把 tmux 从 「从不自动进」修成「连接前探测 → 让用户选会话」。 设计令牌与导航地基 - Theme 拆三层:TXAccent(恒定 blurple) / TXChrome(中性阶×4 表) / TXFlavor(Catppuccin 终端) - vendor JetBrains Mono 四权重(附 OFL 许可) - AppRouter + SessionManager:多会话并存,路由与会话生命周期解耦 - SessionCanvas 常驻挂载会话 surface(摘除即丢内容,也是缩回动画的前提) 按设计稿落地的屏 - 沉浸轨道页:56pt 轨道 + 浮起标题/状态胶囊 + 侧边栏三态(遮罩不 resize、Pin 各一次) - 首页:活动会话卡(readViewportText 文本镜像)+ 主机网格 + 筛选 chips - 关闭二次确认(tmux 仅断开 / 原生窗口两套文案)、分屏菜单、pane 拖拽条 - 空状态:首次运行 / 无会话 / 搜索无命中 / 连接中·失败·已关闭 tmux 真实链路(真机查出并修掉 4 个 bug) - 全代码库从来没人发起 attach → 连接前探测 + 会话选择器(接回 / 新建 / 原生终端) - format 分隔符 tab 经 PTY 变成下划线 → 改用 |:| - controller 变化不冒泡到 session → Combine 转发(否则数据解析对了 UI 不刷新) - 当前会话名不能靠 session_attached 反推 → 改用 display-message 传输层:SSH connect 加超时(原来阻塞到系统 TCP 超时 75s+) 无头验证设施:假会话 fixture · terminalx://ui/* 驱动 · 横屏截图脚本 · 对拍走查法 TXCore 45 tests 绿(新增 5 个会话探测单测) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
4.0 KiB
Swift
85 lines
4.0 KiB
Swift
import Foundation
|
||
import TXTransport
|
||
|
||
/// 凭据 ↔ SSH 认证 的桥。三条各自很薄的路径:
|
||
/// - 密码 → `.password`
|
||
/// - 生成的软件 P-256 → 重建 SecKey → `SSHSigner` → `.publicKeyCallback`(路径 A)
|
||
/// - 粘贴/导入 的 PEM/PKCS#8 → `.privateKey`(libssh2 `publickey_frommemory`,路径 B)
|
||
///
|
||
/// 不自造私钥格式解析器:导入的私钥直接透传 libssh2/mbedTLS。只做**轻量头部嗅探**推断类型/加密/警告。
|
||
enum CredentialAuth {
|
||
|
||
// MARK: - 生成
|
||
|
||
/// 生成一把软件 P-256 私钥 → 返回可存入 CredentialStore 的 (kind, secret)。
|
||
static func generateECDSAP256() -> (kind: Credential.Kind, secret: CredentialSecret)? {
|
||
guard let (provider, x963) = SigningKeyProvider.generateSoftwareP256() else { return nil }
|
||
let kind = Credential.Kind.privateKey(
|
||
keyType: .ecdsaP256, source: .generated, hasPassphrase: false,
|
||
publicKeyBlob: Data(provider.publicKeyBlob))
|
||
return (kind, .rawECPrivateKey(x963: x963))
|
||
}
|
||
|
||
// MARK: - 导入 / 粘贴
|
||
|
||
struct ParsedKey {
|
||
var kind: Credential.Kind
|
||
var secret: CredentialSecret
|
||
/// 人类可读的格式标签(UI 实时提示,如 "PEM · EC 私钥")。
|
||
var formatLabel: String
|
||
/// 非致命警告(如 openssh-key-v1 mbedTLS 不认)。nil = 无。
|
||
var warning: String?
|
||
}
|
||
|
||
/// 嗅探私钥文本头部,产出凭据 kind + 机密(路径 B)。识别不了也照存(交由 libssh2 定夺)。
|
||
static func parsePrivateKey(_ text: String, source: Credential.Source) -> ParsedKey {
|
||
let t = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
let encrypted = t.contains("Proc-Type: 4,ENCRYPTED")
|
||
|| t.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----")
|
||
|
||
var keyType: Credential.KeyType = .unknown
|
||
var label = "SSH 私钥"
|
||
var warning: String?
|
||
|
||
if t.contains("-----BEGIN OPENSSH PRIVATE KEY-----") {
|
||
label = "OpenSSH 新格式"
|
||
warning = "mbedTLS 不识别 openssh-key-v1,可能认证失败。建议先 `ssh-keygen -p -m PEM` 转为 PEM。"
|
||
} else if t.contains("-----BEGIN RSA PRIVATE KEY-----") {
|
||
keyType = .rsa; label = "PEM · RSA (PKCS#1)"
|
||
} else if t.contains("-----BEGIN EC PRIVATE KEY-----") {
|
||
keyType = .ecdsaP256; label = "PEM · EC (SEC1)"
|
||
} else if t.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----") {
|
||
label = "PKCS#8(加密)"
|
||
} else if t.contains("-----BEGIN PRIVATE KEY-----") {
|
||
label = "PKCS#8"
|
||
} else {
|
||
warning = "未识别到 PEM 私钥头,请确认粘贴的是私钥内容。"
|
||
}
|
||
|
||
let kind = Credential.Kind.privateKey(
|
||
keyType: keyType, source: source, hasPassphrase: encrypted, publicKeyBlob: Data())
|
||
let secret = CredentialSecret.pemPrivateKey(pem: Data(text.utf8), passphrase: nil)
|
||
return ParsedKey(kind: kind, secret: secret, formatLabel: label, warning: warning)
|
||
}
|
||
|
||
// MARK: - 构造 Authentication
|
||
|
||
/// 由凭据 meta + 机密构造 SSH 认证。返回 nil = 无法用于连接(如 SE 未签名建不出、机密缺失)。
|
||
/// `passphraseOverride` 供连接时按需补输加密私钥口令。
|
||
static func authentication(for cred: Credential, secret: CredentialSecret,
|
||
passphraseOverride: String? = nil) -> SSHConfig.Authentication? {
|
||
switch secret {
|
||
case .password(let pw):
|
||
return .password(pw)
|
||
case .pemPrivateKey(let pem, let storedPass):
|
||
let pem = String(decoding: pem, as: UTF8.self)
|
||
return .privateKey(pem: pem, passphrase: passphraseOverride ?? storedPass)
|
||
case .rawECPrivateKey(let x963):
|
||
guard let signer = SigningKeyProvider.from(x963: x963) else { return nil }
|
||
return .publicKeyCallback(signer)
|
||
case .none:
|
||
return nil // Secure Enclave 占位:未签名无法签名
|
||
}
|
||
}
|
||
}
|