548 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			548 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package storage
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"crypto/sha256"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"chorus/pkg/crypto"
 | |
| 	slurpContext "chorus/pkg/slurp/context"
 | |
| )
 | |
| 
 | |
| // EncryptedStorageImpl implements the EncryptedStorage interface
 | |
| type EncryptedStorageImpl struct {
 | |
| 	mu            sync.RWMutex
 | |
| 	crypto        crypto.RoleCrypto
 | |
| 	localStorage  LocalStorage
 | |
| 	keyManager    crypto.KeyManager
 | |
| 	accessControl crypto.StorageAccessController
 | |
| 	auditLogger   crypto.StorageAuditLogger
 | |
| 	metrics       *EncryptionMetrics
 | |
| }
 | |
| 
 | |
| // EncryptionMetrics tracks encryption-related metrics
 | |
| type EncryptionMetrics struct {
 | |
| 	mu                   sync.RWMutex
 | |
| 	EncryptOperations    int64
 | |
| 	DecryptOperations    int64
 | |
| 	KeyRotations         int64
 | |
| 	AccessDenials        int64
 | |
| 	EncryptionErrors     int64
 | |
| 	DecryptionErrors     int64
 | |
| 	LastKeyRotation      time.Time
 | |
| 	AverageEncryptTime   time.Duration
 | |
| 	AverageDecryptTime   time.Duration
 | |
| 	ActiveEncryptionKeys int
 | |
| 	ExpiredKeys          int
 | |
| }
 | |
| 
 | |
| // NewEncryptedStorage creates a new encrypted storage implementation
 | |
| func NewEncryptedStorage(
 | |
| 	crypto crypto.RoleCrypto,
 | |
| 	localStorage LocalStorage,
 | |
| 	keyManager crypto.KeyManager,
 | |
| 	accessControl crypto.StorageAccessController,
 | |
| 	auditLogger crypto.StorageAuditLogger,
 | |
| ) *EncryptedStorageImpl {
 | |
| 	return &EncryptedStorageImpl{
 | |
| 		crypto:        crypto,
 | |
| 		localStorage:  localStorage,
 | |
| 		keyManager:    keyManager,
 | |
| 		accessControl: accessControl,
 | |
| 		auditLogger:   auditLogger,
 | |
| 		metrics:       &EncryptionMetrics{},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // StoreEncrypted stores data encrypted for specific roles
 | |
| func (es *EncryptedStorageImpl) StoreEncrypted(
 | |
| 	ctx context.Context,
 | |
| 	key string,
 | |
| 	data interface{},
 | |
| 	roles []string,
 | |
| ) error {
 | |
| 	start := time.Now()
 | |
| 	defer func() {
 | |
| 		es.metrics.mu.Lock()
 | |
| 		es.metrics.EncryptOperations++
 | |
| 		es.updateAverageTime(&es.metrics.AverageEncryptTime, time.Since(start))
 | |
| 		es.metrics.mu.Unlock()
 | |
| 	}()
 | |
| 
 | |
| 	// Serialize the data
 | |
| 	dataBytes, err := json.Marshal(data)
 | |
| 	if err != nil {
 | |
| 		es.recordError("encryption", err)
 | |
| 		return fmt.Errorf("failed to marshal data: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Create encrypted context entries for each role
 | |
| 	for _, role := range roles {
 | |
| 		// Check if role has permission to store data
 | |
| 		if !es.accessControl.CanStore(role, key) {
 | |
| 			es.recordAccessDenial(role, key, "store")
 | |
| 			continue // Skip this role but don't fail the entire operation
 | |
| 		}
 | |
| 
 | |
| 		// Create role-specific key
 | |
| 		roleKey := es.generateRoleKey(key, role)
 | |
| 
 | |
| 		// Encrypt data for this role
 | |
| 		encryptedData, keyFingerprint, err := es.crypto.EncryptForRole(dataBytes, role)
 | |
| 		if err != nil {
 | |
| 			es.recordError("encryption", err)
 | |
| 			return fmt.Errorf("failed to encrypt data for role %s: %w", role, err)
 | |
| 		}
 | |
| 
 | |
| 		// Create encrypted context wrapper
 | |
| 		encCtx := &slurpContext.EncryptedContext{
 | |
| 			Role:           role,
 | |
| 			AccessLevel:    es.determineAccessLevel(role),
 | |
| 			EncryptedData:  encryptedData,
 | |
| 			KeyFingerprint: keyFingerprint,
 | |
| 			CreatedAt:      time.Now(),
 | |
| 		}
 | |
| 
 | |
| 		// Store the encrypted context
 | |
| 		storeOptions := &StoreOptions{
 | |
| 			Encrypt:     false, // Already encrypted
 | |
| 			Replicate:   true,
 | |
| 			Index:       true,
 | |
| 			Cache:       true,
 | |
| 			AccessLevel: crypto.AccessLevel(encCtx.AccessLevel),
 | |
| 			Metadata: map[string]interface{}{
 | |
| 				"role":            role,
 | |
| 				"key_fingerprint": keyFingerprint,
 | |
| 				"encrypted":       true,
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		if err := es.localStorage.Store(ctx, roleKey, encCtx, storeOptions); err != nil {
 | |
| 			es.recordError("storage", err)
 | |
| 			return fmt.Errorf("failed to store encrypted data for role %s: %w", role, err)
 | |
| 		}
 | |
| 
 | |
| 		// Audit log the operation
 | |
| 		es.auditLogger.LogEncryptionOperation(role, key, "store", true)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // RetrieveDecrypted retrieves and decrypts data for current role
 | |
| func (es *EncryptedStorageImpl) RetrieveDecrypted(
 | |
| 	ctx context.Context,
 | |
| 	key string,
 | |
| 	role string,
 | |
| ) (interface{}, error) {
 | |
| 	start := time.Now()
 | |
| 	defer func() {
 | |
| 		es.metrics.mu.Lock()
 | |
| 		es.metrics.DecryptOperations++
 | |
| 		es.updateAverageTime(&es.metrics.AverageDecryptTime, time.Since(start))
 | |
| 		es.metrics.mu.Unlock()
 | |
| 	}()
 | |
| 
 | |
| 	// Check access permissions
 | |
| 	if !es.accessControl.CanRetrieve(role, key) {
 | |
| 		es.recordAccessDenial(role, key, "retrieve")
 | |
| 		return nil, fmt.Errorf("role %s does not have permission to retrieve key %s", role, key)
 | |
| 	}
 | |
| 
 | |
| 	// Generate role-specific key
 | |
| 	roleKey := es.generateRoleKey(key, role)
 | |
| 
 | |
| 	// Retrieve encrypted context
 | |
| 	encryptedData, err := es.localStorage.Retrieve(ctx, roleKey)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to retrieve encrypted data: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Cast to encrypted context
 | |
| 	encCtx, ok := encryptedData.(*slurpContext.EncryptedContext)
 | |
| 	if !ok {
 | |
| 		return nil, fmt.Errorf("invalid encrypted context type")
 | |
| 	}
 | |
| 
 | |
| 	// Verify role matches
 | |
| 	if encCtx.Role != role {
 | |
| 		es.recordAccessDenial(role, key, "role_mismatch")
 | |
| 		return nil, fmt.Errorf("role mismatch: expected %s, got %s", role, encCtx.Role)
 | |
| 	}
 | |
| 
 | |
| 	// Decrypt the data
 | |
| 	decryptedData, err := es.crypto.DecryptForRole(encCtx.EncryptedData, role, encCtx.KeyFingerprint)
 | |
| 	if err != nil {
 | |
| 		es.recordError("decryption", err)
 | |
| 		return nil, fmt.Errorf("failed to decrypt data for role %s: %w", role, err)
 | |
| 	}
 | |
| 
 | |
| 	// Deserialize the data
 | |
| 	var result interface{}
 | |
| 	if err := json.Unmarshal(decryptedData, &result); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to unmarshal decrypted data: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Audit log the operation
 | |
| 	es.auditLogger.LogDecryptionOperation(role, key, "retrieve", true)
 | |
| 
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // CanAccess checks if a role can access specific data
 | |
| func (es *EncryptedStorageImpl) CanAccess(
 | |
| 	ctx context.Context,
 | |
| 	key string,
 | |
| 	role string,
 | |
| ) (bool, error) {
 | |
| 	// Check access control rules
 | |
| 	if !es.accessControl.CanRetrieve(role, key) {
 | |
| 		return false, nil
 | |
| 	}
 | |
| 
 | |
| 	// Check if encrypted data exists for this role
 | |
| 	roleKey := es.generateRoleKey(key, role)
 | |
| 	exists, err := es.localStorage.Exists(ctx, roleKey)
 | |
| 	if err != nil {
 | |
| 		return false, fmt.Errorf("failed to check existence: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return exists, nil
 | |
| }
 | |
| 
 | |
| // ListAccessibleKeys lists keys accessible to a role
 | |
| func (es *EncryptedStorageImpl) ListAccessibleKeys(
 | |
| 	ctx context.Context,
 | |
| 	role string,
 | |
| ) ([]string, error) {
 | |
| 	// List all keys with role prefix
 | |
| 	rolePattern := fmt.Sprintf("role:%s:*", role)
 | |
| 	allKeys, err := es.localStorage.List(ctx, rolePattern)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to list keys for role %s: %w", role, err)
 | |
| 	}
 | |
| 
 | |
| 	// Extract original keys from role-prefixed keys
 | |
| 	var accessibleKeys []string
 | |
| 	for _, roleKey := range allKeys {
 | |
| 		originalKey := es.extractOriginalKey(roleKey, role)
 | |
| 		if originalKey != "" && es.accessControl.CanRetrieve(role, originalKey) {
 | |
| 			accessibleKeys = append(accessibleKeys, originalKey)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return accessibleKeys, nil
 | |
| }
 | |
| 
 | |
| // ReEncryptForRoles re-encrypts data for different roles
 | |
| func (es *EncryptedStorageImpl) ReEncryptForRoles(
 | |
| 	ctx context.Context,
 | |
| 	key string,
 | |
| 	newRoles []string,
 | |
| ) error {
 | |
| 	// This requires admin privileges or special re-encryption role
 | |
| 	adminRole := "admin" // TODO: Make this configurable
 | |
| 
 | |
| 	// Retrieve original data using admin role
 | |
| 	originalData, err := es.RetrieveDecrypted(ctx, key, adminRole)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to retrieve original data for re-encryption: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Delete old encrypted versions
 | |
| 	if err := es.deleteAllRoleVersions(ctx, key); err != nil {
 | |
| 		return fmt.Errorf("failed to delete old encrypted versions: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Store with new roles
 | |
| 	return es.StoreEncrypted(ctx, key, originalData, newRoles)
 | |
| }
 | |
| 
 | |
| // GetAccessRoles gets roles that can access specific data
 | |
| func (es *EncryptedStorageImpl) GetAccessRoles(
 | |
| 	ctx context.Context,
 | |
| 	key string,
 | |
| ) ([]string, error) {
 | |
| 	// List all role-prefixed versions of this key
 | |
| 	pattern := fmt.Sprintf("role:*:%s", es.encodeKey(key))
 | |
| 	roleKeys, err := es.localStorage.List(ctx, pattern)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to list role keys: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Extract roles from keys
 | |
| 	var roles []string
 | |
| 	for _, roleKey := range roleKeys {
 | |
| 		role := es.extractRoleFromKey(roleKey)
 | |
| 		if role != "" {
 | |
| 			roles = append(roles, role)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return roles, nil
 | |
| }
 | |
| 
 | |
| // RotateKeys rotates encryption keys in line with SEC-SLURP-1.1 retention constraints
 | |
| func (es *EncryptedStorageImpl) RotateKeys(
 | |
| 	ctx context.Context,
 | |
| 	maxAge time.Duration,
 | |
| ) error {
 | |
| 	defer func() {
 | |
| 		es.metrics.mu.Lock()
 | |
| 		es.metrics.KeyRotations++
 | |
| 		es.metrics.LastKeyRotation = time.Now()
 | |
| 		es.metrics.mu.Unlock()
 | |
| 	}()
 | |
| 
 | |
| 	// Get keys that need rotation
 | |
| 	keysToRotate, err := es.keyManager.GetKeysForRotation(maxAge)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get keys for rotation: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	for _, keyInfo := range keysToRotate {
 | |
| 		// Rotate the key
 | |
| 		if err := es.rotateKey(ctx, keyInfo); err != nil {
 | |
| 			es.recordError("key_rotation", err)
 | |
| 			// Log but continue with other keys
 | |
| 			es.auditLogger.LogKeyRotation(keyInfo.Role, keyInfo.KeyID, false, err.Error())
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		es.auditLogger.LogKeyRotation(keyInfo.Role, keyInfo.KeyID, true, "")
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ValidateEncryption validates encryption integrity
 | |
| func (es *EncryptedStorageImpl) ValidateEncryption(
 | |
| 	ctx context.Context,
 | |
| 	key string,
 | |
| ) error {
 | |
| 	// Get all role versions of this key
 | |
| 	roles, err := es.GetAccessRoles(ctx, key)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get access roles: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Validate each encrypted version
 | |
| 	for _, role := range roles {
 | |
| 		roleKey := es.generateRoleKey(key, role)
 | |
| 
 | |
| 		// Retrieve encrypted context
 | |
| 		encryptedData, err := es.localStorage.Retrieve(ctx, roleKey)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to retrieve encrypted data for role %s: %w", role, err)
 | |
| 		}
 | |
| 
 | |
| 		encCtx, ok := encryptedData.(*slurpContext.EncryptedContext)
 | |
| 		if !ok {
 | |
| 			return fmt.Errorf("invalid encrypted context type for role %s", role)
 | |
| 		}
 | |
| 
 | |
| 		// Validate key fingerprint
 | |
| 		if !es.keyManager.ValidateKeyFingerprint(role, encCtx.KeyFingerprint) {
 | |
| 			return fmt.Errorf("invalid key fingerprint for role %s", role)
 | |
| 		}
 | |
| 
 | |
| 		// Try to decrypt (validates encryption integrity)
 | |
| 		_, err = es.crypto.DecryptForRole(encCtx.EncryptedData, role, encCtx.KeyFingerprint)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("encryption validation failed for role %s: %w", role, err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Helper methods
 | |
| 
 | |
| func (es *EncryptedStorageImpl) generateRoleKey(key, role string) string {
 | |
| 	return fmt.Sprintf("role:%s:%s", role, es.encodeKey(key))
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) encodeKey(key string) string {
 | |
| 	// Use SHA-256 to create a consistent key encoding
 | |
| 	hash := sha256.Sum256([]byte(key))
 | |
| 	return fmt.Sprintf("%x", hash)
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) extractOriginalKey(roleKey, role string) string {
 | |
| 	prefix := fmt.Sprintf("role:%s:", role)
 | |
| 	if len(roleKey) > len(prefix) && roleKey[:len(prefix)] == prefix {
 | |
| 		return roleKey[len(prefix):]
 | |
| 	}
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) extractRoleFromKey(roleKey string) string {
 | |
| 	// Extract role from "role:ROLE:KEY" format
 | |
| 	if len(roleKey) > 5 && roleKey[:5] == "role:" {
 | |
| 		parts := roleKey[5:] // Remove "role:" prefix
 | |
| 		if idx := len(parts); idx > 0 {
 | |
| 			for i, c := range parts {
 | |
| 				if c == ':' {
 | |
| 					return parts[:i]
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) determineAccessLevel(role string) slurpContext.RoleAccessLevel {
 | |
| 	// Map roles to access levels - this should be configurable
 | |
| 	switch role {
 | |
| 	case "admin", "architect":
 | |
| 		return slurpContext.AccessCritical
 | |
| 	case "senior_developer", "lead":
 | |
| 		return slurpContext.AccessHigh
 | |
| 	case "developer":
 | |
| 		return slurpContext.AccessMedium
 | |
| 	case "junior_developer", "intern":
 | |
| 		return slurpContext.AccessLow
 | |
| 	default:
 | |
| 		return slurpContext.AccessPublic
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) deleteAllRoleVersions(ctx context.Context, key string) error {
 | |
| 	// Get all roles that have access to this key
 | |
| 	roles, err := es.GetAccessRoles(ctx, key)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// Delete each role version
 | |
| 	for _, role := range roles {
 | |
| 		roleKey := es.generateRoleKey(key, role)
 | |
| 		if err := es.localStorage.Delete(ctx, roleKey); err != nil {
 | |
| 			// Log but don't fail - may not exist
 | |
| 			es.auditLogger.LogError(fmt.Sprintf("Failed to delete role key %s: %v", roleKey, err))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) rotateKey(ctx context.Context, keyInfo *crypto.KeyInfo) error {
 | |
| 	// Generate new key for role
 | |
| 	newKeyID, err := es.keyManager.GenerateKey(keyInfo.Role)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to generate new key: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Re-encrypt all data for this role with new key
 | |
| 	rolePattern := fmt.Sprintf("role:%s:*", keyInfo.Role)
 | |
| 	roleKeys, err := es.localStorage.List(ctx, rolePattern)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to list keys for role %s: %w", keyInfo.Role, err)
 | |
| 	}
 | |
| 
 | |
| 	for _, roleKey := range roleKeys {
 | |
| 		if err := es.reEncryptWithNewKey(ctx, roleKey, keyInfo.Role, newKeyID); err != nil {
 | |
| 			// Log but continue
 | |
| 			es.auditLogger.LogError(fmt.Sprintf("Failed to re-encrypt key %s: %v", roleKey, err))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Mark old key as deprecated
 | |
| 	return es.keyManager.DeprecateKey(keyInfo.KeyID)
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) reEncryptWithNewKey(
 | |
| 	ctx context.Context,
 | |
| 	roleKey string,
 | |
| 	role string,
 | |
| 	newKeyID string,
 | |
| ) error {
 | |
| 	// Retrieve and decrypt with old key
 | |
| 	encryptedData, err := es.localStorage.Retrieve(ctx, roleKey)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	encCtx, ok := encryptedData.(*slurpContext.EncryptedContext)
 | |
| 	if !ok {
 | |
| 		return fmt.Errorf("invalid encrypted context type")
 | |
| 	}
 | |
| 
 | |
| 	// Decrypt with old key
 | |
| 	decryptedData, err := es.crypto.DecryptForRole(encCtx.EncryptedData, role, encCtx.KeyFingerprint)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to decrypt with old key: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Encrypt with new key
 | |
| 	newEncryptedData, newKeyFingerprint, err := es.crypto.EncryptForRole(decryptedData, role)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to encrypt with new key: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// Update encrypted context
 | |
| 	encCtx.EncryptedData = newEncryptedData
 | |
| 	encCtx.KeyFingerprint = newKeyFingerprint
 | |
| 	encCtx.CreatedAt = time.Now()
 | |
| 
 | |
| 	// Store updated context
 | |
| 	storeOptions := &StoreOptions{
 | |
| 		Encrypt:   false, // Already encrypted
 | |
| 		Replicate: true,
 | |
| 		Index:     true,
 | |
| 		Cache:     true,
 | |
| 		Metadata: map[string]interface{}{
 | |
| 			"role":            role,
 | |
| 			"key_fingerprint": newKeyFingerprint,
 | |
| 			"encrypted":       true,
 | |
| 			"re_encrypted":    true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	return es.localStorage.Store(ctx, roleKey, encCtx, storeOptions)
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) recordError(operation string, err error) {
 | |
| 	es.metrics.mu.Lock()
 | |
| 	defer es.metrics.mu.Unlock()
 | |
| 
 | |
| 	switch operation {
 | |
| 	case "encryption":
 | |
| 		es.metrics.EncryptionErrors++
 | |
| 	case "decryption":
 | |
| 		es.metrics.DecryptionErrors++
 | |
| 	}
 | |
| 
 | |
| 	es.auditLogger.LogError(fmt.Sprintf("%s error: %v", operation, err))
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) recordAccessDenial(role, key, operation string) {
 | |
| 	es.metrics.mu.Lock()
 | |
| 	es.metrics.AccessDenials++
 | |
| 	es.metrics.mu.Unlock()
 | |
| 
 | |
| 	es.auditLogger.LogAccessDenial(role, key, operation)
 | |
| }
 | |
| 
 | |
| func (es *EncryptedStorageImpl) updateAverageTime(currentAvg *time.Duration, newTime time.Duration) {
 | |
| 	// Simple exponential moving average
 | |
| 	if *currentAvg == 0 {
 | |
| 		*currentAvg = newTime
 | |
| 	} else {
 | |
| 		*currentAvg = time.Duration(float64(*currentAvg)*0.8 + float64(newTime)*0.2)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // GetEncryptionMetrics returns encryption metrics
 | |
| func (es *EncryptedStorageImpl) GetEncryptionMetrics() *EncryptionMetrics {
 | |
| 	es.metrics.mu.RLock()
 | |
| 	defer es.metrics.mu.RUnlock()
 | |
| 
 | |
| 	// Return a copy to avoid race conditions
 | |
| 	metricsCopy := *es.metrics
 | |
| 	return &metricsCopy
 | |
| }
 | 
