package main import ( "crypto/ecdsa" "crypto/elliptic" cryptoRand "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "net" "net/url" "os" "path/filepath" "sort" "strings" "sync" "time" ) var ( localTLSOnce sync.Once localTLSCertPath string localTLSKeyPath string localTLSErr error ) func localTLSPathsCached() (string, string, error) { localTLSOnce.Do(func() { localTLSCertPath, localTLSKeyPath, localTLSErr = ensureLocalTLSCertificate() }) return localTLSCertPath, localTLSKeyPath, localTLSErr } func localTLSDir() (string, error) { if dir, err := os.UserConfigDir(); err == nil && strings.TrimSpace(dir) != "" { return filepath.Join(dir, "nsfwapp", "tls"), nil } home, err := os.UserHomeDir() if err != nil || strings.TrimSpace(home) == "" { return "", fmt.Errorf("user config dir konnte nicht ermittelt werden") } return filepath.Join(home, ".config", "nsfwapp", "tls"), nil } func localTLSHosts() []string { seen := map[string]bool{} out := []string{} add := func(host string) { host = normalizeOriginHost(host) if host == "" || seen[host] { return } seen[host] = true out = append(out, host) } for _, origin := range configuredPublicAppOrigins() { u, err := url.Parse(strings.TrimSpace(origin)) if err == nil { add(u.Hostname()) } } for _, host := range localAppHostCandidates() { add(host) } add("localhost") add("127.0.0.1") add("::1") sort.Strings(out) return out } func tlsCertificateCoversHosts(certPath string, hosts []string) bool { cert, err := readLeafCertificate(certPath) if err != nil { return false } now := time.Now() if now.Before(cert.NotBefore) || now.After(cert.NotAfter.Add(-30*24*time.Hour)) { return false } for _, host := range hosts { host = normalizeOriginHost(host) if host == "" { continue } if err := cert.VerifyHostname(host); err != nil { return false } } return true } func readLeafCertificate(certPath string) (*x509.Certificate, error) { b, err := os.ReadFile(certPath) if err != nil { return nil, err } block, _ := pem.Decode(b) if block == nil || block.Type != "CERTIFICATE" { return nil, fmt.Errorf("keine PEM-Zertifikatsdaten") } return x509.ParseCertificate(block.Bytes) } func ensureLocalTLSCertificate() (string, string, error) { dir, err := localTLSDir() if err != nil { return "", "", err } if err := os.MkdirAll(dir, 0o700); err != nil { return "", "", err } certPath := filepath.Join(dir, "recorder-cert.pem") keyPath := filepath.Join(dir, "recorder-key.pem") hosts := localTLSHosts() if fileExistsNonEmpty(certPath) && fileExistsNonEmpty(keyPath) && tlsCertificateCoversHosts(certPath, hosts) { appLogln("🔐 TLS-Zertifikat aktiv:", certPath) return certPath, keyPath, nil } if err := generateLocalTLSCertificate(certPath, keyPath, hosts); err != nil { return "", "", err } appLogln("🔐 TLS-Zertifikat neu erstellt:", certPath) appLogln("🔐 TLS-Namen:", strings.Join(hosts, ", ")) return certPath, keyPath, nil } func generateLocalTLSCertificate(certPath string, keyPath string, hosts []string) error { key, err := ecdsa.GenerateKey(elliptic.P256(), cryptoRand.Reader) if err != nil { return err } serialLimit := new(big.Int).Lsh(big.NewInt(1), 128) serial, err := cryptoRand.Int(cryptoRand.Reader, serialLimit) if err != nil { return err } notBefore := time.Now().Add(-1 * time.Hour) notAfter := notBefore.Add(397 * 24 * time.Hour) tpl := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ CommonName: "nsfwapp local", Organization: []string{"nsfwapp"}, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } seenDNS := map[string]bool{} seenIP := map[string]bool{} for _, host := range hosts { host = normalizeOriginHost(host) if host == "" { continue } if ip := net.ParseIP(host); ip != nil { key := ip.String() if !seenIP[key] { seenIP[key] = true tpl.IPAddresses = append(tpl.IPAddresses, ip) } continue } if !seenDNS[host] { seenDNS[host] = true tpl.DNSNames = append(tpl.DNSNames, host) } } if len(tpl.DNSNames) == 0 && len(tpl.IPAddresses) == 0 { tpl.DNSNames = []string{"localhost"} } certDER, err := x509.CreateCertificate( cryptoRand.Reader, tpl, tpl, &key.PublicKey, key, ) if err != nil { return err } keyDER, err := x509.MarshalECPrivateKey(key) if err != nil { return err } certPEM := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: certDER, }) keyPEM := pem.EncodeToMemory(&pem.Block{ Type: "EC PRIVATE KEY", Bytes: keyDER, }) if err := os.WriteFile(certPath, certPEM, 0o600); err != nil { return err } if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil { return err } return nil }