feat: 新增安全设备 Demo 及 Android 端 TEE 加解密模块

This commit is contained in:
2026-08-20 16:11:53 +08:00
commit bb33f3b5ae
31 changed files with 2803 additions and 0 deletions

View File

@@ -0,0 +1,397 @@
package com.secure.demo.controller;
import com.secure.demo.crypto.AesGcmUtil;
import com.secure.demo.crypto.RsaUtil;
import com.secure.demo.model.Device;
import com.secure.demo.model.EncryptedPhoto;
import com.secure.demo.service.DeviceBindingService;
import com.secure.demo.service.DeviceBindingService.RecoveryResponse;
import com.secure.demo.service.KeyManagementService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.crypto.SecretKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 设备安全 API 控制器
*
* ┌─────────────────────────────────────────────────────────────┐
* │ 接口清单 │
* ├─────────────────────────────────────────────────────────────┤
* │ 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 用户下载并解密照片 │
* └─────────────────────────────────────────────────────────────┘
*/
@RestController
@RequestMapping("/api")
public class DeviceController {
private final KeyManagementService keyManagementService;
private final DeviceBindingService deviceBindingService;
// 照片数据库(生产环境替换为 JPA/MinIO
private final Map<String, EncryptedPhoto> photoDB = new ConcurrentHashMap<>();
public DeviceController(KeyManagementService keyManagementService,
DeviceBindingService deviceBindingService) {
this.keyManagementService = keyManagementService;
this.deviceBindingService = deviceBindingService;
}
// ==================== 1. 设备注册 ====================
/**
* 设备首次启动 / 恢复出厂后调用
*
* Request:
* {
* "sn": "SN-DEMO-001",
* "publicKeyBase64": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
* }
*
* Response:
* {
* "deviceId": "...",
* "sn": "SN-DEMO-001",
* "message": "Device registered successfully"
* }
*/
@PostMapping("/device/register")
public ResponseEntity<?> registerDevice(@RequestBody Map<String, String> req) {
String sn = req.get("sn");
String publicKeyBase64 = req.get("publicKeyBase64");
if (sn == null || publicKeyBase64 == null) {
return ResponseEntity.badRequest().body(Map.of("error", "sn and publicKeyBase64 required"));
}
Device device = deviceBindingService.registerDevice(sn, publicKeyBase64);
return ResponseEntity.ok(Map.of(
"deviceId", device.getDeviceId(),
"sn", device.getSn(),
"message", "Device registered successfully"
));
}
// ==================== 2. 用户绑定设备 ====================
/**
* 用户登录后绑定 SN
*
* Request:
* {
* "userId": "user-001",
* "sn": "SN-DEMO-001"
* }
*/
@PostMapping("/device/bind")
public ResponseEntity<?> bindDevice(@RequestBody Map<String, String> req) {
String userId = req.get("userId");
String sn = req.get("sn");
if (userId == null || sn == null) {
return ResponseEntity.badRequest().body(Map.of("error", "userId and sn required"));
}
// Demo用户不存在则自动注册生产环境从用户体系获取
if (!keyManagementService.userExists(userId)) {
String phone = req.getOrDefault("phone", "13800138000");
keyManagementService.registerUser(userId, phone);
}
deviceBindingService.bindDeviceToUser(userId, sn);
return ResponseEntity.ok(Map.of("message", "Device bound to user successfully"));
}
// ==================== 3. 短信验证码 ====================
/**
* 发送短信验证码
*
* Request: { "phone": "13800138000" }
*/
@PostMapping("/device/sms/send")
public ResponseEntity<?> sendSms(@RequestBody Map<String, String> req) {
String phone = req.get("phone");
if (phone == null) {
return ResponseEntity.badRequest().body(Map.of("error", "phone required"));
}
deviceBindingService.sendSmsCode(phone);
return ResponseEntity.ok(Map.of("message", "SMS code sent (check server logs for demo code)"));
}
// ==================== 4. 上传加密照片 ====================
/**
* 设备上传加密照片(信封加密)
*
* Request:
* {
* "sn": "SN-DEMO-001",
* "photoId": "photo-001",
* "ciphertextBase64": "...",
* "ivBase64": "...",
* "dekBase64": "...", <-- 明文 DEKHTTPS 传输)
* "metadataSignature": "...", <-- 设备私钥签名
* "metadata": "SN-DEMO-001|ts|photo-001"
* }
*
* 服务端处理:
* 1. 查找设备 & 验证归属
* 2. 验签TODO用设备公钥验证 metadataSignature
* 3. 用 UK 加密 DEK信封加密
* 4. 存储密文 + 加密后的 DEK
*/
@PostMapping("/photo/upload")
public ResponseEntity<?> uploadPhoto(@RequestBody Map<String, String> req) {
String sn = req.get("sn");
String photoId = req.getOrDefault("photoId", UUID.randomUUID().toString().replace("-", ""));
String ciphertextBase64 = req.get("ciphertextBase64");
String ivBase64 = req.get("ivBase64");
String dekBase64 = req.get("dekBase64");
String metadataSignature = req.get("metadataSignature");
String metadata = req.get("metadata");
// 1. 查找设备
Device device = deviceBindingService.findDeviceBySn(sn);
if (device == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", "Device not registered for SN: " + sn));
}
if (!device.isActive()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "Device not active. Please complete recovery."));
}
String userId = device.getUserId();
if (userId == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "Device not bound to any user"));
}
// 2. TODO: 验签
// PublicKey devicePubKey = RsaUtil.publicKeyFromBase64(device.getPublicKeyBase64());
// boolean valid = SignatureUtil.verify(metadata, metadataSignature, devicePubKey);
// if (!valid) return 403;
// 3. 用 UK 加密 DEK服务端永远只存 "UK 加密后的 DEK"
String encryptedDek = keyManagementService.wrapDEK(dekBase64, userId);
// 4. 存储
EncryptedPhoto photo = new EncryptedPhoto(
photoId, device.getDeviceId(), userId,
ciphertextBase64, ivBase64,
encryptedDek, metadataSignature
);
photoDB.put(photoId, photo);
deviceBindingService.addPhotoToUser(userId, photoId);
return ResponseEntity.ok(Map.of(
"photoId", photoId,
"message", "Photo uploaded and encrypted successfully"
));
}
// ==================== 5. 恢复设备(核心) ====================
/**
* 恢复出厂后重新绑定 + 授权
*
* Request:
* {
* "userId": "user-001",
* "sn": "SN-DEMO-001",
* "smsCode": "123456",
* "newPublicKeyBase64": "MFkwEwYHKoZIzj0CAQY..."
* }
*
* Response:
* {
* "deviceId": "...",
* "encryptedRecoveryToken": "...", <-- 用新设备公钥加密
* "nonce": "...",
* "message": "..."
* }
*
* 安全链路:
* 1. 验证短信 → 确认是用户本人
* 2. 确认 SN 归属 → 确认设备所有权
* 3. 更新公钥 → 恢复出厂后密钥对已变
* 4. 生成 Token → 用新公钥加密下发
* 5. 旧设备自动失效TEE 私钥已销毁)
*/
@PostMapping("/device/recover")
public ResponseEntity<?> recoverDevice(@RequestBody Map<String, String> req) {
String userId = req.get("userId");
String sn = req.get("sn");
String smsCode = req.get("smsCode");
String newPublicKeyBase64 = req.get("newPublicKeyBase64");
if (userId == null || sn == null || smsCode == null || newPublicKeyBase64 == null) {
return ResponseEntity.badRequest()
.body(Map.of("error", "userId, sn, smsCode, newPublicKeyBase64 all required"));
}
try {
RecoveryResponse resp = deviceBindingService.recoverDevice(
userId, sn, smsCode, newPublicKeyBase64
);
return ResponseEntity.ok(Map.of(
"deviceId", resp.deviceId,
"encryptedRecoveryToken", resp.encryptedRecoveryToken,
"nonce", resp.nonce,
"message", "Recovery authorized. Device can now fetch DEKs."
));
} catch (SecurityException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("error", e.getMessage()));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
// ==================== 6. 恢复后获取照片 DEK ====================
/**
* 设备用 Recovery Token 获取该用户所有照片的 DEK
* 服务端用设备当前公钥逐一加密 DEK 后下发
*
* Request:
* {
* "deviceId": "...",
* "recoveryToken": "...",
* "userId": "user-001"
* }
*
* Response:
* {
* "deviceId": "...",
* "photoCount": 3,
* "deks": [
* {"photoId": "p1", "encryptedDekBase64": "..."},
* ...
* ]
* }
*/
@PostMapping("/photo/recover")
public ResponseEntity<?> recoverPhotos(@RequestBody Map<String, String> req) {
String deviceId = req.get("deviceId");
String recoveryToken = req.get("recoveryToken");
String userId = req.get("userId");
if (deviceId == null || recoveryToken == null || userId == null) {
return ResponseEntity.badRequest()
.body(Map.of("error", "deviceId, recoveryToken, userId all required"));
}
// 1. 验证设备
Device device = deviceBindingService.getDevice(deviceId);
if (device == null || !device.isActive()) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "Device not active"));
}
if (!userId.equals(device.getUserId())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "Token does not match device owner"));
}
// 2. TODO: 解析并验证 recoveryToken 中的 nonce + 时间窗口
// Token 格式: userId|deviceId|timestamp|nonce
// 验证 deviceId 匹配、时间在窗口内、nonce 未重放
// 3. 获取设备公钥,用于包裹 DEK 下发
PublicKey devicePubKey = RsaUtil.publicKeyFromBase64(device.getPublicKeyBase64());
// 4. 遍历照片UK 解 DEK → 设备公钥加密 DEK → 下发
List<Map<String, String>> dekList = new ArrayList<>();
for (String photoId : deviceBindingService.getUserPhotoIds(userId)) {
EncryptedPhoto photo = photoDB.get(photoId);
if (photo == null) continue;
// 用 UK 解出明文 DEK
SecretKey dek = keyManagementService.unwrapDEK(photo.getEncryptedDekBase64(), userId);
// 用设备公钥加密 DEK设备私钥才能解
String encryptedDek = RsaUtil.encryptBase64(dek.getEncoded(), devicePubKey);
dekList.add(Map.of(
"photoId", photoId,
"encryptedDekBase64", encryptedDek
));
}
return ResponseEntity.ok(Map.of(
"deviceId", deviceId,
"photoCount", dekList.size(),
"deks", dekList
));
}
// ==================== 7. 用户下载照片(解密) ====================
/**
* 用户端(已登录)下载并解密照片
*
* 流程:
* 1. 鉴权X-User-Id Header
* 2. 检查 photo 归属
* 3. UK 解密 DEK → DEK 解密照片
* 4. 明文通过 HTTPS 返回
*/
@GetMapping("/photo/{photoId}/decrypt")
public ResponseEntity<?> downloadAndDecrypt(@PathVariable String photoId,
@RequestHeader("X-User-Id") String userId) {
EncryptedPhoto photo = photoDB.get(photoId);
if (photo == null) {
return ResponseEntity.notFound().build();
}
// 归属检查
if (!userId.equals(photo.getUserId())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "Not your photo"));
}
try {
// 1. UK 解密 DEK
SecretKey dek = keyManagementService.unwrapDEK(photo.getEncryptedDekBase64(), userId);
// 2. DEK 解密照片
byte[] plaintext = AesGcmUtil.decrypt(
photo.getCiphertextBase64(),
photo.getIvBase64(),
dek
);
// 3. 返回明文(生产环境用 StreamingResponseBody 大文件流式传输)
return ResponseEntity.ok()
.header("Content-Type", "application/octet-stream")
.header("Content-Disposition",
"attachment; filename=\"" + photoId + ".bin\"")
.body(Base64.getEncoder().encodeToString(plaintext));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "Decryption failed: " + e.getMessage()));
}
}
// ==================== 8. 健康检查 ====================
@GetMapping("/health")
public ResponseEntity<?> health() {
return ResponseEntity.ok(Map.of("status", "ok", "service", "secure-device-demo"));
}
}