ship-it-th1/qinlin-remote-open

GitHub: ship-it-th1/qinlin-remote-open

一个重新实现「亲邻开门」应用云端网络开门协议的客户端与 Docker HTTP 网关,用于对已授权门禁进行自动化解锁。

Stars: 1 | Forks: 0

# qinlin-remote-open 用于**亲邻开门** Android 应用(Flutter)的远程**网络开门**客户端和 HTTP 网关。 本仓库重新实现了官方应用通过网络(而非蓝牙)解锁门禁时发送的云端“网络开门”请求。其旨在用于**对您的账户已有权开启的门禁进行授权自动化操作**。 [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Python](https://img.shields.io/badge/Python-3.9%2B-blue.svg)](https://www.python.org/) [![Docker](https://img.shields.io/badge/Docker-ready-blue.svg)](docker/) ## 目录 1. [您将获得什么](#what-you-get) 2. [开始前的准备](#what-you-need-before-starting) 3. [仓库结构](#repository-layout) 4. [端到端配置 (CLI) — 分步指南](#end-to-end-setup-cli--step-by-step) 5. [端到端配置 (Docker) — 分步指南](#end-to-end-setup-docker--step-by-step) 6. [auth.json 规范](#authjson-specification) 7. [协议(传输格式)](#protocol-wire-format) 8. [签名算法(可实现)](#signature-algorithm-implementable) 9. [HTTP 网关 API](#http-gateway-api) 10. [Home Assistant 集成](#home-assistant-integration) 11. [运维:轮换会话、多门禁、多主机](#operations-rotate-session-multi-door-multi-host) 12. [验证矩阵](#verification-matrix) 13. [故障排除](#troubleshooting) 14. [安全检查清单](#security-checklist) 15. [文档索引](#documentation-index) 16. [参考资料](#references) 17. [免责声明与许可证](#disclaimer--license) ## 您将获得什么 | 组件 | 路径 | 作用 | |-----------|------|------| | CLI 客户端 | `app/open_door_api.py` | 读取 `auth.json`,构建 `sign`,POST 开门请求 | | Docker 镜像 | `docker/` | 相同的客户端 + `server.py` HTTP 触发器 | | 凭据模板 | `data/auth.json.example` | 您必须填写的字段 | | 协议文档 | `docs/PROTOCOL.md` | 完整的请求/响应/签名细节 | | 会话导出指南 | `docs/GET_SESSION.md` | 如何从已登录的应用中提取会话 | | 凭据维护 | `docs/AUTH.md` | 创建 / 轮换 / 部署凭据 | | 研究笔记 | `docs/RESEARCH.md` | 为什么 Flutter 逆向困难;方法概述 | **典型延迟:** 在普通的住宅/服务器上行链路上,端到端约为 **0.3–1.0 秒**(主要取决于到开门 API 主机的 RTT)。 **不包含:** 官方 APK、个人凭据、抓包转储、一键利用工具包。 ## 开始前的准备 1. 一部已安装并登录**亲邻开门**的手机。 2. 证明在官方 UI 中,**网络开门**(不仅是蓝牙)已经适用于您的门禁。 3. 能够导出 Flutter SharedPreferences 键(root + Frida 是 `docs/GET_SESSION.md` 中记录的方法)。 4. Python **3.9+** *或* Docker Engine + Compose v2。 5. 允许对应用使用的开门 API 主机进行出站 HTTPS 请求(见协议)。 如果网络开门在官方应用中不起作用,本项目也无法修复设备/离线问题。 ## 仓库结构 ``` qinlin-remote-open/ ├── README.md ├── LICENSE ├── SECURITY.md ├── app/ │ └── open_door_api.py # CLI (stdlib only) ├── data/ │ └── auth.json.example ├── docker/ │ ├── Dockerfile │ ├── docker-compose.yml │ ├── app/ │ │ ├── open_door_api.py │ │ └── server.py # HTTP gateway │ ├── data/ │ │ └── auth.json.example │ └── scripts/ │ └── build-and-run.sh └── docs/ ├── PROTOCOL.md ├── AUTH.md ├── GET_SESSION.md ├── ARCHITECTURE.md ├── RESEARCH.md ├── TROUBLESHOOTING.md └── REFERENCES.md ``` ## 端到端配置 (CLI) — 分步指南 ### 第 1 步 — 克隆 ``` git clone https://github.com/ship-it-th1/qinlin-remote-open.git cd qinlin-remote-open ``` ### 第 2 步 — 创建凭据文件 ``` cp data/auth.json.example data/auth.json ``` 使用**您的**值编辑 `data/auth.json`(见 [auth.json 规范](#authjson-specification) 和 [docs/GET_SESSION.md](docs/GET_SESSION.md)): ``` { "sessionId": "app:PASTE_FROM_flutter.sessionId", "deviceId": "PASTE_FROM_flutter.ql_device_id", "communityId": 10001, "commonlyUsedDoor": { "doorControlId": 123456, "doorControlName": "Lobby" } } ``` 规则: - 如果转储看起来像 `"app:...."`,请去除多余的 JSON 引号。 - `sessionId` 和 `deviceId` 必须来自**同一个**已登录的安装实例。 - `doorControlId` 必须是应用中支持**网络**开门的门禁。 ### 第 3 步 — 验证 JSON ``` python -m json.tool data/auth.json > /dev/null && echo "JSON OK" ``` ### 第 4 步 — 执行一次开门 ``` python app/open_door_api.py --auth data/auth.json ``` 可选覆盖项: ``` python app/open_door_api.py --auth data/auth.json --door 123456 --community 10001 ``` ### 第 5 步 — 解释结果 输出包含 `http=… elapsed=…ms ok=…` 以及业务 JSON。 **成功(业务):** ``` { "code": 0, "message": "success", "data": { "openDoorState": 1 } } ``` CLI 退出代码 `0` 表示 `ok=True`(代码 0 且消息成功)。 默认情况下,结果文件会写入 `data/open_door_result.json`(路径可通过 `--result` 覆盖)。 **失败“未登录 / 超时”:** 刷新 `sessionId`(并重新检查与 sign 相关的字段)。见 [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。 ## 端到端配置 (Docker) — 分步指南 ### 第 1 步 — 准备文件 ``` cd docker cp data/auth.json.example data/auth.json # 填充 data/auth.json(字段与 CLI 相同) ``` ### 第 2 步 — 设置网关 token 编辑 `docker-compose.yml`: ``` environment: QINLIN_TOKEN: "replace-with-a-long-random-secret" ``` 使用长随机字符串(≥ 24 个字符)。这**不是**亲邻开门会话;它仅用于保护您的 HTTP `/open` endpoint。 ### 第 3 步 — 构建并启动 ``` docker compose up -d --build docker compose ps docker compose logs -f --tail=50 ``` ### 第 4 步 — 健康检查 ``` curl -sS http://127.0.0.1:8080/health # {"ok": true, "service": "qinlin-opendoor"} ``` ### 第 5 步 — 通过 HTTP 开门 ``` curl -sS -X POST http://127.0.0.1:8080/open \ -H "X-Token: replace-with-a-long-random-secret" \ -H "Content-Type: application/json" \ -d '{"door": 123456, "community": 10001}' ``` 空请求体 `{}` 将使用 `auth.json` 中的默认值。 ### 第 6 步 — 无需长时间运行的服务器执行一次性开门 ``` docker compose run --rm qinlin-opendoor python open_door_api.py ``` ### 第 7 步 — 稍后更新会话(无需重新构建) 1. 覆盖主机上的 `docker/data/auth.json`。 2. 再次调用 `/open`(每次请求都会读取该文件)。 3. 可选:`docker compose restart`。 ## auth.json 规范 完整维护流程:**[docs/AUTH.md](docs/AUTH.md)**。 | 字段 | 必需 | 用途 | |-------|----------|----------| | `sessionId` | 是 | 查询 `sessionId`;签名字段 `token` | | `deviceId` | 是 | HTTP header `openid` | | `communityId` | 是 | 查询 + 签名(除非在 CLI/HTTP 上覆盖) | | `commonlyUsedDoor.doorControlId` | 是 | 默认门禁(除非被覆盖) | | `commonlyUsedDoor.doorControlName` | 否 | 仅用于显示 | | `expireTime_ms` / `expireTime_local` | 推荐 | 轮换规划 | **导出源键(运行时 SharedPreferences 名称 `FlutterSharedPreferences`):** | Preference key | auth.json field | |----------------|-----------------| | `flutter.sessionId` | `sessionId` | | `flutter.ql_device_id` | `deviceId` | | `flutter.communityId` | `communityId` | | `flutter.commonlyUsedDoor` | `commonlyUsedDoor` (JSON 对象) | | `flutter.expireTime` | `expireTime_ms` | 磁盘上的 `FlutterSharedPreferences.xml` 可能是**加密的**;建议在应用进程运行时,通过 Java `SharedPreferences.getAll()` 进行转储([docs/GET_SESSION.md](docs/GET_SESSION.md))。 ## 协议(传输格式) 完整细节:**[docs/PROTOCOL.md](docs/PROTOCOL.md)**。 参考应用构建版本:**亲邻开门 5.2.6** (`qversioncode=3146`)。 ### Endpoint ``` POST https://mobileapi3.qinlinkeji.com/api/open/doorcontrol/v2/open ``` | 项目 | 值 | |------|--------| | 方法 | `POST` | | 请求体 | **空** (`Content-Length: 0`) | | 查询参数 | `sessionId`, `appChannel`, `doorControlId`, `communityId`, `timestamp`, `version`, `nonce`, `sign` | ### 查询参数 | 名称 | 示例格式 | 备注 | |------|---------------|--------| | `sessionId` | `app:…` | 来自 auth | | `appChannel` | `1` | 在参考构建中固定 | | `doorControlId` | 整数转为字符串即可 | 门禁 ID | | `communityId` | 整数转为字符串即可 | 小区 ID | | `timestamp` | 毫秒级 epoch | 客户端时钟 | | `version` | `5.2.6` | 必须与签名字符串匹配 | | `nonce` | 5 位数字补零 | 例如 `04597` | | `sign` | 32 位十六进制 **大写** | 见下一节 | ### Headers(与官方客户端对齐) | Header | 值 | |--------|--------| | `openid` | 来自 auth 的 `deviceId` | | `qversioncode` | `3146`(如果构建版本变化,使用环境变量覆盖) | | `qchannel` | 例如 `xiaomi` | | `qplatform` | 例如 `0` | | `qvendor` | 例如 `Xiaomi` | | `User-Agent` | `Dart/3.10 (dart:io)` | | `Content-Type` | `application/json; charset=utf-8` | | `Accept-Encoding` | `gzip` | ### 成功响应体(格式) ``` { "code": 0, "message": "success", "data": { "openDoorState": 1, "communityId": 10001, "communityName": "…" } } ``` 将成功视为:`code == 0` **并且** `message == "success"`。建议同时检查 `data.openDoorState == 1`。 ## 签名算法(可实现) ``` raw = "appChannel=" + appChannel + # "1" "&communityId=" + communityId + "&doorControlId=" + doorControlId + "&nonce=" + nonce + "×tamp=" + timestamp + "&token=" + sessionId + # NOTE: field name is token "&version=" + version + # "5.2.6" "&key=" + "qiAnlPinP" sign = uppercase_hex( MD5( UTF-8 bytes of raw ) ) ``` ### 关键陷阱 | 错误 | 正确 | |-------|--------| | 签名字符串使用 `sessionId=` | 必须使用 **`token=`** 携带相同的 session 字符串 | | 使用二进制文件中长的 `appsecret` | 此开门 API 使用固定的 **`key=qiAnlPinP`** | | 小写 MD5 十六进制 | 服务器期望 **大写** | | 随意重排字段 | 使用上述精确的拼接顺序 | ### Python 参考实现(标准库) ``` import hashlib def make_sign(session_id, door_id, community_id, timestamp, nonce, version="5.2.6", app_channel="1", key="qiAnlPinP"): raw = ( f"appChannel={app_channel}" f"&communityId={community_id}" f"&doorControlId={door_id}" f"&nonce={nonce}" f"×tamp={timestamp}" f"&token={session_id}" f"&version={version}" f"&key={key}" ) return hashlib.md5(raw.encode("utf-8")).hexdigest().upper(), raw ``` 内置的 `open_door_api.py` 完全实现了这一点。环境变量覆盖: | 环境变量 | 默认值 | 影响 | |-----|---------|---------| | `QINLIN_VERSION` | `5.2.6` | 查询 + 签名 | | `QINLIN_QVERSIONCODE` | `3146` | Header | | `QINLIN_SIGN_KEY` | `qiAnlPinP` | 签名尾部 | | `QINLIN_AUTH` | `data/auth.json` 或 `/data/auth.json` | 凭据路径 | ## HTTP 网关 API 由 `docker/app/server.py` 实现。 | 方法 | 路径 | 认证 | 行为 | |--------|------|------|----------| | `GET` | `/health` | 无 | `{"ok":true,"service":"qinlin-opendoor"}` | | `POST` | `/open` | 必需 | 开门;请求体为可选 JSON | | `GET` | `/open` | 必需 | 相同(不推荐:token 可能会记录在日志中) | ### 认证 满足以下任意一项: ``` X-Token: Authorization: Bearer ``` 如果 `QINLIN_TOKEN` 为空,`/open` 将始终返回 **401**。 ### POST 请求体 ``` { "door": 123456, "community": 10001 } ``` 省略字段即可使用 `auth.json` 默认值。 ### 响应格式(网关) ``` { "ok": true, "http_code": 200, "elapsed_ms": 237, "doorControlId": "123456", "communityId": "10001", "doorControlName": "Lobby", "response": { "code": 0, "message": "success", "data": { "openDoorState": 1 } } } ``` | 网关 HTTP 状态 | 含义 | |--------------|---------| | 200 | 业务 `ok: true` | | 401 | 错误/缺失的 `X-Token` | | 500 | 缺失/无效的 `auth.json` 或内部错误 | | 502 | 上游/业务失败 (`ok: false`) | ## Home Assistant 集成 ### secrets.yaml ``` qinlin_gateway_token: "your-long-random-token" ``` ### configuration.yaml ``` rest_command: qinlin_open_default: url: "http://YOUR_HOST:8080/open" method: POST headers: Content-Type: "application/json" X-Token: !secret qinlin_gateway_token payload: "{}" content_type: "application/json" qinlin_open_lobby: url: "http://YOUR_HOST:8080/open" method: POST headers: Content-Type: "application/json" X-Token: !secret qinlin_gateway_token payload: '{"door": 123456, "community": 10001}' content_type: "application/json" ``` 重新加载 REST 命令 / 重启 HA,然后通过按钮或自动化调用 `rest_command.qinlin_open_lobby`。 官方 HA 文档:[rest_command](https://www.home-assistant.io/integrations/rest_command/)。 ## 运维:轮换会话、多门禁、多主机 ### 轮换会话(标准流程) 1. 确认亲邻开门网络开门在手机上仍然可用。 2. 重新导出 `flutter.sessionId`(如果重装/更换设备,则一并导出 `deviceId`)。 3. 备份:`cp auth.json auth.json.bak-YYYYMMDD`。 4. 在**每台**开门的机器上覆盖 `auth.json`。 5. 测试一次 CLI 或 `POST /open`。 6. 对于仅涉及凭据的更改,无需重新构建 Docker 镜像。 详情:[/AUTH.md](docs/AUTH.md)。 ### 多门禁 保留一个会话;每次调用传入不同的 ID: ``` python app/open_door_api.py --auth data/auth.json --door 111 --community 10001 python app/open_door_api.py --auth data/auth.json --door 222 --community 10001 ``` ### 多主机 将相同的 `auth.json` 复制到每个主机挂载的 `data/` 目录。相对于使用内置了机密的重新构建镜像,首选 `scp`/`rsync`。 ## 验证矩阵 | 层级 | 如何检查 | 通过标准 | |-------|--------------|---------------| | JSON 文件 | `python -m json.tool data/auth.json` | 解析通过 | | Session | 可选的 GET 用户配置 API (PROTOCOL) | 业务成功 | | 签名 + 开门 | CLI / Docker 开门 | `code=0`, `openDoorState=1` | | 网关认证 | 不带 token 进行 POST `/open` | HTTP 401 | | 网关路径 | GET `/health` | `ok: true` | **重要:** 错误的 `sign` 通常会返回与过期会话(“未登录”)相同的业务文本。如果会话检查成功但开门失败,请在无休止地重新登录之前,重新检查签名字符串。 ## 故障排除 完整决策树:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。 | 症状 | 可能原因 | 操作 | |---------|--------------|--------| | 网关 `unauthorized` | 错误的 `QINLIN_TOKEN` | 修复 header / compose 环境 | | `auth file not found` | 缺失挂载或路径 | 将 `auth.json` 放置在预期路径中 | | 业务未登录 | 过期的 session **或** 错误的签名 | 刷新会话;验证 `token=`/`key=`/`version` | | 超时 | 出站流量被阻止 | 允许 HTTPS 请求到达开门 API 主机 | | 在 PC 上正常,在 Docker 中失败 | 不同的 `auth.json` 副本 | 同步文件;检查卷 | 在亲邻开门应用大版本升级之后:确认 UI 网络开门仍然有效,然后如果常量(`version`、路径、key)发生变化,请重新捕获一次请求。 ## 安全检查清单 - [ ] 强随机 `QINLIN_TOKEN` (≥ 24 个字符) - [ ] 服务器上的 `auth.json` 权限为 `600`;切勿提交 - [ ] 没有 TLS 反向代理 + 认证,切勿公开暴露 `/open` - [ ] 将会话视为登录 cookie;泄露时立即轮换 - [ ] 仅配置您有权使用的门禁 ID - [ ] 在提交时优先使用 GitHub 私密邮箱 / noreply 邮箱 ## 文档索引 | 文档 | 内容 | |-----|----------| | [docs/PROTOCOL.md](docs/PROTOCOL.md) | 完整的传输格式与签名 | | [docs/AUTH.md](docs/AUTH.md) | 凭据生命周期(可操作) | | [docs/GET_SESSION.md](docs/GET_SESSION.md) | 从设备导出会话 | | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | 组件与信任边界 | | [docs/RESEARCH.md](docs/RESEARCH.md) | 研究方法与挑战 | | [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | 故障排除矩阵 | | [docs/REFERENCES.md](docs/REFERENCES.md) | 相关工具、文章、引用 URL | ## 参考资料 相关开源工具和技术文章(Flutter TLS、Frida、HA): **[docs/REFERENCES.md](docs/REFERENCES.md)** | 项目 / 文章 | URL | |-------------------|-----| | Frida | https://frida.re/ · https://github.com/frida/frida | | Flutter SSL write locator | https://github.com/vichhka-git/universal-flutter-ssl-pinning | | SensePost: Flutter HTTPS + Frida | https://sensepost.com/blog/2025/intercepting-https-communication-in-flutter-going-full-hardcore-mode-with-frida/ | | Flutter TLS bypass write-up | https://m4kr0x.medium.com/flutter-tls-bypass-how-to-intercept-https-traffic-when-all-other-frida-scripts-fail-bd3d04489088 | | Home Assistant rest_command | https://www.home-assistant.io/integrations/rest_command/ | ### 引用本仓库 ``` [qinlin-remote-open](https://github.com/ship-it-th1/qinlin-remote-open) ([PROTOCOL](https://github.com/ship-it-th1/qinlin-remote-open/blob/main/docs/PROTOCOL.md)) ``` ``` qinlin-remote-open (2026). Network door-open protocol notes and client for 亲邻开门. https://github.com/ship-it-th1/qinlin-remote-open ``` ## 免责声明与许可证 用于**授权自用**和技术文档的独立项目。不提供任何担保。 **许可证:** [MIT](LICENSE)
标签:Docker, Flutter, Home Assistant, 协议分析, 安全防御评估, 智慧社区, 权限提升, 物联网, 网络调试, 自动化, 请求拦截, 逆向工具