Files
mailgoserver/internal/webui/webmail_pgp.go
T

203 lines
7.1 KiB
Go
Raw Normal View History

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