Pull to refresh

Безопасный менеджер паролей или выстрели в колено пользователю

Level of difficultyMedium
Reading time8 min
Views4.2K

Вступление

Всем привет!

Сразу скажу: я не собираюсь претендовать на звание самого умного, просто хочу высказать ряд своих мыслей, которые являются ИМХО. Если вы с чем-то не согласны, прошу в комментарии, обсудим.

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

Популярные менеджеры паролей

1) KeePass - данный менеджер паролей идет первым почти во всех обзорах. Но при этом, на мой взгляд имеет проблемы, основные из них:

Во-первых, все данные хранятся локально. Кто-то может сказать, что это не проблема, ведь данные зашифрованы, а для доступа к ним требуется пароль. Но на мой взгляд, хранить данные локально - самая большая ошибка, даже в зашифрованном виде: например, в Windows, можно утащить почти любой файл, хранящийся в системе, а при использовании мастер-пароля мало кто будет делать его слишком сложным, ведь тогда его придется хранить где-то еще.

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

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

2) Bitwarden - облачный менеджер паролей, сервер которого можно развернуть локально. Вроде бы все в нем идеально: сквозное шифрование, двухфакторная аутентификация. Но есть три "но":

Первое и второе "но" точно такие же, как во втором и третьем пунктах предыдущего менеджера, так что не буду повторяться.

Третье "но" относится к тому, что можно легко получить доступ к паролям с различных устройств, а не только с того, с которого были пароли созданы. Да, можно настроить 2FA (которая не является обязательной), но что, если сольют базу данных самого Bitwarden? Зависит от типа 2FA, но в случае, например, приложений-аутентификаторов (например Google Authentificator), до смены сервера 2FA ваш аккаунт, возможно, будет защищен только мастер-паролем.

Так что же я считаю по-настоящему безопасным менеджером паролей?

Сейчас будет рассказана утопическая ситуация, с которой кто-то посмеется, кто-то поплачет, кто-то скажет, что я сошел с ума, но приступим:

  1. Хранение паролей в облаке, причем пароли защищены сквозным шифрованием.

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

  3. Отсутствие мастер-паролей для входа в систему.

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

  5. Менеджер паролей не должен раскрывать список ваших сервисов и логинов на них.

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

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

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

Алгоритмы реализации менеджера паролей

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

1) Hardware info - строка, содержащая данные об устройстве (такие как наименование процессора, видеокарты. объем оперативной памяти, серийные номера накопителей и т.д.)

2) Hardware key - ключ, генерируемый на основании данных об устройстве. Например, можно вычислять его так: SHA512(Hardware info)

3) Software key - информация о текущей версии клиентского ПО (например, контрольная сумма файлов приложения)

Зная три этих основных определения можно перейти непосредственно к самим алгоритмам.

1) Регистрация пользователя (устройства)

a) На сервер отправляется hardware key устройства.
b) Сервер генерирует случайную 512-битовую последовательность (regSalt).
c) Сервер вычисляет totpKey = SHA512(hardware key | regSalt) и заносится в базу данных для 2FA.
d) Сервер возвращает клиенту totpKey, который пользователь должен где-то надежно сохранить, чтобы в дальнейшем генерировать одноразовые коды для входа в систему.

2) Аутентификация пользователя (устройства)

a) Пользователь вводит одноразовый код, который отправляется на сервер вместе с hardware key
b) Сервер ищет в базе данный hardware key и сравнивает текущее значение одноразового кода (totp) для totpKey с пришедшим от клиента.
c) В случае, если одноразовые коды совпали, сервер возвращает клиенту два JWT-токена: один для осуществления действий с паролями, второй - для повторной аутентификации по истечению времени первого токена.

В пунктах 1 и 2 аутентификация реализована таким образом, что пользователю не нужно иметь мастер-ключ для аутентификации в сервисе.

3) Создание/редактирование пароля на сервере

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

Сервер на все входящие запросы в данном блоке проверяет валидность токена.

1) Клиент отправляет на сервер информацию о сервисе (название), для которого производится получение пароля.
2) Сервер возвращает соль (salt), принадлежащую данному сервису (псевдослучайное 512- битовое значение, генерирующееся в момент регистрации сервиса, т.е. имеющее постоянное значение).
3) Клиент вычисляет идентификатор пароля следующим образом (login - логин пользователя на сервисе):

Id_{pass} = SHA512(salt|login|hardware\ key)

4) Клиент генерирует модуль для алгоритма RSA2048 на основании идентификатора пароля (модуль известен серверу, потому что он может проделать все те же самые действия для идентификатора пароля):

num = PBKDF2(SHA512,Id_{pass}  [1:448], Id_{pass}  [449:512],200,1024)\ mod (p \cdot q)

, где num - последовательно генерируемые числа (последовательности бит). Произведение первых двух различных простых чисел будет являться модулем.
5) Клиент рассчитывает открытую экспоненту e (которая известна только ему):

a) Вычисляется значение, на основании которого будет генерироваться открытая экспонента:

Hash_e = SHA512(login | hardware\ info |software\ key)

Заметим, что hardware info, как и login неизвестны серверу.

b) Вычисляется открытая экспонента e:

e = PBKDF2(SHA512,Hash_e  [1:448],Hash_e  [449:512],2000,2048)\ mod (p \cdot q)

c) Если НОД(e, (p-1)*(q-1)) != 1, берем следующие генерируемые 2048 бит до тех пор, пока НОД не станет равным 1.

6) Вычисляется зашифрованный пароль:

EncPass = password^e\ mod\ (p \cdot q)

7) Вычисляется дополнительное открытое основание totpE (напомню, что totp хранится в зашифрованном виде в токене):

Hash_{totp} = SHA512(totp\ |Id_{pass})

После чего вычисляется totpE, аналогично с вычислением открытого основания, которое знает только клиент.

8) На сервер отправляется тройка:

(Id_{pass}, EncPass^{totpE}\ mod\ (p \cdot q), software\ key)

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

9) Сервер получает тройку данных, вычисляет totpD такое, что:

totpE \cdot totpD \equiv 1\ mod((p-1) \cdot (q-1))

И вычисляет зашифрованный пароль:

EncPass = (EncPass^{totpE})^{totpD}\ mod\ (p \cdot q)

После этого сервер заносит в базу тройку:

(Id_{pass}, EncPass, software\ key)

Дополнительное шифрование необходимо для того, чтобы гарантировать защиту от атаки MITM, в случае, если у нарушителя есть доступ к hardware info (например, если компьютер пользователя заражен): пароли дополнительно шифруются с помощью RSA с тем же модулем, но с другими открытой и закрытой экспонентами, которые генерируются с помощью одноразового кода, который был введен при входе в систему в данном сеансе. Чтобы нельзя было быстро перебрать все открытые экспоненты для каждого значения totpKey (а их всего миллион), используется алгоритм PBKDF2, который сильно замедляет скорость перебора ключей.

Но в использовании PBKDF2 есть и проблема: в среднем на генерацию одной пары p и q данным алгоритмом требуется 2-4 секунды. Соответственно это еще один выстрел пользователю в колено: можно ожидать момента создания/получения пароля несколько эти 3-5 секунд при условии генерации ключа на клиенте во время ожидания ответа от сервера.

4) Получение пароля с сервера:

В каком-то роде для получения пароля с сервера выполняются все те же самые действия, что для его создания или редактирования.

Напомню, что в данном случае сервер всегда проверяет валидность токена при входящих запросах (производит авторизацию пользователя)

1) Клиент отправляет запрос на сервер с указанием идентификатора пароля.

2) Сервер ищет в базе запись с данным идентификатором. Если записи не найдено, возвращает Not Found, иначе переходит на следующий шаг.

3) Сервер генерирует ключ для дополнительного шифрования с помощью RSA. Модуль вычисляется также, как в предыдущем разделе, а открытая экспонента следующим образом:

Hash_{totp} = SHA512(totp\ |Id_{pass})

После чего вычисляет с помощью PBKDF2 открытую экспоненту также, как в предыдущем пункте.

4) Сервер отправляет клиенту следующую пару данных:

(EncPass^{totpE}\ mod\ (p \cdot q), software\ key)

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

5) Клиент вычисляет модуль, открытое основание и totpE (у клиента totp хранится в оперативной памяти после успешной аутентификации на сервере). После этого клиент вычисляет закрытое основание d такое, что:

(totpE \cdot e) \cdot d \equiv 1\ mod ((p-1) \cdot (q-1))

6) Клиент вычисляет пароль возведением полученного дважды зашифрованного значения в степень d:

password = (EncPass^{totpE})^d \equiv password^{e \cdot totpE \cdot d} \equiv password\ mod (p \cdot q)

7) В случае, если пароль был создан более ранней версией программы, клиент обновляет пароль на сервере.

Что дальше?

В дальнейшем я собираюсь собрать эту систему воедино, написав сервер на ASP.NET Core и десктопное приложение на WPF, после чего выложить их в open-source.Сервер по большей части уже завершен, следующая часть будет посвящена ему.

Заключение

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

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

2) Труднодоступность получения пароля нарушителем в случае успешной аутентификации. Для получения пароля обязательно необходимо иметь доступ к устройству пользователя, иначе дешифровать пароль будет достаточно проблематично (сервер не знает открытую/закрытую экспоненту ключа). Поэтому вкупе с первым фактором, нарушителю для получения пароля требуется доступ к двум из трех устройств в системе. Также нарушителю необходимо знать название сервиса, для которого необходимо получить пароль, и логин пользователя для данного сервиса.

3) Усложнение атаки man-in-the-middle. Тут зависит от возможности доступа нарушителя к устройству пользователя: если к нему есть доступ (например стоит вредоносное ПО, которое получает данные об устройстве), то дополнительное шифрование с помощью totp помогает защитить пароль в случае раскрытия только сообщения с паролем. В случае, если нарушитель видит весь трафик между устройством пользователя и сервером, то предпринятые меры не являются достаточными: как одноразовый totp-код, так и идентификатор пароля будут известны нарушителю.

4) Защита от получения паролей владельцем сервера: несмотря на то, что серверу известен модуль ключа RSA, ему не известна открытая или закрытая экспонента, поэтому дешифровать пароль не представляется возможным.

5) Защита от получения списка сервисов и логинов пользователя. Как было сказано в тексте статьи это невозможно по двум причинам: пароли хранятся непривязанными к конкретному пользователю. Информация о логине и сервисе для конкретного сервиса являются лишь частью хэшируемых данных, поэтому даже зная идентификатор пароля не представляется возможным получить первые.

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

Tags:
Hubs:
-10
Comments34

Articles