diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6b87387 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module git.send.nrw/sendnrw/go-pgp-generator + +go 1.24.4 + +require github.com/ProtonMail/go-crypto v1.3.0 // indirect + +require ( + github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/text v0.22.0 // indirect +) + +require ( + github.com/ProtonMail/gopenpgp/v2 v2.9.0 + github.com/cloudflare/circl v1.6.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/sys v0.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c4b3e0d --- /dev/null +++ b/go.sum @@ -0,0 +1,55 @@ +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= +github.com/ProtonMail/gopenpgp/v2 v2.9.0 h1:ruLzBmwe4dR1hdnrsEJ/S7psSBmV15gFttFUPP/+/kE= +github.com/ProtonMail/gopenpgp/v2 v2.9.0/go.mod h1:IldDyh9Hv1ZCCYatTuuEt1XZJ0OPjxLpTarDfglih7s= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..1a72b68 --- /dev/null +++ b/main.go @@ -0,0 +1,152 @@ +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) + }) +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..00d84e7 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,65 @@ + + +
+ + +Verwahren Sie den privaten Schlüssel sicher. Teilen Sie ihn niemals. Nutzen Sie eine starke Passphrase.
+