Files
go-pgp-generator/main.go
2025-09-22 20:45:54 +02:00

153 lines
3.7 KiB
Go

package main
import (
"embed"
"fmt"
"html/template"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
)
//go:embed templates/*
var templateFS embed.FS
var (
indexTmpl = mustParse("templates/index.html")
resultTmpl = mustParse("templates/result.html")
)
func mustParse(path string) *template.Template {
t, err := template.ParseFS(templateFS, path)
if err != nil {
log.Fatalf("template parse error for %s: %v", path, err)
}
return t
}
type genInput struct {
Name string
Email string
Comment string
RSABits int
Passphrase string
}
type genResult struct {
genInput
PublicArmored string
PrivateArmored string
Fingerprint string
Created time.Time
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handleIndex)
mux.HandleFunc("/generate", handleGenerate)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200); _, _ = w.Write([]byte("ok")) })
addr := ":8081"
log.Printf("PGP Keygen läuft auf http://localhost%s", addr)
if err := http.ListenAndServe(addr, securityHeaders(mux)); err != nil {
log.Fatal(err)
}
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
_ = indexTmpl.Execute(w, map[string]any{})
}
func handleGenerate(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Ungültige Eingaben", http.StatusBadRequest)
return
}
in := genInput{
Name: strings.TrimSpace(r.FormValue("name")),
Email: strings.TrimSpace(r.FormValue("email")),
Comment: strings.TrimSpace(r.FormValue("comment")),
Passphrase: r.FormValue("passphrase"),
}
bitsStr := r.FormValue("rsabits")
if bitsStr == "" {
bitsStr = "4096"
}
bits, err := strconv.Atoi(bitsStr)
if err != nil || (bits != 2048 && bits != 3072 && bits != 4096) {
bits = 4096
}
in.RSABits = bits
res, err := generatePGP(in)
if err != nil {
log.Printf("generate error: %v", err)
http.Error(w, fmt.Sprintf("Fehler beim Erzeugen der Schlüssel: %v", err), http.StatusInternalServerError)
return
}
_ = resultTmpl.Execute(w, res)
}
func generatePGP(in genInput) (*genResult, error) {
// Kommentar wird in die User-ID integriert
name := in.Name
if in.Comment != "" {
name = fmt.Sprintf("%s (%s)", in.Name, in.Comment)
}
// 1) Schlüssel erzeugen (noch unverschlüsselt)
key, err := crypto.GenerateKey(name, in.Email, "rsa", in.RSABits)
if err != nil {
return nil, fmt.Errorf("GenerateKey: %w", err)
}
// 2) Optional mit Passphrase sperren (at-rest Verschlüsselung)
if in.Passphrase != "" {
if _, err := key.Lock([]byte(in.Passphrase)); err != nil {
return nil, fmt.Errorf("Lock (Passphrase setzen) fehlgeschlagen: %w", err)
}
}
// 3) Armored ausgeben
armoredPriv, err := key.Armor()
if err != nil {
return nil, fmt.Errorf("armor private: %w", err)
}
armoredPub, err := key.GetArmoredPublicKey()
if err != nil {
return nil, fmt.Errorf("armor public: %w", err)
}
fp := strings.ToUpper(key.GetFingerprint())
created := time.Now()
return &genResult{
genInput: in,
PublicArmored: armoredPub,
PrivateArmored: armoredPriv,
Fingerprint: fp,
Created: created,
}, nil
}
// --- security headers middleware ---
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("Referrer-Policy", "no-referrer")
next.ServeHTTP(w, r)
})
}