Сьогодні ми на Product Hunt! Залишити коментар

Вийшла версія Authenticator 1.16.0. Що нового

Як це працює

Технічний огляд для тих, хто перевіряє безпеку. Усе викладене можна звірити з вихідним кодом — зокрема й те, що нас не прикрашає: ми це називаємо, а не оминаємо.

Архітектура

Це розширення Chrome на Manifest V3, яке генерує коди TOTP локально за допомогою OTPAuth. Генерування кодів, зберігання облікових записів і їх шифрування обходяться без бекенду, а реєстрації користувачів у нас немає. Ми тримаємо один сервер, api.authenticator.sh, для безкоштовного тарифу й Pro — його описано нижче, — і він ніколи не отримує ні секретів, ні назв облікових записів, ні видавців, ні кодів.

Цей сервер не зберігає ключового матеріалу для чиїхось облікових записів: немає ні депонування ключів, ні будь-якого шляху відновлення, який контролювали б ми. Якщо користувач забув пароль і втратив код відновлення, ми не зможемо зарадити — так задумано.

Криптографія

Захист паролем необов'язковий і типово вимкнений. Коли його ввімкнено:

Шифр
AES-256-GCM, випадковий 96-бітний IV для кожного запису й кожного перезапису
Вироблення ключа
PBKDF2-HMAC-SHA256, 600 000 ітерацій, випадкова 128-бітна сіль (за рекомендаціями OWASP)
Майстер-ключ
256 біт, із crypto.getRandomValues, генерується один раз; пароль лише огортає його
Відбитки
HMAC-SHA256 на підключі, виведеному через HKDF; потрібні для зведення записів між пристроями
Зберігання розблокованого ключа
chrome.storage.session — лише в пам'яті, ніколи не пишеться на диск, очищається при закритті браузера, недосяжний для content-скриптів
Автоблокування
За кожного відкриття / через 5, 15, 60 хвилин бездіяльності / до закриття браузера
Реалізація
Тільки WebCrypto. Жоден примітив не написано вручну, жодної криптобібліотеки в збірці немає.

Чому ключів два рівні

Дані ніколи не шифруються ключем, виведеним із пароля, безпосередньо. Записи шифрує випадковий майстер-ключ; вивід PBKDF2 лише огортає цей майстер-ключ. Звідси два наслідки, і обидва важливі:

  • Зміна пароля переписує 32 байти замість перешифрування кожного запису, кожної резервної копії та кожного експорту. Саме на масовому перезаписі сховища й втрачають дані.
  • 160-бітний код відновлення може незалежно огорнути той самий майстер-ключ, тож забутий пароль не вирок. Код показується один раз, його треба ввести назад для підтвердження, і він змінюється після кожного використання. На диск нічого не пишеться, поки користувач його не підтвердив.

Що шифрується, а що ні

Коли захист паролем увімкнено, шифрується весь запис облікового запису, крім ідентифікатора та відбитка — зокрема й назва сервісу, тож із викраденого профілю не зрозуміти, обліковими записами яких сервісів володіє користувач. Під час увімкнення розширення шифрує записи, перевіряє їх розшифруванням і порівнянням і лише тоді видаляє всі копії у відкритому вигляді: з локального сховища браузера, із синхронізації та з усіх семи ротованих знімків резервних копій.

Кажемо прямо, бо для вашої оцінки це суттєво:

  • Якщо захист паролем вимкнено, облікові записи зберігаються незашифрованими і, якщо ввімкнено синхронізацію Chrome, реплікуються через власний обліковий запис Google користувача. До нас вони не потрапляють ніколи, але Google їх зберігає. Синхронізацію можна вимкнути — це заразом видалить і те, що там уже є.
  • Історія використання за сайтами — який обліковий запис застосовується на якому домені — зберігається локально й, поки збирається, сховищем не захищена. Увімкнення захисту паролем її видаляє; її також можна вимкнути окремо.

Модель загроз

Захищає від

  • Викрадення або копіювання каталогу профілю браузера
  • Зловмисних програм-стилерів, які вивантажують дані браузера цілком
  • Фізичного доступу до залишеного без нагляду комп'ютера
  • Потрапляння записів облікових записів до Google через синхронізацію Chrome
  • Втрати чи викрадення файлу резервної копії (експорт під паролем)

Не захищає від

  • Зловмисного коду, що працює від імені користувача, поки сховище розблоковане
  • Кейлогера, який перехоплює пароль під час введення
  • Скомпрометованого браузера чи операційної системи
  • Зламу самих сервісів, для яких призначені коди
  • Незашифрований експорт, який ви самі вирішили зберегти, — звичайний JSON, посилання otpauth:// чи CXF читає будь-хто, хто відкриє файл

Коротко: це захищає дані у стані спокою. Пристрій, який під час роботи вже перебуває під контролем зловмисника, так захистити не можна — і ми цього не обіцяємо.

Дозволи

Маніфест оголошує п'ять дозволів і жодного дозволу на хости:

storage
Облікові записи й налаштування в локальному сховищі, розблокований ключ у session storage і необов'язкова копія для синхронізації
activeTab
Читає ім'я хоста активної вкладки, щоб підсвітити відповідний обліковий запис, і робить знімок вкладки, коли користувач сканує QR-код з екрана
contextMenus
Додає пункт «Вставити код 2FA» до контекстного меню, і лише на редагованих полях сторінок http(s)
scripting
Впроваджує одну функцію, яка вписує код у поле, — для однієї вкладки й одного виклику, лише після вибору цього пункту меню
sidePanel
Відкриває застосунок у бічній панелі Chrome, якщо обрано цей режим; доступу до сторінок не дає

Немає немає content-скриптів, немає дозволів tabs, cookies чи webRequest і немає web-accessible resources. Нічого нашого не існує на сторінці, доки ви не попросите там код; те, що тоді виконується, впроваджується для тієї однієї вкладки й того одного виклику, за дозволом, який дає ваш власний клік, і не лишає по собі нічого.

Камера

Сканування QR-коду камерою працює на сторінці розширення, відкритій у вкладці, а не у спливному вікні: popup знищується, щойно втрачає фокус, а запит дозволу саме це й робить. Для цього не потрібен дозвіл у маніфесті: використовується штатний запит камери браузера, який видається на origin розширення та відкликається в налаштуваннях сайту, і він ніколи не з'являється, доки користувач не відкриє сканер. Відео декодується локально, нікуди не передається й не зберігається.

Безкоштовний тариф, Pro і щоденна звірка з сервером

Починаючи з 1.14.0 розширення раз на добу звертається до api.authenticator.sh — під час запуску браузера або після того, як відмалювалося вікно розширення, і ніколи на шляху до показу коду. Запит — це закритий перелік полів: його формує одна функція в src/billing/sync.ts, а тест не дає цьому переліку розширитися:

iid
Випадковий ідентифікатор, створений цією установкою. Не пов'язаний ні з обліковим записом Google, ні з адресою email, ні з браузером — якщо тільки користувач не купить Pro: тоді запис про покупку пов'язує установку, з якої купили, і будь-яку установку, де активний ліцензійний ключ, з адресою email, вказаною під час оплати
ver, ui_lang, browser_lang
Версія розширення, мова інтерфейсу, мова браузера
installed_at, legacy, legacy_src
Коли з'явилася установка і чи з'явилася вона ще до запровадження білінгу
accounts, groups
Скільки облікових записів вона містить і в скількох вони групах — числа, ніколи не які саме записи й не назви груп
vault, sync, open_mode, view_mode, style, dark, os
Чи ввімкнено захист паролем і синхронізацію та як відкривається розширення, як показано список облікових записів (звичайний, компактний або з прихованими кодами), а також вибраний стиль і тема — світла чи темна, і операційна система (Windows, macOS, Linux, ChromeOS)
pid, lid, has_key, key_hint
Яка підписана політика та яка ліцензія зараз є в установки, чи вже є в неї ліцензійний ключ і останні чотири символи цього ключа
opens
Скільки разів розширення відкривали кожного дня від попереднього запиту (щонайбільше за останні 14 днів) — лише число за день, нічого про те, що при цьому переглядали
events
Події білінгу з черги, із закритого переліку: досягнуто ліміту, екран оплати (paywall) показано або закрито, вибрано тариф, була спроба оплати чи введення ключа

Ніколи не надсилаються: секрети, назви облікових записів, видавці, коди чи сайти, на яких їх використовують. Країну сервер визначає із запиту на своєму edge-вузлі й IP-адресу не зберігає. Сторінка видалення отримує той самий ідентифікатор установки як ?i=: вона повідомляє серверу, що установку видалено, і пов'язує з нею необов'язкову відповідь в опитуванні.

Для цього в chrome.storage.local зберігаються ідентифікатор установки, остання підписана політика й ліцензія, ліцензійний ключ цієї установки (введений вручну або отриманий після покупки в ній), лічильники відкриттів за днями, доки їх не отримає сервер, і події в черзі — жодних даних облікових записів. Поки ввімкнено синхронізацію, ознака того, що установка старша за білінг, і цей ліцензійний ключ потрапляють також у Chrome Sync, щоб їх підхопили інші профілі Chrome цього користувача й самі ввімкнули Pro; якщо вимкнути синхронізацію, обидва видаляються з Chrome Sync, а після повторного ввімкнення повертаються.

Що повертається і як розширення з цим поводиться:

Токени
Політика (ліміт облікових записів безкоштовного тарифу) і ліцензія (Pro) — це JWS, підписані ES256 (ECDSA P-256)
Перевірка
WebCrypto, за публічними ключами, вкомпільованими у збірку (src/billing/keys.ts). Збірка для Chrome Web Store довіряє лише продакшн-ключу, а перевірка збірки відхиляє таку, що містить будь-який інший
Офлайн
Ліміт і ліцензія перевіряються локально, за останніми підписаними токенами. Доки не отримано підписаної політики, нічого не обмежується — недоступний сервер ніколи не може нікого заблокувати
Що може ліміт
Не дати додати обліковий запис — і нічого більше. Він не може приховати, заблокувати, видалити чи змінити обліковий запис або заблокувати коди, експорт, резервні копії, відновлення чи синхронізацію
Покупки
Stripe Checkout у новій вкладці, продавець (merchant of record) — Link. Email покупця надходить до нас від Stripe, щоб видати й відновити ліцензійний ключ; ключі зберігаються як геш, а також у зашифрованій копії, щоб загублений ключ можна було надіслати знову
Ліцензійний ключ
Сам ключ надходить у відповіді на щоденний запит лише тій установці, у якій купили Pro, і лише доки чотири символи, які вона повідомляє в key_hint, не збіжаться з цим ключем, — тож ключ, куплений пізніше, замінює попередній; установці, у яку ключ ввели вручну, він не надсилається ніколи

Ліміт — це не DRM: код відкритий, і форк може його прибрати. Що ніколи не має бути можливим — це отримати чи використати чужу ліцензію або зробити обмеженою чужу установку: таке є вразливістю й підпадає під дію нашої політики безпеки.

Мережеві запити

Самостійно розширення робить два види запитів: перевірку годинника та щоденну звірку з сервером для білінгу. Усе інше нижче стається тому, що користувач на це натиснув. Manifest V3 забороняє віддалений код, і ніщо з переліченого не передає ні облікових записів, ні секретних ключів, ні згенерованих кодів. Повний перелік:

ХостКолиПередає дані користувача?
time.akamai.com
timeapi.io
cloudflare.com
Перевірка розходження годинника, кешується; якщо жодне джерело недоступне, про це сказано в налаштуваннях. Зі збитим годинником TOTP ламається.Ні
authenticator.shСторінка привітання під час встановлення; сторінка зворотного зв'язку під час видалення, з ідентифікатором установки в адресі; сторінки довідки та підтримки, коли їх відкривають із вікна розширення.Ідентифікатор установки — лише на сторінці видалення; в інших випадках ні (звичайні логи вебсервера)
api.authenticator.shЗвірка для білінгу раз на добу; запити оплати, ліцензії та порталу, коли користувач купує Pro, вводить ключ, відновлює покупку або керує підпискою.Поля, перелічені вище, — ніколи не дані облікових записів. Ліцензійний ключ чи адресу email — лише коли користувач сам їх вводить
checkout.stripe.com
billing.stripe.com
Stripe Checkout і клієнтський портал, відкриваються у вкладці, коли користувач купує Pro або керує підпискою.Те, що покупець там вводить. Діють власні умови Stripe і Link
chromewebstore.google.comСторінка в магазині, відкривається у вкладці лише за кліком: форма відгуку — з пропозиції оцінити.Ні (для Google це звичайний захід на сторінку магазину)

Ні шрифти, ні скрипти, ні стилі, ні зображення не завантажуються зі сторонніх хостів. Усе потрібне для відображення інтерфейсу лежить усередині пакета.

Як перевірити те, що ми публікуємо

Нічого з переліченого не треба брати на віру. Розширення має відкритий код, і кожен реліз відтворюваний:

  1. Завантажте опублікований .crx з Chrome Web Store і розпакуйте його
  2. Перемкніться на відповідний git-тег
  3. Виконайте npm ci && npm run build на Node 20 LTS
  4. Порівняйте отриманий dist/ з розпакованим пакетом

SHA-256 для кожного файлу в зібраному dist/ публікується з кожним релізом на GitHub у вигляді SHA256SUMS-v<version>.txt, у форматі sha256sum — тож крок 4 зводиться до одного запуску sha256sum -c. Розбіжності мають обмежуватися порядком файлів усередині архівів і пробілами в мініфікованому виводі на різних патч-версіях Node.

Залежностей у рантаймі свідомо мало — OTPAuth, React, jsQR, Zustand, Framer Motion і Lucide — усі зафіксовані закомміченим lock-файлом. SBOM у форматі CycloneDX надаємо на запит.

Переглянути вихідний код

Чого в нас немає

Ми невеликий незалежний розробник. Якщо щось із переліченого для вашого процесу обов'язкове, краще дізнатися про це зараз, а не на третьому тижні оцінювання:

  • Немає сертифікації SOC 2, ISO 27001 чи аналогічної
  • Незалежний пентест досі не замовлявся
  • Немає платної програми bug bounty

Натомість ми пропонуємо повну прозорість вихідного коду, перевірювану збірку, мінімальний набір дозволів і сервер, якому ваші секрети, назви облікових записів чи коди взагалі ніколи не надсилаються.

Як з нами зв'язатися

Повідомлення про вразливості та питання щодо безпеки: security@authenticator.sh. Політика розкриття, межі застосування, строки відповіді й умови safe harbour — на сторінці політики безпеки; що зберігається і що залишає пристрій — у політиці конфіденційності.

Ми готові заповнити письмову анкету оцінювання постачальника або розібрати код разом із вашою командою безпеки.

З усього, що не стосується безпеки — встановлення, відновлення доступу, питання оплати під час розгортання — див. підтримку. Будь ласка, не надсилайте туди повідомлень про вразливості: ця скринька не вважається конфіденційною.