Cifrado y descifrado de API de huellas dactilares de Android
Estoy usando la API Android M Fingerprint para permitir que los usuarios inicien sesión en la aplicación. Para hacer esto, necesitaría almacenar el nombre de usuario y la contraseña en el dispositivo. Actualmente tengo el inicio de sesión funcionando, así como la API de huellas digitales, pero el nombre de usuario y la contraseña se almacenan como texto sin formato. Me gustaría cifrar la contraseña antes de almacenarla y poder recuperarla después de que el usuario se autentique con su huella digital.
Tengo muchas dificultades para que esto funcione. He estado tratando de aplicar lo que puedo delMuestras de seguridad de Android, pero cada ejemplo parece manejar solo el cifrado o la firma, y nunca el descifrado.
Lo que tengo hasta ahora es que tengo que obtener una instancia de laAndroidKeyStore
, unaKeyPairGenerator
y unCipher
, utilizando criptografía asimétrica para permitir el uso de AndroidKeyGenParameterSpec.Builder().setUserAuthenticationRequired(true)
. La razón de la criptografía asimétrica es porque elsetUserAuthenticationRequired
método bloquearáalguna uso de la clave si el usuario no está autenticado, pero:
Esta autorización se aplica solo a las operaciones de clave secreta y clave privada. Las operaciones de clave pública no están restringidas.
Esto debería permitirme encriptar la contraseña usando la clave pública antes de que el usuario se autentique con su huella digital, luego descifrar usando la clave privada solo después de que el usuario esté autenticado.
public KeyStore getKeyStore() { try { return KeyStore.getInstance("AndroidKeyStore"); } catch (KeyStoreException exception) { throw new RuntimeException("Failed to get an instance of KeyStore", exception); } } public KeyPairGenerator getKeyPairGenerator() { try { return KeyPairGenerator.getInstance("EC", "AndroidKeyStore"); } catch(NoSuchAlgorithmException | NoSuchProviderException exception) { throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception); } } public Cipher getCipher() { try { return Cipher.getInstance("EC"); } catch(NoSuchAlgorithmException | NoSuchPaddingException exception) { throw new RuntimeException("Failed to get an instance of Cipher", exception); } } private void createKey() { try { mKeyPairGenerator.initialize( new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1") .setUserAuthenticationRequired(true) .build()); mKeyPairGenerator.generateKeyPair(); } catch(InvalidAlgorithmParameterException exception) { throw new RuntimeException(exception); } } private boolean initCipher(int opmode) { try { mKeyStore.load(null); if(opmode == Cipher.ENCRYPT_MODE) { PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey(); mCipher.init(opmode, key); } else { PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null); mCipher.init(opmode, key); } return true; } catch (KeyPermanentlyInvalidatedException exception) { return false; } catch(KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException exception) { throw new RuntimeException("Failed to initialize Cipher", exception); } } private void encrypt(String password) { try { initCipher(Cipher.ENCRYPT_MODE); byte[] bytes = mCipher.doFinal(password.getBytes()); String encryptedPassword = Base64.encodeToString(bytes, Base64.NO_WRAP); mPreferences.getString("password").set(encryptedPassword); } catch(IllegalBlockSizeException | BadPaddingException exception) { throw new RuntimeException("Failed to encrypt password", exception); } } private String decryptPassword(Cipher cipher) { try { String encryptedPassword = mPreferences.getString("password").get(); byte[] bytes = Base64.decode(encryptedPassword, Base64.NO_WRAP); return new String(cipher.doFinal(bytes)); } catch (IllegalBlockSizeException | BadPaddingException exception) { throw new RuntimeException("Failed to decrypt password", exception); } }
To be honest, I am not sure if any of this is right, it is bits and pieces from anything I could find on the subject. Everything I change throws a different exception, and this particular build does not run because I cannot instantiate the
Cipher
, it throws aNoSuchAlgorithmException: No provider found for EC
. I have tried switch toRSA
as well, but I get similar errors.So my question is basically this; how can I encrypt plaintext on Android, and make it available for decryption after the user is authenticated by the Fingerprint API?
I have made some progress, mostly due to the discovery of the information on the
KeyGenParameterSpec
documentation page.I have kept
getKeyStore
,encryptePassword
,decryptPassword
,getKeyPairGenerator
andgetCipher
mostly the same, but I changed theKeyPairGenerator.getInstance
andCipher.getInstance
to"RSA"
and"RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
respectively.I also changed the rest of the code to RSA instead of Elliptic Curve, because from what I understand, Java 1.7 (and therefore Android) does not support encryption and decryption with EC. I changed my
createKeyPair
method based on the "RSA key pair for encryption/decryption using RSA OAEP" example on the documentation page:private void createKeyPair() { try { mKeyPairGenerator.initialize( new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_DECRYPT) .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) .setUserAuthenticationRequired(true) .build()); mKeyPairGenerator.generateKeyPair(); } catch(InvalidAlgorithmParameterException exception) { throw new RuntimeException(exception); } }
I also altered my
initCipher
method based on the known issue in theKeyGenParameterSpec
documentation: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 Android Keystore.
private boolean initCipher(int opmode) { try { mKeyStore.load(null); if(opmode == Cipher.ENCRYPT_MODE) { PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey(); PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm()) .generatePublic(new X509EncodedKeySpec(key.getEncoded())); mCipher.init(opmode, unrestricted); } else { PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null); mCipher.init(opmode, key); } return true; } catch (KeyPermanentlyInvalidatedException exception) { return false; } catch(KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException exception) { throw new RuntimeException("Failed to initialize Cipher", exception); } }
Now I can encrypt the password, and save the encrypted password. But when I obtain the encrypted password and attempt to decrypt, I get a
KeyStoreException
Unknown error...03-15 10:06:58.074 14702-14702/com.example.app E/LoginFragment: Failed to decrypt password javax.crypto.IllegalBlockSizeException at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:486) at javax.crypto.Cipher.doFinal(Cipher.java:1502) at com.example.app.ui.fragment.util.LoginFragment.onAuthenticationSucceeded(LoginFragment.java:251) at com.example.app.ui.controller.FingerprintCallback.onAuthenticationSucceeded(FingerprintCallback.java:21) at android.support.v4.hardware.fingerprint.FingerprintManagerCompat$Api23FingerprintManagerCompatImpl$1.onAuthenticationSucceeded(FingerprintManagerCompat.java:301) at android.support.v4.hardware.fingerprint.FingerprintManagerCompatApi23$1.onAuthenticationSucceeded(FingerprintManagerCompatApi23.java:96) at android.hardware.fingerprint.FingerprintManager$MyHandler.sendAuthenticatedSucceeded(FingerprintManager.java:805) at android.hardware.fingerprint.FingerprintManager$MyHandler.handleMessage(FingerprintManager.java:757) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5417) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) Caused by: android.security.KeyStoreException: Unknown error at android.security.KeyStore.getKeyStoreException(KeyStore.java:632) at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:224) at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:473) at javax.crypto.Cipher.doFinal(Cipher.java:1502) at com.example.app.ui.fragment.util.LoginFragment.onAuthenticationSucceeded(LoginFragment.java:251) at com.example.app.ui.controller.FingerprintCallback.onAuthenticationSucceeded(FingerprintCallback.java:21) at android.support.v4.hardware.fingerprint.FingerprintManagerCompat$Api23FingerprintManagerCompatImpl$1.onAuthenticationSucceeded(FingerprintManagerCompat.java:301) at android.support.v4.hardware.fingerprint.FingerprintManagerCompatApi23$1.onAuthenticationSucceeded(FingerprintManagerCompatApi23.java:96) at android.hardware.fingerprint.FingerprintManager$MyHandler.sendAuthenticatedSucceeded(FingerprintManager.java:805) at android.hardware.fingerprint.FingerprintManager$MyHandler.handleMessage(FingerprintManager.java:757) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:148) at android.app.ActivityThread.main(ActivityThread.java:5417) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)