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?
- Carga de Clase Personalizada en Dalvik con Gradle (Android New Build System)
- Java.lang.ClassNotFoundException en dalvik.system.BaseDexClassLoader.findClass
- ¿Por qué JUnit 4 en Android no funciona?
- Cómo ver los bytecodes de Dalvik para mi aplicación en Android Studio?
- La conversión al formato de Dalvik falló con el error 1 puesto que el SDK de Facebook
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; }
- ClassNotFoundException Android
- Android: mi aplicación es demasiado grande y da "No se puede ejecutar dex: método de ID no está en : 65536"?
- Dalvik VM error: Excepción encontrada "Javax.xml.namespace.QName.class"
- Señal fatal 11 (SIGSEGV) cuando el GC se ejecuta después de llamar a KeyChain.getPrivateKey
- Diferencia entre dexopt y dex2oat?
- ¿Cómo se aplica el permiso de Android?
- El tamaño de mapa de bits supera el presupuesto de VM cuando el desarrollo de juegos
- ¿Por qué tantos GC_FOR_ALLOC en una aplicación sencilla?
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
- Prioridad ThreadPoolExecutor en Java (Android)
- ¿Cómo encontrar los dispositivos en el rango mediante el uso de bluetooth?