Я столкнулся со странной проблемой, которая начала возникать после недавнего обновления Android на моем телефоне Galaxy S8.
У меня есть модуль 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);
}
}
}
В какой-то момент я думал, что это из-за преобразования между байтом [] и строкой, но это не проблема.