Files

244 lines
8.9 KiB
Go

package webui
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"mailgoserver/internal/pgp"
)
// TestWebmailPGPGenerateAndDownload confirms a mailbox owner can generate a
// passphrase-protected PGP key, see it reflected on the Certs page, and download
// the public key.
func TestWebmailPGPGenerateAndDownload(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domains, _ := app.DB.ListDomains()
mailboxID := createTestMailboxWithPassword(t, app, "pgp1@example.com", domains[0].ID, "pgp-password-1!")
cookie := webmailLoginSession(t, app, mailboxID)
form := url.Values{"label": {"Work key"}, "passphrase": {"correct horse battery staple"}, "passphrase_confirm": {"correct horse battery staple"}}
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/identity/generate", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("generate: status=%d body=%s", rec.Code, rec.Body.String())
}
identities, err := app.DB.ListPGPIdentities(mailboxID)
if err != nil || len(identities) != 1 {
t.Fatalf("expected 1 PGP identity, got %d (err=%v)", len(identities), err)
}
identity := identities[0]
if identity.Label != "Work key" {
t.Errorf("label = %q, want %q", identity.Label, "Work key")
}
if identity.Fingerprint == "" {
t.Error("expected a non-empty fingerprint")
}
if bytes.Contains([]byte(identity.PrivateKeyArmor), []byte("correct horse")) {
t.Fatal("stored private key armor should not contain the plaintext passphrase")
}
pageReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/certs", nil)
pageReq.AddCookie(cookie)
pageRec := httptest.NewRecorder()
mux.ServeHTTP(pageRec, pageReq)
if pageRec.Code != http.StatusOK || !strings.Contains(pageRec.Body.String(), "Work key") {
t.Fatalf("expected the Certs page to show the PGP identity, status=%d body=%s", pageRec.Code, pageRec.Body.String())
}
if !strings.Contains(pageRec.Body.String(), "Locked") {
t.Fatal("expected the PGP identity to show as locked before any unlock")
}
dlReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/pgp/identity/"+strconv.FormatInt(identity.ID, 10)+"/download", nil)
dlReq.AddCookie(cookie)
dlRec := httptest.NewRecorder()
mux.ServeHTTP(dlRec, dlReq)
if dlRec.Code != http.StatusOK || dlRec.Body.String() != identity.PublicKeyArmor {
t.Fatalf("expected downloaded key to match stored public key, status=%d", dlRec.Code)
}
}
// TestWebmailPGPUnlockWrongPassphraseRejected confirms the unlock endpoint rejects
// an incorrect passphrase and caches nothing.
func TestWebmailPGPUnlockWrongPassphraseRejected(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domains, _ := app.DB.ListDomains()
mailboxID := createTestMailboxWithPassword(t, app, "pgp2@example.com", domains[0].ID, "pgp-password-1!")
cookie := webmailLoginSession(t, app, mailboxID)
form := url.Values{"passphrase": {"the real passphrase"}, "passphrase_confirm": {"the real passphrase"}}
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/identity/generate", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(cookie)
mux.ServeHTTP(httptest.NewRecorder(), req)
identities, _ := app.DB.ListPGPIdentities(mailboxID)
if len(identities) != 1 {
t.Fatalf("expected 1 identity, got %d", len(identities))
}
unlock := func(passphrase string) *httptest.ResponseRecorder {
f := url.Values{"identity_id": {strconv.FormatInt(identities[0].ID, 10)}, "passphrase": {passphrase}}
r := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/unlock", strings.NewReader(f.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
r.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, r)
return rec
}
if rec := unlock("wrong passphrase"); rec.Code != http.StatusFound {
t.Fatalf("status=%d", rec.Code)
}
if _, ok := app.pgpKeys.get(sessionTokenFromCookie(cookie), identities[0].ID); ok {
t.Fatal("expected no key cached after a wrong-passphrase unlock attempt")
}
if rec := unlock("the real passphrase"); rec.Code != http.StatusFound {
t.Fatalf("status=%d", rec.Code)
}
if _, ok := app.pgpKeys.get(sessionTokenFromCookie(cookie), identities[0].ID); !ok {
t.Fatal("expected the key cached after the correct passphrase")
}
}
// TestWebmailPGPContactAddAndRemove confirms a contact's public key can be added
// (validated as a real key), listed, and removed again.
func TestWebmailPGPContactAddAndRemove(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domains, _ := app.DB.ListDomains()
mailboxID := createTestMailboxWithPassword(t, app, "pgp3@example.com", domains[0].ID, "pgp-password-1!")
cookie := webmailLoginSession(t, app, mailboxID)
contactPub, _, err := pgp.GenerateKeyPair("contact@other.example", "contact passphrase")
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
mw.WriteField("email", "contact@other.example")
mw.WriteField("label", "Other")
fw, err := mw.CreateFormFile("key_file", "contact.asc")
if err != nil {
t.Fatal(err)
}
fw.Write(contactPub)
mw.Close()
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/contacts/add", &buf)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("add contact: status=%d body=%s", rec.Code, rec.Body.String())
}
contacts, err := app.DB.ListPGPContacts(mailboxID)
if err != nil || len(contacts) != 1 || contacts[0].Email != "contact@other.example" || contacts[0].Fingerprint == "" {
t.Fatalf("expected 1 contact with a fingerprint, got %+v (err=%v)", contacts, err)
}
rmReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/contacts/"+strconv.FormatInt(contacts[0].ID, 10)+"/remove", nil)
rmReq.AddCookie(cookie)
rmRec := httptest.NewRecorder()
mux.ServeHTTP(rmRec, rmReq)
if rmRec.Code != http.StatusFound {
t.Fatalf("remove contact: status=%d", rmRec.Code)
}
remaining, err := app.DB.ListPGPContacts(mailboxID)
if err != nil || len(remaining) != 0 {
t.Fatalf("expected no contacts left, got %d (err=%v)", len(remaining), err)
}
}
// TestWebmailPGPAddContactRejectsGarbage confirms an upload that isn't a valid PGP
// public key is rejected rather than silently stored.
func TestWebmailPGPAddContactRejectsGarbage(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domains, _ := app.DB.ListDomains()
mailboxID := createTestMailboxWithPassword(t, app, "pgp4@example.com", domains[0].ID, "pgp-password-1!")
cookie := webmailLoginSession(t, app, mailboxID)
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
mw.WriteField("email", "nope@example.com")
fw, err := mw.CreateFormFile("key_file", "notakey.asc")
if err != nil {
t.Fatal(err)
}
fw.Write([]byte("this is not a pgp key"))
mw.Close()
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/contacts/add", &buf)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("status=%d", rec.Code)
}
contacts, err := app.DB.ListPGPContacts(mailboxID)
if err != nil || len(contacts) != 0 {
t.Fatalf("expected the invalid key rejected, got %d contacts (err=%v)", len(contacts), err)
}
}
// TestWebmailPGPImportAlreadyEncryptedKey confirms importing an existing
// passphrase-protected armored key (as if exported from GnuPG) works end to end
// through the real HTTP import endpoint.
func TestWebmailPGPImportAlreadyEncryptedKey(t *testing.T) {
app := newTestApp(t)
mux := app.Mux()
domains, _ := app.DB.ListDomains()
mailboxID := createTestMailboxWithPassword(t, app, "pgp5@example.com", domains[0].ID, "pgp-password-1!")
cookie := webmailLoginSession(t, app, mailboxID)
_, privArmor, err := pgp.GenerateKeyPair("imported@example.com", "existing passphrase")
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
mw := multipart.NewWriter(&buf)
mw.WriteField("label", "Imported")
mw.WriteField("passphrase", "existing passphrase")
fw, err := mw.CreateFormFile("key_file", "imported.asc")
if err != nil {
t.Fatal(err)
}
fw.Write(privArmor)
mw.Close()
req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/identity/import", &buf)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.AddCookie(cookie)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("import: status=%d body=%s", rec.Code, rec.Body.String())
}
identities, err := app.DB.ListPGPIdentities(mailboxID)
if err != nil || len(identities) != 1 {
t.Fatalf("expected 1 identity, got %d (err=%v)", len(identities), err)
}
if identities[0].Email != "imported@example.com" {
t.Errorf("expected the email extracted from the key itself, got %q", identities[0].Email)
}
}