web-dev-qa-db-fra.com

Quelles sont les meilleures pratiques pour utiliser le cryptage AES dans Android?

Pourquoi je pose cette question:

Je sais qu'il y a eu beaucoup de questions sur le cryptage AES, même pour Android. Et il y a beaucoup d'extraits de code si vous effectuez une recherche sur le Web. Mais sur chaque page, dans chaque question Stack Overflow, je trouve une autre implémentation avec des différences majeures.

J'ai donc créé cette question pour trouver une "meilleure pratique". J'espère que nous pourrons collecter une liste des exigences les plus importantes et mettre en place une implémentation vraiment sécurisée!

J'ai lu sur les vecteurs d'initialisation et les sels. Toutes les implémentations que j'ai trouvées n'avaient pas ces fonctionnalités. Vous en avez donc besoin? Cela augmente-t-il beaucoup la sécurité? Comment l'implémentez-vous? L'algorithme doit-il lever des exceptions si les données chiffrées ne peuvent pas être déchiffrées? Ou n'est-ce pas sûr et cela devrait simplement renvoyer une chaîne illisible? L'algorithme peut-il utiliser Bcrypt au lieu de SHA?

Qu'en est-il de ces deux implémentations que j'ai trouvées? Ils vont bien? Parfait ou certaines choses importantes manquent? Qu'est-ce qui est sécurisé?

L'algorithme doit prendre une chaîne et un "mot de passe" pour le chiffrement, puis chiffrer la chaîne avec ce mot de passe. La sortie doit à nouveau être une chaîne (hex ou base64?). Le déchiffrement devrait également être possible, bien sûr.

Quelle est l'implémentation AES parfaite pour Android?

Implémentation # 1:

import Java.security.MessageDigest;
import Java.security.NoSuchAlgorithmException;
import Java.security.NoSuchProviderException;
import Java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class AdvancedCrypto implements ICrypto {

        public static final String PROVIDER = "BC";
        public static final int SALT_LENGTH = 20;
        public static final int IV_LENGTH = 16;
        public static final int PBE_ITERATION_COUNT = 100;

        private static final String RANDOM_ALGORITHM = "SHA1PRNG";
        private static final String HASH_ALGORITHM = "SHA-512";
        private static final String PBE_ALGORITHM = "PBEWithSHA256And256BitAES-CBC-BC";
        private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
        private static final String SECRET_KEY_ALGORITHM = "AES";

        public String encrypt(SecretKey secret, String cleartext) throws CryptoException {
                try {

                        byte[] iv = generateIv();
                        String ivHex = HexEncoder.toHex(iv);
                        IvParameterSpec ivspec = new IvParameterSpec(iv);

                        Cipher encryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
                        encryptionCipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
                        byte[] encryptedText = encryptionCipher.doFinal(cleartext.getBytes("UTF-8"));
                        String encryptedHex = HexEncoder.toHex(encryptedText);

                        return ivHex + encryptedHex;

                } catch (Exception e) {
                        throw new CryptoException("Unable to encrypt", e);
                }
        }

        public String decrypt(SecretKey secret, String encrypted) throws CryptoException {
                try {
                        Cipher decryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
                        String ivHex = encrypted.substring(0, IV_LENGTH * 2);
                        String encryptedHex = encrypted.substring(IV_LENGTH * 2);
                        IvParameterSpec ivspec = new IvParameterSpec(HexEncoder.toByte(ivHex));
                        decryptionCipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
                        byte[] decryptedText = decryptionCipher.doFinal(HexEncoder.toByte(encryptedHex));
                        String decrypted = new String(decryptedText, "UTF-8");
                        return decrypted;
                } catch (Exception e) {
                        throw new CryptoException("Unable to decrypt", e);
                }
        }

        public SecretKey getSecretKey(String password, String salt) throws CryptoException {
                try {
                        PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), HexEncoder.toByte(salt), PBE_ITERATION_COUNT, 256);
                        SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_ALGORITHM, PROVIDER);
                        SecretKey tmp = factory.generateSecret(pbeKeySpec);
                        SecretKey secret = new SecretKeySpec(tmp.getEncoded(), SECRET_KEY_ALGORITHM);
                        return secret;
                } catch (Exception e) {
                        throw new CryptoException("Unable to get secret key", e);
                }
        }

        public String getHash(String password, String salt) throws CryptoException {
                try {
                        String input = password + salt;
                        MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM, PROVIDER);
                        byte[] out = md.digest(input.getBytes("UTF-8"));
                        return HexEncoder.toHex(out);
                } catch (Exception e) {
                        throw new CryptoException("Unable to get hash", e);
                }
        }

        public String generateSalt() throws CryptoException {
                try {
                        SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
                        byte[] salt = new byte[SALT_LENGTH];
                        random.nextBytes(salt);
                        String saltHex = HexEncoder.toHex(salt);
                        return saltHex;
                } catch (Exception e) {
                        throw new CryptoException("Unable to generate salt", e);
                }
        }

        private byte[] generateIv() throws NoSuchAlgorithmException, NoSuchProviderException {
                SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
                byte[] iv = new byte[IV_LENGTH];
                random.nextBytes(iv);
                return iv;
        }

}

Source: http://pocket-for-Android.1047292.n5.nabble.com/Encryption-method-and-reading-the-Dropbox-backup-td4344194.html

Implémentation # 2:

import Java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 * Usage:
 * <pre>
 * String crypto = SimpleCrypto.encrypt(masterpassword, cleartext)
 * ...
 * String cleartext = SimpleCrypto.decrypt(masterpassword, crypto)
 * </pre>
 * @author ferenc.hechler
 */
public class SimpleCrypto {

    public static String encrypt(String seed, String cleartext) throws Exception {
        byte[] rawKey = getRawKey(seed.getBytes());
        byte[] result = encrypt(rawKey, cleartext.getBytes());
        return toHex(result);
    }

    public static String decrypt(String seed, String encrypted) throws Exception {
        byte[] rawKey = getRawKey(seed.getBytes());
        byte[] enc = toByte(encrypted);
        byte[] result = decrypt(rawKey, enc);
        return new String(result);
    }

    private static byte[] getRawKey(byte[] seed) throws Exception {
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
        sr.setSeed(seed);
        kgen.init(128, sr); // 192 and 256 bits may not be available
        SecretKey skey = kgen.generateKey();
        byte[] raw = skey.getEncoded();
        return raw;
    }


    private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
        byte[] encrypted = cipher.doFinal(clear);
        return encrypted;
    }

    private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
        SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec);
        byte[] decrypted = cipher.doFinal(encrypted);
        return decrypted;
    }

    public static String toHex(String txt) {
        return toHex(txt.getBytes());
    }
    public static String fromHex(String hex) {
        return new String(toByte(hex));
    }

    public static byte[] toByte(String hexString) {
        int len = hexString.length()/2;
        byte[] result = new byte[len];
        for (int i = 0; i < len; i++)
            result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
        return result;
    }

    public static String toHex(byte[] buf) {
        if (buf == null)
            return "";
        StringBuffer result = new StringBuffer(2*buf.length);
        for (int i = 0; i < buf.length; i++) {
            appendHex(result, buf[i]);
        }
        return result.toString();
    }
    private final static String HEX = "0123456789ABCDEF";
    private static void appendHex(StringBuffer sb, byte b) {
        sb.append(HEX.charAt((b>>4)&0x0f)).append(HEX.charAt(b&0x0f));
    }

}

Source: http://www.tutorials-Android.com/learn/How_to_encrypt_and_decrypt_strings.rhtml

84
caw

Aucune implémentation que vous donnez dans votre question n'est entièrement correcte, et aucune implémentation que vous donnez ne doit être utilisée telle quelle. Dans ce qui suit, je vais discuter des aspects du mot de passe cryptage basé sur Android.

Clés et hachages

Je vais commencer à discuter du système basé sur un mot de passe avec des sels. Le sel est un nombre généré aléatoirement. Il n'est pas "déduit". L'implémentation 1 inclut une méthode generateSalt() qui génère un nombre aléatoire cryptographiquement fort. Parce que le sel est important pour la sécurité, il doit être gardé secret une fois qu'il est généré, bien qu'il ne doive être généré qu'une seule fois. S'il s'agit d'un site Web, il est relativement facile de garder le secret secret, mais pour les applications installées (pour les ordinateurs de bureau et les appareils mobiles), ce sera beaucoup plus difficile.

La méthode getHash() renvoie un hachage du mot de passe et du sel donnés, concaténés en une seule chaîne. L'algorithme utilisé est SHA-512, qui renvoie un hachage de 512 bits. Cette méthode retourne un hachage qui est utile pour vérifier l'intégrité d'une chaîne, il pourrait donc aussi bien être utilisé en appelant getHash() avec juste un mot de passe ou juste un sel, car il concatène simplement les deux paramètres. Étant donné que cette méthode ne sera pas utilisée dans le système de cryptage basé sur un mot de passe, je n'en discuterai pas plus avant.

La méthode getSecretKey(), dérive une clé d'un tableau char du mot de passe et d'un sel codé hexadécimal, tel que renvoyé par generateSalt(). L'algorithme utilisé est PBKDF1 (je pense) de PKCS5 avec SHA-256 comme fonction de hachage, et renvoie une clé de 256 bits. getSecretKey() génère une clé en générant à plusieurs reprises des hachages du mot de passe, du sel et d'un compteur (jusqu'au nombre d'itérations donné dans PBE_ITERATION_COUNT, ici 100) afin d'augmenter le temps nécessaire au montage une attaque par force brute. La longueur du sel doit être au moins aussi longue que la clé générée, dans ce cas, au moins 256 bits. Le nombre d'itérations doit être défini aussi longtemps que possible sans provoquer de retard déraisonnable. Pour plus d'informations sur les sels et le nombre d'itérations dans la dérivation des clés, voir la section 4 dans RFC2898 .

L'implémentation dans le PBE de Java, cependant, est défectueuse si le mot de passe contient des caractères Unicode, c'est-à-dire ceux qui nécessitent plus de 8 bits pour être représentés. Comme indiqué dans PBEKeySpec , "le mécanisme PBE défini dans PKCS # 5 ne regarde que les 8 bits de poids faible de chaque caractère". Pour contourner ce problème, vous pouvez essayer de générer une chaîne hexadécimale (qui ne contiendra que des caractères 8 bits) de tous les caractères 16 bits du mot de passe avant de le transmettre à PBEKeySpec. Par exemple, "ABC" devient "004100420043". Notez également que PBEKeySpec "demande le mot de passe comme un tableau de caractères, afin qu'il puisse être remplacé [avec clearPassword()] une fois terminé". (En ce qui concerne la "protection des chaînes en mémoire", voir cette question .) Je ne vois aucun problème, cependant, avec la représentation d'un sel comme une chaîne codée hexadécimale.

Cryptage

Une fois qu'une clé est générée, nous pouvons l'utiliser pour crypter et décrypter du texte. Dans l'implémentation 1, l'algorithme de chiffrement utilisé est AES/CBC/PKCS5Padding, C'est-à-dire AES en mode de chiffrement CBC (Cipher Block Chaining), avec un remplissage défini dans PKCS # 5. (Les autres modes de chiffrement AES incluent le mode compteur (CTR), le mode livre de codes électroniques (ECB) et le mode compteur Galois (GCM). ne autre question sur le débordement de pile contient des réponses qui décrivent en détail les différents modes de chiffrement AES et celles qu'il est recommandé d'utiliser. Sachez également qu'il existe plusieurs attaques contre le cryptage en mode CBC, dont certaines sont mentionnées dans la RFC 7457.)

Si le texte chiffré sera mis à la disposition des étrangers, il est recommandé d'appliquer un code d'authentification de message, ou MAC, aux données chiffrées (et éventuellement des paramètres supplémentaires) pour protéger leur intégrité (une technique connue sous le nom de chiffrement authentifié avec associé , AEAD, décrit dans la RFC 5116). Les MAC à hachage, ou HMAC, sont basés sur SHA-256 ou d'autres fonctions de hachage sécurisées. Si un MAC est utilisé, cependant, en utilisant un secret qui est au moins deux fois plus long qu'une clé de cryptage normale est recommandé, pour éviter les attaques de clés associées: la première moitié sert de clé de cryptage et la seconde moitié sert de clé pour le MAC. (C'est-à-dire, dans ce cas, générer un seul secret à partir d'un mot de passe et d'un sel, et diviser ce secret en deux.)

Implémentation Java

Les différentes fonctions de l'implémentation 1 utilisent un fournisseur spécifique, à savoir "BC", pour ses algorithmes. En général, cependant, il n'est pas recommandé de demander des fournisseurs spécifiques, car tous les fournisseurs ne sont pas disponibles sur toutes les implémentations Java, que ce soit par manque de support, pour éviter la duplication de code ou pour d'autres raisons. Ce conseil est devenu particulièrement important depuis la sortie de Android P preview au début de 2018, car certaines fonctionnalités du fournisseur "BC" y sont déconseillées - voir l'article "Modifications de la cryptographie dans Android P "dans le Android Blog des développeurs. Voir aussi Introduction to Oracle Providers .

Ainsi, PROVIDER ne devrait pas exister et la chaîne -BC Devrait être supprimée de PBE_ALGORITHM. La mise en œuvre 2 est correcte à cet égard.

Il est inapproprié qu'une méthode intercepte toutes les exceptions, mais plutôt qu'elle ne gère que les exceptions qu'elle peut. Les implémentations données dans votre question peuvent déclencher une variété d'exceptions vérifiées. Une méthode peut choisir d'envelopper uniquement les exceptions vérifiées avec CryptoException, ou spécifier ces exceptions vérifiées dans la clause throws. Pour plus de commodité, l'encapsulation de l'exception d'origine avec CryptoException peut être appropriée ici, car il existe potentiellement de nombreuses exceptions vérifiées que les classes peuvent lever.

SecureRandom dans Android

Comme détaillé dans l'article "Quelques réflexions sur SecureRandom", dans le Android Developers Blog, la mise en œuvre de Java.security.SecureRandom Dans Android versions avant 2013 a une faille qui réduit la force des nombres aléatoires qu'elle délivre. Cette faille peut être atténuée en passant un bloc de données imprévisible et aléatoire (comme la sortie de /dev/urandom) à la méthode setSeed de cette classe.

35
Peter O.

# 2 ne doit jamais être utilisé car il utilise uniquement "AES" (ce qui signifie un chiffrement en mode ECB sur le texte, un gros non-non) pour le chiffrement. Je vais juste parler de # 1.

La première implémentation semble respecter les meilleures pratiques de chiffrement. Les constantes sont généralement OK, bien que la taille du sel et le nombre d'itérations pour effectuer PBE soient sur le côté court. De plus, cela semble être pour AES-256 puisque la génération de clés PBE utilise 256 comme valeur codée en dur (une honte après toutes ces constantes). Il utilise CBC et PKCS5Padding qui est au moins ce que vous attendez.

Il manque complètement toute protection d'authentification/intégrité, de sorte qu'un attaquant peut modifier le texte de chiffrement. Cela signifie que le remplissage des attaques Oracle est possible dans un modèle client/serveur. Cela signifie également qu'un attaquant peut essayer de modifier les données chiffrées. Cela entraînera probablement une erreur quelque part, car le remplissage ou le contenu n'est pas accepté par l'application, mais ce n'est pas une situation dans laquelle vous souhaitez être.

La gestion des exceptions et la validation des entrées pourraient être améliorées, la capture d'exceptions est toujours mauvaise dans mon livre. De plus, la classe implémente ICrypt, que je ne connais pas. Je sais qu'avoir uniquement des méthodes sans effets secondaires dans une classe est un peu bizarre. Normalement, vous les rendriez statiques. Il n'y a pas de mise en mémoire tampon des instances de chiffrement, etc., donc chaque objet requis est créé ad-nauseum. Cependant, vous pouvez supprimer ICrypto en toute sécurité de la définition qu'il semble, dans ce cas, vous pouvez également refactoriser le code en méthodes statiques (ou le réécrire pour être plus orienté objet, votre choix).

Le problème est que tout wrapper fait toujours des hypothèses sur le cas d'utilisation. Dire qu'un wrapper a raison ou tort est donc superposé. C'est pourquoi j'essaie toujours d'éviter de générer des classes wrapper. Mais au moins, cela ne semble pas explicitement faux.

15
Maarten Bodewes

Vous avez posé une question assez intéressante. Comme pour tous les algorithmes, la clé de chiffrement est la "sauce secrète", car une fois connue du public, tout le reste l'est aussi. Donc, vous cherchez des moyens de ce document par Google

sécurité

En plus de la facturation Google In-App, la réflexion sur la sécurité est également intéressante.

billing_best_practices

1
the100rabh

J'ai trouvé une implémentation de Nice ici: http://nelenkov.blogspot.fr/2012/04/using-password-based-encryption-on.html et https: // github. com/nelenkov/Android-pbe Cela a également été utile dans ma quête d'une implémentation AES assez bonne pour Android

0
clément francomme

Utilisez l'API BouncyCastle Lightweight. Il fournit 256 AES avec PBE et sel.
Voici un exemple de code, qui peut crypter/décrypter des fichiers.

public void encrypt(InputStream fin, OutputStream fout, String password) {
    try {
        PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest());
        char[] passwordChars = password.toCharArray();
        final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
        pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount);
        CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
        ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128);
        aesCBC.init(true, aesCBCParams);
        PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
        aesCipher.init(true, aesCBCParams);

        // Read in the decrypted bytes and write the cleartext to out
        int numRead = 0;
        while ((numRead = fin.read(buf)) >= 0) {
            if (numRead == 1024) {
                byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)];
                int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
                final byte[] plain = new byte[offset];
                System.arraycopy(plainTemp, 0, plain, 0, plain.length);
                fout.write(plain, 0, plain.length);
            } else {
                byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
                int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
                int last = aesCipher.doFinal(plainTemp, offset);
                final byte[] plain = new byte[offset + last];
                System.arraycopy(plainTemp, 0, plain, 0, plain.length);
                fout.write(plain, 0, plain.length);
            }
        }
        fout.close();
        fin.close();
    } catch (Exception e) {
        e.printStackTrace();
    }

}

public void decrypt(InputStream fin, OutputStream fout, String password) {
    try {
        PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest());
        char[] passwordChars = password.toCharArray();
        final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars);
        pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount);
        CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine());
        ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128);
        aesCBC.init(false, aesCBCParams);
        PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding());
        aesCipher.init(false, aesCBCParams);

        // Read in the decrypted bytes and write the cleartext to out
        int numRead = 0;
        while ((numRead = fin.read(buf)) >= 0) {
            if (numRead == 1024) {
                byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)];
                int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
                // int last = aesCipher.doFinal(plainTemp, offset);
                final byte[] plain = new byte[offset];
                System.arraycopy(plainTemp, 0, plain, 0, plain.length);
                fout.write(plain, 0, plain.length);
            } else {
                byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)];
                int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0);
                int last = aesCipher.doFinal(plainTemp, offset);
                final byte[] plain = new byte[offset + last];
                System.arraycopy(plainTemp, 0, plain, 0, plain.length);
                fout.write(plain, 0, plain.length);
            }
        }
        fout.close();
        fin.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
0
kelheor