В вашей схеме есть излишества, ведущие к потенциальным уязвимостям. Например, ваш клиент вычисляет md5(md5(secret | clientSalt) | challenge) — зачем, когда можно вычислять md5(secret | clientSalt | challenge)? В вашем случае, если каким-то образом утечет md5(secret | clientSalt) (с сервера, например, потому что сервер хранит именно md5(pass + salt)), появляется возможность использовать его повторно, а это критическая уязвимость.
Сервер генерирует challenge, используя ip — зачем? Соль должна быть случайна, а если соль случайна, её придется временно хранить. Если так, то добавление ip излишне, можно просто отправлять длинную случайную строку и хранить её на время проведения аутентификации.
В правильном случае сервер отправляет клиенту длинную случайную строку, которая никак не связана с любыми характеристиками клиента — challenge. Клиент вычисляет response = md5(salt | secret | challenge) и отправляет серверу response и salt, где последняя — это длинная случайная строка, которую генерирует сам клиент. Сервер, зная secret, salt и challenge повторяет вычисление у себя и либо отшивает клиента, либо аутентифицирует его.
Очевидный недостаток — пароли хранятся в открытом виде :), а ведь именно этого мы и пытались избежать. Зато это самый всамделишно-настоящий CHAP! :) Поэтому есть вариант лучше, вот его протокол:
1. Пользователь вводит некий пароль password.
2. Генератору ECDSA-ключей скармливается pbkdf2(password) — на выходе получаем secret и public.
3. Клиент говорит серверу — «Привет, я vasya@yandex.ru»
4. Сервер проверяет, есть ли у него в базе публичный ключ vasya@yandex.ru, и если да, то говорит клиенту — «Окей, держи длинную случайную строку challenge и некий последовательный счетчик seq, на который ты не можешь влиять и который растет монотонно, никогда не повторяясь».
5. Клиент вычисляет response = ecdsaSign(secret, challenge | seq | длинная случайная строка) и отправляет подписанное сообщение серверу.
6. Сервер отбрасывает соль, сверяет, что challenge совпадает с отправленным ранее, и проверяет публичным ключом, извлеченным из БД, что подпись верна.
7. Готово!
В этом случае сервер не знает ни пароля, ни его хэша, и пароль никогда не передается никуда даже во время регистрации. Наличие счетчика обеспечивает неуязвимость к replay-атакам даже в случае дублирующегося challenge. MITM при этом, правда, возможен, так что этот протокол всё равно нужно пускать поверх TLS, чтобы клиент был уверен, что говорит с сервером, а не с хакером.
Я насчитал 1339 учетки, в которых одновременно используются большие, маленькие латинские буквы, знаки !"#$%&'()*+,./:;<>?@[\]^_`{|}~ и цифры. Если убрать требование разного регистра, то 5902 учеток. Если убрать требование букв, то 8166. Если требовать только пунктуацию, то 11670 учеток.
Не соглашусь. Можно, допустим, принять, что «популярный пароль» — это тот, который использует более 3 человек. В таком случае первые три счастливчика заимеют себе пароль SuperXtra, остальным же скажут «Вы знаете, мы не в курсе, что там у вас за пароль, но в нашей базе говорится, что точно такой же пароль уже используют три человека. Поднапрягите фантазию, пожалуйста.» Даже если пользователь после этоо изменит пароль на SuperXtra1 или SuperXtraMARINKA, это уже значительно улушит ситуацию, по сравнению с сейчас, когда на пароль 123456 приходится более 39 тысяч учеток?
Что до требования длины, какой от неё толк, если в слитом файле есть вот такие перлы?
Кошмар, из миллиона паролей уникальных только 58.8%, все остальные — дубли. Можно ведь вести базу несолёных хэшей без привязки к аккаунту, просто для статистики, и говорить «не используйте этот пароль, он слишком популярен», в чем техническая сложность? Воровство такой базы ничего не даст, насколько я понимаю.
В этой задачке используется особая система типов, состоящая из численных (num) и из жирночисленных (bold) значений.
Жирночисленные значения дуальны — их запись вида 75 соответствует одновременно и числу 75 (числовая часть), и символу с ASCII-кодом 0x75 (строковая часть).
Операции над ними определены как-то так: (α — жирночисленное, β — числовое)
− := (α as bold, β as num) as bold −>
return α + (-β)
+ := (α as bold, β as num) as bold −> γ:num = α:num10 + β10 γ:str = concat(α:str, chr(γ:num10))
return γ
+= := (α as bold) as string −>
return concat(α:str, α:num10)
md5(md5(secret | clientSalt) | challenge)— зачем, когда можно вычислятьmd5(secret | clientSalt | challenge)? В вашем случае, если каким-то образом утечетmd5(secret | clientSalt)(с сервера, например, потому что сервер хранит именноmd5(pass + salt)), появляется возможность использовать его повторно, а это критическая уязвимость.Сервер генерирует
challenge, используя ip — зачем? Соль должна быть случайна, а если соль случайна, её придется временно хранить. Если так, то добавление ip излишне, можно просто отправлять длинную случайную строку и хранить её на время проведения аутентификации.В правильном случае сервер отправляет клиенту длинную случайную строку, которая никак не связана с любыми характеристиками клиента —
challenge. Клиент вычисляетresponse = md5(salt | secret | challenge)и отправляет серверуresponseиsalt, где последняя — это длинная случайная строка, которую генерирует сам клиент. Сервер, знаяsecret,saltиchallengeповторяет вычисление у себя и либо отшивает клиента, либо аутентифицирует его.Очевидный недостаток — пароли хранятся в открытом виде :), а ведь именно этого мы и пытались избежать. Зато это самый всамделишно-настоящий CHAP! :) Поэтому есть вариант лучше, вот его протокол:
1. Пользователь вводит некий пароль password.
2. Генератору ECDSA-ключей скармливается
pbkdf2(password)— на выходе получаемsecretиpublic.3. Клиент говорит серверу — «Привет, я
vasya@yandex.ru»4. Сервер проверяет, есть ли у него в базе публичный ключ
vasya@yandex.ru, и если да, то говорит клиенту — «Окей, держи длинную случайную строкуchallengeи некий последовательный счетчикseq, на который ты не можешь влиять и который растет монотонно, никогда не повторяясь».5. Клиент вычисляет
response = ecdsaSign(secret, challenge | seq | длинная случайная строка)и отправляет подписанное сообщение серверу.6. Сервер отбрасывает соль, сверяет, что challenge совпадает с отправленным ранее, и проверяет публичным ключом, извлеченным из БД, что подпись верна.
7. Готово!
В этом случае сервер не знает ни пароля, ни его хэша, и пароль никогда не передается никуда даже во время регистрации. Наличие счетчика обеспечивает неуязвимость к replay-атакам даже в случае дублирующегося challenge. MITM при этом, правда, возможен, так что этот протокол всё равно нужно пускать поверх TLS, чтобы клиент был уверен, что говорит с сервером, а не с хакером.
!"#$%&'()*+,./:;<>?@[\]^_`{|}~и цифры. Если убрать требование разного регистра, то 5902 учеток. Если убрать требование букв, то 8166. Если требовать только пунктуацию, то 11670 учеток.SuperXtra, остальным же скажут «Вы знаете, мы не в курсе, что там у вас за пароль, но в нашей базе говорится, что точно такой же пароль уже используют три человека. Поднапрягите фантазию, пожалуйста.» Даже если пользователь после этоо изменит пароль наSuperXtra1илиSuperXtraMARINKA, это уже значительно улушит ситуацию, по сравнению с сейчас, когда на пароль123456приходится более 39 тысяч учеток?Что до требования длины, какой от неё толк, если в слитом файле есть вот такие перлы?
Да, это пароли, состоящие только из звездочек. Или вот пароль
11111111111111111111, встречающийся 41 раз.Жирночисленные значения дуальны — их запись вида 75 соответствует одновременно и числу 75 (числовая часть), и символу с ASCII-кодом 0x75 (строковая часть).
Операции над ними определены как-то так: (α — жирночисленное, β — числовое)
− := (α as bold, β as num) as bold −>
return α + (-β)
+ := (α as bold, β as num) as bold −>
γ:num = α:num10 + β10
γ:str = concat(α:str, chr(γ:num10))
return γ
+= := (α as bold) as string −>
return concat(α:str, α:num10)
a += aa
75 += u117
48 += H72
Попробовал дублировать формулу, т.е. 0x75−7−5−6+12−11+1+0x75−7−5−6+12−11+1, на выходе ЪУОИФЙК в cp1251, и «зснхтий» в koi8-r. Я в задумчивости.