Статьи

История Android, криптографии и искалеченных 3DES


Асимметричное и симметричное шифрование, различные алгоритмы (AES / DES), блочные / потоковые шифры, режимы работы — все эти термины сами по себе достаточно сложны, за исключением их конкретной реализации в выбранной вами среде программирования. Обычно вам нужны сильные математические навыки, чтобы пройти через все крошечные детали, которые имеют значение в криптографии. Что еще хуже, атаки криптоанализа постоянно совершенствуются, поэтому вам также нужно быть в курсе событий.

К счастью, вам не нужно столько всего, чтобы просто
использовать его. В конце концов, вы не изобретаете новый алгоритм шифрования домашнего приготовления (если вы это сделаете —
немедленно остановитесь !), Поэтому все, что вам нужно сделать, это RTFM.

Но этого достаточно сложно, потому что в Интернете полно примеров кода, которые просто неверны. Это всегда хорошая идея сделать некоторый обзор кода для связанных с криптографией частей вашего проекта. В качестве примера рассмотрим проект Android
Remote Notifier — в нем было несколько интересных уязвимостей, которые значительно ослабили его шифр. Автор учел мои комментарии и исправил
проблемы , поэтому считаю это «ответственным раскрытием»;).

Краткое введение

Удаленное оповещение Android — это приложение для телефонов Android, которое «отправляет уведомления на настольный компьютер, когда на устройстве Android происходят определенные события, такие как звонок телефона, получение SMS или разрядка аккумулятора. Уведомления можно отправлять через Wi-Fi. Bluetooth или (в будущем) USB. » В телефоне есть приложение, которое транслирует уведомления, а на рабочем столе находится приемник, который их прослушивает и реагирует, например, отображает окно сообщения с текстом SMS. Это новый проект (бета-качества), но он уже привлекает внимание сообщества Android.

Уведомления содержат конфиденциальную информацию и могут передаваться по небезопасному каналу (например, транслироваться на номер 255.255.255.255 через текущее соединение WiFi). Это было не весело — в конце концов, простой сетевой анализатор мог показать все SMS-сообщения вашего коллеги, сидящего поблизости.

Нам нужно шифрование!

Из-за этой уязвимости несколько дней назад автор внедрил симметричное шифрование сообщений. Выбранный шифр был
Triple DES  (DES-EBE в сообществе Java), 112-битный
блочный шифр , работающий в
режиме
CBC .

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

В теории у нас все хорошо. Ключ никогда не передается и становится общим секретом, и был выбран надежный шифр. Любые атаки грубой силы против Triple DES в настоящее время занимают огромное количество времени.
Но дьявол кроется в деталях . Давайте посмотрим на (ныне устаревшую) реализацию схемы шифрования / дешифрования в Remote Notifier:

Вопрос заполнения

import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import javax.crypto.Cipher;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
 
/**
 * @author rdamazio
 * @see http://code.google.com/p/android-notifier/
 */
public class Encryption {
 
  public static int MAX_KEY_LENGTH = DESedeKeySpec.DES_EDE_KEY_LEN;
  private static String ENCRYPTION_KEY_TYPE = "DESede";
  private static String ENCRYPTION_ALGORITHM = "DESede/CBC/PKCS5Padding";
  private final SecretKeySpec keySpec;
 
  public Encryption(String passphrase) {
    byte[] key;
    try {
      // get bytes representation of the password
      key = passphrase.getBytes("UTF8");
    } catch (UnsupportedEncodingException e) {
      throw new IllegalArgumentException(e);
    }
 
    key = padKeyToLength(key, MAX_KEY_LENGTH);
    keySpec = new SecretKeySpec(key, ENCRYPTION_KEY_TYPE);
  }
 
  // !!! - see post below
  private byte[] padKeyToLength(byte[] key, int len) {
    byte[] newKey = new byte[len];
    System.arraycopy(key, 0, newKey, 0, Math.min(key.length, len));
    return newKey;
  }
 
  // standard stuff
  public byte[] encrypt(byte[] unencrypted) throws GeneralSecurityException {
    return doCipher(unencrypted, Cipher.ENCRYPT_MODE);
  }
 
  public byte[] decrypt(byte[] encrypted) throws GeneralSecurityException {
    return doCipher(encrypted, Cipher.DECRYPT_MODE);
  }
 
  private byte[] doCipher(byte[] original, int mode) throws GeneralSecurityException {
    Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM);
    // IV = 0 is yet another issue, we'll ignore it here
    IvParameterSpec iv = new IvParameterSpec(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 });
    cipher.init(mode, keySpec, iv);
    return cipher.doFinal(original);
  }
}

Класс шифрования инициализируется байтовым массивом — паролем, введенным пользователем (скорее всего, буквенно-цифровыми символами — например, «
mybaby69 »). Тройной DES требует 168-битного ключа. В Java это 192 бита (мы вернемся к этому позже), поэтому нам нужно как-то сгенерировать эти 192 бита (24 байта) на основе пароля пользователя, который, вероятно, короче.

Это цель функции
 padKeyToLength () . Он инициализирует массив из 24 байтов (
newKey ), заполненный
байтами
NUL, и вставляет заданный пароль (
ключ ) в начале. В нашем примере:

key        m  y  b  a  b  y  6  9
key hex    6d 79 62 61 62 79 36 39
newKey     m  y  b  a  b  y  6  9  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .
newKeyHex  6d 79 62 61 62 79 36 39 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Не будет выдано никаких исключений, так как ключ в порядке (длина 192 бита), шифрование и дешифрование пройдут успешно. Но если вы знаете внутреннюю работу Triple DES, вам должно быть страшно.

Мы все любим математику, не так ли?

Тройной DES (DES-EDE) использует алгоритм DES 3 раза (сначала
E ncrypts, затем
D ecrypts, чем
E ncrypts, отсюда и имя), каждый раз с другим ключом 56 бит (k1..k3). Из Википедии:


зашифрованный текст = E
K3 (D
K2 (E
K1 (открытый текст)))

Но откуда
взялись K1,2,3 ? Ну, в нашем случае
newKey = K1 || K2 || К3 . Первые 8 байтов будут
K1 , байты 8-16 станут
K2 , а
K3 — последние 8 байтов.

key1  6d 79 62 61 62 79 36 39
key2  00 00 00 00 00 00 00 00
key3  00 00 00 00 00 00 00 00

Чтобы 3DES оставался сильным, ключи должны быть разными! Почему? Давайте сделаем вычисление:


DES симметричен, поэтому 
E
K
(D
K
(a)) = a . Также в нашем примере
K2 = K3


Пусть
E
K1
(открытый текст) = x (простой DES с
K1 )


Пусть
D
K2
(x) = y 

E
K3
(y) = E
K2
(y) = E
K2
(D
K2
(x)) = x 

зашифрованный текст = x

Ключ 2 и ключ 3 взаимно уничтожают друг друга,
эффективно превращая очень сильный 3DES-шифр в DES (который все еще довольно силен для сценаристов, но может быть сломан в разумные сроки,
если у вас есть $$$ ). Если вы мне не верите,
я могу это доказать .

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

Если вас интересуют проблемы заполнения и как они могут ослабить безопасность, ознакомьтесь с
Padding Oracle — это здорово и легко читать.

Проверка четности хромает

Другая проблема с этим кодом связана исключительно с реализацией Java: 3DES требует 168 битов для ключа. Java хочет, чтобы вы вводили 8 байтов = 192 бита. Как выясняется, каждый байт состоит из 7 битов для ключа и 1 бита четности (
LSB ). Хотя это не имеет смысла для меня (кому понадобится проверка четности на этом уровне?), Это просто так. Это что-то меняет в нашем примере? Конечно ?

Как только мы буквально копируем предоставленный пользователем 8-битный байт-пароль в Java
newKey , последний бит каждого байта просто игнорируется (четность, которая никогда не проверяется в коде).

В результате сообщения, зашифрованные с помощью «
Rugged », в точности совпадают с сообщениями, зашифрованными с помощью «
Ruffed».«в качестве ключа (коды ASCII для«
f »и«
g »отличаются только последним битом) — и это также уменьшает силу шифра.

Как решить эту проблему?
Расширьте ключ , используя все 8 битов и добавляя четность по пути — или сделайте так, чтобы последние биты были не так важны для вас — хешируя пароль.

Финальные заметки

Криптографию сложно сделать правильно, но как только вы это сделаете, это того стоит. Если вы не знакомы с используемой схемой шифрования — читайте и учитесь. Много.

Как обычно — зайдите на github, чтобы проверить весь
код, перечисленный здесь , я также
провел тесты jUnit, доказывающие, что описанные уязвимости реальны. Текущая версия Android Remote Notifier изменила алгоритм на AES и использует хеши MD5, чтобы избежать проблем с заполнением / четностью клавиш, но я извлек уязвимую версию в github, чтобы вы могли сами поэкспериментировать.