Cómo utilizar excepciones no compatibles para versión de plataforma inferior

Tengo un DialogFragment que maneja la autentificación de la entrada y de la huella digital para mi uso. Este fragmento utiliza dos clases que son exclusivas de API 23, KeyGenParameterSpec y KeyPermanentlyInvalidatedException . Había tenido la impresión de que podía usar estas clases, siempre y cuando compruebe la versión de compilación antes de intentar inicializar las clases (descritas aquí ):

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

Pero parece que este no es el caso. Si intento ejecutar este código en una versión anterior a la API 20, la VM Dalvik rechaza toda la clase y lanza VerifyError . Sin embargo, el código funciona para API 20 y superior. ¿Cómo puedo utilizar estos métodos en mi código mientras se sigue permitiendo que el código se utilice para los niveles anteriores de la API?

La traza de pila completa es la siguiente:

 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) 

Actualizado con Código

El método login() es sólo un método de conveniencia para iniciar el LoginFragment :

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

El código correspondiente se encuentra en el propio LoginFragment . Específicamente los createKeyPair() e 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(); } } 

Actualizar

Bueno, así que me di cuenta de que es la KeyPermanentlyInvalidatedException que está causando el error. Si comento el bloque catch que maneja esa excepción, el código funciona bien en cualquier dispositivo. El problema es que necesito poder manejar esa excepción en los dispositivos de API 23+:

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

Mi conjetura es que cualquiera de FingerprintCallback.Callback extiende una interfaz API Level 23 + o que LoginFragment tiene campos que hacen referencia a nivel API 23 + cosas.

Su regla sobre poder llamar a los métodos API Level 23+ de forma segura dentro del bloque de guardia de versión es correcta. Sin embargo, no puede:

  • Heredar de clases que no existen en el dispositivo
  • Implementar interfaces que no existen en el dispositivo
  • Tienen campos cuyos tipos no existen en el dispositivo
  • Aceptar parámetros del constructor o del método cuyos tipos no existen en el dispositivo (donde realmente llamamos estos)
  • Tienen valores de retorno del método cuyos tipos no existen en el dispositivo (donde realmente llamamos estos)

En muchos casos, no necesitamos nada de eso, en cuyo caso basta con comprobar Build.VERSION.SDK_INT antes de llamar al método API Level 23+.

Si necesitas hacer algunas de las cosas en la lista con viñetas, eso está bien, pero entonces necesitas aislarlas en clases que solo uses en dispositivos API Level 23+.

Así, por ejemplo, supongamos que el problema es que FingerprintCallback.Callback extiende alguna interfaz API Level 23+. En lugar de implementar FingerprintCallback.Callback en el LoginFragment , puede implementar eso como una clase interna anónima y ejecutar sólo el código que crea esa instancia de clase interna anónima si Build.VERSION.SDK_INT es lo suficientemente alta. Entonces, sólo está haciendo referencia a FingerprintCallback.Callback en los dispositivos más nuevos, y debe estar a salvo.

Tuve el mismo error y lo resolví de la siguiente manera:

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

No es muy agradable, pero funciona

Como usted dijo el problema es con ese bloque de la captura

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

Debido a que se agrega la excepción en API NIVEL 23 , pero no sé por qué el error de verificación se activa en el momento de la inicialización.

De todas maneras puedes coger la excepción usando

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

Desde KeyPermanentlyInvalidatedException extiende InvalidKeyExceptionexception

  • Consulta sobre propiedades en /system/build.prop
  • ¿Existe un archivo de clase universal o "grasa" para Java?
  • ¿Es posible cargar dinámicamente una biblioteca en tiempo de ejecución desde una aplicación de Android?
  • La biblioteca de terceros se agrega bajo el directorio dexedLib, pero la biblioteca no tiene classes.dex
  • Desaparecidos constructores por defecto
  • Android: la verificación falló en la clase ART / Dalvik
  • ¿Cómo puedo usar la opción `--multi-dex`?
  • ¿Los métodos heredados cuentan contra el límite del método Dex en Android?
  • ¿Los procesos de VM de Dalvik liberan la RAM del sistema?
  • ¿Cómo integrar Scala en la plataforma Android principal?
  • Android 4.4 KitKat accidente aleatorio
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.