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)
|
|
})
|
|
}
|