feat: 新增安全设备 Demo 及 Android 端 TEE 加解密模块
This commit is contained in:
334
android-device/DeviceCrypto.java
Normal file
334
android-device/DeviceCrypto.java
Normal file
@@ -0,0 +1,334 @@
|
||||
package com.secure.device;
|
||||
|
||||
import android.content.Context;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
/**
|
||||
* 设备端安全模块(Android)
|
||||
*
|
||||
* 核心设计:
|
||||
* - TEE 密钥对(RSA-2048):私钥不可导出,用于解密服务端下发数据 + 签名
|
||||
* - 每次拍照生成随机 DEK(AES-256):用后即弃
|
||||
* - DEK 明文通过 HTTPS 传给服务端,服务端用 UK 加密后存储
|
||||
*
|
||||
* 安全边界:
|
||||
* - 私钥永不出 TEE
|
||||
* - SN 仅做身份标识,不参与加密
|
||||
* - 恢复出厂 → 密钥销毁 → 新密钥对 → 用户短信验证恢复
|
||||
*/
|
||||
public class DeviceCrypto {
|
||||
|
||||
// ==================== 常量 ====================
|
||||
|
||||
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
|
||||
private static final String KEY_ALIAS = "device_tee_key";
|
||||
private static final String KEY_ALGO = KeyProperties.KEY_ALGORITHM_RSA;
|
||||
private static final int RSA_KEY_SIZE = 2048;
|
||||
private static final String SIGN_ALGO = "SHA256withRSA";
|
||||
private static final String RSA_TRANSFORM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";
|
||||
private static final int AES_KEY_SIZE = 256;
|
||||
private static final int GCM_IV_LENGTH = 12;
|
||||
private static final int GCM_TAG_LENGTH = 128;
|
||||
|
||||
private final Context context;
|
||||
private KeyStore keyStore;
|
||||
|
||||
// ==================== 构造与初始化 ====================
|
||||
|
||||
public DeviceCrypto(Context context) {
|
||||
this.context = context;
|
||||
initKeyStore();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 AndroidKeyStore,如不存在密钥对则生成
|
||||
*
|
||||
* 安全属性:
|
||||
* - PURPOSE_DECRYPT + PURPOSE_SIGN(私钥只用于解密下发数据 + 签名)
|
||||
* - StrongBox 优先(硬件安全模块)
|
||||
* - 不可导出
|
||||
*/
|
||||
private void initKeyStore() {
|
||||
try {
|
||||
keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
|
||||
keyStore.load(null);
|
||||
|
||||
if (!keyStore.containsAlias(KEY_ALIAS)) {
|
||||
generateTeeKeyPair();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("KeyStore init failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 TEE / StrongBox 中生成 RSA-2048 密钥对
|
||||
*/
|
||||
private void generateTeeKeyPair() {
|
||||
try {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance(
|
||||
KEY_ALGO, KEYSTORE_PROVIDER);
|
||||
|
||||
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
|
||||
KEY_ALIAS,
|
||||
KeyProperties.PURPOSE_DECRYPT |
|
||||
KeyProperties.PURPOSE_SIGN
|
||||
)
|
||||
.setKeySize(RSA_KEY_SIZE)
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
|
||||
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
|
||||
.setUserAuthenticationRequired(false) // 无登录体系
|
||||
.setIsStrongBoxBacked(true) // 优先 StrongBox
|
||||
.build();
|
||||
|
||||
kpg.initialize(spec);
|
||||
kpg.generateKeyPair();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("TEE key gen failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 公钥导出 ====================
|
||||
|
||||
/**
|
||||
* 获取设备公钥(Base64 编码)
|
||||
* 公钥可导出,用于:
|
||||
* - 上传服务端存储
|
||||
* - 服务端加密下发数据
|
||||
*/
|
||||
public String getPublicKeyBase64() {
|
||||
try {
|
||||
KeyStore.PrivateKeyEntry entry =
|
||||
(KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
|
||||
PublicKey publicKey = entry.getCertificate().getPublicKey();
|
||||
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Get public key failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备 SN
|
||||
* 实际实现:android.os.Build.getSerial()
|
||||
*/
|
||||
public String getDeviceSN() {
|
||||
// return android.os.Build.getSerial();
|
||||
return "SN-DEMO-001";
|
||||
}
|
||||
|
||||
// ==================== 数据信封加密 ====================
|
||||
|
||||
/**
|
||||
* 加密敏感数据(如照片)
|
||||
*
|
||||
* 流程:
|
||||
* 1. 生成随机 AES-256 DEK
|
||||
* 2. 生成随机 12 字节 IV
|
||||
* 3. AES-256-GCM 加密数据
|
||||
* 4. 返回密文 + IV + 明文DEK(DEK 由服务端用 UK 加密存储)
|
||||
*
|
||||
* @return EncryptedPayload
|
||||
*/
|
||||
public EncryptedPayload encryptData(byte[] plaintext) {
|
||||
try {
|
||||
// 1. 随机 DEK
|
||||
KeyGenerator kg = KeyGenerator.getInstance("AES");
|
||||
kg.init(AES_KEY_SIZE);
|
||||
SecretKey dek = kg.generateKey();
|
||||
|
||||
// 2. 随机 IV
|
||||
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||
java.security.SecureRandom random = new java.security.SecureRandom();
|
||||
random.nextBytes(iv);
|
||||
|
||||
// 3. AES-256-GCM 加密
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, dek, gcmSpec);
|
||||
byte[] ciphertext = cipher.doFinal(plaintext);
|
||||
|
||||
return new EncryptedPayload(
|
||||
Base64.getEncoder().encodeToString(ciphertext),
|
||||
Base64.getEncoder().encodeToString(iv),
|
||||
Base64.getEncoder().encodeToString(dek.getEncoded())
|
||||
);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Encrypt failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 元数据签名 ====================
|
||||
|
||||
/**
|
||||
* 对元数据做 RSA 签名(防伪造、防篡改)
|
||||
*
|
||||
* 签名内容示例:"SN-xxx|timestamp|photoId"
|
||||
* 服务端用设备公钥验签
|
||||
*/
|
||||
public String signMetadata(String metadata) {
|
||||
try {
|
||||
KeyStore.PrivateKeyEntry entry =
|
||||
(KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
|
||||
PrivateKey privateKey = entry.getPrivateKey();
|
||||
|
||||
Signature sig = Signature.getInstance(SIGN_ALGO);
|
||||
sig.initSign(privateKey);
|
||||
sig.update(metadata.getBytes("UTF-8"));
|
||||
byte[] signature = sig.sign();
|
||||
|
||||
return Base64.getEncoder().encodeToString(signature);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Sign failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 恢复流程 ====================
|
||||
|
||||
/**
|
||||
* 恢复出厂后,服务端下发用新公钥加密的 Recovery Token
|
||||
* 本方法用 TEE 私钥解密,获取 Token
|
||||
*
|
||||
* @param encryptedToken 服务端用设备公钥加密的 Token
|
||||
* @return 明文 Token(格式:userId|deviceId|timestamp|nonce)
|
||||
*/
|
||||
public String decryptRecoveryToken(byte[] encryptedToken) {
|
||||
try {
|
||||
KeyStore.PrivateKeyEntry entry =
|
||||
(KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
|
||||
PrivateKey privateKey = entry.getPrivateKey();
|
||||
|
||||
Cipher cipher = Cipher.getInstance(RSA_TRANSFORM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
byte[] decrypted = cipher.doFinal(encryptedToken);
|
||||
|
||||
return new String(decrypted, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Decrypt recovery token failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密服务端下发的加密 DEK
|
||||
* 恢复流程中,服务端用设备公钥加密每个照片的 DEK
|
||||
*
|
||||
* @param encryptedDek 加密的 DEK(Base64)
|
||||
* @return 明文 DEK 字节
|
||||
*/
|
||||
public byte[] decryptDek(String encryptedDekBase64) {
|
||||
try {
|
||||
KeyStore.PrivateKeyEntry entry =
|
||||
(KeyStore.PrivateKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
|
||||
PrivateKey privateKey = entry.getPrivateKey();
|
||||
|
||||
Cipher cipher = Cipher.getInstance(RSA_TRANSFORM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
return cipher.doFinal(Base64.getDecoder().decode(encryptedDekBase64));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Decrypt DEK failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用解出的 DEK 解密照片
|
||||
*/
|
||||
public byte[] decryptPhoto(String ciphertextBase64, String ivBase64, byte[] dekBytes) {
|
||||
try {
|
||||
SecretKey dek = new javax.crypto.spec.SecretKeySpec(dekBytes, "AES");
|
||||
byte[] iv = Base64.getDecoder().decode(ivBase64);
|
||||
byte[] ciphertext = Base64.getDecoder().decode(ciphertextBase64);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||
cipher.init(Cipher.DECRYPT_MODE, dek, spec);
|
||||
|
||||
return cipher.doFinal(ciphertext);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Decrypt photo failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 数据模型 ====================
|
||||
|
||||
/**
|
||||
* 加密后的数据载体
|
||||
*/
|
||||
public static class EncryptedPayload {
|
||||
public final String ciphertextBase64; // AES-GCM 密文
|
||||
public final String ivBase64; // GCM IV
|
||||
public final String dekBase64; // 明文 DEK(HTTPS 传输给服务端)
|
||||
|
||||
public EncryptedPayload(String ciphertextBase64, String ivBase64, String dekBase64) {
|
||||
this.ciphertextBase64 = ciphertextBase64;
|
||||
this.ivBase64 = ivBase64;
|
||||
this.dekBase64 = dekBase64;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备注册请求体
|
||||
*/
|
||||
public static class DeviceRegisterRequest {
|
||||
public String sn;
|
||||
public String publicKeyBase64;
|
||||
|
||||
public DeviceRegisterRequest(String sn, String publicKeyBase64) {
|
||||
this.sn = sn;
|
||||
this.publicKeyBase64 = publicKeyBase64;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传加密照片请求体
|
||||
*/
|
||||
public static class UploadPhotoRequest {
|
||||
public String sn;
|
||||
public String photoId;
|
||||
public String ciphertextBase64;
|
||||
public String ivBase64;
|
||||
public String dekBase64;
|
||||
public String metadataSignature;
|
||||
public String metadata;
|
||||
|
||||
public UploadPhotoRequest(String sn, String photoId, String ciphertextBase64,
|
||||
String ivBase64, String dekBase64,
|
||||
String metadataSignature, String metadata) {
|
||||
this.sn = sn;
|
||||
this.photoId = photoId;
|
||||
this.ciphertextBase64 = ciphertextBase64;
|
||||
this.ivBase64 = ivBase64;
|
||||
this.dekBase64 = dekBase64;
|
||||
this.metadataSignature = metadataSignature;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复请求体(用户已通过短信验证)
|
||||
*/
|
||||
public static class RecoveryRequest {
|
||||
public String sn;
|
||||
public String userId;
|
||||
public String smsCode;
|
||||
public String newPublicKeyBase64;
|
||||
|
||||
public RecoveryRequest(String sn, String userId, String smsCode, String newPublicKeyBase64) {
|
||||
this.sn = sn;
|
||||
this.userId = userId;
|
||||
this.smsCode = smsCode;
|
||||
this.newPublicKeyBase64 = newPublicKeyBase64;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user