Budování kryptografického toolkitu se zaměřením na lokální provoz

Generátory hesel, JWT dekodéry, hashovací nástroje – všechny po vás chtějí důvěřovat serveru. Germond Security provádí vše ve vašem prohlížeči a dodává stejný kód jako CLI a importovatelnou knihovnu.

·5 min čtení

Většina kryptografických nástrojů na webu má stejný problém. Vložíte JWT do dekodéru a obsah přejde přes síť na cizí server. Zadáte heslo do kontroly síly a to heslo skončí někde v HTTP logu. „HTTPS znamená bezpečnost” míjí pointu — server je stále hrozba.

Germond Security nic nikam neposílá. Každá operace běží ve vašem prohlížeči nad node:crypto, v prohlížeči polyfillovaném přes WebCrypto. Jediný klíč localStorage, který se zapíše, je přepínač tématu. Obnovte stránku a veškerý vstup analyzátoru zmizí.

Dodává se jako tři věci sdílející jeden codebase: webová SPA, CLI (gsec) a TypeScript knihovna.

Co umí

KategorieNástroje
GenerátoryHesla, přístupové fráze (Diceware 2000+ slov), tajné klíče, API klíče
IDUUID v1–v5/v7, NanoID, ULID, KSUID, CUID
HasheSHA-1/256/384/512, HMAC, scrypt, PBKDF2
ŠifryAES-256-GCM, JWT HS256, klíčové páry RSA/Ed25519/ECDSA
Enkodérybase32, base58, base64, hex, octal, binárně — obousměrně
AnalyzátorySíla hesla, entropie, odhad doby prolomení

Vše je implementováno ručně — žádné externí enkódovací knihovny, žádný JWT balíček, žádné generátory ID třetích stran.

Jeden zdroj, tři výstupy

Monorepo má dvě aplikace (web, cli) a hlavní balíček (@germondai/security). Webová aplikace přistupuje ke knihovně přes Vite path alias, který ukazuje přímo na TypeScript zdroj — ne na zkompilovaný výstup:

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

Mezi knihovnou a webovou aplikací není žádný samostatný build krok. Turborepo řídí pořadí závislostí pro CLI, ale webová aplikace importuje přímo ze zdroje. Změňte funkci v packages/security a oba výstupy ji zachytí při dalším uložení.

RNG metodou rejection sampling

Zjevný přístup k výběru náhodného znaku ze sady je charset[randomBytes(1)[0] % charset.length]. Funguje, ale je mírně zkreslený. Pokud délka sady znaků nedělí 256 beze zbytku, znaky s nižším indexem se vyskytují o něco častěji.

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

Rejection sampling čerpá bajty, dokud nezíská takový, který je pod největším násobkem n vejdoucím se do rozsahu bajtu, a pak vezme modulo. Každý znak hesla, každý znak NanoID, každé slovo přístupové fráze se vybírá tímto způsobem.

Crypto shim pro různá prostředí

Spuštění node:crypto v prohlížeči přes vite-plugin-node-polyfills narazí na problém: createHash() z crypto-browserify v moderních Vite nastaveních vyhazuje výjimku (Cannot read properties of undefined (reading 'call')). Shim detekuje globalThis.crypto.subtle a místo toho směruje na nativní 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
}

Obě cesty vrátí stejné bajty. Prohlížeč dostane nativní výkon WebCrypto; Node a Bun dostanou node:crypto. Duální synchronní a asynchronní varianty existují pro AES-GCM a generování klíčových párů ze stejného důvodu — Ed25519 přes WebCrypto je stále nespolehlivé (Chrome 113+, Safari 17+, Firefox 130+), takže asynchronní varianta zachycuje výjimky a přepíná na zálohu.

Efektivní entropie v analyzátoru hesel

Analyzátor síly rozlišuje dvě čísla entropie. Naivní entropie je log2(charsetPool) × délka — co dává matematika hrubé síly za předpokladu plně náhodných, nezávislých znaků. Efektivní entropie odečítá za detekované vzory:

  • Běžná slova: ~10 bitů za každou nalezenou shodu ve slovníku
  • Klávesnicové sekvence: qwerty, 12345, zxcvbn
  • Opakující se znaky a podřetězce
  • Vzory dat: čtyřciferné roky, formáty MM/DD

Detekce slov respektuje CamelCase. MyDog'sNameIsRex se na základě přechodů velikosti písmen a oddělovačů rozdělí na ["My", "Dog", "s", "Name", "Is", "Rex"] před prohledáváním slovníku. Slova kratší než 6 znaků jsou ignorována, aby se předešlo falešným poplachům z náhodných sekvencí znaků obsahujících krátká běžná slova.

Výstup odpovídá pěti scénářům doby prolomení:

ScénářRychlost útoku
Online (s omezením)100/hod
Offline pomalý hash (bcrypt)10 000/s
Offline rychlý hash (MD5)10 mld/s
Distribuovaný GPU cluster100 mld/s
Kvantový (Groverův algoritmus)sqrt(keyspace)/s

Heslo, které vypadá silně na naivní entropii, často spadne o dvě nebo tři úrovně na efektivní entropii. MyDog'sNameIsRex!2024 má 82 naivních bitů, ale výrazně pod 40 efektivními bity, jakmile se odečte pět slovníkových shod a vzor roku.

CLI

Spuštění bunx gsec bez argumentů otevře interaktivního průvodce pokrývajícího všech pět kategorií. S argumenty průvodce přeskočí:

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"

Příznak -r při generování hesla zajistí, že se ve výstupu objeví alespoň jeden znak z každé vybrané třídy. -x odstraní nejednoznačné znaky (0Oo1lI|). Oboje ověřuje smoke test v CI — pipeline vygeneruje skutečná hesla a zkontroluje omezení pomocí bashe.

Nasazení

Třífázový Docker build: turbo prune --docker ořízne monorepo pouze na tranzitivní závislosti webové aplikace, Bun builder instaluje a kompiluje, nginx:alpine obsluhuje statickou SPA. Runtime image neobsahuje Node ani Bun.

Kód je na github.com/germondai/security. CLI funguje dnes; webové rozhraní pokrývá většinu kategorií. Pár věcí v UI stále chybí (podpora argon2, lepší správa klíčových párů) a jsou na seznamu.