- 新增 Web 用户端(登录体系 + 统一 REST API 调用) - 后端增加用户认证、统一 ApiResponse、CORS 支持 - Android 设备端迁移至 MVVM + DataBinding + Retrofit 网络层 - 完善 README 架构说明与密码学原理文档 - 新增 .gitignore 与持久化数据表说明
389 lines
20 KiB
Markdown
389 lines
20 KiB
Markdown
# 🔐 安全设备 Demo — 前后端分离(Web 用户端 + Android 设备端 + Spring Boot)
|
||
|
||
> **场景**:高权限设备端(无登录)+ 用户端(有登录体系)+ SN 绑定 + 私密照片加密
|
||
> **目标**:设备端 TEE 密钥不可导出、服务端零知识、恢复出厂后可安全恢复
|
||
> **架构**:前后端分离 —— Web 用户端(真实登录体系)与 Android 设备端(无登录)都通过统一 REST API 访问后端
|
||
|
||
---
|
||
|
||
## 📐 架构总览
|
||
|
||
```
|
||
┌─────────────────────┐ HTTPS ┌──────────────────────┐
|
||
│ Android 设备端 │ ◄──────────────────► │ Spring Boot 后端 │
|
||
│ (无登录/高权限) │ │ ──────────────── │
|
||
└─────────────────────┘ │ · 统一响应 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 鉴权
|
||
|
||
---
|
||
|
||
## 🔑 密钥分层模型
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ │
|
||
│ User Key (UK) — 用户主密钥 │
|
||
│ ├── 生成:用户注册时创建(AES-256) │
|
||
│ ├── 存储:服务端加密存储(生产环境用 KMS 托管) │
|
||
│ └── 用途:加密所有 DEK │
|
||
│ │
|
||
│ Data Encryption Key (DEK) — 每照片一个 │
|
||
│ ├── 生成:设备端 SecureRandom(每次随机) │
|
||
│ ├── 加密:UK 加密后存服务端 │
|
||
│ └── 用途:AES-256-GCM 加密照片 │
|
||
│ │
|
||
│ Device Key Pair — TEE 硬件密钥 │
|
||
│ ├── 生成:Android Keystore(StrongBox 优先) │
|
||
│ ├── 私钥:永不导出,仅签名/解密 │
|
||
│ ├── 公钥:上传服务端,用于加密下发 │
|
||
│ └── 销毁:恢复出厂时自动清除 │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 完整流程
|
||
|
||
### Phase 1:设备注册 + 用户绑定
|
||
|
||
```
|
||
设备端 后端 用户端
|
||
│ │ │
|
||
│── 生成 TEE 密钥对 ──────────│ │
|
||
│── POST /api/device/register │ │
|
||
│ {sn, publicKey} ──►│ │
|
||
│ │── 存储 device(sn, pubKey) │
|
||
│◄── {deviceId} ─────────────│ │
|
||
│ │ │
|
||
│ │◄── POST /api/device/bind │
|
||
│ │ {userId, sn} │
|
||
│ │── 绑定 userId ↔ deviceId │
|
||
│ │◄── {ok} ────────────────────│
|
||
```
|
||
|
||
### Phase 2:拍照 → 信封加密 → 上传
|
||
|
||
```
|
||
设备端 后端
|
||
│ │
|
||
│── SecureRandom → DEK (256bit)│
|
||
│── AES-GCM(photo, DEK) → ct │
|
||
│── Sign(metadata, PrivKey) │
|
||
│ │
|
||
│── POST /api/photo/upload │
|
||
│ {sn, ct, iv, dek, sig} ──►│
|
||
│ │── 验签(PubKey)
|
||
│ │── wrapDEK(dek, UK) → encDEK
|
||
│ │── 存储 {ct, iv, encDEK, sig}
|
||
│◄── {photoId} ───────────────│
|
||
```
|
||
|
||
### Phase 3:恢复出厂 → 短信验证 → 恢复
|
||
|
||
```
|
||
设备端(新) 后端 用户端
|
||
│ │ │
|
||
│── 新 TEE 密钥对 │ │
|
||
│── POST /api/device/register │ │
|
||
│ {sn, newPubKey} ──►│(旧设备自动停用) │
|
||
│◄── {newDeviceId} ───────────│ │
|
||
│ │ │
|
||
│ │◄── POST /api/device/sms/send │
|
||
│ │ {phone} │
|
||
│ │── 发送短信 │
|
||
│ │ │
|
||
│ │◄── POST /api/device/recover │
|
||
│ │ {userId, sn, smsCode, │
|
||
│ │ newPubKey} │
|
||
│ │── ① 验证短信 │── 输入验证码
|
||
│ │── ② 确认 SN 归属 │
|
||
│ │── ③ 生成 Recovery Token │
|
||
│ │── ④ RSA(newPubKey, Token) │
|
||
│◄── {encToken, nonce} ────────│ │
|
||
│ │ │
|
||
│── PrivKey 解密 Token │ │
|
||
│── POST /api/photo/recover │ │
|
||
│ {deviceId, token} ──►│ │
|
||
│ │── ① 验证 Token │
|
||
│ │── ② 遍历照片 │
|
||
│ │── ③ UK 解 DEK → PubKey 加密 │
|
||
│◄── [{photoId, encDEK}, ...] ─│ │
|
||
│ │ │
|
||
│── PrivKey 解密每个 DEK │ │
|
||
│── AES-GCM 解密照片 │ │
|
||
│── ✅ 照片恢复完成 │ │
|
||
```
|
||
|
||
---
|
||
|
||
## 📁 项目结构
|
||
|
||
```
|
||
secure-device-demo/
|
||
├── android-app/ # Android 设备端(无登录体系)
|
||
│ └── app/src/main/java/com/secure/
|
||
│ ├── demo/activity/main/ # MainViewModel:设备端演示链路
|
||
│ ├── demo/network/ # Retrofit + DeviceApi + ApiResponse
|
||
│ └── device/DeviceCrypto.java# TEE 安全模块(Keystore 密钥对)
|
||
│
|
||
├── 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/ # WebConfig(CORS)
|
||
│ │ │ ├── controller/
|
||
│ │ │ │ └── DeviceController.java
|
||
│ │ │ ├── service/
|
||
│ │ │ │ ├── KeyManagementService.java
|
||
│ │ │ │ └── DeviceBindingService.java
|
||
│ │ │ ├── model/
|
||
│ │ │ │ ├── User.java
|
||
│ │ │ │ ├── Device.java
|
||
│ │ │ │ └── EncryptedPhoto.java
|
||
│ │ │ └── crypto/
|
||
│ │ │ ├── AesGcmUtil.java
|
||
│ │ │ └── RsaUtil.java
|
||
│ │ └── resources/
|
||
│ │ └── application.properties
|
||
│ └── test/
|
||
│ └── java/com/secure/demo/IntegrationTest.java
|
||
│
|
||
└── README.md
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 快速启动
|
||
|
||
### 1. 后端
|
||
|
||
```bash
|
||
cd springboot-server
|
||
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
|
||
cd springboot-server
|
||
mvn test
|
||
```
|
||
|
||
---
|
||
|
||
## 🛡️ 安全分析
|
||
|
||
### 攻击场景 vs 防护
|
||
|
||
| 攻击场景 | 结果 | 原因 |
|
||
|---|---|---|
|
||
| 设备被 root | 拿不到 TEE 私钥 | Keystore 硬件保护 |
|
||
| 设备被盗 | 无法解密历史数据 | 无用户登录态 |
|
||
| 恢复出厂 | 旧密钥销毁 | TEE 安全擦除 |
|
||
| 服务端被拖库 | DEK 被 UK 加密 | 信封加密 |
|
||
| SN 被伪造 | 无法绑定/恢复 | 需短信验证 + SN 归属校验 |
|
||
| 短信被截获 | 仍需 SN 归属 | 多层校验 |
|
||
| 旧设备残留 | 已停用 | 重新注册时停用旧设备 |
|
||
|
||
### 安全原则
|
||
|
||
1. ✅ **设备零信任** — 设备只持有签名密钥,不持有解密密钥
|
||
2. ✅ **前向安全** — 每次恢复生成新密钥对
|
||
3. ✅ **信封加密** — DEK 永不明文存库
|
||
4. ✅ **短信 + SN 双因子** — 恢复必须两者同时通过
|
||
5. ✅ **一次性令牌** — Recovery Token 含 nonce + 时间窗口
|
||
|
||
---
|
||
|
||
## ❓ 为什么 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) |
|
||
| 短信码随机生成 | 对接腾讯云/阿里云短信服务 |
|
||
| 设备/用户/照片索引已用 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/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` | 我的设备列表 |
|