FRE-681: Fix security review findings (3 HIGH, 3 MEDIUM, 2 LOW)

HIGH fixes:
- Access Token now used as PGP Passphrase: replaced session.AccessToken
  with session.MailPassphrase for all PGP operations
- Session stored encrypted in keyring and file (was plain JSON)
- Added checkAuthenticated() helper with IsAuthenticated() guard

MEDIUM fixes:
- Added MailPassphrase field to Session, collected during login
- Added email validation in LoginInteractive
- Added keyring cleanup on Logout
- Implemented RefreshToken with actual API call

LOW fixes:
- Added mutex to PGPKeyRing for thread safety
- Added ZeroPrivateKeyData() for memory cleanup
- Use net/mail.ParseAddress for proper recipient parsing
- Renamed internal/mail import to internalmail to avoid conflict
This commit is contained in:
Paperclip
2026-04-28 12:36:27 -04:00
committed by Michael Freno
parent e499d16b7c
commit 0684e726bb
6 changed files with 232 additions and 153 deletions

View File

@@ -6,9 +6,8 @@ import (
"strconv"
"github.com/frenocorp/pop/internal/api"
"github.com/frenocorp/pop/internal/auth"
"github.com/frenocorp/pop/internal/config"
"github.com/frenocorp/pop/internal/mail"
internalmail "github.com/frenocorp/pop/internal/mail"
"github.com/spf13/cobra"
)
@@ -49,12 +48,12 @@ func draftSaveCmd() *cobra.Command {
}
recipients := parseRecipients(to)
var ccRecipients []mail.Recipient
var ccRecipients []internalmail.Recipient
if cc != "" {
ccRecipients = parseRecipients(cc)
}
var bccRecipients []mail.Recipient
var bccRecipients []internalmail.Recipient
if bcc != "" {
bccRecipients = parseRecipients(bcc)
}
@@ -65,20 +64,16 @@ func draftSaveCmd() *cobra.Command {
return fmt.Errorf("failed to load config: %w", err)
}
sessionMgr, err := auth.NewSessionManager()
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}
session, err := sessionMgr.GetSession()
session, err := checkAuthenticated()
if err != nil {
return fmt.Errorf("not authenticated: %w", err)
}
client := api.NewProtonMailClient(cfg)
client.SetAuthHeader(session.AccessToken)
mailClient := mail.NewClient(client)
mailClient := internalmail.NewClient(client)
draft := mail.Draft{
draft := internalmail.Draft{
To: recipients,
CC: ccRecipients,
BCC: bccRecipients,
@@ -86,7 +81,7 @@ func draftSaveCmd() *cobra.Command {
Body: msgBody,
}
messageID, err := mailClient.SaveDraft(draft, session.AccessToken)
messageID, err := mailClient.SaveDraft(draft, session.MailPassphrase)
if err != nil {
return fmt.Errorf("failed to save draft: %w", err)
}
@@ -130,20 +125,16 @@ func draftListCmd() *cobra.Command {
return fmt.Errorf("failed to load config: %w", err)
}
sessionMgr, err := auth.NewSessionManager()
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}
session, err := sessionMgr.GetSession()
session, err := checkAuthenticated()
if err != nil {
return fmt.Errorf("not authenticated: %w", err)
}
client := api.NewProtonMailClient(cfg)
client.SetAuthHeader(session.AccessToken)
mailClient := mail.NewClient(client)
mailClient := internalmail.NewClient(client)
result, err := mailClient.ListDrafts(pageVal, pageSizeVal, session.AccessToken)
result, err := mailClient.ListDrafts(pageVal, pageSizeVal, session.MailPassphrase)
if err != nil {
return fmt.Errorf("failed to list drafts: %w", err)
}
@@ -159,7 +150,7 @@ func draftListCmd() *cobra.Command {
}
func draftEditCmd() *cobra.Command {
var to, cc, subject, bodyFile, body string
var to, cc, bcc, subject, bodyFile, body string
cmd := &cobra.Command{
Use: "edit <draft-id>",
@@ -169,16 +160,21 @@ func draftEditCmd() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
messageID := args[0]
var recipients []mail.Recipient
var recipients []internalmail.Recipient
if to != "" {
recipients = parseRecipients(to)
}
var ccRecipients []mail.Recipient
var ccRecipients []internalmail.Recipient
if cc != "" {
ccRecipients = parseRecipients(cc)
}
var bccRecipients []internalmail.Recipient
if bcc != "" {
bccRecipients = parseRecipients(bcc)
}
msgBody := body
if bodyFile != "" {
data, err := os.ReadFile(bodyFile)
@@ -194,27 +190,24 @@ func draftEditCmd() *cobra.Command {
return fmt.Errorf("failed to load config: %w", err)
}
sessionMgr, err := auth.NewSessionManager()
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}
session, err := sessionMgr.GetSession()
session, err := checkAuthenticated()
if err != nil {
return fmt.Errorf("not authenticated: %w", err)
}
client := api.NewProtonMailClient(cfg)
client.SetAuthHeader(session.AccessToken)
mailClient := mail.NewClient(client)
mailClient := internalmail.NewClient(client)
draft := mail.Draft{
draft := internalmail.Draft{
To: recipients,
CC: ccRecipients,
BCC: bccRecipients,
Subject: subject,
Body: msgBody,
}
if err := mailClient.UpdateDraft(messageID, draft, session.AccessToken); err != nil {
if err := mailClient.UpdateDraft(messageID, draft, session.MailPassphrase); err != nil {
return fmt.Errorf("failed to update draft: %w", err)
}
@@ -225,6 +218,7 @@ func draftEditCmd() *cobra.Command {
cmd.Flags().StringVarP(&to, "to", "t", "", "New recipient addresses (comma-separated)")
cmd.Flags().StringVarP(&cc, "cc", "c", "", "New CC addresses (comma-separated)")
cmd.Flags().StringVarP(&bcc, "bcc", "b", "", "New BCC addresses (comma-separated)")
cmd.Flags().StringVarP(&subject, "subject", "s", "", "New draft subject")
cmd.Flags().StringVarP(&bodyFile, "body-file", "f", "", "File containing new draft body")
cmd.Flags().StringVar(&body, "body", "", "New inline draft body")
@@ -247,20 +241,16 @@ func draftSendCmd() *cobra.Command {
return fmt.Errorf("failed to load config: %w", err)
}
sessionMgr, err := auth.NewSessionManager()
if err != nil {
return fmt.Errorf("failed to create session manager: %w", err)
}
session, err := sessionMgr.GetSession()
session, err := checkAuthenticated()
if err != nil {
return fmt.Errorf("not authenticated: %w", err)
}
client := api.NewProtonMailClient(cfg)
client.SetAuthHeader(session.AccessToken)
mailClient := mail.NewClient(client)
mailClient := internalmail.NewClient(client)
if err := mailClient.SendDraft(messageID, session.AccessToken); err != nil {
if err := mailClient.SendDraft(messageID, session.MailPassphrase); err != nil {
return fmt.Errorf("failed to send draft: %w", err)
}