198 lines
5.0 KiB
Go
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)
|
||
})
|
||
}
|