[Python, Информационная безопасность] Сказ о том, как я токен в Линуксе хранил
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
JSON Web Token — это открытый стандарт для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Wikipedia
Когда речь идёт о хранении sensitive data в браузере, достаточно воспользоваться одним из двух доступных вариантов: cookies или localStorage. Тут каждый выбирает по вкусу. Однако я посвятил эту статью Secret Service – службе, которая работает через D-Bus и предназначена для хранения «секретов» в Linux.У службы есть API, которым пользуется GNOME Keyring для хранения аутентификационных данных пользовательских приложений.Почему Secret ServiceДело в том, что я получал токен не в браузере. Я писал клиентскую аутентификацию для консольного приложения, похожую на ту, что используется в git.Вопрос о способе хранения реквизитов встал сразу, так как я не хотел вынуждать пользователей логиниться при очередном запуске моего приложения.Сначала был вариант хранить токен в зашифрованном файле, но он отпал сразу потому, что я догадывался, что мои функции шифрования и дешифровки будут велосипедом.Тогда я задумался о том, как хранит секреты Linux, и оказалось, что подобные механизмы реализованы и в других ОС.В итоге ключом доступа к токену будет служить пароль учетной записи пользователя Linux.Архитектура Secret Service вкратцеОсновная структура данных Secret Service — это коллекция элементов с атрибутами и секретом.КоллекцияЭто набор всевозможных аутентификационных данных. В системе используется коллекция по-умолчанию под псевдоним «default». В нее записываются все пользовательские приложения. Seahorse нам её покажет.
Как видно, в моё хранилище сохранились Google Chrome и VSCode. Сюда же будет сохраняться и моё приложение.Каждая такая запись называется элементом.ЭлементЧасть коллекции, хранящая атрибуты и секрет.АтрибутыПара вида ключ, значение, которая содержит название приложения и служит для идентификации элемента.
СекретНазвание говорит само за себя, но здесь хранятся различные структуры данных в байтовой репрезентации, содержащие пользовательскую почту, пароль и так далее.Алгоритм взаимодействия с Secret ServiceЯ долго думал над алгоритмом аутентификации, пока не набросал flow chart.
Определяем операторы
- «Токен в хранилище?» — функция и условие.
- «Извлечь токен из хранилища» — функция.
- «Запросить регистрационные данные у пользователя» — функция.
- «Запросить токен у API» — функция.
- «Сохранить токен в хранилище» — функция.
- «Использовать токен» — конец.
Реализация на PythonЯ решил попробовать Click Framework для создания CLI приложения.
import click
Он предоставляет декораторы для определения команд, опций, аргументов и так далее. Все команды приложения принято оборачивать в группу. Я сделал это для того, чтобы задать точку входа в скрипт.
@click.group()
def cli():
pass
Для логина в приложение я создам одноименную команду.В консоле это будет выглядеть так:
$ app login
Email:
Password:
Или так когда токен получен:
$ app login
Logged in!
Команда login
@cli.command(help="Login into your account.")
@click.option(
'--email',
prompt=True,
help='Registered email address.')
@click.option(
'--password',
prompt=True,
hide_input=True,
help='Password provided at registration.'
)
def login(email, password):
pass
if __name__ == '__main__':
cli()
Все декораторы применяются к функции login, которая пока не имплементирована, но принимает значения параметров email и password.Декоратор @cli.command добавляет команду в группу, а @click.option делает параметры функции опциями команды.В опции пароля, параметр hide_input скрывает символы при вводе в консоле.Параметр prompt принимает булевые значения, согласно которым Сlick Framework решает, запрашивать ли значение параметра у пользователя.С последним у меня проблемыЯ не могу просто присвоить ему True или False потому, что:
- в случае True Click Framework запрашивает опцию при каждом запуске. Мне это не подходит. Достаточно запросить почту и пароль при первом запуске, получить токен от WEB API и сохранить его в Secret Service, а в дальнейшем запрашивать токен у Secret Service API;
- В случае False Click Framework вообще не запрашивает опцию. Значит, я не смогу получить токен, если приложение запущено впервые и токен отсутствует в Secret Service.
РешениеМне нужна функция, которая примет решение и вернет соответствующее значение в переменную prompt_desicion. А зависит это решение от наличия или отсутствия токена в Secret Service. Отсюда следует, что функция подключается к Secret Service API и запрашивает токен.ПоследствияНа этом моменте я решил собрать всю аутентификационную логику в классе отдельного модуля. На мой взгляд, глобальные переменные, классы и не декорированные функции испортят читаемость в контексте паттерна Click Framework.Напротив, если основной модуль будет содержать код, исключительно относящийся к Click Framework, он будет выглядеть понятно и лаконично.В итоге модуль app содержит логику паттерна Click Framework. В него я буду импортировать модуль auth, в котором будет класс Auth с логикой аутентификации.
.
├── auth.py
└── app.py
Я буду хранить значение решения о запросе почты и пароля у пользователя в атрибуте prompt_desicion объекта auth класса Auth модуля auth.
@cli.command(help="Login into your account.")
@click.option(
'--email',
prompt=auth.prompt_desicion,
help='Registered email address.')
@click.option(
'--password',
prompt=auth.prompt_desicion,
hide_input=True,
help='Password provided at registration.'
)
def login(email, password):
pass
if __name__ == '__main__':
cli()
Пишем модуль аутентификацииДля Python доступен пакет SecreteStorage, который использует Secret Service API.Он оперирует основными понятиями службы и включает в себя два модуля, которые реализуют классы и функции доступа к основным объектам Secret Service.Здесь будет реализован класс доступа к Secret Service API и WEB API, определен атрибут prompt_desicion и соответствующий метод.Импортируем необходимые пакеты
- requests — для HTTP запросов к WEB API.
- secretstorage — для запросов к Secret Service API.
- json — для десериализации байтов в словарь.
import requests
import secretstorage
import json
Получаем секрет из Secrete Storage
class Auth:
def __init__(self, email=None, password=None):
# атрибуты, по которым осуществляется поиск элемента
# Secret Service
self._attributes = {'application': 'MyApp'}
# подключение к Dbus
self._connection = secretstorage.dbus_init()
# запрос коллекции по-умолчанию
self._collection = secretstorage.collection.get_default_collection(
self._connection
)
# запрос всех элементов коллекции с указанными атрибутами
self._items = self._collection.search_items(self._attributes)
# получение конечного атрибута
self._stored_secret = self.get_stored_secret()
На данном этапе я запросил нужный мне элемент коллекции.Критерием поиска для Secret Service служит атрибут self._attributes.О символе «_» в названиях атрибутовНижнее подчеркивание в названиях атрибутов означает, что к ним не предполагается обращаться извне. Впрочем, это не делает их недоступными для других объектов. Интерпретатор не изолирует их в отдельном пространстве имён, доступном только из области видимости объекта. Так обозначаются атрибуты, которые нигде, кроме самого объекта не используются. И это не более, чем солгашение.Стоит отметить, что заданному критерию поиска в коллекции может соответствовать не один элемент. По этой причине автор(ы) SecretStorage решил(и) возвращать генератор в ответ на поисковый запрос. Не вдаваясь в подробности, это означает, что необходимо итерировать по self._items в поисках нужного элемента.Я делаю это в методе get_stored_secret, который возвращает готовый секрет.
class Auth:
def get_stored_secret(self):
for item in self._items:
if item:
return json.loads(item.get_secret())
В цикле итератор становится экземпляром класса Item пакета secretstorage, поэтому на нём можно вызвать метод get_secret, который возвращает секрет.В условии можно указать дополнительный критерий отбора элемента. Но мне достаточно факта самого существования такового.Далее следует десериализация секрета и возвращение словаря.True или False — вот, в чём вопросВ отличие от источника, моя аллегория в заголовке носит менее риторический характер, и я смело могу ответить: «Для начала — False».
class Auth:
def __init__(self, email=None, password=None):
# все, что было написано до этого
self.prompt_desicion = False
Только постоянное изменяется; изменчивое подвергается не изменению, а только смене. Иммануил Кант
И Буль своей логикой нисколько не противоречит Канту. Возможно, Гамлету стоило взять это на вооружение.
class Auth:
def __init__(self, email=None, password=None):
# все, что было написано до этого
# если секрет получен
if self._stored_secret:
# он будет доступен в атрибуте token
self.token = self._stored_secret['token']
# если пароль и почта запрошены у пользователя
elif email and password:
# получить токен у WEB API
self.token = self.get_token(email, password)
# сохранить токен как актуальный
self._valid_secret = {'token': self.token}
# сохранить токен в Secret Service
self.set_stored_secret()
else:
# если токена нет в Secret Storage, нужно запросить почту и пароль
# пользователя
self.prompt_desicion = True
Таким образом объекты моего класса инициализируются по-разному в зависимости от наличия параметров почты и пароля.Инициализация без параметров
- Запросить токен у Secret Storage API.
- Если токен найден, не запрашивать почту и пароль у пользователя.
- Использовать токен из Secret Storage.
Инициализация с параметрами
- Запросить токен у Secret Storage API.
- Если токен найден, не запрашивать почту и пароль у пользователя и использовать токен из Secret Storage.
- Если токен не найден, принять решение о запросе почты и пароля у пользователя.
- Если пароль и почта предоставлены, запросить токен у WEB API.
- Если токен получен, сохранить его в Secret Storage.
Осталось реализовать методы сохранения токена в Secret Storage и запроса к WEB API.Получаем актуальный токен у WEB API
class Auth:
def get_token(self, email: str, password: str) -> str:
try:
response = requests.post(
API_URL,
data={
'email': email,
'passwd': password
})
data = response.json()
except requests.exceptions.ConnectionError:
raise requests.exceptions.ConnectionError()
if response.status_code != 200:
raise requests.exceptions.HTTPError(data['msg'])
return data['data']['token']
В константе API_URL располагается адрес моего API. По понятным причинам, я не могу его опубликовать. Однако, он возвращает токен при POST запросе с параметрами «email» и «passwd».Метод возвращает два исключения при ошибки подключения к API и во всех случаях, когда ответ API не содержит токена.Во всех таких случаях API возвращает объект «msg» с соответствующим сообщением из тела ответа. После сериализации в блоке try я могу просто выводить это сообщение в консоль.Сам токен хранится в объекте «data».Сохраняем токен в Secret Storage
class Auth:
def set_stored_secret(self):
self._collection.create_item(
'MyApp',
self._attributes,
bytes((json.dumps(self._valid_secret)), 'utf-8')
)
Чтобы сохранить секрет методу create_item нужно название сохраняемого элемента, его атрибуты и сам секрет в байтовой репрезентации.Используем модуль в Сlick FrameworkСперва импортируем модуль auth.
from auth import Auth
Затем инициализируем объект без параметров для проверки Secret Storage.
auth = Auth()
Передаем решение в декораторы опций.
@cli.command(help="Login into VPN Manager account.")
@click.option(
'--email',
prompt=auth.prompt_desicion,
help='Registered email address.')
@click.option(
'--password',
prompt=auth.prompt_desicion,
hide_input=True,
help='Password provided at registration.'
)
Дописываем команду login.
def login(email, password):
global auth
try:
# если было принято решение о запросе почты и пароля
if auth.prompt_desicion:
# получить новый токен и сохранить его в Secret Storage
auth = Auth(email, password)
except Exception:
return click.echo('No API connection')
# далее следует любая логика работы, связанная с токеном.
click.echo(auth.token)
Радуемся автоматически сгенерированной справкеClick Framework генерирует справку автоматически. Для этого ему нужны строки, которые я указывал в параметреhelpего декораторов.
$ python app.py
Usage: app.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
login Login into your account.
Справка команды login
$ python app.py login --help
Usage: app.py login [OPTIONS]
Login into your account
Options:
--email TEXT Registered email address
--password TEXT Password provided at registration
--help Show this message and exit.
Ссылки
- Secret Service API Draft
- Welcome to SecretStorage documentation!
- Click Framework
- Requests: HTTP for Humans™
===========
Источник:
habr.com
===========
Похожие новости:
- [Информационная безопасность, Разработка веб-сайтов, Облачные сервисы] Как ELK помогает инженерам по ИБ бороться с атаками на сайты и спать спокойно
- [Python] Мелкая питонячая радость #11: реактивное программирование, парсинг страниц и публикация моделей машинного обучения
- [Assembler, Информационная безопасность, Разработка под Windows, Реверс-инжиниринг] Пишем шеллкод под Windows на ассемблере
- [Высокая производительность, Python, Программирование, Машинное обучение] Deep Learning Inference Benchmark — измеряем скорость работы моделей глубокого обучения
- [Информационная безопасность, DevOps] Способы и примеры внедрения утилит для проверки безопасности Docker
- [Информационная безопасность, Big Data] Участники рынка больших данных в РФ хотят интегрировать данные частных компаний и ГИС
- [Информационная безопасность, Статистика в IT] С действиями кибермошенников сталкивались 86% россиян
- [Python, Браузеры, Занимательные задачки, Софт] Как я получил пожизненный запас чесночной пиццы с помощью Python и Selenium (перевод)
- [Информационная безопасность, Разработка веб-сайтов, Habr, Разработка под Linux] Секретная информация? Используй 2FA для VPS/VDS
- [Информационная безопасность] Шести россиянам предъявлены обвинения в атаках с использованием вируса NotPetya
Теги для поиска: #_python, #_informatsionnaja_bezopasnost (Информационная безопасность), #_python, #_jwt, #_token, #_click, #_python, #_informatsionnaja_bezopasnost (
Информационная безопасность
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:06
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
JSON Web Token — это открытый стандарт для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Wikipedia
Как видно, в моё хранилище сохранились Google Chrome и VSCode. Сюда же будет сохраняться и моё приложение.Каждая такая запись называется элементом.ЭлементЧасть коллекции, хранящая атрибуты и секрет.АтрибутыПара вида ключ, значение, которая содержит название приложения и служит для идентификации элемента. СекретНазвание говорит само за себя, но здесь хранятся различные структуры данных в байтовой репрезентации, содержащие пользовательскую почту, пароль и так далее.Алгоритм взаимодействия с Secret ServiceЯ долго думал над алгоритмом аутентификации, пока не набросал flow chart. Определяем операторы
import click
@click.group()
def cli(): pass $ app login
Email: Password: $ app login
Logged in! @cli.command(help="Login into your account.")
@click.option( '--email', prompt=True, help='Registered email address.') @click.option( '--password', prompt=True, hide_input=True, help='Password provided at registration.' ) def login(email, password): pass if __name__ == '__main__': cli()
.
├── auth.py └── app.py @cli.command(help="Login into your account.")
@click.option( '--email', prompt=auth.prompt_desicion, help='Registered email address.') @click.option( '--password', prompt=auth.prompt_desicion, hide_input=True, help='Password provided at registration.' ) def login(email, password): pass if __name__ == '__main__': cli()
import requests
import secretstorage import json class Auth:
def __init__(self, email=None, password=None): # атрибуты, по которым осуществляется поиск элемента # Secret Service self._attributes = {'application': 'MyApp'} # подключение к Dbus self._connection = secretstorage.dbus_init() # запрос коллекции по-умолчанию self._collection = secretstorage.collection.get_default_collection( self._connection ) # запрос всех элементов коллекции с указанными атрибутами self._items = self._collection.search_items(self._attributes) # получение конечного атрибута self._stored_secret = self.get_stored_secret() class Auth:
def get_stored_secret(self): for item in self._items: if item: return json.loads(item.get_secret()) class Auth:
def __init__(self, email=None, password=None): # все, что было написано до этого self.prompt_desicion = False Только постоянное изменяется; изменчивое подвергается не изменению, а только смене. Иммануил Кант
class Auth:
def __init__(self, email=None, password=None): # все, что было написано до этого # если секрет получен if self._stored_secret: # он будет доступен в атрибуте token self.token = self._stored_secret['token'] # если пароль и почта запрошены у пользователя elif email and password: # получить токен у WEB API self.token = self.get_token(email, password) # сохранить токен как актуальный self._valid_secret = {'token': self.token} # сохранить токен в Secret Service self.set_stored_secret() else: # если токена нет в Secret Storage, нужно запросить почту и пароль # пользователя self.prompt_desicion = True
class Auth:
def get_token(self, email: str, password: str) -> str: try: response = requests.post( API_URL, data={ 'email': email, 'passwd': password }) data = response.json() except requests.exceptions.ConnectionError: raise requests.exceptions.ConnectionError() if response.status_code != 200: raise requests.exceptions.HTTPError(data['msg']) return data['data']['token'] class Auth:
def set_stored_secret(self): self._collection.create_item( 'MyApp', self._attributes, bytes((json.dumps(self._valid_secret)), 'utf-8') ) from auth import Auth
auth = Auth()
@cli.command(help="Login into VPN Manager account.")
@click.option( '--email', prompt=auth.prompt_desicion, help='Registered email address.') @click.option( '--password', prompt=auth.prompt_desicion, hide_input=True, help='Password provided at registration.' ) def login(email, password):
global auth try: # если было принято решение о запросе почты и пароля if auth.prompt_desicion: # получить новый токен и сохранить его в Secret Storage auth = Auth(email, password) except Exception: return click.echo('No API connection') # далее следует любая логика работы, связанная с токеном. click.echo(auth.token) $ python app.py
Usage: app.py [OPTIONS] COMMAND [ARGS]... Options: --help Show this message and exit. Commands: login Login into your account. $ python app.py login --help
Usage: app.py login [OPTIONS] Login into your account Options: --email TEXT Registered email address --password TEXT Password provided at registration --help Show this message and exit.
=========== Источник: habr.com =========== Похожие новости:
Информационная безопасность ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 23:06
Часовой пояс: UTC + 5