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

@@ -122,7 +122,7 @@ func (c *Client) GetMessage(messageID string, passphrase string) (*Message, erro
func (c *Client) Send(req SendRequest) error {
payload := map[string]interface{}{
"Type": "0",
"Type": MessageTypeRegular,
"Passphrase": req.Passphrase,
"Subject": req.Subject,
"HTML": req.HTML,
@@ -222,7 +222,7 @@ func (c *Client) PermanentlyDelete(messageID string) error {
func (c *Client) SaveDraft(draft Draft, passphrase string) (string, error) {
body := map[string]interface{}{
"Type": "2",
"Type": MessageTypeDraft,
"Passphrase": passphrase,
"Subject": draft.Subject,
"To": draft.To,

View File

@@ -3,14 +3,16 @@ package mail
import (
"crypto/rand"
"fmt"
"sync"
"github.com/ProtonMail/gopenpgp/v2/crypto"
)
type PGPKeyRing struct {
mu sync.Mutex
PrivateKey *crypto.Key
PublicKey []byte
PrivateKeyData string
PrivateKeyData []byte
}
type PGPService struct {
@@ -32,7 +34,7 @@ func NewPGPService(privateKeyArmored string) (*PGPService, error) {
keyRing: &PGPKeyRing{
PrivateKey: privateKey,
PublicKey: publicKey,
PrivateKeyData: privateKeyArmored,
PrivateKeyData: []byte(privateKeyArmored),
},
}, nil
}
@@ -121,7 +123,9 @@ func (s *PGPService) EncryptAndSign(plaintext string, recipientPublicKey *crypto
}
func (s *PGPService) getUnlockedKeyRing(passphrase string) (*crypto.KeyRing, error) {
key, err := crypto.NewKeyFromArmored(s.keyRing.PrivateKeyData)
s.keyRing.mu.Lock()
key, err := crypto.NewKeyFromArmored(string(s.keyRing.PrivateKeyData))
s.keyRing.mu.Unlock()
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
@@ -185,6 +189,17 @@ func (s *PGPService) GetFingerprint() (string, error) {
return fingerprint, nil
}
func (s *PGPService) ZeroPrivateKeyData() {
if s.keyRing == nil {
return
}
s.keyRing.mu.Lock()
defer s.keyRing.mu.Unlock()
for i := range s.keyRing.PrivateKeyData {
s.keyRing.PrivateKeyData[i] = 0
}
}
func (s *PGPService) SignData(data []byte, passphrase string) (string, error) {
pgpMessage := crypto.NewPlainMessage(data)

View File

@@ -12,6 +12,11 @@ const (
FolderSpam Folder = 5
)
const (
MessageTypeRegular = "0"
MessageTypeDraft = "2"
)
func (f Folder) Name() string {
names := map[Folder]string{
FolderInbox: "Inbox",
@@ -48,10 +53,10 @@ type Message struct {
}
func (m *Message) Folder() Folder {
if m.Type == 2 {
if m.Type == int(FolderDraft) {
return FolderDraft
}
if m.Type == 3 {
if m.Type == int(FolderSent) {
return FolderSent
}
return FolderInbox