Files
mailgoserver/internal/webui/webmail_smime.go
T

179 lines
5.9 KiB
Go

package webui
import (
"io"
"net/http"
"strings"
"mailgoserver/internal/smime"
)
// webmailCertsPage shows both of a mailbox owner's certificate/key types: S/MIME
// certificates (generate/import/remove/download, used for signing, stored plain)
// and PGP keys (used for encryption — see webmail_pgp.go — each protected by its
// own passphrase), plus their collected contact certificates/keys for each
// protocol.
func (a *App) webmailCertsPage(w http.ResponseWriter, r *http.Request) {
mbox := mailboxFromContext(r)
token := sessionToken(r)
identities, err := a.DB.ListSMIMEIdentities(mbox.ID)
if err != nil {
setFlash(w, "error", "Error loading your S/MIME certificates")
}
contacts, err := a.DB.ListSMIMEContacts(mbox.ID)
if err != nil {
setFlash(w, "error", "Error loading S/MIME contact certificates")
}
pgpIdentities, err := a.DB.ListPGPIdentities(mbox.ID)
if err != nil {
setFlash(w, "error", "Error loading your PGP keys")
}
pgpContacts, err := a.DB.ListPGPContacts(mbox.ID)
if err != nil {
setFlash(w, "error", "Error loading PGP contact keys")
}
pgpUnlocked := make(map[int64]bool, len(pgpIdentities))
for _, id := range pgpIdentities {
if _, ok := a.pgpKeys.get(token, id.ID); ok {
pgpUnlocked[id.ID] = true
}
}
a.render(w, r, "webmail_certs.html", M{
"mailbox": mbox,
"identities": identities,
"contacts": contacts,
"pgp_identities": pgpIdentities,
"pgp_unlocked": pgpUnlocked,
"pgp_contacts": pgpContacts,
"flashes": popFlashes(w, r),
})
}
// storeIdentity adds a new identity with certPEM/keyPEM stored as-is — no
// passphrase wrapping (see the schema comment on esrv_mailbox_smime_identities).
func (a *App) storeIdentity(mailboxID int64, certPEM, keyPEM []byte) error {
cert, err := smime.ParseCertPEM(certPEM)
if err != nil {
return err
}
_, err = a.DB.CreateSMIMEIdentity(mailboxID, string(certPEM), string(keyPEM), cert.NotAfter)
return err
}
func (a *App) webmailSMIMEGenerate(w http.ResponseWriter, r *http.Request) {
mbox := mailboxFromContext(r)
certPEM, keyPEM, err := smime.GenerateSelfSigned(mbox.Email, smime.DefaultValidity)
if err == nil {
err = a.storeIdentity(mbox.ID, certPEM, keyPEM)
}
if err != nil {
a.Logger.Error("smime generate for mailbox %d: %v", mbox.ID, err)
setFlash(w, "error", "Error generating certificate")
} else {
setFlash(w, "success", "S/MIME certificate generated")
}
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
}
func (a *App) webmailSMIMEImport(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
}
file, _, err := r.FormFile("p12_file")
if err != nil {
setFlash(w, "error", "Please choose a .p12/.pfx file")
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err == nil {
var certPEM, keyPEM []byte
// p12_password unlocks the uploaded .p12 bundle itself, a one-time-use secret
// distinct from anything stored afterward — the key is kept plain from here on.
certPEM, keyPEM, err = smime.ImportPKCS12(data, r.FormValue("p12_password"))
if err == nil {
err = a.storeIdentity(mbox.ID, certPEM, keyPEM)
}
}
if err != nil {
setFlash(w, "error", "Error importing certificate: "+err.Error())
} else {
setFlash(w, "success", "S/MIME certificate imported")
}
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
}
func (a *App) webmailSMIMERemoveIdentity(w http.ResponseWriter, r *http.Request) {
mbox := mailboxFromContext(r)
identityID := int64(atoi(r.PathValue("identity_id")))
if err := a.DB.DeleteSMIMEIdentity(mbox.ID, identityID); err != nil {
setFlash(w, "error", "Error removing identity")
} else {
setFlash(w, "success", "S/MIME identity removed")
}
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
}
func (a *App) webmailSMIMEDownloadCert(w http.ResponseWriter, r *http.Request) {
mbox := mailboxFromContext(r)
identityID := int64(atoi(r.PathValue("identity_id")))
identity, err := a.DB.GetSMIMEIdentity(mbox.ID, identityID)
if err != nil || identity == nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/x-x509-user-cert")
w.Header().Set("Content-Disposition", `attachment; filename="`+mbox.Email+`.crt"`)
w.Write([]byte(identity.CertPEM))
}
func (a *App) webmailSMIMEAddContact(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"))
file, _, err := r.FormFile("cert_file")
if email == "" || err != nil {
setFlash(w, "error", "Please provide an email and a certificate file")
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
return
}
defer file.Close()
data, err := io.ReadAll(file)
if err == nil {
_, err = smime.ParseCertPEM(data)
}
if err != nil {
setFlash(w, "error", "That doesn't look like a valid certificate file")
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
return
}
if err := a.DB.UpsertSMIMEContact(mbox.ID, email, string(data)); err != nil {
setFlash(w, "error", "Error saving contact certificate")
} else {
setFlash(w, "success", "Contact certificate added")
}
http.Redirect(w, r, MailboxPrefix+"/certs", http.StatusFound)
}
func (a *App) webmailSMIMERemoveContact(w http.ResponseWriter, r *http.Request) {
mbox := mailboxFromContext(r)
contactID := int64(atoi(r.PathValue("contact_id")))
if err := a.DB.DeleteSMIMEContact(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)
}