Методы хранения паролей - Хранение длины пароля – почему и как

ОГЛАВЛЕНИЕ

Хранение длины пароля – почему и как

Почему надо хранить длину пароля?

Длина пароля нужна только при шифровке и расшифровке пароля с помощью блочного шифра. Многие блочные шифры имеют размер блока 64 бит – размер зашифрованных данных кратен 8 байтам. Шифрование 12 байтового пароля даст на выходе 16 байт. При расшифровке 16 байт результатом будет 16 байтовая строка с мусором в последних 4 байтах. Если хранится длина пароля, лишнее дополнение можно убрать при расшифровке пароля. Без его удаления любые примитивные попытки сравнить строки провалятся – 12 символьный пароль не совпадает с 16 символьным расшифрованным паролем. Попытка сравнить зашифрованные пароли для проверки на совпадение может провалиться, потому что лишние 4 байта дополнения могут отличаться от шифрования к шифрованию.

Хеширование не требует длины пароля – хеширование 12 символьного пароля с помощью SHA2 всегда дает на выходе 320 битов, и хеширование того же самого пароля всегда дает на выходе те же самые 320 битов.

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

Как можно хранить длину пароля?

Одно решение – хранить длину пароля в виде столбца в таблице. Недостаток этого подхода состоит в том, что если хакер получит доступ к данным, то значение длины пароля очень облегчает атаку перебором. Знание, что пароль содержит всего 5 символов, позволяет хакеру ограничить число попыток подбора пароля, необходимых для взлома пароля.

Решение получше – хранить длину пароля в составе зашифрованной строки. Длину пароля можно добавить к началу строки пароля (например, как первые два символа). При расшифровке пароля по первым двум символам восстанавливается длина, и пароль безопасно обрезается. Хранение длины, зашифрованной вместе с паролем, гарантирует, что никто не может иметь доступ к длине пароля без знания тайны, используемой для шифрования пароля.

Пример – Хранение длины и обрезание паролей

Хранение сообщения вместе с длиной:

// Кодируется длина как первые 4 байта
// Данные находятся в строке «сообщение»
byte[] length = new byte[4];
length[0] = (byte)(message.Length & 0xFF);
length[1] = (byte)((message.Length >> 8) & 0xFF);
length[2] = (byte)((message.Length >> 16) & 0xFF);
length[3] = (byte)((message.Length >> 24) & 0xFF);
csEncrypt.Write(length, 0, 4);
csEncrypt.Write(toEncrypt, 0, toEncrypt.Length);

Восстановление сообщения из двоичного массива:

// Хранилище для незашифрованного сообщения
byte[] fromEncrypt = new byte[encrypted.Length-4];

// Используется для преобразования массива байтов в строку
// с конкретной кодировкой
UTF8Encoding textConverter = new UTF8Encoding();

//Читаются данные из зашифрованного потока.
// Первые 4 байта – фактическая длина
// остальное - сообщение + дополнение
byte[] length = new byte[4];
// Читаются данные из расшифрованного потока
csDecrypt.Read(length, 0, 4);
csDecrypt.Read(fromEncrypt, 0, fromEncrypt.Length);
int len = (int)length[0] | (length[1] << 8) |
          (length[2] << 16) | (length[3] << 24);

//Массив байтов преобразуется обратно в строку.
// Строка обрезается до заданной длины, чтобы удалить
// дополнение
// textConverter по умолчанию - UTF8
return textConverter.GetString(fromEncrypt).Substring(0, len);

Соление – соль улучшает вкус всего

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

Простое соление – использование дополнительного куска данных

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

Пользователь: Bob
Пароль: Snake
Хешированный пароль: hash(“Snake”) -> k468dD8F
Пользователь: Eve
Пароль: Snake
Хешированный пароль: hash(“Snake”) -> k468dD8F

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

Если добавить логин в сочетание:

Пользователь: Bob
Пароль: Snake
Хешированный пароль: hash(“Bob.Snake”) -> 4Fgja93Q
Пользователь: Eve
Пароль: Snake
Хешированный пароль: hash(“Eve.Snake”) -> k468dD8F

Сейчас Боб и Ив имеют разные хеши паролей. Если хакер завладеет хранилищем паролей, теперь хакеру придется вычислить хеш каждого пароля конкретно для каждого пользователя. Хакеру придется заранее вычислить словарные хеши с префиксом “Bob.” для Боба и с префиксом “Eve.” для Ив – нелегкая задача.

Продвинутое соление – использование случайной информации

Использование случайной соли значительно повышает стойкость шифрования паролей и сильно затрудняет взлом перебором.

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

Например:

// Генерируется 6-байтовая соль
public static byte[] GenerateSALT()
{
  byte[] data = new byte[6];
  new System.Security.Cryptography.RNGCryptoServiceProvider().GetBytes(data);
  return data;
}

При первоначальном сохранении пароля для Боба:

1.    Вычислить случайное число (использовать надежный криптографический случайный генератор, такой как в System.Security.Cryptography).
2.    Добавить случайное число к строке пароля.
3.    Вычислить хеш или зашифровать полученную строку.
4.    Сохранить хеш или результат шифрования и случайное число в хранилище паролей.

При сравнении паролей следовать такому же алгоритму:

1.    Достать случайное число из хранилища паролей.
2.    Добавить случайное число к строке пароля.
3.    Вычислить хеш или зашифровать полученную строку.
4.    Сравнить результат с сохраненным хешем или зашифрованным паролем, совпадение означает совпадение паролей.

Использование случайного числа лучше, чем использование логина. Логины не очень случайны – они придерживаются очень строгих правил. Случайные числа (исходящие из надежного криптографического генератора случайных чисел) вносят больше случайности в полученный хеш или зашифрованные выходные данные. Заметьте, что если в качестве соли используется логин, то логин вообще нельзя менять. При изменении логина хеш станет недействительным.

Кодирование двоичных выходных данных в виде текста

Выходные данные из хеш-функций и алгоритмов шифрования двоичные. Чтобы хранить двоичные данные в виде текстовых строк, можно закодировать данные.

Две популярные схемы кодирования - UUENCODE (популярна в мире Unix) и Base64 (популярна везде). Base64 – схема кодирования, применяемая для преобразования двоичных вложений для отправки через электронную почту SMTP(простой протокол пересылки почты).

Пример – кодирование двоичного массива в строку

//Получить зашифрованный массив байтов.
byte[] encrypted = msEncrypt.ToArray();
// Преобразовать в строку Base64
string b64 = Convert.ToBase64String(encrypted);

Пример – раскодирование строки в двоичный массив

// Раскодировать Base64
// данные находятся в строке ‘b64’
byte[] encrypted = Convert.FromBase64String(b64);