feat: 重构为前后端分离架构并完善设备端演示
- 新增 Web 用户端(登录体系 + 统一 REST API 调用) - 后端增加用户认证、统一 ApiResponse、CORS 支持 - Android 设备端迁移至 MVVM + DataBinding + Retrofit 网络层 - 完善 README 架构说明与密码学原理文档 - 新增 .gitignore 与持久化数据表说明
This commit is contained in:
233
README.md
233
README.md
@@ -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/ # WebConfig(CORS)
|
||||
│ │ │ ├── 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` | 我的设备列表 |
|
||||
|
||||
Reference in New Issue
Block a user