PlainApp 安全详解
PlainApp 如何在本地 Wi-Fi 上保护你的数据 —— 所有内容均可在开源代码中验证。
为什么本地网络安全很重要?
当你用一个 App 通过电脑浏览器管理 Android 手机时,你的短信、照片、通讯录和文件都在 Wi-Fi 网络上传输。大多数 App 把这些数据以明文发送,同一网络上的任何人都能看到。
你可能会想:"这是我家的 Wi-Fi,谁会偷看?" 但你想想这些场景:
- 公共 Wi-Fi — 咖啡馆、机场、酒店 —— 附近任何人都能抓包
- 共享网络 — 室友、客人或者被入侵的智能设备
- 路由器漏洞 — 过时的固件可能有已知的安全漏洞
如果一个 App 通过 HTTP 明文发送你的短信,任何人用免费工具(比如 Wireshark)就能读取。这不是理论风险 —— 操作非常简单。
大多数手机管理 App 就是这样做的。打开 Chrome 开发者工具(F12 → 网络标签),你可以看到每一个 API 调用的明文 JSON —— 短信内容、文件路径、联系人姓名,一览无余。
PlainApp 有什么不同
PlainApp 使用多层加密和认证机制。以下用通俗的语言解释每一层的作用。
GraphQL API —— 只获取需要的数据
大多数 App 使用老式的 REST API:一个资源对应一个 URL,不管你需不需要,整个对象全部返回。PlainApp 使用 GraphQL —— 一种现代化的方式,浏览器只请求它需要的字段,不多不少。
这为什么重要:
- 更少的数据暴露 — 如果你只需要文件名,就只传文件名
- 单一入口 — 只有一个 /graphql 地址,而不是几十个接口,减少了攻击面
- 按查询控制权限 — 每个查询单独检查权限(例如读取短信需要 READ_SMS 权限,即使你已经登录了也要单独验证)
加密的 API 调用(XChaCha20-Poly1305)
这是 PlainApp 和大多数竞品最大的区别。
每一个 API 请求和响应——包括 HTTP 和 WebSocket 流量——都使用 XChaCha20-Poly1305 加密,HTTP 和 HTTPS 模式均如此。这是 WireGuard VPN、Cloudflare 和 Google Tink 都在使用的现代 AEAD 加密算法。
简单来说:
- 1你的浏览器构建一个请求(比如"显示我的短信")
- 2附加一个时间戳和随机 nonce,用于防重放保护
- 3用 XChaCha20-Poly1305 将整个请求体加密成不可读的二进制数据
- 4加密后的二进制数据直接在网络上传输——不转 base64,不包 JSON,纯二进制
- 5只有你的手机能解密和验证
即使有人截获了流量,他们看到的只是无意义的二进制数据 —— 不是可读的 JSON。
加密细节:
- 256 位密钥(极强的安全性)
- 192 位随机数(防止某些影响其他加密算法的攻击类型)
- 内置篡改检测 —— 如果有人在传输中修改了数据,解密会失败
- Android 端:使用 Google Tink
- Web 端:使用 @noble/ciphers(经过审计的零依赖 JavaScript 加密库)
数字签名(Ed25519)—— 设备间通信
当两台 PlainApp 设备互相通信时(例如手机对手机聊天),仅加密还不够——每台设备需要验证消息确实来自对方,而非伪造的。
时间戳
接收端设备使用发送方的公钥验证签名,并拒绝时间戳超过 5 分钟的请求。这同时防止了伪造和重放攻击。
安全的设备配对(ECDH 密钥交换)
当两台 PlainApp 设备互相配对时(比如两部手机),它们需要一个共享的加密密钥。但把密钥通过网络发送就失去了意义。
PlainApp 用 ECDH(椭圆曲线 Diffie-Hellman)密钥交换来解决 —— 这和 TLS、Apple Secure Enclave、WebAuthn 使用的是同样的数学原理:
- 1每台设备生成一对密钥(公钥 + 私钥)
- 2他们只交换公钥
- 3每台设备用自己的私钥和对方的公钥进行计算
- 4双方得到同一个共享密钥 —— 但密钥本身从未在网络上传输过
URL 加密 —— 隐藏文件路径
很多人没注意到的一个细节:URL 路径也会泄露信息。
当你下载文件时,URL 告诉服务器你要哪个文件。在很多 App 里,URL 长这样:
/fs?path=/sdcard/DCIM/Camera/photo_2024.jpg任何抓包的人(或者查看浏览器历史记录的人)都能看到你访问了什么文件,以及你手机的文件夹结构。
PlainApp 对 URL 中的文件路径进行了加密:
/fs?id=AxK9f2mQ7vR3xP8nWz...id 参数是用 XChaCha20-Poly1305 加密后的文件路径,加密密钥称为 URL Token。默认情况下,这个密钥是永久性的,即使重启 App 也不会改变,因此分享过的链接始终有效。没有密钥,谁也猜不出真实路径,无法枚举你的文件,也无法伪造下载链接。
HTTPS 与自签名证书
PlainApp 在你的手机上生成一个自签名 TLS 证书,建立加密的 HTTPS 通道 —— 和银行、网站使用的技术一样。
证书参数:
- ECDSA P-256 曲线(和主流网站相同)
- SHA256 签名
- 存储在 App 的私有目录中
结合应用层的 XChaCha20 加密,这提供了两层独立的加密。攻破其中一层,没有另一层也无济于事。
登录保护
PlainApp 限制登录尝试次数,防止密码暴力破解:
- 每个 IP 地址,每 60 秒最多 5 次尝试
- 超过之后连接被拒绝
- 过期的限制记录会自动清理
HTTP 和 HTTPS 模式:实话实说
PlainApp 同时提供 HTTP 和 HTTPS 两种模式。以下是真实的区别:
| HTTP 模式 | HTTPS 模式 | |
|---|---|---|
| 文件路径 URL | 加密(XChaCha20) | 加密(XChaCha20) |
| API 调用 | 加密(XChaCha20) | 加密(XChaCha20 + TLS) |
| 文件下载 | 传输时不加密 | TLS 加密 |
| 适合谁 | 新手(不会看到浏览器 SSL 警告) | 注重隐私的用户 |
两种模式都用 XChaCha20-Poly1305 加密 API 调用 —— 你的短信、通讯录和其他数据无论 HTTP 还是 HTTPS 都是加密的。
HTTP 模式存在的原因:自签名证书会触发浏览器警告("您的连接不是私密连接")。对于不太懂技术的用户来说,这个警告很吓人。HTTP 模式避免了这个问题。
但 HTTP 模式不会加密文件传输。如果你在共享或公共网络上,请务必使用 HTTPS。
建议:尽量使用 HTTPS。浏览器提示的自签名证书警告是正常的,对于本地网络 App 来说是预期行为,点击继续访问即可。
供应链安全 —— 更少的依赖,更低的风险
很多用户没想到的一个安全威胁:供应链攻击。当一个 App 使用了几百个第三方库,其中任何一个都可能被攻击者污染 —— 注入窃取数据、挖矿或者开后门的恶意代码。
这在 JavaScript 生态中尤其危险。近年来,event-stream、ua-parser-js、colors 等流行的 npm 包被劫持,向数百万项目注入了恶意代码。如果一个手机管理 App 的 Web 界面引入了几百个 JS 包,每一个都是潜在的攻击入口。
PlainApp 的做法是:尽量少用依赖,能自己写的就自己写。
Android App(plain-app)
- 不使用依赖注入框架(没有 Hilt,没有 Dagger)—— 用简单的单例模式
- 加密只使用 3 个知名库:Google Tink、Bouncy Castle 和内置的 Java Cryptography Extension
- 服务器运行在 Ktor(JetBrains 官方 Kotlin 框架)上 —— 没有冷门的第三方服务器库
Web 界面(plain-desktop)
只有 28 个运行时依赖 —— 远少于典型的 Web 应用。好几个通常需要 npm 包的功能,PlainApp 选择了从零构建:
| 功能 | 普通 App 的做法 | PlainApp 的做法 |
|---|---|---|
| 提示音 | howler.js / tone.js | 自己用 Web Audio API 合成 |
| UUID 生成 | uuid npm 包 | 自己实现 RFC 4122 |
| 文件上传 | uppy / axios | 自己写分块上传和队列管理 |
| WebRTC 视频 | Twilio / Daily.co SDK | 自己实现 PeerConnection |
| HTTP 请求 | axios / ky | 原生浏览器 fetch |
Web 端的加密使用 @noble/ciphers —— 一个经过审计的、零依赖的 JavaScript 加密库,而不是引入庞大臃肿的替代品。
为什么这很重要
你添加的每个依赖,都是你无法控制的代码。PlainApp 的做法意味着:
- 恶意代码更难藏身
- 更小的攻击面
- 更容易审计(整个代码库都是开源的)
- 不会受到被遗弃或被劫持的 npm 包的影响
竞品的常见问题
这里不针对某个具体 App,而是这个品类中的常见模式:
| 安全方面 | PlainApp | 常见竞品做法 |
|---|---|---|
| API 流量 | 加密(XChaCha20),HTTP 和 HTTPS 都加密 | 明文 JSON over HTTP |
| WebSocket 流量 | 加密二进制帧(XChaCha20) | 明文 JSON 或不使用 WebSocket |
| 认证 Token 处理 | 用作加密密钥,从不放在请求头中 | 放在 HTTP 请求头中(容易泄露) |
| 文件 URL | 加密的路径 ID | 明文路径暴露 |
| 密钥交换 | ECDH (P-256) | 无 |
| 请求签名 | Ed25519(仅设备间通信) | 无 |
| 防重放保护 | 30 秒窗口 + nonce(Web);5 分钟 + 签名(设备间) | 无 |
| 登录保护 | 频率限制 + 双重确认 | 无限制 |
| 数据路由 | 100% 本地,不经过云端 | 部分经过云端 |
| 依赖数量 | 精简,使用知名库 | 数百个 npm 包 |
| 源代码 | 开源 (GPL-3.0) | 大多闭源 |
检验任何 App 最简单的方法:在使用 App 的时候打开 Chrome 开发者工具(F12)→ 网络标签。如果你能看到明文 JSON 格式的 API 响应,说明这个 App 没有加密。
自动化、可追溯、抗篡改的发布流水线
所有安装包均由 GitHub 与 F-Droid 基础设施自动构建。构建过程可追溯、安全,并且没有人工打包干预痕迹。PlainApp 是少数同时提供可验证构建(SLSA)与 VirusTotal 扫描发布的 Android 应用之一。
GitHub 与 F-Droid 自动构建
发布产物由 CI 流水线生成,而不是本地手工打包。每个产物都可以追溯到源代码提交与可复现构建流程。
SLSA Provenance(3 级)+ VirusTotal 扫描
每个发布都包含 SLSA 来源证明和 VirusTotal 扫描结果,使供应链可审计,并帮助用户在安装前验证完整性。
常见问题
使用 HTTP 模式时 PlainApp 真的加密了吗?
是的。所有 API 调用(短信、通讯录、文件列表等)在 HTTP 和 HTTPS 两种模式下都用 XChaCha20-Poly1305 加密。HTTP 模式只有文件下载没有加密。如果需要完全保护,请使用 HTTPS。
为什么浏览器会显示安全警告?
PlainApp 使用自签名 TLS 证书进行加密。因为不是公共证书机构签发的,浏览器会显示警告。这对于本地网络 App 是正常和预期的。点击「高级」然后「继续访问」即可。
同一 Wi-Fi 上的人能看到我的数据吗?
使用 PlainApp 的话,不能。API 调用使用 XChaCha20-Poly1305 加密,在 HTTPS 模式下文件传输也受 TLS 保护。但使用大多数竞品 App 的话,可以 —— 它们的 API 调用是明文 JSON。
PlainApp 和 VPN 相比怎么样?
VPN 加密的是你的设备和 VPN 服务器之间的流量。PlainApp 加密的是你的手机和浏览器在本地网络上的流量。它们用途不同 —— PlainApp 保护本地设备管理,VPN 保护互联网流量。
代码真的是开源的吗?
是的。PlainApp 采用 GPL-3.0 许可。完整源代码可以在 github.com/plainhub/plain-app 查看。你可以检查每一个加密实现,从源码编译,或者参与贡献。