TongTongStudio 417c4989f7 feat(device): 设备状态查询增加 Challenge-Response 签名认证
- Android 端先获取挑战值,用 TEE 私钥签名后查询设备状态
- 服务端新增挑战值接口,校验时效、防重放并验签后才返回状态
2026-08-23 14:01:20 +08:00

🔐 安全设备 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 KeystoreStrongBox 优先)          │
│   ├── 私钥:永不导出,仅签名/解密                       │
│   ├── 公钥:上传服务端,用于加密下发                    │
│   └── 销毁:恢复出厂时自动清除                          │
│                                                         │
└─────────────────────────────────────────────────────────┘

🔄 完整流程

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/         # WebConfigCORS
│       │   │   ├── 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. 后端

cd springboot-server
mvn spring-boot:run        # http://localhost:8080

2. Web 用户端(前后端分离演示)

cd web-client
python3 -m http.server 3000    # 浏览器打开 http://localhost:3000

浏览器打开后依次体验:注册/登录 → 绑定设备 → 发送短信 → 恢复授权 → 照片列表 → 下载解密。 所有请求携带 Authorization: Bearer <token>,可在页面底部看到统一响应 {code, message, data}

3. Android 设备端

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

测试

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 → 包裹 DEKTEE 私钥只出现在「签名」这一步:

设备端加密照片: 随机 DEK --AES-GCM--> 照片密文
                元数据 --TEE私钥签名--> 防伪造

服务端存储:    DEK --UK加密--> encDEK落库
                照片密文 + encDEK + IV 永久存储

用户下载解密:  encDEK --UK解密--> DEK --AES-GCM--> 照片明文
                (全程不碰设备 TEE 密钥)

对应代码:

    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");
            // 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-IdAndroid 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=truebound=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 我的设备列表
Description
No description provided
Readme 255 KiB
Languages
Java 93.4%
JavaScript 3.9%
HTML 1.7%
CSS 1%