package webui import ( "net/http" "net/http/httptest" "net/url" "strconv" "strings" "testing" ) // TestWebmailReadPGPEncryptedMessageRequiresUnlockThenDecrypts confirms the full // decrypt-then-view flow: opening a PGP-encrypted message the recipient hasn't // unlocked yet shows a passphrase prompt (not the content), and submitting the // actual /pgp/unlock form makes the same message decrypt and render on the next // view. func TestWebmailReadPGPEncryptedMessageRequiresUnlockThenDecrypts(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() domainID := domains[0].ID senderID := createTestMailboxWithPassword(t, app, "pread-enc-sender@example.com", domainID, "sender-password-1!") recipientID := createTestMailboxWithPassword(t, app, "pread-enc-recip@example.com", domainID, "recip-password-1!") genPGPIdentity(t, app, senderID, "pread-enc-sender@example.com", "sender passphrase") recipIdentityID := genPGPIdentity(t, app, recipientID, "pread-enc-recip@example.com", "recipient passphrase") recipIdentity, err := app.DB.GetPGPIdentity(recipientID, recipIdentityID) if err != nil || recipIdentity == nil { t.Fatal(err) } if err := app.DB.UpsertPGPContact(senderID, "pread-enc-recip@example.com", "", recipIdentity.Fingerprint, recipIdentity.PublicKeyArmor); err != nil { t.Fatal(err) } recipContact, err := app.DB.GetPGPContact(senderID, "pread-enc-recip@example.com") if err != nil || recipContact == nil { t.Fatal(err) } senderCookie := webmailLoginSession(t, app, senderID) form := url.Values{ "to": {"pread-enc-recip@example.com"}, "subject": {"Encrypted read test"}, "body_html": {"the vault code is 9999"}, "pgp_encrypt": {"1"}, "pgp_recipient_id": {strconv.FormatInt(recipContact.ID, 10)}, } req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(senderCookie) mux.ServeHTTP(httptest.NewRecorder(), req) msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX") if err != nil || len(msgs) != 1 { t.Fatalf("expected 1 message, got %d (err=%v)", len(msgs), err) } messageURL := MailboxPrefix + "/mail/INBOX/" + strconv.FormatInt(msgs[0].ID, 10) recipientCookie := webmailLoginSession(t, app, recipientID) // First view: nothing unlocked yet — should prompt, not decrypt. viewReq := httptest.NewRequest(http.MethodGet, messageURL, nil) viewReq.AddCookie(recipientCookie) viewRec := httptest.NewRecorder() mux.ServeHTTP(viewRec, viewReq) if viewRec.Code != http.StatusOK { t.Fatalf("view: status=%d body=%s", viewRec.Code, viewRec.Body.String()) } body := viewRec.Body.String() if !strings.Contains(body, "enter your passphrase to decrypt") { t.Fatalf("expected a needs-unlock prompt on first view, got body: %s", body) } if strings.Contains(body, "vault code") { t.Fatal("plaintext should not render before unlocking") } // Unlock via the real endpoint, exactly as the rendered form would submit. unlockForm := url.Values{"identity_id": {strconv.FormatInt(recipIdentityID, 10)}, "passphrase": {"recipient passphrase"}, "next": {messageURL}} unlockReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/unlock", strings.NewReader(unlockForm.Encode())) unlockReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") unlockReq.AddCookie(recipientCookie) unlockRec := httptest.NewRecorder() mux.ServeHTTP(unlockRec, unlockReq) if unlockRec.Code != http.StatusFound || unlockRec.Header().Get("Location") != messageURL { t.Fatalf("unlock: status=%d location=%q", unlockRec.Code, unlockRec.Header().Get("Location")) } // Second view: now decrypts. viewReq2 := httptest.NewRequest(http.MethodGet, messageURL, nil) viewReq2.AddCookie(recipientCookie) viewRec2 := httptest.NewRecorder() mux.ServeHTTP(viewRec2, viewReq2) if viewRec2.Code != http.StatusOK { t.Fatalf("view after unlock: status=%d", viewRec2.Code) } body2 := viewRec2.Body.String() if !strings.Contains(body2, "PGP encrypted & decrypted") { t.Fatalf("expected a decrypted badge after unlock, got body: %s", body2) } if !strings.Contains(body2, "the vault code is 9999") { t.Fatal("expected the decrypted body rendered after unlock") } } // TestWebmailReadSMIMESignAndPGPEncryptTogether confirms a message both S/MIME-signed // and PGP-encrypted unwraps correctly on read once unlocked — the trickiest path // through unwrapCrypto's loop, since it must peel two DIFFERENT protocols' layers in // the right order. func TestWebmailReadSMIMESignAndPGPEncryptTogether(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() domainID := domains[0].ID senderID := createTestMailboxWithPassword(t, app, "pboth-sender@example.com", domainID, "sender-password-1!") recipientID := createTestMailboxWithPassword(t, app, "pboth-recip@example.com", domainID, "recip-password-1!") genIdentity(t, app, senderID, "pboth-sender@example.com") genPGPIdentity(t, app, senderID, "pboth-sender@example.com", "pgp sender passphrase") recipIdentityID := genPGPIdentity(t, app, recipientID, "pboth-recip@example.com", "pgp recipient passphrase") recipIdentity, err := app.DB.GetPGPIdentity(recipientID, recipIdentityID) if err != nil || recipIdentity == nil { t.Fatal(err) } if err := app.DB.UpsertPGPContact(senderID, "pboth-recip@example.com", "", recipIdentity.Fingerprint, recipIdentity.PublicKeyArmor); err != nil { t.Fatal(err) } recipContact, err := app.DB.GetPGPContact(senderID, "pboth-recip@example.com") if err != nil || recipContact == nil { t.Fatal(err) } senderCookie := webmailLoginSession(t, app, senderID) form := url.Values{ "to": {"pboth-recip@example.com"}, "subject": {"Sign and encrypt"}, "body_html": {"both protections applied"}, "smime_sign": {"1"}, "pgp_encrypt": {"1"}, "pgp_recipient_id": {strconv.FormatInt(recipContact.ID, 10)}, } req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(senderCookie) sendRec := httptest.NewRecorder() mux.ServeHTTP(sendRec, req) if sendRec.Code != http.StatusFound { t.Fatalf("compose send: status=%d body=%s", sendRec.Code, sendRec.Body.String()) } msgs, err := app.DB.ListMessagesInFolder(recipientID, "INBOX") if err != nil || len(msgs) != 1 { t.Fatalf("expected 1 message, got %d (err=%v)", len(msgs), err) } messageURL := MailboxPrefix + "/mail/INBOX/" + strconv.FormatInt(msgs[0].ID, 10) recipientCookie := webmailLoginSession(t, app, recipientID) unlockForm := url.Values{"identity_id": {strconv.FormatInt(recipIdentityID, 10)}, "passphrase": {"pgp recipient passphrase"}} unlockReq := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/pgp/unlock", strings.NewReader(unlockForm.Encode())) unlockReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") unlockReq.AddCookie(recipientCookie) mux.ServeHTTP(httptest.NewRecorder(), unlockReq) viewReq := httptest.NewRequest(http.MethodGet, messageURL, nil) viewReq.AddCookie(recipientCookie) viewRec := httptest.NewRecorder() mux.ServeHTTP(viewRec, viewReq) if viewRec.Code != http.StatusOK { t.Fatalf("view: status=%d body=%s", viewRec.Code, viewRec.Body.String()) } body := viewRec.Body.String() if !strings.Contains(body, "PGP encrypted & decrypted") { t.Fatalf("expected a decrypted PGP badge, got body: %s", body) } if !strings.Contains(body, "Signature verified") { t.Fatalf("expected a verified S/MIME signature badge, got body: %s", body) } if !strings.Contains(body, "both protections applied") { t.Fatal("expected the plaintext rendered after unwrapping both layers") } } // TestWebmailReadPGPEncryptedMessageNoIdentityShowsUndecryptable confirms a // recipient with no PGP key at all sees an honest "could not decrypt" badge rather // than a needs-unlock prompt (there's nothing to unlock) or a crash. func TestWebmailReadPGPEncryptedMessageNoIdentityShowsUndecryptable(t *testing.T) { app := newTestApp(t) mux := app.Mux() domains, _ := app.DB.ListDomains() domainID := domains[0].ID senderID := createTestMailboxWithPassword(t, app, "pread-enc-sender2@example.com", domainID, "sender-password-1!") recipientID := createTestMailboxWithPassword(t, app, "pread-enc-recip2@example.com", domainID, "recip-password-1!") genPGPIdentity(t, app, senderID, "pread-enc-sender2@example.com", "sender passphrase") recipIdentityID := genPGPIdentity(t, app, recipientID, "pread-enc-recip2@example.com", "recipient passphrase") recipIdentity, _ := app.DB.GetPGPIdentity(recipientID, recipIdentityID) app.DB.UpsertPGPContact(senderID, "pread-enc-recip2@example.com", "", recipIdentity.Fingerprint, recipIdentity.PublicKeyArmor) recipContact, err := app.DB.GetPGPContact(senderID, "pread-enc-recip2@example.com") if err != nil || recipContact == nil { t.Fatal(err) } senderCookie := webmailLoginSession(t, app, senderID) form := url.Values{ "to": {"pread-enc-recip2@example.com"}, "subject": {"x"}, "body_html": {"secret"}, "pgp_encrypt": {"1"}, "pgp_recipient_id": {strconv.FormatInt(recipContact.ID, 10)}, } req := httptest.NewRequest(http.MethodPost, MailboxPrefix+"/mail/compose", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(senderCookie) mux.ServeHTTP(httptest.NewRecorder(), req) msgs, _ := app.DB.ListMessagesInFolder(recipientID, "INBOX") if len(msgs) != 1 { t.Fatalf("expected 1 message, got %d", len(msgs)) } // Recipient loses their PGP key (e.g. removed it) before reading. if err := app.DB.DeletePGPIdentity(recipientID, recipIdentityID); err != nil { t.Fatal(err) } recipientCookie := webmailLoginSession(t, app, recipientID) viewReq := httptest.NewRequest(http.MethodGet, MailboxPrefix+"/mail/INBOX/"+strconv.FormatInt(msgs[0].ID, 10), nil) viewReq.AddCookie(recipientCookie) viewRec := httptest.NewRecorder() mux.ServeHTTP(viewRec, viewReq) if viewRec.Code != http.StatusOK { t.Fatalf("view: status=%d body=%s", viewRec.Code, viewRec.Body.String()) } body := viewRec.Body.String() if !strings.Contains(body, "could not decrypt") { t.Fatalf("expected an undecryptable badge, got body: %s", body) } if strings.Contains(body, "secret") { t.Fatal("plaintext should not have leaked without successful decryption") } }