# 趣智校园热水功能:证据与网页移植报告 分析日期:2026-10-15。部署更新:已通过 Docker 上线 https://water.lyy0709.cn,HTTPS 和证书续期演练已通过,现场水表验收状态不变。用户场景:中国石油大学(北京)东校区,操作需在水表附近开启蓝牙。 **结论:已实现无广告网页的登录、设备查询、BLE 收发、开关阀、结算及恢复链路;本校真实水表兼容性与实际扣费尚未验收。这个 APK 的业务 DEX 被加固,不能将参考客户端的完整协议描述冒称为从本 APK 全部反编译所得。** ## 一、APK 直接证据 以下 E01–E06 是本报告的本地证据编号,不是 REA Evidence ID。原件为根目录 `base.apk`,未修改。 | 编号 | 观察 | 可复查位置 | 结论边界 | |---|---|---|---| | E01 | 包名 `com.klcxkj.zqxy`,名称趣智校园,版本 `6.5.29` / `679`,194,948,564 字节,minSdk 24 / targetSdk 31 | [artifact.json](artifact.json)、[AndroidManifest.xml](AndroidManifest.xml) | 文件身份与声明,不证明运行行为 | | E02 | 清单声明 `BathingBtActivity`、`BathingBleActivity`、`BathingNetActivity`,以及蓝牙权限 | [AndroidManifest.xml](AndroidManifest.xml) 第 3–9、172–174 行 | 确有多个通信入口;不能只凭类名判定某校采用哪个分支 | | E03 | 默认资源包含“公寓洗澡”“开始洗澡”“结束洗澡”“热水供应时间”“设备网络不可用,是否切换为蓝牙模式” | [hot-water-strings.xml](hot-water-strings.xml) | 确认界面语义;按钮文案不是开阀指令 | | E04 | 洗澡页面布局内含 `com.anythink.nativead.api.ATNativeAdView` | [activity_bathing.xml](activity_bathing.xml) 第 96 行;APK 原始路径 `res/oTd.xml`,映射见 [resource-map.json](resource-map.json) | 广告控件直接嵌在洗澡布局内;不能仅凭布局断言每次都展示广告 | | E05 | Application 是 `com.netease.nis.wrapper.MyApplication`,可解析 DEX 仅 30 个壳类,包括 `MyJni`、`Entry`;包内存在 `libnesec.so` | 清单第 141 行;[shell-classes.txt](shell-classes.txt) | 普通静态 DEX 没有公开业务实现;未完成壳内业务解密 | | E06 | APK 自带 `libklcxkjencry.so` 导出 `signParams`、`encryptByAES`、`decryptByAES`;可直接定位签名与 AES 实现 | [native-disassembly.txt](native-disassembly.txt) | 以下密码学结构来自本 APK ARM64 指令与符号;未在 Android 运行 JNI | 原始资源也已解码保留:[公寓设备选择](activity_apartment_chose.xml)、[使用结束页面](activity_device_using_finish.xml)。这里只提取业务相关资源,没有把广告 SDK 放入网页。 ## 二、原生库中的可定位证据 分析对象是从 `base.apk!/lib/arm64-v8a/libklcxkjencry.so` 原样提取的文件。汇编由 Capstone 生成,ELF 符号和 PLT 重定位由 pyelftools 解析,全部指令见 `native-disassembly.txt`。这些是静态观察,不是源代码或真机调用日志。 **签名函数入口:`0x3ebd4`。** - `0x3ec18–0x3ec44` 取得 JNI 的两个字符串参数。 - `0x3ec58–0x3ec68` 追加地址 `0xa20eb` 的 `&key=`;随后追加另一个参数。 - `0x3ecd8` 调用 `MD5` 构造,`0x3ece4` 调用 `MD5::toStr`。 - `0x3ece8–0x3ed0c` 追加全局字符串 `0xcc0d0`。 - `0x3ed30` 再次调用 `MD5`,`0x3ed3c` 再次取十六进制文本。 - 全局构造函数 `0x3e3a0` 将只读地址 `0xa1fd0` 的 `&key=sign-kailu-855c74a88b8b494187c99b08b8c9a744` 写入 `0xcc0d0`。 据此推导,并与公开参考实现交叉核对: ```text raw = 参与签名的字段按 key 升序拼接 key + value inner = MD5(raw + "&key=" + loginCode) signature = MD5(inner + "&key=sign-kailu-855c74a88b8b494187c99b08b8c9a744") ``` 字段选择与排序来自参考实现,双重 MD5 结构及外层常量由本 APK 汇编支持。这里 MD5 是兼容既有协议,不是新增加的完整性机制。原函数入口还有应用签名检查状态分支;网页自行实现协议算法,没有执行或修改该分支。 **AES 解密 JNI 入口:`0x3f324`。** `0x3f35c–0x3f378` 将 `0xa282e` 作为 key 地址传给 `decryptByAES`;`0x40c80` / `0x40c84` 将 key / block 长度设为 `0x10`,`0x40c90` 调用 `AES::MakeKey`,`0x40ca8` 调用 `AES::Decrypt`。JNI 传入模式参数 0,与参考实现中的 ECB 路径相符。 原始 key 字节为: ```text 32 30 32 31 30 31 31 38 6b 6c 63 78 40 30 30 32 ASCII: 20210118klcx@002 长度:16 字节;后续的 00 是 C 字符串结束符,不计入密钥。 ``` 网页服务端使用 Node `crypto` 解密,验证密文长度、填充和写回明文格式,再把合法数据交给设备。先前工作进度中将文本误数为 15 字节,已按原始字节更正,最终实现使用上述 16 字节。 ## 三、热水链路如何工作 **当前最有根据的协议模型是:手机同时充当蓝牙控制端和互联网中转端。服务器生成带账户/费率信息的授权数据,手机将数据传给水表;结束后再将水表消费数据交给服务器结算。** 这不是纯本地的开关按钮,也不是网页能够随意构造的免费开水指令。 完整协议来自以下公开实现的特定版本,与上面的本 APK 原生库部分交叉验证。其作者对其他学校的描述不能直接当作本校实测结论: - [LinYu 源码,提交 fa440709…](https://github.com/yehu-imei/linyu/tree/fa4407094864c2e2e540d9768363a3d1aa635a36):`ble/BleFrame.kt`、`ble/BleController.kt`、`data/BleShowerController.kt`、`data/BleSignBuilder.kt`、`api/QzxyService.kt`、`utils/KlcxkjSigner.kt`。协议移植遵循 MIT 许可,声明见项目 `THIRD_PARTY_NOTICES.md`。 - [Funnyass_school 源码,提交 3e1b8806…](https://github.com/JDCK380/Funnyass_school/tree/3e1b88065f6c35084d357a78c5cca0761419109d):独立核对 `FrameUtils.kt`、`BathActivity.kt` 第 929 行附近的状态/账号解析,以及 `ApiClient.kt` 的登录失效代码。仅用于核对协议事实。 - [公开的 SDKQZNetworkOffline](https://github.com/zhiquanlo/SDKQZNetworkOffline/tree/123dd09f1ab9dc5a7a8cf1a8ff93ca134486cf08):旧版 iOS SDK 头文件也声明设备状态读取、下费率、消费数据处理等接口。它不是本 APK 6.5.29 的运行证据。 ```mermaid sequenceDiagram participant W as 网页 / 手机 participant S as 趣智校园服务端 participant B as 附近的蓝牙水表 W->>S: 登录,取得本人的 loginCode W->>S: 按二维码地址查询设备 W->>B: 0x23 查询状态 B-->>W: 协议版本、账户、状态、randomNumber W->>S: 签名请求 rateOrder S-->>W: downData、consumeDate 等 W->>B: 0x21 原样下发 downData B-->>W: 设备确认,开始用水 W->>B: 0x22 关阀 B-->>W: 关阀确认 W->>B: 重连并查询,再以 0x85 采集消费 B-->>W: 消费数据 xfData W->>S: 签名上传 xfData S-->>W: 结算写回密文 clData W->>B: 解密后以 0x86 写回 B-->>W: 写回确认,状态回到空闲 ``` GATT 服务为 `0000ff00-0000-1000-8000-00805f9b34fb`,通知特征 `ff01`,写特征 `ff02`。实际写入内容是 `#` + 大写 HEX ASCII + 换行,而非原始二进制;每片最多 20 字节,片间 20 ms,命令期限 8 秒。二进制帧如下: ```text 60 00 [payload长度+3] 80 [命令] 00 [payload] [加和校验] 16 加和校验 = (0x80 + 命令 + payload各字节之和) & 0xff 查询状态例:#600003802300A316\n ``` 状态载荷中,`p[9..12]` 为大端 accountId,`p[19]` 为协议版本,`p[24..27]` 为设备随机数,`p[28]` 为状态,`p[29..30]` 为 macType。这些偏移来自参考客户端,尚未通过东校区水表回包验证。网页不会把本地随机数代替设备随机数,不会把其他账户的使用中水表认作自己的。 服务端固定基址为 `https://v3-api.china-qzxy.cn/`,目前实现: | 用途 | 接口 | |---|---| | 密码 / 短信登录 | `user/login` / `user/registerAndLogin` | | 短信验证码 / 校验登录 | `user/verification/code/get` / `user/info` | | 查询设备 / 钱包 / 月账单 | `device/info/mac` / `account/wallet` / `order/query/account/bill/list` | | 取得设备计费数据 | `order/downRate/bluetooth/rateOrder` | | 上传消费 / 上报未开阀订单 | `order/upload/bluetooth/data` / `order/upload/bluetooth/fail` | 接口路径和字段来自上述公开客户端,尚未用用户账号调用生产服务。上传的 `xfData` 和参与签名的字符串统一为小写。仅在设备明确空闲且保有 `consumeDate` 时提供失败上报,不会在水流状态未知时直接取消订单。 ## 四、登录持久化及手机支持 用户要求已经落实:账号资料及 `loginCode` 存到当前浏览器 `localStorage['water.auth']`;不保存密码、验证码。页面重新打开时通过服务端校验。新设备、新浏览器或不同 origin 没有该记录,显示未登录。 HTTP 401/403、业务码 8/10000 视为凭据失效,清除登录态,要求重新登录;普通网络失败不清除。用水恢复数据独立放在 `water.active`,登录过期不删除。Node 服务端按每次请求处理凭据,没有跨设备共享的登录会话、磁盘凭据或账号日志。 安卓 Chrome 的 Web Bluetooth 能提供所需 GATT 能力,但必须 HTTPS 或电脑 localhost。[MDN 请求设备接口](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice) iPhone Safari 和 iOS Chrome 原生不提供这条 Web Bluetooth 路径,换普通浏览器或添加到主屏幕不能补上该能力。[WebKit 当前立场](https://webkit.org/standards-positions/#position-570) 无需侧载的候选方案是从 App Store 安装 Bluefy,在其中打开同一 HTTPS 网页。开发者明确说明支持网页访问 BLE / GATT,但本次没有在 iPhone 上验收,也未确认用户商店地区是否可下载。[Bluefy 官方使用说明](https://bluefy.app/guide.html) · [App Store](https://apps.apple.com/app/bluefy-web-ble-browser/id1492822055) ## 五、验证结果与未决项 | 验证 | 实际结果 | |---|---| | `npm test` 初次完整通过 | 19 项通过、0 失败 | | Docker amd64 构建阶段,Node 24.22.0,`npm test` | 当前完整 21 项通过、0 失败 | | 增补全部登录过期信号后,`node --test test/api.test.mjs` | 5 项通过 | | 增补写回 ACK 丢失恢复后,`node --test test/controller.test.mjs` | 9 项通过 | | `node --test test/protocol.test.mjs` | 7 项通过;包括碎片通知、即时 ACK、错误帧、写入挂起超时 | | Playwright 执行 `test/browser-flow.js` | 7 类交互检查通过:凭证持久化、不存密码、恢复验证、新浏览器未登录、失效保留用水、完整控制流程、写回重试不重复上传 | | 1200px 桌面和 390px 手机页面 | 无 JS 页面错误,手机无横向溢出;截图见 `web-desktop.png` / `web-mobile.png` | 部署阶段已在新 Docker/Node 环境重新运行完整 21 项测试并全部通过。测试全部使用合成数据、测试替身或截获响应;没有发送真实短信、没有登录生产账号、没有控制水表、没有真实扣费。 明确尚未解决的事项: 1. **本 APK 加固业务 DEX**:REA Android provider 缺少 JADX MCP JAR,环境也无 Java;原生深度 provider 同样不可用。已用本地解析器和 Capstone 获取上述证据,没有宣称 REA 完成了反编译,也没有用安装引擎掩盖加固问题。 2. **东校区协议与费用**:未获得真实设备回包或已授权账户的服务响应。FF00 协议、状态偏移、金额口径、版本差异和正确扣费需要实测;不把单元测试通过等同于收费正确。 3. **iPhone / Bluefy**:尚未进行实际扫描、GATT 通知、断线重连或锁屏测试。页面不承诺在后台或锁屏后自动关阀。 4. **手机 HTTPS 入口(已解决)**:在用户授权的服务器以 Docker 上线 https://water.lyy0709.cn;已有 Nginx 为此子域提供 HTTPS。证书已签发、续期演练成功。部署方式见项目 README。 5. **功能范围**:重建热水控制核心流程;没有迁移商城、广告、充值、退款、免密签约、其他设备类型或第三方平台身份授权。 因此交付应称为“有可复查证据、可运行且离线验证通过的网页移植,等待本校实机验收”,不能称为“已经完整实测替换原 APK”。