Русский
Русский
English
Статистика
Реклама

Тегиниктонечитает

Продолжаем прокачивать Ansible

22.02.2021 06:06:33 | Автор: admin

Поводом для этой статьи послужил пост в чате @pro_ansible:

Vladislav ? Shishkov, [17.02.21 20:59]Господа, есть два вопроса, касаются кастомной долгой операции, например, бекапа: 1. Можно ли через ансибл прикрутить прогрессбар выполнения кастомного баша? (если через плагин, то пните в какой-нибудь пример или документацию плиз) 2. Вроде хочется для этого баша написать плагин, но встает вопрос, как быть и как решать моменты выполнения, которые идемпотентны?

Беглый поиск по задворкам памяти ничего подходящего не подсказал. Тем не менее, я точно вспомнил, что код Ansible легко читаемый, и искаропки поддерживает расширение как плагинами, так и обычными Python-модулями. А раз так, то ничего не мешает в очередной раз раздвинуть границы возможного. Hold my beer!...

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

Исходный вопрос можно свести к двум простейшим шагам:

  1. Захватить stdout команды на целевом хосте

  2. Передать его на управляющий хост.

Передаём данные на управляющий хост

Предлагаю начать с конца: с организации дополнительного канала передачи на управляющий хост. Решение этого вопроса выглядит достаточно очевидным: вспоминаем, что Ansible работает поверх ssh, и используем функцию обратного проброса порта:

Код на Python
# добавляем куда-нибудь сюда:# https://github.com/ansible/ansible/blob/5078a0baa26e0eb715e86c93ec32af6bc4022e45/lib/ansible/plugins/connection/ssh.py#L662self._add_args(    b_command,    (b"-R", b"127.0.0.1:33333:" + to_bytes(self._play_context.remote_addr, errors='surrogate_or_strict', nonstring='simplerepr') + b":33335"),    u"ANSIBLE_STREAMING/streaming set")

Как это работает? При сборке аргументов командной строки для установления ssh-соединения эта конструкция предоставит нам на целевом хосте порт 33333 по адресу 127.0.0.1, который будет туннелировать входящие соединения на контроллер - прямиком на порт 33335.

Для простоты используем netcat (ну правда, ну что за статья без котиков?): nc -lk 33335.

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

Перехватываем stdout

Полдела сделано - идём дальше. Мы хотим перехватить stdout какой-то команды - по логике работы Ansible нам подойдёт модуль shell. Забавной, что он оказался пустышкой - в нём ни строчки кода, кроме документации и примеров, зато находим в нём отсылку к модулю command. С ним всё оказалось хорошо, кроме того факта, что нужная функция в нём напрямую не описана, хотя и использована. Но это уже было почти попадание в яблочко, потому что в итоге она нашлась в другом файле.

Под мысленное просто добавь воды просто добавляем щепотку своего кода:

Опять код
# в начале basic.py, рядом с прочими import'ами import socket# в функции run_command - где-нибудь тут:# https://github.com/ansible/ansible/blob/5078a0baa26e0eb715e86c93ec32af6bc4022e45/lib/ansible/module_utils/basic.py#L2447clientSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM);clientSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)clientSocket.connect(("127.0.0.1",33333));# в функции run_command - где-нибудь тут:# https://github.com/ansible/ansible/blob/5078a0baa26e0eb715e86c93ec32af6bc4022e45/lib/ansible/module_utils/basic.py#L2455clientSocket.send(b_chunk);# в функции run_command - где-нибудь тут# https://github.com/ansible/ansible/blob/5078a0baa26e0eb715e86c93ec32af6bc4022e45/lib/ansible/module_utils/basic.py#L2481clientSocket.close()

Собираем воедино и запускаем

Осталось сделать что? Правильно, определиться со способом подключения изменённых модулей в стоковый Ansible. На всякий случай напоминаю: мы поправили один connection plugin, и один модуль из стандартной библиотеки Ansible. Новичкам в этом деле могу рекомендовать статью хабраюзера chemtech с расшифровкой моего доклада на Стачке-2019 (там как раз в том числе объясняется, какие Python-модули куда складывать), ну а опытным бойцам эти пояснения вроде и не нужны :-)

Итак, время Ч. Результат в виде статичной картинки не очень показателен, поэтому я настроил tmux и запустил запись скринкаста.

Для внимательных зрителей скринкаста

В анимации можете увидеть два полезных побочных эффекта:

  • Теперь мы видим stdout всех не-Python процессов, которые запускаются Ansible'ом на целевом хосте - например, тех, что запускаются при сборе фактов;

  • Настройки переиспользования ssh-соединений из другой моей статьи позволяют получать этот самый stdout от удалённой команды уже после отключения Ansible от хоста.

Хотите ко мне на тренинг по Ansible?

Раз уж вы здесь - значит, вам как минимум интересно то, что я пишу об Ansible. Так вот, у меня есть опыт ведения такого тренинга внутри компании для коллег.

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

Подробнее..

C и Python мост между мирами

14.03.2021 12:14:15 | Автор: admin

Подход к снаряду

Не так давно, в феврале, у меня случился замечательный день: ещё один проект-долгострой "полетел". О чём речь? Речь о моей давней задумке на тему использования интерпретатора Python в программах на C: я реализовал добавление хуков на Python в ReOpenLDAP. Сама по себе тема, понятное дело, большая, поэтому в таких ситуациях я пишу минимальный код на C, который служит как раз для проверки концепта и его обкатки - его очень удобно запускать под инструментами типа Valgrind, которые незамедлительно укажут на явные ошибки ляпы в работе с памятью. Однако после окончания работы я понял, что сам по себе минимальный код может быть полезен кому-то ещё, кроме меня. Почему? Потому перед началом работы я наивно предполагал, что официальная документация по C API поможет всё сделать легко и быстро, но увы! - внятного примера с пошаговым разбором не нашёл. Что ж, это open source, детка, не нравится - сделай сам.

Для большей точности: пример разрабатывался на CentOS 7 с установленными пакетами python3, python3-devel, то есть всё описанное было написано, отлажено и проделано запущено именно в этом окружении.

Для удобства весь разбираемый код лежит в репозитории на моём ГитХабе.

Инициализация

Начало начал - подключение к нашей программе заголовочного файла:

#include <Python.h>

Далее готовим нужные константы (понятно, что у вас их значения будут другими, просто следите за смыслом значений):

char hook_file_path[] = "./";char hook_file[] = "ldap_hooks";char *hook_functions[] = {  "add_hook","bind_hook","unbind_hook","compare_hook","delete_hook","modify_hook",  "modrdn_hook","search_hook","abandon_hook","extended_hook","response_hook",NULL };PyObject *pName, *pModule, *pFunc, *pValue, *sys, *path, *newPaths;

Здесь:

  • hook_file_path - каталог в файловой системе, в котором вы хотите хранить свой код на Python;

  • hook_file- имя файла с кодом, расширение .py указывать не надо;

  • hook_functions - массив с названиями функций в файле, которые мы будем искать и вызывать; последний элемент, NULL, использован как костыль для обозначения конца массива.

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

Готовим интерпретатор к работе:

Py_Initialize();
HERE BE DRAGONS

Помните, что в программе фактически придётся управлять памятью сразу в двух местах: на уровне кучи (malloc/free), и на уровне чёрного ящика интерпретатора Питона. Объекты, возвращаемые функциями интерпретатора, будут размещёны в памяти, им же и управляемой, поэтому придётся периодически сообщать интерпретатору Python, что тот или иной объект мы больше не используем, и можно его добавить в список для garbage collector'а. Для этого нам пригодится вызов Py_XDECREF(*Py_Object).Он умеет сам проверять, не NULL ли передан в параметре, и если да - функция не делает ничего, в отличие от Py_DECREF(*Py_Object), которая в этом случае вернёт ошибку.

Далее загружаем модуль sys и добавляем в его список path нужный нам путь:

// credits to https://stackoverflow.com/questions/50198057/python-c-api-free-errors-after-using-py-setpath-and-py-getpath// get handle to python sys.path objectsys = PyImport_ImportModule("sys");path = PyObject_GetAttrString(sys, "path");// make a list of paths to add to sys.pathnewPaths = PyUnicode_Split(PyUnicode_FromString(hook_file_path), PyUnicode_FromWideChar(L":", 1), -1);// iterate through list and add all pathsfor(i=0; i<PyList_Size(newPaths); i++) {    PyList_Append(path, PyList_GetItem(newPaths, i));}Py_XDECREF(newPaths);Py_XDECREF(path);Py_XDECREF(sys);

Я не делаю какого-то большого секрета из того факта, что часть этого кода взята со StackOverflow: что же тут поделать, ищу специфические вещи, которые редко в полном объёме покрываются документацией.

Загрузка файла с Python-кодом

Дальше будет чуть попроще - всего лишь выполним importдля нашего модуля. Почему так - ровно потому, что налицо проблема курицы и яйца: некому вызвать import ldap_hooks.

pName = PyUnicode_DecodeFSDefault(hook_file);if (pName == NULL){  fprintf(stderr,"No Python hook file found\n");  return 1;}pModule = PyImport_Import(pName);Py_DECREF(pName);// fprintf(stderr,"No C errors until now\n");

Поиск функций в файле и их вызов

Итак, теперь у нас есть загруженный в память интерпретатор, готовый к работе, а его состояние соответствует тому, как если бы мы из кода на Python вызвали import для файла, чьё имя указано в строке hook_file.

Далее получаем объект нужной функции и вызываем её:

pFunc = PyObject_GetAttrString(pModule,hook_functions[i]);if (pFunc && PyCallable_Check(pFunc)) {    fprintf(stderr,"function %s exists and can be called\n", hook_functions[i]);    fprintf(stderr, "Calling %s\n", hook_functions[i]);    pValue = PyObject_CallFunction(pFunc, "s", hook_functions[i]);

Обратите внимание: после получения объекта по имени всегда полезно проверить, можем ли мы к нему обратиться. Именно это делает вторая строка. А пятая строка этого фрагмента вызывает функцию, передавая ей аргумент типа "строка" (на это указывает "s"). Для удобства каждая функция нашего кода на Python будет вызываться с единственным строковым аргументом, равным названию этой самой функции.

Вообще по документацииPyObject_CallFunction ровно так и вызывается:

  • первый параметр - объект вызываемой функции в Python-коде, ранее полученный через PyObject_GetAttrString;

  • второй, строка - сообщает интерпретатору тип и количество аргументов (более подробно об этой строке - в документации);

  • третий и далее аргументы - аргументы, то есть то, что наша Python-функция получит внутри входного кортежа (питонистам это известно как *args).

Итак, ссылка на объект, содержащий в себе то, что вернул наш код на Python - в pyValue. Можно праздновать?... Нет, рано. Переходим к следующей части.

Разбор результата

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

Осторожно, код
if (pValue != NULL) {  if (pValue == Py_None) {   fprintf(stderr,"==> Дружище, это None, тут правда ничего нет\n");  }  else if ((pValue == Py_False) || (pValue == Py_True)) {    fprintf(stderr,"==> Bool:\n");    if (pValue == Py_False) {      fprintf(stderr, " False\n");    } else {      fprintf(stderr, " True \n");    }  } else if (PyUnicode_Check(pValue)) {    fprintf(stderr,"==> String:\n");    const char* newstr = PyUnicode_AsUTF8(pValue);    fprintf(stderr,"\"%s\"\n", newstr);  } else if (PyDict_Check(pValue)) {    PyObject *key, *value;    Py_ssize_t pos =0;    fprintf(stderr,"==> Dict:\n");    while (PyDict_Next(pValue, &pos, &key, &value)) {     fprintf(stderr, "%s: %s\n", PyUnicode_AsUTF8(key), PyUnicode_AsUTF8(value));    }  } else if (PyList_Check(pValue)) {    fprintf(stderr,"==> List:\n");    Py_ssize_t i, seq_len;    PyObject *item;    seq_len = PyList_Size(pValue);    for (i=0; i<seq_len; i++) {      item = PyList_GetItem(pValue, i);      fprintf(stderr, " %s\n", PyUnicode_AsUTF8(item));      // !!!--> NOT NEEDED <--!!!  Py_DECREF(item);      }  } else if (PyTuple_Check(pValue)) {    fprintf(stderr,"==> Tuple:\n");    Py_ssize_t i, seq_len;    PyObject *item;    seq_len = PyTuple_Size(pValue);    for (i=0; i<seq_len; i++) {      item = PyTuple_GetItem(pValue, i);      fprintf(stderr, " %s\n", PyUnicode_AsUTF8(item));      // !!!--> NOT NEEDED <--!!! Py_DECREF(item);      }  } else if (PyFloat_Check(pValue)) {    fprintf(stderr, "==> Float: %f\n", PyFloat_AsDouble(pValue));  } else if (PyLong_Check(pValue)) {    fprintf(stderr, "==> Long: %ld\n", PyLong_AsLong(pValue));  } else if (PySet_Check(pValue)) {    fprintf(stderr,"==> Set:\n");    PyObject *str_repr = PyObject_Repr(pValue);    fprintf(stderr, " %s\n", PyUnicode_AsUTF8(str_repr));    Py_XDECREF(str_repr);  } else {    fprintf(stderr, "==> Какая-то дичь! Проверь-ка тип результата функции %s\n", hook_functions[i]);  }  Py_XDECREF(pValue);} else {  fprintf(stderr, "WTF");}

Некоторые важные моменты по поводу разбора результатов, возвращаемых функциями:

  • особняком стоят значения None, True и False: для них нет каких-то отдельных проверочных функций, и мы в коде на C проверяем, не они ли это, простым сравнением со специальными константами: Py_None, Py_True, Py_False;

  • значения-словари для иллюстрации статьи обойдём встроенным итератором, но вообще, конечно, можем получить нужный элемент по ключу;

  • для списков и кортежей функции вида PyXXXX_GetItem возвращают "чужие" ссылки - то есть вместе с ними вашему коду на C не передаётся ни владение объектом, ни обязанность этот объект уничтожить через Py_DECREF()

  • если реализовать поддержку не конкретных типов, а протоколов - ваш C-код получит способность поддерживать питонячью утиную типизацию.

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

Функции обработки результатов

Значение в Python

Проверка

Использование в C

None

True

False

pValue == Py_None

pValue == PyTrue

pValue == Py_False

нет

строка

PyUnicode_Check(pValue)

PyUnicode_AsUTF8(pValue)

словарь

PyDict_Check(pValue)

PyObject *key, *value;
Py_ssize_t pos =0;
while ( PyDict_Next(
pValue, &pos, &key, &value)) {
.....
}

список

PyList_Check(pValue)

Py_ssize_t i, seq_len;
PyObject *item;
seq_len = PyList_Size(pValue);
for (i=0; i<seq_len; i++) {
item = PyList_GetItem(pValue, i);
.....}

кортеж

PyTuple_Check(pValue)

Py_ssize_t i, seq_len;
PyObject *item;
seq_len = PyTuple_Size(pValue);
for (i=0; i<seq_len; i++) {
item = PyTuple_GetItem(pValue, i);
.....}

число с плавающей точкой

PyFloat_Check(pValue)

PyFloat_AsDouble(pValue)

целое число

PyLong_Check(pValue)

PyLong_AsLong(pValue)

Заключение

В статье намеренно не освещались вопросы передачи каких-нибудь хитросложенных аргументов, объявления объектов-типов внутри интерпретатора и тому подобные вещи, включая обработку ошибок Python-кода - это всё-таки crash-course, а не олимпиада. Поэтому на этом откланиваюсь, и могу только добавить, что весь код лежит в репозитории на моём ГитХабе, а в комментариях попробую ответить на вопросы по теме статьи.

UPD. Поправил очепятку (Py_DECREF(*Py_Object) -> Py_DECREF(*Py_Object)).

Подробнее..

Ansible-vault decrypt обходимся без Ansible

25.04.2021 02:04:46 | Автор: admin

Исходные данные

Дано:

  • конвейер CI/CD, реализованный, к примеру, в GitLab. Для корректной работы ему требуются, как это очень часто бывает, некие секреты - API-токены, пары логи/пароль, приватные SSH-ключи - да всё, о чём только можно подумать;

  • работает этот сборочный конвейер, как это тоже часто бывает, на базе контейнеров. Соответственно, чем меньше по размеру образы - тем лучше, чем меньше в них всякой всячины - тем лучше.

Требуется консольная утилита, которая:

  • занимает минимум места;

  • умеет расшифровывать секреты, зашифрованные ansible-vault;

  • не требует никаких внешних зависимостей;

  • умеет читать ключ из файла.

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

На всякий случай сразу напоминаю, что по действующему законодательству разработка средств криптографической защиты информации в РФ - лицензируемая деятельность. Иначе говоря, без наличия лицензии вы не можете просто так взять и продавать получившееся решение.

По поводу допустимости полных текстов расшифровщиков в статьях вроде этой - надеюсь, что компетентные в этом вопросе читатели смогут внести свои уточнения в комментариях.

Начнём сначала

Итак, предположим, что у нас на Linux-хосте с CentOS 7 уже установлен Ansible, к примеру, версии 2.9 для Python версии 3.6. Установлен, конечно же, с помощью virtualenv в каталог "/opt/ansible". Дальше для целей удовлетворения чистого научного любопытства возьмём какой-нибудь YaML-файл, и зашифруем его с помощью утилиты ansible-vault:

ansible-vault encrypt vaulted.yml --vault-password-file=.password

Этот вызов, как можно догадаться, зашифрует файл vaulted.yml с помощью пароля, который хранится в файле .password.

Итак, что получается после зашифровывания файла с помощью утилиты ansible-vault? На первый взгляд - белиберда какая-то, поэтому спрячу её под спойлер:

Содержимое файла vaulted.yml
$ANSIBLE_VAULT;1.1;AES256613735363539633137393665366436613138616632663731303737306666343433373565363336643365393033623439356364663537353365386464623836640a356464633264626330383232353362636131353736383936656639623035303230613764323339313061613039666333383035656663376465393837636665300a633732313730626265636538363339383237306264633830653665343639303538636331373138663935666436613235366336663438376231303639666133633739623436303438623463323636336332643666663064393731363034623038653861373536643136393431636437346337323833333165386534353432386663343465333836643131643237313262386634396534383166303565306264303162383833643765613936373632626136663738363462626665366131646631663834316262663162353532366664386330323139643266636562653639306238653162316563613934323836303536613532623864303839313038336232616134626433353166383837643165643439363835643731316238316439633039

Ну а как именно эта белиберда работает "под капотом" - давайте разбираться.

Открываем файл /opt/ansible/lib/python3.6/site-packages/ansible/parsing/vault/__init__.py, и в коде метода encrypt класса VaultLib видим следующий вызов:

VaultLib.encrypt
 ... b_ciphertext = this_cipher.encrypt(b_plaintext, secret) ...

То есть результирующее содержимое нашего файла будет создано в результате вызова метода encrypt некоторого класса. Какого именно - в общем-то, невелика загадка, ниже по файлу есть всего один класс с именем VaultAES256.

Смотрим в его метод encrypt:

VaultAES256.encrypt
@classmethoddef encrypt(cls, b_plaintext, secret):    if secret is None:        raise AnsibleVaultError('The secret passed to encrypt() was None')    b_salt = os.urandom(32)    b_password = secret.bytes    b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt)    if HAS_CRYPTOGRAPHY:        b_hmac, b_ciphertext = cls._encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv)    elif HAS_PYCRYPTO:        b_hmac, b_ciphertext = cls._encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv)    else:        raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in encrypt)')    b_vaulttext = b'\n'.join([hexlify(b_salt), b_hmac, b_ciphertext])    # Unnecessary but getting rid of it is a backwards incompatible vault    # format change    b_vaulttext = hexlify(b_vaulttext)    return b_vaulttext

То есть перво-наперво генерируется "соль" длиной 32 байта. Затем из побайтного представления пароля и "соли" вызовом _gen_key_initctr генерируется пара ключей (b_key1, b_key2) и вектор инициализации (b_iv).

Генерация ключей

Что же происходит в _gen_key_initctr?

_gen_key_initctr:
@classmethoddef _gen_key_initctr(cls, b_password, b_salt):    # 16 for AES 128, 32 for AES256    key_length = 32    if HAS_CRYPTOGRAPHY:        # AES is a 128-bit block cipher, so IVs and counter nonces are 16 bytes        iv_length = algorithms.AES.block_size // 8        b_derivedkey = cls._create_key_cryptography(b_password, b_salt, key_length, iv_length)        b_iv = b_derivedkey[(key_length * 2):(key_length * 2) + iv_length]    elif HAS_PYCRYPTO:        # match the size used for counter.new to avoid extra work        iv_length = 16        b_derivedkey = cls._create_key_pycrypto(b_password, b_salt, key_length, iv_length)        b_iv = hexlify(b_derivedkey[(key_length * 2):(key_length * 2) + iv_length])    else:        raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in initctr)')    b_key1 = b_derivedkey[:key_length]    b_key2 = b_derivedkey[key_length:(key_length * 2)]    return b_key1, b_key2, b_iv

Если по сути, то внутри этого метода вызов _create_key_cryptography на основе пароля, "соли", длины ключа и длины вектора инициализации генерирует некий производный ключ (строка 10 приведённого фрагмента). Далее этот производный ключ разбивается на части, и получаются те самые b_key1, b_key2 и b_iv.

Следуем по кроличьей норе дальше. Что внутри _create_key_cryptography?

_create_key_cryptography:
@staticmethoddef _create_key_cryptography(b_password, b_salt, key_length, iv_length):    kdf = PBKDF2HMAC(        algorithm=hashes.SHA256(),        length=2 * key_length + iv_length,        salt=b_salt,        iterations=10000,        backend=CRYPTOGRAPHY_BACKEND)    b_derivedkey = kdf.derive(b_password)    return b_derivedkey

Ничего особенного. Если оставить в стороне всю мишуру, то в итоге вызывается функция библиотеки OpenSSL под названием PBKDF2HMAC с нужными параметрами. Можете, кстати, самолично в этом убедиться, открыв файл /opt/ansible/lib/python3.6/site-packages/cryptography/hazmat/backends/openssl/backend.py.

Кстати, длина производного ключа, как видите, специально выбирается таким образом, чтобы хватило и на b_key1, и на b_key2, и на b_iv.

Собственно шифрование

Движемся дальше. Здесь нас встречает вызов _encrypt_cryptography с параметрами в виде открытого текста, обоих ключей и вектора инициализации:

_encrypt_cryptography
@staticmethoddef _encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv):    cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND)    encryptor = cipher.encryptor()    padder = padding.PKCS7(algorithms.AES.block_size).padder()    b_ciphertext = encryptor.update(padder.update(b_plaintext) + padder.finalize())    b_ciphertext += encryptor.finalize()    # COMBINE SALT, DIGEST AND DATA    hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND)    hmac.update(b_ciphertext)    b_hmac = hmac.finalize()    return to_bytes(hexlify(b_hmac), errors='surrogate_or_strict'), hexlify(b_ciphertext)

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

Полученные в итоге байты подписи и шифртекста преобразуются в строки своих шестнадцатеричных представлений через hexlify. (см. строка 14 фрагмента выше)

Окончательное оформление файла

Возвращаемся к строкам 16-20 фрагмента VaultAES256.encrypt: три строки, содержащие "соль", подпись и шифртекст, склеиваются вместе, после чего снова преобразуются в шестнадцатеричное представление (комментарий прямо подсказывает, что это - для обратной совместимости).

Дальше дописывается заголовок (помните, тот самый - $ANSIBLE_VAULT;1.1;AES256), ну и, в общем-то, всё.

Обратный процесс

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

Понятно, что Python нам не подходит, иначе можно было и огород не городить: ansible-vault одинаково хорошо работает в обе стороны. С другой стороны, никто не мешает на базе библиотек Ansible написать что-либо своё - в качестве разминки перед "подходом к снаряду" я так и сделал, и о результате напишу отдельную статью.

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

Итак, нам понадобятся: FreePascal версии 3.0.4 (эта версия в виде готовых пакетов - самая свежая, нормально устанавливающаяся в CentOS 7), и библиотека DCPCrypt версии 2.1 (на GitHub). Интересно, что прямо вместе с компилятором (fpc) и обширным набором библиотек в rpm-пакете поставляется консольная среда разработки fp.

К сожалению, "искаропки" модули этой библиотеки не собираются компилятором fpc - в них нужны минимальные правки. С другой стороны, я предполагаю, что без этих правок предмет статьи перестаёт относиться к лицензируемым видам деятельности и начинает представлять чисто академический интерес - именно поэтому выкладываю статью без них.

Часть кода, относящуюся к генерированию производного ключа (реализацию той самой функции PBKDF2), я нашёл в интернете, и поместил в отдельный модуль под названием "kdf".

Вот этот модуль собственной персоной:

kdf.pas
{$MODE OBJFPC}// ALL CREDITS FOR THIS CODE TO https://keit.co/p/dcpcrypt-hmac-rfc2104/unit kdf;interfaceuses dcpcrypt2,math;function PBKDF2(pass, salt: ansistring; count, kLen: Integer; hash: TDCP_hashclass): ansistring;function CalcHMAC(message, key: string; hash: TDCP_hashclass): string;implementationfunction RPad(x: string; c: Char; s: Integer): string;var  i: Integer;begin  Result := x;  if Length(x) < s then    for i := 1 to s-Length(x) do      Result := Result + c;end;function XorBlock(s, x: ansistring): ansistring; inline;var  i: Integer;begin  SetLength(Result, Length(s));  for i := 1 to Length(s) do    Result[i] := Char(Byte(s[i]) xor Byte(x[i]));end;function CalcDigest(text: string; dig: TDCP_hashclass): string;var  x: TDCP_hash;begin  x := dig.Create(nil);  try    x.Init;    x.UpdateStr(text);    SetLength(Result, x.GetHashSize div 8);    x.Final(Result[1]);  finally    x.Free;  end;end;function CalcHMAC(message, key: string; hash: TDCP_hashclass): string;const  blocksize = 64;begin  // Definition RFC 2104  if Length(key) > blocksize then    key := CalcDigest(key, hash);  key := RPad(key, #0, blocksize);  Result := CalcDigest(XorBlock(key, RPad('', #$36, blocksize)) + message, hash);  Result := CalcDigest(XorBlock(key, RPad('', #$5c, blocksize)) + result, hash);end;function PBKDF1(pass, salt: ansistring; count: Integer; hash: TDCP_hashclass): ansistring;var  i: Integer;begin  Result := pass+salt;  for i := 0 to count-1 do    Result := CalcDigest(Result, hash);end;function PBKDF2(pass, salt: ansistring; count, kLen: Integer; hash: TDCP_hashclass): ansistring;  function IntX(i: Integer): ansistring; inline;  begin    Result := Char(i shr 24) + Char(i shr 16) + Char(i shr 8) + Char(i);  end;var  D, I, J: Integer;  T, F, U: ansistring;begin  T := '';  D := Ceil(kLen / (hash.GetHashSize div 8));  for i := 1 to D do  begin    F := CalcHMAC(salt + IntX(i), pass, hash);    U := F;    for j := 2 to count do    begin      U := CalcHMAC(U, pass, hash);      F := XorBlock(F, U);    end;    T := T + F;  end;  Result := Copy(T, 1, kLen);end;end.

Из бросающегося в глаза - обратите внимание, что в Pascal и его потомках отсутствует классическое разделение на заголовочные файлы и файлы собственно с кодом, в этом смысле модульная организация роднит его с Python, и отличает от C.

Однако от питонячьего модуля паскалевский отличается ещё и тем, что "снаружи" доступны только те функции/переменные, которые объявлены в секции interface. То есть по умолчанию внутри модуля ты можешь хоть "на ушах стоять" - снаружи никто не сможет вызвать твои внутренние API. Так устроен язык, а хорошо это или плохо - вопрос вкуса, поэтому оценки оставим в стороне (питонистам передают привет функции/методы, начинающиеся на "_" и "__").

Заголовочная часть

Код, как обычно, под спойлером.

Заголовочная часть ("шапка", header)
program devault;uses  math, sysutils, strutils, getopts, DCPcrypt2, DCPsha256, DCPrijndael, kdf;

Далее нам понадобится пара функций - hexlify и unhexlify (набросаны, конечно, "на скорую руку"). Они являются аналогами соответствующих функций Python - вторая возвращает строку из шестнадцатеричных представлений байтов входного аргумента, а первая - наоборот, переводит строку шестнадцатеричных кодов обратно в байты.

hexlify/unhexlify
function unhexlify(s:AnsiString):AnsiString;var i:integer;    tmpstr:AnsiString;begin  tmpstr:='';  for i:=0 to (length(s) div 2)-1 do    tmpstr:=tmpstr+char(Hex2Dec(Copy(s,i*2+1,2)));  unhexlify:=tmpstr;end;function hexlify(s:AnsiString):AnsiString;var i:integer;    tmpstr:AnsiString;begin  tmpstr:='';  for i:=1 to (length(s)) do    tmpstr:=tmpstr+IntToHex(ord(s[i]),2);  hexlify:=tmpstr;end;

Назначение функций showbanner(), showlicense() и showhelp() очевидно из названий, поэтому я просто приведу их без комментариев.

showbanner() / showlicense() / showhelp()
showbanner()
procedure showbanner();begin  WriteLn(stderr, 'DeVault v1.0');  Writeln(stderr, '(C) 2021, Sergey Pechenko. All rights reserved');  Writeln(stderr, 'Run with "-l" option to see license');end;
showlicense()
procedure showlicense();begin  WriteLn(stderr,'Redistribution and use in source and binary forms, with or without modification,');  WriteLn(stderr,'are permitted provided that the following conditions are met:');  WriteLn(stderr,'* Redistributions of source code must retain the above copyright notice, this');  WriteLn(stderr,'   list of conditions and the following disclaimer;');  WriteLn(stderr,'* Redistributions in binary form must reproduce the above copyright notice, ');  WriteLn(stderr,'   this list of conditions and the following disclaimer in the documentation');  WriteLn(stderr,'   and/or other materials provided with the distribution.');  WriteLn(stderr,'* Sergey Pechenko''s name may not be used to endorse or promote products');  WriteLn(stderr,'   derived from this software without specific prior written permission.');  WriteLn(stderr,'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"');  WriteLn(stderr,'AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,');  WriteLn(stderr,'THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE');  WriteLn(stderr,'ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE');  WriteLn(stderr,'FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES');  WriteLn(stderr,'(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;');  WriteLn(stderr,'LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON');  WriteLn(stderr,'ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT');  WriteLn(stderr,'(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,');  WriteLn(stderr,'EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.');  WriteLn(stderr,'Commercial license can be obtained from author');end;
showhelp()
procedure showhelp();begin  WriteLn(stderr,'Usage:');  WriteLn(stderr,Format('%s <-p password | -w vault_password_file> [-f secret_file]',[ParamStr(0)]));  WriteLn(stderr,#09'"password" is a text string which was used to encrypt your secured content');  WriteLn(stderr,#09'"vault_password_file" is a file with password');  WriteLn(stderr,#09'"secret_file" is a file with encrypted content');  WriteLn(stderr,'When "-f" argument is absent, stdin is read by default');end;

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

Переменные и константы
var secretfile, passwordfile, pass, salt, b_derived_key, b_key1, b_key2, b_iv,    hmac_new, cphrtxt, fullfile, header, tmpstr, hmac:Ansistring;    Cipher: TDCP_rijndael;    key, vector, data, crypt: RawByteString;    fulllist: TStringArray;    F: Text;    c: char;    opt_idx: LongInt;    options: array of TOption;const KEYLENGTH=32; // for AES256const IV_LENGTH=128 div 8;const CONST_HEADER='$ANSIBLE_VAULT;1.1;AES256';

Код

Ну, почти код - всё ещё вспомогательная функция, которая в рантайме готовит массив записей для разбора параметров командной строки. Почему она здесь - потому что работает с переменными, объявленными в секции vars выше.

preparecliparams()
procedure preparecliparams();begin  SetLength(options, 6);  with options[1] do    begin      name:='password';      has_arg:=Required_Argument;      flag:=nil;      value:=#0;    end;  with options[2] do    begin      name:='file';      has_arg:=Required_Argument;      flag:=nil;      value:=#0;    end;  with options[3] do    begin      name:='passwordfile';      has_arg:=Required_Argument;      flag:=nil;      value:=#0;    end;  with options[4] do    begin      name:='version';      has_arg:=No_Argument;      flag:=nil;      value:=#0;    end;  with options[5] do    begin      name:='license';      has_arg:=No_Argument;      flag:=nil;      value:=#0;    end;  with options[6] do    begin      name:='help';      has_arg:=No_Argument;      flag:=nil;      value:=#0;    end;end;

А вот теперь точно код самой утилиты:

Весь остальной код
begin  repeat    c:=getlongopts('p:f:w:lh?',@options[1],opt_idx);    case c of      'h','?' : begin showhelp(); halt(0); end;      'p' : pass:=optarg;      'f' : secretfile:=optarg;      'w' : passwordfile:=optarg;      'v' : begin showbanner(); halt(0); end;      'l' : begin showlicense(); halt(0); end;      ':' : writeln ('Error with opt : ',optopt); // not a mistake - defined in getops unit     end;  until c=endofoptions;  if pass = '' then // option -p not set    if passwordfile <> '' then      try        Assign(F,passwordfile);        Reset(F);        Readln(F,pass);        Close(F);      except        on E: EInOutError do        begin          Close(F);          writeln(stderr, 'Password not set and password file cannot be read, exiting');          halt(1);        end;      end    else      begin // options -p and -w are both not set          writeln(stderr, 'Password not set, password file not set, exiting');          showhelp();          halt(1);      end;  try    Assign(F,secretfile);    Reset(F);  except    on E: EInOutError do    begin      writeln(stderr, Format('File %s not found, exiting',[secretfile]));      halt(1);    end;  end;  readln(F,header);  if header<>CONST_HEADER then    begin      writeln(stderr, 'Header mismatch');      halt(1);    end;  fullfile:='';  while not EOF(F) do    begin    Readln(F,tmpstr);    fullfile:=fullfile+tmpstr;    end;  Close(F);  fulllist:=unhexlify(fullfile).Split([#10],3);  salt:=fulllist[0];  hmac:=fulllist[1];  cphrtxt:=fulllist[2];  salt:=unhexlify(salt);  cphrtxt:=unhexlify(cphrtxt);  b_derived_key:=PBKDF2(pass, salt, 10000, 2*32+16, TDCP_sha256);  b_key1:=Copy(b_derived_key,1,KEYLENGTH);  b_key2:=Copy(b_derived_key,KEYLENGTH+1,KEYLENGTH);  b_iv:=Copy(b_derived_key,KEYLENGTH*2+1,IV_LENGTH);  hmac_new:=lowercase(hexlify(CalcHMAC(cphrtxt, b_key2, TDCP_sha256)));  if hmac_new<>hmac then    begin    writeln(stderr, 'Digest mismatch - file has been tampered with, or an error has occured');    Halt(1);    end;  SetLength(data, Length(crypt));  Cipher := TDCP_rijndael.Create(nil);  try    Cipher.Init(b_key1[1], 256, @b_iv[1]);    Cipher.DecryptCTR(cphrtxt[1], data[1], Length(data));    Cipher.Burn;  finally    Cipher.Free;  end;  Writeln(data);end.

Дальше будет странная таблица, но, кажется, это - самый удобный способ рассказа об исходном коде.

Стр.

Назначение

2-13

разбор параметров командной строки с отображением нужных сообщений;

14-34

проверка наличия пароля в параметрах, при отсутствии - попытка прочесть пароль из файла, при невозможности - останавливаем работу;

35-44

попытка прочесть зашифрованный файл, указанный в параметрах;

Небольшой чит: по умолчанию имя файла (переменная secretfile) равно пустой строке; в этом случае вызов Assign(F, secretfile) в строке 36 свяжет переменную F с stdin

45-50

проверка наличия в файле того самого заголовка $ANSIBLE_VAULT;1.1;AES256;

51-57

читаем всё содержимое зашифрованного файла и закрываем его;

58-63

разбираем файл на части: "соль", дайджест, шифртекст - всё отдельно; при этом все три части нужно будет ещё раз прогнать через unhexlify (помните примечание в VaultAES256.encrypt?)

64-73

вычисление производного ключевого материала; разбиение его на части; расчёт дайджеста; проверка зашифрованного файла на корректность дайждеста;

74-83

подготовка буфера для расшифрованного текста; расшифровка; затирание ключей в памяти случайными данными; вывод расшифрованного содержимого в поток stdout

Интересная информация для питонистов

Кстати, вы же слышали, что в Python 3.10 наконец-то завезли оператор case (PEP-634)? Интересно, что его ввёл сам BDFL, и произошло это примерно через 14 лет после того, как по результатам опроса на PyCon 2007 первоначальный PEP-3103 был отвергнут.

Собственно, теперь всё на месте, осталось собрать:

[root@ansible devault]# time fpc devault.pas -Fudcpcrypt_2.1:dcpcrypt_2.1/Ciphers:dcpcrypt_2.1/Hashes -MOBJFPC

Здесь имейте в виду, что форматирование Хабра играет злую шутку - никакого разрыва строки после первого минуса нет.

Вывод компилятора
Free Pascal Compiler version 3.0.4 [2017/10/02] for x86_64Copyright (c) 1993-2017 by Florian Klaempfl and othersTarget OS: Linux for x86-64Compiling devault.pasCompiling ./dcpcrypt_2.1/DCPcrypt2.pasCompiling ./dcpcrypt_2.1/DCPbase64.pasCompiling ./dcpcrypt_2.1/Hashes/DCPsha256.pasCompiling ./dcpcrypt_2.1/DCPconst.pasCompiling ./dcpcrypt_2.1/Ciphers/DCPrijndael.pasCompiling ./dcpcrypt_2.1/DCPblockciphers.pasCompiling kdf.pasLinking devault/usr/bin/ld: warning: link.res contains output sections; did you forget -T?3784 lines compiled, 0.5 secreal    0m0.543suser    0m0.457ssys     0m0.084s

Вроде неплохо: 3,8 тысячи строк кода собраны до исполняемого файла за 0.6 сек. На выходе - статически связанный бинарник, которому для работы от системы требуется только ядро. Ну то есть для запуска достаточно просто скопировать этот бинарник в файловую систему - и всё. Кстати, я забыл указать его размер: 875К. Никаких зависимостей, компиляций по несколько минут и т.д.

Ах да, чуть не забыл самое интересное! Запускаем, предварительно сложив пароль в файл ".password":

[root@ansible devault]# ./devault -w .password -f vaulted.yml---collections:- name: community.general  scm: git  src: https://github.com/ansible-collections/community.general.git  version: 1.0.0

Вот такой нехитрый YaML я использовал в самом начале статьи для создания зашифрованного файла.

Исходный код для самостоятельного изучения можно взять здесь.

Хотите ещё Ansible? (осторожно, денежные вопросы!)

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

Если же хотите систематизировать и углубить свои знания Ansible - я провожу тренинги по Ansible, пишите мне в Telegram.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru