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

198 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 EMail-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)
})
}