Budowanie lokalnego zestawu narzędzi kryptograficznych

Generatory haseł, dekodery JWT, narzędzia do hashowania — wszystkie wymagają zaufania serwerowi. Germond Security uruchamia wszystko w przeglądarce i dostarcza ten sam kod jako CLI i bibliotekę.

·5 min czytania

Większość narzędzi kryptograficznych w sieci ma ten sam problem. Wklejasz JWT do dekodera i payload trafia na czyjś serwer. Wpisujesz hasło do sprawdzarki siły i to hasło ląduje gdzieś w logu HTTP. “HTTPS znaczy, że jest bezpiecznie” mija się z celem — serwer nadal jest zagrożeniem.

Germond Security nie wysyła niczego nigdzie. Każda operacja działa w przeglądarce na node:crypto, z polyfill dla przeglądarki przez WebCrypto. Jedynym kluczem zapisywanym do localStorage jest przełącznik motywu. Odśwież stronę, a wszystkie dane wejściowe analizatora znikają.

Dostarcza trzy rzeczy korzystające z jednej bazy kodu: webową SPA, CLI (gsec) i bibliotekę TypeScript.

Co obejmuje

KategoriaNarzędzia
GeneratoryHasła, passphrase (Diceware, 2000+ słów), sekrety, klucze API
IDUUID v1–v5/v7, NanoID, ULID, KSUID, CUID
HasheSHA-1/256/384/512, HMAC, scrypt, PBKDF2
SzyfryAES-256-GCM, JWT HS256, pary kluczy RSA/Ed25519/ECDSA
Enkoderybase32, base58, base64, hex, octal, binary — dwukierunkowe
AnalizatorySiła hasła, entropia, szacowany czas łamania

Wszystko napisane od zera — żadnych zewnętrznych bibliotek do enkodowania, żadnego pakietu JWT, żadnych zewnętrznych generatorów ID.

Jedno źródło, trzy wyjścia

Monorepo ma dwie aplikacje (web, cli) i pakiet rdzennych funkcji (@germondai/security). Aplikacja webowa korzysta z rdzenia przez alias ścieżki Vite wskazujący bezpośrednio na źródło TypeScript, nie na skompilowany output:

// apps/web/vite.config.ts
"@germondai/security": fileURLToPath(
  new URL("../../packages/security/src/index.ts", import.meta.url)
)

Żadnego osobnego kroku budowania między biblioteką a aplikacją webową. Turborepo obsługuje kolejność zależności dla CLI, ale aplikacja webowa importuje prosto ze źródła. Zmień funkcję w packages/security, a oba wyjścia pobiorą ją przy następnym zapisie.

RNG z próbkowaniem odrzucającym

Oczywiste podejście do wybierania losowego znaku z zestawu znaków to charset[randomBytes(1)[0] % charset.length]. Działa, ale jest subtelnie stronnicze. Gdy długość zestawu znaków nie dzieli równomiernie 256, znaki o niskim indeksie pojawiają się nieco częściej niż te o wysokim.

function uniformInt(n: number): number {
  const limit = 256 - (256 % n)
  let x: number
  do { x = randomBytes(1)[0] } while (x >= limit)
  return x % n
}

Próbkowanie odrzucające losuje bajty, dopóki nie trafi na taki poniżej największej wielokrotności n mieszczącej się w zakresie bajtu, a następnie bierze modulo. Każdy znak hasła, każdy znak NanoID, każde słowo passphrase jest wybierane w ten sposób.

Shim kryptograficzny dla różnych środowisk

Uruchomienie node:crypto w przeglądarce przez vite-plugin-node-polyfills napotyka na przeszkodę: createHash() z crypto-browserify rzuca wyjątek w nowoczesnych konfiguracjach Vite (Cannot read properties of undefined (reading 'call')). Shim wykrywa globalThis.crypto.subtle i zamiast tego kieruje do natywnego WebCrypto:

async function digest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer> {
  if (globalThis.crypto?.subtle) {
    return globalThis.crypto.subtle.digest(algorithm, data)
  }
  const { createHash } = await import('node:crypto')
  return createHash(algorithm.replace('-', '').toLowerCase())
    .update(data)
    .digest()
    .buffer as ArrayBuffer
}

Obie ścieżki zwracają te same bajty. Przeglądarka otrzymuje natywną wydajność WebCrypto; Node i Bun dostają node:crypto. Podwójne warianty synchroniczne i asynchroniczne istnieją dla AES-GCM i generowania par kluczy z tego samego powodu — Ed25519 przez WebCrypto jest nadal niestabilny (Chrome 113+, Safari 17+, Firefox 130+), więc wariant asynchroniczny łapie wyjątki i stosuje fallback.

Efektywna entropia w analizatorze haseł

Analizator siły rozdziela dwie liczby entropii. Naiwna entropia to log2(charsetPool) × length — to, co daje matematyka brute-force zakładając w pełni losowe, niezależne znaki. Efektywna entropia odejmuje za wykryte wzorce:

  • Popularne słowa: ~10 bitów odjętych za każde unikalne trafienie słownikowe
  • Sekwencje klawiaturowe: qwerty, 12345, zxcvbn
  • Powtórzone znaki i podciągi
  • Wzorce dat: czterocyfrowe lata, formaty MM/DD

Wykrywanie słów uwzględnia CamelCase. MyDog'sNameIsRex jest dzielone na granicach wielkości liter i separatorach na ["My", "Dog", "s", "Name", "Is", "Rex"] przed skanowaniem pod kątem listy słów. Słowa krótsze niż 6 znaków są ignorowane, aby uniknąć fałszywych trafień ze losowych ciągów znaków, które zawierają krótkie popularne słowa.

Wynik mapuje się na pięć scenariuszy czasu łamania:

ScenariuszSzybkość zgadywania
Online (z rate limiting)100/godz.
Offline wolny hash (bcrypt)10K/sek.
Offline szybki hash (MD5)10B/sek.
Rozproszony klaster GPU100B/sek.
Kwantowy (algorytm Grovera)sqrt(keyspace)/sek.

To samo hasło, które wygląda silnie pod względem naiwnej entropii, często spada o dwa lub trzy poziomy w entropii efektywnej. MyDog'sNameIsRex!2024 ma 82 naiwne bity, ale dobrze poniżej 40 efektywnych bitów po odjęciu pięciu trafień słownikowych i wzorca roku.

CLI

Uruchomienie bunx gsec bez argumentów otwiera interaktywny wizard obejmujący wszystkie pięć kategorii. Z argumentami pomija wizard:

bunx gsec gen password -l 24 --no-symbols
bunx gsec gen id --type uuid-v7 -n 10
bunx gsec hash sha256 "hello world"
bunx gsec cipher aes-gcm encrypt --passphrase secret "message"
bunx gsec analyze strength "MyDog'sNameIsRex!2024"

Flaga -r przy generowaniu hasła wymusza, że przynajmniej jeden znak z każdej wybranej klasy pojawia się w wyjściu. -x usuwa niejednoznaczne znaki (0Oo1lI|). Obie są weryfikowane przez smoke test CI — pipeline generuje prawdziwe hasła i sprawdza ograniczenia przez bash.

Wdrożenie

Trzystopniowy build Docker: turbo prune --docker przycina monorepo do tylko zależności transytywnych aplikacji webowej, builder Bun instaluje i kompiluje, a nginx:alpine serwuje statyczną SPA. Obraz runtime nie ma Node ani Bun.

Kod jest na github.com/germondai/security. CLI działa dziś; web UI pokrywa większość kategorii. Kilka rzeczy wciąż brakuje w UI (wsparcie argon2, lepsze zarządzanie parami kluczy) — są na liście.