phix

PHIX 技术文档

关于我们 产品 文档 PHIX 日志 下载 登录·注册

技术文档

以下是 phix 系统的公开技术架构说明。所有敏感细节(密钥、令牌实例)均已脱敏。

概览

phix 是一套把「心履」「Pinghe Launcher」「Pinghe Launcher Lite」打通的统一账号系统:一套用户名 + 密码登录,端到端加密地跨设备同步日程、选课、账号、课表、心情记录等数据。

  • 服务端只做三件事:管账号、发令牌、存不透明密文对象。
  • 所有数据在客户端本地加密后上传,服务端只保存密文,没有你的口令与密钥就看不懂内容。
  • 本页是 phix 的公开技术说明,所有密钥与令牌细节均已脱敏。

服务端没有任何密码学依赖,看不懂用户上传的内容;它只存「密文信封 + 令牌 + 密钥包裹」。

支持的平台

三个产品共用同一套账号与数据模型,登录同一账号即可跨设备同步:心履是四端(网页版 + Windows + macOS + Android),Pinghe Launcher 是桌面端(Windows / macOS 13+)加网页版,Pinghe Launcher Lite 是轻量桌面端。

平台定位技术栈 / 形态
心履心情记录 + 日程提醒四端:网页端(Django)、桌面端(JavaFX,Windows / macOS)、安卓端(Java + Room),跨设备同步
Pinghe Launcher日程与校园信息中枢桌面端(Electron,Windows / macOS 13+);网页版(/app/)只有日程 / EduPage / ManageBac / 邮箱四个入口,不做真实收发
Pinghe Launcher Lite轻量日程助手桌面端(Python);本地独立可用,登录 phix 账号后可选开启云同步

五分钟上手

  1. 到「下载」页选择对应平台的客户端并安装。
  2. 首次启动选择「有 phix 账号」或「没有」。
  3. 有账号 → 直接登录;没有 → 注册,注册时记下一次性展示的恢复码。
  4. 登录后自动拉一次云同步,数据在各设备之间同步。
  5. 换设备时用同一账号登录,即可恢复云端数据。

引导期间任何一步失败都不阻塞,可以选择「跳过 / 稍后再说」;离线也能正常使用,联网后自动补齐同步。

统一账号

一套用户名 + 密码,登录所有客户端与网页端;各产品不各自存密码。

  • 登录时在本地派生出登录凭证交给 phix 校验,通过即建立或关联本地账号。
  • 用户名只允许字母 / 数字 / 下划线 / 点 / @ / + / - / 中文,最长 150 字符。
  • 服务端存的是派生后的登录凭证(AuthHash),不是口令本身。

服务端只保存密文,你的口令与密钥不上传——它只收到派生出的 AuthHash,反推不出口令。

AuthHash 与口令派生

客户端在本地派生登录凭证,只把 AuthHash 发给服务器。两代 KDF 都支持:

  • v1scrypt-n15-r8-p1(scrypt N=2^15、r=8、p=1),由口令直接派生 KEK,服务端比对口令原文的兼容路径。
  • v2scrypt-hkdf-v2 —— MK = scrypt(口令, salt),再由 HKDF 派生:HKDF("auth") 得 AuthHash(发服务器、用于登录校验),HKDF("enc") 得 KEK(永不出客户端)。
  • 两条盐链分开:auth_salt 派生 AuthHash(注册后不变);kdf_salt 派生 KEK(改密码时会变)。不可混用
v2: 口令 --scrypt--> MK ──HKDF("auth")──> AuthHash(发服务器,登录校验)
                     └──HKDF("enc")──> KEK(永不出客户端,本地解包 DEK)

HKDF 是单向的:拿到 AuthHash 反推不出口令、也算不出 KEK——即使服务器被入侵并记下登录时收到的一切,也解不开你的云端数据。

旧账号迁移

老用户用原用户名 + 原密码登录,官网 / 心履在本地与旧哈希比对:

  • 比对通过 → 用该口令在 phix 建号并关联,数据无缝迁移;口令明文从不发给 phix(只发本地派生的 AuthHash)。
  • phix 不可达 → 不迁移,退回本地账号校验,不影响离线使用。
  • 新账号默认 scrypt-hkdf-v2,服务端永不见口令;老账号(v1)走 scrypt-n15-r8-p1 兼容路径。

迁移采用「认证委托」:不改数据、不迁库。

修改密码

  • 换密码只需在客户端用旧口令解包 DEK、再用新口令重新包裹(替换 key_wrap),云端密文一个字节都不用动。
  • 换登录密码必须同时换 AuthHash(登录凭证);切同步口令时 AuthHash 不变。
  • 注册时生成 24 位恢复码(base32),一次性展示;忘记口令时用恢复码解包 DEK 重设口令。

换密码 ≠ 重新加密数据:只更换「包裹 DEK 的那把钥匙」,密文不动。

三层密钥

phix 采用三层密钥架构,确保换密码时云端数据无需重新加密:

口令 →(scrypt/HKDF)→ KEK →(解开)→ DEK →(HKDF)→ 对象密钥 →(AES-GCM)→ 密文信封
  • DEK(Data Encryption Key):随机生成的 32 字节根密钥,是所有数据的根。
  • KEK(Key Encryption Key):由口令派生而来(派生方式见「AuthHash 与口令派生」),专门用来包裹 DEK。
  • 对象密钥:由 DEK 经 HKDF 派生,每个同步对象有独立的对象密钥,绑定用户名与对象名。

服务端只保存密文,你的口令与密钥不上传;换密码只重新包裹 DEK,云端密文无需重新加密。

phix 端到端加密链路示意
端到端加密链路:口令经 scrypt 派生 KEK,KEK 解开 DEK,DEK 经 HKDF 派生每个对象独立的对象密钥,对象密钥以 AES-256-GCM 加密出 PHIX1.<nonce>.<密文> 信封上传;服务端只保存密文。AAD 把用户与对象名绑进认证范围,服务端把对象张冠李戴就解不开;换密码只需用新口令重新包裹 DEK,云端密文一个字节都不用重传。

按图从上到下走一遍:

  1. 口令 → KEK:口令在客户端经 scrypt 派生 KEK,KEK 只在本机内存里用,从不上传。
  2. KEK → DEK:KEK 解开被包裹的 DEK(32 字节根密钥);服务端存的是「包裹后的 DEK」,没有口令解不开。
  3. DEK → 对象密钥:DEK 经 HKDF 派生每个对象独立的对象密钥,对象名与用户名一起吃进派生输入。
  4. 对象密钥 → 密文信封:对象密钥以 AES-256-GCM 加密数据,产出 PHIX1.<nonce>.<密文||tag>;AAD 绑定 phix/v1/object|{user_id}|{对象名}
  5. 上传:客户端只把密文信封交给服务端,服务端只保存密文,看不到明文内容。
  6. 换密码:用旧口令解开 DEK、再用新口令重新包裹一次即可,云端密文不用重传,也不必重新加密。

代价是:端到端加密没有「找回」。登录口令与注册时的一次性恢复码都丢失后,云端数据将无法解开——请把恢复码单独保存好。

对象加密信封

同步的最小单位是一个具名对象,内容是一段密文信封,服务端完全不理解内容。

  • 信封格式:PHIX1.<base64url(nonce)>.<base64url(密文||tag)>,一个字符串便于存 JSON。
  • AES-256-GCM:12 字节随机 nonce,密文尾部含 16 字节认证 tag;base64url 无填充。
  • 两族 AAD:身份族前缀 phix/v1/identity|…key_wrap / recovery_wrap / key_check);对象族 phix/v1/object|{user_id}|{对象名}(所有同步对象),绑定用户与对象名,服务端张冠李戴就解不开。
PHIX1.<base64url(nonce)>.<base64url(ciphertext || tag)>

传输加密

应用层加密:即使没有 HTTPS,网线上也只有密文。每个请求 / 响应都经过 X25519 密封盒 + HKDF(info phix/v1/seal)+ AES-256-GCM,临时会话密钥每次请求随机换一把。

请求:X-Phix-Enc: 1
内层 = {方法, 路径, 查询串, 请求体, 时间戳, nonce}
密文 = AES-256-GCM(会话密钥, iv, JSON(内层), aad="phix/v1/req|{方法}|{路径}")
体   = {sealed_sk, iv, ct}

X-Phix-Enc: 1 头的请求走加密路径;没有该头则走明文路径,兼容老客户端与调试工具。

这一层保护「网线上怎么走」,不替代「服务器上怎么存」;两层独立,都要有。

公钥固定与抗重放

  • 公钥固定:客户端首次连接时信任服务器公钥并固定;之后对不上就拒绝连接,防中间人冒充。
  • 抗重放:时间戳允许 ±300 秒偏差 + nonce 5 分钟去重;两者都在密文里,网线上改不了。

有人冒充服务器时公钥对不上即拒绝连接;重放的旧请求会被 nonce 去重拦下。

访问令牌

  • Ed25519 签名的 JWT,有效期 900 秒(15 分钟)。
  • 包含用户 ID、会话 ID、签发时间与过期时间,签名在令牌里,改不了。
  • 业务机可用公钥本地验签,不必回连认证中心。
payload = {"sub": "<user_id>", "sid": "<会话id>",
           "iat": …, "exp": …, "jti": …, "typ": "access"}

Refresh 轮换

  • refresh 令牌有效期 30 天,只能用来换新访问令牌,不能调业务接口。
  • 用一次换一次(轮换):成功后旧 refresh 作废、发一个新的,客户端必须存下新的。
  • refresh 只存摘要(SHA-256 指纹);明文只在签发那一刻返回一次。

换来的新 refresh 一定要存下来,否则旧令牌失效后就得重新登录。

宽限期

  • 并发续期时两个请求可能手里都是同一个 refresh,这不是攻击。
  • 120 秒宽限期内用旧令牌 → 200 + rotated:false,不重发新的。
  • 超出窗口再用旧令牌 = 重放 → 撤销整个会话。

宽限期只覆盖「同一会话、几秒内的并发续期」,不是允许重放。

公钥验签

  • 签名公钥通过 /auth/jwks 端点发布(JWKS 格式,免认证)。
  • 别的服务只拿公钥就能验签,不必共享密钥。
  • kid = 公钥 SHA-256 前 16 位;健康探测里也有,客户端可据此察觉服务器换钥匙。

同步对象一览

同步的最小单位是一个具名对象,内容是一段密文信封,服务端只存密文:

对象名内容谁写
settings.accounts四平台凭据(邮箱 / ManageBac / Edupage / 心履),两层字典、端到端加密客户端;官网个人中心可读改写
settings.lessons选课(教学组)客户端
settings.ui界面排序偏好客户端
settings.aiAI 供应商与 Key客户端
schedule日程 {id, day, time, title, note, created}客户端 / Pinghe Launcher 网页版
timetable课表(按教学组展开的周视图)客户端
school学校数据快照(课表 / 作业 / 邮箱摘要,来自 EduPage 与 ManageBac 的抓取缓存)客户端;Pinghe Launcher 网页版只读
profile{display_name, avatar(data URL,≤200KB,上传前压到 256×256), updated_at}任一端;官网个人中心可改头像
mood心情记录 {"entries":[{id, ts, text, intensity}]}心履客户端
agent:<会话id>AI 会话记录(每个会话一个对象)客户端

各端的默认同步清单略有差异:Pinghe Launcher 默认 7 个(settings.accountssettings.lessonssettings.uischeduletimetableschoolprofile),Pinghe Launcher Lite 默认 8 个(再加 mood),官网与心履只碰各自需要的对象。可以按需增减,清单只是客户端的默认值。

这些只是「对象名」,服务端只存密文、零改动即可支持新增对象;对象名会进入 HKDF 与 AAD,改动即等于换密钥。对象名以字母数字开头,可含 . _ : - 三种符号。

版本与冲突

  • 每个对象有 revision,单调递增;服务端不会静默覆盖。
  • 乐观锁:客户端带 base_revision 写入,对不上返回 409,客户端拉最新 → 解密 → 合并 → 重推(最多 3 次)。
  • 删除 = 写入墓碑(revision 照常 +1),避免「删了又被别的设备同步回来」;服务端保留每个对象最近 10 个 revision。
PUT /sync/objects/<name>
{"base_revision": 4, "payload": "PHIX1.…", "device": "笔记本"}
→ 成功 200;冲突 409 revision_conflict

配额与限流

配额(每账号):

默认值
单对象密文上限8 MiB
对象数2000
总容量200 MiB
单次批量同步50 个对象

限流(每 IP 每小时):

默认值
注册10 次
找回(恢复码)8 次
取密钥材料60 次
刷新令牌120 次

以上均为默认值,部署时可调;限流在服务进程内存里维护,将来多进程部署需换成共享存储。

心履

心履是一套「记录心情 + 日程提醒」的客户端矩阵,四端共用同一账号与数据模型:

  • 网页端:Django + waitress,部署在社团服务器上对外服务。
  • 桌面端:JavaFX 打包(Windows 出 exe、macOS 出 DMG),本地 SQLite 存离线数据。
  • 安卓端:原生 Java + Gradle 构建(Room 本地库,compileSdk 36 / minSdk 26),签名 APK 分发。

账号打通:登录时本地派生 AuthHash 交 phix 校验;phix 不可用时退回本地账号校验,不影响离线使用。服务间调用 phix 校验接口时带共享服务密钥,请求同样走应用层信封。

Pinghe Launcher

Pinghe Launcher 是「日程与校园信息中枢」:课表、作业、邮箱、日程一屏管完。

  • 桌面端:Electron,支持 Windows 与 macOS 13.0 及以上。
  • 邮箱(桌面端):通过 IMAP 读取平和邮箱,默认只拉最近 100 封,以纯文本展示、不加载外部图片;邮箱授权码只用于连接学校邮箱服务器。邮件内容不交给 AI。
  • 网页版(/app/:只有日程 / EduPage / ManageBac / 邮箱入口四个标签,不做真实收发,邮箱页只提示「去客户端收发」。
  • AI 助手:有「本地 / API」两种模式,只有本地模式不出本机;用 API 模式时回复由第三方服务商生成,发给它的内容会离开本机。
  • ManageBac / EduPage:同步对应凭据并在客户端内打开,网页版同样只做到入口一级。

Pinghe Launcher Lite

Pinghe Launcher Lite 是轻量日程助手,适合偏好轻量功能的用户。

  • 桌面端:Python,支持 Windows 与 macOS。
  • 本地独立可用:不登录也能正常使用;登录 phix 账号后可自行选择是否开启云同步。
  • 与 Pinghe Launcher 共用同一套本地共享数据(课表 / 学校快照 / 账号);两程序不同时运行,天然不会打架。

错误码

错误统一为 {"error": {"code": "...", "message": "中文说明"}}

code含义建议
bad_request请求参数不合法检查请求体字段
bad_credentials账号或密码不对提示用户核对凭据
unauthorized缺令牌 / 令牌无效或已吊销跳登录页重新登录
forbidden权限不足检查服务密钥或权限
not_found资源不存在核对对象名 / 路径
revision_conflict乐观锁冲突拉最新、合并、重推
quota_exceeded超出配额清理对象或扩容
payload_too_large请求体或对象超上限拆分或压缩
rate_limited触发限流稍后重试
token_expired令牌过期,该续期自动用 refresh 续期
name_invalid对象名不合法检查对象名规则
server_error服务端内部错误稍后重试或联系支持

两个 401 要分清:bad_credentials 是「你输错了」,unauthorized 是「你的登录失效了」。

常见问题

问:忘记口令怎么办?

答:用注册时保存的恢复码解包 DEK 并重设口令。恢复码只展示一次,请妥善保存。

问:换密码云端数据会丢吗?

答:不会。换密码只替换「包裹 DEK 的钥匙」,云端密文一个字节都不动。

问:服务器能看到我的数据吗?

答:服务端只保存密文信封,你的口令与密钥不上传,因此它拿到的内容解不开。真正会离开本机的是 AI 请求:心履的 AI 回复由第三方服务商生成,发给它的内容会离开本机;Pinghe Launcher 只有在 API 模式下才会把内容发给服务商,本地模式不出本机;邮件内容不交给 AI。详见「隐私与数据安全」。

问:多台设备同时改会不会冲突?

答:靠乐观锁 + 三方合并,冲突保留并报告,不会静默覆盖。

问:本机上的数据也加密吗?

答:不是全部。云同步的对象是端到端加密的;但 Pinghe Launcher 与 Pinghe Launcher Lite 共享的账号文件当前以明文保存在本机共享数据目录(为了两个应用能互相读取),凭据库在系统支持时用系统密钥加密。

问:心履账号和 phix 账号什么关系?

答:统一账号。心履老账号零影响,新账号走「认证委托」自动关联。

隐私与数据安全

这里把「能做什么」和「做不到什么」分开写清楚,避免把端到端加密理解过头:

  • 服务端只保存密文:密文信封、令牌、密钥包裹;你的口令与密钥不上传,服务端拿到的内容它解不开。请求体(含登录口令)不写入服务端日志。
  • 本机侧不是全加密:Pinghe Launcher 与 Pinghe Launcher Lite 共享的账号文件当前以明文保存在本机共享数据目录(为的是两个应用能互相读取,当前版本未启用加密);凭据库在系统支持时使用系统密钥加密。请不要把本机数据目录分享出去或提交到公开仓库。
  • AI 的内容会出本机:心履的 AI 回复由第三方服务商生成,发给它的内容会离开本机;Pinghe Launcher 的 AI 有「本地 / API」两种模式,只有本地模式不出本机。邮件内容不交给 AI
  • 同步哪些对象、能不能关schedule(日程)、mood(心情)、timetable(课表)、school(学校快照)、profile(头像昵称)、settings.accounts(凭据)、settings.lessons(选课)、settings.ui(界面偏好)、settings.ai(AI 配置)、agent:<会话id>(AI 会话)。客户端可以关闭同步,也可以删除云端对象。
  • 数据存在哪:本地 + 云端密文两份。本地是唯一真相源,云同步是「加在旁边」的一层。
  • 本地不上云的:本地缓存、日志、备份、浏览器登录态、个人令牌。
  • 口令与恢复码都丢失时:这是端到端加密的设计后果——注册时的恢复码和登录口令都丢失后,云端数据无法解开,我们也没有后门可以帮你恢复。请把恢复码单独保管好。

客户端支持本地保存与云端同步;重要内容建议定期备份。服务端即使数据库泄露或被入侵,没有口令与密钥也解不开这些密文对象。

「端到端加密」不等于「一定丢不了」:设备损坏、误删、口令与恢复码同时丢失,都可能让数据取不回来。重要内容请另外留一份备份。