Каковы наилучшие методы использования AES-шифрования в Android?

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

Я знаю, что было много вопросов об шифровании 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/Encryption-method-and-reading-the-Dropbox-backup-td4344194.html

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

}

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

Ответ 1

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

Ключи и Хеши

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

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

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

Однако реализация в Java PBE имеет недостатки, если пароль содержит символы 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

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

Ответ 2

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

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

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

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

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

Ответ 3

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

security

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

billing_best_practices

Ответ 4

Использовать легкий API-интерфейс BouncyCastle. Он обеспечивает 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();
    }
}