🎭 CHORUS now contains full BZZZ functionality adapted for containers Core systems ported: - P2P networking (libp2p with DHT and PubSub) - Task coordination (COOEE protocol) - HMMM collaborative reasoning - SHHH encryption and security - SLURP admin election system - UCXL content addressing - UCXI server integration - Hypercore logging system - Health monitoring and graceful shutdown - License validation with KACHING Container adaptations: - Environment variable configuration (no YAML files) - Container-optimized logging to stdout/stderr - Auto-generated agent IDs for container deployments - Docker-first architecture All proven BZZZ P2P protocols, AI integration, and collaboration features are now available in containerized form. Next: Build and test container deployment. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
550 lines
15 KiB
Go
550 lines
15 KiB
Go
package storage
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"chorus.services/bzzz/pkg/crypto"
|
|
"chorus.services/bzzz/pkg/ucxl"
|
|
slurpContext "chorus.services/bzzz/pkg/slurp/context"
|
|
)
|
|
|
|
// EncryptedStorageImpl implements the EncryptedStorage interface
|
|
type EncryptedStorageImpl struct {
|
|
mu sync.RWMutex
|
|
crypto crypto.RoleCrypto
|
|
localStorage LocalStorage
|
|
keyManager crypto.KeyManager
|
|
accessControl crypto.AccessController
|
|
auditLogger crypto.AuditLogger
|
|
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.AccessController,
|
|
auditLogger crypto.AuditLogger,
|
|
) *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
|
|
func (es *EncryptedStorageImpl) RotateKeys(
|
|
ctx context.Context,
|
|
maxAge time.Duration,
|
|
) error {
|
|
start := time.Now()
|
|
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
|
|
}
|