MFA fix, added IP blacklist, update webmail client
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user