10

Mam numer DialogFragment, który obsługuje uwierzytelnianie logowania i odcisków palców dla mojej aplikacji. Ten fragment wykorzystuje dwie klasy, które są dostępne tylko dla API 23, KeyGenParameterSpec i KeyPermanentlyInvalidatedException. Byłem pod wrażeniem, że mogę korzystać z tych klas, tak długo, jak sprawdzić wersję kompilacji przed próbuję zainicjować klas (nakreślono here):Jak używać nieobsługiwanego wyjątku dla niższej platformy Wersja

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

Ale wydaje się, że to nie jest przypadek. Jeśli spróbuję uruchomić ten kod w wersji wcześniejszej niż API 20, Dalvik VM odrzuci całą klasę i rzuci VerifyError. Mimo to kod działa na API 20 i więcej. Jak mogę korzystać z tych metod w moim kodzie, nadal pozwalając, aby kod był używany na poprzednich poziomach interfejsu API?

Pełne ślad stosu jest następująca:

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) 

Aktualizacja z Kodeksem

Sposób login() jest tylko wygodną metodą na rozpoczęcie LoginFragment:

public static void login(FragmentManager manager) { 
    manager.beginTransAction().add(LoginFragment.newInstance(), null).commit(); 
} 

The r jeden kod jest w samym LoginFragment. Konkretnie createKeyPair() i initializeCipher metody:

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

} 

Aktualizuj

Ok, więc zorientowali się, że jest to KeyPermanentlyInvalidatedException który jest przyczyną błędu. Jeśli skomentuję blok catch, który obsługuje ten wyjątek, kod działa poprawnie na każdym urządzeniu. Problemem jest to, że muszę być w stanie obsłużyć ten wyjątek na urządzeniach o API 23+:

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

Naprawdę nie możemy ci pomóc z redakcyjnym kodem. Opublikuj [mcve] demonstrujący twój problem, na przykład rzeczywistą implementację metody 'login()', w której się zawieszasz, oraz metodę 'initializeCipher()' twojego 'LoginFragment' (która wydaje się być tam, gdzie nierozpoznane rzeczy kłamstwa). – CommonsWare

+0

@CommonsWare Zaktualizowałem pytanie za pomocą kodu. – Bryan

Odpowiedz

8

Domyślam się, że albo FingerprintCallback.Callback rozszerza 23+ interfejs API Level albo że LoginFragment posiada pola, które odwołują API poziom 23 + rzeczy.

Twoja reguła dotycząca bezpiecznego wywoływania metod API Level 23+ w bloku zabezpieczenia wersji jest prawidłowa. Jednak nie można:

  • dziedziczą z klas, które nie istnieją w urządzeniu
  • wdrożenia interfejsy, które nie istnieją w urządzeniu
  • mają Pola, których typy nie istnieje na urządzeniu
  • zaakceptować parametry konstruktora lub metody, których typy nie istnieje na urządzeniu (gdzie faktycznie nazywają te)
  • mieć wartości zwracanych metod, których typy nie istnieją na urządzeniu (gdzie faktycznie nazywają te)

W wielu przypadkach nie jest nam potrzebna żadna z tych opcji. W takim przypadku wystarczy tylko sprawdzić Build.VERSION.SDK_INT przed wywołaniem metody API Level 23+.

Jeśli chcesz zrobić niektóre rzeczy na liście punktowanej, nie ma problemu, ale musisz je oddzielić na klasy, których używasz tylko na urządzeniach z interfejsem API poziomu 23+.

Na przykład, udawajmy, że problem polega na tym, że FingerprintCallback.Callback rozszerza interfejs API Level 23+. Zamiast implementować FingerprintCallback.Callback na LoginFragment, możesz zaimplementować to jako anonimową klasę wewnętrzną i wykonać kod tworzący tę anonimową instancję klasy wewnętrznej, jeśli Build.VERSION.SDK_INT jest wystarczająco wysoki. Wtedy odnosisz się tylko do FingerprintCallback.Callback na nowszych urządzeniach i powinieneś być bezpieczny.

+0

Widzę, co mówisz, chociaż nie powinno to być "FingerprintCallback", ponieważ jest to klasa, która rozszerza "FingerprintManagerCompat.AuthenticationCallback", która jest częścią [biblioteki wsparcia czytnika linii papilarnych] (https://developer.android .com/reference/android/support/v4/hardware/fingerprint/package-summary.html). Nie widzę też żadnych pól, które odwołują się do klas z API 23+. Będę musiał przejrzeć kod, aby sprawdzić, czy nie spełniam któregoś z pozostałych wymagań. – Bryan

+1

@Bryan: Jeśli skończysz być zakłopotany, podziel "LoginFragment" na 2-3 klasy. Albo mają podklasę 'LoginFragment' i' FingerprintLoginFragment', albo 'LoginFragmentBase' z podklasami' LoginFragment' i 'FingerprintLoginFragment'. Umieść wszystkie rzeczy odcisków palców w 'FingerprintLoginFragment'. Następnie, gdy przychodzi czas na 'add()' fragment, wybierz klasę fragmentów na podstawie poziomu API urządzenia. Jest to również przydatne w przypadkach, w których można skończyć z zillionem 'Build.VERSION.SDK_INT' sprawdza inaczej, ponieważ' FingerprintLoginFragment' może wykorzystywać dowolny poziom API powyżej 23+, który chce. – CommonsWare

+0

Znalazłem źródło problemu, który jest 'KeyPermanentlyInvalidatedException' w bloku' startizeCipher() 'try/catch. Ale ta metoda nigdy nie jest wywoływana na urządzeniach przed API 23 (w przeciwnym razie nie sądzę, by kod działał na urządzeniach API 20+). Myślę, że mogę skończyć, robiąc dwie klasy, takie jak ty, ale jakiekolwiek pomysły, dlaczego tak się dzieje? – Bryan

1

Jak pan powiedział, że problem jest z tego bloku catch

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

Ponieważ ten wyjątek jest dodawany na API poziom 23, ale nie wiem, dlaczego błąd zweryfikować zostaje rzucony w sam raz inicjalizacji.

W każdym razie można złapać wyjątek korzystając

catch (InvalidKeyExceptionexception) { 
    .... 
    return false; 
} 

od KeyPermanentlyInvalidatedException rozciąga InvalidKeyExceptionexception

+1

Tak, skończyło się na tym, ale muszę złapać 'KeyPermanentlyInvalidatedException' specjalnie. [Powodem tego] (https://developer.android.com/reference/android/security/keystore/KeyPermanentlyInvalidatedException.html) jest to, że ten wyjątek jest używany do przechwytywania za każdym razem, gdy użytkownik powinien ponownie uwierzytelnić się przy użyciu hasła. Wyjątek 'InvalidKeyException' może być zgłoszony w wielu innych przypadkach, dlatego, jak wspomniałem @CommonsWare, sprawdzam, czy wyjątek jest' instanceof' 'KeyPermanentlyInvalidatedException' w bloku catch, po pierwszym sprawdzeniu, czy API ma 23+. – Bryan

4

Miałem ten sam błąd i rozwiązać go w następujący sposób:

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

To ISN” t vey ładne, ale działa

+0

Tak, to działa. Zatwardziały. Nie jestem fanem tego, ale mam większe ryby do smażenia. To było tylko dla mnie problemem 4.x. Tak więc za kilka lat mogę usunąć ten brzydki kod. – KickingLettuce