Как стать автором
Обновить

Комментарии 78

НЛО прилетело и опубликовало эту надпись здесь
Вот тут: stackoverflow.com/ кнопочки всё таки лучше :-)
на аскдеве иконки не очень, т.к дизайном занимался не дизайнер. со временем это поправлю.
Я не про дизайн :-) Это была ирония :-)
и трава там зеленее, сразу не дошло про иронию :)
Ну и по приведённой ссылке facebook/vkontakte аутентификации таки нет.
Забыли про аудиторию mail.ru, у которой есть и почта, и блоги, и микроблоги, и соцсеть, и IM «не отходя от кассы», так что 99% можно счесть за художественное преувеличение :)

А вот логиниться на какой-то важный для меня сайт через соцсети я бы точно не стал, максимум посмотреть «что за фигня».
Да, это удобно, но только большинство этих пользователей подумают, что ваш сервис тырит пароль от стороннего сервиса.
Я думаю, что таким логином я ввожу лишнюю зависимость от независящих от меня сущностей. Потеряв контроль над мылом/аккаунтом соцсети я не смогу получить доступ к своему аккаунту на этом сервисе.
Да, скорей всего потеряете. Но большинство его потеряет всего лишь потому, что 'пароль от почты' = 'пароль от соц.сети' = 'пароль от всех-всех сервисов'.
задумка интересная, но посмотрев на эту кашу из кода я и дальше буду хранить ключ в бд, вы уж извините :)
1. «Такое решение не подойдёт если нужен одноразовый ключ.»
Т.е. задача из заголовка поста не решена. Потому как все три задачи из примера приемлемо решать только с одноразовым ключом

2. Если кто-то как-то случайно узнает алгоритм генерации ключа (.svn, например, забыли закрыть; или реализация такого ключа в опенсорсном продукте; итд итп) — тогда он получает безграничный доступ к системе от имени любого пользователя.

Так что решение, которое «Очень часто такое решение приходит первым. На самом деле его место в самом конце.» — всё таки предпочтительнее чем ваше.

Более того: «sha1($userId. «secret_key». time())» — не надо никаких плясок вокруг времени, ид пользователя и неизвестно зачем секретного ключа. «sha1(uniqid('', true))» + уникальный ключ на поле, и всё.

«малой кровью». Решение «в лоб» в реализации куда более простое и (внезапно) эффективное. Так что не совсем ясно что вы выбрали критерием «малости» «крови».
А ещё как вариант, генерацию ключиков перенести на плечи БД, а из приложения делать обычный запрос к БД.
Да не принципиально как и где, лишь бы ключик был уникальным.
1. Ключ не всегда нужен однооазовый. Например в рассылке для неактивных пользователей можно указать ключ, который работает сутки или неделю, чтобы они могли вернуться по ссылкам из письма несколько раз.
2. Скажите пожалуйста, каким образом Вы, зная алгоритм, создадите ключ для входа любого пользователя, не зная ключ шифрования? Никто же не собирается хранить эти ключи в файле с алгоритмом?
1. Можно. Это очень клёво делается и таблицей.
2. Никто? Вы слишком хорошо думаете о программистах. Если что-то где-то можно хранить неправильно, то это обязательно будут хранить неправильно. Более того, если украли сорсы — то могли украсть и ключ, который так и так лежит где-то рядом.
Вы сами же храните ключ в файле с алгоритмом :-) Разве нет? :-)))
Я сюда вынес его только для публикации. А потеря исходников — проблема несколько серьёзнее.
И тем не менее — вы выдали «готовое» решение :-)

Потеря исходников не всегда такая проблема, как потеря данных. В случае огромной системы вы могли лишь потерять только систему аутентификации (или её часть). Что плохо, но не смертельно. И отдать при этом доступ ко всем данным находчивому злоумышленнику, что само по себе уже пострашнее будет.
Присоединюсь ко второму пункту, раскрытие алгоритма фатально для системы безопасности сайта, такое решение в серьезной работе применять нельзя.
И в БД эти ключи не так уж много место занимают, их можно регулярно подчищать.
AES действительно так легко подбирается?
При чём тут подбор. Вы сами же показали пример исходников с ключом. ОООООоооооочень много из неопытных разработчиков возьмут ваш код, без изменений.

Не надо ёрничать — вы ведь всё прекрасно понимаете.
Хорошо, я укажу в статье, что эти ключи надо убрать подальше.
Лучше совсем убрать :)

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

Ключ можно в таком решении генерировать на основе данных о железе (MAC там, lspci).
обновили железо и все ссылки сломались х)
Да, это правда, но я не каждый день меняю железо и эта проблема мне представляется гораздо менее страшной, чем потеря кода ⇒ полный доступ ко всем учеткам.
а потеря информации о железе на коллокейшене не страшно? х)
ну это чревато при миграции…
Да ничем это не чревато. После миграции при попытках пройти по неправильной ссылке — нужно показывать сообщение «ваша ссылка неверна или устарела, мы тут винт пивом залили, простите нас, запросите ссылку снова _вот тут_».
Если мы не про Facebook — обиженных пользователей будет три с половиной человека. Если про Facebook — у предложенного метода миллион гораздо более узких мест.
Всё таки, дабы прояснить окончательно (потому как этого в статье нет).

Зачем делать что-то сложнее чем это можно сделать (случайный токен в персистентном хранилище, которое у вас и так уже есть)?

Ваше решение чем-нибудь (кроме «просто ещё одно») лучше хранения случайных токенов?
Раскрытие алгоритма генерации одноразового ключа тоже может плохо сказаться на безопасности.
www.xakep.ru/post/46797/default.asp
Первое предложение: «Недавно обнаруженная известным IT Security специалистом Стефаном Эссером уязвимость в интерпретаторе PHP теоретически может затронуть миллионы веб-сайтов, на которых используется PHP
Блин, парсер глюкнул
Первое предложение «Недавно обнаруженная известным IT Security специалистом Стефаном Эссером уязвимость в интерпретаторе PHP теоретически может затронуть миллионы веб-сайтов, на которых используется PHP<=5.2.5»

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

Во-вторых, 5.2.5 — очень старая версия, думаю, она уже мало где стоит.
ух, мне бы ваш оптимизм :(
ЗЫ. поддерживаю систему на PHP 4.3 :(
а что мешает перейти на более новую версию?
мне ничего, но надо переводить/настраивать/тестить и т.д.
то есть по принципу «работает и ладно»
Не понимаю чем плохо работать без базы, все равно ведь для контента она нужна. Для защиты от брутфорса достаточно небольшой фильтр повесить на уровне reverse proxy.
И еще имхо лучше просто одноразовый случайный uid использовать — это защищает от раскрытия кода.
А не лишняя ли это абстракция? Если юзер приходит к Вам что бы активировать аккаунт, то в URL-е уже содержится вся необходимая информация. Вы же в любом случае ищите в таблице users секретный ключик для активации.
Достаточно лиш в конце скрипта написать
$_SESSION['user'] = $user;
И не надо никаких лишних параметров в URL.
Сессии? Омг. А что если два человека на одной тачке зарегистрируются, не активировав сразу? А что если сессионную куку потеряли уже (выключили свет, bsod, просто закрыли браузер), не дожидаясь подтверждения.
Человек защищает подход хранения ключа в бд. А узнать айди по ключу в бд сможет даже школьник.
по поводу стиля написания кода.
чтобы избежать вложенности if'ов:

у вас:

if (strlen($info) == 18) {

}
else
{
return false;
}

я бы сделал так:

if (strlen($info) != 18) {
return false;
}



и так далее.
Всё таки, дабы прояснить окончательно (потому как этого в статье нет).

Зачем делать что-то сложнее чем это можно сделать (случайный токен в персистентном хранилище, которое у вас и так уже есть)?

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

Классический случай с БД.
1. Добавили код для генерации/проверки ключа
2. Добавили поле(я)/таблицу в схему
3. Накатили миграцию на тестовый сервер
4. Проверили работу
5. Перенесли всё на продакшн

В данном случае:
1. Добавили код для генерации/проверки ключа
2. Проверили работу
3. Перенесли всё на продакшн

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

Накатывание миграций (если они есть в проекте) — уже решённая задача. Вычёркиваем пункты два и три. Вписываем «написание миграции». В общем и целом — это создание одной таблицы с несколькими полями. Алгоритмически очень простая задача.

Теперь сравним пункты 1: реализация решения с рандомными токенами в базе гораздо более простая. Гораздо более.

Генерация токена: md5(uniqid('', true)), добавление в базу — примитивный INSERT
Проверка — примитивный селект.

Итого — гораздо меньше мест сделать ошибку.
А знаете ли вы, что вы практически переизобрели Kerberos-тикет? :)

На таком же походе основана, к примеру, безопасность доменов Windows.

Вообще, респект за статью. Идея известна очень давно, но её применение именно в вебе я пока что встречал только в PhpMyAdmin.
Интересно, а что на хабре используется… Хэш для поиска в таблице или какой-то вариант шифрования пароля.
По-моему, проще было бы генерировать ключ как sha1($user_id . microtime() . rand(0, 10000)) и не писать такой огромный алгоритм шифрования \ расшифрования, а такие вещи как его назначение и срок хранения хранить в других полях БД.
Как вариант, можно завести всего лишь одно целочисленное поле в БД, куда записывать уникальное рэндомное число (unique_rand_id) — при запросе восстановления пароля/подтверждения пароля.
Сбрасывать его в 0, когда такая процедура пройдена. И, например, делать уникальную ссылку по такому алгоритму: md5(unique_rand_id % $user_id + 'секретный_ключ_для_сайта') — взятие остатка от числа нужно, чтоб наверняка защититься от одинаковости unique_rand_id у двух различных пользователей; секретный_ключ_для_сайта — чтобы не отбрутфорсить полученный хэш.
Проверять, соответственно, такой хэш полученный от пользователя в URL'e и сгенерированный в скрипте во время этой процедуры.
Главное, не забыть сделать одно ОЧЕНЬ важное условие:
if(unique_rand_id == '0') { 
    exit('Восстановление/проверка мыла не было запрошено!'); 
}


По-моему, не самое простое и не самое сложное для реализации. Это что-то среднее между обычным подходом и описанным в статье. Плюс этого решения в том, что не надо хранить в БД много данных. Хотя в этом решении и минусы тоже есть :)
>>Например установка в memchache флага проверки такого ключа в сочетании с ограниченным временем действия ключа дадут нужный результат в большинстве случаев.

Деплоймент становиться сложнее!
+1 строчка в Deployment plan
Зачем тогда в ключе делать лимиты, если сразу можно всё хранить в memcached?

Более того, зачем тогда применять 30 строк шифрования, если в мемкэш можно положить пару случайный_токен (ключ) => user_id (значение)?
service memcached restart
ой, а где же ключики?
Прочитайте, пожалуйста, концовку статьи и комментарий, на который я отвечал.

Люди предлагают использовать memcached создания одноразовых ключей, поверх предложенного алгоритма.

Так что рестарт мемкэша и их вариант тоже испортит.

Посему мой коммент был, а если рестарт портит и там, и там — то зачем вообще много кода и сложных алгоритмов.
Ситуация 1:
В memcache попадает только флаг об использовании ключа, который нам надо продержать там сутки.
Если флаг оттуда пропадёт — страшного ничего случиться не должно. Если человек второй раз пройдёт по ссылке активации аккаунта или восстановления пароля пострадать никто не должен. Если есть такая опасность это либо неправильная архитектура либо действительно тот небольшой процент случаев, когда такое решение не подходит.

Ситуация 2:
В memcache лежит сам ключ. При сбросе этого ключа оттуда — он становиться не валидным. Пользователь не сможет зайти.

При этом надо принимать во внимание вероятность наступления таких событий, как потеря данных из memcache.
Спасибо, взял на заметку!

А то, блин, сейчас у большинства программистов вся криптография одними md5 и sha хешами ограничивается.
Хотелось бы заметить, что плюсов метода автор так и не назвал, а вот минусов было озвучено более чем достаточно :-) Так что я бы на вашем месте насчёт заметок не очень бы торопился.
Моё мнение: если можно решить проблему без обращения к БД и без лишних таблиц и полей в БД — её нужно решить без БД. Это моё мнение. Оно не обязательно должно совпадать с Вашим.
Когда нагрузка и количество записей в таблицах уже не дают так просто выполнить ALTER TABLE и добавить ещё один индекс — этот вопрос становится намного острее.
Зачем увеличивать объём БД, которую для максимальной производительности нужно стараться держать в ОЗУ? Это только усложнит работу.
БД как правило только увеличивается и замедляется по мере роста проекта. И не всегда она рассчитана изначально на шардинг и прочие методы оптимизации.
Поэтому я считаю, что лишних данных в БД нужно избегать.
К сожелению, библиотека Mcrypt доступна не на всех хостингах (если у вас не свой впс\вдс).
Решение простое, но существуют и намного попроще, которые ограничиваются только ф-циями md5\sha1
Реализацию алгоритма шифрования можно взять из других источников. Я показал метод. Как его использовать — дело каждого.
чем плохой такой простой алгоритм:

<?php
    // получаем с базы $user_id, $user_name
    [...]
    $s = "AGH@D&B!N(M!!()!@A!@A";
    $c = md5($user_id . $s . $user_name . $s . md5($user_id . $s . $user_name) );
    $c = md5($c . md5($s));
    $c = substr($c010);
    [...]
    if (@$_GET['c'== $c)
    {
        // true
    }
    else
    {
        // false
    }
?>


линк будет вида "/approve.php?user=itspoma&c=965b2275e4" — короткий совсем.
тем более можна его прератить в такой: /approve.php?itspoma=965b2275e4
и что главное — без использования сторонних библиотек
невозможно ограничить срок действия как минимум.
// ЗАЧЕМ? Ну зачем делать столько раз md5 и столько раз подмешивать $s ?????
срок действия можна ограничить например так:
/approve.php?itspoma=965b2275e4&t=123456789
где переменная «t» — это конечная дата в unixtime,
а в хеш («c») добавить еще $t, типа:
$t = intval($_GET['t']);
$c = md5($c . md5($s . $t));
Ух, извиняюсь насчет невозможности ограничить/добавить еще какие то данные не сообразил.

Применительно к авторизационным токенам ваш метод, пожалуй, несколько проще

$secret="AGH@D&B!N(M!!()!@A!@A";
$token=md5($secret+$data);

против
$sercet="AGH@D&B!N(M!!()!@A!@A";
$iv_size = mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_ECB);
$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);
$token = mcrypt_encrypt(MCRYPT_RIJNDAEL_256,$secret, $data, MCRYPT_MODE_OFB, $iv);


Разница в том что второй, хотя и требует наличия mcrypt, но позволяет не показывать открыто данные, которые вы передаете в URL-е (типа t=123456789 и пр). Хотя в данном конкретном случае это в общем то и не важно.
Ну там все равно ключ симметричный, так что можно и
$token = $data XOR md5($secret);

Чтобы не показывать открыто данные, не требовать наличия mcrypt на хостинге и чуть меньше грузить процессор.

Главный минус всех этих методов — невозможность токен сделать одноразовым. И боюсь, без сохранения данных на сервере эту проблему не побороть никак.
Похоже что-то похожее :) используется на хабре для ответа в топик по уведомлению на e-mail. А вот как таким способом сделать одноразовость ссылки, для например, восстановления пароля, без хранения лишних сущностей в БД я не придумал :( Была идея завязываться на хэш пароля — так пароль юзер может ввести тот же, что и был.
А чьи user_id, user_name получаем?
user_name = itspoma
user_id получаем с бд, в коментах написано же
За "@$_GET['c']" уже можно давать по рукам.
я показал всего лиш алгоритм, за безопасность внедряемого кода будет отвечать программист.

да и $_GET['c'] в этом примере используется всего один раз, для простой проверки. так что нету смысла его слешировать и проверять.
да уже давно имного где применяется такой подход. в УРЛ помещается 2Кб (ИЕ, по-крайней мере. другие еще больше). Соответственно, все что влезет в эти рамки после AES+base64, можно запихивать. Для регистрации новых пользователей с проверкой почты — самое оно. Обычно минимальной информации достаточно для 2Кб. Соответственно, если был левый адрес, то значит и нет мусора в базе, а если нормальный, то в ссылке будет вся та информация, что была при регистрации. В любом случае никто эти ссылки не диктует по телефону, а просто жмут на нее. Туда же можно запихивать и всякие таймауты и прочие данные для последующей проверки на валидность ссылки.
т.е. по-сути можно просто ограничиться простым AES с 256-битным ключом и выбрать нормальный «пароль».
Что-то типа того:

$url = base64_encode(AES::crypt(serialize($arr), «супер сложный пароль», 256));

ну и в обратку:

$arr= unserialize(base64_decode(AES::decrypt($url, «супер сложный пароль, 256)));

p.s.: это только пример, но суть примерно такая :)
точнее: $arr= unserialize(AES::decrypt(base64_decode($url), «супер сложный пароль, 256)));
Идея интересная, но немного поразмыслив, понимаю почему мой организм её не хочет принять.

1) Увести 1 ключ проще, чем увести все уникальные идентификаторы, особенно если учесть, что уникальные ID, которые генерируются с элементом случайности и будут созданы в будущем, украсть из базы невозможно (т.к. их там ещё нет) а вот ключ меняется не каждую секунду.

2) Как уже писали выше — проще от шифрования не стало.
— Вам не нужна база данных? Помилуйте, как же вы без неё храните и идентифицируете юзеров? Если у Вас нету БД, то указанные задачи перед вашим сайтом вряд ли стоят.
— генерация случайного числа используется в обоих случаях
— быстрее работает? Вряд ли.
— Реализация занимает явно больше времени.
Так где выигрыш то?
Единственное для чего такой подход хорош, это когда надо переправить юзера с одного ресурса на другой. Оба сайта знают ключ, и потому понимают инфу друг от друга. Так например делают некоторые биллинги.

А зачем такой гемморой?
Если нужно авторизовать 1 раз, то так:
md5($lastLoginTimeFromDB, $login, md5($hashedPasswodFromDb+$salt))

Для периода конечно не получится, но зато дополнительных действий совершать не надо. Last логин и так обновляется каждый раз при авторизации. Ну и запаковать это в объект пользователя для логина по хэшу.
Хороший подход, единственное что меня смутило
1) Зачем нам нужно случайной число, если у нас уже используется время жизни? Оно гарантирует нам, что чуть чуть поменяв время хеш сильно изменится, т.е. подобрать хеш никак не выдет
2) Зачем хранить контрольную сумму, если опять же при небольшом повреждении хеша расшифровать его не удастся.

Спасибо за ответы )
1. Случайное число введено для большей рандомизации. Например, когда несколько ключей генерируются в 1 секунду. В принципе, согласен — итоговый ключ будет вполне надёжным и без него. Это дополнительная защита.
2. По контрольной сумме проверяется валидность ключа. Пришедшие параметры считаются верными при совпадении контрольной суммы. Это защита от случая, когда подставной ключ расшифровывается в мусорные данные внешне, внешне напоминающие подлинные хотя бы частично.
Спасибо.
Остается только один вопрос.
Сгенерировав 1000 таких ссылок и зная свой пароль и т.д. не сможет ли злоумышленник восстановить закрытый ключ и потом генерить ссылки
AES — криптостойкий алгоритм симметричного шифрования. Сейчас он является стандартом шифрования в США. Насколько я знаю — на данный момент его надёжность не скомпрометирована.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации