feat: 重构为前后端分离架构并完善设备端演示

- 新增 Web 用户端(登录体系 + 统一 REST API 调用)
- 后端增加用户认证、统一 ApiResponse、CORS 支持
- Android 设备端迁移至 MVVM + DataBinding + Retrofit 网络层
- 完善 README 架构说明与密码学原理文档
- 新增 .gitignore 与持久化数据表说明
This commit is contained in:
TongTongStudio
2026-08-21 04:34:58 +08:00
parent eda03d241d
commit 93499fa189
63 changed files with 4398 additions and 455 deletions

233
README.md
View File

@@ -1,7 +1,8 @@
# 🔐 安全设备 Demo — Android + Spring Boot
# 🔐 安全设备 Demo — 前后端分离Web 用户端 + Android 设备端 + Spring Boot
> **场景**:高权限设备端(无登录)+ 用户端(有登录体系)+ SN 绑定 + 私密照片加密
> **目标**:设备端 TEE 密钥不可导出、服务端零知识、恢复出厂后可安全恢复
> **目标**:设备端 TEE 密钥不可导出、服务端零知识、恢复出厂后可安全恢复
> **架构**:前后端分离 —— Web 用户端(真实登录体系)与 Android 设备端(无登录)都通过统一 REST API 访问后端
---
@@ -10,16 +11,25 @@
```
┌─────────────────────┐ HTTPS ┌──────────────────────┐
│ Android 设备端 │ ◄──────────────────► │ Spring Boot 后端 │
│ (无登录/高权限) │ │ (用户有登录体系)
└─────────────────────┘ └──────────────────────┘
│ │
│ ① TEE 密钥对Keystore │ ① 用户主密钥UK
│ ② AES-GCM 信封加密照片 │ ② DEK 信封加密存储
│ ③ ECDSA 签名元数据 │ ③ SN 绑定 + 短信验证
│ ④ 恢复出厂 → 新密钥对 │ ④ Recovery Token 下发
│ ⑤ UK 解 DEK → 设备公钥加密下发
│ (无登录/高权限) │ │ ────────────────
└─────────────────────┘ │ · 统一响应 ApiResponse│
│ · CORS 跨域
│ ① TEE 密钥对Keystore · 用户登录 Token 鉴权 │
│ ② AES-GCM 信封加密照片 · UK 信封加密存储 │
│ ③ RSA-SHA256 签名元数据 │ · SN 绑定 + 短信验证
│ ④ 恢复出厂 → 新密钥对 │ · Recovery Token 下发
└──────────┬───────────┘
┌─────────────────────┐ REST + Bearer Token │
│ Web 用户端 │ ◄───────────────────────────────┘
│ (web-client/) │ · 登录/注册(真实登录体系)
│ (有登录体系) │ · 绑定设备 / 恢复授权 / 下载解密
└─────────────────────┘
```
- **设备端Android**:无登录体系,只持有 TEE 密钥,信任边界 = 密钥 + SN + Recovery Token
- **用户端Web**:先登录获取 `Bearer Token`,再操作绑定 / 短信 / 恢复授权 / 下载解密
- **后端**:统一 `ApiResponse{code, message, data}`,跨域开放,用户端接口强制 Token 鉴权
---
## 🔑 密钥分层模型
@@ -126,15 +136,26 @@
```
secure-device-demo/
├── android-device/
│ └── DeviceCrypto.java # Android 端完整安全模块
├── android-app/ # Android 设备端(无登录体系)
│ └── app/src/main/java/com/secure/
│ ├── demo/activity/main/ # MainViewModel设备端演示链路
│ ├── demo/network/ # Retrofit + DeviceApi + ApiResponse
│ └── device/DeviceCrypto.java# TEE 安全模块Keystore 密钥对)
├── springboot-server/
├── web-client/ # Web 用户端(独立前端,有登录体系)
│ ├── index.html # 单页控制台
│ ├── style.css
│ └── app.js # fetch 调后端Bearer Token 认证
├── springboot-server/ # Spring Boot 后端(统一 API
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/com/secure/demo/
│ │ │ ├── SecureDemoApplication.java
│ │ │ ├── common/ # ApiResponse 统一响应 + 全局异常
│ │ │ ├── auth/ # AuthController + TokenService登录体系
│ │ │ ├── config/ # WebConfigCORS
│ │ │ ├── controller/
│ │ │ │ └── DeviceController.java
│ │ │ ├── service/
@@ -150,10 +171,7 @@ secure-device-demo/
│ │ └── resources/
│ │ └── application.properties
│ └── test/
── java/com/secure/demo/
│ │ └── IntegrationTest.java
│ └── resources/
│ └── application-test.properties
── java/com/secure/demo/IntegrationTest.java
└── README.md
```
@@ -162,13 +180,37 @@ secure-device-demo/
## 🚀 快速启动
### 后端
### 1. 后端
```bash
cd springboot-server
mvn spring-boot:run
mvn spring-boot:run # http://localhost:8080
```
### 2. Web 用户端(前后端分离演示)
```bash
cd web-client
python3 -m http.server 3000 # 浏览器打开 http://localhost:3000
```
浏览器打开后依次体验:注册/登录 → 绑定设备 → 发送短信 → 恢复授权 → 照片列表 → 下载解密。
所有请求携带 `Authorization: Bearer <token>`,可在页面底部看到统一响应 `{code, message, data}`
### 3. Android 设备端
```bash
cd android-app
./gradlew :app:assembleDebug # 模拟器运行,基地址 BuildConfig.API_BASE=http://10.0.2.2:8080
```
Android 端模拟真实操作:打开 App 仅做本地 TEE 自检(设备开机),之后由用户按
① 设备注册 → ② 绑定用户 → ③ 加密上传图片 → ④ 下载解密 → ⑤ 恢复出厂(销毁 TEE 密钥)
→ ⑥ 重新注册 → ⑦ 发送短信 → ⑧ 恢复授权 → ⑨ 恢复照片,逐步手动触发,
界面实时显示设备状态SN/公钥/注册/绑定/恢复进度)。
绑定/短信/恢复授权等「用户端」操作Android 以 `X-User-Id` 兼容方式演示,
真实场景请使用 Web 用户端Bearer Token
### 测试
```bash
@@ -176,28 +218,6 @@ cd springboot-server
mvn test
```
### Android 端集成
`DeviceCrypto.java` 复制到 Android 项目的对应包路径下,
`Application``MainActivity` 中初始化:
```java
DeviceCrypto crypto = new DeviceCrypto(context);
// 注册
String pubKey = crypto.getPublicKeyBase64();
String sn = crypto.getDeviceSN();
// → POST /api/device/register {sn, publicKeyBase64: pubKey}
// 拍照加密
byte[] photo = capturePhoto();
DeviceCrypto.EncryptedPayload payload = crypto.encryptData(photo);
String signature = crypto.signMetadata(sn + "|" + timestamp + "|" + photoId);
// → POST /api/photo/upload {sn, photoId, ciphertextBase64, ivBase64,
// dekBase64: payload.dekBase64,
// metadataSignature: signature, metadata}
```
---
## 🛡️ 安全分析
@@ -224,30 +244,145 @@ String signature = crypto.signMetadata(sn + "|" + timestamp + "|" + photoId);
---
## ❓ 为什么 TEE 密钥销毁后依然能解密照片
> 这是本项目最常见的一个疑问Android 端执行「恢复出厂」销毁了 TEE 密钥,为什么照片还能被解密?答案不是 bug而是**信封加密设计使然**。
### 一句话结论
**照片内容从来就不是用 TEE 密钥加密的**TEE 密钥对只负责「设备身份认证」照片的加密密钥DEK由服务端用用户主密钥UK包裹后存于云端与设备上的 TEE 密钥完全解耦。
### 三层密钥职责对照
| 密钥 | 生成位置 | 算法 | 真正的职责 | 销毁后的影响 |
|---|---|---|---|---|
| **TEE 密钥对** | Android Keystore / StrongBox | RSA-2048 | **设备身份**:① 对元数据签名(防伪造)② 解密服务端下发的 Recovery Token / DEK | 设备失去「身份」,无法再签名上传、无法走恢复链路;**照片内容不受影响** |
| **UK 用户主密钥** | 服务端(用户注册时) | AES-256 | **解密钥匙**:加密存储所有照片的 DEK | 照片彻底无法解密(唯一关键) |
| **DEK 数据加密密钥** | 设备端(每张照片随机) | AES-256 | **真正加密照片内容** | 单张照片无法解密(由 UK 包裹后存服务端) |
### 密码学原理信封加密Envelope Encryption
照片的加密链路是 `DEK → 加密照片``UK → 包裹 DEK`TEE 私钥只出现在「签名」这一步:
```
设备端加密照片: 随机 DEK --AES-GCM--> 照片密文
元数据 --TEE私钥签名--> 防伪造
服务端存储: DEK --UK加密--> encDEK落库
照片密文 + encDEK + IV 永久存储
用户下载解密: encDEK --UK解密--> DEK --AES-GCM--> 照片明文
(全程不碰设备 TEE 密钥)
```
对应代码:
```169:195:android-app/app/src/main/java/com/secure/device/DeviceCrypto.java
public EncryptedPayload encryptData(byte[] plaintext) {
try {
// 1. 随机 DEK
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(AES_KEY_SIZE);
SecretKey dek = kg.generateKey();
...
// 3. AES-256-GCM 加密
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
```
```364:372:springboot-server/src/main/java/com/secure/demo/controller/DeviceController.java
// 1. UK 解密 DEK
SecretKey dek = keyManagementService.unwrapDEK(photo.getEncryptedDekBase64(), userId);
// 2. DEK 解密照片
byte[] plaintext = AesGcmUtil.decrypt(
ciphertextBase64,
photo.getIvBase64(),
dek
);
```
### 那「恢复出厂」到底破坏了什么
销毁 TEE 密钥破坏的是**「设备继续使用 / 恢复的授权能力」**,而不是「已有数据的解密能力」:
- ✅ 旧私钥没了 → 旧设备**无法再签名上传**、**无法解密云端下发的 Recovery Token / DEK**
- ✅ 服务端随即**停用旧设备**
- ✅ 想找回数据,必须重走完整恢复链路:`重新注册 → 短信验证 → 恢复授权(新公钥加密下发 Token→ 恢复照片`。
### 类比
TEE 密钥是「**门禁卡**」UK / DEK 才是「**保险柜钥匙**」。门禁卡销毁后你进不了门(失去身份),但保险柜里的东西和钥匙都还在(数据可解密)。
### 安全含义
- 照片的解密能力**始终由云端 UK + DEK 掌控**,天然支持「换机恢复」「多端访问」。
- 真正的数据安全边界是 **UK**:一旦服务端 UK 被攻破,所有照片都能被解开——这正是 README 末尾强调「UK 生产环境必须上 KMS 托管」的原因。
- TEE 密钥的定位是「设备身份凭证」,用于**防伪造、防设备被冒用**,而不是「数据加密钥匙」。
---
## ⚠️ 生产环境注意事项
| Demo 简化 | 生产环境应改为 |
|---|---|
| UK 直接存 DB | KMS 托管(阿里云 KMS / AWS KMS |
| 短信码随机生成 | 对接腾讯云/阿里云短信服务 |
| 内存 ConcurrentHashMap | JPA + PostgreSQL/MySQL |
| 设备/用户/照片索引已用 JPA + MySQL 持久化(见下表) | Redis 缓存热数据、读写分离 |
| 明文 DEK 传输 | 设备端用服务端公钥加密 DEK 后传输 |
| 无验签实现 | 服务端用设备公钥验证 ECDSA 签名 |
| 无频率限制 | Redis 限流(短信/API |
| 无审计日志 | 所有密钥操作写审计表 |
| Recovery Token 无过期 | 加 5 分钟时间窗口 + Redis 防重放 |
### 数据持久化说明JPA + MySQL
设备状态、用户、照片索引已落库,**服务重启 / App 重启均不丢失注册与绑定状态**
| 表名 | 对应实体 | 存储内容 |
|---|---|---|
| `device` | `Device` | 设备注册/绑定状态SN 唯一、公钥、userId、active |
| `app_user` | `User` | 用户 + UK + 登录密码 |
| `encrypted_photo` | `EncryptedPhoto` | 照片元数据(密文仍落盘 `uploads/` |
| `user_photo` | `UserPhoto` | 用户 ↔ 照片索引 |
- 数据库脚本:`springboot-server/sql/schema.sql`(含建库建表 DDL
- 依赖 `spring.jpa.hibernate.ddl-auto=update` 会在启动时自动建表,也可手动执行 `schema.sql`。
- **仍为内存态(短时效,无需持久化)**短信验证码、Recovery Token nonce 防重放集合、登录 Token重启失效符合预期。生产环境这三者应迁至 Redis。
---
## 📋 API 接口清单
所有响应统一为 `ApiResponse{code, message, data}`code=0 成功)。
用户端接口鉴权优先级:`Authorization: Bearer <token>`(真实场景)> `X-User-Id`Android demo 兼容)> `body.userId`(测试兼容)。
### 公开(无需鉴权)
| Method | Path | 说明 |
|---|---|---|
| POST | `/api/device/register` | 设备注册(SN + 公钥 |
| POST | `/api/device/bind` | 用户绑定设备 |
| POST | `/api/device/sms/send` | 发送短信验证码 |
| POST | `/api/photo/upload` | 上传加密照片 |
| POST | `/api/device/recover` | 恢复设备(短信验证) |
| POST | `/api/photo/recover` | 恢复后获取照片 DEK |
| GET | `/api/photo/{id}/decrypt` | 用户下载并解密照片 |
| POST | `/api/auth/register` | 用户注册(返回 Token |
| POST | `/api/auth/login` | 用户登录(返回 Token |
| POST | `/api/auth/logout` | 登出(注销 Token |
| GET | `/api/health` | 健康检查 |
### 设备端(无登录,信任边界 = TEE 密钥 / SN / Recovery Token
| Method | Path | 说明 |
|---|---|---|
| GET | `/api/device/status?sn=xxx` | 查询设备注册/绑定状态App 启动复用,免重复注册绑定) |
| POST | `/api/device/register` | 设备注册SN + 公钥,幂等:同 SN 同公钥返回原 deviceId |
| POST | `/api/photo/upload` | 上传加密照片(设备签名验签) |
| POST | `/api/photo/recover` | 恢复后获取照片 DEK |
> **注册/绑定幂等说明**`/api/device/register` 对「同一 SN + 相同公钥」幂等——App 重启后重复注册不会生成新 deviceId也不会误停用自己仅当公钥变化恢复出厂时才停旧换新。App 启动时建议先调 `GET /api/device/status?sn=xxx`,若 `registered=true` 且 `bound=true`,直接复用返回的 `deviceId`/`userId`,跳过注册与绑定。
### 用户端(需要登录 Token
| Method | Path | 说明 |
|---|---|---|
| POST | `/api/device/bind` | 绑定设备SN |
| POST | `/api/device/sms/send` | 发送短信验证码(登录态防轰炸) |
| POST | `/api/device/recover` | 恢复授权(短信 + SN 双因子) |
| GET | `/api/photo/{photoId}/decrypt` | 下载并解密照片 |
| GET | `/api/user/photos` | 我的照片列表 |
| GET | `/api/user/devices` | 我的设备列表 |