# 🔐 安全设备 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 `,可在页面底部看到统一响应 `{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 `(真实场景)> `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` | 我的设备列表 |