Kryptograficzny system awaryjnego dostępu do bazy haseł — dzieli hasło główne między zaufane osoby algorytmem Shamira, zabezpieczony 2FA i bcrypt.
Każdy z nas przechowuje dziesiątki haseł — do banków, poczty, mediów społecznościowych. Co się z nimi stanie po naszej śmierci? Rodzina zostaje odcięta od kont, nie może anulować subskrypcji, odzyskać pieniędzy.
Secret Key rozwiązuje ten problem, tworząc bezpieczny plan awaryjny — bez kompromisów wobec bezpieczeństwa za życia właściciela.
Minimalna wymagana liczba osób (np. 3 z 5) zbiera się razem. Każda ma kartę Secret Key z fragmentem klucza kryptograficznego.
02
Logowanie do systemu
Każda osoba loguje się danymi z karty (login + hasło) i weryfikuje tożsamość kodem SMS wysłanym na przypisany numer telefonu.
03
Wprowadzanie udziałów
Każda osoba wpisuje swój udział Shamira (ciąg hex) lub skanuje kod QR z karty. Eliminuje to błędy przepisywania.
04
Rekonstrukcja hasła
Hasło odtwarzane jest lokalnie w przeglądarce algorytmem Shamira — nie trafia na serwer. System wyświetla hasło główne do bazy KeePassXC (lub innego menedżera).
💡
Self-hosted = pełna kontrola
Dane nigdy nie opuszczają Twojego serwera. Żadnej centralnej bazy danych, żadnej chmury, żadnych zewnętrznych zależności poza wysyłką SMS przez SMSPlanet.
Otwórz dashboard.html i przejdź do zakładki Konfiguracja.
1
Dodaj wyznaczone osoby
Dla każdej osoby: login, silne hasło, imię, nazwisko, numer telefonu (do weryfikacji SMS) i czy ma być widoczna na liście posiadaczy w panelu. System obsługuje od 2 do 7+ osób.
2
Uzupełnij pliki, instrukcję i powiadomienia
W tym samym formularzu: pliki do pobrania (baza haseł, 2FA, instalator programu), kroki instrukcji dla panelu i dane do powiadomienia email o logowaniu. Szczegóły każdego pola — patrz Konfiguracja treści i danych.
3
Generuj konfigurację
Kliknij „Generuj konfigurację" — otrzymasz plik secret-key.php z zahashowanymi hasłami bcrypt i całą resztą configu w jednym miejscu.
4
Pobierz plik
Zachowaj secret-key.php — trafi na serwer poza katalogiem publicznym (/private/).
# Struktura katalogów na serwerze
/home/user/
├── public_html/
│ └── app/← system logowania
│ └── decrypt/← panel użytkownika
└── private/← POZA public_html !
└── secret-key.php← plik konfiguracyjny
⛔
Plik konfiguracyjny musi być poza public_html
Umieszczenie secret-key.php w katalogu publicznym grozi ujawnieniem zahashowanych haseł i numerów telefonów. Folder /private/ musi być niedostępny przez HTTP.
Zaktualizuj ścieżkę w auth.php:
php
// auth.php — ścieżka do konfiguracjirequire_once'/home/user/private/secret-key.php';
Otwórz zakładkę Generator kart w dashboard.html. Wprowadź dane każdej osoby i udziały Shamira z pliku secret-key-shares.txt, a następnie pobierz karty jako PDF — gotowe do wydruku lub laminowania.
✅
System gotowy
Po rozdaniu kart system jest aktywny. Każda wyznaczona osoba ma własne dane logowania, udział Shamira i adres instancji. Bez wymaganej liczby kart odtworzenie hasła jest matematycznie niemożliwe.
Sekret kodowany jest jako wyraz wolny wielomianu nad ciałem GF(2⁸). Każdy udział to punkt na tym wielomianie — znając wymaganą liczbę punktów, można go jednoznacznie odtworzyć interpolacją Lagrange'a:
Hasło główne:"MojeHasloDoKeePass2024!"
│
▼ Podział na 5 udziałów (próg: 3)
│
┌────┴──────────────────────────────────┐
│ │
│ S1: 801a3f9c2e4b7d1... → Osoba A │
│ S2: 802c8f1a5e9b3d7... → Osoba B │ Każdy udział
│ S3: 803e2a7f4c1b9d5... → Osoba C │ jest bezużyteczny
│ S4: 804b6d3e8f2a1c9... → Osoba D │ bez wymaganej
│ S5: 805d9f7b2e4c3a1... → Osoba E │ liczby pozostałych
│ │
└────┬──────────────────────────────────┘
│
▼ Rekonstrukcja — wystarczą dowolne 3 z 5
│
S1 + S2 + S3 → "MojeHasloDoKeePass2024!" ✓
S2 + S4 + S5 → "MojeHasloDoKeePass2024!" ✓
S1 → brak informacji o sekrecie ✗
⚠️
Information-theoretic security
Posiadanie mniejszej niż wymagana liczby udziałów nie daje żadnej informacji o sekrecie — to własność matematyczna algorytmu, nie zależna od mocy obliczeniowej atakującego. Dodany padding 1024 bitów uniemożliwia ataki na małe sekrety.
Każda karta = osobna strona (przód + tył oddzielnie)
Technologia
Czyste SVG (<path>, <rect> z gradientami — brak <image>)
💡
Brak elementów rastrowych
Wszystkie elementy graficzne karty to czyste ścieżki SVG. Gwarantuje to ostrość wydruku przy dowolnej rozdzielczości i eliminuje problemy z drukowaniem obrazów rastrowych w Chrome.
01🔒 Hasła (bcrypt)cost=10, format $2y$, weryfikacja odporna na ataki czasowe (hash_equals())
02🛡️ CSRFToken 64 hex znaków z bin2hex(random_bytes(32)), weryfikowany na wszystkich endpointach zmieniających stan (logowanie, weryfikacja 2FA, ponowna wysyłka kodu, log zdarzeń)
03🚫 Brute-force3 próby/IP + 3 próby/konto w oknie 15 min; 3 błędne kody SMS/godz. Liczniki trwałe po stronie serwera (plik, niezależny od sesji/cookies klienta) — nie da się ich zresetować bez odczekania okna czasowego
04📱 2FA SMSKod 6-cyfrowy z random_int(), ważny 10 min, cooldown 60s między wysyłkami
05💻 Trusted devicesSHA-256, HttpOnly + Secure + SameSite=Strict, plik poza public_html, TTL 7 dni
06⏱️ SesjaCiasteczko sesji z jawnie ustawionymi flagami HttpOnly + Secure + SameSite=Strict (session_set_cookie_params()). Auto-logout 30 min, session_regenerate_id() po każdej weryfikacji, dwuetapowa: pending_2fa → logged_in
07🖥️ Ochrona interfejsuDetekcja otwarcia DevTools (embedded devtools-detector, bez CDN). Po wykryciu — fizyczne usunięcie węzłów DOM z drzewa (podmiana na komentarze, nie ukrycie przez CSS) i pełnoekranowy ekran ostrzegawczy z licznikiem czasu; każde otwarcie/zamknięcie rejestrowane w logu z IP, numerem referencyjnym (REF#) i czasem trwania. Dodatkowo: blokada zaznaczania/kopiowania/przeciągania obrazów, kontekstowego menu na grafikach i czyszczenie schowka po PrintScreen
08📥 Bramkowane pobieraniedownload.php + biała lista budowana z configu. Pliki do pobrania leżą poza public_html i nie mają bezpośredniego URL — wymagana aktywna sesja, log zawsze po stronie serwera, niezależna twarda walidacja stanu blokady czasowej (patrz warstwa 09)
09⏳ Blokada czasowa (Timelock)48h blokada fizycznego pobierania plików po pierwszej udanej rekonstrukcji hasła w panelu + jednorazowy link „Panic Button" do natychmiastowego, trwałego zablokowania — patrz Ochrona przed zmową powierników
Niezależnie od siły wszystkich pozostałych warstw — bez wymaganej liczby udziałów odtworzenie hasła jest matematycznie niemożliwe. Nawet pełny dostęp do serwera, pliku konfiguracyjnego i logów nie ujawnia hasła głównego.
⛔
Co nie jest chronione przez system
Ataki wymagające fizycznego dostępu do serwera, ataki socjotechniczne na posiadaczy kart, błędna konfiguracja serwera WWW po stronie użytkownika — leżą poza zakresem systemu.
SSL/TLS na całym serwerze — dane logowania i kody SMS w sieci
🔄 Aktualizacje PHP
Regularnie aktualizuj do najnowszej PHP 8.x
🔑 Silne hasła
Unikalne, długie hasła dla każdego konta na kartach
🚫 secret-key.php nie publicznie
Nigdy nie udostępniaj pliku konfiguracyjnego
📄 Liczniki brute-force w pliku, nie w sesji
Trzymanie limitów prób logowania w $_SESSION jest pozorną ochroną — atakujący resetuje licznik po prostu nie odsyłając ciasteczka. Liczniki muszą żyć w trwałym magazynie po stronie serwera (plik z flock() lub baza), kluczowane po IP i po koncie
📥 Pliki do pobrania poza public_html
Nie linkuj bezpośrednio do bazy haseł czy innych wrażliwych plików w katalogu publicznym — nawet zaszyfrowane, ich bezpośredni URL pozwala ominąć logowanie i 2FA, a pobranie nie trafia do logu. Serwuj je przez skrypt PHP z requireLogin(), białą listą nazw i logowaniem po stronie serwera (patrz „Konfiguracja index.php" → „Bramkowane pobieranie plików")
⚠️ filesize() + fread() zamiast stream_get_contents()
Przy odczycie plików pod blokadą (flock()) unikaj filesize() — pod PHP-FPM (workery wielokrotnego użytku) wynik bywa cache'owany między żądaniami i może zwrócić nieaktualny rozmiar po modyfikacji pliku przez inny request, ucinając odczyt. Użyj stream_get_contents($fp), które czyta bezpośrednio z uchwytu aż do EOF, niezależnie od cache'u stat()
🌐 Wyjątek w CDN dla cache'u całej strony
Jeśli domena ma włączony cache na poziomie CDN dla całej strony (np. „Cache Everything" w Cloudflare), dodaj regułę „bypass" dla ścieżki systemu przed ogólną regułą cache — taki tryb domyślnie ignoruje nagłówki Cache-Control z originu. Patrz „Instalacja krok po kroku" → „Wyjątek w CDN/cache"
🔐 Klucz sprzętowy (YubiKey/FIDO2) jako 2FA bazy haseł
Niezależnie od Timelocka i Panic Buttona — dla maksymalnego poziomu ochrony zabezpiecz samą bazę haseł (KeePassXC i inne menedżery to wspierają) kluczem sprzętowym jako drugim składnikiem. Wtedy sama znajomość hasła głównego, nawet odzyskanego przez powierników, nie wystarcza do otwarcia bazy
48-godzinna blokada czasowa pobierania plików z alertem e-mail i przyciskiem awaryjnym („Panic Button"), chroniąca przed zmową wyznaczonych powierników za życia właściciela.
Algorytm Shamira matematycznie pozwala wyznaczonym osobom odtworzyć samo hasło główne, jeśli się w tym celu zmówią — to nieunikniona własność każdego progowego systemu dzielenia sekretu, nie tylko tego. Do wersji z tym mechanizmem jedyną barierą przed nadużyciem były powiadomienia email wysyłane po fakcie — właściciel dowiadywał się o logowaniu, ale nic nie stało na przeszkodzie natychmiastowemu pobraniu bazy haseł. System dodatkowo blokuje fizyczny dostęp do plików na 48 godzin od momentu odzyskania hasła, dając właścicielowi realny czas na reakcję.
⚠️
Nie da się zablokować samego odzyskania hasła
System nie walczy z matematyką Shamira — zakłada, że powiernicy mogą odzyskać samo hasło, nawet poza panelem (np. na iancoleman.io/shamir). Bramkowany jest za to fizyczny plik z bazą haseł: dopiero pobranie pliku jest tym, co ta funkcja realnie opóźnia i o czym alarmuje właściciela.
Wcześniejszy warunek sukcesu sprawdzał tylko, czy rekonstrukcja Shamira nie rzuciła wyjątku — czyste dzielenie sekretu nie ma wbudowanej integralności, więc błędne lub niewystarczające udziały i tak niemal zawsze zwracają jakiś niepusty, losowy ciąg zamiast błędu. System nie odróżniał więc „poprawnie odzyskanego hasła" od kryptograficznego śmiecia — a to właśnie ten sygnał uzbraja blokadę czasową opisaną niżej. Dodane zostały dwie niezależne metody weryfikacji:
Metoda
Jak działa
Spójność podzbiorów
Własność matematyczna Shamira: każdy podzbiór udziałów o rozmiarze ≥ próg z tego samego podziału musi zrekonstruować identyczny sekret. Jeśli powiernicy podali więcej udziałów niż ściśle wymagane minimum, sprawdzane jest empirycznie (leave-one-out) — pominięcie dowolnego pojedynczego udziału nadal musi dać ten sam wynik. Matematycznie pewne, nie heurystyka.
Test formatu wyniku
Fallback stosowany dla dokładnie progowej liczby udziałów (brak nadmiaru do porównania metodą wyżej). Wymaga, żeby wszystkie znaki złożonego sekretu należały do „sensownych" kategorii Unicode (litera/cyfra/interpunkcja/symbol/spacja) — obsługuje pełny zakres języków (polskie znaki diakrytyczne, cyrylica, CJK), nie tylko ASCII. Im dłuższy sekret, tym mocniejszy test.
Przyciski pobierania wyszarzone, bez href (żeby przeglądarka nie pokazywała podglądu URL), z dymkiem po najechaniu wyjaśniającym dlaczego są nieaktywne
B
Trwa odliczanie 48h
Cała karta (ikona + tytuł + opis + przyciski) rozmyta, z wyśrodkowanym, żywym licznikiem (00h 00m 00s, aktualizowanym co sekundę). Aktywuje się natychmiast po udanym odszyfrowaniu, bez odświeżania strony
C
Zablokowane przez Panic Button
Ta sama rozmyta karta, zamiast licznika czerwony komunikat o trwałej blokadzie
D
Minęło 48h
Normalne, aktywne przyciski, jak przed wprowadzeniem tego mechanizmu
Panel na żywo wykrywa zmianę stanu (tl-status.php, odpytywanie co 10s) — Panic Button kliknięty w innej karcie, upływ czasu czy ręczny reset configu odświeżają widok automatycznie, bez akcji użytkownika.
#Panic Button — alert e-mail i jednorazowy link blokujący
Pierwsza potwierdzona udana rekonstrukcja sekretu w panelu automatycznie: tworzy stan blokady (private/timelock.json, 48h okno), loguje zdarzenie TIMELOCK ARMED i wysyła e-mail alarmowy do właściciela (patrz Konfiguracja treści i danych → Powiadomienie email). Mail zawiera jednorazowy link panic.php?token=... (token 256-bitowy, bez wymogu logowania — właściciel może nie mieć jak się szybko zalogować w sytuacji kryzysowej). Kliknięcie natychmiast i trwale blokuje pobieranie, niezależnie od tego, czy 48h już minęło, czy nie.
⛔
download.php waliduje stan niezależnie od panelu
Bezpośrednie wejście na URL z pominięciem interfejsu też respektuje blokadę — kod 423 w trakcie odliczania, 403 po Panic Button — z pełną, stylistycznie spójną stroną błędu zamiast surowego kodu HTTP.
💡
Odpowiedź do przeglądarki leci przed wysyłką maila
Uzbrojenie licznika (odpowiedź JSON do panelu) wysyłane jest przed wysyłką maila — przez fastcgi_finish_request() (dostępne pod PHP-FPM na większości hostingów) z fallbackiem opartym o flush(). Dzięki temu czas wysyłki przez SMTP nie opóźnia startu licznika na ekranie.
#Reset po Panic Button — automatyczny, bez ręcznej ingerencji
Ważność timelock.json jest powiązana z odciskiem (hashem) aktualnego $people w secret-key.php. Gdy właściciel po incydencie zmieni hasło główne i wygeneruje nowy config (co i tak musi zrobić), stary timelock automatycznie traci ważność przy najbliższym odświeżeniu panelu — bez potrzeby ręcznego kasowania pliku przez FTP/SSH w stresującej, powypadkowej sytuacji.
✅
Nic do zrobienia ręcznie po incydencie
Wystarczy standardowa procedura: wygeneruj nowe hasło główne w zakładce Szyfrowanie, nowe udziały, nowy secret-key.php w zakładce Konfiguracja i wgraj go na serwer. Stary timelock.json (razem ze starą blokadą) przestaje obowiązywać automatycznie, bo nie pasuje już do odcisku nowej konfiguracji.
/home/user/private/← POZA public_html !
├── secret-key.php← $people, $downloads, $instructions, $email_notify...
├── lang.php← Teksty interfejsu — jeden język, patrz t()
├── rate-limit.php← Trwały rate-limiting (niezależny od sesji)
├── rate_limits.json← Liczniki prób logowania (tworzy się automatycznie)
├── trusted_devices.json← Tokeny zaufanych urządzeń (tworzy się automatycznie)
├── timelock.json← Stan blokady czasowej (tworzy się automatycznie po pierwszej rekonstrukcji)
├── secret-key.log← Logi zdarzeń
└── moja-baza-hasel.kdbx← Pliki do pobrania — serwowane tylko przez download.php
ℹ️
secret-key.php trzyma teraz cały config, nie tylko hasła
Poza loginami i hashami haseł ($people), ten sam plik trzyma listę plików do pobrania ($downloads), kroki instrukcji ($instructions), teksty sekcji pobierania i ustawienia powiadomienia email ($email_notify). Wszystko generuje dashboard — patrz Konfiguracja treści i danych.
ℹ️
rate-limit.php i rate_limits.json
rate-limit.php udostępnia funkcje rateLimitCheckAndIncrement() i rateLimitReset(), wymagane przez auth.php do trwałego liczenia prób logowania (plik, nie sesja). Katalog private/ musi mieć prawo zapisu — rate_limits.json tworzy się automatycznie przy pierwszej próbie logowania, nie trzeba go zakładać ręcznie.
ℹ️
timelock.json
Nie istnieje dopóki nikt nie odzyska hasła w panelu — tworzy się automatycznie przy pierwszej udanej rekonstrukcji sekretu. Ważność jest powiązana z odciskiem (hashem) aktualnego $people, więc regeneracja secret-key.php automatycznie unieważnia stary plik. Szczegóły: Ochrona przed zmową powierników.
# Otwierasz lokalnie w przeglądarce (file://)
├── dashboard.html← Główny panel (iframe z zakładkami)
├── generate-hash.html← Generator konfiguracji bcrypt
├── generate-shamir.html← Generator udziałów Shamira
├── generate-card.html← Generator kart PDF
├── card-front.js← Szablon SVG przodu karty (~553KB)
├── card-back.js← Szablon SVG tyłu karty (~41KB)
└── favicon.ico
ℹ️
card-front.js i card-back.js
Muszą znajdować się w tym samym folderze co generate-card.html. Zawierają szablony SVG kart jako zmienne JS (CARD_FRONT_TPL, CARD_BACK_TPL) ładowane przez <script src="...">.
System wysyła kody 2FA przez SMSPlanet API. Wymagane:
Element
Opis
Konto SMSPlanet
Aktywne konto z saldem na wysyłkę SMS
Token API
Wpisywany do secret-key.php podczas konfiguracji
Numer telefonu
Jeden unikalny numer per wyznaczona osoba
Treść SMS-a i autouzupełnianie kodu (WebOTP)
Treść wiadomości SMS składa się z dwóch części z osobnych źródeł: sam tekst kodu pochodzi z lang.php (klucz twofa_sms_body, przez t() — dokładnie ten sam mechanizm vsprintf co przy verify_attempts_left_*), a przyrostek do autouzupełniania WebOTP budowany jest w auth.php ze stałej ustawionej w secret-key.php:
php
// secret-key.phpdefine('SMS_AUTOFILL_DOMAIN', '@moja-domena.pl');
// private/lang.php — %s = kod, %d = ważność w minutach'twofa_sms_body' => 'Kod weryfikacyjny: %s. Wazny %d min. Nie udostepniaj go nikomu.',
// auth.php — treść z lang.php + przyrostek WebOTP z secret-key.php$msg = t('twofa_sms_body', $code, $ttlMin) . "\n\n" . SMS_AUTOFILL_DOMAIN . " #$code";
Ostatni fragment (@domena #kod) to nie literówka ani ozdobnik — to format wymagany przez WebOTP API. Dzięki niemu przeglądarka na telefonie (Chrome/Android, Safari/iOS) sama rozpoznaje kod w przychodzącym SMS-ie i automatycznie wypełnia pole na stronie logowania, bez kopiowania go ręcznie z aplikacji SMS.
⚠️
Bez polskich znaków w treści SMS-a
Wartość twofa_sms_body w lang.php jest celowo zapisana bez polskich znaków diakrytycznych (ż/ą/ę/ć...) — SMS zawierający choćby jeden znak spoza zestawu GSM-7 jest rozliczany przez operatora jako droższy/krótszy limit znaków na wiadomość (czasem podział na kilka SMS-ów). Tłumacząc ten klucz na inny język, zachowaj tę samą zasadę — unikaj znaków diakrytycznych w treści SMS-a, nawet jeśli reszta lang.php ich używa bez ograniczeń.
⛔
Zmień domenę na swoją — inaczej autouzupełnianie nie zadziała
SMS_AUTOFILL_DOMAIN musi być dokładną domeną, pod którą faktycznie hostujesz system (bez https://, bez ścieżki). WebOTP porównuje tę wartość z domeną strony, na której użytkownik jest zalogowany — jeśli się nie zgadzają, przeglądarka po prostu zignoruje SMS i nie zaproponuje autouzupełnienia. Kod nadal dotrze i zadziała po ręcznym wpisaniu, ale wygoda automatycznego wypełnienia zniknie. Szczegóły — patrz Konfiguracja treści i danych.
Pobierz pliki projektu i otwórz dashboard.html lokalnie w Chrome.
Zakładka Konfiguracja
1
Dodaj wszystkie wyznaczone osoby
Dla każdej osoby podaj: unikalny login, silne hasło, imię, nazwisko, numer telefonu komórkowego (wybierz kraj z rozwijanej listy — flaga i kierunkowy — a obok wpisz sam numer lokalny, bez kierunkowego) i czy ma być widoczna na liście posiadaczy w panelu.
2
Uzupełnij pliki, instrukcję i powiadomienia
Klucz/etykietę/nazwę pliku dla każdego pliku do pobrania, kroki instrukcji panelu (z formatowaniem **pogrubienie**/*kursywa*), oraz adres i domenę powiadomienia email.
3
Ustaw domenę SMS i wygeneruj
Wpisz domenę do autouzupełniania SMS (własna domena z @ na początku), a następnie kliknij „Generuj konfigurację" — hasła są hashowane bcrypt w przeglądarce. Pobierz secret-key.php.
Zakładka Szyfrowanie
1
Podziel hasło główne
Wpisz hasło do bazy KeePassXC, ustaw parametry (zalecane: 5 udziałów, próg 3), wygeneruj i pobierz secret-key-shares.txt.
# Wgraj folder /app/ do public_html (zawiera też /decrypt/ wewnątrz)
scp -r ./app/ user@serwer:~/public_html/
# Wgraj konfigurację i teksty interfejsu POZA public_html
scp ./secret-key.php ./lang.php user@serwer:~/private/
⚠️
lang.php musi trafić na serwer razem z secret-key.php
auth.php ładuje lang.php przez require_once — jeśli plik nie trafi do /private/, cały system przestanie się ładować (fatal error na każdej stronie), nie tylko przestaną działać tłumaczenia. Wgrywaj oba pliki w tej samej operacji.
#2a. Wyjątek w CDN/cache (jeśli używasz Cloudflare lub podobnego)
Jeśli Twoja domena ma włączony cache na poziomie całej strony (np. reguła typu „Cache Everything" w Cloudflare, albo podobny mechanizm u innego dostawcy CDN), musisz dodać wyjątek dla katalogu, w którym stoi system, zanim zaczniesz cokolwiek testować.
⛔
Dlaczego to nie wystarczy załatwić samym .htaccess
System wysyła własne nagłówki Cache-Control: no-store (patrz app/.htaccess i app/decrypt/.htaccess), ale tryb „Cache Everything" w Cloudflare domyślnie ignoruje nagłówki Cache-Control z originu — cache'uje odpowiedź niezależnie od tego, co wysyła serwer. Bez osobnej reguły „bypass" na poziomie CDN, panel logowania, kod 2FA i strona z odszyfrowanym hasłem mogą zostać zapisane w cache'u i serwowane innym odwiedzającym.
Przykład dla Cloudflare (Caching → Cache Rules):
1
Utwórz nową regułę cache
Nazwa np. „Bypass cache — Secret Key". Warunek: URI Pathcontains/app/ (podmień na rzeczywistą ścieżkę, pod którą wgrałeś system).
2
Ustaw akcję na „Bypass cache"
To wyłącza cache CDN dla wszystkiego pod tą ścieżką — niezależnie od nagłówków wysyłanych przez origin.
3
Ustaw regułę jako pierwszą w kolejności
Jeśli masz też ogólną regułę cache'ującą całą domenę, reguła „bypass" musi być wyżej na liście (kolejność „First") — reguły cache wykonują się po kolei i wygrywa pierwsze trafienie.
💡
Jak sprawdzić, czy zadziałało
Otwórz panel logowania w przeglądarce, zakładka Sieć/Network w narzędziach deweloperskich, sprawdź nagłówki odpowiedzi. Cloudflare dodaje nagłówek cf-cache-status — dla ścieżek objętych wyjątkiem powinien pokazywać BYPASS lub DYNAMIC, nigdy HIT.
Przed wdrożeniem warto sprawdzić czy system działa poprawnie — bez wysyłania prawdziwych SMS-ów i zużywania tokenów SMSPlanet. Plik app/auth.php zawiera wbudowany tryb testowy.
php
// ── TRYB TESTOWY — usuń przed wdrożeniem na produkcję! ──// $smsResult = true; $code = '123456';// ────────────────────────────────────────────────────────$smsResult = sendSmsCode($phoneFull, $code);
Aby włączyć tryb testowy — odkomentuj linię z $smsResult = true i zakomentuj wywołanie sendSmsCode():
php
// ── TRYB TESTOWY — usuń przed wdrożeniem na produkcję! ──$smsResult = true; $code = '123456'; // ← odkomentuj// ────────────────────────────────────────────────────────// $smsResult = sendSmsCode($phoneFull, $code); // ← zakomentuj
Po włączeniu trybu testowego — kod weryfikacyjny SMS będzie zawsze wynosił 123456, niezależnie od numeru telefonu. Możesz spokojnie przetestować cały przepływ logowania bez kosztów.
⛔
Usuń tryb testowy przed wdrożeniem na produkcję
Pozostawienie odkomentowanego $smsResult = true na produkcji całkowicie wyłącza weryfikację 2FA — każdy będzie mógł zalogować się wpisując kod 123456. Przed udostępnieniem systemu wyznaczonym osobom przywróć oryginalne wywołanie sendSmsCode().
💡
Schemat testowania
Zalecana kolejność: włącz tryb testowy → przetestuj logowanie wszystkimi kontami z kart → sprawdź czy rekonstrukcja Shamira zwraca poprawne hasło → wyłącz tryb testowy → rozdaj karty wyznaczonym osobom.
W dashboard.html przejdź do zakładki Generator kart. Upewnij się, że card-front.js i card-back.js są w tym samym folderze.
2
Uzupełnij dane
Wpisz adres instancji, loginy, hasła i udziały Shamira z pliku secret-key-shares.txt dla każdej osoby.
3
Generuj i drukuj
Kliknij „Generuj karty", następnie „Pobierz PDF wszystkich kart". Wydrukuj i rozdaj wyznaczonym osobom. Zalecane: laminowanie kart.
✅
Instalacja zakończona
System jest aktywny. Przetestuj logowanie jedną z kart — wejdź na adres instancji, zaloguj się danymi z karty i zweryfikuj kod SMS. Następnie sprawdź rekonstrukcję hasła w panelu odszyfrowania.
Wszystko na tej stronie generuje dashboard.html (zakładka Konfiguracja) do jednego pliku — secret-key.php. Poniżej opisane jest, co faktycznie trafia do configu i po co, żebyś wiedział, co wypełniasz w formularzu (albo co zmieniasz, jeśli wolisz poprawić secret-key.php ręcznie).
✅
Jedno źródło prawdy
Cała treść i dane panelu — osoby, pliki do pobrania, instrukcja, powiadomienia — trzyma jeden plik: secret-key.php. Zmieniasz dane w jednym miejscu, a panel, przyciski pobierania i biała lista w download.php aktualizują się automatycznie.
$people to jedna tablica z jednym rekordem na osobę — login, hasło, dane kontaktowe i widoczność w panelu w jednym miejscu.
php
$people = [
[
'login' => 'anna',
'password' => '$2y$10$...', // hash bcrypt — generowany przez dashboard'first_name' => 'Anna',
'last_name' => 'Nowak',
'phone_cc' => '+48', // kierunkowy — osobno, dowolny kraj'phone' => '123456789', // sam numer lokalny, bez kierunkowego'show_in_panel' => true,
],
// kolejne osoby...
];
Pole
Opis
login
Login do panelu — unikalny, wpisywany przez osobę na karcie
password
Hash bcrypt hasła. Dashboard hashuje w przeglądarce — nigdy nie wpisujesz tu hasła jawnego
first_name / last_name
Wyświetlane w powitaniu panelu i na liście posiadaczy
phone_cc
Kierunkowy kraju w formacie +XX (np. +48, +49, +31) — osobne pole, niezależne od phone. Dzięki temu system działa poprawnie dla dowolnego kraju, nie tylko Polski
phone
Sam numer lokalny do kodu 2FA, bez kierunkowego (ten jest w phone_cc). Z obu tych pól liczone są oba warianty wyświetlania (pełny na liście posiadaczy — bez kierunkowego, zamaskowany na ekranie logowania — z prawdziwym kierunkowym)
show_in_panel
false = konto loguje się normalnie, ale nie pojawia się na liście posiadaczy. Przydatne dla kont bez fizycznej karty (np. administracyjnych)
⚠️
Migracja ze starszej wersji configu
Wcześniejsze wersje trzymały cały numer w jednym polu phone razem z kierunkowym (np. 'phone' => '+48123456789'). Jeśli aktualizujesz istniejący secret-key.php, rozdziel to ręcznie na dwa pola: 'phone_cc' => '+48' i 'phone' => '123456789' — dla każdej osoby osobno. Bez tej migracji logowanie 2FA nie zadziała poprawnie (kod SMS pójdzie na niekompletny numer).
ℹ️
Liczba osób w $people nie musi się zgadzać z liczbą udziałów Shamira
$people to lista kont logowania do panelu — kto może wejść i kto widnieje na liście posiadaczy do skontaktowania. To osobna sprawa od liczby udziałów Shamira (kart), które odtwarzają hasło główne. Można mieć np. 6 kont w $people (w tym jedno administracyjne z show_in_panel => false) i 5 udziałów Shamira dla 5 z nich.
⚠️
Pusta lista posiadaczy = czerwony alert w panelu
Jeśli wszystkie osoby mają show_in_panel => false (albo $people jest puste), panel nie zostaje po prostu pusty — pokazuje widoczny, czerwony komunikat z podpowiedzią, co skonfigurować. To celowe: łatwiej złapać błąd configu na etapie testowania niż domyślać się, dlaczego lista jest pusta.
$downloads to jedno źródło zarówno dla przycisków pobierania w panelu, jak i białej listy w download.php — to dosłownie ta sama tablica, więc oba miejsca są zawsze zgodne ze sobą.
Identyfikator w adresie pobierania (download.php?file=klucz). Musi być unikalny
label
Tekst na przycisku w panelu
filename
Rzeczywista nazwa pliku leżącego w /private/
name
Opcjonalne. Obecność tego klucza oznacza „to jest instalator programu do odszyfrowywania" — jego wartość trafia do nagłówka panelu („Hasło do bazy X")
⛔
Sam plik nadal wgrywasz ręcznie
$downloads trzyma tylko metadane — klucz, etykietę i nazwę pliku. Sama zawartość (baza haseł, baza 2FA, instalator programu) nigdy nie przechodzi przez dashboard ani przez ten config. Musisz osobno wgrać każdy plik na serwer do /private/, pod dokładnie taką nazwą jak w polu filename.
Obok samej listy plików, trzy dodatkowe zmienne kontrolują teksty tej sekcji panelu:
php
$download_heading = 'Pliki i program do odzyskania dostępu'; // zwykły tekst, nagłówek <h3>$download_intro = 'Pobierz bazę haseł, bazę kodów 2FA oraz program...'; // zwykły tekst, akapit nad przyciskami$alert_box_text = 'Pamiętaj: bez fizycznego klucza YubiKey...'; // zwykły tekst, niebieski box z ikoną
⚠️
Nagłówek i przynajmniej jeden plik są wymagane razem
Jeśli $download_heading jest puste albo$downloads jest puste, cała siatka przycisków zamienia się w czerwony alert z instrukcją, co uzupełnić — nawet jeśli tylko jedna z tych dwóch rzeczy brakuje. To zamierzone: sekcja bez tytułu albo bez plików nie ma sensu wyświetlać częściowo. $download_intro zostawiony pusty po prostu nie pokaże akapitu, a $alert_box_text pusty całkowicie ukrywa niebieski box — żadne z tych dwóch nie blokuje przycisków.
ℹ️
Dopasuj $download_heading do liczby plików
Polska odmiana liczebników nie jest automatyzowana — jeśli masz tylko jeden plik do pobrania (np. samą bazę haseł + program, bez osobnej bazy 2FA), wpisz „Plik i program do odzyskania dostępu" zamiast „Pliki i program...". To zwykłe pole tekstowe, w pełni pod Twoją kontrolą.
Kroki instrukcji w panelu też są danymi, nie kodem HTML. Każdy krok to jedno pole tekstowe z prostym formatowaniem — markdown-lite.
php
$instructions = [
['num' => '01', 'text' => '**Zbierzcie się razem.** Skontaktujcie się z osobami z listy — potrzebujecie minimum **3 osoby z 5**.'],
['num' => '02', 'text' => '**Wejdźcie na tę stronę razem.** Każda osoba potrzebuje swojej karty (np. *8015c7c4...*).'],
// kolejne kroki...
];
Formatowanie
Efekt
**tekst**
Pogrubienie (<strong>) — użyj np. na tytuł kroku na początku zdania
*tekst*
Kursywa (<em>) — użyj np. na przykładowy kod czy nazwę pliku
ℹ️
To wyłącznie markdown-lite, nic więcej
Obsługiwane są tylko te dwa znaczniki — żadnych list, nagłówków ani linków. Renderowanie robi funkcja md_lite() w auth.php: najpierw htmlspecialchars() całego tekstu (więc wpisany przez pomyłkę znacznik HTML wyrenderuje się jako martwy tekst, nie jako kod), dopiero potem podmiana **/* na tagi.
⚠️
Puste $instructions = czerwony alert zamiast listy kroków
Ten sam wzorzec co przy liście posiadaczy i plikach do pobrania — brak choćby jednego kroku pokazuje widoczne ostrzeżenie w panelu zamiast po prostu pustego miejsca.
Panel może wysyłać email po każdym udanym logowaniu — z adresem IP, przeglądarką i czasem. Wszystkie dane nadawcy/odbiorcy trzyma jedna tablica, żeby domena nie powtarzała się osobno w nagłówkach From, Reply-To, Return-Path, X-Sender i Message-ID.
false wyłącza wysyłkę całkowicie — bez ruszania kodu
to
Adres, na który przychodzi powiadomienie o każdym logowaniu
from_email
Adres nadawcy. Domena stąd trafia też automatycznie do Message-ID
from_name
Nazwa nadawcy widoczna w kliencie pocztowym
panel_url
Link do panelu wklejany w treści wiadomości
text
Temat: 🔐 Logowanie do Secret Key Panel — Jan Kowalski
Nowe logowanie do panelu Secret Key.
─────────────────────────────
Użytkownik: Jan Kowalski (jan)
Data i czas: 01.05.2026 14:32:17
Adres IP: 89.123.45.67
Przeglądarka: Mozilla/5.0 (Windows NT 10.0...)
─────────────────────────────
Panel: https://moja-domena.pl
ℹ️
Email wysyłany jest raz po zalogowaniu
Powiadomienie jest wysyłane jednorazowo po poprawnym przejściu weryfikacji 2FA — nie blokuje przekierowania do panelu ani nie spowalnia logowania. Jeśli enabled => false, nic nie jest wysyłane ani logowane — to świadome wyłączenie. Jeśli enabled => true, ale brakuje to lub from_email, w logu pojawi się MAIL SKIPPED — to sygnał błędu configu, nie cichej awarii. Jeśli wysyłka się faktycznie nie powiedzie (np. serwer bez funkcji mail()), zapisywane jest MAIL FAILED.
⛔
enabled => false wyłącza też alert Panic Button
Te same dane nadawcy/odbiorcy z $email_notify (to, from_email, from_name) są używane przez arm-timelock.php do wysyłki alertu bezpieczeństwa z linkiem Panic Button, gdy ktoś po raz pierwszy odzyska hasło w panelu — patrz Ochrona przed zmową powierników. Ta wysyłka respektuje to samo pole enabled co zwykłe powiadomienie o logowaniu (log TIMELOCK MAIL SKIPPED, jeśli wyłączone) — jeśli wyłączysz powiadomienia email, nie dostaniesz też alertu o odzyskaniu hasła ani linku blokującego. Blokada czasowa (48h) sama w sobie uzbraja się zawsze, niezależnie od enabled — traci się tylko mail z linkiem Panic Button do natychmiastowego, wcześniejszego zablokowania.
⚠️
Aby uniknąć folderu spam
Użyj adresu no-reply@ na tej samej domenie co serwer. Jeśli Twój serwer ma skonfigurowane rekordy SPF i DKIM — emaile będą trafiać do skrzynki głównej.
Domena widoczna w treści SMS-a (do automatycznego wypełniania kodu na Android/iOS — patrz strona Wymagania) też jest teraz configiem, nie ręczną edycją auth.php:
php
define('SMS_AUTOFILL_DOMAIN', '@moja-domena.pl');
⛔
Musi zaczynać się od @ i być dokładną domeną
Bez https://, bez ścieżki — sama domena, na której faktycznie hostujesz system. WebOTP porównuje tę wartość z domeną strony logowania; jeśli się nie zgadzają, przeglądarka po prostu nie zaproponuje autouzupełnienia (kod nadal zadziała po ręcznym wpisaniu).
Panel odzyskiwania (/decrypt/index.php) musi wiedzieć, ile kodów z kart wymaganych jest do złożenia hasła — czyli ten sam próg (K), który ustawiasz w zakładce dashboardu „Szyfrowanie" przy dzieleniu hasła głównego na udziały:
php
define('SHARES_REQUIRED', 4); // np. 4 z 7
⚠️
Musi być zgodne z progiem z zakładki Szyfrowanie
Dashboard nie ma wspólnego stanu między zakładkami — nikt automatycznie nie sprawdzi, czy wartość tutaj zgadza się z tym, co wpisałeś przy generowaniu udziałów. Jeśli się rozjadą, panel będzie żądać innej liczby kodów niż realnie potrzeba do złożenia hasła. Najłatwiej ustawić to pole w zakładce Konfiguracja dashboardu (sekcja „Ustawienia podstawowe") zamiast edytować plik ręcznie.
ℹ️
Brak tej stałej w starym configu? Nic się nie dzieje
Domyślny fallback to 3 — configi sprzed wprowadzenia tego pola działają dokładnie tak jak wcześniej, bez żadnej migracji.
System celowo nie ma przełącznika języka ani osobnych plików pl.php/en.php. Jest jeden plik lang.php z jednym językiem naraz (domyślnie polski). Powód: treść, którą wpisujesz w $instructions czy $alert_box_text, jest osobista — piszesz ją dla konkretnych, znanych sobie ludzi. Tłumaczenie samego interfejsu bez przepisania tej treści na nowo nie miałoby większego sensu.
Jeśli chcesz uruchomić system w innym języku niż polski, edytujesz lang.php ręcznie — całość, jednorazowo — oraz przepisujesz treść w secret-key.php ($instructions, $download_intro, itd.) na ten sam język. To świadomy wybór prostoty zamiast rozbudowanej warstwy i18n, której i tak nikt by tu nie przełączał w locie.
Jeśli t('cos') odwoła się do klucza, którego nie ma w $lang, funkcja po cichu zwraca sam klucz jako widoczny tekst zastępczy — np. zobaczysz na stronie napis login_submit_btn zamiast „Zaloguj się". Nie dostaniesz błędu PHP na produkcji, a literówka w kluczu jest od razu widoczna gołym okiem, więc łatwo ją złapać przy testowaniu.
t() wspiera też placeholdery w stylu sprintf — np. do poprawnej polskiej odmiany liczby pozostałych prób:
Większość tekstów jest escapowana przy renderowaniu (bezpiecznie, jak każdy inny input). Kilka kluczy — te zawierające celowe znaczniki <strong> czy encje (np. tekst wprowadzający na ekranie logowania) — renderuje się bez escapowania, żeby formatowanie działało. Są jasno oznaczone komentarzem w lang.php. Edytując je, zachowaj tagi i encje w całości — dosłowny znak & wpisany jako zwykły tekst wyrenderuje się jako widoczny &, nie jako spacja czy pogrubienie.
Same klucze (lewa strona =>) zostają bez zmian — tłumaczysz tylko wartości tekstowe po prawej.
2
Zmień atrybut _html_lang
$lang['_html_lang'] steruje atrybutem <html lang="..."> na obu stronach (logowanie i panel) — zmień np. z 'pl' na 'en'.
3
Przepisz własną treść w secret-key.php
$instructions, $download_heading, $download_intro, $alert_box_text — to Twoja treść, nie generyczny UI, więc lang.php jej nie obejmuje. Przepisujesz ją osobno, tym samym wzorcem co resztę configu (patrz Konfiguracja treści i danych).
Sama karta jest bezużyteczna bez dostępu do telefonu przypisanego do danego konta — logowanie wymaga weryfikacji SMS. Ryzyko jest ograniczone, jednak właściciel powinien zostać poinformowany i rozważyć wygenerowanie nowej konfiguracji z nowym zestawem udziałów. Po zastąpieniu pliku secret-key.php na serwerze, stare karty przestają działać.
Nie. Jest to matematycznie niemożliwe. Pojedynczy udział nie ujawnia żadnej informacji o sekrecie — to własność algorytmu Shamira zwana information-theoretic security. Dopiero zebranie wymaganej liczby udziałów pozwala na odtworzenie hasła głównego.
#Co jeśli jedna z wyznaczonych osób umrze lub będzie niedostępna?
System jest zaprojektowany z nadmiarowością — wystarczy zebrać minimalną wymaganą liczbę udziałów (np. 3 z 5). Niedostępność jednej lub dwóch osób nie blokuje procedury awaryjnej.
#Czy hasło trafia na serwer podczas odszyfrowania?
Nie. Rekonstrukcja hasła z udziałów Shamira odbywa się całkowicie po stronie przeglądarki (JavaScript). Serwer służy tylko do uwierzytelnienia użytkownika — sam sekret nigdy go nie opuszcza.
#Co jeśli powiernicy zmówią się i odzyskają hasło za mojego życia, bez mojej wiedzy?
Samo hasło im nie wystarczy. Pliki bazy danych leżą poza katalogiem publicznym serwera, a dostęp do nich kontroluje download.php. W momencie pierwszej udanej rekonstrukcji hasła w panelu system automatycznie blokuje pobieranie plików na 48 godzin i wysyła Ci e-mail z alertem oraz jednorazowym linkiem „Panic Button" — jedno kliknięcie trwale odcina dostęp, dając Ci czas na zmianę hasła głównego w spokoju. Szczegóły: Ochrona przed zmową powierników. Jeśli dodatkowo zabezpieczasz bazę haseł kluczem sprzętowym (np. YubiKey), sama znajomość hasła nie wystarczy do jej otwarcia nawet po odblokowaniu plików.
#Czy mogę używać systemu z innym menedżerem haseł niż KeePassXC?
Tak. Secret Key przechowuje i odtwarza dowolne hasło główne — niezależnie od używanego menedżera. Kompatybilny z KeePassXC, Bitwarden, 1Password i każdym innym programem obsługującym hasło główne.
Im wyższy próg, tym większe bezpieczeństwo — ale też trudniejsze zebranie się w sytuacji kryzysowej. Rekomendowany kompromis to 3 z 5 — pozwala na niedostępność dwóch osób przy zachowaniu dobrego poziomu ochrony. Wybrany próg wpisz też jako SHARES_REQUIRED w configu (pole „Wymagane kody" w zakładce Konfiguracja dashboardu) — patrz Konfiguracja treści i danych — inaczej panel odzyskiwania będzie żądał innej liczby kodów niż realnie potrzeba.
Bezterminowo — o ile właściciel nie wygeneruje nowej konfiguracji. Po zastąpieniu pliku secret-key.php na serwerze stare karty przestają działać i konieczne jest rozdanie nowych wszystkim wyznaczonym osobom.
#Dlaczego mail z alertem bezpieczeństwa (Panic Button) czasem trafia do spamu?
Ten alert wysyłany jest automatycznie po pierwszej udanej rekonstrukcji hasła w panelu — patrz Ochrona przed zmową powierników. Taki mail zawiera jednorazowy link z długim, losowym tokenem (panic.php?token=...) — automatyczne filtry antyspamowe często oznaczają linki tego kształtu, niezależnie od treści maila, bo pod względem struktury przypominają linki phishingowe. To dotyczy tylko maila z alertem — zwykłe powiadomienia o logowaniu (bez klikalnego linku z tokenem) zwykle trafiają do odebranych bez problemu.
Rozwiązanie: dodaj adres nadawcy (domyślnie no-reply@twoja-domena, skonfigurowany w $email_notify['from_email']) do białej listy w panelu poczty, na który przychodzą powiadomienia — w cPanel jest to sekcja „Biała lista" (Email → Biała lista), gdzie można dodać zaufanego nadawcę osobno dla wybranej skrzynki. Po dodaniu wyjątku mail z Panic Buttonem trafia już bezpośrednio do odebranych.
Jak zgłaszać luki bezpieczeństwa i czego możesz oczekiwać w odpowiedzi.
Zwykłe błędy, literówki w dokumentacji czy propozycje ulepszeń zgłaszaj normalnie jako Issue na GitHubie — to standardowa droga i nie ma w tym nic tajnego. Ta strona dotyczy wyłącznie luk bezpieczeństwa (patrz zakres zgłoszeń niżej) — tych proszę nie zgłaszać publicznie.
⛔
Luki bezpieczeństwa zgłaszaj tylko prywatnie
Wyłącznie na dev@secretkey.website — nigdy jako publiczne Issue na GitHubie. Publiczne ujawnienie exploita przed opublikowaniem łatki naraża wszystkich, którzy mają wdrożony system.
Obejście uwierzytelnienia (bcrypt, 2FA) · Podatności CSRF mimo zabezpieczeń · Odtworzenie sekretu bez wymaganej liczby udziałów · Ujawnienie danych z /private/ · Obejście rate limitingu · XSS
❌
Poza zakresem
Ataki wymagające fizycznego dostępu do serwera · Ataki socjotechniczne na posiadaczy kart · Błędna konfiguracja serwera po stronie użytkownika · Raporty skanerów bez PoC
Dashboard to lokalny panel konfiguracyjny Secret Key — plik HTML otwierany bezpośrednio w przeglądarce bez serwera. Zawiera trzy zakładki: Konfiguracja, Szyfrowanie i Generator kart.
ℹ️
Działa całkowicie offline
Otwórz plik dashboard.html lokalnie w Chrome (Plik → Otwórz plik lub przeciągnij na okno przeglądarki). Żadne dane nie są wysyłane przez sieć — wszystkie operacje odbywają się wyłącznie w pamięci przeglądarki.
Widok główny dashboard.html z widocznymi czterema zakładkami nawigacyjnymi
1
Pasek zakładek
Cztery przyciski nawigacyjne u góry ekranu: Home, Konfiguracja, Szyfrowanie, Generator kart. Aktywna zakładka jest podświetlona gradientem fioletowym.
2
Obszar roboczy
Główna część ekranu renderowana jako <iframe> — każda zakładka ładuje osobny plik HTML (generate-hash.html, generate-shamir.html, generate-card.html).
3
Przełącznik języka (PL/EN)
Przypięty po prawej stronie paska zakładek, w formie animowanego suwaka (pill). Przełącza dashboard i zapamiętuje wybór w localStorage (klucz sk_lang). Wszystkie cztery pliki dashboardu — łącznie ze stroną Home (dashboard.html) — mają własny, niezależny przełącznik; nie są ze sobą synchronizowane. Zmiana języka przebudowuje już wygenerowane wyniki (hashe, podgląd configu, karty) bez ponownego przeliczania.
4
Strona Home
Ekran startowy z logo Secret Key, krótkim opisem narzędzia i dwoma skrótami — Konfiguracja i Szyfrowanie — przenoszącymi bezpośrednio do tych zakładek.
Dashboard używa <iframe src="..."> do ładowania zakładek. Jeśli pliki generate-*.html lub card-*.js nie znajdą się w tym samym folderze, zakładki nie załadują się poprawnie.
Generuje plik secret-key.php — serce systemu. Zawiera zahashowane hasła bcrypt, dane wszystkich wyznaczonych osób, pliki do pobrania, instrukcję panelu i ustawienia powiadomienia email. Operacja w pełni offline — nic nie jest nigdzie wysyłane.
Token API SMSPlanet, nazwa nadawcy SMS, domena do autouzupełniania kodu na Android/iOS (musi zaczynać się od @ i być Twoją rzeczywistą domeną — patrz Konfiguracja treści i danych) oraz pole Wymagane kody — próg (K) udziałów Shamira wymaganych w panelu odzyskiwania, ze stepperem +/− i podpowiedzią przypominającą o zgodności z zakładką Szyfrowanie.
2
Użytkownicy systemu
Każda osoba to karta z polami: Login i Hasło (oba mają obok siebie ikony do automatycznego generowania oraz kopiowania do schowka — hasło generowane jest jako 12 znaków, duże i małe litery), Imię, Nazwisko, kierunkowy kraju (przeszukiwalna lista 186 krajów z flagami) i Telefon (sam numer lokalny, do 2FA) oraz przełącznik Widoczny w panelu — wyłącz go dla kont bez fizycznej karty (np. administracyjnych). Przycisk „+ Dodaj kolejnego użytkownika" dodaje kolejne karty, „✕ Usuń" na nagłówku karty ją zdejmuje.
3
Pliki do pobrania
Nagłówek sekcji, tekst wprowadzający i tekst ostrzeżenia (wszystkie trzy — zwykły tekst, dopasuj gramatykę do liczby plików). Dla każdego pliku: klucz (identyfikator w adresie), etykieta przycisku i pole na nazwę pliku — wskazujesz plik lokalnie tylko po to, by wpisać jego nazwę, żadna zawartość nigdzie nie jest wysyłana. Przełącznik „To instalator programu" dodaje pole „Nazwa programu", które zasila nagłówek „Hasło do bazy X" w panelu.
4
Instrukcja
Jedno pole tekstowe na krok (bez osobnych pól „tytuł"/„treść"). Wspiera formatowanie **pogrubienie** i *kursywa* — numer kroku liczy się automatycznie z kolejności kart.
5
Powiadomienie e-mail o logowaniu
Przełącznik włącz/wyłącz, adres odbiorcy, adres i nazwa nadawcy, oraz link do panelu wklejany w treści wiadomości. Wyłącznik nie kasuje wypełnionych pól — można wyłączyć i włączyć bez ponownego wpisywania danych.
6
Przycisk „Generuj konfigurację"
Hashuje wszystkie hasła algorytmem bcrypt (cost=10) w przeglądarce i generuje plik PHP z gotową konfiguracją. Przed wygenerowaniem sprawdza też, czy loginy, hasła i numery telefonów są unikalne dla każdej osoby — jeśli coś się powtarza, pokazuje błąd i podświetla konfliktujące pola zamiast utworzyć plik. Operacja może potrwać kilka sekund przy wielu osobach.
7
Podgląd i pobieranie pliku
Po generowaniu pojawia się podgląd wygenerowanych hashy oraz pełny kod PHP. Przycisk „Pobierz secret-key.php" zapisuje plik na dysk.
Zaloguj się na smsplanet.pl, przejdź do ustawień konta → API i skopiuj token. Wklej go w pierwsze pole, a w polu domeny wpisz własną domenę z @ na początku.
2
Dodaj wyznaczone osoby
Dla każdej osoby wypełnij login (unikalny, bez spacji), silne hasło (min. 12 znaków), imię, nazwisko, kierunkowy kraju (wybierz z przeszukiwalnej listy 186 krajów — flaga, nazwa, kod) i sam numer telefonu, bez kierunkowego. Zdecyduj, czy dana osoba ma się pojawić na liście posiadaczy w panelu.
3
Uzupełnij pliki do pobrania
Dodaj wpis dla każdego pliku (baza haseł, baza 2FA, instalator programu). Klucze muszą być unikalne — trafiają do adresu pobierania.
4
Napisz instrukcję i skonfiguruj powiadomienia
Dodaj kroki instrukcji dla panelu oraz — jeśli chcesz dostawać maila o każdym logowaniu — uzupełnij sekcję powiadomień email.
5
Generuj i pobierz plik
Kliknij „Generuj konfigurację". Po zakończeniu hashowania pobierz secret-key.php i zachowaj go bezpiecznie — trafi na serwer poza katalogiem publicznym.
⛔
Nie zamykaj przeglądarki podczas hashowania
Bcrypt cost=10 przy wielu osobach może trwać kilkanaście sekund. Zamknięcie karty lub odświeżenie strony podczas operacji przerwie generowanie i będziesz musiał zacząć od nowa.
⚠️
Formularz generuje od zera, nie edytuje istniejącego pliku
Ten dashboard nie wczytuje istniejącego secret-key.php do edycji — każde uruchomienie „Generuj konfigurację" tworzy nowy plik od podstaw, na podstawie tego, co aktualnie wypełniłeś w formularzu. Drobne poprawki (np. zmianę jednego tekstu) często szybciej wprowadzić ręczną edycją samego secret-key.php na serwerze niż wypełniać cały formularz od nowa.
Dzieli hasło główne do bazy haseł na udziały algorytmem Shamira. Generuje plik secret-key-shares.txt z fragmentami klucza do rozdania wyznaczonym osobom.
Wpisz hasło do bazy KeePassXC (lub innego menedżera). Pole ma przycisk podglądu — kliknij oko żeby sprawdzić czy nie ma literówki przed podziałem.
2
Łączna liczba udziałów
Ile osób otrzyma fragment klucza. Musi odpowiadać liczbie osób w zakładce Konfiguracja. Zalecane: 5.
3
Wymagane udziały do odszyfrowania
Minimalna liczba osób potrzebna do odtworzenia hasła. Musi być mniejsza lub równa łącznej liczbie udziałów. Zalecane: 3.
4
Podgląd udziałów
Po wygenerowaniu każdy udział wyświetla się jako długi ciąg hex. Każda linia = jeden udział dla jednej osoby. Kolejność odpowiada kolejności osób z zakładki Konfiguracja.
5
Przycisk pobierania
Pobiera plik secret-key-shares.txt z nagłówkiem „Secret Key Sharing — udziały" i wszystkimi udziałami ponumerowanymi od 1 do N.
Wprowadź dokładnie to samo hasło które używasz do otwarcia bazy haseł. Użyj przycisku podglądu żeby upewnić się że nie ma błędu — po podziale nie ma możliwości weryfikacji bez zebrania udziałów.
2
Ustaw parametry podziału
Ustaw łączną liczbę udziałów (np. 5) i wymagane minimum (np. 3). Parametry muszą być spójne z liczbą osób w zakładce Konfiguracja.
3
Generuj i pobierz udziały
Kliknij „Generuj udziały". Pobierz plik secret-key-shares.txt — każdy udział (jedna linia) przypisz konkretnej osobie z listy z zakładki Konfiguracja.
4
Zweryfikuj odtworzenie
Przed rozdaniem kart warto przetestować: wpisz dowolne 3 udziały z pliku w zakładkę Szyfrowanie (tryb odtwarzania) i sprawdź czy hasło wraca poprawnie.
💡
Kompatybilność z iancoleman.io
Udziały są w pełni kompatybilne z iancoleman.io/shamir — możesz tam niezależnie zweryfikować odtworzenie hasła, wklejając dowolne wymagane minimum udziałów.
Tworzy spersonalizowane karty Secret Key w formacie ISO ID-1 (85.6×54mm) gotowe do wydruku lub laminowania. Każda karta zawiera dane logowania, udział Shamira i kod QR.
Adres instancji (URL) Twojego serwera Secret Key oraz imię i nazwisko właściciela — oba pola są wspólne dla wszystkich kart i wykorzystywane w treści wiadomości na tylnej stronie.
2
Parametry podziału
Łączna liczba udziałów (N) i Wymagane udziały do odszyfrowania (K) — muszą być spójne z tym, co ustawiłeś w zakładkach Konfiguracja i Szyfrowanie. Wpływają na domyślną treść wiadomości na karcie.
3
Wiadomość na karcie (tył)
Edytowalne pole tekstowe (licznik na żywo) z treścią widoczną na tylnej stronie każdej karty. Limit 320 znaków to wskazówka, nie blokada — licznik zmienia kolor na czerwony po przekroczeniu, ale pisanie dalej jest możliwe (miejsca na karcie jest jednak niewiele). Puste pole = domyślny tekst (widoczny jako podpowiedź). Dostępne żetony — {{N}} (liczba udziałów), {{N_MINUS_1}} (pozostałe osoby), {{K}} (wymagany próg), {{URL}} (adres instancji) — można wpisać ręcznie albo kliknąć odpowiednią pigułkę pod polem, żeby wstawić żeton dokładnie w miejscu kursora.
4
Opcje PDF — Spad i Linie cięcia
Spad 2mm — powiększa obszar druku o 2mm z każdej strony (standard drukarni, eliminuje białe krawędzie). Linie cięcia — drukuje znaczniki do precyzyjnego cięcia nożyczkami.
5
Lista kart (osób)
Każda karta to wiersz z polami: Login, Hasło i Udział Shamira (hex) — dane logowania z zakładki Konfiguracja, udział z pliku secret-key-shares.txt. Imię i nazwisko nie są tu wpisywane per osoba — pojawiają się raz, w „Ustawieniach globalnych" (dotyczą właściciela systemu, nie odbiorcy karty).
6
Podgląd karty
Po kliknięciu „Generuj karty" pojawia się podgląd przodu i tyłu każdej karty. Sprawdź czy dane są poprawne przed drukiem.
7
Pobieranie PDF
Dwa przyciski: ↓ Pobierz PDF karty (jedna karta, 2 strony) i ↓ Pobierz PDF wszystkich kart (wszystkie karty naraz). PDF generowany przez window.print() — Chrome obsługuje to najlepiej.
Podaj adres URL swojego serwera Secret Key — bez https://, np. secretkey.moja-domena.pl — oraz imię i nazwisko właściciela. Oba pola pojawią się w treści karty.
2
Ustaw parametry podziału
Liczba udziałów (N) i próg (K) muszą być spójne z tym, co ustawiłeś w zakładkach Konfiguracja i Szyfrowanie. Wpływają też na domyślną treść wiadomości na karcie.
3
Dostosuj wiadomość na karcie (opcjonalnie)
Zostaw pole puste, żeby użyć domyślnego tekstu, albo napisz własny. Licznik zmienia kolor na czerwony po 320 znakach jako wskazówka (nie blokuje pisania) — miejsca na karcie jest niewiele, więc warto się streszczać. Żetony {{N}}, {{N_MINUS_1}}, {{K}}, {{URL}} możesz wpisać ręcznie albo kliknąć ich pigułki pod polem, żeby wstawić je dokładnie w miejscu kursora.
4
Wybierz opcje druku
Jeśli zlecasz druk w drukarni — włącz Spad i Linie cięcia. Jeśli drukujesz samodzielnie na zwykłej drukarce — możesz zostawić oba wyłączone.
5
Uzupełnij dane każdej osoby
Dla każdej osoby wpisz login i hasło (z zakładki Konfiguracja) oraz udział Shamira (odpowiednia linia z pliku secret-key-shares.txt). Udziały przypisuj w tej samej kolejności co osoby.
6
Generuj podgląd i sprawdź
Kliknij „Generuj karty". Sprawdź dokładnie każdą kartę w podglądzie — zweryfikuj dane logowania, udział (pierwsze i ostatnie znaki hex) oraz adres instancji.
7
Pobierz i wydrukuj
Kliknij „Pobierz PDF wszystkich kart". W oknie druku Chrome: ustaw Marginesy: Brak, włącz Grafika w tle, rozmiar strony ustaw na niestandardowy zgodny z wybraną opcją spadu. Każda karta = 2 strony (przód + tył).
✅
Po wydrukowaniu — zalaminuj karty
Laminowanie chroni kartę przed zalaniem, zabrudzeniem i mechanicznym uszkodzeniem. Polecane folie: 80–125 µm. Nie używaj folii grubszych niż 150 µm — mogą powodować bąble na kolorowych gradientach SVG.