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

198 lines
5.0 KiB
Go

package main
import (
"embed"
"errors"
"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
UIDOnKey string // tatsächliche UID im Schlüssel
}
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)
}
// sanitizeName entfernt verbotene Zeichen aus Name/Comment-Feldern, die vom OpenPGP-Format für UID reserviert sind
func sanitizeName(s string) string {
s = strings.TrimSpace(s)
// Entferne reservierte und Steuerzeichen, die in der UID nicht vorkommen dürfen
replacer := strings.NewReplacer("<", "", ">", "", "(", "", ")", "", "\r", "", "\n", "")
s = replacer.Replace(s)
// trim doppelte Spaces
for strings.Contains(s, " ") {
s = strings.ReplaceAll(s, " ", " ")
}
return s
}
func sanitizeEmail(s string) string {
s = strings.TrimSpace(s)
s = strings.ReplaceAll(s, " ", "") // keine Spaces
replacer := strings.NewReplacer("<", "", ">", "", "(", "", ")", "", "\r", "", "\n", "")
s = replacer.Replace(s)
return s
}
func validateEmailBasic(s string) bool {
if s == "" {
return false
}
if strings.ContainsAny(s, " <>()\r\n\t") {
return false
}
// sehr einfache Plausibilitätsprüfung
at := strings.IndexByte(s, '@')
if at <= 0 || at == len(s)-1 {
return false
}
if strings.Contains(s[at+1:], "@") {
return false
}
return true
}
func generatePGP(in genInput) (*genResult, error) {
// Kommentar nicht in UID einbetten (gopenpgp's GenerateKey nimmt nur Name+Email)
name := sanitizeName(in.Name)
email := sanitizeEmail(in.Email)
if !validateEmailBasic(email) {
return nil, errors.New("ungültige E-Mail-Adresse")
}
if name == "" {
return nil, errors.New("Name darf nicht leer sein")
}
// 1) Schlüssel erzeugen (UID = "Name <email>")
key, err := crypto.GenerateKey(name, email, "rsa", in.RSABits)
if err != nil {
return nil, fmt.Errorf("GenerateKey: %w", err)
}
// 2) Optional mit Passphrase sperren
if in.Passphrase != "" {
if _, err := key.Lock([]byte(in.Passphrase)); err != nil {
return nil, fmt.Errorf("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,
UIDOnKey: fmt.Sprintf("%s <%s>", name, email),
}, 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)
})
}