??? ???? ?????? ??????

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

Давайте для начала по крестьянски, без умных слов, объясню, что именно я пытаюсь получить.

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

Что предлагается для этого сделать?

Берем rc4 и пароль для него, держим это на сервере, не раскрываем.

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

Делаем перестановку байт id в обратном порядке и конкатенируем с id. То есть, если было abc, то станет abccba.
Шифруем с помощью rc4, результат режем на пополам. Одну половину отдаем как session id (http-only кука), вторую — как токен (читаемая javascript-ом кука, при запросе дублируется в форме либо дополнительным хидером, подробно смотрите в предыдущей статье).

Когда получаем запрос, смотрим куки. Конкатенируем session_id и token, пропускаем через rc4, в одной половинке переворачиваем порядок следования байт и сравниваем полученные половинки. Если совпадают — делаем вывод что пользователь авторизован, иначе — нет (я не рассматриваю здесь проверку на csrf)

Давайте то же самое в подробностях.

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

Прогон данных через rc4 — это по сути операция xor нашего текста с сгенерированной псевдослучайной последовательностью.

Назовем последовательность, сгенерированную rc4 для первой половины нашего текста — X. Вторую половину, соответственно Y.

Тогда сессия у нас получается X xor id, а токен, соответственно Y xor id.
Если злоумышленник зарегистрируется у нас на сайте, то он может сделать xor полученной сессии и токена. (X xor id) xor (Y xor id), что дает зловреду X xor Y.

Владея этой информацией, при успешно проведенной XSS атаке и получив доступ к токену другого пользователя — (Y xor id), зловред может восстановить сессию, сделав (Y xor id) xor (X xor Y), что дает в итоге (X xor id) — что и является кукой сессии, к которой доступа у зловреда быть не должно, даже при успешном XSS.

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

Как теперь зловред может реализовать атаку?

Во первых, сессия и токен у нас теперь (X xor id) и (Y xor id'').
Что получит зловред, сделав xor токена и сессии? Ничего осмысленного, в выражении нет ни одного совпадающего значения, попадающего под сокращение — (X xor id) xor (Y xor id'').

Но! Зловред может сам перевернуть токен и восстановить порядок следования байт в id. То есть — если токен был (Y xor id''), то перевернув его, мы получим (Y'' xor id). Ок, что нам даст xor этого значения с сессией?
(X xor id) xor (Y'' xor id) == X xor Y''
Ммм. И что это дает? На мой взгляд, ничего! Сессию по токену не восстановить, X от Y не отличить. То же самое и в случае, если переворачивать не токен, а сессию.

Следующий момент, на который необходимо обратить внимание — это режим использования rc4. Фактически мы используем rc4 как генератор, берем от него (2 * длину id) байт и используем эту последовательность ДЛЯ ВСЕХ ПОЛЬЗОВАТЕЛЕЙ.

Но постойте, поправляют меня из зала. Ведь нельзя, категорически нельзя использовать одну и ту же последовательность несколько раз! Совершенно верно!

Но мы можем себе позволить эту вольность. Почему же?

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

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

Что же произойдет, если мы используем одну и туже последовательность дважды?
У нас есть два сообщения, которые надо зашифровать, Т1 и Т2, и сгенерированная псевдослучайная последовательность С.

Первый шифртекст будет равен Т1 xor C, второй шифртекст — Т2 xor C.
Если злоумышленник получит оба шифртекста, то он сделает следующее:

(Т1 xor C) xor (Т2 xor C), что дает ему Т1 xor Т2, то есть ксор двух НЕслучайных сообщений, который тоже, соответственно, неслучаен и относительно легко подвергается частотному анализу, что, при достаточной длине сообщений, позволяет их расшифровать.

Так получается, дважды код использовать нельзя? Совершенно верно, нельзя.

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

Потому что — у нас нет Неслучайных текстов, ведь мы шифруем этой последовательностью id, сгенерированный для посетителя, а он (псевдо) случаен, то есть один и тот же пользователь, авторизовавшись одну тысячу раз и получив одну тысячу пар token:session id, не может выделить из них ни шифрующую последовательность ни исходный id, ни подвергнуть их XOR частотному анализу.

Ок, давайте теперь рассмотрим код, реализующий предложенную схему (это концепт, для просмотра в браузере, а так код конечно для node/io.js). В ноде работает, проблем нет. Рассматриваю работу в браузере для простоты изложения.
портянка
<!DOCTYPE html>
<html lang="ru">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
<script>
    var cookie = {};
    var bin = function (str) {
        var map = '0123456789abcdef';
        var res = "";
        var i = 0;
        while (i < str.length) {
            res += String.fromCharCode(map.indexOf(str[i++]) * 16 + map.indexOf(str[i++]));
        };
        return res;
    };
    var hex = function (str) {
        var map = '0123456789abcdef';
        var res = "";
        for (var i = 0; i < str.length; i++) {
            res += map[Math.floor(str.charCodeAt(i) / 16)] + map[str.charCodeAt(i) % 16];
        }
        return res;
    };
    
    var id = "c2bb525124fe5bd853a01310fce9755fb2cf3f1022eaf46e7e9fabad27d6e3577a526931116899050c1bb089a7c5b8fddf70fcc5542f72bad8ad0c024835060d";

    id = bin(id);
    var key = "some ascii password";

    //количество символов, отбрасываемых для усиления стойкости
    var offset = 1024;
    //можно больше, это выполняется разово при старте сервера и не влияет на производительность

    var i, j, x;
    var s = [];
    var res = "";
    //init s
    for(i = 0; i < 256; s[i] = i++);
   
    i = 0;
    j = 0;
    offset += 2 * id.length;
    while(offset > 0){
        offset--;
        ++i; i %= 256;
        j = (j + s[i]) % 256;
       x = s[i]; s[i] = s[j]; s[j] = x;
       res += String.fromCharCode(s[(s[i] + s[j]) % 256]);
    }
     //отбрасываем первых n байт шифртекста для усиления кода)
    res = res.substr(offset);
    alert(res);

    var rc4 = function (cstr) {

        return function (str) {
            var res = '';
            for (var i = 0; i < str.length; i++) {

                res += String.fromCharCode(str.charCodeAt(i) ^ cstr.charCodeAt(i));
                //да! всего один xor на байт - минимальный оверхед!
            }
            return res;
        };
    }(res);

    //здесь переворачиваем полученный id
    var c = id.length;
    while (c--) id += id[c]; //собственно все.

    alert("зеркало: " + id);
    //получаем шифртекст
    var cipher = hex(rc4(id));
    alert("cipher: " + cipher);

    var token = cipher.substr(0, cipher.length/2);
    cookie.id = cipher.substr(cipher.length/2);
    alert("result:" + token + "\n" + cookie.id);
    //выставляем куки
    //---------------------   
    //получили куки
    //а вот так проверяем:
    alert("bin:" + bin(token + cookie.id));
    var cipher = rc4(bin(token + cookie.id));
    token = cipher.substr(0, cipher.length/2); //его еще надо перевернуть
    cookie.id = cipher.substr(cipher.length/2);
    var tmp = "";
    var c = token.length;
    while (c--) tmp += token[c];
    token = tmp;

    if (token !== cookie.id) {
        alert("все плохо");
    } else {
        alert("все хорошо");
    }
</script>
</body>
</html>


Первое — это реализация rc4. На основании пароля мы не просто инициализируем s таблицы, а сразу генерируем последовательность байт для дальнейшего xor. Это удвоенная длина id + число отбрасываемых байт (защита от атаки Флурера, Мартина и Шамира). После того как отбросили первые плохие байты, остаток длиной 2 * id.length остается в переменной res. В принципе можно было бы ее оставить в глобале и дергать с функций, но в ноде никогда не знаешь, куда может деться контекст в череде коллбэков, поэтому фиксируем результат в замыкании. То есть по сути наша функция rc4 сводится к ксору по заранее подготовленной последовательности байт.

Обратите внимание — в ноде это все происходит один раз, при старте сервера. При каждой обработке запроса дергаем функцию rc4, которая уже имеет все необходимое. То есть затраты вообще минимальны — две операции на байт (XOR и перестановка).

Дальше в принципе все прозрачно. Переворачиваем id, шифруем, режем. Обратно склеиваем, расшифровываем, переворачиваем, сравниваем.

Эта схема работает только на один уровень — авторизован/нет. Если нужно вести несколько групп (новичок/старожил/легенда) то: генерим несколько id, с каждым из них делаем то же самое — шифруем, но! Каждую следующую пару можно просто класть в http-only куки, без дублирования значения при запросе, как мы это делали с csrf-токеном (одного токена достаточно для защиты от csrf атаки).

Что нам это дает? Мы можем регулировать доступ, вообще не обращаясь к каким либо storages. Никаких сверхбыстрых nosql, никаких sql не надо! То есть не вообще не надо никогда, а не надо там, где надо выделить принадлежность пользователя какой либо группе, не уточняя кто он такой.

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

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

ссылки по теме:
одноразовый блокнот (шифр Вернама)
RC4
Частотный анализ


P.S. коллеги, призываю воздержаться от комментариев в стиле чепуха это все, умные люди давно бы уже придумали это, если бы это работало. Если схема слабая (я вполне это допускаю, у меня ЧСВ достаточно давно обнулено) — прошу просто изложить последовательность действий, ее ломающих. Либо ссылку на источник. В общем надеюсь на конструктив.

Комментарии (9)


  1. kyprizel
    14.05.2015 00:57
    +8

    скажите что это такой троллинг?


    1. rocknrollnerd
      14.05.2015 02:16
      +1

      Ну а чо, логично все. Генерируем случайную последовательность, и шифруем ее еще до кучи. Чтобы еще больше случайности было! А если еще раз зашифровать — наверное, будет еще и еще.


      1. kyprizel
        14.05.2015 11:22

        главное байты не забыть перевернуть!


  1. alff31
    14.05.2015 01:54

    посмотреть эту картинку могут только тролли 80-го уровня
    А что если мы выясняем что троль оказался 79 уровня, как у него отозвать право смотреть картинку?


    1. gonzazoid Автор
      14.05.2015 02:00

      так мы троллю 79 уровня при авторизации не вешаем куку тролля 80-ого уровня.
      Другое дело, если тролль 80-ого уровня может быть понижен со временем в звании — тут да, однажды отданная кука позволяет пользоваться услугой в будущем, даже при изменении звания. Этот момент надо учитывать и не использовать в подобных процессах. Но на процессах типа авторизован/не авторизован, имеет 20 постов/не имеет — вполне работает, на мой взгляд.
      И кстати, нет. Если у пользователя есть кука тролля 79-го уровня, можно не принимать во внимание куку 80-ого уровня, даже если она есть.
      Хотя да, он ее сам может убрать, согласен.


    1. gonzazoid Автор
      14.05.2015 02:22

      у схемы есть другой серьезный недостаток. Если используется одно и тоже преобразование для любой проверки, то получив пару токен: сессия для авторизации, можно их подсовывать под видом разрешения на другое действие. Отсюда вывод — на каждое действие должна генерироваться пара с уникальной длиной!
      А вообще нет, там же на каждое действие своя последовательность rc4, все, отменить панику.


  1. alff31
    14.05.2015 23:13

    Ну и еще немного конструктива. Хорошо что вы придумали такую схему, но все давно уже придумано и реализовано. Предлагаемое вами решение называется криптографическая подпись.
    И, например, во фреймоврке django например это реализовано by disign: docs.djangoproject.com/en/1.8/topics/signing И даже есть подпись со временем жизни.


    1. gonzazoid Автор
      14.05.2015 23:55

      спасибо за ссылку. Но речь вообще то не о заявке на права первооткрывателя. Мне интересно — стойка ли предложенная мной *реализация*. Интерес в том, что она очень, ОЧЕНЬ простая, примитивная, я бы даже сказал. И если окажется, что при этом она стойкая — лично для меня это будет очень хорошее решение в своем проекте.


      1. Chikey
        15.05.2015 01:36
        +1

        Рельсы используют session jar уже давно, как и масса других фреймворков. Это удобно и stateless. А вот хранить некие «права» в них не вижу смысла, права должны проверяться на сервер сайде и только там.