Entrixy — 端到端加密

零知识架构:除了技术标识符,服务器不掌握任何解密密钥。所有有意义的字段——电话号码、 地址、坐标、Webhook 密钥、Wi-Fi BSSID、问候语——都在主人的设备上加密,在服务器上只是 一团不透明的数据。解密密钥随邀请链接送达访客,藏在地址的片段部分,而浏览器从不把它 发给服务器。

于 2026-04-19 定案。工作分阶段推进——见“阶段”一节。

1. 目标与边界

2. 密码学原语

Algorithm: AES-256-GCM。JVM 上开箱可用(通过 javax.crypto),并受 Android Keystore 支持。它一次性提供保密性与认证。

密文格式

v1:<base64url(nonce || ciphertext || tag)>

熵源

SecureRandom() (系统提供的)。在 Android 上就是 /dev/urandom + Linux 的 prng。

3. 密钥层级

K_master——主人的主密钥

K_obj——对象密钥

K_guest——访客捆绑包密钥

Changing K_guest 只能通过签发新链接来实现。吊销正是建立在这一点上: 服务器删除密钥记录,访客便不再收到最新的 bundle_cipher。

4. 什么存在哪里

在主人的设备上(EncryptedSharedPreferences)

Key取值
master_key32 个明文字节(SharedPreferences 本来就会加密)。
obj_key_{id}对象的 K_obj,用 K_master 包裹。
obj_plain_{id}(可选)解密后 JSON 的缓存,让界面绘制得更快。

在服务器上

表 / 字段目录
numbers.data_cipher用 K_obj 加密的 JSON: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}.
numbers.id / type明文——路由和显示图标类型都需要。
keys.bundle_cipher用 K_guest 加密的 JSON: {obj_ids: [1,2,3], obj_keys: {1: K_obj1, 2: K_obj2, ...}, welcome_cipher: ..., ...}.
keys.user_key / mode / force_when_busy明文——用于路由和快速的访问校验。
给访客的问候语在捆绑包内单独加密,因此服务器同样看不到。
https://entrixy.com/key#<user_key(32)><K_guest_base64url(43)>

6. 主要流程

创建对象

  1. 主人按下“添加”——于是生成 K_obj.
  2. 字段被打包成 JSON,并用 K_objdata_cipher.
  3. POST /api/number_add ,带 data_cipher 加上类型。服务器返回一个 id。
  4. 本地保存: obj_key_{id} = encrypt_k_master(K_obj).

更新对象

在本地构建新的 JSON,用已有的 K_objPOST /api/number_update. K_obj 加密,该密钥不变。

创建访客密钥

  1. 主人选择了 [obj1, obj2, obj3]。于是生成 K_guest.
  2. 组装捆绑包的 JSON: {obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.
  3. 用以下密钥加密: K_guestbundle_cipher.
  4. POST /api/key_create ,带 bundle_cipher, obj_ids (明文——用于访问矩阵)。
  5. 服务器返回 user_key.
  6. 主人会看到带链接的二维码 entrixy.com/key#{user_key}{base64url(K_guest)}.

访客接收密钥

  1. 访客打开二维码或链接。页面 /key reads location.hash ,并把密钥交给应用或网页版。
  2. 客户端请求 POST /api/key_bundle.php ,带 user_key → 得到 bundle_cipher 以及一份 obj_ids.
  3. 用以下密钥解密捆绑包: K_guest→ 得到 {obj_keys, welcome}.
  4. K_guest 以及解密后的 obj_keys 存入 EncryptedSharedPreferences 访客客户端的存储中。
  5. 此后每次调用:GET 加密的 data_cipher 对象数据 → 用本地的 obj_key_{id}.

吊销访客密钥

主人按下“删除” → POST /api/key_revoke.php → 服务器删除密钥记录。 访客下次请求捆绑包时会被拒绝,从而失去访问权。

轮换对象密钥

主人在对象设置里按下“重新生成密钥”。于是生成 K_obj_new, 并重新加密所有包含该对象的 bundle_cipher。客户端并不凭空知道它们, 因此主人的客户端会重建捆绑包:取回自己每把密钥的 bundle_cipher,解密,放入 K_obj_new,再重新加密。 新的数据块一批发往服务器。

7. 备份与恢复

本节描述的是 一个尚未实现的计划:应用中目前没有主密钥导出功能。

Losing K_master 意味着失去对所有对象和密钥的访问,因此导出必不可少。

没有口令短语,备份二维码就只是 K_master,因此口令短语是必须的。

8. 推进阶段

第 1 阶段——客户端的加密仓库

Add Crypto.kt:AES-256-GCM 包装层,生成并保存 K_master, 以及用于 K_obj的辅助函数,格式 v1:<base64url>的序列化。 单元测试:加密 → 解密 → 比对。 目前还没有任何东西发往服务器——只是把管道铺好了。

第 2 阶段——服务端表结构迁移(已完成)

所有敏感字段都迁入了 numbers.data_cipheruser_keys.bundle_cipher。 旧的明文列(phone, label, radius, time_*, geo_*, wifi_*, share_*, has_avatar, security_level, user_keys.label, hosts.label, pending_actions.phone, hosts.last_ip)已从数据库中删除(第 7 阶段)。 webhook_urlwebhook_secret 仅在以下情况下保持明文: webhook_mode='server' ——没有它们服务器就发不出 HTTP 请求。而在 webhook_mode='phone' 的情况下,它们会被加密进 data_cipher.

第 3 阶段——客户端读写数据块

创建或更新时,客户端加密 JSON 并发送 data_cipher。 同步时读取数据块并解密。新对象完全存活在数据块中;没有 K_obj 的旧对象 出于向后兼容仍继续使用明文字段。

第 4 阶段——访客捆绑包

创建密钥时生成 K_guest,组装捆绑包并发送 bundle_cipher到服务器。 链接把密钥带在片段里,而页面 /key 读取哈希并把密钥交给应用。

第 5 阶段——备份与恢复

设置界面加上“导出主密钥”:口令短语 → Argon2id → 二维码。 通过扫码恢复,并测试在设备之间迁移。 尚未实现。

第 6 阶段——迁移既有数据

客户端在新版本首次启动时执行的一次性流程:

  1. 从服务器读取自己的全部对象(旧的明文字段)。
  2. 为它们各自生成一个 K_obj
  3. 加密字段并发送 data_cipher.
  4. 服务器清空旧列(phone=NULL, …)——但仅在确认之后。
迁移成功后,用 ALTER 删除废弃字段。

第 7 阶段——审计

安全审计,外部或自查。我们核实服务器确实看不到敏感数据: 数据库转储里除了数据块不应有别的东西。

9. 风险与对策

RiskMeasure
K_master 丢失强制性的备份流程(第 5 阶段)。在没有副本之前,会显示警告横幅。
主人设备被攻破无法完全避免:应用入口由 PIN 或生物识别把守,数据交给 EncryptedSharedPreferences 与 Android Keystore。
轮换对象密钥需要重建全部捆绑包主人的客户端取来全部 bundle_cipher(本地或来自服务器),重建后一批发出。这是罕见操作。
服务器篡改捆绑包AES-GCM 标签对不上,客户端会报告密钥已损坏。
K_guest 因二维码截图而泄露技术上无法防范:密钥本身就在码里。只把二维码给你信任的访客看。
地址片段留在浏览器历史里页面 /key 在读取哈希后立即调用 history.replaceState(..., '#'),从而抹掉片段。

最后更新:2026 年 4 月 19 日