首页 > 解决方案 > 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 之间的转换,但这不是问题。

标签: androidreact-nativeencryptionandroid-keystore

解决方案


所以几个小时后,我发现了问题,现在代码可以工作了。

这是工作代码:

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();
    }
}

推荐阅读