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

@@ -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)