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)
[](https://www.python.org/)
[](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, 协议分析, 安全防御评估, 智慧社区, 权限提升, 物联网, 网络调试, 自动化, 请求拦截, 逆向工具