web-dev-qa-db-fra.com

Android Cryptage et décryptage de l'API d'empreintes digitales

J'utilise l'API Android M d'empreintes digitales pour permettre aux utilisateurs de se connecter à l'application. Pour ce faire, je devrais stocker le nom d'utilisateur et le mot de passe sur l'appareil. Actuellement, la connexion fonctionne, ainsi que l'API Fingerprint, mais le nom d'utilisateur et le mot de passe sont tous deux stockés en texte brut. Je voudrais crypter le mot de passe avant de le stocker et pouvoir le récupérer après que l'utilisateur s'authentifie avec son empreinte digitale.

J'ai beaucoup de difficulté à faire fonctionner cela. J'ai essayé d'appliquer ce que je pouvais à partir de exemples de sécurité Android , mais chaque exemple ne semble gérer que le cryptage ou la signature, et jamais le décryptage.

Ce que j'ai jusqu'à présent, c'est que je dois obtenir une instance de AndroidKeyStore, un KeyPairGenerator et un Cipher, en utilisant une cryptographie asymétrique pour permettre l'utilisation de Android KeyGenParameterSpec.Builder().setUserAuthenticationRequired(true) . La raison de la cryptographie asymétrique est que la méthode setUserAuthenticationRequired bloquera any l'utilisation de la clé si l'utilisateur n'est pas authentifié, mais:

Cette autorisation s'applique uniquement aux opérations de clé secrète et de clé privée. Les opérations sur clé publique ne sont pas limitées.

Cela devrait me permettre de chiffrer le mot de passe à l'aide de la clé publique avant que l'utilisateur ne s'authentifie avec son empreinte digitale, puis de déchiffrer à l'aide de la clé privée uniquement après que l'utilisateur est authentifié.

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

Pour être honnête, je ne sais pas si tout cela est vrai, ce sont des morceaux de tout ce que j'ai pu trouver sur le sujet. Tout ce que je change lève une exception différente, et cette version particulière ne s'exécute pas car je ne peux pas instancier le Cipher, il lève un NoSuchAlgorithmException: No provider found for EC. J'ai également essayé de passer à RSA, mais j'obtiens des erreurs similaires.

Donc, ma question est essentiellement la suivante; comment puis-je chiffrer le texte en clair sur Android et le rendre disponible pour le déchiffrement après que l'utilisateur est authentifié par l'API Fingerprint?


J'ai fait quelques progrès, principalement grâce à la découverte des informations sur la page de documentation KeyGenParameterSpec .

J'ai conservé getKeyStore, encryptePassword, decryptPassword, getKeyPairGenerator et getCipher essentiellement les mêmes, mais j'ai changé le KeyPairGenerator.getInstance et Cipher.getInstance à "RSA" et "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" respectivement.

J'ai également changé le reste du code en RSA au lieu de Elliptic Curve, car d'après ce que je comprends, Java 1.7 (et donc Android) ne prend pas en charge le cryptage et le décryptage avec EC. J'ai changé ma méthode createKeyPair en fonction de l'exemple "RSA paire de clés pour le chiffrement/déchiffrement à l'aide de RSA OAEP" sur la page de documentation:

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

J'ai également modifié ma méthode initCipher en fonction du problème connu dans la documentation KeyGenParameterSpec:

Un bogue connu dans Android 6.0 (API niveau 23) entraîne l'application des autorisations liées à l'authentification des utilisateurs, même pour les clés publiques. Pour contourner ce problème, extrayez le matériel de clé publique à utiliser en dehors du magasin de clés Android.

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

Maintenant, je peux crypter le mot de passe et enregistrer le mot de passe crypté. Mais lorsque j'obtiens le mot de passe chiffré et que je tente de déchiffrer, j'obtiens une KeyStoreException Erreur inconnue ...

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)
41
Bryan

J'ai trouvé la dernière pièce du puzzle sur le Android Issue Tracker , un autre bogue connu rend le PublicKey non restreint incompatible avec le Cipher lors de l'utilisation d'OAEP. La solution consiste à ajouter un nouveau OAEPParameterSpec au Cipher lors de l'initialisation:

OAEPParameterSpec spec = new OAEPParameterSpec(
        "SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);

mCipher.init(opmode, unrestricted, spec);

Voici le code 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);
    }
}
36
Bryan