Каковы лучшие практики для использования шифрования AES в Android? - PullRequest
84 голосов
/ 24 декабря 2011

Почему я задаю этот вопрос:

Я знаю, что было много вопросов о шифровании AES, даже для Android.И есть много фрагментов кода, если вы ищете в Интернете.Но на каждой странице, в каждом вопросе переполнения стека я нахожу другую реализацию с существенными отличиями.

Поэтому я создал этот вопрос, чтобы найти «наилучшую практику».Я надеюсь, что мы сможем собрать список наиболее важных требований и настроить действительно безопасную реализацию!

Я читал о векторах инициализации и солях.Не все реализации, которые я обнаружил, имели эти функции.Так тебе это нужно?Это сильно повышает безопасность?Как вы это реализуете?Должен ли алгоритм вызывать исключения, если зашифрованные данные не могут быть расшифрованы?Или это небезопасно и должно просто возвращать нечитаемую строку?Может ли алгоритм использовать Bcrypt вместо SHA?

Как насчет этих двух реализаций, которые я нашел?Они в порядке?Идеально или некоторые важные вещи отсутствуют?Что из этого является безопасным?

Алгоритм должен взять строку и «пароль» для шифрования, а затем зашифровать строку этим паролем.Выходными данными снова должна быть строка (hex или base64?).Разумеется, расшифровка также возможна.

Какая идеальная реализация AES для Android?

Реализация №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;
        }

}

Источник: http://pocket -for-android.1047292.n5.nabble.com / Метод шифрования и чтение-Dropbox-backup-td4344194.html

Реализация # 2:

<code>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)
 * 
* @author ferenc.hechler * / public class SimpleCrypto {public static String encrypt (String seed, String cleartext) выдает исключение {byte [] rawKey =getRawKey (seed.getBytes ());byte [] result = encrypt (rawKey, cleartext.getBytes ());возврат к Hex (результат);} public static String decrypt (String seed, String encrypted) создает исключение {byte [] rawKey = getRawKey (seed.getBytes ());byte [] enc = toByte (зашифрованный);byte [] result = decrypt (rawKey, enc);вернуть новую строку (результат);} закрытый статический byte [] getRawKey (byte [] seed) создает исключение {KeyGenerator kgen = KeyGenerator.getInstance ("AES");SecureRandom sr = SecureRandom.getInstance ("SHA1PRNG");sr.setSeed (семян);kgen.init (128, ср);// 192 и 256 битов могут быть недоступны SecretKey skey = kgen.generateKey ();byte [] raw = skey.getEncoded ();вернуть сырье;} private static byte [] encrypt (byte [] raw, byte [] clear) создает исключение {SecretKeySpec skeySpec = new SecretKeySpec (raw, "AES");Cipher cipher = Cipher.getInstance ("AES");cipher.init (Cipher.ENCRYPT_MODE, skeySpec);byte [] encrypted = cipher.doFinal (clear);возврат зашифрован;} частный статический byte [] decrypt (byte [] raw, byte [] зашифрованный) создает исключение {SecretKeySpec skeySpec = new SecretKeySpec (raw, "AES");Cipher cipher = Cipher.getInstance ("AES");cipher.init (Cipher.DECRYPT_MODE, skeySpec);byte [] decrypted = cipher.doFinal (зашифрованный);возврат расшифрован;} public static String toHex (String txt) {return toHex (txt.getBytes ());} public static String fromHex (String hex) {return new String (toByte (hex));} открытый статический byte [] toByte (String hexString) {int len ​​= hexString.length () / 2;байт [] результат = новый байт [лен];для (int i = 0; i > 4) & 0x0F)) добавить (HEX.charAt (б & 0x0F)); } }

Источник: http://www.tutorials -android.com / learn / How_to_encrypt_and_decrypt_strings.rhtml

Ответы [ 5 ]

35 голосов
/ 31 декабря 2011

Ни одна из реализаций, которые вы даете в своем вопросе, не является полностью правильной, и ни одна из реализаций, которую вы даете, не должна использоваться как есть. В дальнейшем я буду обсуждать аспекты шифрования на основе паролей в Android.

Ключи и хэши

Я начну обсуждение парольной системы с солями.Соль - это случайное число.Это не "вывод".Реализация 1 включает метод generateSalt(), который генерирует криптографически сильное случайное число.Поскольку соль важна для безопасности, она должна храниться в секрете после генерации, хотя ее нужно генерировать только один раз.Если это веб-сайт, то хранить секретность относительно легко, но для установленных приложений (для настольных и мобильных устройств) это будет гораздо сложнее.

Метод getHash() возвращает хэшданный пароль и соль, объединенные в одну строку.Используется алгоритм SHA-512, который возвращает 512-битный хэш.Этот метод возвращает хеш, который полезен для проверки целостности строки, поэтому его также можно использовать, вызывая getHash() только с паролем или просто солью, поскольку он просто объединяет оба параметра.Поскольку этот метод не будет использоваться в системе шифрования на основе пароля, я не буду обсуждать его дальше.

Метод getSecretKey(), получает ключ из массива char пароля ишестнадцатеричная соль, возвращенная из generateSalt().Используемый алгоритм - это PBKDF1 (я думаю) от PKCS5 с SHA-256 в качестве хэш-функции и возвращает 256-битный ключ.getSecretKey() генерирует ключ путем многократного генерирования хэшей пароля, соли и счетчика (вплоть до числа итераций, указанного в PBE_ITERATION_COUNT, здесь 100), чтобы увеличить время, необходимое для проведения атаки методом перебора.Длина соли должна составлять не менее длины генерируемого ключа, в данном случае не менее 256 бит.Счетчик итераций должен быть установлен как можно дольше, не вызывая необоснованной задержки.Для получения дополнительной информации о солях и количестве итераций при получении ключа см. Раздел 4 в RFC2898 .

Однако реализация в PBE Java имеет недостатки, если пароль содержит символы Unicode, то естьте, которые требуют более 8 бит для представления.Как указано в PBEKeySpec, «механизм PBE, определенный в PKCS # 5, рассматривает только младшие 8 бит каждого символа».Чтобы обойти эту проблему, вы можете попытаться сгенерировать шестнадцатеричную строку (которая будет содержать только 8-битные символы) из всех 16-битных символов в пароле перед передачей его на PBEKeySpec.Например, «ABC» становится «004100420043».Также обратите внимание, что PBEKeySpec «запрашивает пароль как массив символов, поэтому его можно переписать [с clearPassword()] после завершения».(Что касается "защиты строк в памяти", см. этот вопрос .) Однако я не вижу никаких проблем с представлением соли в виде шестнадцатеричной строки.

Шифрование

Как только ключ сгенерирован, мы можем использовать его для шифрования и дешифрования текста.В реализации 1 используется алгоритм шифрования AES/CBC/PKCS5Padding, то есть AES в режиме шифрования Cipher Block Chaining (CBC) с заполнением, определенным в PKCS # 5.(Другие режимы шифрования AES включают режим счетчика (CTR), режим электронной кодовой книги (ECB) и режим счетчика Галуа (GCM). Еще один вопрос о переполнении стека содержит ответы, в которых подробно обсуждаются различные режимы шифрования AES ирекомендуемые для использования. Также имейте в виду, что существует несколько атак на шифрование в режиме CBC, некоторые из которых упомянуты в RFC 7457.)

Если зашифрованный текст будет доступен посторонним, то для защиты его целостности рекомендуется применять код аутентификации сообщения или MAC к зашифрованным данным (и, необязательно, дополнительные параметры) (метод, известный как аутентифицированное шифрование)со связанными данными , AEAD, описанными в RFC 5116).Здесь популярны MAC-адреса на основе хеш-функции или HMAC, основанные на SHA-256 или других безопасных хеш-функциях.Однако, если используется MAC, рекомендуется использовать секрет, который, как минимум, вдвое дольше обычного ключа шифрования, чтобы избежать атак с использованием соответствующего ключа: первая половина служит ключом шифрования, а вторая половина служит ключом дляMAC.(То есть в этом случае сгенерируйте один секрет из пароля и соли и разделите этот секрет на два.)

Реализация Java

Различные функции вВ реализации 1 используют определенный поставщик, а именно «BC», для своих алгоритмов.В целом, однако, не рекомендуется запрашивать конкретных поставщиков, так как не все поставщики доступны во всех реализациях Java, будь то из-за отсутствия поддержки, чтобы избежать дублирования кода или по другим причинам.Этот совет стал особенно важным после выпуска предварительного просмотра Android P в начале 2018 года, потому что некоторые функции от поставщика «BC» там устарели - см. Статью «Изменения в криптографии в Android P» в блоге разработчиков Android.См. Также Введение в провайдеров Oracle .

Таким образом, PROVIDER не должно существовать и строка -BC должна быть удалена из PBE_ALGORITHM.В этом отношении реализация 2 верна.

Неуместно, чтобы метод перехватывал все исключения, а обрабатывал только те исключения, которые он может.Реализации, приведенные в вашем вопросе, могут создавать различные проверенные исключения.Метод может выбрать обтекание только тех проверенных исключений с помощью CryptoException или указать эти проверенные исключения в предложении throws.Для удобства здесь может быть уместно оборачивать исходное исключение с помощью CryptoException, поскольку существует потенциально много проверенных исключений, которые могут выдавать классы.

SecureRandom в Android

Asподробно описано в статье «Некоторые мысли о SecureRandom» в блоге разработчиков Android. Реализация java.security.SecureRandom в выпусках Android до 2013 года имеет недостаток, который уменьшает силу случайных чисел, которые она доставляет.Этот недостаток можно устранить, передав непредсказуемый и случайный блок данных (например, вывод /dev/urandom) методу setSeed этого класса.

15 голосов
/ 29 декабря 2011

# 2 никогда не следует использовать, поскольку он использует только «AES» (что означает шифрование в режиме ECB для текста, большое нет-нет) для шифра. Я просто расскажу о # 1.

Первая реализация, похоже, придерживается лучших практик шифрования. Константы, как правило, в порядке, хотя размер соли и количество итераций для выполнения PBE находятся на короткой стороне. Кроме того, похоже, что это для AES-256, поскольку при генерации ключа PBE 256 используется как жестко закодированное значение (позор после всех этих констант). Он использует CBC и PKCS5Padding, по крайней мере, то, что вы ожидаете.

Полностью отсутствует какая-либо аутентификация / защита целостности, поэтому злоумышленник может изменить текст шифра. Это означает, что в модели клиент / сервер возможны атаки оракула. Это также означает, что злоумышленник может попытаться изменить зашифрованные данные. Скорее всего, это приведет к некоторой ошибке где-то, потому что дополнение или контент не будут приняты приложением, но это не та ситуация, в которую вы хотите попасть.

Обработка исключений и проверка правильности ввода могут быть улучшены, поймать исключение всегда неправильно в моей книге. Кроме того, класс реализует ICrypt, чего я не знаю. Я знаю, что иметь в классе только методы без побочных эффектов немного странно. Обычно вы бы сделали это статичным. Буферизация экземпляров Cipher и т. Д. Отсутствует, поэтому каждый необходимый объект создается до появления тошноты. Тем не менее, вы можете безопасно удалить ICrypto из определения, которое вам кажется, в этом случае вы также можете рефакторинг кода в статические методы (или переписать его, чтобы он был более объектно-ориентированным, на ваш выбор).

Проблема в том, что любая оболочка всегда делает предположения относительно варианта использования. Сказать, что обертка является правильным или неправильным, значит, чушь. Вот почему я всегда стараюсь избегать создания классов-оболочек. Но, по крайней мере, это не кажется явно неправильным.

1 голос
/ 01 января 2012

Вы задали довольно интересный вопрос. Как и во всех алгоритмах, ключ шифрования - это «секретный соус», поскольку, как только он станет известен публике, все остальное тоже. Итак, вы посмотрите на этот документ Google

безопасность

Кроме того, Google In-App Billing также рассуждает о безопасности, которая также проницательна

billing_best_practices

0 голосов
/ 11 февраля 2015

Я нашел хорошую реализацию здесь: http://nelenkov.blogspot.fr/2012/04/using-password-based-encryption-on.html и https://github.com/nelenkov/android-pbe Это также помогло в моих поисках достаточно хорошей реализации AES для Android

0 голосов
/ 31 декабря 2011

Используйте BouncyCastle Lightweight API. Он обеспечивает 256 AES с PBE и солью.
Вот пример кода, который может зашифровать / расшифровать файлы.

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