Безопасная аутентификация на сайте без https

в 12:02, , рубрики: javascript, php, аутентификация, безопасность, Веб-разработка, метки: ,

Сегодня все ещё большинство сайтов работает по протоколу http, а не https. Это хуже в плане безопасности, но дешевле и проще в поддержке. Безопасность аутентификации на таких сайтах — проблема, так как чаще всего она реализуется простой отправкой формы c логином/паролем на сервер, где хэш пароля сверяется c хэшем, который соответствует указанному логину. Входя на такой сайт в открытой Wi-Fi сети или другом публичном месте любой кто умеет пользоваться google сможет без особых усилий перехватить ваш пароль. Некоторые идут чуть дальше, отправляя на сервер хэш пароля, но и это не на много лучше так как имея радужную таблицу (особенно для md5) можно попробовать найти пароль, имея не нулевой на успех, и каждый день вероятность успеха растет.

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

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

Аутентификация будет проходить в два этапа c использованием AJAX.

1 Этап

Подготовка, отправляем на сервер sha224 хэш логина, получаем случайный хэш. JavaScript пример:

$.ajax(
    base_url+'/api/System/user/login',
    {
        cache   : false,
        data    : {
            login : hash('sha224', login)
        },
        success : function(random_hash) {
            //Get random hash
        },
        error   : function() {
            //Error
        }
    }
);

hash() — это JavaScript обертка, которая делает то же самое, что и соответствующая php функция.

Случайный хэш генерируется на сервере. PHP пример:

$random_hash = hash('sha224', microtime(true));

Случайный хэш запоминается на сервере, и связывается c логином.

2 Этап

Собственно, аутентификация. JavaScript пример (вместе c первым):

**
 * Login into system
 *
 * @param {string} login
 * @param {string} password
 */
function login (login, password) {
    $.ajax(
        base_url+'/api/System/user/login',
        {
            cache   : false,
            data    : {
                login: hash('sha224', login)
            },
            success : function(random_hash) {
                if (random_hash.length == 56) {
                    $.ajax(
                        base_url+"/api/user/login",
                        {
                            cache   : false,
                            data    : {
                                login        : hash('sha224', login),
                                auth_hash    : hash(
                                    'sha512',
                                    hash('sha224', login)+hash('sha512', hash('sha512', password)+public_key)+navigator.userAgent+random_hash
                                )
                            },
                            success : function(result) {
                                if (result == 'reload') {
                                    location.reload();
                                } else {
                                    //Error
                                }
                            },
                            error   : function() {
                                //Error
                            }
                        }
                    );
                } else {
                    //Error
                }
            },
            error   : function() {
                //Error
            }
        }
    );
}

Как можно увидеть, для пароля используется sha512 хэш. public_key — это глобальная переменная, которая содержит строчку длиной в 56 знаков, и используется как соль. Эта переменная общедоступна, но благодаря тому, что используется относительно сложный алгоритм получения хэша sha512 (дважды) — генерация радужных таблиц для каждого отдельного сайта будет достаточно сложной, дорогой и длительной процедурой, чтобы это имело большой смысл.

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

Что тогда c регистрацией

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

Смена пароля/восстановление

Восстановление пароля в данной схеме тоже производится на почтовый ящик.

Смену пароля можно реализовать следующим образом:

  • со старого и нового паролей генерируем хэши как в примере выше:
    hash('sha512', hash('sha512', password)+public_key)

  • делаем посимвольный XOR обоих паролей, например, c помощью такой функции xor_string():
    String.prototype.replaceAt=function(index, symbol) {
        return this.substr(0, index)+symbol+this.substr(index+symbol.length);
    };
    function xor_string (string1, string2) {
        var    len1    = string1.length,
               len2    = string2.length;
        if (len2 > len1) {
            var tmp    = string1;
            string1    = string2;
            string2    = tmp;
            tmp        = len1;
            len1       = len2;
            len2       = tmp;
        }
        for (var i = 0; i < len1; ++i) {
            var pos    = i % len2;
            string1    = string1.replaceAt(i, String.fromCharCode(string1.charCodeAt(i) ^ string2.charCodeAt(pos)));
        }
        return string1;
    }

  • Отправляем на сервер два хэша. Один полученный в результате xor_string, второй от хэша текущего пароля и id текущей сессии пользователя. JavaScript пример:
    /**
     * Password changing
     *
     * @param {string} current_password
     * @param {string} new_password
     */
    function change_password (current_password, new_password) {
        if (!current_password) {
            //Error
            return;
        } else if (!new_password) {
           //Error
            return;
        } else if (current_password == new_password) {
            //Error
            return;
        }
        current_password    = hash('sha512', hash('sha512', current_password)+public_key);
        new_password        = hash('sha512', hash('sha512', new_password)+public_key);
        $.ajax(
            base_url+'/api/System/user/change_password',
            {
                cache   : false,
                data    : {
                    verify_hash     : hash('sha224', current_password+session_id),
                    new_password    : xor_string(current_password, new_password)
                },
                success : function(result) {
                    if (result == 'OK') {
                        //Success
                    } else {
                        //Error
                    }
                },
                error   : function() {
                    //Error
                }
            }
        );
    }

  • На сервере, зная что за пользователь, мы легко генерируем verify_hash. Если он совпадает c полученным от пользователя — текущий пароль был введен корректно, делаем посимвольный xor хэша строки текущего пароля пользователя, и new_password, полученного от пользователя. Таким образом мы восстанавливаем хэш нового пароля пользователя, которым и заменяем текущий. PHP пример аналога xor_string из JavaScript примера:
    function xor_string ($string1, $string2) {
        $len1    = mb_strlen($string1);
        $len2    = mb_strlen($string2);
        if ($len2 > $len1) {
            list($string1, $string2, $len1, $len2) = [$string2, $string1, $len2, $len1];
        }
        for ($i = 0; $i < $len1; ++$i) {
            $pos         = $i % $len2;
            $string1[$i] = chr(ord($string1[$i]) ^ ord($string2[$pos]));
        }
        return $string1;
    }
        

Дополнительные меры безопасности

Дополнительно рекомендую добавлять в запрос id текущей сессии пользователя, и при его отсутствии или не совпадении c текущей сессией обнулять данные POST запроса.

На этом всё

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

Автор: nazarpc

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js