203 lines
7.1 KiB
Go
203 lines
7.1 KiB
Go
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)
|
||
|
|
}
|