feat: 新增安全设备 Demo 及 Android 端 TEE 加解密模块
This commit is contained in:
30
android-app/app/build.gradle
Normal file
30
android-app/app/build.gradle
Normal file
@@ -0,0 +1,30 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "com.secure.demo"
|
||||
compileSdkVersion 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.secure.demo"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 36
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
}
|
||||
18
android-app/app/src/main/AndroidManifest.xml
Normal file
18
android-app/app/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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):私钥不可导出,用于解密服务端下发数据 + 签名
|
||||
* - 每次拍照生成随机 DEK(AES-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 + 明文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;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
android-app/app/src/main/res/layout/activity_main.xml
Normal file
27
android-app/app/src/main/res/layout/activity_main.xml
Normal 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>
|
||||
4
android-app/app/src/main/res/values/colors.xml
Normal file
4
android-app/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
3
android-app/app/src/main/res/values/strings.xml
Normal file
3
android-app/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">SecureDevice</string>
|
||||
</resources>
|
||||
5
android-app/app/src/main/res/values/themes.xml
Normal file
5
android-app/app/src/main/res/values/themes.xml
Normal 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>
|
||||
21
android-app/build.gradle
Normal file
21
android-app/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.13.2'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
20
android-app/gradle.properties
Normal file
20
android-app/gradle.properties
Normal file
@@ -0,0 +1,20 @@
|
||||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx1536m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
|
||||
BIN
android-app/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
android-app/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
android-app/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
android-app/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#Thu Dec 21 10:49:41 CST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||
172
android-app/gradlew
vendored
Normal file
172
android-app/gradlew
vendored
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
84
android-app/gradlew.bat
vendored
Normal file
84
android-app/gradlew.bat
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
10
android-app/local.properties
Normal file
10
android-app/local.properties
Normal file
@@ -0,0 +1,10 @@
|
||||
## This file is automatically generated by Android Studio.
|
||||
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||
#
|
||||
# This file should *NOT* be checked into Version Control Systems,
|
||||
# as it contains information specific to your local configuration.
|
||||
#
|
||||
# Location of the SDK. This is only used by Gradle.
|
||||
# For customization when using a Version Control System, please read the
|
||||
# header note.
|
||||
sdk.dir=F\:\\AndroidSDK
|
||||
2
android-app/settings.gradle
Normal file
2
android-app/settings.gradle
Normal file
@@ -0,0 +1,2 @@
|
||||
rootProject.name = 'SecureDeviceApp'
|
||||
include ':app'
|
||||
Reference in New Issue
Block a user