import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyInfo;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import org.json.JSONObject;
import java.security.KeyStore;
import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.security.auth.x500.X500Principal;
/**
* 通过AES加密方式,用KeyGenerator生成秘钥,保存在Android Keystore中
* 对数据进行加解密
* <p>
* 1、创建秘钥,保存在AndroidKeystore里面,秘钥别名为alias
* 2、创建并初始化cipher对象,获取秘钥,对数据进行加解密
*/
class HwAesSecProvider {
private static final String TAG = "HwAesSecProvider";
private static final String KEYSTORE_AES_ALIAS = "Alias_AES_";
// 算法/模式/补码方式 不使用CBC模式,注意Padding Oracle攻击
private static final String AES_MODE = "AES/GCM/NoPadding";
private final static String KEY_IV = "iv";
private final static String KEY_DATA = "data";
private final Context context;
private final KeyStore keyStore;
public HwAesSecProvider(@NonNull final Context context, @NonNull final KeyStore keyStore) {
this.context = context;
this.keyStore = keyStore;
}
@NonNull
private static String getKeyStoreAlias(@NonNull final Context context) {
return KEYSTORE_AES_ALIAS + PrefsHelper.getPackageNameHash(context);
}
@NonNull
public String decryptAES(@NonNull final String encryptedText) {
try {
final JSONObject json = new JSONObject(encryptedText);
final byte[] iv = Base64.decode(json.optString(KEY_IV, ""), Base64.DEFAULT);
final byte[] data = Base64.decode(json.optString(KEY_DATA, ""), Base64.DEFAULT);
if (iv.length <= 0 || data.length <= 0) {
return "";
}
final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(getKeyStoreAlias(context), null);
if (null != secretKeyEntry) {
final SecretKey secretKey = secretKeyEntry.getSecretKey();
// KeyGenParameterSpecs中设置的block模式是KeyProperties.BLOCK_MODE_GCM,所以这里只能使用这个模式解密数据。
final Cipher cipher = Cipher.getInstance(AES_MODE);
// 需要为GCMParameterSpec 指定一个加密的block的长度(可以是128、120、112、104、96),从密钥信息中读取后填入
// 并且用到之前的加密过程中用到的IV。
final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(getSecureKeySize(secretKey), iv, 0, iv.length);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
return new String(cipher.doFinal(data));
}
} catch (Throwable t) {
Logs.e(TAG, t.getMessage(), t);
}
return "";
}
@NonNull
public String encryptAES(@NonNull final String plainText) {
try {
final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(getKeyStoreAlias(context), null);
if (null != secretKeyEntry) {
final SecretKey secretKey = secretKeyEntry.getSecretKey();
// KeyGenParameterSpecs中设置的block模式是KeyProperties.BLOCK_MODE_GCM,所以这里只能使用这个模式解密数据。
final Cipher cipher = Cipher.getInstance(AES_MODE);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
final JSONObject json = new JSONObject();
json.put(KEY_DATA, Base64.encodeToString(cipher.doFinal(plainText.getBytes()), Base64.DEFAULT));
/* 使用相同的IV多次加密不同的数据,会存在被恶意穷举的风险,我们要求硬件每次加密返回不同的加密向量IV
* 这就要求我们需要存储加密数据的同时,存储返回的随机向量,然后在解密的时候提供加密时候的随机向量
*/
json.put(KEY_IV, Base64.encodeToString(cipher.getIV(), Base64.DEFAULT));
return json.toString();
}
} catch (Throwable t) {
Logs.e(TAG, t.getMessage(), t);
}
return "";
}
/*
* 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿
* 建议全部调度到一个统一的HanderThread子线程进行处理
**/
@WorkerThread
@RequiresApi(api = Build.VERSION_CODES.M)
private void generateHwAesKey_AboveApi23(@NonNull final String keystoreAlias, @NonNull final String provider) throws Exception {
// https://developer.android.com/training/articles/keystore.html#SupportedCiphers
// https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder.html#setRandomizedEncryptionRequired(boolean))
final KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec
.Builder(keystoreAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setCertificateSubject(new X500Principal("CN=" + getKeyStoreAlias(context)))
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
/* 使用相同的IV多次加密不同的数据,会存在被恶意穷举的风险,我们要求硬件每次加密返回不同的加密向量IV
* 这就要求我们需要存储加密数据的同时,存储返回的随机向量,然后在解密的时候提供加密时候的随机向量
*/
.setRandomizedEncryptionRequired(true); //要求硬件生成随机向量
if (hasStrongBox(context) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)) {
try {
/* 如果系统支持硬件StrongBox保存密钥,则尝试交给StrongBox保护 */
builder.setIsStrongBoxBacked(true);
generateHwAesKey_AboveApi23(builder, provider);
} catch (Throwable t) {
Logs.e(TAG, t.getMessage(), t);
/*
* 2024/06
* 已知在小米 14/15 Xiaomi HyperOS 系统上,部分手机在启用 StrongBox 的情况下,会发生如下异常:
* <p>
* java.security.ProviderException: Keystore key generation failed
* at android.security.keystore2.AndroidKeyStoreKeyGeneratorSpi.engineGenerateKey(AndroidKeyStoreKeyGeneratorSpi.java:413)
* at javax.crypto.KeyGenerator.generateKey(KeyGenerator.java:612)
* at com.xxxx.xxxx.xxxx.xxxx.xxxx.xxxx.x(SourceFile:28)
* <p>
* Caused by: android.security.KeyStoreException: Not implemented (internal Keystore code: -100 message: system/security/keystore2/src/security_level.rs:622
* <p>
* Caused by:
* 0: system/security/keystore2/src/security_level.rs:620: While generating Key without explicit attestation key.
* 1: Error::Km(r....
*
* 我们需要先尝试使用 StrongBox 生成密钥,如果生成失败,则尝试不使用 StrongBox 生成密钥
**/
builder.setIsStrongBoxBacked(false);
generateHwAesKey_AboveApi23(builder, provider);
}
} else {
generateHwAesKey_AboveApi23(builder, provider);
}
}
@WorkerThread
@RequiresApi(api = Build.VERSION_CODES.M)
private void generateHwAesKey_AboveApi23(@NonNull final KeyGenParameterSpec.Builder builder, @NonNull final String provider) throws Exception {
final KeyGenerator keyGenerator = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, provider);
final KeyGenParameterSpec spec = builder.build();
keyGenerator.init(spec);
keyGenerator.generateKey();
}
/*
* 注意KeyStore的API线程不安全,不建议主线程执行,主线程可能导致界面卡顿
* 建议全部调度到一个统一的HanderThread子线程进行处理
**/
@WorkerThread
public boolean generateSecStoreKeys() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
generateHwAesKey_AboveApi23(getKeyStoreAlias(context), SecConst.AND_KEYSTORE_PROVIDER);
return isInsideSecureHardware();
} catch (Throwable t) {
Logs.e(TAG, t.getMessage(), t);
}
}
return false;
}
public boolean isInsideSecureHardware() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final KeyInfo info = getKeyInfo();
if (null != info) {
return info.isInsideSecureHardware();
}
}
return false;
}
public int getSecureKeySize() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final KeyInfo info = getKeyInfo();
if (null != info) {
return info.getKeySize();
}
}
return 0;
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Nullable
private KeyInfo getKeyInfo(@NonNull final SecretKey secretKey) throws Exception {
final SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKey.getAlgorithm(), SecConst.AND_KEYSTORE_PROVIDER);
if (null != factory) {
final KeySpec spec = factory.getKeySpec(secretKey, KeyInfo.class);
if (null != spec) {
return (KeyInfo) spec;
}
}
return null;
}
private int getSecureKeySize(@NonNull final SecretKey secretKey) throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final KeyInfo info = getKeyInfo(secretKey);
if (null != info) {
return info.getKeySize();
}
}
return 0;
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Nullable
private KeyInfo getKeyInfo(@NonNull final String alias) {
try {
final SecretKey key = (SecretKey) keyStore.getKey(alias, null);
if (null != key) {
return getKeyInfo(key);
}
} catch (Throwable t) {
Logs.e(TAG, "Exception getting key info", t);
}
return null;
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Nullable
private KeyInfo getKeyInfo() {
return getKeyInfo(getKeyStoreAlias(context));
}
/**
* Returns whether the device has a StrongBox backed KeyStore.
*/
public static boolean hasStrongBox(@NonNull final Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return context.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE);
}
return false;
}
}