Jesteśmy dziś na Product Hunt! Zostaw komentarz

Authenticator 1.16.0 jest już dostępny. Zobacz nowości

Jak to działa

Przegląd techniczny dla osób audytujących bezpieczeństwo. Wszystko tutaj da się sprawdzić w kodzie źródłowym — łącznie z tym, co stawia nas w gorszym świetle i co nazywamy wprost, zamiast pomijać.

Architektura

To rozszerzenie Chrome w Manifest V3, które generuje kody TOTP lokalnie przy użyciu OTPAuth. Generowanie kodów, przechowywanie kont i ich szyfrowanie obywają się bez żadnego backendu, a kont użytkowników nie zakładamy. Prowadzimy jeden serwer, api.authenticator.sh, na potrzeby darmowego planu i Pro — opisany niżej — i nigdy nie trafia do niego żaden sekret, nazwa konta, wystawca ani kod.

Ten serwer nie przechowuje materiału klucza do niczyich kont: nie ma depozytu kluczy ani żadnej ścieżki odzyskiwania kontrolowanej przez nas. Jeśli ktoś zapomni hasła i zgubi kod odzyskiwania, nie jesteśmy w stanie pomóc — tak to zaprojektowaliśmy.

Kryptografia

Ochrona hasłem jest opcjonalna i domyślnie wyłączona. Gdy jest włączona:

Szyfr
AES-256-GCM, losowy 96-bitowy IV dla każdego rekordu i każdego zapisu
Wyprowadzanie klucza
PBKDF2-HMAC-SHA256, 600 000 iteracji, losowa 128-bitowa sól (zgodnie z zaleceniami OWASP)
Klucz główny
256 bitów, z crypto.getRandomValues, generowany jednorazowo; hasło jedynie go opakowuje
Odciski
HMAC-SHA256 na podkluczu wyprowadzonym przez HKDF, służy do scalania rekordów między urządzeniami
Przechowywanie odblokowanego klucza
chrome.storage.session — wyłącznie w pamięci, nigdy nie zapisywany na dysk, czyszczony przy zamknięciu przeglądarki, nieosiągalny dla content scriptów
Automatyczna blokada
Przy każdym otwarciu / po 5, 15 lub 60 minutach bezczynności / do zamknięcia przeglądarki
Implementacja
Wyłącznie WebCrypto. Żaden prymityw nie jest pisany ręcznie i żadna biblioteka kryptograficzna nie trafia do paczki.

Dlaczego dwa poziomy klucza

Dane nigdy nie są szyfrowane bezpośrednio kluczem wyprowadzonym z hasła. Rekordy szyfruje losowy klucz główny; wynik PBKDF2 tylko opakowuje ten klucz główny. Wynikają z tego dwie rzeczy i obie mają znaczenie:

  • Zmiana hasła przepisuje 32 bajty zamiast ponownie szyfrować każdy rekord, każdą kopię zapasową i każdy eksport. To właśnie przy takich masowych zapisach systemy przechowywania gubią dane.
  • 160-bitowy kod odzyskiwania może niezależnie opakować ten sam klucz główny, więc zapomniane hasło nie oznacza utraty danych. Kod pokazywany jest raz, trzeba go przepisać dla potwierdzenia i jest wymieniany po każdym użyciu. Nic nie trafia na dysk, dopóki użytkownik go nie potwierdzi.

Co jest szyfrowane, a co nie

Przy włączonej ochronie hasłem szyfrowany jest cały rekord konta poza identyfikatorem i odciskiem — łącznie z nazwą usługi, więc skradziony profil nie zdradza, w jakich serwisach użytkownik ma konta. Włączenie ochrony szyfruje rekordy, weryfikuje je przez odszyfrowanie i porównanie, i dopiero potem usuwa wszystkie kopie jawne: z lokalnego magazynu przeglądarki, z synchronizacji i ze wszystkich siedmiu rotowanych migawek kopii zapasowych.

Mówimy wprost, bo to ma znaczenie przy ocenie:

  • Przy wyłączonej ochronie hasłem konta są przechowywane bez szyfrowania i — jeśli włączona jest synchronizacja Chrome — replikowane przez konto Google użytkownika. Do nas nigdy nie trafiają, ale ma je Google. Synchronizację można wyłączyć, co usuwa też to, co już się tam znalazło.
  • Historia użycia w rozbiciu na witryny — które konto jest używane na której domenie — jest przechowywana lokalnie i w trakcie zbierania nie jest objęta sejfem. Włączenie ochrony hasłem ją kasuje; można ją też wyłączyć osobno.

Model zagrożeń

Chroni przed

  • Kradzieżą lub skopiowaniem katalogu profilu przeglądarki
  • Złośliwym oprogramowaniem typu infostealer, które hurtowo wykrada dane przeglądarki
  • Osobą z fizycznym dostępem do pozostawionego bez nadzoru komputera
  • Trafieniem rekordów kont do Google przez synchronizację Chrome
  • Zgubieniem lub kradzieżą pliku kopii zapasowej (eksport chroniony hasłem)

Nie chroni przed

  • Złośliwym kodem działającym jako użytkownik, gdy sejf jest odblokowany
  • Keyloggerem przechwytującym hasło w trakcie wpisywania
  • Skompromitowaną przeglądarką lub systemem operacyjnym
  • Włamaniem do zewnętrznych serwisów, których dotyczą kody
  • Niezaszyfrowany eksport, który sam zapisałeś — zwykły JSON, linki otpauth:// lub CXF są czytelne dla każdego, kto otworzy plik

Krótko: to chroni dane w spoczynku. Nie broni — i nie może obronić — urządzenia, które w trakcie używania jest już pod kontrolą atakującego.

Uprawnienia

Manifest deklaruje pięć uprawnień i żadnych uprawnień hosta:

storage
Konta i ustawienia w magazynie lokalnym, odblokowany klucz w magazynie sesji oraz opcjonalna kopia do synchronizacji
activeTab
Odczytuje nazwę hosta aktywnej karty, żeby podświetlić pasujące konto, i przechwytuje kartę, gdy użytkownik skanuje kod QR z ekranu
contextMenus
Dodaje pozycję „Wstaw kod 2FA” do menu kontekstowego, i tylko na edytowalnych polach stron http(s)
scripting
Wstrzykuje jedną funkcję, która wpisuje kod do pola — dla jednej karty i jednego wywołania, dopiero po wybraniu tej pozycji menu
sidePanel
Otwiera aplikację w panelu bocznym Chrome, gdy wybrano tę powierzchnię; nie daje dostępu do żadnej strony

Nie ma Nie ma żadnych content scriptów, żadnych uprawnień tabs, cookies ani webRequest, ani web-accessible resources. Nic naszego nie istnieje na stronie, dopóki nie poprosisz tam o kod; to, co wtedy działa, jest wstrzykiwane dla tej jednej karty i tego jednego wywołania, na podstawie zgody, której udziela Twoje własne kliknięcie, i nie zostawia po sobie niczego.

Kamera

Skanowanie QR kamerą działa na stronie rozszerzenia otwartej w karcie, a nie w okienku popup — popup jest niszczony, gdy traci fokus, a prośba o uprawnienie robi dokładnie to. Do tego nie jest potrzebne żadne uprawnienie w manifeście: używana jest standardowa prośba przeglądarki o dostęp do kamery, przyznawana dla origin rozszerzenia i odwoływalna w ustawieniach witryny, i nigdy nie pojawia się, zanim użytkownik nie otworzy skanera. Obraz jest dekodowany lokalnie, nigdy nie jest przesyłany ani zapisywany.

Darmowy plan, Pro i codzienne połączenie z serwerem

Od wersji 1.14.0 rozszerzenie raz dziennie łączy się z api.authenticator.sh — przy starcie przeglądarki albo po wyrenderowaniu okienka popup, nigdy po drodze do wyświetlenia kodu. Żądanie to zamknięta lista pól: składa je jedna funkcja w src/billing/sync.ts, a test pilnuje, żeby nie wyszło poza tę listę:

iid
Losowy identyfikator utworzony przez tę instalację. Niepowiązany z kontem Google, adresem e-mail ani przeglądarką — chyba że użytkownik kupi Pro: wtedy zapis zakupu wiąże instalację, z której dokonano zakupu, i każdą instalację z aktywnym kluczem licencyjnym z adresem e-mail podanym przy płatności
ver, ui_lang, browser_lang
Wersja rozszerzenia, język interfejsu, język przeglądarki
installed_at, legacy, legacy_src
Kiedy pojawiła się instalacja i czy powstała przed wprowadzeniem płatności
accounts, groups
Ile kont przechowuje i w ilu grupach — same liczby, nigdy które konta ani nazwy grup
vault, sync, open_mode, view_mode, style, dark, os
Czy ochrona hasłem i synchronizacja są włączone oraz jak otwiera się rozszerzenie, jak wyświetlana jest lista kont (zwykła, kompaktowa lub z ukrytymi kodami) oraz jaki styl jest wybrany — jasny czy ciemny oraz system operacyjny (Windows, macOS, Linux, ChromeOS)
pid, lid, has_key, key_hint
Którą podpisaną politykę i licencję ma obecnie instalacja, czy ma już klucz licencyjny oraz ostatnie cztery znaki tego klucza
opens
Ile razy rozszerzenie otwarto każdego dnia od ostatniego żądania (najwyżej z ostatnich 14 dni) — liczba na dzień, nic o tym, co oglądano
events
Zdarzenia rozliczeniowe czekające w kolejce, z zamkniętej listy: osiągnięto limit, paywall został pokazany lub zamknięty, wybrano plan, podjęto próbę płatności lub użycia klucza

Nigdy nie są wysyłane: sekrety, nazwy kont, wystawcy, kody ani strony, na których się ich używa. Serwer ustala kraj z żądania na brzegu sieci i nie zapisuje adresu IP. Strona odinstalowania otrzymuje ten sam identyfikator instalacji jako parametr ?i=: zgłasza serwerowi, że instalacja została usunięta, i wiąże z nią opcjonalną odpowiedź z ankiety.

Na te potrzeby w chrome.storage.local przechowywane są: identyfikator instalacji, ostatnia podpisana polityka i licencja, klucz licencyjny, który ma ta instalacja (wpisany albo otrzymany po zakupie na niej), liczby otwarć z poszczególnych dni, dopóki serwer ich nie odbierze, oraz zdarzenia w kolejce — żadnych danych kont. Gdy synchronizacja jest włączona, do Chrome Sync trafia też informacja, czy instalacja powstała przed wprowadzeniem płatności, oraz ten klucz licencyjny, żeby przejęły je pozostałe profile Chrome tego użytkownika i same włączyły Pro; wyłączenie synchronizacji usuwa oba z Chrome Sync, a ponowne włączenie je przywraca.

Co wraca z serwera i jak rozszerzenie to traktuje:

Tokeny
Polityka (limit kont w darmowym planie) i licencja (Pro) to JWS podpisane ES256 (ECDSA P-256)
Weryfikacja
WebCrypto, względem kluczy publicznych wbudowanych w kompilację (src/billing/keys.ts). Kompilacja dla Chrome Web Store ufa wyłącznie kluczowi produkcyjnemu, a kontrola przy budowaniu odrzuca taką, która zawiera jakikolwiek inny
Bez połączenia
Limit i licencja są sprawdzane lokalnie, na podstawie ostatnich podpisanych tokenów. Dopóki nie nadejdzie podpisana polityka, nic nie jest ograniczone — nieosiągalny serwer nigdy nikogo nie zablokuje
Co może limit
Zatrzymać dodanie konta — i nic poza tym. Nie może ukryć, zablokować, usunąć ani zmienić konta, nie może też zablokować kodów, eksportu, kopii zapasowych, przywracania ani synchronizacji
Zakupy
Stripe Checkout w nowej karcie, a sprzedawcą (merchant of record) jest Link. E-mail kupującego trafia do nas od Stripe, żeby wydać klucz licencyjny i móc go odzyskać; klucze są przechowywane jako skrót, a do tego jako zaszyfrowana kopia, żeby zgubiony klucz dało się wysłać ponownie
Klucz licencyjny
Odpowiedź na codzienne żądanie zawiera sam klucz tylko dla instalacji, na której kupiono Pro, i tylko do chwili, gdy cztery znaki zgłaszane przez nią w key_hint będą pasować do tego klucza — dzięki temu klucz kupiony później zastępuje starszy; instalacja, w której klucz wpisano, nigdy go nie dostaje

Limit nie jest DRM: kod jest otwarty, a fork może go usunąć. Nigdy natomiast nie może być możliwe, żeby ktoś zdobył lub wykorzystał cudzą licencję albo sprawił, że czyjaś instalacja zostanie ograniczona — to byłaby podatność, objęta zakresem naszej polityki bezpieczeństwa.

Ruch wychodzący

Samoczynnie wykonywane są dwa rodzaje żądań: sprawdzenie zegara i codzienne połączenie z serwerem rozliczeń. Cała reszta poniżej dzieje się dlatego, że użytkownik w coś kliknął. Manifest V3 zakazuje zdalnego kodu, a nic z poniższych nie przenosi kont, tajnych kluczy ani wygenerowanych kodów. Pełna lista:

HostKiedyCzy przenosi dane użytkownika?
time.akamai.com
timeapi.io
cloudflare.com
Sprawdzenie rozjazdu zegara, buforowane; gdy żadne źródło nie odpowiada, Ustawienia to pokazują. Rozjechany zegar psuje TOTP.Nie
authenticator.shStrona powitalna przy instalacji; strona opinii przy odinstalowaniu, z identyfikatorem instalacji w adresie; strony pomocy i wsparcia, gdy otwiera się je z okienka rozszerzenia.Identyfikator instalacji, tylko na stronie odinstalowania; poza tym nie (zwykłe logi serwera WWW)
api.authenticator.shPołączenie rozliczeniowe raz dziennie; wywołania płatności, licencji i portalu, gdy użytkownik kupuje Pro, wpisuje klucz, przywraca zakup lub zarządza subskrypcją.Pola wymienione wyżej — nigdy dane kont. Klucz licencyjny lub adres e-mail tylko wtedy, gdy użytkownik sam go wpisze
checkout.stripe.com
billing.stripe.com
Stripe Checkout i portal klienta, otwierane w karcie, gdy użytkownik kupuje Pro lub zarządza subskrypcją.To, co kupujący tam wpisze. Obowiązują własne warunki Stripe i Link
chromewebstore.google.comStrona w sklepie, otwierana w karcie dopiero po kliknięciu: formularz recenzji z prośby o ocenę.Nie (dla Google to wejście na stronę sklepu jak każde inne)

Żadne fonty, skrypty, style ani obrazy nie ładują się z zewnętrznych hostów. Wszystko, co potrzebne do wyrenderowania interfejsu, jest w paczce.

Jak sprawdzić to, co publikujemy

Nic z powyższego nie trzeba przyjmować na wiarę. Rozszerzenie ma otwarty kod, a każde wydanie jest powtarzalne:

  1. Pobierz opublikowany plik .crx z Chrome Web Store i rozpakuj go
  2. Przełącz się na odpowiedni tag gita
  3. Uruchom npm ci && npm run build na Node 20 LTS
  4. Porównaj powstały dist/ z rozpakowaną paczką

Przy każdym wydaniu na GitHubie publikujemy SHA-256 dla każdego pliku w zbudowanym dist/, jako SHA256SUMS-v<version>.txt i w formacie sha256sum — dzięki temu krok 4 sprowadza się do jednego uruchomienia sha256sum -c. Różnice powinny ograniczać się do kolejności plików w archiwach i białych znaków w zminifikowanym wyniku między wersjami łatek Node.

Zależności w czasie działania są celowo nieliczne — OTPAuth, React, jsQR, Zustand, Framer Motion i Lucide — przypięte zacommitowanym plikiem lock. SBOM w formacie CycloneDX udostępniamy na życzenie.

Zobacz kod źródłowy

Czego nie mamy

Jesteśmy małym niezależnym dostawcą. Jeśli którykolwiek z poniższych punktów jest w waszym procesie wymogiem twardym, lepiej dowiedzieć się teraz niż po trzech tygodniach oceny:

  • Brak certyfikacji SOC 2, ISO 27001 ani równoważnej
  • Do tej pory nie zlecono zewnętrznego testu penetracyjnego
  • Brak płatnego programu bug bounty

W zamian oferujemy pełną jawność kodu, weryfikowalną kompilację, minimalny zestaw uprawnień i serwer, do którego w ogóle nie są wysyłane wasze sekrety, nazwy kont ani kody.

Kontakt z nami

Zgłoszenia podatności i pytania o bezpieczeństwo: security@authenticator.sh. Polityka ujawniania, zakres, czasy reakcji i warunki safe harbour są na stronie polityki bezpieczeństwa; co jest przechowywane i co opuszcza urządzenie, opisuje polityka prywatności.

Chętnie wypełnimy pisemną ankietę oceny dostawcy albo przejdziemy przez kod razem z zespołem bezpieczeństwa.

We wszystkim, co nie dotyczy bezpieczeństwa — instalacja, odzyskiwanie konta, pytania o rozliczenia przy wdrożeniu — zajrzyj do pomocy. Prosimy nie wysyłać tam zgłoszeń podatności: ta skrzynka nie jest traktowana jako poufna.