BoshConnect:从 ConnectBot 到全功能 SSH/SFTP 客户端的开发实战

基于开源项目 ConnectBot,用 Kotlin + Jetpack Compose 从零打造一款支持加密备份、云同步、GitHub 备份的 Android SSH/SFTP 客户端。本文记录整个开发过程中的架构设计、踩坑经验和技术决策。

为什么要做这个项目

作为一个经常管理多台 VPS 的开发者,我需要一个好用的 Android SSH 客户端。现有的方案各有问题:

  • Termius:功能强大但收费,免费版限制多
  • JuiceSSH:界面陈旧,Compose 时代的产品长那样说不过去
  • ConnectBot:开源老牌,但 UI 还停留在 Android 5 时代

于是我决定:基于 ConnectBot 的 SSH 内核,用现代 Android 技术栈重新打造一个。

项目取名 BoshConnect,开源在 GitHub

技术栈选型

层级选择理由
语言KotlinAndroid 官方推荐,类型安全
UIJetpack Compose + Material 3声明式 UI,开发效率高
架构MVVM + StateFlowGoogle 推荐架构,响应式
DIHilt标准 Android DI,与 Compose 集成好
数据库Room类型安全的 SQLite 封装
SSHConnectBot sshlib成熟稳定的 SSH 实现
SFTPJSch老牌 Java SSH 库
加密PBKDF2 + AES-256-GCM业界标准,无需额外依赖

架构设计

整个项目分为四层:

1
2
3
4
5
6
7
UI Layer (Compose Screens + ViewModels)
Domain Layer (SettingsManager · CloudSyncApi · CryptoManager)
Data Layer (Room DB · ConnectBot Service · SSH Transport)
Crypto Layer (SessionKeyHolder → FieldCrypto → AES-256-GCM)

关键决策:去掉登录页

最初版本有主密码登录页,用户需要输入密码才能进入。但经过实际使用发现:

  1. 手机本身有锁屏,App 再加一层密码是多余的安全感
  2. 多端同步时密码管理复杂,需要 salt 同步、密钥派生一致
  3. 用户反馈:每次打开都要输密码太麻烦

最终方案:

  • 设备密钥自动生成:随机 256-bit 密钥,持久化存储,用户无感知
  • 备份密码独立:只在备份/恢复时使用,与设备密钥无关
1
2
3
4
// 自动生成设备密钥
val deviceKey = ByteArray(32).also { SecureRandom().nextBytes(it) }
cryptoManager.saveDeviceKey(deviceKey)
SessionKeyHolder.set(deviceKey)

密钥体系设计

1
2
3
设备密钥 (自动生成) → 加密数据库字段 (密码/私钥)
备份密码 (用户设置) → 加密备份文件 + GitHub 备份
云同步密码 (服务器账号) → 加密云端数据

三个密钥完全独立,互不影响。这样做的好处:

  • 本地数据自动加密,用户无需操作
  • 备份文件用独立密码保护,跨设备恢复只需密码
  • 云同步用服务器账号密码,天然支持多端

功能实现

1. SSH 终端

保留了 ConnectBot 的终端内核,但 UI 完全重写:

  • 快捷键栏常驻(Ctrl/Esc/Tab/方向键/功能键)
  • Compose 实现,支持深色模式
  • 光标自动避让手机键盘

2. SFTP 文件管理

用 JSch 的 ChannelSftp 直接实现,不依赖 ConnectBot 的旧代码:

  • 远程目录浏览(面包屑导航)
  • 上传/下载(进度条 + 取消)
  • 删除/重命名/权限修改
  • Android 10+ 分区存储兼容

3. 加密备份

备份文件格式(BackupEnvelope):

1
2
3
4
5
6
7
{
  "format": "boshconnect_backup_v1",
  "encrypted": true,
  "payload": "AES-GCM 加密的 JSON",
  "salt": "base64 编码的 PBKDF2 salt",
  "createdAt": 1743638400000
}

关键设计:备份文件内嵌 salt。这样任何设备只要有备份密码,就能派生相同的密钥来解密。

4. GitHub 备份

这是最让我满意的功能。原理很简单:

1
2
3
4
5
6
7
8
// 备份
PUT https://api.github.com/repos/{owner}/{repo}/contents/boshconnect/backup_20260403.enc
Authorization: token {PAT}
Body: { message, content: base64(encrypted_data) }

// 恢复
GET https://api.github.com/repos/{owner}/{repo}/contents/boshconnect/
 下载最新文件  输入备份密码  解密  写入本地

优势:

  • 零服务器维护
  • GitHub 免费私有仓库
  • 天然版本控制(每次备份都是一个 commit)
  • 安全性高

5. 云同步(SbSSH Server)

自建 FastAPI 服务端,端到端加密:

  • 注册/登录 → JWT 认证
  • 数据加密后上传,服务端只存密文
  • Smart Sync:增量合并,支持保留或删除云端多余数据

踩坑记录

1. Kotlin 版本兼容

ConnectBot 的 termlib 库编译用了 Kotlin 2.3.0,但项目用的是 2.0.21。直接导致:

1
2
Module was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.3.0, expected version is 2.0.0.

解决:升级 Kotlin 到 2.3.0,但又遇到 kotlinOptions DSL 被废弃的问题,需要迁移到 compilerOptions。最终回退到 2.0.21,在 termlib 依赖上排除 Kotlin stdlib。

2. Google Maven TLS 握手失败

服务器通过代理访问 dl.google.com 时 TLS 握手失败。代理不支持 TLSv1.2/1.3。

解决:所有 build.gradle 加阿里云 Maven 镜像。但 npx cap sync 会覆盖 capacitor-cordova-android-plugins/build.gradle,每次 sync 后要重新加。

3. 多端同步密钥不一致

早期版本,每个设备生成自己的 salt,导致不同设备派生出不同的密钥。设备 A 加密的数据,设备 B 解不开。

解决:登录时从服务器获取 salt(GET /api/v1/sync/salt),用服务器的 salt 派生密钥。所有设备用同一个 salt → 同一个密钥。

4. Android 14 图标显示问题

Android 8+ 需要 Adaptive Icon,但 XML 矢量图和 PNG 的混合使用导致图标显示异常。

最终方案:用 Python PIL 生成 PNG 图标,通过 layer-list drawable 包装,作为 Adaptive Icon 的 foreground。

版本规划

版本内容状态
v1.0.0基础 SSH/SFTP + 加密 + 云同步✅ 已发布
v1.1.0独立备份密码 + GitHub 备份 + 去掉登录✅ 已发布

总结

这个项目的核心价值在于:

  1. 复用而不是重写:SSH 内核用 ConnectBot,省去了大量协议层工作
  2. 现代技术栈:Compose + Material 3 让 UI 开发效率提升 10 倍
  3. 安全优先:字段级加密、独立备份密码、端到端加密同步
  4. 用户友好:去掉登录页、GitHub 备份零配置

如果你也想自己管理 SSH 密钥,不想把数据交给第三方,可以试试 BoshConnect


本文由博客助手大龙虾整理。

热爱生活 学无止境
使用 Hugo 构建
主题 StackJimmy 设计