Docs
Wprowadzenie

Czym jest Secret Key?

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 ma dziesiątki haseł — do poczty, chmury ze zdjęciami, kodów 2FA, kryptowalut. Co się z nimi stanie po naszej śmierci? Banki i spadek mają własną drogę prawną, ale poczta, chmura czy krypto trzymane na własnym kluczu często zostają zamknięte na zawsze.

Secret Key niczego nie przechowuje i nie odszyfrowuje sam: dzieli hasło główne do Twojej bazy haseł (np. KeePass, Bitwarden) między zaufane osoby. Plik bazy leży na Twoim serwerze, a bliscy mogą go pobrać dopiero po zebraniu kworum, potwierdzeniu SMS-em i 48-godzinnej blokadzie. Co jest w bazie, decydujesz Ty. Secret Key nie jest narzędziem prawnym i nie zastępuje testamentu ani postępowania spadkowego; uzupełnia je tam, gdzie prawo nie pomoże.

Secret Key rozwiązuje ten problem, tworząc bezpieczny plan awaryjny — bez kompromisów wobec bezpieczeństwa za życia właściciela.

#Dwa cele systemu

Ochrona za życia
Każda próba dostępu wymaga unikalnego hasła i kodu SMS — nawet przy posiadaniu karty logowanie bez przypisanego telefonu jest niemożliwe.
Awaryjny dostęp po śmierci
Wyznaczone osoby zbierają wymaganą liczbę fragmentów klucza i rekonstruują hasło algorytmem Shamira — lokalnie w przeglądarce.

#Stack technologiczny

WarstwaTechnologiaOpis
BackendPHP 8+Pliki flat (JSON + PHP config), zero SQL
Autoryzacjabcrypt + SMScost=10, 2FA via SMSPlanet API
Kryptografiasecrets.jsShamir Secret Sharing, kompatybilny z iancoleman.io
FrontendHTML + JSZero zewnętrznych zależności, działa offline
HostingSelf-hostedDane nigdy nie opuszczają własnego serwera

#Jak to działa — procedura awaryjna

01
Zebranie wyznaczonych osób

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. Jeśli skonfigurowałeś SECRET_HASH, po odtworzeniu jest przesyłane przez HTTPS na Twój serwer wyłącznie w celu weryfikacji — nie jest zapisywane ani logowane. 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.
Wprowadzenie

Szybki start

Od zera do działającego systemu w czterech krokach. Konfigurację generujesz w pełni offline — samo wdrożenie wymaga własnego hostingu z PHP.

ℹ️
Narzędzia działają całkowicie offline
Otwórz dashboard.html lokalnie w przeglądarce (protokół file://). Żadne dane nie opuszczają Twojego komputera podczas konfiguracji.

#Krok 1 — Generowanie konfiguracji

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/).

#Krok 2 — Podział hasła algorytmem Shamira

Przejdź do zakładki Szyfrowanie w dashboard.html.

1
Wpisz hasło główne

Wprowadź hasło do bazy KeePassXC (lub innego menedżera haseł). Hasło jest przetwarzane wyłącznie lokalnie.

2
Ustaw parametry podziału

Wybierz łączną liczbę udziałów i wymagane minimum. Zalecane: 5 udziałów, próg 3.

3
Pobierz udziały

Kliknij „Generuj udziały" i pobierz plik secret-key-shares.txt. Każdy udział to jedna linia hex — przypisz je osobom z listy.

#Krok 3 — Wgranie na serwer

# 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.

W pliku auth.php (folder app/) ścieżka do folderu /private/ występuje w 7 miejscach. Trzeba poprawić wszystkie, nie tylko pierwszy require_once:

php
// app/auth.php
require_once __DIR__ . '/../../../private/secret-key.php';
require_once __DIR__ . '/../../../private/rate-limit.php';
require_once __DIR__ . '/../../../private/lang.php';

define('LOG_FILE',    __DIR__ . '/../../../private/secret-key.log');
define('PRIVATE_DIR', __DIR__ . '/../../../private');

// ×2 (rememberDevice() i sprawdzanie zaufanego urządzenia)
$file = __DIR__ . '/../../../private/trusted_devices.json';

Domyślnie w każdym z tych miejsc stoi fragment /../../../private. Ścieżka jest liczona od folderu, w którym leży auth.php, więc dopasuj ją do swojej struktury katalogów. Przykład: auth.php leży w /public_html/twoja-domena/, a /private/ obok /public_html/, więc zmieniasz /../../../private na /../../private we wszystkich siedmiu miejscach (w edytorze użyj „Zamień wszystko”).

#Krok 4 — Generowanie i dystrybucja kart

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.
Kryptografia

Algorytm Shamira

Shamir Secret Sharing — matematycznie gwarantuje, że mniejsza niż wymagana liczba fragmentów nie ujawnia żadnej informacji o sekrecie.

#Jak działa podział sekretu

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:

matematyka
f(x) = a₀ + a₁x + a₂x² + ... + aₖ₋₁xᵏ⁻¹  (mod p)

gdzie: a₀ = sekret (hasło główne)
       k   = wymagane minimum udziałów
       a₁..aₖ₋₁ = losowe współczynniki

#Przykład — podział 5/3

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.

#Parametry implementacji

ParametrWartośćUwagi
Bibliotekasecrets.jsgrempe/secrets.js (fork amper5and/secrets.js), zwendorowany przez iancoleman.io/shamir — stąd pełna kompatybilność formatu udziałów
Format udziałów8 + 2-hex x-coord + dataPrefiks 8 oznacza 8-bitowe ciało GF
minPad1024 bitówIdentyczny jak iancoleman — zapewnia kompatybilność
KodowanieUTF-8 (str2hex)Obsługa polskich znaków
RekonstrukcjaJavaScript (przeglądarka)Hasło nie jest zapisywane — przy SECRET_HASH weryfikowane na serwerze przez HTTPS

#Dobór parametrów

Łączna liczba udziałówWymagane minimumScenariusz
32Mała rodzina
53Standardowy (zalecany) — toleruje niedostępność 2 osób zalecany
74Większa rodzina lub firma
ℹ️
Niezależna weryfikacja
Format udziałów jest w pełni kompatybilny z iancoleman.io/shamir — możliwa jest niezależna weryfikacja odtworzenia hasła poza systemem Secret Key.
Kryptografia

Karty Secret Key

Spersonalizowany nośnik dla każdej wyznaczonej osoby — drukowany PDF w formacie ISO ID-1 (85.6×54mm), jak karta kredytowa.

#Elementy karty

Login i hasło
Unikalne dane dostępowe — każda osoba ma własne konto z przypisanym numerem telefonu do weryfikacji SMS.
Udział Shamira
Fragment klucza w formacie hex — jeden z N, bezużyteczny bez pozostałych. Stanowi główny element kryptograficzny karty.
Kod QR
Ten sam udział zakodowany jako QR — skanowanie eliminuje błędy ręcznego przepisywania długiego ciągu hex.
Adres systemu
URL własnej instancji Secret Key — prowadzi bezpośrednio do panelu logowania bez konieczności znajomości adresu na pamięć.

#Generator kart

Generator dostępny jest jako zakładka Generator kart w dashboard.html. Wymaga plików card-front.js i card-back.js w tym samym folderze.

OpcjaOpis
Spad 2mmDodaje 2mm spadu drukarskiego z każdej strony (standard drukarni)
Linie cięciaZnaczniki drukarskie do precyzyjnego cięcia
Pobierz PDF kartyEksport pojedynczej karty (przód + tył = 2 strony)
Pobierz PDF wszystkichEksport wszystkich kart naraz

#Specyfikacja techniczna

ParametrWartość
FormatISO ID-1, 85.6 × 54 mm
SVG viewBox0 0 325.03964 204.09428
Spad2mm z każdej strony (standard)
PDFKażda karta = osobna strona (przód + tył oddzielnie)
TechnologiaCzyste 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.
System

Architektura systemu

Wielowarstwowy przepływ weryfikacji — od wpisania loginu do odtworzenia hasła w przeglądarce.

#Przepływ autoryzacji

Użytkownik → wpisuje login + hasło │ System PHP → weryfikuje hasło (bcrypt), sprawdza rate limiting i CSRF │ SMS / 2FA → wysyła jednorazowy kod na przypisany numer telefonu │ Użytkownik → wpisuje kod SMS │ System PHP → weryfikuje kod, tworzy sesję, opcjonalnie zapamiętuje urządzenie │ Przeglądarka → przyjmuje udziały Shamira, odtwarza sekret lokalnie w JS

#Warstwy systemu

Warstwa autoryzacji (/app/)

PlikRola
login.phpFormularz logowania z ochroną CSRF
auth.phpGłówna logika: bcrypt, 2FA, brute-force, trusted devices
verify.phpWeryfikacja kodu SMS
resend.phpPonowna wysyłka SMS (cooldown 60s, max 3/sesję)
logout.phpWylogowanie i zniszczenie sesji

Warstwa dostępu (/app/decrypt/)

PlikRola
index.phpPanel odszyfrowania z rekonstrukcją Shamira w JavaScript
download.phpBramkowane pobieranie plików — wymaga sesji, biała lista budowana z $downloads, log po stronie serwera (patrz Konfiguracja treści i danych)
log.phpLogowanie zdarzeń (próby dostępu, kody SMS, błędy)
devtools-log.phpLogowanie prób otwarcia DevTools (z rate-limitingiem per IP)
timelock.phpBiblioteka logiki stanu blokady czasowej — patrz Ochrona przed zmową powierników
arm-timelock.phpUzbraja 48h blokadę pobierania po pierwszej udanej rekonstrukcji hasła + wysyła mail alarmowy z linkiem Panic Button
panic.phpObsługa jednorazowego linku blokującego (Panic Button) — bez wymogu logowania, autoryzacja przez 256-bitowy token
tl-status.phpOdpytywanie stanu blokady na żywo (co 10s) — panel wykrywa zmianę stanu bez odświeżania strony
card-secret-key.webpGrafika karty Secret Key wyświetlana w panelu
key.svgLogo systemu wyświetlane w panelu

Warstwa danych (/private/ — poza public_html)

PlikZawartość
secret-key.php$people, $downloads, $instructions, $email_notify — cały config danych i treści
lang.phpTeksty interfejsu — jeden język, patrz Teksty interfejsu (lang.php)
rate-limit.phpTrwały rate-limiting (liczniki prób logowania niezależne od sesji)
rate_limits.jsonLiczniki prób logowania per IP/konto (tworzy się automatycznie)
trusted_devices.jsonTokeny zaufanych urządzeń (SHA-256, TTL 7 dni) — tworzy się automatycznie
timelock.jsonStan blokady czasowej (48h) i Panic Button — tworzy się automatycznie po pierwszej udanej rekonstrukcji hasła, patrz Ochrona przed zmową powierników
secret-key.logLogi zdarzeń (logowania, błędy, 2FA)
moja-baza-hasel.kdbx (i inne pliki do pobrania)Pliki serwowane wyłącznie przez download.php — nigdy bezpośrednio przez HTTP, bo leżą poza public_html
⚠️
Wrażliwe pliki poza katalogiem publicznym
Wszystkie pliki konfiguracyjne przechowywane są poza public_html — błędna konfiguracja serwera WWW nie grozi ich ujawnieniem przez HTTP.
System

Bezpieczeństwo

Dziewięć niezależnych warstw ochrony — kompromitacja jednej nie daje dostępu do systemu.

#Warstwy ochrony

01Hasła (bcrypt)
cost=10, format $2y$, weryfikacja odporna na ataki czasowe (hash_equals())
02CSRF
Token 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ń)
03Brute-force
3 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
042FA SMS
Kod 6-cyfrowy z random_int(), ważny 10 min, cooldown 60s między wysyłkami
05Trusted devices
SHA-256, HttpOnly + Secure + SameSite=Strict, plik poza public_html, TTL 7 dni
06Sesja
Ciasteczko 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
07Ochrona interfejsu
Detekcja 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
08Bramkowane pobieranie
download.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)
09Blokada czasowa (Timelock)
48h blokada fizycznego pobierania plików po pierwszej udanej rekonstrukcji hasła w panelu (opcjonalnie zweryfikowanej po stronie serwera przez SECRET_HASH) + jednorazowy link „Panic Button" do natychmiastowego, trwałego zablokowania — patrz Ochrona przed zmową powierników

#Gwarancja kryptograficzna Shamira

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.

#Dobre praktyki wdrożeniowe

PraktykaSzczegóły
📁 Katalog /private/ poza public_htmlBezwzględnie wymagane — patrz sekcja instalacji
🔒 HTTPSSSL/TLS na całym serwerze — dane logowania i kody SMS w sieci
🔄 Aktualizacje PHPRegularnie aktualizuj do najnowszej PHP 8.x
🔑 Silne hasłaUnikalne, długie hasła dla każdego konta na kartach
🚫 secret-key.php nie publicznieNigdy nie udostępniaj pliku konfiguracyjnego
📄 Liczniki brute-force w pliku, nie w sesjiTrzymanie 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_htmlNie 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 stronyJeś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
System

Ochrona przed zmową powierników

48-godzinna blokada czasowa pobierania plików z alertem e-mail, przyciskiem awaryjnym („Panic Button") i opcjonalną weryfikacją hasła po stronie serwera, chroniąca przed zmową wyznaczonych powierników za życia właściciela.

#Dlaczego to potrzebne

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.

#Weryfikacja po stronie przeglądarki

Warunek sukcesu w przeglądarce 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. Dodane zostały dwie niezależne metody weryfikacji klienckiej:

MetodaJak działa
Spójność podzbiorówWł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 wynikuFallback 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.
⛔
To nie jest weryfikacja poprawności — tylko spójności
Obie metody wyżej sprawdzają, czy podane udziały są wewnętrznie spójne (pochodzą z tego samego podziału i razem dają jeden, konkretny wynik) — nie czy ten wynik jest prawdziwym hasłem. Zalogowany, złośliwy powiernik może samodzielnie wygenerować zupełnie nowy, spójny podział Shamira z dowolnym, wymyślonym „hasłem" (np. na zewnętrznej stronie), wkleić te fałszywe udziały do panelu i przejść oba testy wyżej — mimo że nie zna prawdziwego hasła. Do wersji v3.1.0 to wystarczało, żeby wymusić uzbrojenie blokady 48h i alert Panic Button. Patrz sekcja niżej.

#Weryfikacja po stronie serwera (SECRET_HASH)

Od v3.1.0 arm-timelock.php nie ufa już bezkrytycznie sygnałowi z przeglądarki. Jeśli w configu ustawiona jest opcjonalna stała SECRET_HASH, serwer weryfikuje złożony sekret przez password_verify() — dokładnie tym samym mechanizmem co hasła logowania — zanim uzbroi blokadę czasową. Od v3.1.1 panel dołącza złożony sekret do żądania tylko wtedy, gdy SECRET_HASH jest ustawiony i blokada nie jest jeszcze uzbrojona — w pozostałych przypadkach hasło nie opuszcza przeglądarki:

php
define('SECRET_HASH', '$2y$10$...'); // bcrypt hash prawdziwego hasła
⚠️
Bez weryfikacji — usuń linię, nie zostawiaj pustej wartości
Jeśli nie chcesz korzystać z weryfikacji, usuń całą linię define('SECRET_HASH', …) z configu. W v3.1.0 pusty hash (define('SECRET_HASH', '');) powodował, że panel uznawał weryfikację za włączoną, a serwer odrzucał każde hasło („Kody złożyły się poprawnie, ale to nie jest prawidłowe hasło”) nawet przy poprawnych udziałach. Od v3.1.1 pusta wartość jest traktowana jak brak stałej, ale dla czytelności configu i tak lepiej usunąć linię. Generator konfiguracji w dashboardzie sam pomija ją, gdy pole „Prawdziwe hasło główne” jest puste.
💡
Nie trzeba nic haszować ręcznie
W dashboardzie, zakładka Konfiguracja → sekcja „Weryfikacja odzyskiwania hasła" → pole „Prawdziwe hasło główne" — wpisujesz to samo hasło co w zakładce Szyfrowanie, a dashboard sam liczy jego bcrypt hash (ten sam mechanizm co dla haseł logowania). Samo hasło nigdzie nie jest zapisywane — do wygenerowanego pliku trafia wyłącznie hasz.

Rate-limiting niezależny od logowania

5 nieudanych prób weryfikacji / 30 minut, licznik per-konto (nie per-IP) — żeby jedna zablokowana osoba nie blokowała innej, logującej się z tej samej sieci w trakcie kryzysu. Blokuje wyłącznie kolejne próby weryfikacji sekretu — logowanie, przeglądanie panelu i edycja pola z kodami działają bez ograniczeń przez cały czas. Po przekroczeniu limitu panel prosi o odczekanie; te same (poprawne) udziały przejdą normalnie po zresetowaniu licznika.

LogKiedy
TIMELOCK ARM REJECTEDZłożony sekret nie zgadza się z SECRET_HASH — z IP, loginem i numerem próby (X/5), bez logowania samej wartości
TIMELOCK ARM RATE LIMITEDPrzekroczono 5 prób w 30 minut

Panel odzwierciedla to na żywo nowym, pośrednim stanem: hasło złożone lokalnie w przeglądarce nie pojawia się już natychmiast w zielonym polu — najpierw pokazuje się neutralny, niebieski komunikat „Weryfikuję hasło…", a zielone pole z hasłem pojawia się dopiero po potwierdzeniu przez serwer. Błędny sekret nigdy nie zdąży się pokazać jako poprawny — czerwony błąd zastępuje pośredni stan bezpośrednio.

⚠️
Opcjonalne, ale mocno zalecane
Bez SECRET_HASH panel działa dokładnie tak jak przed v3.1.0 (wsteczna kompatybilność) — łącznie z luką opisaną wyżej. To pole domyka realną furtkę bezpieczeństwa, nie tylko usprawnienie. Od v3.1.1, bez SECRET_HASH panel w ogóle nie wysyła hasła na serwer.

#Cztery stany panelu pobierania

StanKiedyZachowanie panelu
APrzed odszyfrowaniemPrzyciski pobierania wyszarzone, bez href (żeby przeglądarka nie pokazywała podglądu URL), z dymkiem po najechaniu wyjaśniającym dlaczego są nieaktywne
BTrwa odliczanie 48hCał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
CZablokowane przez Panic ButtonTa sama rozmyta karta, zamiast licznika czerwony komunikat o trwałej blokadzie
DMinęło 48hNormalne, 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 (i, jeśli SECRET_HASH jest ustawiony, pozytywna weryfikacja serwerowa — patrz wyżej) 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.
System

Struktura plików

Kompletne drzewo plików projektu — zarówno pliki serwera jak i lokalnych narzędzi offline.

#Pliki na serwerze

app/ # Publiczny — system logowania ├── decrypt/ # Chroniony — panel użytkownika │ ├── .htaccess │ ├── arm-timelock.php ← Uzbraja blokadę 48h + mail alarmowy │ ├── card-secret-key.webp │ ├── devtools-log.php ← Logowanie prób otwarcia DevTools │ ├── download.php ← Bramkowane pobieranie plików │ ├── favicon.ico │ ├── index.php ← Panel odszyfrowania + Shamir JS │ ├── key.svg │ ├── log.php ← Logowanie zdarzeń │ ├── panic.php ← Obsługa linku Panic Button │ ├── timelock.php ← Biblioteka logiki stanu blokady │ └── tl-status.php ← Odpytywanie stanu na żywo (co 10s) ├── .htaccess ├── auth.php ← Autoryzacja + t(), md_lite(), empty_state_box() ├── favicon.ico ├── key.svg ├── login.php ← Formularz logowania ├── logout.php ← Wylogowanie ├── resend.php ← Ponowna wysyłka SMS └── verify.php ← Weryfikacja kodu SMS

#Pliki poza public_html

/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.

#Pliki lokalne (narzędzia offline)

# 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="...">.
Instalacja

Wymagania

Co potrzebujesz, żeby uruchomić Secret Key na własnym serwerze.

#Serwer

WymaganieMinimalna wersjaUwagi
PHP8.0+Wymagane funkcje: password_hash(), random_bytes(), random_int()
Serwer WWWApache lub NginxMusi wspierać .htaccess lub odpowiednik dla ochrony /private/
HTTPSTLS 1.2+Obowiązkowe — dane logowania i kody SMS
Katalog poza public_html—Niezbędny dla pliku secret-key.php

#Konto SMSPlanet

System wysyła kody 2FA przez SMSPlanet API. Wymagane:

ElementOpis
Konto SMSPlanetAktywne konto z saldem na wysyłkę SMS
Token APIStała SMSPLANET_TOKEN w secret-key.php — patrz Konfiguracja treści i danych
Nazwa nadawcyStała SMS_SENDER (domyślnie „Secret Key”) — nazwa wyświetlana jako nadawca SMS-a
Numer telefonuJeden 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.php
define('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.

#Narzędzia lokalne (offline)

WymaganieOpis
Nowoczesna przeglądarkaChrome / Firefox / Edge — do otwierania dashboard.html przez file://
Brak połączenia z internetemWszystkie narzędzia działają offline (poza fontami Google przy pierwszym załadowaniu)
💡
Chrome zalecany do generowania PDF
Generator kart używa window.open() + window.print() — jedynej metody działającej przez file:// bez ograniczeń CORS. Chrome obsługuje to najlepiej.
Instalacja

Instalacja krok po kroku

Kompletny przewodnik od pobrania plików do działającego systemu na własnym serwerze.

#1. Konfiguracja offline

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.

#2. Wgranie plików na serwer

bash
# 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 Path contains /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.

#3. Konfiguracja ścieżki

W pliku auth.php (folder app/) ścieżka do folderu /private/ występuje w 7 miejscach. Trzeba poprawić wszystkie, nie tylko pierwszy require_once:

php
// app/auth.php
require_once __DIR__ . '/../../../private/secret-key.php';
require_once __DIR__ . '/../../../private/rate-limit.php';
require_once __DIR__ . '/../../../private/lang.php';

define('LOG_FILE',    __DIR__ . '/../../../private/secret-key.log');
define('PRIVATE_DIR', __DIR__ . '/../../../private');

// ×2 (rememberDevice() i sprawdzanie zaufanego urządzenia)
$file = __DIR__ . '/../../../private/trusted_devices.json';

Domyślnie w każdym z tych miejsc stoi fragment /../../../private. Ścieżka jest liczona od folderu, w którym leży auth.php, więc dopasuj ją do swojej struktury katalogów. Przykład: auth.php leży w /public_html/twoja-domena/, a /private/ obok /public_html/, więc zmieniasz /../../../private na /../../private we wszystkich siedmiu miejscach (w edytorze użyj „Zamień wszystko”).

#3a. Tryb testowy SMS

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.

#4. Generowanie i dystrybucja kart

1
Otwórz Generator kart

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.
Instalacja

Konfiguracja treści i danych

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.

#1. Osoby ($people)

$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...
];
PoleOpis
loginLogin do panelu — unikalny, wpisywany przez osobę na karcie
passwordHash bcrypt hasła. Dashboard hashuje w przeglądarce — nigdy nie wpisujesz tu hasła jawnego
first_name / last_nameWyświetlane w powitaniu panelu i na liście posiadaczy
phone_ccKierunkowy 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
phoneSam 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_panelfalse = 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.

#2. Pliki do pobrania ($downloads)

$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ą.

php
$downloads = [
  ['key' => 'baza-hasel', 'label' => 'Pobierz bazę haseł',     'filename' => 'moja-baza-hasel.kdbx'],
  ['key' => 'aegis',      'label' => 'Pobierz bazę kodów 2FA (Aegis)', 'filename' => 'moja-baza-aegis.json'],
  ['key' => 'program',    'label' => 'Pobierz KeePassXC',      'filename' => 'KeePassXC-2.7.9-Win64.msi', 'name' => 'KeePassXC'],
];
PoleOpis
keyIdentyfikator w adresie pobierania (download.php?file=klucz). Musi być unikalny
labelTekst na przycisku w panelu
filenameRzeczywista nazwa pliku leżącego w /private/
nameOpcjonalne. 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ą.

#3. Instrukcja ($instructions)

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...
];
FormatowanieEfekt
**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.

#4. Powiadomienie email ($email_notify)

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.

php
$email_notify = [
  'enabled'    => true,
  'to'         => 'jan@moja-domena.pl',
  'from_email' => 'no-reply@moja-domena.pl',
  'from_name'  => 'Secret Key',
  'panel_url'  => 'https://moja-domena.pl',
];
PoleOpis
enabledfalse wyłącza wysyłkę całkowicie — bez ruszania kodu
toAdres, na który przychodzi powiadomienie o każdym logowaniu
from_emailAdres nadawcy. Domena stąd trafia też automatycznie do Message-ID
from_nameNazwa nadawcy widoczna w kliencie pocztowym
panel_urlLink 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.

#5. SMS: token, nadawca i autouzupełnianie

Połączenie z SMSPlanet opisują dwie stałe na początku secret-key.php. Używa ich auth.php przy każdej wysyłce kodu 2FA:

php
define('SMSPLANET_TOKEN', 'twój-token-api');
define('SMS_SENDER',      'Secret Key');
StałaOpis
SMSPLANET_TOKENToken API z konta SMSPlanet. Bez niego kody 2FA nie zostaną wysłane. Trzymaj go wyłącznie w secret-key.php poza public_html — nigdy w kodzie ani w repozytorium
SMS_SENDERNazwa nadawcy widoczna na telefonie odbiorcy (pole „od”). Domyślnie „Secret Key”. Zmień ją, jeśli chcesz inną nazwę — pamiętaj, że sieć i SMSPlanet mogą ograniczać dozwolone nazwy nadawcy, więc po zmianie wyślij testowy SMS

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).

#6. Wymagany próg odzyskiwania (SHARES_REQUIRED)

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 „Weryfikacja odzyskiwania hasła") 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.

#7. Weryfikacja hasła po stronie serwera (SECRET_HASH)

Opcjonalna, ale mocno zalecana stała — bcrypt hash prawdziwego hasła głównego, używany przez arm-timelock.php do weryfikacji złożonego sekretu przez password_verify(), zanim uzbroi blokadę czasową (patrz Ochrona przed zmową powierników → Weryfikacja na serwerze):

php
define('SECRET_HASH', '$2y$10$...'); // bcrypt hash prawdziwego hasła
⚠️
Bez weryfikacji — usuń linię, nie zostawiaj pustej wartości
Jeśli nie chcesz korzystać z weryfikacji, usuń całą linię define('SECRET_HASH', …) z configu. W v3.1.0 pusty hash (define('SECRET_HASH', '');) powodował, że panel uznawał weryfikację za włączoną, a serwer odrzucał każde hasło („Kody złożyły się poprawnie, ale to nie jest prawidłowe hasło”) nawet przy poprawnych udziałach. Od v3.1.1 pusta wartość jest traktowana jak brak stałej, ale dla czytelności configu i tak lepiej usunąć linię. Generator konfiguracji w dashboardzie sam pomija ją, gdy pole „Prawdziwe hasło główne” jest puste.
💡
Dashboard sam liczy hasz — nie trzeba nic haszować ręcznie
Zakładka Konfiguracja → sekcja „Weryfikacja odzyskiwania hasła" → pole „Prawdziwe hasło główne". Wpisujesz to samo hasło co w zakładce Szyfrowanie, a dashboard sam liczy bcrypt hash — dokładnie tym samym mechanizmem co dla haseł logowania. Samo hasło nigdzie nie jest zapisywane, do pliku trafia wyłącznie hasz.
⚠️
Brak tej stałej = brak weryfikacji serwerowej
Bez SECRET_HASH panel odzyskiwania ufa bezkrytycznie przeglądarce, że złożone udziały to prawdziwe hasło — zalogowany powiernik mógłby wymusić uzbrojenie blokady i alert Panic Button bez znajomości prawdziwego hasła. Fallback (brak stałej) istnieje dla wstecznej kompatybilności, ale zdecydowanie warto to pole ustawić.

#8. Limity i czasy (auth.php)

Wartości limitów prób, czasu ważności kodów i sesji nie są częścią secret-key.php — to stałe na początku pliku app/auth.php. Domyślne ustawienia są rozsądne dla typowego wdrożenia, ale możesz je zmienić, edytując plik ręcznie:

StałaDomyślnieCo kontroluje
LOGIN_MAX_ATTEMPTS
LOGIN_WINDOW
3 / 900 s (15 min)Błędne próby hasła — osobno na IP i na konto
TWO_FA_TTL600 s (10 min)Ważność kodu SMS
RESEND_COOLDOWN60 sOdstęp między kolejnymi wysyłkami SMS-a (dodatkowo maksymalnie 3 wysyłki na sesję)
TWO_FA_MAX_ATTEMPTS3Błędne kody SMS w jednej sesji logowania
TWO_FA_IP_MAX
TWO_FA_IP_WINDOW
3 / 3600 s (1 godz.)Błędne kody SMS z jednego adresu IP
DEVICE_TOKEN_TTL7 dniJak długo urządzenie jest zapamiętane jako zaufane
SESSION_TIMEOUT1800 s (30 min)Czas nieaktywności do automatycznego wylogowania; panel używa tej samej wartości do odliczania w przeglądarce
DEVTOOLS_LOG_MAX_ATTEMPTS
DEVTOOLS_LOG_WINDOW
30 / 300 s (5 min)Limit zapisów zdarzeń DevTools na IP (ochrona logu przed zaśmieceniem)
⚠️
Zmiany w auth.php znikną przy aktualizacji
Jeśli nadpiszesz auth.php nową wersją z repozytorium, wprowadzone ręcznie wartości wrócą do domyślnych. Po każdej aktualizacji tego pliku sprawdź je jeszcze raz. Luzowanie limitów osłabia ochronę przed zgadywaniem haseł i kodów — zmieniaj je tylko z wyraźnego powodu.
ℹ️
Limit weryfikacji hasła głównego jest osobny
Limit 5 prób na 30 minut dla weryfikacji złożonego sekretu (SECRET_VERIFY_MAX_ATTEMPTS i SECRET_VERIFY_WINDOW) jest zdefiniowany w arm-timelock.php, nie w auth.php — patrz Ochrona przed zmową powierników.
Instalacja

Teksty interfejsu (lang.php)

Cały widoczny tekst interfejsu — przyciski, etykiety, komunikaty błędów — jest wydzielony z kodu do jednego pliku, private/lang.php.

#Zasada działania

⚠️
To nie jest system wielojęzyczny z przełącznikiem
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.

#Klucze i funkcja t()

lang.php definiuje tablicę $lang — klucz w stylu snake_case na wartość tekstową. Kod odczytuje ją przez funkcję t() zdefiniowaną w auth.php:

php
// private/lang.php
$lang = [
  'login_submit_btn'   => 'Zaloguj się',
  'login_empty_fields' => 'Wpisz login i hasło.',
  // ...
];
php
// auth.php
function t(string $key, ...$args): string {
    global $lang;
    $text = $lang[$key] ?? $key;
    return $args ? vsprintf($text, $args) : $text;
}
✅
Brakujący klucz nie wywala strony
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:

php
// w lang.php
'verify_attempts_left_1'    => 'Pozostało %d próba.',
'verify_attempts_left_few'  => 'Pozostało %d próby.',
'verify_attempts_left_many' => 'Pozostało %d prób.',

// wywołanie
t('verify_attempts_left_few', $remaining);
⚠️
Kilka kluczy renderuje się bez htmlspecialchars()
Większość tekstów jest escapowana przy renderowaniu (bezpiecznie, jak każdy inny input). Kilka kluczy — te zawierające celowe znaczniki <strong> czy encje &nbsp; (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 &amp;, nie jako spacja czy pogrubienie.

#Zmiana języka systemu

Żeby uruchomić system w innym języku:

1
Przetłumacz wszystkie wartości w lang.php

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).

Inne

FAQ

Najczęściej zadawane pytania dotyczące bezpieczeństwa, działania i wdrożenia systemu.

#Co jeśli zgubię kartę Secret Key?

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ć.

#Czy posiadając kartę mogę samodzielnie odczytać hasło?

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?

Rekonstrukcja odbywa się w przeglądarce (JavaScript). Jeśli w configu ustawiłeś SECRET_HASH (zalecane), po jej zakończeniu hasło jest przesyłane przez HTTPS do Twojego serwera tylko po to, by password_verify() mogło je porównać z SECRET_HASH — serwer go nie zapisuje ani nie loguje. Bez SECRET_HASH hasło w ogóle nie opuszcza przeglądarki.

#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.

#Jak wybrać próg — ile udziałów jest wymaganych?

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.

#Jak długo ważne są dane logowania z karty?

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.

#Czy Secret Key zastępuje testament lub postępowanie spadkowe?

Nie. Secret Key nie jest narzędziem prawnym. Pieniądze z konta bankowego rodzina odzyska formalną drogą spadkową, a Secret Key uzupełnia ją tam, gdzie prawo nie pomoże: dostęp do poczty, chmury ze zdjęciami, kodów 2FA czy kryptowalut trzymanych na własnym kluczu. To nie jest porada prawna, a regulaminy niektórych usług mogą ograniczać udostępnianie haseł.

#Czy rodzina zobaczy całą moją prywatną korespondencję?

Secret Key niczego nie odszyfrowuje i nie ma dostępu do Twojej poczty ani komunikatorów. Dzieli tylko hasło główne do Twojej bazy haseł (np. KeePass, Bitwarden), a plik bazy leży na Twoim serwerze. Bliscy po zebraniu kworum, potwierdzeniu SMS i 48-godzinnej blokadzie pobierają ten plik, a to, co w nim jest, wybierasz Ty. Możesz przygotować osobną bazę awaryjną tylko z kontami, które mają trafić do rodziny.

#Czy to nie jest za skomplikowane? Czy nie wystarczy zaszyfrowany pendrive i kartka z hasłem?

Dla wielu osób pendrive z kartką wystarczy i jest prostszy. Secret Key jest dla tych, którzy nie chcą trzymać wszystkiego w jednym miejscu u jednej osoby: hasło jest dzielone między kilku zaufanych, a dostęp chroni SMS i 48-godzinna blokada, którą możesz anulować. Konfiguracja jest techniczna (własny serwer; dashboard offline i dokumentacja ją upraszczają), a Twoi bliscy dostają tylko karty i instrukcję.

Inne

Polityka bezpieczeństwa

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.

#Jak zgłosić lukę

Wyślij szczegółowy opis na: dev@secretkey.website

Element zgłoszeniaOpis
📝 Opis lukiOpis i potencjalny wpływ na bezpieczeństwo systemu
🔁 Kroki do odtworzeniaProof of concept — minimalne kroki umożliwiające reprodukcję
🏷️ Wersja systemuKtórej wersji dotyczy luka
💡 Propozycja naprawyOpcjonalnie — sugestia rozwiązania

#Czas odpowiedzi

CzasOdpowiedź
48hPotwierdzenie otrzymania zgłoszenia
7 dniInformacja o postępach i planowanym terminie naprawy
Po naprawiePubliczne podziękowanie (jeśli sobie życzysz)

#Zakres zgłoszeń

W zakresie
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
Narzędzia offline

Przegląd dashboard

Dashboard to lokalny panel konfiguracyjny Secret Key — plik HTML otwierany bezpośrednio w przeglądarce bez serwera. Zawiera stronę główną (Home) i trzy zakładki narzędziowe: Szyfrowanie, Konfiguracja i Generator kart, a także przełącznik języka PL/EN.

ℹ️
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.

#Interfejs główny

Dane w animacji są przykładowe.

#Elementy interfejsu

1
Pasek zakładek
Cztery przyciski nawigacyjne u góry ekranu: Home, Szyfrowanie, Konfiguracja, 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.

#Wymagane pliki

Wszystkie pliki muszą znajdować się w tym samym folderze:

dashboard/ ├── dashboard.html ← otwórz ten plik w przeglądarce ├── generate-hash.html ← zakładka Konfiguracja ├── generate-shamir.html ← zakładka Szyfrowanie ├── generate-card.html ← zakładka Generator kart ├── card-front.js ← szablon SVG przodu karty └── card-back.js ← szablon SVG tyłu karty
⚠️
Nie przenoś plików osobno
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.

#Zalecana przeglądarka

PrzeglądarkaObsługaUwagi
Chrome / ChromiumZalecanaNajlepsza obsługa PDF przez window.print(), pełna obsługa file://
EdgeDziałaBazuje na Chromium — identyczna obsługa
FirefoxCzęściowaMoże wymagać zezwolenia na ładowanie lokalnych iframe
SafariNiezalecanaOgraniczona obsługa file:// i lokalnych iframe
Narzędzia offline

Zakładka: Konfiguracja

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.

#Widok zakładki

Dane w animacji są przykładowe.

#Elementy interfejsu

1
Konfiguracja SMS
Nazwa nadawcy SMS, token API SMSPlanet i domena do autouzupełniania kodu na Android/iOS (musi zaczynać się od @ i być Twoją rzeczywistą domeną — patrz Konfiguracja treści i danych).
2
Weryfikacja odzyskiwania hasła
Dwa pola: Wymagane kody — próg (K) udziałów Shamira wymaganych w panelu odzyskiwania, ze stepperem +/− (liczba całkowita od 2 do 20 — tyle co maksymalnie udziałów w zakładce Szyfrowanie) i podpowiedzią przypominającą o zgodności z zakładką Szyfrowanie — oraz Prawdziwe hasło główne, do którego wystarczy wpisać to samo hasło co w zakładce Szyfrowanie: dashboard sam liczy jego bcrypt hash (ten sam mechanizm co dla haseł logowania niżej) i zapisuje w configu wyłącznie hasz, nigdy samo hasło. Patrz Ochrona przed zmową powierników.
3
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; login — jako imię-nazwisko-trzy cyfry albo, gdy imienia i nazwiska brak, trzy losowe słowa), Imię, Nazwisko, kierunkowy kraju (przeszukiwalna lista 186 krajów z flagami) i Telefon (sam numer lokalny, 4–15 cyfr, 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.
4
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.
5
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.
6
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.
7
Przycisk „Generuj konfigurację"
Hashuje wszystkie hasła (użytkowników i, jeśli podane, hasło główne z pola „Prawdziwe hasło główne") 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. Pilnuje też unikalności kluczy plików do pobrania, poprawności numerów telefonu (kierunkowy i 4–15 cyfr) oraz sparowania znaczników ** i * w instrukcji. Operacja może potrwać kilka sekund przy wielu osobach.
8
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.

#Krok po kroku

1
Wpisz token SMSPlanet i domenę autouzupełniania

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
Ustaw próg i weryfikację odzyskiwania hasła

W sekcji „Weryfikacja odzyskiwania hasła" wpisz próg (K) zgodny z zakładką Szyfrowanie oraz — opcjonalnie, ale zalecane — to samo hasło główne w polu „Prawdziwe hasło główne". Dashboard sam policzy jego hasz.

3
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.

4
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.

5
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.

6
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.
Narzędzia offline

Zakładka: Szyfrowanie

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.

#Widok zakładki

Dane w animacji są przykładowe.

#Elementy interfejsu

1
Pole hasła głównego
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 udziałów zostanie wygenerowanych — zwykle tyle, ile osób dostanie kartę (konta bez fizycznej karty, np. administracyjne, nie liczysz). Przyciski +/−, zakres 2–20, domyślnie 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. Kto dostanie który udział, ustalasz sam — zachowaj tę kolejność w Generatorze kart.
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. Przy każdym udziale jest też „Kopiuj” oraz „↓ .txt” (osobny plik „Udział i z n” dla jednej osoby); kliknięcie udziału kopiuje go do schowka.

#Krok po kroku

1
Wpisz hasło główne

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 sprawdź, czy udziały dają z powrotem hasło. Ta zakładka tylko dzieli sekret (nie ma w niej trybu odtwarzania), więc wklej wymagane minimum udziałów na iancoleman.io/shamir albo przejdź cały proces według strony Test przed rozdaniem kart.

💡
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.
Narzędzia offline

Zakładka: Generator kart

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.

#Widok zakładki

Dane w animacji są przykładowe.

#Elementy interfejsu

1
Ustawienia globalne
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.

#Krok po kroku

1
Wpisz adres instancji i dane właściciela

Podaj adres URL swojego serwera Secret Key — bez https://, np. 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.
Pomoc

Test przed rozdaniem kart

Lista kontrolna od początku do końca: tryb testowy, logowanie, rekonstrukcja, blokada i sprzątanie.

Zanim rozdasz karty, przejdź cały proces od początku do końca na konfiguracji testowej. Poniższa lista zbiera w jednym miejscu kroki, które są opisane osobno na stronach Instalacja krok po kroku, Ochrona przed zmową powierników i Zakładka: Szyfrowanie.

⛔
Nie testuj na prawdziwych kartach i prawdziwym haśle
Pierwsza udana rekonstrukcja hasła w panelu naprawdę uzbraja blokadę 48 godzin i wysyła mail z linkiem Panic Button. Do testu użyj osobnego, testowego hasła, udziałów i configu. Jeśli mimo to testujesz na produkcyjnym configu, po teście wykonaj sekcję „Sprzątanie” poniżej.

#Przygotowanie

01
Wygeneruj testowy config i udziały

W dashboardzie (Przegląd dashboard) wpisz testowe hasło główne, podziel je na udziały i wygeneruj secret-key.php z kontami testowymi. Ustaw SHARES_REQUIRED zgodnie z progiem z zakładki Szyfrowanie oraz SECRET_HASH dla testowego hasła.

02
Wgraj pliki na serwer

Wgraj app/ do public_html oraz private/ poza nim, tak jak w instalacji. Wgraj też przynajmniej jeden testowy plik do pobrania pod nazwą z pola filename.

03
Włącz tryb testowy SMS

W auth.php odkomentuj linię $smsResult = true; $code = '123456'; i zakomentuj wywołanie sendSmsCode(). Kod weryfikacyjny wynosi wtedy zawsze 123456, a SMS nie jest wysyłany (szczegóły: Instalacja krok po kroku).

#Logowanie i panel

01
Zaloguj się każdym kontem

Dla każdej osoby z $people wpisz login i hasło z karty, a potem kod 123456. Sprawdź, czy lista posiadaczy, instrukcja i przyciski pobierania wyglądają tak, jak zaplanowałeś (konta z show_in_panel => false nie powinny być na liście).

02
Sprawdź limity

Wpisz celowo błędne hasło i błędny kod. Powinieneś zobaczyć komunikat o liczbie pozostałych prób, a po przekroczeniu limitu (3 próby) blokadę, a nie błąd PHP.

03
Sprawdź mail o logowaniu

Jeśli $email_notify jest włączone, po poprawnym logowaniu powinien przyjść mail. Sprawdź, czy nie trafił do spamu.

#Rekonstrukcja i blokada

01
Podaj fałszywe udziały

Najpierw wpisz udziały z innego, spójnego podziału (np. wygenerowane osobno). Z ustawionym SECRET_HASH panel powinien odrzucić hasło, a w logu pojawi się TIMELOCK ARM REJECTED. Blokada nie może się uzbroić.

02
Podaj prawdziwe udziały testowe

Wpisz progową liczbę udziałów z kart testowych. Hasło powinno się pokazać dopiero po komunikacie „Weryfikuję hasło…”, a licznik 48 godzin ruszyć od razu. Przyciski pobierania są wtedy rozmyte.

03
Sprawdź mail z Panic Buttonem

Mail z linkiem panic.php?token=… powinien dotrzeć niemal natychmiast. Jeśli go nie ma, zajrzyj do Rozwiązywania problemów.

04
Kliknij link Panic Button

Panel powinien w ciągu około 10 sekund przejść do stanu „zablokowane”, a bezpośrednie wejście na download.php zwracać kod 403. Pamiętaj, że to blokada trwała do czasu wygenerowania nowego configu lub usunięcia timelock.json.

#Prawdziwy SMS i autouzupełnianie

Autouzupełnianie kodu (WebOTP) działa tylko z prawdziwym SMS-em, więc sprawdź je dopiero po wyłączeniu trybu testowego: zaloguj się z telefonu i zobacz, czy przeglądarka sama wypełnia pole kodu. Jeśli nie, najczęściej SMS_AUTOFILL_DOMAIN nie zgadza się z domeną panelu.

#Sprzątanie przed rozdaniem kart

01
Wyłącz tryb testowy

Przywróć wywołanie sendSmsCode() i ponownie zakomentuj linię z kodem 123456.

02
Zresetuj blokadę

Usuń private/timelock.json albo wgraj finalny secret-key.php. Stary stan blokady wygasa automatycznie, bo jest powiązany z odciskiem listy $people.

03
Wgraj finalny config

Wgraj prawdziwy secret-key.php z prawdziwym SECRET_HASH i SHARES_REQUIRED, a także prawdziwe pliki do pobrania. Usuń pliki testowe.

04
Zrób jedno prawdziwe logowanie

Zaloguj się własnym kontem z prawdziwym SMS-em. Nie wpisuj prawdziwych udziałów w panelu — to uzbroiłoby blokadę.

#Gotowe do rozdania kart, gdy…

SprawdzenieOczekiwany wynik
Logowanie każdym kontemDziała z prawdziwym SMS-em, lista posiadaczy jest poprawna
Fałszywe udziałyOdrzucone, w logu TIMELOCK ARM REJECTED
Prawdziwe udziały testoweHasło pojawia się po weryfikacji, licznik 48 h rusza
Mail z Panic ButtonemDotarł do skrzynki głównej, link blokuje pobieranie
Tryb testowy i timelock.jsonWyłączony i usunięty, config produkcyjny wgrany
Pomoc

Aktualizacja do nowej wersji

Co wgrać po wydaniu nowej wersji i czego nie nadpisywać.

Aktualizacja polega zwykle na podmianie kilku plików PHP. Poniżej ogólne zasady oraz tabela, które pliki zmieniały się w kolejnych wydaniach. Pełna lista zmian jest w historii wersji.

#Zasady ogólne

01
Zrób kopię

Skopiuj folder aplikacji i cały katalog private/ (config, logi, timelock.json). Kopia pozwala wrócić do poprzedniej wersji, gdyby coś poszło nie tak.

02
Sprawdź, co się zmieniło

W historii wersji otwórz wydanie i przeczytaj tabelę „Zmienione pliki”. Wgrywasz tylko te pliki, nie całą paczkę.

03
Wgraj pliki razem, gdy tak jest napisane

Niektóre wydania wymagają kompletu, np. index.php razem z lang.php. Wgranie tylko jednego z nich skutkuje nazwami kluczy w interfejsie.

04
Sprawdź wersję w stopce

Zaloguj się do panelu. W stopce jest numer wersji systemu — powinien być zgodny z wydaniem.

#Które pliki w której wersji

AktualizacjaPliki do podmianyUwagi
v3.1.0 → v3.1.1app/decrypt/index.php, app/decrypt/arm-timelock.phpsecret-key.php bez zmian. Pusty define('SECRET_HASH', '') możesz usunąć.
v3.1.1 → v3.1.2app/decrypt/index.php, private/lang.phpWgraj oba razem. Dwa nowe klucze: person_locked i footer_system_version.
v3.1.2 → v3.1.3app/decrypt/index.php, dashboard/generate-hash.htmlPlik z dashboardu podmieniasz lokalnie, na serwer trafia tylko index.php. Config bez zmian.
Starsze wersje—Zajrzyj do historii wersji i przejdź kolejne wydania po kolei.

#Masz własne tłumaczenie lang.php?

Nie nadpisuj go. Dopisz brakujące klucze do swojego pliku, w swoim języku. Przykład dla v3.1.2:

php
'person_locked'         => 'zaszyfrowane',
'footer_system_version' => 'WERSJA SYSTEMU:',

#Czego nie nadpisywać

PlikDlaczego
private/secret-key.phpTwój config: osoby, pliki do pobrania, instrukcja, SECRET_HASH, SHARES_REQUIRED
private/lang.phpJeśli tłumaczysz interfejs, nadpisanie cofnie tłumaczenie
app/auth.phpJeśli zmieniałeś limity lub tryb testowy, wartości wrócą do domyślnych — patrz Konfiguracja treści i danych
private/timelock.json, rate_limits.json, trusted_devices.jsonStan systemu. Usunięcie timelock.json w trakcie blokady zdejmuje blokadę
💡
Po aktualizacji sprawdź
Zaloguj się, przejrzyj listę posiadaczy i stopkę, a przy większych zmianach przejdź skróconą wersję testu przed rozdaniem kart na konfiguracji testowej.
Pomoc

Rozwiązywanie problemów

Objaw, przyczyna i co sprawdzić — SMS, tłumaczenia, weryfikacja hasła, limity, maile i pobieranie.

Najczęstsze objawy z przyczynami i sposobem sprawdzenia. Zdarzenia z serwera zapisują się w pliku private/secret-key.log — to pierwsze miejsce, w które warto zajrzeć.

#SMS z kodem nie dochodzi

Możliwa przyczynaCo sprawdzić
Pusty lub błędny SMSPLANET_TOKENToken w secret-key.php. W logu pojawi się wpis 2FA SMS ERROR z komunikatem od SMSPlanet.
Brak środków na koncie SMSPlanetDoładuj saldo — bez niego SMS nie zostanie wysłany.
Numer w złym formacieKierunkowy w phone_cc (np. +48), sam numer w phone. Stary config z jednym polem wymaga migracji.
Odstęp 60 s lub limit 3 wysyłekPrzycisk ponownej wysyłki działa po odliczeniu, a po 3 wysyłkach w jednej sesji zaloguj się od nowa.
Kod wygasłKod jest ważny 10 minut. Poproś o nowy.
Włączony tryb testowyW trybie testowym SMS nie jest wysyłany, a kodem jest 123456. Patrz Test przed rozdaniem kart.

#Panel pokazuje nazwy kluczy zamiast tekstu

Zamiast „zaszyfrowane” widzisz np. person_locked. Funkcja t() nie znalazła klucza w lang.php i pokazuje sam klucz. Najczęściej to efekt wgrania nowego index.php bez nowego lang.php (albo z własnym tłumaczeniem bez nowych kluczy). Wgraj lang.php razem z index.php lub dopisz brakujące klucze — patrz Aktualizacja do nowej wersji i Teksty interfejsu (lang.php).

#„Kody złożyły się poprawnie, ale to nie jest prawidłowe hasło”

Możliwa przyczynaCo zrobić
SECRET_HASH policzony dla innego hasłaOtwórz dashboard → zakładka Konfiguracja → sekcja „Weryfikacja odzyskiwania hasła” → pole „Prawdziwe hasło główne”. Wpisz tam dokładnie to samo hasło, które wpisałeś w zakładce Szyfrowanie (pole „Sekret”), kliknij „Generuj konfigurację” i wgraj nowy secret-key.php do /private/.
Pusty define('SECRET_HASH', '') w wersji v3.1.0Usuń linię albo zaktualizuj do v3.1.1 — patrz Ochrona przed zmową powierników.
Udziały z różnych podziałówPo wygenerowaniu nowych udziałów stare karty przestają pasować. Używaj kompletu z jednego podziału.
Literówka w udzialeSkanuj kod QR z karty zamiast przepisywać udział ręcznie.
Wyczerpany limit 5 prób na 30 minutOdczekaj, aż okno minie. W logu: TIMELOCK ARM RATE LIMITED.

#Panel żąda innej liczby kodów, niż potrzeba

Wartość SHARES_REQUIRED w configu musi być równa progowi K z zakładki Szyfrowanie. Dashboard nie sprawdza tego sam. Bez tej stałej panel przyjmuje 3, a najniższa dozwolona wartość to 2. Popraw wartość i wgraj config ponownie (patrz Konfiguracja treści i danych).

#Logowanie albo weryfikacja jest zablokowana

LimitOknoGdzie zmienić
Błędne hasło: 3 próby, osobno na IP i na konto15 minutauth.php
Błędny kod SMS: 3 na sesję i 3 na IPsesja / 1 godzinaauth.php
Weryfikacja hasła głównego: 5 prób na konto30 minutarm-timelock.php

Wystarczy odczekać do końca okna. Liczniki są trwałe i leżą w private/rate_limits.json, więc odświeżenie strony ani usunięcie ciasteczek ich nie zeruje. Katalog private/ musi mieć prawo zapisu.

#Nie dostaję maila z powiadomieniem lub alertem Panic Button

Wpis w logu lub objawPrzyczyna
Brak wpisu, mail nie przychodzi$email_notify ma 'enabled' => false. Wyłącza to też alert Panic Button.
MAIL SKIPPED / TIMELOCK MAIL SKIPPEDBrakuje to lub from_email w $email_notify.
TIMELOCK MAIL FAILEDFunkcja mail() nie wysłała wiadomości — sprawdź konfigurację poczty serwera.
Mail jest w spamieDodaj nadawcę do białej listy, ustaw SPF i DKIM. Patrz FAQ: Dlaczego mail z alertem czasem trafia do spamu.

#Przyciski pobierania są nieaktywne albo pobieranie zwraca błąd

ObjawZnaczenie
Przyciski wyszarzoneHasło nie zostało jeszcze odtworzone (stan A).
Karta rozmyta z licznikiemTrwa blokada 48 godzin (stan B).
Kod 423 z download.phpOdliczanie jeszcze trwa.
Kod 403 z download.phpPanic Button został użyty — blokada trwała.
Plik nie istniejeBrakuje pliku w private/ pod nazwą z pola filename albo klucz nie występuje w $downloads.

#Po zmianie configu nikt nie może się zalogować

Możliwa przyczynaCo sprawdzić
Stary format numerów telefonuRozdziel phone na phone_cc i phone.
Nowy config, stare kartyPo nowym podziale i nowym configu stare karty przestają działać — rozdaj nowe.
Zła ścieżka do private/W auth.php ścieżka jest liczona od folderu z tym plikiem. Patrz Instalacja krok po kroku (konfiguracja ścieżki).
Brak prawa zapisu w private/System nie może utworzyć rate_limits.json ani zapisać logu.

#Zmiany w panelu nie są widoczne

Jeśli domena działa za CDN z agresywnym cache (np. „Cache Everything” w Cloudflare), dodaj regułę „bypass” dla ścieżki systemu. Szczegóły w Instalacja krok po kroku (wyjątek w CDN/cache).

Pomoc

Po alercie Panic Button

Co zrobić po otrzymaniu maila z alertem: ocena sytuacji, blokada, sprawdzenie logu i reset.

Dostałeś mail z alertem Panic Button. Oznacza on, że ktoś w panelu po raz pierwszy złożył prawidłowe hasło główne i system uzbroił 48-godzinną blokadę pobierania plików. Mechanizm opisuje strona Ochrona przed zmową powierników. Ta strona mówi, co zrobić krok po kroku.

#Co jest w mailu

Mail podaje, kto uzbroił blokadę (imię i login), kiedy i z jakiego adresu IP, oraz zawiera jednorazowy link panic.php?token=…. Link nie wymaga logowania, żebyś mógł go użyć nawet bez dostępu do konta.

#Najpierw ustal, czy to oczekiwane

SytuacjaCo robić
Żyjesz, a bliscy odzyskują dostęp w umówionej sytuacji (np. awaria, utrata dostępu do konta, uzgodniony test)Nie klikaj linku. Po 48 godzinach pliki staną się dostępne dla bliskich.
To był test lub pomyłka Twoja albo bliskiej osobyNie klikaj linku. Zresetuj blokadę — sekcja „Fałszywy alarm” niżej.
Nikt nie powinien tego robić, imię i IP nie pasująKliknij link Panic Button od razu i przejdź do sekcji „Zmowa lub nadużycie”.
ℹ️
A po Twojej śmierci?
Ten mail czytasz za życia. Po Twojej śmierci nikt nie kliknie Panic Buttona, więc blokada po prostu minie po 48 godzinach i bliscy pobiorą pliki — tak to ma działać. Panic Button służy do ochrony przed zmową za Twojego życia.

#Po kliknięciu linku

Pobieranie plików zostaje zablokowane trwale, niezależnie od tego, czy 48 godzin już minęło. Bezpośrednie wejście na download.php zwraca kod 403, a panel w ciągu około 10 sekund pokazuje stan „zablokowane”. W logu zobaczysz wpis PANIC BUTTON z adresem IP, z którego kliknięto link; nieprawidłowy lub nieaktualny token zapisuje PANIC BUTTON FAILED, a próby pobrania podczas blokady DOWNLOAD BLOCKED.

#Sprawdź log

W private/secret-key.log szukaj wpisów DECRYPT SUCCESS i TIMELOCK ARMED (kto, z jakiego IP i kiedy), logowań tych kont, wpisów DOWNLOAD (czy ktoś pobrał plik) oraz ewentualnych TIMELOCK ARM REJECTED — to próby podania fałszywego hasła przy ustawionym SECRET_HASH.

#Zmowa lub nadużycie

01
Zablokuj pobieranie

Kliknij link z maila. Dopóki trwa 48 godzin, pliki nie mogą zostać pobrane, a kliknięcie blokuje je na stałe.

02
Zmień hasło główne

Powiernicy mogli odtworzyć stare hasło, więc ustaw nowe w menedżerze haseł. Jeśli baza ma drugi składnik (np. klucz YubiKey), samo hasło i tak nie wystarcza do jej otwarcia.

03
Wygeneruj wszystko od nowa

W dashboardzie: nowe hasło w zakładce Szyfrowanie, nowe udziały, nowy secret-key.php i nowe karty. Zdecyduj, kto ma ponownie dostać karty.

04
Wgraj nowy config i podmień plik bazy

Stary stan blokady wygasa automatycznie, bo nie pasuje do odcisku nowej listy osób. Wgraj też nową bazę haseł.

#Fałszywy alarm

Jeśli blokada uzbroiła się przez test lub pomyłkę i nikt nie kliknął Panic Button, usuń private/timelock.json z serwera (FTP lub SSH) albo wgraj nowy secret-key.php. Stan blokady zniknie, a kolejna prawdziwa rekonstrukcja uzbroi ją od nowa. Uprzedź bliskich, że był to test.

ℹ️
Po wszystkim
Opisz incydent w notatce: kto, kiedy, co ustalono i co zmieniono. Przy kolejnym teście wykorzystaj listę kontrolną na osobnym configu, żeby nie wywoływać prawdziwych alertów.
Pomoc

Słowniczek

Najważniejsze pojęcia w jednym miejscu.

Krótkie wyjaśnienia pojęć używanych w dokumentacji, z odsyłaczami do stron, na których temat jest opisany szerzej.

#Kryptografia

PojęcieZnaczenie
Hasło główneHasło do Twojej bazy haseł (np. KeePassXC). To je dzieli system. Algorytm Shamira
UdziałJeden fragment hasła głównego. Sam nie ujawnia nic o haśle. Udział jest zapisany na karcie jako ciąg hex i kod QR.
Próg (K z N)Ile udziałów z wszystkich potrzeba do odtworzenia hasła, np. 3 z 5. W configu: SHARES_REQUIRED.
KartaWydruk z loginem, hasłem logowania i udziałem dla jednej osoby. Karty Secret Key
Powiernik (posiadacz karty)Osoba, której oddajesz kartę. Razem z innymi może odtworzyć hasło.

#Logowanie i panel

PojęcieZnaczenie
2FADrugi składnik logowania: kod z SMS wysyłany na numer przypisany do konta.
bcryptSposób zapisu haseł logowania jako haszy. Haseł jawnych nie ma w configu.
Zaufane urządzeniePrzeglądarka zapamiętana na 7 dni po poprawnym logowaniu, aby nie wymagać kodu za każdym razem.
WebOTPMechanizm przeglądarki, który sam wypełnia pole kodu z SMS-a. Wymaga SMS_AUTOFILL_DOMAIN.
Rate limitingLimit prób logowania i weryfikacji w oknie czasowym, zapisywany w pliku, nie w sesji.
CSRFAtak polegający na wysłaniu żądania w imieniu zalogowanego użytkownika. Chroni przed nim token w formularzach i żądaniach.

#Blokada czasowa

PojęcieZnaczenie
Timelock (blokada czasowa)48 godzin, w czasie których nie można pobrać plików po pierwszej udanej rekonstrukcji hasła. Ochrona przed zmową powierników
Panic ButtonJednorazowy link z maila, który trwale blokuje pobieranie. Po alercie Panic Button
SECRET_HASHHash prawdziwego hasła głównego. Serwer porównuje z nim złożony sekret, zanim uzbroi blokadę.
Uzbrojenie blokadyMoment, w którym system zapisuje stan blokady i wysyła alert. Dzieje się raz, po pierwszej poprawnej rekonstrukcji.

#Konfiguracja

PojęcieZnaczenie
secret-key.phpGłówny plik configu w private/: osoby, pliki do pobrania, instrukcja, mail, SMS. Konfiguracja treści i danych
private/Katalog poza public_html z configiem, logami i plikami do pobrania.
DashboardZestaw narzędzi offline do generowania configu, udziałów i kart. Przegląd dashboard
Tryb testowyWyłączenie wysyłki SMS w auth.php (kod 123456) na czas prób. Test przed rozdaniem kart