Как я делал веб-версию KeePass
Как-то мне надо было добавить в админку просмотр списка паролей. База хранилась на сервере в формате KeePass (kdbx v2), сервер был на ноде — недолго думая, я взял первый попавшийся пакет и сделал. А потом понадобилось то же самое, но прямо у пользователя в браузере, без сервера. Ничего не нашлось. Первым желанием было форкнуть либу и заменить использование node api, но от первого просмотра кода желание пропало, решил сделать сам.
Под катом расскажу о проблемах, с которыми я столкнулся, и способах их решения
Пришлось изучить формат kdbx, не описанный, к сожалению, нигде кроме исходников и этой небольшой статьи. Но зато он достаточно прост. Чтобы прочитать kdbx, нужно:
- прочитать бинарный заголовок (что в нём?)
- инициализировать алгоритм шифрования и генератор случайных значений (salsa-20)
- посчитать хэш пароля и ключа, получить credentials hash
- N раз (значение из заголовка) зашифровать хэш, получив мастер-ключ — это самая вычислительно трудоёмкая операция
- дешифровать данные
- проверить корректность расшифровки, сравнив блок данных с блоком из заголовка
- разGZIPать данные
- распарсить xml
- сгенерировать соль для защищённых полей
- по требованию, когда они нужны, теперь можно расXORить защищённые поля
- прочитать xml-метаданные (что в них?)
- прочитать группы и записи
Из алгоритмов там используются AES для симметричного шифрования и SHA256 для хеширования. Самая быстрая реализация для web, которую я нашёл — это asmCrypto, она написана на asm.js, работает быстрее остальных браузерных альтернатив и, что немаловажно, умеет собираться по кусочкам, включая только нужные модули с алгоритмами. Пропатчив её так, чтобы все циклы трансформации ключа происходили внутри asm.js, без дополнительного копирования данных, мне удалось достичь скорости в 4..7 раз меньшей, чем у native-реализации (если в KeePass поставить задержку в 1 секунду, откроется файл секунд за 6). С дефолтными настройками время открытия файла в среднем около 200мс. WebAssembly нас всех спасёт, а пока что вот так.
Для работы с gzip взял pako — он маленький, быстрый и MIT. Сравнительно с шифрованием, значимых затрат на декомпрессии не заметил.
В результате получилась kdbxweb, которая работает в node.js и современных браузерах. Из-за необходимости работать с бинарным форматом, в списке поддерживаемых браузеров нет старых версий. Библиотека+зависимости получились в 150кб.
Итак, либа есть. А почему бы теперь не написать веб-интерфейс для неё? Хороший менеджер паролей для веба должен:
- быть одиним файлом, чтобы можно было взять и, скажем, положить себе в дропбокс, на хостинг или даже на диск
- не требовать абсолютно никакого сервера, ничего никуда не пересылать, работать в браузере
- занимать меньше 1МБ (условно; важен порядок на самом деле)
- уметь работать из файла и оффлайн
- быстро загружаться
- работать во всех браузерах
- не хранить в памяти пароли в открытом виде
- синкаться с дропбоксом
- использовать существующий формат, не изобретать велосипед
- так или иначе поддерживать все фичи формата
- работать с несколькими файлами/базами одновременно
- быть обратно-совместимым с оригинальным приложением
- быть опен-сорсным
Большинство из этого удалось достичь.
Приложение написал на backbone+zepto (сначала хотел попробовать Angular2, но как-то не пошло, сгенерированный код самого фреймворка пока что получается размером более 1MB, наверное ещё слишком бета и потом будет лучше).
Версии браузеров, поддерживающих читалку формата, не так далеки от последних, поэтому кроссбраузерных костылей практически нет.
Когда вы вводите текст в любой инпут, все вводимые значения даже с историей ввода могут сохраняться в памяти браузера бесконечно долго. И если в десктоп-приложении можно почистить память, то в браузере у вас нет контроля за памятью, занимаемой строками: когда GC захочет, тогда и почистит, при этом память он может занулить, а может и нет:
Некоторые приложения считают, что память можно не чистить даже после закрытия файла, хотя для приложений это, конечно, не так критично, как для веба:
Так не пойдёт, если в приложении ещё куда ни шло, то в браузере я бы в такой инпут мастер-пароль вводить не стал, поэтому пришлось сделать велосипед: при вводе символы XOR-ятся с рандомной последовательностью, подобранной так, чтобы на выходе получились символы некоего неиспользуемого блока unicode. Их и показывает инпут, думая, что это значение (всё равно там звёздочки), само значение при этом посимвольно сохраняется в буфер. Сложить значение с ключом, имея полный доступ к памяти, конечно, можно, но это уже другой уровень сложности:
Сохранить файл, сгенерированный в браузере, не так-то просто, но нужно:
Есть FileSaver.js, но в нём засада в Safari, вот тред с интересными комментариями, где автор просит купить ему макбук. Почти все браузеры сейчас поддерживают
<a href="" download="file-name.ext">
Но только не Сафари. Сафари упорно не хочет ни понимать атрибут download (чувсвтва по этому поводу можно излить на webkit.org), ни скачивать Blob, жалуясь на то, что это якобы небезопасно. Но если законвертировать файл в base64, скачать его таки можно. Отправил автору патч, теперь в сафари FileSaver работает, но имя файла всё равно осталось Unknown. Если кто-то знает способ сделать лучше — расскажите, пожалуйста.
Работать, каждый раз скачивая файл, неудобно, собрал приложения на electron. Работает он шустро, API отлично описанное и интуитивно понятное, проблем с ним не заметил вообще:
Авто-апдейтер пока только под мак, но в будущем скорее всего его допишут и под другие ос. Как и для node-webkit, для электрона уже появились electron-builder и electron-packager, которыми я и воспользовался для сборки приложения и инсталлятора.
Конфиг для сборки инсталлятора, который делает dmg для mac os x и exe installer для windows (сборку deb для linux пока они не поддерживают):
{
"osx" : {
"title": "KeeWeb",
"background": "../graphics/dmg-bg.png",
"icon": "../graphics/app.icns",
"icon-size": 80,
"contents": [
{ "x": 438, "y": 344, "type": "link", "path": "/Applications" },
{ "x": 192, "y": 344, "type": "file" }
]
},
"win" : {
"title": "KeeWeb",
"icon": "graphics/app.ico"
}
}
Electron добавляет приятную возможность получить путь открываемых файлов. Когда в приложение тащат файл или открывают его из инпута, в File добавляется свойство path, в котором содержится полный путь к файлу. Сохранить файл можно потом как обычно в ноде, fs.writeFile.
В KeePass есть много иконок со странными названиями в енуме. Но во-первых, они немного устарели для 2015, а во-вторых, растровую графику использовать не хотелось, чтобы сохранить приложение маленьким. Пришлось составить соответствие вручную, к счастью, в font awesome нашлось абсолютно всё:
Формат позволяет задать цвет записи и цвет фона, что KeePass и использует, однако я считаю возможность выбирать произвольный цвет текста и фона самому из колор-пикера не лучшим решением. Цвет записи нужен для отнесения записи к какой-любо логической группе (например, зелёные — деньги) и быстрого их поиска — чтобы отмеченные бросались в глаза и были быстро доступны.
Поэтому в приложении цвет используется как пометка звёздочкой, но вместо жёлтой звезды можно выбрать звезду любого цвета:
В случае, когда в импортитуемом файле есть цвета, алгоритм вычисляет ближайший по схожести тона, Hue. Обратно в KeePass цвета экспортируются как светлый фон выбранного оттенка.
Приятно удивился, что для подключения к дропбоксу из SPA через dropbox-js теперь не надо встраивать «зашифрованный» secret key в приложение, как это было раньше. Он не так и не научился отслеживать закрытие вкладки в popup-авторизации, но его легко научить (но надо патчить либу).
API копирования заработало в браузерах не так давно: в chrome оно появилось летом, в firefox — осенью этого года, в IE было раньше, но с вопросом пользователю. В сафари его до сих пор нет, а копировать текст как-то хочется. Особенно в мобильном сафари. Но если копировать там и нельзя, то можно показать пользователю баббл Copy, сделав прозрачный input. В экран ещё раз тыкнуть придётся, но всё-таки уже не так болезненно, как выделение и копирование:
Если человек открыл несколько файлов, скорее всего, он хочет, чтобы поиск был по ним всем. Поэтому поиск, фильтры, сортировка и корзина сделаны общими. Фильтры расположены в порядке востребованности пользователем. Сначала All Items (он даже доступен по шорткату Ctrl/Cmd-A, т.к. чаще всего хочется показать всё и просто найти в поиске необходимое), потом цвета-фавориты, за ними теги, структура и уже совсем внизу мусорка:
Поиск умеет работать по всему: поля, теги, вложения. В списке записей, при сортировке по разным полям, на второй строке показывается поле, по которому сортировали:
Формат поддерживает файлы без корзины, однако в UI корзина всё время есть. Это сделано затем, чтобы показать её отдельно от списка групп, одну на все файлы. В списке обычных групп корзине делать нечего, она должна быть особенной.
Когда я рассказал о приложении на sourceforge в форуме KeePass, Dominik Reichl (разработчик KeePass) написал мне на почту, что получилось хорошо, предложил добавить приложение на страничку неофициальных клиентов, и попросил поменять название, что я и сделал.
В приложении пока что встречаются баги, которые я постепенно вылавливаю. Используйте на свой страх и риск, делайте бэкапы. В крайнем случае есть экспорт в XML, где можно посмотреть, что пошло не так. Многое пока не сделано, но вообще идея приложения мне понравилась, я собираюсь его развивать и поддерживать.
Все компоненты приложения, как и оно само, доступны под MIT.
keeweb — веб-приложение
releases — десктоп-релиз
github — код
features — фичи на одной страничке в виде картинок
kdbxweb — читалка kdbx для браузеров и node.js