bitwarden не надёжен, но пользоваться можно
Этот пост про потерю данных, а не про конфиденциальность.
tl;dr
Я пользовался менеджером паролей SafeInCloud и перешел на Bitwarden.
Импорт теряет пароли и обрезает длинные урлы. Тестов почти нет. Баги есть.
Почему перехожу
Есть пост на хабре “Извлечение мастер-пароля из заблокированного менеджера паролей SafeInCloud”. Автор SafeInCloud воспринял критику негативно. Отрицать проблемы - это плохой выход. Хороший выход - написать официальный ответ на пост. Нужно признать проблему и опубликовать способ решения. Автор этого не сделал.
Следующая причина перехода - opensource. У bitwarden есть проблемы и их видно в коде. За 3 года использования SafeInCloud я не замечал багов. Но это не значит что багов нет - просто они не доступны для аудита
Аудит безопасности (не мой)
В октябре 2018 года компания Cure53 провела аудит кода Bitwarden. Автор Bitwarden не скрывает проблемы и публикует pdf отчет на своем cdn. Честность - это хорошо. Отчет подробный, даже куски кода с подсветкой проблем указаны. Его можно читать не только экспертам по безопасности, он написан для людей. Я не нашел причин не использовать bitwarden.
История моего перехода на bitwarden
Bitwarden умеет импортировать базу SafeInCloud в формате xml. Я экспортировал базу через десктопное приложение SafeInCloud и импортировал в консольной версии Bitwarden. Bitwarden немного подумал и без ошибок завершил процесс. Я проверил несколько из своих записей и успокоился. Прошло несколько дней и я решил авторизоваться в гитхабе. Нашел запись github, но в ней не было пароля. Проверил другие записи и там тоже были утеряны пароли. Решил изучить исходники. Я ни когда не писал на typescript, но на поиск ошибки ушло менее 5ти минут. Вот строки с ошибкой:
src/importers/safeInCloudXmlImporter.ts#L72
Array.from(this.querySelectorAllDirectChild(cardEl, 'field')).forEach((fieldEl) => {
const text = fieldEl.textContent;
if (this.isNullOrWhitespace(text)) {
return;
}
const name = fieldEl.getAttribute('name');
const fieldType = this.getValueOrDefault(fieldEl.getAttribute('type'), '').toLowerCase();
if (fieldType === 'login') {
cipher.login.username = text;
} else if (fieldType === 'password') {
cipher.login.password = text;
}
// удалил код, который не относится к ошибке
else {
this.processKvp(cipher, name, text);
}
});
В цикле идём по всем полям записи из SafeInCloud. Если тип записи password - происходит присвоение переменной cipher.login.password
. Если запись имеет 2 поля с типом password, сначала запишется первое, потом второе его сотрёт. Вот именно это и произошло. У меня было поле Password(тип password) и поле Token(тип password). Password потерялся, на его место записался token. Такое произошло для каждой записи, где кроме пароля есть другие поля.
Я не обнаружил ошибку сразу и возникли проблемы. Я мог бы написать свой конвертер и перенести пароли без проблем. Но за несколько дней я сменил в “битой” базе много паролей к разным записям. Нужно было как то синхронизировать “битый” импорт и старую базу SafeInCloud.
Для сравнения разъехавшихся баз я решил использовать diff в intellij idea. Написал скрипт для приведения разных баз к одинаковому виду. Строка начинается с названия записи. Дальше идут отсортированные пароли через разделитель. В конце строки печатается notes. В таком виде хорошо видно какие записи обновлялись. Результат этого сравнения я обрабатываю вручную.
Я написал свой скрипт конвертации базы паролей с учетом всех недостатков safeInCloudXmlImporter из bitwarden/jslib
.
Мой скрипт импорта
https://github.com/d10xa/safeincloudxml-to-bitwardenjson
У меня нет организации и перенос папок в коллекции я не делал (и не разбирался что это значит)
if (this.organization) {
this.moveFoldersToCollections(result);
}
Файлы, картинки и иконки я не переносил (Но, так как я сохраняю id записей, я смогу их дописать при необходимости)
Идентификаторы директорий я беру из предыдущего бэкапа, но к сожалению bitwarden их игнорирует и создает новые директории.
Если есть поле больше 500 символов - скрипт не будет его обрезать а упадёт с ошибкой.
Валидация моего импорта
Для проверки моего скрипта я написал тест.
Проверяю вот что:
- password
- username
- uri
- Два поля с одинаковым типом
- Два поля с одинаковым именем
- Заметки
- Директории
Такого простого теста не достаточно. Нужно валидировать результат внимательным чтением.
Конвертация, которой можно доверять без дополнительных проверок выглядит так:
toSafeInCloud(toBitwarden(safeInCloudDatabase)) == safeInCloudDatabase
Такое преобразование не работает. Есть информация, которая теряется при переходах (не куда записать значения в БД bitwarden):
- Имя поля. В SafeInCloud поле может называться password или code, а в Bitwarden я его сохраняю по пути
cipher.login.password
. Поля login или email пишу вcipher.login.username
- Шаблоны
- Примеры
- Картинки
- Файлы
- websiteIcon
- customIcon
- labelId (теряются все кроме одного)
- score
- hash
- symbol
- color
- star
- time_stamp
Я добавил в каждую запись поле safeincloud_card_id на случай, если мне понадобится что то обновить в записях или сделать обратный переход на SafeInCloud.
Отсутствие тестов
Цитата из лайфхакер ру
Если вы переезжаете на Bitwarden из другого парольного менеджера, вам не придётся переносить все данные и комбинации вручную. Программа поддерживает функции импорта и экспорта и может принимать пароли из огромного количества других приложений и браузеров — LastPass, 1Password, Blur, Chrome, Dashlane, Enpass, Firefox, KeePass, Opera, PassKeep, RoboForm, Vivaldi и Zoho.
Я насчитал 46 скриптов импорта из разных менеджеров паролей. Только для двух написаны unit тесты. Для 1password написан тест, где на вход даётся заполненная база и проверяются пароли после импорта таким образом expect(ph.password).toEqual('oldpass5');
Второй счастливчик это keepass. На вход даётся заполненная база, а тест проверяет результат на ≠ null:
describe('KeePass2 Xml Importer', () => {
it('should parse XML data', async () => {
const importer = new Importer();
const result = importer.parse(TestData);
expect(result != null).toBe(true);
});
});
Все остальные скрипты не тестируются.
Я посмотрел на другие репозитории bitwarden. Большинство тестов автоматически генерируемые. Клиентские приложения не имеют тестов: плагин для браузера, веб версия, консольное, мобильное и десктоп приложение.
server. бэкэнд на C#
- 28 тестовых классов. 25 из них такие:
// Remove this test when we add actual tests. It only proves that
// we've properly constructed the system under test.
[Fact(Skip = "Needs additional work")]
public void ServiceExists()
{
Assert.NotNull(_sut);
}
- Тесты есть в CollectionServiceTests.cs 5 тестов, DeviceServiceTests.cs 1 тест, OrganizationServiceTests.cs 2 теста
- Всё взаимодействие с MSSQL реализовано на хранимых процедурах
jslib. Общий js код
- импортируется в большинстве других проектов
- Тестов мало (7 файлов), но сильно больше чем в остальных репозиториях
- весь код импорта из других менеджеров паролей тут
browser. Расширение для браузеров
- Тестов нет
- Есть зависимость на jslib.
desktop. приложение на electron+angular
- Тестов нет
- Есть зависимость на jslib.
mobile. Android+iOS на Xamarin
- Весь код на C#
- Долго открывается на старом телефоне
- Тестов нет
web. Веб версия менеджера паролей vault.bitwarden.com
- Тестов нет
- Есть зависимость на jslib.
cli. Консольная утилита bw
- Тестов нет
- Есть зависимость на jslib.
Остальные репозитории мне не интересны; website, help, docs, directory-connector, brand
Я отключил автообновление bitwarden через google play. С таким подходом к тестированию можно в любой момент остаться без паролей под рукой.
Другие проблемы
Cвежий коммит на момент написания статьи 65e78ec1c8de13f3d6eac4a6862e532f698149b.
Метод fixUri
отрезает строку, если она длиньше 1000 символов. Без предупреждений.
if (uri.length > 1000) {
return uri.substring(0, 1000);
}
Метод nameFromUrl удаляет www из урлов. Если я указываю www, возможно я на самом деле хочу сохранить такой url а не сокращенный.
return hostname.startsWith('www.') ? hostname.replace('www.', '') : hostname;
Для поля notes будет вызван trim(удаление пробелов в начале и конце строки). В этом ничего плохого не вижу, но сравнивать с исходником это мешает.
cipher.notes = cipher.notes.trim();
Текстовые поля длиньше 200 символов запишутся в поле notes через разрыв строки.
issue#87 Максимальная длинна пароля 1000 символов. Но пароль из 688 символов не сохранится. 1000 символов это длинна зашифрованной строки а не пароля.
Лимит на 10к для заметок. Это не проблема, но при переходе на bitwarden стоит это учесть.
Поле one-time-password из SafeInCloud это не то же самое, что totp в bitwarden. Стандартный импорт переносит строку как есть и потом не понимает что с этим делать.
Без платной подписки можно сканировать totp, но воспользоваться нельзя. Это не очевидно и ни где не указано. issue#450
В SafeInCloud есть атрибут <card deleted="true"/>
. Bitwarden import про него не знает и все удалённые записи появятся в списке живых.
У меня есть записи, в которых указано что это шаблон. Такое может легко произойти если в SafeInCloud при добавлении записи промахнуться кнопкой. Такие записи теряются при импорте. Я так потерял 4 записи из 300
Bitwarden при экспорте присваивает директориям uuid. Но при импорте игнорирует их. И если сделать импорт дважды, он создаст дубликаты папок. Удаление папок доступно в веб версии и на одну папку нужно 4 клика. У меня ~ 20 папок. Items удаляются через веб интерфейс пачками по 500 штук. В целях дебага моего собственного импорта я удалял папки много раз. Написал скрипт (очень медленно работает, не сильно быстрее ручного удаления).
bw_folder_ids=$(bw list folders | jq '.[] | .id | select(.!=null)' -r)
for i in $bw_folder_ids; do
bw delete folder $i;
done;
UUID для директорий генерируются такие, что последние 12 символов всегда одинаковые. (Это не проблема, а просто наблюдение)
Если у записи в SafeInCloud больше одного лэйбла - остальные теряются.
Bitwarden теряет файлы, картинки и кастомные иконки.
Safeincloud учит не забывать пароль. Раз в неделю нужно вводить полный пароль вместо сканирования пальца. В мобильном Bitwarden такой возможности нет.
Баги в android приложении bitwarden
Баги наблюдал в версии 2.2.7 (2130)
- Если сразу после старта приложения нажать поиск и что нибудь поискать - приложение закрывается.
- Если быстро ввести строку запроса и нажать поиск, отобразится “There are no items to list.” При повторном нажатии покажется результат.
- Сразу после включения интерфейс не реагирует на нажатия. Помогает выгрузка из памяти. Закономерность воспроизведения бага не обнаружил. Случается редко.
- После поиска открыть любую запись на просмотр, нажать назад 2 раза. Отобразится общий список, но в нем будет неправильное количество записей
Я не тестировщик. У меня не бета версия. Я зашел на гугл плэй и перепроверил себя. Увидел предложение “Join the beta. Try new features before they’re officially released…”. Стабильная версия работает не очень стабильно. Ставить бету - страшно.
Не смотря на все эти проблемы - пользоваться можно.
Другие менеджеры паролей
Мой первый менеджер паролей - keepass. Я перешел на SafeInCloud из-за бага. Приложения для разных платформ работают по-разному. У меня некоторые записи не отображались в мобильном приложении. Я сначала подумал что они удалились и начал восстанавливать бэкапы. Потом, заметил что в веб версии эти записи существуют. Еще был неприятный баг с веб версией. Приходилось периодически чистить хранилище в браузере.
Кроме bitwarden я рассматривал другие варианты. Одним из самых интересных оказался https://www.passwordstore.org. Я его не выбрал только потому, что он не поддерживает сканер отпечатка. Android-Password-Store использует для работы с gpg ключами стороннее приложение OpenKeychain. Issue#1642 в статусе help_wanted создано в конце 2015 года и я сомневаюсь что это будет скоро реализовано.
Интересная особенность это хранение паролей в git. Имя файла в гите - имя записи(расширение файла gpg). Внутри записи есть только Password и extra. Другие поля добавить нельзя. Файл представляет собой одно PGP сообщение. При считывании пароля дешифруется только один пароль а не вся БД.
Password-Store имеет все шансы стать моим следующим менеджером паролей.
Выводы
Для упрощения перехода между менеджерами паролей не стоит пользоваться дополнительными фичами: хранение файлов, кастомные поля, totp.
После перехода стоит внимательно просмотреть на результат переноса паролей и другой информации.
Не удаляйте бэкапы от старого менеджера паролей после перехода.
Проверяйте свои бэкапы.