Cifrado y descifrado de Android Fingerprint API
Estoy utilizando la API de huellas dactilares de Android M para permitir a los usuarios iniciar sesión en la aplicación. Para ello 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 Fingerprint API, 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 ser capaz de recuperarla después de que el usuario se autentica con su huella digital.
Estoy teniendo una gran cantidad de dificultad para que esto funcione. He estado tratando de aplicar lo que puedo de las muestras de seguridad de Android , pero cada ejemplo parece manejar sólo el cifrado o la firma, y nunca descifrar.
- ¿Cómo comprobar el nombre del certificado y el alias en los archivos del almacén de claves?
- ¿Puedo fusionar varios archivos de keystore de Android en uno?
- ¿Dónde puedo encontrar la ubicación predeterminada para las librerías de claves android creadas por eclipse?
- No hay debug.keystore en la carpeta .android
- Almacenamiento seguro de una clave simétrica en el uso de Android KeyChain
Lo que tengo hasta ahora es que tengo que obtener una instancia de la AndroidKeyStore
, un KeyPairGenerator
y un Cipher
, utilizando criptografía asimétrica para permitir el uso de Android KeyGenParameterSpec.Builder().setUserAuthenticationRequired(true)
. La razón de la criptografía asimétrica es porque el método setUserAuthenticationRequired
bloqueará cualquier uso de la clave si el usuario no está autenticado, pero:
Esta autorización se aplica únicamente a las operaciones con 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 autentica con su huella dactilar, luego descifrar utilizando la clave privada sólo después de que el usuario sea 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); } }
Para ser honesto, no estoy seguro de si algo de esto es correcto, es bits y piezas de todo lo que podría encontrar sobre el tema. Todo lo que cambia arroja una excepción diferente, y esta compilación en particular no se ejecuta porque no puedo instanciar el Cipher
, que lanza una NoSuchAlgorithmException: No provider found for EC
. He intentado cambiar a RSA
también, pero consigo errores similares.
Así que mi pregunta es básicamente esto; ¿Cómo puedo cifrar el texto sin formato en Android y hacerlo disponible para el descifrado después de que el usuario sea autenticado por la API de huellas dactilares?
He hecho algunos progresos, principalmente debido al descubrimiento de la información en la página de documentación KeyGenParameterSpec
.
He mantenido getKeyStore
, encryptePassword
, decryptPassword
, getKeyPairGenerator
y getCipher
casi siempre lo mismo, pero he cambiado el KeyPairGenerator.getInstance
y Cipher.getInstance
a "RSA"
y "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
respectivamente.
También cambié el resto del código a RSA en lugar de Elliptic Curve, porque de lo que yo entiendo, Java 1.7 (y por lo tanto Android) no soporta cifrado y descifrado con EC. Cambié mi método createKeyPair
basado en el ejemplo "Par de claves RSA para encriptación / desencriptación utilizando RSA OAEP" en la página de documentación:
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); } }
También modifiqué mi método initCipher
basado en el problema conocido en la documentación de KeyGenParameterSpec
:
Un error conocido en Android 6.0 (nivel 23 de API) hace que las autorizaciones relacionadas con la autenticación de usuario se apliquen incluso para claves públicas. Para evitar este problema extraer el material de clave pública para utilizar fuera de 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); } }
Ahora puedo cifrar la contraseña y guardar la contraseña cifrada. Pero cuando obtengo la contraseña cifrada e intento descifrar, consigo un error desconocido de KeyStoreException
…
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)
- Problemas en la publicación de archivos .apk sin firmar?
- Cómo almacenar la contraseña en Android
- No puede firmar el APK sin firmar
- Cómo importar clave privada RSA, generada por openssl, en AndroidKeyStore
- Cordova build -release Android Unsigned apk
- Android Marketplace: ¿Los detalles (nombre, etc) en el almacén de claves son visibles para el usuario?
- Android - ¿Qué archivo debug.keystore es IntelliJ firmando mi aplicación?
- No se puede encontrar la entrada de Keystore. Pero sé que está ahí
Encontré la pieza final del rompecabezas en el Controlador de problemas de Android , otro error conocido hace que la PublicKey
sin restricciones sea incompatible con el Cipher
cuando se usa OAEP. El trabajo alrededor es agregar un nuevo OAEPParameterSpec
al Cipher
sobre la inicialización:
OAEPParameterSpec spec = new OAEPParameterSpec( "SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT); mCipher.init(opmode, unrestricted, spec);
A continuación se muestra el código final:
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("RSA", "AndroidKeyStore"); } catch(NoSuchAlgorithmException | NoSuchProviderException exception) { throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception); } } public Cipher getCipher() { try { return Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); } catch(NoSuchAlgorithmException | NoSuchPaddingException exception) { throw new RuntimeException("Failed to get an instance of Cipher", exception); } } 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("Failed to generate key pair", exception); } } 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())); OAEPParameterSpec spec = new OAEPParameterSpec( "SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT); mCipher.init(opmode, unrestricted, spec); } 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 encrypted = Base64.encodeToString(bytes, Base64.NO_WRAP); mPreferences.getString("password").set(encrypted); } catch(IllegalBlockSizeException | BadPaddingException exception) { throw new RuntimeException("Failed to encrypt password", exception); } } private String decrypt(Cipher cipher) { try { String encoded = mPreferences.getString("password").get(); byte[] bytes = Base64.decode(encoded, Base64.NO_WRAP); return new String(cipher.doFinal(bytes)); } catch (IllegalBlockSizeException | BadPaddingException exception) { throw new RuntimeException("Failed to decrypt password", exception); } }
- Android – Diálogo personalizado – No se puede obtener texto de EditText
- Encontrar dibujable por cadena