Недавно в блоге у Вастрика вышла статья про End-to-end Шифрование, при этом у нас примерно в тоже время появился - Менеджер паролей.
Кажется это идеальное время рассказать про то почему мы его сделали и о его устройстве.
Что такое Tuna я уже ранее рассказывал тут, но напомню в 2х словах. Изначально мы делали сервис реверс-прокси туннелей, а-ля Ngrok, но как видите не смогли остановиться только на туннелях.
Нафига мы сделали менеджер паролей ?
Мы много работали по фрилансу в разных компаниях, поэтому есть насмотренность как люди меняются корпоративными паролями к сервисам, инфраструктуре и прочему. Ну так вот, если в компании нет купленного 1Password или чего-то подобного (что там Дудь рекламирует), то это просто аншлаг.
- Нет единого хранилища, чаще всего это просто какой-то 1 ответственный человек, если он уйдёт, то всё...
- Пароли передают в чатиках Tg или Slack
- При увольнении сотрудника, если есть какая то общая учётка от внешнего сервиса, приходится почти рассылать новый пароль всем.
Там ещё скоуп проблем, но это основное.
В общем посмотрели мы на это всё и подумали, что у нас 99% пользователей это разработчики, наверняка у них та же самая проблема. Есть потребность хранить какие то инфраструктурные пароли, SSH ключи, офисный WI-FI и так далее. Но не всегда есть доступные инструменты для безопасного и удобного способа делиться ими с командой, заказчиками, аудиторами или просто друзьями.
Так вот и решили, что надо - ну и сделали.
Сколько стоит ?
Мы решили, что это будет базовым бесплатным функционалом в рамках нашей платформы. Основным продуктом пока так и остаются туннели.
Безопасность - это базовая потребность (даже по пирамиде Маслоу), и мы хотим, чтобы даже самые маленькие стартапы на 5 человек могли себе позволить безопасно хранить и обмениваться паролями в компании или друзьями.
Возможно позже мы введём какие то разумные ограничения или новые возможности которые будут доступны только с подпиской, но пока весь функционал доступен бесплатно всем. Вы можете зарегистрироваться, создать Команду, пригласить пользователей и завести общие каталоги с паролями без оформления подписки.
Архитектура
Понятное дело, что пароли это очень важно и чувствительно и тут не всё так просто. Важно учитывать, что компрометация нашей инфраструктуры не должна приводить к компрометации паролей пользователей, а значит - мы ничего не должны знать. Исследовав как работают другие менеджеры паролей, оказалось, что основной принцип модели нулевого знания заключается в том, чтобы всё делать на клиенте и никогда и ничего не передавать на сервер в открытом виде.
Что ж, ничего сложного, генерируем на клиенте какой-то ключ показываем его пользователю, нигде не сохраняем, этим ключом пользователь на клиенте шифрует пароли, в зашифрованном виде передаёт на сервер, мы храним. Когда пользователь хочет прочитать пароль, отдаём его, пользователь расшифровывает этот пароль своим ключом который есть только у него - профит.
Кажется всё просто, но сложность начинается когда нам надо не только хранить пароли одного пользователя, а иметь какие то общие каталоги в командах и делиться паролями наружу одноразовой ссылкой. Тут то мы и начинаем замешивать симметричные и асимметричные ключи, шифровать одно другим и вообще становится не так всё просто.
Общие каталоги в команде
Вообще в этом параграфе мы разберём почти полное устройство, так как будем распутывать клубок из конца в начало.
Начнём с задачи - иметь общие каталоги с паролями в команде. Из статьи Вастрика мы помним, что тут 2 пути:
- Пароли должны быть зашифрованы 1 ключом который есть у всех.
- При добавлении нового пользователя мы дублируем все пароли и шифруем его публичным ключом.
Второй поход нам не подходит, так как пользователь 1 должен иметь возможность редактировать пароль, а пользователь 2 должен сразу это видеть и наоборот. Значит - пароли должны быть зашифрованы одним ключом который есть у всех. Как такое провернуть? А вот так:
В итоге пароли шифруются симметричным AES ключом, назовём его - AES-ключ каталога. Но не храним же мы его в открытом виде? Верно, этот AES-ключ каталога на самом деле зашифрован RSA-ключом пользователя.
Распутываем клубок ...
Когда вы инициируете менеджер паролей, для вашего пользователя генерируется ассиметричный RSA ключ, его приватная часть шифруется Мастер-ключом, а публичная сохраняетися в открытом виде.
Итого, пользователь хранит:
- свой Мастер-ключ
Итого, мы храним:
- публичный RSA-ключ
- зашифрованный приватный RSA-ключ
- зашифрованный AES-ключ каталога
- зашифрованный пароли
Давайте попробуем посмотреть картину в целом и собрать в кучу весь алгоритм что там вообще происходит ?
Если коротко, то основная концепция в том, что на стороне сервера если что-то и хранится, то исключительно в зашифрованном виде, а на клиенте в открытом виде всё может существовать только в Runtime и перманентно тоже нигде не хранится.
Чтобы проще это понять нужно пройтись по шагам:
- Пользователь инициирует менеджер паролей
- На клиенте (браузер) генерируется пара RSA-ключей приватный и публичный.
- На клиенте генерируется Мастер-ключ, пользователь должен сохранить его себе
- На клиенте приватный RSA-ключ шифруется Мастер-ключом
- На сервер передаётся зашифрованный приватный RSA-ключ и публичный RSA-ключ
- Пользователь создаёт новый каталог
- На клиенте генерируется AES-ключ каталога
- На клиенте AES-ключ каталога шифруется публичным RSA-ключом
- На клиенте пароли шифруются AES-ключом каталога
Когда нам надо сделать пароли общими, мы находим пользователя в команде, берём его публичный RSA-ключ, берём AES-ключ каталога расшифровываем его своим приватным RSA-ключом, шифруем публичным RSA-ключом нового пользователя и отправляем на сервер, на сервере у каталога появляется новый пользователь с AES-ключом каталога и новый пользователь теперь может расшифровать его своим приватным RSA-ключом, а затем и пароли зашифрованные AES-ключом каталога.
В итоге получается примерно вот такая колбаса:
Фух ...
Делимся паролями наружу
Если с пользователями внутри команды разобрались, то как делиться паролями наружу, ведь у незарегистрированных пользователей нет RSA-ключей и они вообще ничего не знают?
Тут мы приходим к тому нужно сделать копию пароля. Снова генерируем AES-ключ но в этот раз он будет только для 1 пароля. Зашифрованному паролю присваиваем UID и отправляем это на сервер.
Что знает клиент:
- UID пароля
- AES-ключ которым он зашифрован
Что знает сервер:
- UID пароля
- Зашифрованный пароль
Далее клиент генерирует ссылку, где часть URI- это UID пароля, а другая AES-ключ:
https://my.tuna.am/password#2tIrpHCLZNoQGHFhRDIiLkInitp_aZh4Iv4OtO3SROk7FXdaib4yCyNP0zL/O1//t6k36G0=
Ссылка выше с пасхалкой, действует 30 дней, переходите
Соответственно у себя в базе мы не храним AES-ключ и не можем расшифровать пароль, но пользователь, что перейдёт по ссылке без труда его расшифрует и посмотрит.
Да, формально мы можем расшифровать пароль в момент 1-го же обращения, так как в запросе мы увидим полную ссылку, но делать этого мы конечно не будем. В целом есть идеи как улучшить этот момент, но пока всё на этапе обсуждения. Тем не менее именно в базе данных всё зашифровано.
Думаю тут можно завершить с архитектурой и перейти дальше.
Какие планы ?
Мы уже получили обратную связь от CTO одного из наших клиентов, с просьбой добавить поддержку TOPT (Time-based one-time Password), думаю добавим в ближайшем релизе. Это актуально если в компании есть какой-то сервис с одним аккаунтом и вход там с подтверждением через TOPT.
Думаем как сделать функционал Делимся паролями наружу более безопасным, но при этом таким же удобным.
Также есть планы внедрить работу с паролями в консольную утилиту tuna
чтобы можно было получать пароли в консоли.
В целом продукт ещё в стадии Бета и мы активно собираем обратную связь.
Пожалуй тут и закончим, с удовольствием отвечу на вопросы в комментариях 🤗.
Классно, что пилите такой продукт, но мне как пользователю интересно, почему это решение лучше, чем развернуть Bitwarden на своём сервере?