Обновление хэширования пароля без принудительного ввода нового пароля для существующих пользователей

32

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

Предположим «упрощенную» модель базы данных для пользователей, которая содержит:

  1. МНЕ БЫ
  2. Эл. адрес
  3. пароль

Как можно решить такое требование?


Мои нынешние мысли:

  • создать новый метод хеширования в соответствующем классе
  • обновить таблицу пользователей в базе данных для хранения дополнительного поля пароля
  • Как только пользователь успешно войдет в систему с использованием устаревшего хэша пароля, заполните второе поле пароля обновленным хэшем

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

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

Здесь я в основном ищу несколько советов по дизайну, так как мое текущее «решение» грязное, неполное, а что нет, но если для описания возможного решения требуется реальный код, не стесняйтесь использовать любой язык.

Willem
источник
4
Почему вы не сможете отличить набор от неустановленного вторичного хэша? Просто сделайте столбец базы данных обнуляемым и проверьте на ноль.
Килиан Фот
6
В качестве нового столбца используйте хеш-тип вместо поля для нового хеша. Когда вы обновите хеш, измените поле типа. Таким образом, когда это произойдет снова в будущем, вы уже будете иметь возможность заключать сделки, и у вас нет шансов удержать старый (предположительно менее безопасный) хеш.
Майкл Кохне
1
Я думаю, что вы на правильном пути. Через 6 месяцев или через год вы можете заставить остальных пользователей сменить пароль. Год спустя или что-то еще, вы можете избавиться от старого поля.
ГленПетерсон
3
Почему бы просто не хешировать старый хеш?
Сиюань Рен
потому что вы хотите, чтобы пароль хэшировался (а не старый хеш), чтобы его не хэширование (то есть сравнение его с хешированным данным, заданным пользователем) давало вам ... пароль - а не другое значение, которое затем необходимо "дешифровать" по старому методу. Возможно, но не чище.
Майкл Даррант

Ответы:

25

Я бы предложил добавить новое поле "hash_method", возможно, с 1 для обозначения старого метода и 2 для обозначения нового метода.

Разумно говоря, если вы заботитесь о такого рода вещах и ваше приложение является относительно долгоживущим (что, по-видимому, уже есть), это, вероятно, произойдет снова, поскольку криптография и информационная безопасность - это развивающаяся, довольно непредсказуемая область. Было время, когда простой прогон через MD5 был стандартным, если хеширование вообще использовалось! Тогда можно подумать, что они должны использовать SHA1, и теперь есть соление, глобальная соль + индивидуальная случайная соль, SHA3, различные методы генерации случайных чисел с криптографией ... это не просто остановит, так что вы можете хорошо это исправить в расширяемом, повторяемом виде.

Итак, допустим, теперь у вас есть что-то вроде (в псевдо-javascript для простоты, я надеюсь):

var user = getUserByID(id);
var tryPassword = hashPassword(getInputPassword());


if (user.getPasswordHash() == tryPassword)
{
    // Authenticated!
}

function hashPassword(clearPassword)
{
    // TODO: Learn what "hash" means
    return clearPassword + "H@$I-I";
}

Теперь, понимая, что есть лучший метод, вам просто нужно провести небольшой рефакторинг:

var user = getUserByID(id);
var tryPassword = hashPassword(getInputPassword(), user.getHashingMethod());

if (user.getPasswordHash() == tryPassword)
{
    // Authenticated!
}

function hashPassword(clearPassword, hashMethod)
{
    // Note: Hash doesn't mean what we thought it did. Oops...

    var hash;
    if (hashMethod == 1)
    {
        hash = clearPassword + "H@$I-I";
    }
    else if (hashMethod == 2)
    {
        // Totally gonna get it right this time.
        hash = SuperMethodTheNSASaidWasAwesome(clearPassword);
    }
    return hash;
}

Никакие секретные агенты или программисты не пострадали в производстве этого ответа.

BrianH
источник
+1 Это кажется довольно разумным абстрагированием, спасибо. Хотя я могу в конечном итоге переместить поля хеш-функции и хеш-метода в отдельную таблицу, когда я реализую это.
Виллем
1
Проблема этого метода заключается в том, что для учетных записей не выполняется вход в систему, вы не можете обновить хэши. По моему опыту, большинство пользователей не будут входить в систему годами, если вообще не удалят неактивных пользователей. Ваша абстракция тоже не работает, так как она не учитывает соли. Стандартная абстракция имеет две функции: одну для проверки и одну для создания.
CodesInChaos
За последние 15 лет хеширование паролей практически не развивалось. Bcrypt был опубликован в 1999 году и до сих пор является одним из рекомендованных хэшей. С тех пор произошло только одно значительное улучшение: последовательные жесткие хэши памяти, впервые появившиеся в скрипте.
CodesInChaos
Это делает старые хеши уязвимыми (например, если кто-то украл базу данных). Хэширование каждого старого хэша с помощью нового более безопасного метода в некоторой степени смягчает это (вам все еще понадобится тип хэша, чтобы различать newHash(oldHash, salt)илиnewHash(password, salt)
dbkk
42

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

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

hash = H2(hash)  # where hash was previously equal to H1(p) 

После этого преобразования вы все еще можете выполнять проверку пароля; вам просто нужно вычислить H2(H1(p'))вместо предыдущего H1(p')всякий раз, когда пользователь пытается войти с паролем p'.

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

hash = H2(p)

Вам также понадобится дополнительный столбец, чтобы запомнить, какой хеш вы храните: пароль или старый хеш. В неудачном случае утечки базы данных этот столбец не должен облегчать работу взломщика; независимо от его значения, злоумышленнику все равно придется изменить H2алгоритм безопасности , а не старый H1.

Xion
источник
3
Гигантский +1: H2 (H1 (p)) обычно так же безопасен, как и непосредственное использование H2 (p), даже если H1 ужасен, потому что (1) соль и растяжение заботятся H2, и (2) проблемы с "битыми" хешами, такими как MD5, не влияют на хеширование паролей. Связанный: crypto.stackexchange.com/questions/2945/...
orip
Интересно, я бы подумал, что хэширование старого хеша создаст какую-то «проблему» безопасности. Кажется, я ошибся.
Виллем
4
если H1 действительно ужасен, это не будет безопасно. Быть ужасным в этом контексте - это в основном потеря энтропии от ввода. Даже MD4 и MD5 почти идеальны в этом отношении, так что этот подход небезопасен только для некоторых хэшей или хэшей, созданных в домашних условиях, с очень коротким выводом (менее 80 бит).
CodesInChaos
1
В этом случае, вероятно, единственный вариант - признать, что вы сильно облажались и просто сбросили пароль для всех пользователей. Это меньше о миграции на этом этапе и больше о контроле за ущербом.
Сион,
8

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

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

  1. Удалите учетные записи пользователей, которые не проходят аутентификацию слишком долго. Нет ничего плохого в удалении аккаунта пользователя, который не заходил на сайт в течение трех лет.

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

    Будьте осторожны: если у вас есть опция без спама, пользователи могут проверить ваш сайт, не отправляйте электронное письмо пользователям, которые его проверили.

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

Арсений Мурзенко
источник
1

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

Если методы имеют разную длину хеш-функции и длина хеш-функции каждого метода постоянна (скажем, при переходе от md5 к hmac-sha1), вы можете отличить метод от длины хеш-функции. Если они имеют одинаковую длину, вы можете сначала вычислить хеш, используя новый метод, а затем старый метод, если первый тест не пройден. Если у вас есть поле (не показано), в котором указано, когда пользователь последний раз обновлялся / создавался, вы можете использовать его в качестве индикатора того, какой метод использовать.

Горизонт событий
источник
1

Я сделал что-то подобное, и есть способ узнать, является ли это новым хешем, и сделать его еще более безопасным. Я полагаю, что безопаснее - это хорошо для вас, если вы нашли время обновить хэш ;-)

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

Таким образом, если мой пароль «pass123», а случайная соль - 3333, мой новый пароль будет хешем «pass1233333».

Если у пользователя есть соль, вы знаете, что это новый хеш. Если соль пользователя равна нулю, вы знаете, что это старый хеш.

Бен
источник
0

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

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

Флориан Зима
источник
1
Один компромисс , который я сделал в прошлом, чтобы сохранить старые пароли в течение периода времени, перефразируя их как войти люди в, но после того, что прошло время вы затем вызвать сброс пароля на тех , кто не войти в течение то время. Таким образом, у вас есть период, когда у вас все еще есть менее безопасные пароли, но он ограничен по времени, и вы также минимизируете сбои для пользователей, которые регулярно входят в систему.
Шон Бертон,