commit c7070ec08b2e29b4eebb322458f2953ae29caa20 Author: Lukas Batelka Date: Mon Sep 8 21:09:47 2025 +0200 jednoducha sifrovaci appka diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..13ee2b0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "nuxt.isNuxtApp": false +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e452a9 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +## Šifrovací appka + +Mini Go/HTMX app na šifrování zpráv cizím veřejným klíčem. + +* Při startu vygeneruje/načte RSA identitu (2048 bit). +* Umožní sdílet veřejný klíč (`public.pem`) a self-signed cert (`identity.crt`). +* Šifruje hybridně: **RSA-OAEP(SHA-256) + AES-256-GCM**. +* UI je čisté přes **HTMX**, tlačítka „Copy“ všude, kde to dává smysl. + +--- + +## Požadavky + +* Go 1.21+ +* (volitelné) Make, Docker, atd. – není potřeba pro základní běh + +--- + +## Rychlý start + +```bash +# stačí spustit +go run . + +# otevři v prohlížeči +http://localhost:8080/ +``` + +Na prvním startu se vytvoří soubory: + +``` +identity_key.pem # RSA private key (PKCS#1) +public.pem # veřejný klíč PEM (PKIX) +identity.crt # self-signed cert s veřejným klíčem (jen „vizitka“) +``` + +--- + +## Regenerace klíčů + +Chceš novou identitu? + +```bash +# přes flag +go run . -regen + +# nebo přes env proměnnou +REGEN_KEYS=1 go run . +``` + +--- + +## Jak to používat (UI) + +1. Otevři `http://localhost:8080/`. +2. V sekci **Můj veřejný klíč** stáhni/kopíruj `public.pem` nebo `identity.crt` a pošli kontaktům. +3. V sekci **Šifrovat pro cizí klíč** vlož svou zprávu a **cizí** public key/cert → klikni **Encrypt**. + + * Výstup (**Zašifrovaný payload**) se ukáže **hned pod formulářem** a má tvar JSON: + + ```json + { + "ek": "base64(RSA-OAEP(aesKey))", + "n": "base64(nonce)", + "ct": "base64(aes-gcm-ciphertext)" + } + ``` +4. Příjemce vloží payload do sekce **Dešifrovat** → dostane plaintext. +5. Všude můžeš použít tlačítko **Copy**. + +--- + +## HTTP endpointy + +* `GET /` – HTMX UI +* `GET /public.pem` – veřejný klíč (PEM) +* `GET /public.crt` – self-signed cert (PEM) +* `POST /encrypt` – form fields: + + * `message` – plaintext k zašifrování + * `pubkey` – cílový klíč (PEM `PUBLIC KEY` **nebo** `CERTIFICATE`) +* `POST /decrypt` – form field: + + * `payload` – JSON s poli `ek`, `n`, `ct` (viz výše) +* `GET /static/style.css` – styl + +--- + +## Struktura + +``` +. +├─ main.go +├─ templates/ +│ ├─ index.html +│ ├─ encrypt.html +│ └─ decrypt.html +└─ static/ + └─ style.css +``` + +--- + +## Bezpečnostní poznámky + +* Klíče se ukládají **lokálně** vedle binárky. Chraň `identity_key.pem` (chmod 600 je nastaven). +* Certifikát je jen pro sdílení veřejného klíče (nepoužívá se pro TLS). +* RSA-OAEP se SHA-256 + AES-GCM (12B nonce) pro zprávy libovolné délky. +* Pro produkci řeš správu identit (HSM, password-protected export, rotace, audit). + +--- + +## Problémy & tipy + +* „Neplatný public key/cert“ – zkontroluj, že vkládáš **PEM blok** (`-----BEGIN ...`). +* Payload musí být validní JSON, base64 hodnoty bez zalomení. +* Pokud migruješ mezi stroji, přenes `identity_key.pem` + `public.pem` + `identity.crt`. + +--- + +## Licence + +MIT (nebo si dopiš dle potřeby). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..214d940 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module fckeusp-go + +go 1.24.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..b45f4a7 --- /dev/null +++ b/main.go @@ -0,0 +1,270 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "encoding/pem" + "html/template" + "log" + "math/big" + "net/http" + "time" +) + +var ( + priv *rsa.PrivateKey + pubPEM []byte + certPEM []byte // self-signed cert jen pro sdílení identity (volitelné) + tmpl *template.Template +) + +type envelope struct { + // Encrypted AES key, Nonce, Ciphertext (GCM) + EK string `json:"ek"` // base64(RSA-OAEP(aesKey)) + N string `json:"n"` // base64(nonce 12B) + CT string `json:"ct"` // base64(GCM(ciphertext||tag)) +} + +func main() { + var err error + // 1) identita: RSA klíče + priv, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Fatal(err) + } + pubASN1, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey) + pubPEM = pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubASN1}) + + // 2) volitelně vygeneruj self-signed cert pro export (není pro HTTPS) + certPEM = generateSelfSignedCert(&priv.PublicKey) + + // 3) šablony + tmpl = template.Must(template.ParseGlob("templates/*.html")) + + // 4) routing + http.HandleFunc("/", indexHandler) + http.HandleFunc("/public.pem", publicKeyHandler) + http.HandleFunc("/public.crt", publicCertHandler) + http.HandleFunc("/encrypt", encryptHandler) // POST + http.HandleFunc("/decrypt", decryptHandler) // POST + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + + log.Println("Server běží na http://localhost:8080 (TLS neřešíme; klíč slouží jen k šifrování zpráv).") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func indexHandler(w http.ResponseWriter, r *http.Request) { + _ = tmpl.ExecuteTemplate(w, "index.html", nil) +} + +func publicKeyHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-pem-file") + w.Write(pubPEM) +} + +func publicCertHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/x-pem-file") + w.Write(certPEM) +} + +func encryptHandler(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Bad form", http.StatusBadRequest) + return + } + msg := r.Form.Get("message") + peer := r.Form.Get("pubkey") // může to být PEM PUBLIC KEY nebo CERT + + pubKey, err := parsePeerPublicKey(peer) + if err != nil { + http.Error(w, "Neplatný public key/cert: "+err.Error(), http.StatusBadRequest) + return + } + + // --- hybrid: AES-GCM + RSA-OAEP(SHA-256) --- + // 1) vygeneruj náhodný sym. klíč + aesKey := make([]byte, 32) // AES-256 + if _, err := rand.Read(aesKey); err != nil { + http.Error(w, "Rand fail", 500) + return + } + + block, err := aes.NewCipher(aesKey) + if err != nil { + http.Error(w, "AES fail", 500) + return + } + gcm, err := cipher.NewGCM(block) + if err != nil { + http.Error(w, "GCM fail", 500) + return + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + http.Error(w, "Nonce fail", 500) + return + } + + ciphertext := gcm.Seal(nil, nonce, []byte(msg), nil) + + // 2) zašifruj AES klíč cizím RSA klíčem (OAEP SHA-256) + label := []byte{} // prázdný label + ek, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, aesKey, label) + if err != nil { + http.Error(w, "RSA-OAEP fail: "+err.Error(), 500) + return + } + + env := envelope{ + EK: base64.StdEncoding.EncodeToString(ek), + N: base64.StdEncoding.EncodeToString(nonce), + CT: base64.StdEncoding.EncodeToString(ciphertext), + } + + payload, _ := json.MarshalIndent(env, "", " ") + _ = tmpl.ExecuteTemplate(w, "encrypt.html", string(payload)) +} + +func decryptHandler(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "Bad form", http.StatusBadRequest) + return + } + in := r.Form.Get("payload") + + var env envelope + if err := json.Unmarshal([]byte(in), &env); err != nil { + http.Error(w, "Neplatné JSON", 400) + return + } + + ek, err := base64.StdEncoding.DecodeString(env.EK) + if err != nil { + http.Error(w, "ek base64", 400) + return + } + nonce, err := base64.StdEncoding.DecodeString(env.N) + if err != nil { + http.Error(w, "nonce base64", 400) + return + } + ct, err := base64.StdEncoding.DecodeString(env.CT) + if err != nil { + http.Error(w, "ct base64", 400) + return + } + + // 1) rozšifruj AES klíč naším RSA + label := []byte{} + aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ek, label) + if err != nil { + http.Error(w, "RSA-OAEP decrypt fail: "+err.Error(), 400) + return + } + + block, err := aes.NewCipher(aesKey) + if err != nil { + http.Error(w, "AES fail", 500) + return + } + gcm, err := cipher.NewGCM(block) + if err != nil { + http.Error(w, "GCM fail", 500) + return + } + plain, err := gcm.Open(nil, nonce, ct, nil) + if err != nil { + http.Error(w, "GCM open fail: "+err.Error(), 400) + return + } + + _ = tmpl.ExecuteTemplate(w, "decrypt.html", string(plain)) +} + +// parsePeerPublicKey umí vzít buď PEM PUBLIC KEY, nebo PEM CERT a vrátí *rsa.PublicKey +func parsePeerPublicKey(pemOrCert string) (*rsa.PublicKey, error) { + block, _ := pem.Decode([]byte(pemOrCert)) + if block == nil { + return nil, errf("nenašel jsem PEM blok") + } + switch block.Type { + case "PUBLIC KEY": + k, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + rsaPub, ok := k.(*rsa.PublicKey) + if !ok { + return nil, errf("očekávám RSA PUBLIC KEY") + } + return rsaPub, nil + case "CERTIFICATE": + c, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + rsaPub, ok := c.PublicKey.(*rsa.PublicKey) + if !ok { + return nil, errf("cert neobsahuje RSA klíč") + } + return rsaPub, nil + default: + return nil, errf("nepodporovaný PEM typ: %s", block.Type) + } +} + +func generateSelfSignedCert(pub *rsa.PublicKey) []byte { + tpl := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "Encryptor Local Identity", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + // Tenhle cert je jen „vizitka“ (není pro TLS). + BasicConstraintsValid: true, + } + der, _ := x509.CreateCertificate(rand.Reader, &tpl, &tpl, pub, priv) + buf := &bytes.Buffer{} + _ = pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: der}) + return buf.Bytes() +} + +// malá helper chyba +type strErr string + +func (e strErr) Error() string { return string(e) } +func errf(s string, a ...any) error { return strErr(fmtS(s, a...)) } +func fmtS(format string, a ...any) string { + var b bytes.Buffer + b.WriteString(format) + if len(a) > 0 { + b.WriteString(": ") + } + for i, v := range a { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(anyToString(v)) + } + return b.String() +} +func anyToString(v any) string { + switch t := v.(type) { + case string: + return t + default: + j, _ := json.Marshal(t) + return string(j) + } +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..db7966f --- /dev/null +++ b/static/style.css @@ -0,0 +1,81 @@ +:root { + --bg: #f9fafb; + --card-bg: #fff; + --border: #e5e7eb; + --primary: #2563eb; + --primary-hover: #1d4ed8; + --secondary: #6b7280; + --radius: 12px; + --shadow: 0 4px 8px rgba(0,0,0,0.05); +} + +body { + font-family: system-ui, sans-serif; + background: var(--bg); + color: #111; + max-width: 800px; + margin: 2rem auto; + padding: 0 1rem; +} + +header { text-align:center; margin-bottom:2rem; } +header h1 { margin:0; font-size:2rem; } +header .subtitle { color:#555; margin-top:.25rem; } + +.card { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 1.5rem; + margin-bottom: 1.5rem; +} + +h2 { margin-top:0; font-size:1.3rem; } + +textarea { + flex:1; + padding:.6rem; + border-radius: var(--radius); + border:1px solid var(--border); + font-family: monospace; + resize: vertical; + min-height:100px; +} + +.row { display:flex; gap:.5rem; margin-bottom:1rem; align-items:flex-start; } +.link-row { display:flex; gap:.5rem; margin:.4rem 0; } + +button { + border:none; + border-radius: var(--radius); + padding:.6rem 1rem; + cursor:pointer; + font-weight:500; + transition: background .2s; +} + +button.primary { + background: var(--primary); + color:#fff; +} +button.primary:hover { background: var(--primary-hover); } + +button.secondary { + background: #f3f4f6; + color: var(--secondary); +} +button.secondary:hover { background: #e5e7eb; } + +.result { margin-top:1rem; } + +.note { font-size:.85rem; color:#666; margin-top:.3rem; } + +#toast { + position:fixed; bottom:16px; left:50%; transform:translateX(-50%); + background:#111; color:#fff; + padding:.6rem 1rem; border-radius:var(--radius); + opacity:0; transition:opacity .25s; + pointer-events:none; +} +#toast.show { opacity:1; } diff --git a/templates/decrypt.html b/templates/decrypt.html new file mode 100644 index 0000000..61a4ebb --- /dev/null +++ b/templates/decrypt.html @@ -0,0 +1,7 @@ +
+

Dešifrovaná zpráva

+
+ + +
+
diff --git a/templates/encrypt.html b/templates/encrypt.html new file mode 100644 index 0000000..bacb4f0 --- /dev/null +++ b/templates/encrypt.html @@ -0,0 +1,8 @@ +
+

Zašifrovaný payload

+
+ + +
+

Obsahuje RSA-OAEP šifrovaný AES klíč, nonce a AES-GCM ciphertext.

+
diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..f118118 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,83 @@ + + + + + Encryptor (HTMX) + + + + +
+

🔐 Encryptor

+

Jednoduché HTMX rozhraní pro šifrování zpráv cizím klíčem

+
+ +
+

Můj veřejný klíč

+ + + Pošli tohle kontaktům; můžou šifrovat pro tebe. +
+ +
+

Šifrovat pro cizí klíč

+
+ +
+ + +
+ + +
+ + +
+ + +
+
+
+ +
+

Dešifrovat

+
+ +
+ + +
+ +
+
+
+ +
Zkopírováno
+ + +