monorepo/native/desktop/maplefile/internal/service/keycache/keycache.go

181 lines
5 KiB
Go

// Package keycache provides secure in-memory caching of cryptographic keys during a session.
// Keys are stored in memguard Enclaves (encrypted at rest in memory) and automatically
// cleared when the application shuts down or the user logs out.
package keycache
import (
"fmt"
"sync"
"github.com/awnumar/memguard"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/utils"
)
// Service manages cached cryptographic keys in secure memory
type Service struct {
logger *zap.Logger
mu sync.RWMutex
// Map of email -> Enclave containing master key
// Enclave stores data encrypted in memory, must be opened to access
masterKeys map[string]*memguard.Enclave
}
// ProvideService creates a new key cache service (for Wire)
func ProvideService(logger *zap.Logger) *Service {
return &Service{
logger: logger.Named("keycache"),
masterKeys: make(map[string]*memguard.Enclave),
}
}
// StoreMasterKey stores a user's master key in an encrypted memory Enclave
// The key will remain cached until cleared or the app exits
func (s *Service) StoreMasterKey(email string, masterKey []byte) error {
if email == "" {
return fmt.Errorf("email is required")
}
if len(masterKey) == 0 {
return fmt.Errorf("master key is empty")
}
s.mu.Lock()
defer s.mu.Unlock()
// If there's already a cached key, remove it first
if existing, exists := s.masterKeys[email]; exists {
// Enclaves are garbage collected when removed from map
delete(s.masterKeys, email)
s.logger.Debug("Replaced existing cached master key", zap.String("email", utils.MaskEmail(email)))
_ = existing // Prevent unused variable warning
}
// Create a LockedBuffer from the master key bytes first
// This locks the memory pages and prevents swapping
lockedBuf := memguard.NewBufferFromBytes(masterKey)
// Create an Enclave from the LockedBuffer
// Enclave stores the data encrypted at rest in memory
enclave := lockedBuf.Seal()
// The LockedBuffer is consumed by Seal(), so we don't need to Destroy() it
// Store the enclave
s.masterKeys[email] = enclave
s.logger.Info("Master key cached securely in memory",
zap.String("email", utils.MaskEmail(email)),
zap.Int("size", len(masterKey)))
return nil
}
// GetMasterKey retrieves a cached master key for the given email
// Returns the key bytes and a cleanup function that MUST be called when done
// The cleanup function destroys the LockedBuffer to prevent memory leaks
func (s *Service) GetMasterKey(email string) ([]byte, func(), error) {
if email == "" {
return nil, nil, fmt.Errorf("email is required")
}
s.mu.RLock()
enclave, exists := s.masterKeys[email]
s.mu.RUnlock()
if !exists {
return nil, nil, fmt.Errorf("no cached master key found for email: %s", email)
}
// Open the enclave to access the master key
lockedBuf, err := enclave.Open()
if err != nil {
return nil, nil, fmt.Errorf("failed to open enclave for reading: %w", err)
}
// Get the bytes (caller will use these)
masterKey := lockedBuf.Bytes()
// Return cleanup function that destroys the LockedBuffer
cleanup := func() {
lockedBuf.Destroy()
}
s.logger.Debug("Retrieved cached master key from secure memory",
zap.String("email", utils.MaskEmail(email)))
return masterKey, cleanup, nil
}
// WithMasterKey provides a callback pattern for using a cached master key
// The key is automatically cleaned up after the callback returns
func (s *Service) WithMasterKey(email string, fn func([]byte) error) error {
masterKey, cleanup, err := s.GetMasterKey(email)
if err != nil {
return err
}
defer cleanup()
return fn(masterKey)
}
// HasMasterKey checks if a master key is cached for the given email
func (s *Service) HasMasterKey(email string) bool {
if email == "" {
return false
}
s.mu.RLock()
defer s.mu.RUnlock()
_, exists := s.masterKeys[email]
return exists
}
// ClearMasterKey removes a cached master key for a specific user
func (s *Service) ClearMasterKey(email string) error {
if email == "" {
return fmt.Errorf("email is required")
}
s.mu.Lock()
defer s.mu.Unlock()
if enclave, exists := s.masterKeys[email]; exists {
delete(s.masterKeys, email)
s.logger.Info("Cleared cached master key from secure memory",
zap.String("email", utils.MaskEmail(email)))
_ = enclave // Enclave will be garbage collected
return nil
}
return fmt.Errorf("no cached master key found for email: %s", email)
}
// ClearAll removes all cached master keys
// This should be called on logout or application shutdown
func (s *Service) ClearAll() {
s.mu.Lock()
defer s.mu.Unlock()
count := len(s.masterKeys)
if count == 0 {
return
}
// Clear all enclaves
for email := range s.masterKeys {
delete(s.masterKeys, email)
}
s.logger.Info("Cleared all cached master keys from secure memory",
zap.Int("count", count))
}
// Cleanup performs cleanup operations when the service is shutting down
// This is called by the application shutdown handler
func (s *Service) Cleanup() {
s.logger.Info("Cleaning up key cache service")
s.ClearAll()
}