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,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.SecureDevice">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,73 @@
package com.secure.demo;
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.secure.device.DeviceCrypto;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* 最精简演示:在 KeyStore(TEE) 中生成 RSA-2048 密钥对,
* 完成「信封加密 → 解密还原」与「元数据签名 → 公钥验签」全流程,
* 无需任何后端即可在真机/模拟器上验证 DeviceCrypto 可用。
*/
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.log);
new Thread(() -> {
StringBuilder sb = new StringBuilder();
try {
DeviceCrypto crypto = new DeviceCrypto(this);
String pub = crypto.getPublicKeyBase64();
sb.append("设备公钥(前48字符):\n").append(pub, 0, Math.min(48, pub.length()))
.append("...\n\n");
// 1) 信封加密(模拟拍照)
String plaintext = "私密照片数据 Hello TEE!";
DeviceCrypto.EncryptedPayload p = crypto.encryptData(plaintext.getBytes("UTF-8"));
sb.append("原始数据: ").append(plaintext).append("\n");
sb.append("密文: ").append(p.ciphertextBase64, 0, Math.min(32, p.ciphertextBase64.length()))
.append("...\n");
sb.append("IV: ").append(p.ivBase64).append("\n");
sb.append("DEK(明文, 实际应仅经HTTPS给服务端): ")
.append(p.dekBase64, 0, Math.min(24, p.dekBase64.length())).append("...\n\n");
// 2) 服务端用 UK 加密 DEK 后下发,此处用相同 DEK 还原,验证解密链路
byte[] dekBytes = Base64.getDecoder().decode(p.dekBase64);
byte[] decrypted = crypto.decryptPhoto(p.ciphertextBase64, p.ivBase64, dekBytes);
sb.append("解密还原: ").append(new String(decrypted, "UTF-8")).append("\n\n");
// 3) 元数据签名 + 公钥验签
String metadata = crypto.getDeviceSN() + "|" + System.currentTimeMillis() + "|photo001";
String sig = crypto.signMetadata(metadata);
sb.append("元数据: ").append(metadata).append("\n");
sb.append("签名(前32字符): ").append(sig, 0, Math.min(32, sig.length())).append("...\n");
boolean ok = verify(metadata, sig, pub);
sb.append("验签结果: ").append(ok ? "通过 ✅" : "失败 ❌").append("\n");
} catch (Exception e) {
sb.append("异常: ").append(e).append("\n");
e.printStackTrace();
}
final String text = sb.toString();
runOnUiThread(() -> tv.setText(text));
}).start();
}
private boolean verify(String metadata, String sigBase64, String pubKeyBase64) throws Exception {
java.security.PublicKey pub = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(pubKeyBase64)));
Signature s = Signature.getInstance("SHA256withRSA");
s.initVerify(pub);
s.update(metadata.getBytes("UTF-8"));
return s.verify(Base64.getDecoder().decode(sigBase64));
}
}

View File

@@ -0,0 +1,358 @@
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私钥不可导出用于解密服务端下发数据 + 签名
* - 每次拍照生成随机 DEKAES-256用后即弃
* - DEK 明文通过 HTTPS 传给服务端,服务端用 UK 加密后存储
*
* 安全边界:
* - 私钥永不出 TEE
* - SN 仅做身份标识,不参与加密
* - 恢复出厂 → 密钥销毁 → 新密钥对 → 用户短信验证恢复
*
* 适配说明generateTeeKeyPair 优先使用 StrongBox失败时回退到普通 TEE
* 以保证在无 StrongBox 的模拟器/普通设备上也能运行。
*/
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 优先(硬件安全模块),无则回退 TEE
* - 不可导出
*/
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 strongBoxSpec = 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();
try {
kpg.initialize(strongBoxSpec);
kpg.generateKeyPair();
return;
} catch (Exception strongBoxUnavailable) {
// 回退到普通 TEE如模拟器 / 不支持 StrongBox 的设备)
}
KeyGenParameterSpec teeSpec = 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(false)
.build();
kpg.initialize(teeSpec);
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 + 明文DEKDEK 由服务端用 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 加密的 DEKBase64
* @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; // 明文 DEKHTTPS 传输给服务端)
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;
}
}
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="SecureDevice Demo"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
android:typeface="monospace" />
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,4 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">SecureDevice</string>
</resources>

View File

@@ -0,0 +1,5 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.SecureDevice" parent="Theme.Material3.DayNight.NoActionBar">
<item name="android:windowBackground">@android:color/white</item>
</style>
</resources>