Поставщик SecureRandom "Crypto" недоступен в Android N для детерминированной генерации ключа

Пользователи могут приобрести "Pro" версию моего приложения. Когда они это делают, я храню и проверяю их покупку следующим образом.

  • Объедините UUID пользователя и другую уникальную строку.
  • Результирующая строка затем шифруется с использованием статического начального числа. Я делаю это используя SecureRandom.getInstance("SHA1PRNG", "Crypto") - Это проблема!
  • В результате зашифрованная строка является "кодом разблокировки".
  • Поэтому я всегда знаю ожидаемое уникальное значение кода разблокировки для пользователя.
  • Когда пользователь покупает "Pro", я сохраняю "код разблокировки" в базе данных.
  • Я проверяю, есть ли у пользователя "Pro", проверяя, соответствует ли сохраненный "код разблокировки" в базе данных ожидаемому коду на основе его уникальной информации.

Итак, не самая лучшая система, но все достаточно запутано для моего скромного приложения.

Проблема в том, что SecureRandom.getInstance("SHA1PRNG", "Crypto") терпит неудачу на N, потому что "Crypto" не поддерживается. Я узнал, что полагаться на определенных провайдеров - это плохая практика, и Crypto не поддерживается на N. К сожалению.

Так что у меня есть проблема: я полагаюсь на шифрование пары "значение-семя", чтобы всегда иметь один и тот же вывод. Android N не поддерживает провайдера шифрования, который я использую, поэтому я не знаю, как обеспечить, чтобы вывод шифрования был таким же на N, как и на других устройствах.

Мои вопросы:

  1. Можно ли включить "Crypto" в мой APK, чтобы он всегда был доступен?
  2. Можно ли в противном случае обеспечить такой же вывод при шифровании пары начальных значений в Android N?

Мой код:

public static String encrypt(String seed, String cleartext) throws Exception {
    byte[] rawKey = getRawKey(seed.getBytes(), seed);
    byte[] result = encrypt(rawKey, cleartext.getBytes());
    return toHex(result); // "unlock code" which must always be the same for the same seed and clearText accross android versions
}

private static byte[] getRawKey(byte[] seed, String seedStr) throws Exception {
    SecureRandom sr;
    sr = SecureRandom.getInstance("SHA1PRNG", "Crypto");  // what used to work
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    sr.setSeed(seed);
    kgen.init(128, sr); 
    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;
}

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

3 ответа

Я недавно обсудил это с командой безопасности Android.

В Android N SHA1PRNG был удален, потому что у нас нет его безопасной реализации. В частности, звонит .setSeed(long) перед запросом вывода из PRNG заменяет всю энтропию в экземпляре SecureRandom.

Это поведение долгое время называлось ошибкой безопасности (читай: часто вызывает незначительные ошибки в приложениях), поэтому мы решили не копировать ее, когда был заменен поставщик SecureRandom.

Если вам нужен PRNG, просто используйте new SecureRandom(),

Тем не менее... SecureRandom () не предназначен для использования в качестве функции выведения ключей, как вы сделали в своем примере. Пожалуйста, не делай этого! Вместо этого используйте алгоритм, такой как PBKDF2, доступный через SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"),

Мы предупреждали разработчиков об этом некоторое время. Пожалуйста, смотрите эти сообщения:

ЕСЛИ ВАМ ДЕЙСТВИТЕЛЬНО НУЖЕН SHA1PRNG, ДАЖЕ ПОСЛЕ ВСЕГО ЭТОГО... то обходной путь - скопировать реализацию из исходного кода Android, как @ artjom-b, упомянутый в его ответе.

Но, пожалуйста, делайте это только в том случае, если вам нужна совместимость при миграции на PBKDF2 или аналогичную версию.

Использование PRNG, такого как SecureRandom, для детерминированного получения данных, как правило, является плохой идеей, поскольку существует история критических изменений. Всегда полезно использовать конкретную реализацию и включить ее в свое приложение. Можно просто скопировать код реализации в вашем случае.

SecureRandom.getInstance("SHA1PRNG", "Crypto"); ищет провайдера "Crypto", который org.apache.harmony.security.provider.crypto.CryptoProvider в Android 5.1.1. Перенаправляет на org.apache.harmony.security.provider.crypto.SHA1PRNG_SecureRandomImpl как фактическая реализация. Вы можете легко скопировать код в свой проект в другом пакете и убедиться, что он соответствует лицензии на код.

Тогда вы можете использовать это так:

sr = new SecureRandom(new your.pkg.SHA1PRNG_SecureRandomImpl(), null);

Второй аргумент провайдера не используется в соответствии с кодом, но вы можете создать фиктивного провайдера.


Правильный способ генерации ключа из некоторого начального числа - использовать функцию вывода ключа (KDF). Если seed подобен паролю, тогда PBKDF2 является хорошим KDF, когда указывается много итераций. Если seed является ключоподобным, то рекомендуется использовать KBKDF, как HKDF.

Я добавил один класс для CryptoProvider, который вы можете заменить SecureRandom.getInstance("SHA1PRNG", "Crypto"); в SecureRandom.getInstance ("SHA1PRNG", новый CryptoProvider());

Вы можете обратиться по ссылке для решения, это работает для меня;

Безопасность "Крипто" провайдера устарела в Android N

Другие вопросы по тегам