如何对较低平台版本使用不受支持的异常

How to Use Unsupported Exception for Lower Platform Version

我有一个 DialogFragment 可以处理我的应用程序的登录和指纹验证。此片段使用两个 API23、KeyGenParameterSpecKeyPermanentlyInvalidatedException 独有的 classes。我的印象是我可以使用这些 classes,只要我在尝试初始化 classes 之前检查构建版本(概述 here):

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    ...
} else {
    ...
}

但似乎并非如此。如果我尝试在 API 20 之前的版本上 运行 此代码,Dalvik VM 会拒绝整个 class 并抛出 VerifyError。不过,该代码确实适用于 API 20 及更高版本。如何在我的代码中使用这些方法,同时仍然允许代码用于以前的 API 关卡?

完整的堆栈跟踪如下:

05-31 14:35:50.924 11941-11941/com.example.app E/dalvikvm: Could not find class 'android.security.keystore.KeyGenParameterSpec$Builder', referenced from method com.example.app.ui.fragment.util.LoginFragment.createKeyPair
05-31 14:35:50.924 11941-11941/com.example.app W/dalvikvm: VFY: unable to resolve new-instance 263 (Landroid/security/keystore/KeyGenParameterSpec$Builder;) in Lcom/example/app/ui/fragment/util/LoginFragment;
05-31 14:35:50.924 11941-11941/com.example.app D/dalvikvm: VFY: replacing opcode 0x22 at 0x000c
05-31 14:35:50.924 11941-11941/com.example.app W/dalvikvm: VFY: unable to resolve exception class 265 (Landroid/security/keystore/KeyPermanentlyInvalidatedException;)
05-31 14:35:50.924 11941-11941/com.example.app W/dalvikvm: VFY: unable to find exception handler at addr 0x3f
05-31 14:35:50.924 11941-11941/com.example.app W/dalvikvm: VFY:  rejected Lcom/example/app/ui/fragment/util/LoginFragment;.initializeCipher (I)Z
05-31 14:35:50.924 11941-11941/cp W/dalvikvm: VFY:  rejecting opcode 0x0d at 0x003f
05-31 14:35:50.924 11941-11941/com.example.app W/dalvikvm: VFY:  rejected Lcom/example/app/ui/fragment/util/LoginFragment;.initializeCipher (I)Z
05-31 14:35:50.924 11941-11941/com.example.app W/dalvikvm: Verifier rejected class Lcom/example/app/ui/fragment/util/LoginFragment;
05-31 14:35:50.924 11941-11941/com.example.app D/AndroidRuntime: Shutting down VM
05-31 14:35:50.924 11941-11941/com.example.app W/dalvikvm: threadid=1: thread exiting with uncaught exception (group=0x9cca9b20)
05-31 14:35:50.934 11941-11941/com.example.app E/AndroidRuntime: FATAL EXCEPTION: main
        Process: com.example.app, PID: 11941 java.lang.VerifyError: com/example/app/ui/fragment/util/LoginFragment
            at com.example.app.util.NetworkUtility.login(NetworkUtility.java:41)
            at com.example.app.ui.activity.AbstractNavActivity.onOptionsItemSelected(AbstractNavActivity.java:68)
            at android.app.Activity.onMenuItemSelected(Activity.java:2600)
            at android.support.v4.app.FragmentActivity.onMenuItemSelected(FragmentActivity.java:403)
            at android.support.v7.app.AppCompatActivity.onMenuItemSelected(AppCompatActivity.java:189)
            at android.support.v7.view.WindowCallbackWrapper.onMenuItemSelected(WindowCallbackWrapper.java:100)
            at android.support.v7.view.WindowCallbackWrapper.onMenuItemSelected(WindowCallbackWrapper.java:100)
            at android.support.v7.app.ToolbarActionBar.onMenuItemClick(ToolbarActionBar.java:69)
            at android.support.v7.widget.Toolbar.onMenuItemClick(Toolbar.java:169)
            at android.support.v7.widget.ActionMenuView$MenuBuilderCallback.onMenuItemSelected(ActionMenuView.java:760)
            at android.support.v7.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:811)
            at android.support.v7.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:152)
            at android.support.v7.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:958)
            at android.support.v7.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:948)
            at android.support.v7.view.menu.MenuPopupHelper.onItemClick(MenuPopupHelper.java:191)
            at android.widget.AdapterView.performItemClick(AdapterView.java:299)
            at android.widget.AbsListView.performItemClick(AbsListView.java:1113)
            at android.widget.AbsListView$PerformClick.run(AbsListView.java:2904)
            at android.widget.AbsListView.run(AbsListView.java:3638)
            at android.os.Handler.handleCallback(Handler.java:733)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:136)
            at android.app.ActivityThread.main(ActivityThread.java:5017)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:515)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
            at dalvik.system.NativeStart.main(Native Method)

更新代码

login() 方法只是启动 LoginFragment:

的一种便捷方法
public static void login(FragmentManager manager) {
     manager.beginTransAction().add(LoginFragment.newInstance(), null).commit();
}

相关代码在LoginFragment本身。特别是 createKeyPair()initializeCipher 方法:

public class LoginFragment extends DialogFragment
        implements TextView.OnEditorActionListener, FingerprintCallback.Callback {

    ...

    public static LoginFragment newInstance() {
        return newInstance(null);
    }

    public static LoginFragment newInstance(Intent intent) {
        LoginFragment fragment = new LoginFragment();

        Bundle args = new Bundle();
        args.putParcelable(EXTRA_INTENT, intent);
        fragment.setArguments(args);

        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Injector.getContextComponent().inject(this);
        setStyle(STYLE_NO_TITLE, R.style.DialogTheme);
        setRetainInstance(true);
        setCancelable(false);

        mSaveUsernamePreference = mPreferences.getBoolean(getString(R.string.key_auth_username_retain));
        mUseFingerprintPreference = mPreferences.getBoolean(getString(R.string.key_auth_fingerprint));
        mUsernamePreference = mPreferences.getString(getString(R.string.key_auth_username));
        mPasswordPreference = mPreferences.getString(getString(R.string.key_auth_password));
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.dialog_login_container, container, false);
        ButterKnife.bind(this, view);

        mPasswordView.setOnEditorActionListener(this);

        if(!mFingerprintManager.isHardwareDetected()) {
            mUseFingerprintToggle.setVisibility(View.GONE);
        } else {
            mGenerated = initializeKeyPair(false);
        }

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            setStage(isFingerprintAvailable() ? Stage.FINGERPRINT : Stage.CREDENTIALS);
        } else {
            setStage(Stage.CREDENTIALS);
        }

        return view;
    }

    @Override
    public void onResume() {
        super.onResume();

        ...

        if(mStage == Stage.FINGERPRINT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            startListening(initializeCipher(Cipher.DECRYPT_MODE));
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        stopListening();
    }

    ...

    @Override
    public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
        Timber.i("Fingerprint succeeded");
        showFingerprintSuccess();

        mSubscriptions.add(
            mGenerated.subscribeOn(Schedulers.newThread())
                    .observeOn(AndroidSchedulers.mainThread())
                    .doOnCompleted(() -> {
                        try {
                            mUsername = mUsernamePreference.get();
                            mPassword = decryptPassword(result.getCryptoObject().getCipher());
                            initLoginAttempt();
                        } catch (IllegalBlockSizeException | BadPaddingException exception) {
                            Timber.e(exception, "Failed to decrypt password");
                        }
                    }).subscribe());
    }

    @Override
    public void onAuthenticationHelp(int messageId, CharSequence message) {
        Timber.i("Fingerprint help id: " + messageId + " message: " + message);
        showFingerprintError(message);
    }

    @Override
    public void onAuthenticationError(int messageId, CharSequence message) {
        Timber.i("Fingerprint error id: " + messageId + " message: " + message);
        if(messageId != 5) {
            showFingerprintError(message);
        }
    }

    @Override
    public void onAuthenticationFailed() {
        Timber.i("Fingerprint failed");
        showFingerprintError(getResources().getString(R.string.msg_fingerprint_error_unknown));
    }

    @OnClick(R.id.button_cancel)
    public void onCancel() {
        dismiss();
    }

    @OnClick(R.id.button_continue)
    public void onContinue() {
        switch (mStage) {
            case CREDENTIALS:
                mUsername = mUsernameView.getText().toString();
                mPassword = mPasswordView.getText().toString();
                initLoginAttempt();
                break;
            case FINGERPRINT:
                setStage(Stage.CREDENTIALS);
                break;
        }
    }

    private void showFingerprintSuccess() {
        int colorAccent = ThemeUtil.getColorAttribute(getContext(), android.R.attr.colorAccent);
        mFingerprintIcon.setImageResource(R.drawable.ic_done_white_24dp);
        mFingerprintIcon.setCircleColor(colorAccent);
        mFingerprintStatus.setText(R.string.msg_fingerprint_success);
        mFingerprintStatus.setTextColor(colorAccent);
    }

    private void showFingerprintError(CharSequence message) {
        int colorError = ContextCompat.getColor(getContext(), R.color.material_deep_orange_600);
        mFingerprintIcon.setImageResource(R.drawable.ic_priority_high_white_24dp);
        mFingerprintIcon.setCircleColor(colorError);
        mFingerprintStatus.setText(message);
        mFingerprintStatus.setTextColor(colorError);
        resetFingerprintStatus();
    }

    private void resetFingerprintStatus() {
        mSubscriptions.add(Observable.timer(1600, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(finished -> {
                    mFingerprintIcon.setImageResource(R.drawable.ic_fingerprint_white_24dp);
                    mFingerprintIcon.setCircleColor(ContextCompat
                            .getColor(getContext(), R.color.material_blue_gray_500));
                    mFingerprintStatus.setText(R.string.msg_fingerprint_input);
                    mFingerprintStatus.setTextColor(ThemeUtil
                            .getColorAttribute(getContext(), android.R.attr.textColorHint));
                }));
    }

    private void onSaveUsernameChanged(boolean checked) {
        if(!checked) {
            mUseFingerprintToggle.setChecked(false);
        }
    }

    private void onUseFingerprintChanged(boolean checked) {
        if(checked) {
            mSaveUsernameToggle.setChecked(true);

            if(!mFingerprintManager.hasEnrolledFingerprints()) {
                displaySettingsDialog();
                mUseFingerprintToggle.setChecked(false);
            }
        }
    }

    public void setStage(Stage stage) {
        switch (stage) {
            case CREDENTIALS:
                Timber.d("Set stage Credentials");
                mPositiveButton.setText(R.string.btn_login);
                mFingerprintContent.setVisibility(View.GONE);
                mCredentialContent.setVisibility(View.VISIBLE);
                setForm();
                break;
            case FINGERPRINT:
                mPositiveButton.setText(R.string.btn_password);
                mCredentialContent.setVisibility(View.GONE);
                mFingerprintContent.setVisibility(View.VISIBLE);
                break;
        } mStage = stage;
    }

    private void startListening(boolean cipher) {
        Timber.v("Start listening for fingerprint input");
        mCancellationSignal = new CancellationSignal();
        if(cipher) {
            mFingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(mCipher),
                    0, mCancellationSignal, new FingerprintCallback(this), null);
        } else {
            setStage(Stage.CREDENTIALS);
        }
    }

    private void stopListening() {
        if(mCancellationSignal != null) {
            mCancellationSignal.cancel();
            mCancellationSignal = null;
        }
    }

    private void setForm() {
        if(mSaveUsernamePreference.isSet() && mSaveUsernamePreference.get()
                && mUsernamePreference.isSet()) {
            mUsernameView.setText(mUsernamePreference.get());
            mUsernameView.setSelectAllOnFocus(true);
            mPasswordView.requestFocus();
        } else {
            mUsernameView.requestFocus();
        }
    }

    public void initLoginAttempt() {
        mProgressBar.setVisibility(View.VISIBLE);
        mAuthenticationService.getLoginForm().subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::onLoginFormResponse, this::onError);
    }

    private void onLoginFormResponse(ResponseBody response) {
        try {
            attemptLogin(LoginForm.parse(response.string()));
        } catch (IOException exception) {
            Timber.w(exception, "Failed to parse login form");
        }
    }

    private void attemptLogin(LoginForm loginForm) {
        mAuthenticationService
                .login(loginForm.getLoginTicket(), loginForm.getExecution(), loginForm.getEventIdentifier(),
                        mUsername, mPassword, loginForm.getSubmitValue())
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::onLoginResponse, this::onError);
    }

    public void onLoginResponse(ResponseBody response) {
        Timber.d("LOGIN RESPONSE");
        try {
            Timber.d(response.string());
        } catch (IOException exception) {
            Timber.w(exception, "Failed to retrieve attemptLogin response");
        }

        mSubscriptions.add(NetworkUtility.getAuthentication()
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::onAuthenticationChanged, this::onError));
    }

    public void onAuthenticationChanged(Boolean authenticated) {
        if(authenticated) {
            Timber.d("Authentication success");

            if(mStage == Stage.CREDENTIALS) {
                if (mSaveUsernameToggle.isChecked()) {
                    storeUsername();
                } else {
                    clearUsername();
                }

                if (mUseFingerprintToggle.isChecked()) {
                    mGenerated = initializeKeyPair(true);
                    storePassword();
                } else {
                    clearPassword();
                    finishIntent();
                }
            } else {
                finishIntent();
            }
        } else {
            Timber.d("Authentication failed");
            setStage(Stage.CREDENTIALS);
            mCaptionView.setTextColor(ContextCompat.getColor(getContext(), R.color.material_deep_orange_600));
            mCaptionView.setText(getString(R.string.msg_login_failed));
            mPasswordView.setText("");
        }
    }

    private void finishIntent() {
        mProgressBar.setVisibility(View.INVISIBLE);
        Intent intent = getArguments().getParcelable(EXTRA_INTENT);
        if(intent != null) {
            startActivity(intent);
        } dismiss();
    }

    private void onError(Throwable throwable) {
        Timber.w(throwable, "Login attempt failed");
        mProgressBar.setVisibility(View.INVISIBLE);
        mCaptionView.setTextColor(ContextCompat.getColor(getContext(), R.color.material_deep_orange_600));
        mCaptionView.setText("Login attempt failed\nPlease check your internet connection and try again");
        mPasswordView.setText("");
    }

    private void storeUsername() {
        String username = mUsernameView.getText().toString();
        mUsernamePreference.set(username);
        if(mPreferences.getBoolean(getString(R.string.key_auth_push), false).get()) {
            UAirship.shared().getPushManager().getNamedUser().setId(username);
        }
    }

    private void clearUsername() {
        UAirship.shared().getPushManager().getNamedUser().setId(null);
        mUsernamePreference.delete();
    }

    private void storePassword() {
        Timber.d("STORE PASSWORD");
        mSubscriptions.add(mGenerated.subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .doOnCompleted(() -> {
                    try {
                        Timber.d("Store password");
                        initializeCipher(Cipher.ENCRYPT_MODE);

                        String password = mPasswordView.getText().toString();
                        byte[] bytes = password.getBytes();
                        byte[] encrypted = mCipher.doFinal(bytes);
                        String encoded = Base64.encodeToString(encrypted, Base64.NO_WRAP);

                        mPasswordPreference.set(encoded);

                        finishIntent();

                    } catch (IllegalBlockSizeException | BadPaddingException exception) {
                        Timber.e(exception, "Failed to encrypt password");
                    }
                }).subscribe());
    }

    private String decryptPassword(Cipher cipher) throws IllegalBlockSizeException, BadPaddingException {
        String encoded = mPasswordPreference.get();

        Timber.d("ENCODED STRING " + encoded);

        byte[] encrypted = Base64.decode(encoded, Base64.NO_WRAP);

        byte[] bytes = cipher.doFinal(encrypted);

        return new String(bytes);
    }

    private void clearPassword() {
        mPasswordPreference.delete();
    }

    private boolean isFingerprintAvailable() {
        return mUseFingerprintPreference.isSet() && mUseFingerprintPreference.get()
                && mFingerprintManager.hasEnrolledFingerprints()
                && mSaveUsernamePreference.isSet()
                && mPasswordPreference.isSet();
    }

    private void displaySettingsDialog() {
        new AlertDialog.Builder(getContext())
                .setTitle(R.string.title_dialog_secure_lock)
                .setMessage(R.string.msg_fingerprint_unavailable)
                .setPositiveButton(R.string.btn_settings, (dialog, which) -> {
                    startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS));
                    dialog.dismiss();
                }).setNegativeButton(R.string.btn_cancel, (dialog, which) -> {
            dialog.dismiss();
        }).create().show();
    }

    @TargetApi(Build.VERSION_CODES.M)
    private boolean initializeCipher(int opmode) {
        try {
            mKeyStore.load(null);

            /**
             * A known bug in the Android 6.0 (API Level 23) implementation of Bouncy Castle
             * RSA OAEP causes the cipher to default to an SHA-1 certificate, making the SHA-256
             * certificate of the public key incompatible
             * To work around this issue, explicitly provide a new OAEP specification upon
             * initialization
             * @see <a href="https://code.google.com/p/android/issues/detail?id=197719">Issue 197719</a>
             */
            AlgorithmParameterSpec spec = generateOAEPParameterSpec();
            Key key;

            if(opmode == Cipher.ENCRYPT_MODE) {
                Key publicKey = mKeyStore.getCertificate(CIPHER_KEY_ALIAS).getPublicKey();

                /**
                 * A known bug in Android 6.0 (API Level 23) causes user authentication-related
                 * authorizations to be enforced even for public keys
                 * To work around this issue, extract the public key material to use outside of
                 * the Android Keystore
                 * @see <a href="http://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html">KeyGenParameterSpec Known Issues</a>
                 */
                key = KeyFactory.getInstance(publicKey.getAlgorithm())
                        .generatePublic(new X509EncodedKeySpec(publicKey.getEncoded()));
            } else {
                key = mKeyStore.getKey(CIPHER_KEY_ALIAS, null);
            }

            mCipher.init(opmode, key, spec);
            return true;
        } catch (KeyPermanentlyInvalidatedException exception) {
            Timber.w(exception, "Failed to initialize Cipher");
            handleKeyPermanentlyInvalidated();
            return false;
        } catch (IOException | KeyStoreException | UnrecoverableEntryException
                | InvalidKeySpecException | CertificateException | InvalidKeyException
                | NoSuchAlgorithmException | InvalidAlgorithmParameterException exception) {
            throw new RuntimeException("Failed to initialize Cipher", exception);
        }
    }

    private OAEPParameterSpec generateOAEPParameterSpec() {
        return new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
    }

    private void handleKeyPermanentlyInvalidated() {
        mCaptionView.setText(getString(R.string.msg_fingerprint_invalidated));
        mGenerated = initializeKeyPair(true);
        clearPassword();
    }

    private Observable<KeyPair> initializeKeyPair(boolean generate) {
        return Observable.create(subscriber -> {
            try {
                mKeyStore.load(null);

                if(!generate || mKeyStore.containsAlias(CIPHER_KEY_ALIAS)) {
                    PublicKey publicKey = mKeyStore.getCertificate(CIPHER_KEY_ALIAS).getPublicKey();
                    PrivateKey privateKey = (PrivateKey) mKeyStore.getKey(CIPHER_KEY_ALIAS, null);
                    subscriber.onNext(new KeyPair(publicKey, privateKey));
                } else {
                    subscriber.onNext(createKeyPair());
                }

                subscriber.onCompleted();
            } catch (IOException | KeyStoreException | UnrecoverableKeyException
                    | CertificateException | NoSuchAlgorithmException
                    | InvalidAlgorithmParameterException exception) {
                Timber.e(exception, "Failed to generate key pair");
                subscriber.onError(exception);
            }
        });
    }

    @TargetApi(Build.VERSION_CODES.M)
    private KeyPair createKeyPair() throws InvalidAlgorithmParameterException {
        // Set the alias of the entry in Android KeyStore where the key will appear
        // and the constrains (purposes) in the constructor of the Builder
        Timber.d("Initialize key pair");
        mKeyPairGenerator.initialize(
                new KeyGenParameterSpec.Builder(CIPHER_KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT)
                    .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                        .setUserAuthenticationRequired(true)
                        .build());

        return mKeyPairGenerator.generateKeyPair();
    }

}

更新

好的,所以我发现是 KeyPermanentlyInvalidatedException 导致了错误。如果我注释掉处理该异常的 catch 块,代码 运行 在任何设备上都可以。问题是我需要能够在 API 23+:

上的设备上处理该异常
catch (KeyPermanentlyInvalidatedException exception) {
    Timber.w(exception, "A new fingerprint was added to the device");
    handleKeyPermanentlyInvalidated();
    return false;
}

我的猜测是 FingerprintCallback.Callback 扩展了一个 API 级别 23+ 接口,或者 LoginFragment 具有引用 API 级别 23+ 内容的字段。

关于能够在版本保护块内安全地调用 API 级别 23+ 方法的规则是正确的。但是,您不能:

  • 继承自class设备上不存在的元素
  • 实现设备上不存在的接口
  • 具有设备上不存在的类型的字段
  • 接受设备上不存在类型的构造函数或方法参数(我们实际调用它们的地方)
  • 有方法 return 类型在设备上不存在的值(我们实际调用它们的地方)

在许多情况下,我们不需要任何这些,在这种情况下,只需在调用 API 级别 23+ 方法之前检查 Build.VERSION.SDK_INT 就足够了。

如果您需要执行项目符号列表中的某些操作,那很好,但是您需要将它们隔离到仅在 API 级别 23+ 设备上使用的 classes .

因此,例如,假设问题是 FingerprintCallback.Callback 扩展了一些 API 级别 23+ 接口。与其在 LoginFragment 上实现 FingerprintCallback.Callback,不如将其实现为匿名内部 class,并且仅在 [=12= 时执行创建该匿名内部 class 实例的代码】 够高了。然后,您只在较新的设备上引用 FingerprintCallback.Callback,您应该是安全的。

正如你所说,问题出在那个 catch 块上

catch (KeyPermanentlyInvalidatedException exception) {
    Timber.w(exception, "A new fingerprint was added to the device");
    handleKeyPermanentlyInvalidated();
    return false;
}

因为在API LEVEL 23添加了异常,但我不知道为什么在初始化时会抛出验证错误。

无论如何你可以使用

捕获异常
catch (InvalidKeyExceptionexception) {
    ....
    return false;
}

因为 KeyPermanentlyInvalidatedException 扩展了 InvalidKeyExceptionexception

我有同样的错误并通过以下方式解决了它:

catch (Exception e) {
    if (e instanceof KeyPermanentlyInvalidatedException) {
        //your error handling goes here
    }

它不是很好,但是很管用