Как использовать неподдерживаемое исключение для версии с более низкой платформой

У меня есть DialogFragment, который обрабатывает вход и аутентификацию по отпечатку пальца для моего приложения. В этом фрагменте используются два класса, эксклюзивные для API 23, KeyGenParameterSpec и KeyPermanentlyInvalidatedException. У меня сложилось впечатление, что я могу использовать эти классы, если я проверяю версию сборки, прежде чем пытаться инициализировать классы (обозначено здесь):

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

Но похоже, что это не так. Если я попытаюсь запустить этот код в версии до API 20, виртуальная машина Dalvik отклонит весь класс и выдаст ошибку 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$2.onMenuItemClick(ToolbarActionBar.java:69)
            at android.support.v7.widget.Toolbar$1.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$3.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;
}

person Bryan    schedule 31.05.2016    source источник
comment
Мы не можем помочь вам с отредактированным кодом. Пожалуйста, опубликуйте минимальный воспроизводимый пример, демонстрирующий вашу проблему, например, реальную реализацию вашего метода login(), в которой происходит сбой, и метод initializeCipher() вашего LoginFragment (который, по-видимому, находится там, где лежат нераспознанные вещи).   -  person CommonsWare    schedule 31.05.2016
comment
@CommonsWare Я обновил вопрос с кодом.   -  person Bryan    schedule 31.05.2016


Ответы (3)


Я предполагаю, что либо FingerprintCallback.Callback расширяет интерфейс API уровня 23+, либо LoginFragment имеет поля, которые ссылаются на вещи API уровня 23+.

Ваше правило о возможности безопасного вызова методов API уровня 23+ внутри блока защиты версии верно. Однако вы не можете:

  • наследовать от классов, которых нет на устройстве
  • реализовать интерфейсы, которых нет на устройстве
  • иметь поля, типы которых не существуют на устройстве
  • принимать параметры конструктора или метода, типы которых не существуют на устройстве (где мы их фактически вызываем)
  • имеют возвращаемые методом значения, типы которых не существуют на устройстве (где мы их фактически вызываем)

Во многих случаях нам ничего из этого не нужно, и в этом случае достаточно просто проверить Build.VERSION.SDK_INT перед вызовом методов API уровня 23+.

Если вам нужно сделать что-то из маркированного списка, это нормально, но тогда вам нужно выделить их в классы, которые вы используете только на устройствах уровня API 23+.

Итак, например, давайте представим, что проблема в том, что FingerprintCallback.Callback расширяет какой-то интерфейс API уровня 23+. Вместо реализации FingerprintCallback.Callback в LoginFragment вы можете реализовать это как анонимный внутренний класс и выполнять код, создающий экземпляр этого анонимного внутреннего класса, только если Build.VERSION.SDK_INT достаточно высок. Тогда вы ссылаетесь только на FingerprintCallback.Callback на более новых устройствах, и вы должны быть в безопасности.

person CommonsWare    schedule 31.05.2016
comment
Я понимаю, что вы говорите, хотя это не должно быть FingerprintCallback, потому что это класс, расширяющий FingerprintManagerCompat.AuthenticationCallback, который является частью Библиотека поддержки отпечатков пальцев. Я также не вижу никаких полей, которые ссылаются на классы из API 23+. Мне придется просмотреть код, чтобы убедиться, что я не соответствую одному из других требований. - person Bryan; 31.05.2016
comment
@Bryan: Если вы окажетесь в тупике, разделите LoginFragment на 2-3 класса. Либо иметь подклассы LoginFragment и FingerprintLoginFragment, либо LoginFragmentBase с подклассами LoginFragment и FingerprintLoginFragment. Поместите все отпечатки пальцев в FingerprintLoginFragment. Затем, когда придет время add() фрагмента, выберите класс фрагмента на основе уровня API устройства. Это также удобно в тех случаях, когда в противном случае вы получили бы миллионы Build.VERSION.SDK_INT проверок, поскольку FingerprintLoginFragment может использовать любой материал API уровня 23+, который он хочет. - person CommonsWare; 31.05.2016
comment
Я нашел источник проблемы, который является KeyPermanentlyInvalidatedException в блоке initializeCipher() try/catch. Но этот метод никогда не вызывается на устройствах до API 23 (иначе я не думаю, что код будет работать на устройствах API 20+). Я думаю, что в конечном итоге я могу просто создать два класса, как вы предложили, но есть идеи, почему это произойдет? - person Bryan; 01.06.2016
comment
@Bryan: я не припомню, чтобы когда-либо пытался поймать исключение, которое было бы новее моего minSdkVersion. Я не был бы ужасно шокирован, если бы это было чем-то, что принадлежало моему маркированному списку в моем ответе. В зависимости от того, какие другие обработчики исключений у вас есть для этого try, вы можете переключить его, чтобы поймать суперкласс java.security.InvalidKeyException, который существует с уровня API 1. При необходимости выполните instanceof внутри проверки SDK_INT в обработчике InvalidKeyException, чтобы специально проверить наличие и разобраться с KeyPermanentlyInvalidatedException. - person CommonsWare; 01.06.2016
comment
Да! Спасибо, это сработало отлично. Я чувствую, что это должно быть упомянуто в документации по отпечаткам пальцев, ну да ладно. - person Bryan; 01.06.2016
comment
Хотя для KeyPermanentlyInvalidatedException требуется API 23, я не получил эту VerifyError на Android 5.0, а на Android 4.3. Означает ли это, что в Android 5.0 есть KeyPermanentlyInvalidatedException? Где я могу получить информацию о том, существует ли класс на устройстве? - person BakaWaii; 18.01.2018
comment
@BakaWaii: я не вижу, чтобы этот класс отображался при поиске исходного кода Android 5.0.2. Вы, безусловно, можете использовать отражение, чтобы попытаться найти этот класс, хотя это может быть невозможно в будущем. - person CommonsWare; 18.01.2018
comment
Если исходный код обеих версий не содержит этого класса, почему поведение отличается в разных версиях (сбой в 4.3, но не в 5.0)? Что-то о разнице между ART и dalvik? - person BakaWaii; 19.01.2018
comment
@BakaWaii: понятия не имею, извини. - person CommonsWare; 19.01.2018

У меня была такая же ошибка, и я решил ее следующим образом:

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

Это не очень приятно, но это работает

person Jasmin    schedule 29.08.2016
comment
Да, это работает. Подтвержденный. Не любитель этого делать, но у меня есть рыба покрупнее. Это было проблемой только для меня на 4.x. Так что через пару лет я смогу удалить этот уродливый код. - person TheLettuceMaster; 16.04.2017

Как вы сказали, проблема в этом блоке 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

person Rolbin    schedule 14.07.2016
comment
Да, в итоге я это сделал, но мне нужно конкретно поймать KeyPermanentlyInvalidatedException. причина этого заключается в том, что это исключение используется для перехвата каждый раз, когда пользователь должен повторно аутентифицироваться с помощью пароля. InvalidKeyException может быть выбрано в ряде других случаев, поэтому, как упоминалось в @CommonsWare, я проверяю, является ли исключение instanceof KeyPermanentlyInvalidatedException в блоке catch, после первой проверки, является ли API 23+. - person Bryan; 14.07.2016