Доброго времени суток, хабрапараноик! Сегодня мы поговорим о немного необычном способе повышения безопасности, а именно замедлении хеширования паролей. Казалось бы, когда всё вокруг стараются оптимизировать, зачем что то замедлять?
Хотя бы затем, что даже в самой супер-пупер защищенной системе самым слабым звеном остается человек. А именно, его пароль.
Вы пытались когда нибудь взломать зашифрованный rar архив? И сколько паролей в секунду оно перебирало? 50-100-200? Даже на хорошем GPU, при использовании небезызвестного cRARk, скорость перебора всего около 2400 вариантов/сек. И это-то по сравнению с десятками (сотнями) миллионов паролей/сек для zip/md5/SHA1.
Под катом моя вольная интерпретация этого процесса.
Смысл всего действа сводится к следующему:
Да, чуть не забыл про всеми любимые rainbow tables, которые довольно успешно генерятся даже для систем, использующихсахар соль. Их генерация тоже станет если не бесполезной, то очень трудозатратной.
Я не говорю, что системы с солью плохие, просто их можно еще усилить.
И так, есть соль, есть пароль. Что дальше?
А дальше все довольно просто:
В Winrar (спасибо ему, кстати, за вдохновение), если мне не изменяет память, раундовой солью является номер итерации. Я же пошел немножко дальше.
Итак, код. Всё написано на java с использованием библиотеки BouncyCastle, но вдумчивому хабрапараноику не составит сложности перенести это (и может быть добавить своё) на любой язык программирования, полный по тьюрингу. К тому же BouncyCastle есть и для C#.
1. Я использую 2 алгоритма хеширования (SHA-256 и SHA-512), поэтому для начала небольшой интерфейсик, который поможет нам всё это дело более-менее универсализировать.
2. Пример реализации этого интерфейса для алгоритма SHA-256 (используется класс SHA256Digest из библиотеки BouncyCastle):
3. Самая вкуснятина. Её я объясню подробно.
Хотя бы затем, что даже в самой супер-пупер защищенной системе самым слабым звеном остается человек. А именно, его пароль.
Вы пытались когда нибудь взломать зашифрованный rar архив? И сколько паролей в секунду оно перебирало? 50-100-200? Даже на хорошем GPU, при использовании небезызвестного cRARk, скорость перебора всего около 2400 вариантов/сек. И это-то по сравнению с десятками (сотнями) миллионов паролей/сек для zip/md5/SHA1.
Под катом моя вольная интерпретация этого процесса.
Смысл всего действа сводится к следующему:
- Ключом шифрования важной является не пароль, а (медленный) хэш от него
- Юзеру ничего не стоит подождать секунду\пол секунды пока пароль прохешируется
- А вот homo brutforsus придется запастись терпением
Да, чуть не забыл про всеми любимые rainbow tables, которые довольно успешно генерятся даже для систем, использующих
Я не говорю, что системы с солью плохие, просто их можно еще усилить.
И так, есть соль, есть пароль. Что дальше?
А дальше все довольно просто:
- Конкатенируем соль и пароль
- Хэшируем, запоминаем результат
- while (много много раз) do{
- сгенерировать раундовую соль
- Сконкатенировать её и хэш
- Прохэшировать, запомнить результат}
В Winrar (спасибо ему, кстати, за вдохновение), если мне не изменяет память, раундовой солью является номер итерации. Я же пошел немножко дальше.
Итак, код. Всё написано на java с использованием библиотеки BouncyCastle, но вдумчивому хабрапараноику не составит сложности перенести это (и может быть добавить своё) на любой язык программирования, полный по тьюрингу. К тому же BouncyCastle есть и для C#.
1. Я использую 2 алгоритма хеширования (SHA-256 и SHA-512), поэтому для начала небольшой интерфейсик, который поможет нам всё это дело более-менее универсализировать.
public interface IDigest
{
public byte[] process(byte[] data);
public int getSize();
}
2. Пример реализации этого интерфейса для алгоритма SHA-256 (используется класс SHA256Digest из библиотеки BouncyCastle):
public class SHA256 implements IDigest
{
private SHA256Digest m_SHA256 = new SHA256Digest();
@Override
public byte[] process(byte[] data)
{
m_SHA256.reset();
m_SHA256.update(data, 0, data.length);
byte[] result = new byte[m_SHA256.getDigestSize()];
m_SHA256.doFinal(result, 0);
return result;
}
@Override
public int getSize()
{
return m_SHA256.getDigestSize();
}
}
3. Самая вкуснятина. Её я объясню подробно.
public class SlowHasher
{
private final static int BITS_IN_BYTE = 8;
private static final int[] s_primeIndices = new int[] { 7, 11, 17, 23, 31, 41, 47, 53, 61 };
/**
* this method hashes password 0x50000 times adding round salt each round
*
* param digest
* param password
* return
*/
public byte[] calculateSlowHash(IDigest digest, String password, byte[] salt)
{
int roundSaltSize = digest.getSize() / BITS_IN_BYTE;
byte[] bPasswd = password.getBytes();
byte[] toHash = new byte[bPasswd.length + salt.length];
/*
* composing array of bytes that will be hashed
*/
System.arraycopy(salt, 0, toHash, 0, salt.length);
System.arraycopy(bPasswd, 0, toHash, salt.length, bPasswd.length);
byte[] res = digest.process(toHash);
byte[] temp = new byte[res.length + roundSaltSize];
for (int i = 0; i < 0x50000; i++)
{
System.arraycopy(res, 0, temp, 0, res.length);
/***
* calculating salt
*/
for (int j = 0; j < roundSaltSize; j++)
{
int btmp = res[s_primeIndices[j]] & 0xFF;
for (int k = 1; k < BITS_IN_BYTE; k++)
{
btmp = ror((btmp + (res[ror(btmp, k) % res.length] & 0xFF)) % 256, BITS_IN_BYTE-k);
}
temp[res.length + j] = (byte)btmp;
}
res = digest.process(temp);
}
return res;
}
/**
* rotate bits right treating input as byte
* param value 0 <= value <=255
* param n number of bits to shift
* return
*/
public static int ror(int value, int n)
{
return ((value >> (n % BITS_IN_BYTE)) | ((value << (8 — (n % BITS_IN_BYTE))) & 0xFF));
}
}
Людям не знакомым с java скажу сразу, что не стоит пугаться многочисленных вставок& 0xFF
. Это всего лишь конвертация signed byte в unsigned путем преобразования его в int и применения такой вот битовой маски. А всё потому, что в java нет unsigned типов! Совсем! Ну да ладно, это не смертельно.
Вся красивость тут заключается в формировании раундовой соли, поэтому основное внимание будет уделяться ей.
Немного о переменных:
- res — массив из 32 байт для SHA-256 или 64 байт для SHA-512. В нем хранится результат хеширования
- roundSaltSize — размер раундовой соли. 4 байта для SHA-256 и 8 байт для SHA-512
- temp — массив из элементов размера массива res плюс размер раундовой соли roundSaltSize
- s_primeIndices — массив индексов элементов в массиве res, начиная с которых мы будем вычислять соответствующий байт раундовой соли. То есть, начинать вычисления первого байта раундовой соли для SHA-256 мы будем с res[7], второго с res[11] и т.д. Для SHA-512 будут задействованы все индексы
- btemp — байт, который после серии хитрых преобразований станет на своё место в раундовой соли
Еще одна ремарка прежде чем мы перейдем к разъяснениям формирования алгоритма раундовой соли. Весь алгоритм не является результатом каких либо научных исследований и тестов. Он создан для формирования значения, которое было бы зависимо от некоторых байтов результирующего хэша и помогло бы его замедлить и усложнить. Ну а теперь описание:
- В начале в массив toHash собираются основная соль и пароль
- Массив toHash хешируется и результат запоминается в массиве res. Всё, про основную соль и пароль забыли
- Запускаем длинный цикл в 0x50000 итераций
- Далее работаем с массивом temp, в который копируем массив res. В конце массива temp еще остается 4 или 8 байт для соли. Нам предстоит их заполнить
- Для каждого из байтов раундовой соли:
- Запоминаем в btmp элемент, который находится по одному из индексов (7, 11, 17… ) в зависимости от номера текущего байта (j)
- Затем 7(k) раз делаем следующее:
- Берем остаток от деления btmp, у которого биты сдвинуты вправо на k позиций, на длину массива res
- предыдущее число используем как индекс для массива res, вытаскиваем число по этому индексу
- btmp присваиваем это число, сложенное с btmp по модулю 256 и прокрученное вправо на 8-k бит
- Помещаем btmp на своё место после хэша в массиве temp
- Хэшируем temp
Вызов этого метода выглядит следующим образом:
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt); // generate random 16byte salt
byte[] hashedPassword = new SlowHasher().calculateSlowHash(new SHA256(), password, salt);
В результате получится хэш(хэш(хэш+ранудовая соль)+раундовая соль)… который можно использовать как 256 битный ключ шифрования важной информации для AES-256, например.
На моей машине (C2D 2.6) на генерацию одного хэша приходится около 0.25 секунды, что я считаю приемлемым для использования. в своих проектах. Можно увеличить число раундов и тогда время соответственно вырастет.
Если уважаемой публике будет интересно, то я могу рассказать и о других прикладных аспектах работы с библиотекой BouncyCastle как то симметричное\ассиметричное шифрование, генерация сертификатов и т.д.
UPD:
В комментах указали ссылку на доку, в которой такого рода схема принимает еще больший размах