package webui import ( "io" "net/http" "strings" "mailgoserver/internal/pgp" ) func (a *App) webmailPGPGenerate(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) label := strings.TrimSpace(r.FormValue("label")) passphrase := r.FormValue("passphrase") if len(passphrase) < 8 { setFlash(w, "error", "Choose a passphrase of at least 8 characters — this is the only thing protecting the key, so make it a real one") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } if passphrase != r.FormValue("passphrase_confirm") { setFlash(w, "error", "Passphrases don't match") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } pubArmor, privArmor, err := pgp.GenerateKeyPair(mbox.Email, passphrase) if err == nil { err = a.storePGPIdentity(mbox.ID, label, mbox.Email, pubArmor, privArmor) } if err != nil { a.Logger.Error("pgp generate for mailbox %d: %v", mbox.ID, err) setFlash(w, "error", "Error generating PGP key") } else { setFlash(w, "success", "PGP key generated") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) } // storePGPIdentity parses pubArmor for its fingerprint, then stores both halves — // privArmor is expected already passphrase-protected by the caller (pgp.GenerateKeyPair // or pgp.ImportPrivateKey both guarantee this). func (a *App) storePGPIdentity(mailboxID int64, label, email string, pubArmor, privArmor []byte) error { pubEntity, err := pgp.ParsePublicKey(pubArmor) if err != nil { return err } fingerprint := pgp.Fingerprint(pubEntity) _, err = a.DB.CreatePGPIdentity(mailboxID, label, email, fingerprint, string(pubArmor), string(privArmor)) return err } func (a *App) webmailPGPImport(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) if err := r.ParseMultipartForm(1 << 20); err != nil { setFlash(w, "error", "Error reading upload") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } label := strings.TrimSpace(r.FormValue("label")) passphrase := r.FormValue("passphrase") if passphrase == "" { setFlash(w, "error", "Enter the key's passphrase — its own, if it already has one, or a new one to protect it with if it doesn't") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } file, _, err := r.FormFile("key_file") if err != nil { setFlash(w, "error", "Please choose an armored PGP private key file (.asc)") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } defer file.Close() data, err := io.ReadAll(file) if err == nil { var pubArmor, privArmor []byte pubArmor, privArmor, err = pgp.ImportPrivateKey(data, passphrase) if err == nil { email := mbox.Email if entity, perr := pgp.ParsePublicKey(pubArmor); perr == nil { if id := entity.PrimaryIdentity(); id != nil && id.UserId != nil && id.UserId.Email != "" { email = id.UserId.Email } } err = a.storePGPIdentity(mbox.ID, label, email, pubArmor, privArmor) } } if err != nil { setFlash(w, "error", "Error importing PGP key: "+err.Error()) } else { setFlash(w, "success", "PGP key imported") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) } func (a *App) webmailPGPRemoveIdentity(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) identityID := int64(atoi(r.PathValue("identity_id"))) if err := a.DB.DeletePGPIdentity(mbox.ID, identityID); err != nil { setFlash(w, "error", "Error removing PGP key") } else { setFlash(w, "success", "PGP key removed") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) } func (a *App) webmailPGPDownloadKey(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) identityID := int64(atoi(r.PathValue("identity_id"))) identity, err := a.DB.GetPGPIdentity(mbox.ID, identityID) if err != nil || identity == nil { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/pgp-keys") w.Header().Set("Content-Disposition", `attachment; filename="`+mbox.Email+`.asc"`) w.Write([]byte(identity.PublicKeyArmor)) } // webmailPGPUnlock verifies a passphrase against one identity's stored private key // and, on success, caches the unlocked entity for the rest of this login session // (see webmail_pgp_cache.go) — the one interactive "enter your passphrase" flow in // this codebase now, since S/MIME keys are stored plain and never need unlocking. func (a *App) webmailPGPUnlock(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) next := r.FormValue("next") if !strings.HasPrefix(next, MailboxPrefix+"/") { next = MailboxPrefix + "/certs" } identityID := int64(atoi(r.FormValue("identity_id"))) identity, err := a.DB.GetPGPIdentity(mbox.ID, identityID) if err != nil || identity == nil { setFlash(w, "error", "Unknown PGP key") http.Redirect(w, r, next, http.StatusFound) return } entity, err := pgp.ParsePrivateKey([]byte(identity.PrivateKeyArmor)) if err != nil { a.Logger.Error("parse stored pgp key for identity %d: %v", identity.ID, err) setFlash(w, "error", "That key's stored data is corrupted") http.Redirect(w, r, next, http.StatusFound) return } if err := pgp.UnlockPrivateKey(entity, r.FormValue("passphrase")); err != nil { setFlash(w, "error", "Wrong passphrase") http.Redirect(w, r, next, http.StatusFound) return } a.pgpKeys.put(sessionToken(r), identity.ID, entity) setFlash(w, "success", "PGP key unlocked for this session") http.Redirect(w, r, next, http.StatusFound) } func (a *App) webmailPGPAddContact(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) if err := r.ParseMultipartForm(1 << 20); err != nil { setFlash(w, "error", "Error reading upload") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } email := strings.TrimSpace(r.FormValue("email")) label := strings.TrimSpace(r.FormValue("label")) file, _, err := r.FormFile("key_file") if email == "" || err != nil { setFlash(w, "error", "Please provide an email and a public key file") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } defer file.Close() data, err := io.ReadAll(file) var fingerprint string if err == nil { parsed, perr := pgp.ParsePublicKey(data) if perr != nil { err = perr } else { fingerprint = pgp.Fingerprint(parsed) } } if err != nil { setFlash(w, "error", "That doesn't look like a valid PGP public key file") http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) return } if err := a.DB.UpsertPGPContact(mbox.ID, email, label, fingerprint, string(data)); err != nil { setFlash(w, "error", "Error saving contact key") } else { setFlash(w, "success", "Contact key added") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) } func (a *App) webmailPGPRemoveContact(w http.ResponseWriter, r *http.Request) { mbox := mailboxFromContext(r) contactID := int64(atoi(r.PathValue("contact_id"))) if err := a.DB.DeletePGPContact(mbox.ID, contactID); err != nil { setFlash(w, "error", "Error removing contact") } else { setFlash(w, "success", "Contact removed") } http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound) }