android - android.security.KeyStoreException:签名/MAC 验证失败尝试在 React Native 模块中解密时
问题描述
我面临一个奇怪的问题,在我的 Galaxy S8 手机最近更新 Android 后开始发生。
我有一个 React Native 模块,用于使用存储在 Android KeyStore 中的密钥对值进行加密,其中运行得非常好,但在解密过程中突然开始失败,出现以下异常:
android.security.KeyStoreException: Signature/MAC verification failed
我做了一些调查,发现如果我在加密一个值后立即调用我的解密方法,它工作正常,但如果我尝试在 JS 端的另一个调用中进行解密,它会失败。
此外,如果我提示用户进行生物识别,无论是同一个呼叫还是另一个呼叫,解密都可以正常工作。
这是我的模块代码:
public class BiometricsModule extends ReactContextBaseJavaModule {
private Promise _promise;
private ReactApplicationContext _context;
private String CIPHER_IV = "CIPHER_IV";
private SettingsStore settingsStore;
public FVBiometricsModule(@NonNull ReactApplicationContext reactContext) {
super(reactContext);
_context = reactContext;
settingsStore = new SettingsStore(_context);
}
@NonNull
@Override
public String getName() {
return "Biometrics";
}
@ReactMethod
public void isEnrolledAsync(final Promise promise) {
_promise = promise;
try {
WritableMap resultData = new WritableNativeMap();
Integer biometricsCheckResult = BiometricManager.from(_context).canAuthenticate();
String reason = parseResult(biometricsCheckResult);
resultData.putBoolean("result", reason == "SUCCESS");
if (reason != "SUCCESS") {
resultData.putString("reason", reason);
}
promise.resolve(resultData);
} catch (Exception e) {
promise.reject(e);
}
}
@ReactMethod
public void promptForBiometricsAsync(String prompt, String title, String cancelButtonText, final Promise promise) {
_promise = promise;
prompt(null, null, prompt, title, cancelButtonText, false, null);
}
@ReactMethod
public void setPasswordAsync(String username, String password, Boolean biometricsProtected, String prompt, String title, String cancelButtonText, final Promise promise) {
_promise = promise;
try {
generateKey(biometricsProtected);
Cipher cipher = getCipher(biometricsProtected);
SecretKey secretKey = getSecretKey();
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
settingsStore.setValue(CIPHER_IV, Base64.encodeToString(cipher.getIV(), Base64.DEFAULT));
if (biometricsProtected) {
prompt(username, password, prompt, title, cancelButtonText, false, cipher);
} else {
encrypt(username, password, cipher);
}
} catch (Exception e) {
promise.reject(e);
}
}
@ReactMethod
public void getPasswordAsync(String username, Boolean biometricsProtected, String prompt, String title, String cancelButtonText, final Promise promise) {
_promise = promise;
try {
Cipher cipher = getCipher(biometricsProtected);
SecretKey secretKey = getSecretKey();
byte[] _iv = Base64.decode(settingsStore.getValue(CIPHER_IV, null), Base64.DEFAULT);
cipher.init(Cipher.DECRYPT_MODE, secretKey, biometricsProtected ? new IvParameterSpec(_iv) : new GCMParameterSpec(128, _iv));
if (biometricsProtected) {
prompt(username, null, prompt, title, cancelButtonText, true, cipher);
} else {
decrypt(username, cipher);
}
} catch (Exception e) {
promise.reject(e);
}
}
private String parseResult(Integer biometricsCheckResult) {
String result = "SUCCESS";
switch (biometricsCheckResult) {
case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
result = "NOT_AVAILABLE";
break;
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
result = "NOT_AVAILABLE";
break;
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
result = "NOT_ENROLLED";
break;
}
return result;
}
private void generateKey(Boolean biometricsProtected) {
generateSecretKey(new KeyGenParameterSpec.Builder(
_context.getPackageName(),
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(biometricsProtected ? KeyProperties.BLOCK_MODE_CBC : KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(biometricsProtected ? KeyProperties.ENCRYPTION_PADDING_PKCS7 : KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(biometricsProtected)
.setInvalidatedByBiometricEnrollment(biometricsProtected)
.build());
}
private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
} catch (Exception e) {
_promise.reject(e);
}
}
private SecretKey getSecretKey() {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
return ((SecretKey)keyStore.getKey(_context.getPackageName(), null));
} catch (Exception e) {
_promise.reject(e);
return null;
}
}
private Cipher getCipher(Boolean biometricsProtected) {
try {
return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ (biometricsProtected ? KeyProperties.BLOCK_MODE_CBC : KeyProperties.BLOCK_MODE_GCM) + "/"
+ (biometricsProtected ? KeyProperties.ENCRYPTION_PADDING_PKCS7 : KeyProperties.ENCRYPTION_PADDING_NONE));
} catch (Exception e) {
_promise.reject(e);
return null;
}
}
private void prompt(String username, String password, String prompt, String title, String cancelBButtonText, Boolean decrypt, Cipher cipher) {
MainActivity.mainActivity.runOnUiThread(new Runnable() {
public void run() {
WritableMap resultData = new WritableNativeMap();
Executor _executor = ContextCompat.getMainExecutor(MainActivity.mainActivity);
BiometricPrompt _biometricPrompt = new BiometricPrompt(MainActivity.mainActivity,
_executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode,
@NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
try {
resultData.putBoolean("result", false);
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
@Override
public void onAuthenticationSucceeded(
@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
try {
if (password != null && !decrypt) {
byte[] encryptedInfo = result.getCryptoObject().getCipher().doFinal(password.getBytes(Charset.defaultCharset()));
settingsStore.setValue(username, Base64.encodeToString(encryptedInfo, Base64.DEFAULT));
resultData.putBoolean("result", true);
} else if (decrypt) {
String decryptedInfo = new String(result.getCryptoObject().getCipher().doFinal(Base64.decode(settingsStore.getValue(username, null), Base64.DEFAULT)));
resultData.putString("password", decryptedInfo);
}
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
try {
resultData.putBoolean("result", false);
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
});
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(prompt)
.setNegativeButtonText(cancelBButtonText)
.setConfirmationRequired(false)
.build();
if (cipher != null)
_biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipher));
else
_biometricPrompt.authenticate(promptInfo);
}
});
}
private void encrypt(String username, String password, Cipher cipher) {
try {
WritableMap resultData = new WritableNativeMap();
byte[] encryptedInfo = cipher.doFinal(password.getBytes(Charset.defaultCharset()));
settingsStore.setValue(username, Base64.encodeToString(encryptedInfo, Base64.DEFAULT));
resultData.putBoolean("result", true);
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
private void decrypt(String username, Cipher cipher) {
try {
WritableMap resultData = new WritableNativeMap();
String decryptedInfo = new String(cipher.doFinal(Base64.decode(settingsStore.getValue(username, null), Base64.DEFAULT)));
resultData.putString("password", decryptedInfo);
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
}
在某些时候,我虽然是因为 byte[] 和 string 之间的转换,但这不是问题。
解决方案
所以几个小时后,我发现了问题,现在代码可以工作了。
这是工作代码:
public class BiometricsModule extends ReactContextBaseJavaModule {
private Promise _promise;
private ReactApplicationContext _context;
private SettingsStore settingsStore;
public BiometricsModule(@NonNull ReactApplicationContext reactContext) {
super(reactContext);
_context = reactContext;
settingsStore = new SettingsStore(_context);
}
@NonNull
@Override
public String getName() {
return "Biometrics";
}
@ReactMethod
public void isEnrolledAsync(final Promise promise) {
_promise = promise;
try {
WritableMap resultData = new WritableNativeMap();
Integer biometricsCheckResult = BiometricManager.from(_context).canAuthenticate();
String reason = parseResult(biometricsCheckResult);
resultData.putBoolean("result", reason == "SUCCESS");
if (reason != "SUCCESS") {
resultData.putString("reason", reason);
}
promise.resolve(resultData);
} catch (Exception e) {
promise.reject(e);
}
}
@ReactMethod
public void promptForBiometricsAsync(String prompt, String title, String cancelButtonText, final Promise promise) {
_promise = promise;
prompt(null, null, prompt, title, cancelButtonText, false, null);
}
@ReactMethod
public void setPasswordAsync(String username, String password, Boolean biometricsProtected, String prompt, String title, String cancelButtonText, final Promise promise) {
_promise = promise;
try {
Cipher cipher = getCipher(biometricsProtected);
SecretKey secretKey = getSecretKey(biometricsProtected);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
if (biometricsProtected) {
prompt(username, password, prompt, title, cancelButtonText, false, cipher);
} else {
encrypt(username, password, cipher);
}
} catch (Exception e) {
promise.reject(e);
}
}
@ReactMethod
public void getPasswordAsync(String username, Boolean biometricsProtected, String prompt, String title, String cancelButtonText, final Promise promise) {
_promise = promise;
try {
Cipher cipher = getCipher(biometricsProtected);
SecretKey secretKey = getSecretKey(biometricsProtected);
byte[] _iv = stringToByteArray(extractIv(settingsStore.getValue(username, null)));
cipher.init(Cipher.DECRYPT_MODE, secretKey, biometricsProtected ? new IvParameterSpec(_iv) : new GCMParameterSpec(128, _iv));
if (biometricsProtected) {
prompt(username, null, prompt, title, cancelButtonText, true, cipher);
} else {
decrypt(username, cipher);
}
} catch (Exception e) {
promise.reject(e);
}
}
private String parseResult(Integer biometricsCheckResult) {
String result = "SUCCESS";
switch (biometricsCheckResult) {
case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
result = "NOT_AVAILABLE";
break;
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
result = "NOT_AVAILABLE";
break;
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
result = "NOT_ENROLLED";
break;
}
return result;
}
private void generateKey(String alias, Boolean biometricsProtected) {
generateSecretKey(new KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(biometricsProtected ? KeyProperties.BLOCK_MODE_CBC : KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(biometricsProtected ? KeyProperties.ENCRYPTION_PADDING_PKCS7 : KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(biometricsProtected)
.setInvalidatedByBiometricEnrollment(biometricsProtected)
.setKeySize(256)
.setRandomizedEncryptionRequired(true)
.build());
}
private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(keyGenParameterSpec);
keyGenerator.generateKey();
} catch (Exception e) {
_promise.reject(e);
}
}
private SecretKey getSecretKey(Boolean biometricsProtected) {
try {
String alias = _context.getPackageName() + "_SECRET_KEY_" + biometricsProtected.toString().toUpperCase();
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
if (!keyStore.containsAlias(alias)) {
generateKey(alias, biometricsProtected);
}
return ((SecretKey)keyStore.getKey(alias, null));
} catch (Exception e) {
return null;
}
}
private Cipher getCipher(Boolean biometricsProtected) {
try {
return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ (biometricsProtected ? KeyProperties.BLOCK_MODE_CBC : KeyProperties.BLOCK_MODE_GCM) + "/"
+ (biometricsProtected ? KeyProperties.ENCRYPTION_PADDING_PKCS7 : KeyProperties.ENCRYPTION_PADDING_NONE));
} catch (Exception e) {
_promise.reject(e);
return null;
}
}
private void prompt(String username, String password, String prompt, String title, String cancelBButtonText, Boolean decrypt, Cipher cipher) {
MainActivity.mainActivity.runOnUiThread(new Runnable() {
public void run() {
WritableMap resultData = new WritableNativeMap();
Executor _executor = ContextCompat.getMainExecutor(MainActivity.mainActivity);
BiometricPrompt _biometricPrompt = new BiometricPrompt(MainActivity.mainActivity,
_executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode,
@NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
try {
resultData.putBoolean("result", false);
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
@Override
public void onAuthenticationSucceeded(
@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
try {
if (password != null && !decrypt) {
byte[] encryptedInfo = result.getCryptoObject().getCipher().doFinal(password.getBytes(Charset.defaultCharset()));
settingsStore.setValue(username, byteArrayToString(encryptedInfo) + " || " + byteArrayToString(result.getCryptoObject().getCipher().getIV()));
resultData.putBoolean("result", true);
} else if (decrypt) {
String decryptedInfo = new String(result.getCryptoObject().getCipher().doFinal(stringToByteArray(extractEncryptedContent(settingsStore.getValue(username, null)))));
resultData.putString("password", decryptedInfo);
}
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
try {
resultData.putBoolean("result", false);
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
});
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(prompt)
.setNegativeButtonText(cancelBButtonText)
.setConfirmationRequired(false)
.build();
if (cipher != null)
_biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipher));
else
_biometricPrompt.authenticate(promptInfo);
}
});
}
private void encrypt(String username, String password, Cipher cipher) {
try {
WritableMap resultData = new WritableNativeMap();
byte[] encryptedInfo = cipher.doFinal(password.getBytes(Charset.defaultCharset()));
settingsStore.setValue(username, byteArrayToString(encryptedInfo) + " || " + byteArrayToString(cipher.getIV()));
resultData.putBoolean("result", true);
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
private void decrypt(String username, Cipher cipher) {
try {
WritableMap resultData = new WritableNativeMap();
String decryptedInfo = new String(cipher.doFinal(stringToByteArray(extractEncryptedContent(settingsStore.getValue(username, null)))));
resultData.putString("password", decryptedInfo);
_promise.resolve(resultData);
} catch (Exception e) {
_promise.reject(e);
}
}
private String byteArrayToString(byte buf[]) {
return Base64.encodeToString(buf, Base64.DEFAULT);
}
private byte[] stringToByteArray(String s) {
return Base64.decode(s, Base64.DEFAULT);
}
private String extractEncryptedContent(String source) {
return source.substring(0, source.indexOf(" || ")).trim();
}
private String extractIv(String source) {
return source.substring(source.lastIndexOf(" || ") + 4).trim();
}
}
推荐阅读
- javascript - 具有动态值的半圆形进度条
- mysql - 为什么索引会使查询变慢?
- apache - 某些请求的 Apache https 请求状态挂起,返回 502 状态
- json - 错误项目的 package.json 文件似乎格式错误
- r - 如何用图像(闪亮)自动替换列名中的文本?
- asp.net-core - ASP.NET Core MVC - 如何在没有会话的情况下保留购物篮数据
- c# - Azure 服务总线侦听器打开太多 TCP 连接(耗尽)
- php - 在 xampp 上安装 magento 时
- django - Django Rest Framework 使用嵌套序列化器发布数据两个模型
- ros - ros-sharp 服务调用示例