// 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() }