- PVSM.RU - https://www.pvsm.ru -

Как я писал логин по протоколу SRP6a на Python, а получил… JavaScript и Python в одной коробке

Большое и увлекательное путешествие начинается с простого и банального шага. Когда мне на работе понадобилось реализовывать процесс логина для набора автоматизированных тестов, я даже не представлял, куда это приведет.
Дальше в статье вы узнаете, как доказать, что вы знаете пароль, ни разу не передав его в каком бы то ни было виде (доказательство с нулевым разглашением), и как я спотыкался на готовых примерах, чтобы получить работающий код на Python в конце пути.
ОТКАЗ ОТ АВТОРИТЕТНОСТИ: Я программирую на Питоне не очень давно, потому, вещи о которых я расскажу, и ошибки, которые я совершал могут показаться вам тривиальными.

Я тебе конечно верю, разве могут быть сомненья?
Я и сам всё это видел, это наш с тобой секрет

image
Для начала, если вы не знаете о незаслуженно мало упоминаемом протоколе SRP-6a тот вамобязательно надо ознакомиться с замечательной статьей на Хабре [1] и крайне подробная англовики [2].

Лирическое отступление о доказательстве без разглашения

Итак, во время тестирования команды молодых космонавтов Света и Саша должны доказать Федору, что у нее них есть секретный ключ. Причем, ключ есть только у одного члена команды. Но просто показать сам ключ категорически нельзя.
Конструкция комнаты для испытаний показана на рисунке:

image.

  • Ключ открывает проход между C и D.
  • Из точки A не видно человека в точке B

Федор предлагает простой алгоритм: он находится в точке А. Проверяемый на наличие ключа — в точке B. Затем, проверяемый идет в любую сторону, которую сам случайно выберет. Федор идет в точку B и кричит — «выйди справа» или «выйди слева». При первой попытке вероятность обнаружить, что у Светы или Сашы нет ключа, равна 50 %. Если же повторить процесс k раз, то вероятность будет $frac{1}{2^k}$. При 20 же повторениях эта вероятность будет порядка 10−6. (То есть, один к миллиону).
Затем, Федор задумался, что кроме него никто не может точно сказать, у кого был ключ. Если даже Федор запишет все происходящее на камеру, то полученная кинохроника не будет являться доказательством для какой-либо другой стороны. Ведь они могли заранее сговориться, откуда будет выходить Света или Саша. Соответственно, они всегда будут выходить с правильной стороны, не имея при этом самого ключа. Существует ещё один способ: Федор просто вырезает все неудачные попытки из кинопленки.
Когда Федя, Света и Саша успешно прошли испытание, то в кибернетическом центре им сообщили, что найденное ими решение давно известно и называется "нулевое разглашение [3]", только в ЭВМ всё реализовано намного сложнее…
(конец лирической части)

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

Итак, первое, что я нашел в интернете, была замечательна библиотека srp от Tom Cocagne, где даже был дан пример, как она работает [4].
Но для меня, как человека нового, пример в документации был довольно бесполезен. Как послать данные на сервер и получить ответ? Что слать? Пришлось обновить в памяти работу библиотеки request, и изучать запросы-ответы браузере.
Итак, если вы входите на сайт по SRP, то вход происходит в два этапа.

Мини-инструкция для тех, кто не знает, как записать запрос:

Откройте браузер (Firefox или Chrome)
Нажмите F12
На вкладке Network будут показаны все данные, нужные для анализа
image
После ввода данных и клика по кнопке это две первых строки.

После того, как мы ввели правильный (или не правильный — пока что не важно) логин и пароль и кликнули на кнопке, мы получаем оба этапа логина в виде запросов к серверу и ответов от него. Теперь нам надо понять, что посылать на сервер а что — анализировать от него (В Хроме это раздел Request Headers и Form Data).
Для данного конкретного сайта мы получаем для первого этапа:
user.login: name
user.token.srp: 00c5191d86a6e0c2382..................

и для второго этапа:
user.login: name
user.token.srp: 00de732d6099a346b79..................

Что ж, попробуем подставить это в наш код.

Установка необходимых библиотек:

в командной строке выполнить:
pip install requests
pip install srp

import requests, binascii
import xml.etree.ElementTree as ET
import srp #py3srp

lgn = 'name'
pwd = 'password'
cs  = 'https://testsite.biz'

usr = srp.User(lgn, pwd)
s   = requests.session()
A   = usr.start_authentication()
# преобразовываем А в шестнадцатеричную строку
a   = str(binascii.b2a_hex(A[1]))[2:-1]
print (a)

# код для вывода ответов сервера
def _printResponse(res, name=''):
    print('t','=== response : === ', name)
    for child in res.iter('*'):
        print( child.tag, child.attrib)
    print('n')

# формируем запрос
r = s.post(
        cs + "/authentication/challenge",
        data={
            "user.login": lgn,
            "user.token.srp": a
        }
    )
# читаем ответ
r_challenge = ET.fromstring(r.content)
_printResponse(r_challenge, 'r_challenge')
# создаем такие же заголовки, как подглядели в браузере
heads = r.headers
h = {
        'Connection': 'keep-alive',
        'Origin' : cs,
        'X-CSRF-Token': heads['X-CSRF-Token'],
        'Content-Type': 'application/x-www-form-urlencoded',
        'Referer':  cs+'/ib/',
        'Accept-Encoding': 'gzip, deflate, br',
        'Cookie' : heads['Set-Cookie'].split(';')[0],
        'Pragma': 'no-cache'
    }
# print('headers : n', heads, 'n')

salt = r_challenge[0][0].attrib['salt']
B = r_challenge[0][0].attrib['challenge']

def B_S_trim(val_sb):
    if val_sb[:2] == '00':
        #print('sb0')
        return str(val_sb)[2:]
    else:
        return str(val_sb)

M = usr.process_challenge (binascii.a2b_hex(salt), binascii.a2b_hex(B))

r = s.post(
    cs + "/authentication/login",
    data={
        "user.login": lgn,
        "user.token.srp": M
    },
    headers=h    # cookies= cook
)
r_login = ET.fromstring(r.content)
_printResponse(r_login, 'r_login')

print ('login : ', r_login.attrib['result'])

Еще одно отступление по анализу ответов для начинающих:

Ответы, которые приходят от сервера легко преобразовать в XML-структуру вида:

<response result="true" context="0:ffffffffffffffff:12345678908272:90f894" level="0">
<tokenlist>
<token type="srp"
salt="0083de737ddcfade27e4905d5d0accd7150e7703faf6fc1424eb425eaff3d11f78"
challenge="19bc0623258fd1b648637f3d53edfd78450abdab424af7f4890bce3b14d756da7725ceb5819f5c53f78958db02c649c099919ccaf81ed9d706d4c24968895c94043d9f0f8b2feb0550cc62e84a9ce4442a7c736c3f980130764b1986266069ef203239d9585b32cfc0cf4f23cad65c0a22a78bfaac99103e6bf6931b4def8723"/>
</tokenlist>
</response>

Чтобы сделать это в Python мы в строке r_challenge = ET.fromstring(r.content) преобразовывает ответ в такое дерево, а дальше обращаемся к его веткам по индексам, а к значениям — по ключам перед знаком "="
то есть код
salt = r_challenge[0][0].attrib['salt']
прочтет в нулевой ветви нулевую подветвь, а в ней — значение атрибута 'salt'

Изначально, мне было не очевидно, что обмен должен проходить в рамках одной сессии.
Это решается строкой s = requests.session()
Затем, как оказалось, сайт, под который писал логин, страхует пользователя от перекрестных перехватов запроса, потому надо передавать ему значение X-CSRF-Token.
И вот, когда всё казалось правильным, логин всё равно не проходил.
И тут мне на глаза попалась чудная картинка про протокол SRP6a:
Как я писал логин по протоколу SRP6a на Python, а получил… JavaScript и Python в одной коробке - 5
это тот случай, когда графическое представление информации очень помогает пониманию материала. Вверху над всем висит общая договоренность про N, g и k.
Более того — k это хеш. И если в моем коде применялось N, g и хеш SHA1 по умолчанию — то понятно, что я «не совпал» с сервером.
Итак, как казалось, выход прост: указать явно хеш и N.
Добавляем в строку создания объекта «пользователь» два значения, подсмотренные в документации:
usr = srp.User(lgn, pwd, 2,0)
(2 — хеширование SHA 256, 0 — N1024 — значения добыты в ходе допроса с пристрастием админа сервера)
и…
опять логин не происходит. Хотя, с точки зрения Python и протокола SRP6a мы сделали всё так же, как происходит в браузере.
Так же, но не так.

Соединяем JavaScript и Python

Мы (я и JS-дев) стали ковырять практически построчно, по всем переменным, которые генерировались в ходе процесса, зафиксировав константами стартовые значения. И выяснилось, что всё значения в основном совпадают. Кроме одного случая. В JS в нескольких местах происходило хеширование не строки, а объекта WordArray.

Объект WordArray добывается так:

function WordArray (words, sigBytes) {
            words = this.words = words || [];

            if (sigBytes != undefined) {
                this.sigBytes = sigBytes;
            } else {
                this.sigBytes = words.length * 4;
            }
        }
function hexWord (hexStr) {
            // Shortcut
            var hexStrLength = hexStr.length;
            // Convert
            var words = [];
            for (var i = 0; i < hexStrLength; i += 2) {
                words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
            }
            return new WordArray(words, hexStrLength / 2);
        }

И хотя наверное можно было переписать подобное на Питон, но по коду было еще пару нюансов. Кроме того, чтобы разобраться со всеми переменными и правильностью передачи данных уже ушла прорва времени, так что было решено связать JavaScript и Python.
Да, да, это вполне возможно и требует всего лишь установки чудесной библиотеки js2py от Piotr Dabkowski [5].
Правда есть две вещи, омрачающие жизнь: библиотека требует установки node.js и в ней сломан модуль обнаружения зависимостей для node в Windows (исправленная нами на коленке версия выложена в баге [6] — используйте, но ничего не гарантируем.)

Моя попытка номер два

Для попытки номер два в код модуля SRP был добавлен “js-хелпер”

import js2py

## JS helpers : start
CryptoJS = js2py.require('crypto-js')
def _jsParseHex(str):
	return CryptoJS.enc.Hex.parse(str)

def _jsH(obj):
	_hash = CryptoJS.SHA256(obj).toString()
	return _hash.lower()

def _jsCaclculate_x(hsf, slt, usn, pas):
	_hash = _H(hsf(), usn, DEFAULT_SEPARATOR, pas)
	return _jsH(_jsParseHex(slt + _hash))

def _pad_A_B_S(str):
	sym = str[0]
	if  int(sym, 16) > 7:
		return '00' + str
	else:
		return str

def _jsCaclculate_M(A, B, S):
	A = _pad_A_B_S(A)
	B = _pad_A_B_S(B)
	S = _pad_A_B_S(S)
	_M = A+B+S
	return _jsH(_jsParseHex(_M))
## JS helpers : end

И внесены вызовы по коду в нескольких местах, где это происходило на JS
После этого чудо свершилось — процедура логина стала успешной.

Если статья вам понравилась/не понравилась ответьте на вопрос:

Автор: Александр Панченко

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/282518

Ссылки в тексте:

[1] замечательной статьей на Хабре: https://habr.com/post/121021/

[2] англовики: https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol

[3] нулевое разглашение: https://ru.wikipedia.org/wiki/%D0%94%D0%BE%D0%BA%D0%B0%D0%B7%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D1%82%D0%B2%D0%BE_%D1%81_%D0%BD%D1%83%D0%BB%D0%B5%D0%B2%D1%8B%D0%BC_%D1%80%D0%B0%D0%B7%D0%B3%D0%BB%D0%B0%D1%88%D0%B5%D0%BD%D0%B8%D0%B5%D0%BC

[4] пример, как она работает: https://pythonhosted.org/srp/srp.html#example

[5] библиотеки js2py от Piotr Dabkowski: https://github.com/PiotrDabkowski/Js2Py/

[6] выложена в баге : https://github.com/PiotrDabkowski/Js2Py/issues/125

[7] Источник: https://habr.com/post/413131/?utm_source=habrahabr&utm_medium=rss&utm_campaign=413131