This comprehensive refactoring addresses critical architectural issues: IMPORT CYCLE RESOLUTION: • pkg/crypto ↔ pkg/slurp/roles: Created pkg/security/access_levels.go • pkg/ucxl → pkg/dht: Created pkg/storage/interfaces.go • pkg/slurp/leader → pkg/election → pkg/slurp/storage: Moved types to pkg/election/interfaces.go MODULE PATH MIGRATION: • Changed from github.com/anthonyrawlins/bzzz to chorus.services/bzzz • Updated all import statements across 115+ files • Maintains compatibility while removing personal GitHub account dependency TYPE SYSTEM IMPROVEMENTS: • Resolved duplicate type declarations in crypto package • Added missing type definitions (RoleStatus, TimeRestrictions, KeyStatus, KeyRotationResult) • Proper interface segregation to prevent future cycles ARCHITECTURAL BENEFITS: • Build now progresses past structural issues to normal dependency resolution • Cleaner separation of concerns between packages • Eliminates circular dependencies that prevented compilation • Establishes foundation for scalable codebase growth 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
494 lines
16 KiB
Go
494 lines
16 KiB
Go
// Package crypto provides Age encryption implementation for role-based content security in BZZZ.
|
|
//
|
|
// This package implements the cryptographic foundation for BZZZ Phase 2B, enabling:
|
|
// - Role-based content encryption using Age (https://age-encryption.org)
|
|
// - Hierarchical access control based on agent authority levels
|
|
// - Multi-recipient encryption for shared content
|
|
// - Secure key management and validation
|
|
//
|
|
// The Age encryption system ensures that UCXL content is encrypted before storage
|
|
// in the distributed DHT, with access control enforced through role-based key distribution.
|
|
//
|
|
// Architecture Overview:
|
|
// - Each role has an Age key pair (public/private)
|
|
// - Content is encrypted for specific roles based on creator's authority
|
|
// - Higher authority roles can decrypt lower authority content
|
|
// - Admin roles can decrypt all content in the system
|
|
//
|
|
// Security Model:
|
|
// - X25519 elliptic curve cryptography (Age standard)
|
|
// - Per-role key pairs for access segmentation
|
|
// - Authority hierarchy prevents privilege escalation
|
|
// - Shamir secret sharing for admin key distribution (see shamir.go)
|
|
//
|
|
// Cross-references:
|
|
// - pkg/config/roles.go: Role definitions and authority levels
|
|
// - pkg/dht/encrypted_storage.go: Encrypted DHT storage implementation
|
|
// - pkg/ucxl/decision_publisher.go: Decision publishing with encryption
|
|
// - docs/ARCHITECTURE.md: Complete system architecture
|
|
// - docs/SECURITY.md: Security model and threat analysis
|
|
package crypto
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"filippo.io/age" // Modern, secure encryption library
|
|
"filippo.io/age/agessh" // SSH key support (unused but available)
|
|
"chorus.services/bzzz/pkg/config"
|
|
)
|
|
|
|
// AgeCrypto handles Age encryption for role-based content security.
|
|
//
|
|
// This is the primary interface for encrypting and decrypting UCXL content
|
|
// based on BZZZ role hierarchies. It provides methods to:
|
|
// - Encrypt content for specific roles or multiple roles
|
|
// - Decrypt content using the current agent's role key
|
|
// - Validate Age key formats and generate new key pairs
|
|
// - Determine decryption permissions based on role authority
|
|
//
|
|
// Usage Example:
|
|
// crypto := NewAgeCrypto(config)
|
|
// encrypted, err := crypto.EncryptForRole(content, "backend_developer")
|
|
// decrypted, err := crypto.DecryptWithRole(encrypted)
|
|
//
|
|
// Thread Safety: AgeCrypto is safe for concurrent use across goroutines.
|
|
type AgeCrypto struct {
|
|
config *config.Config // BZZZ configuration containing role definitions
|
|
}
|
|
|
|
// NewAgeCrypto creates a new Age crypto handler for role-based encryption.
|
|
//
|
|
// Parameters:
|
|
// cfg: BZZZ configuration containing role definitions and agent settings
|
|
//
|
|
// Returns:
|
|
// *AgeCrypto: Configured crypto handler ready for encryption/decryption
|
|
//
|
|
// The returned AgeCrypto instance will use the role definitions from the
|
|
// provided configuration to determine encryption permissions and key access.
|
|
//
|
|
// Cross-references:
|
|
// - pkg/config/config.go: Configuration structure
|
|
// - pkg/config/roles.go: Role definitions and authority levels
|
|
func NewAgeCrypto(cfg *config.Config) *AgeCrypto {
|
|
return &AgeCrypto{
|
|
config: cfg,
|
|
}
|
|
}
|
|
|
|
// GenerateAgeKeyPair generates a new Age X25519 key pair for role-based encryption.
|
|
//
|
|
// This function creates cryptographically secure Age key pairs suitable for
|
|
// role-based content encryption. Each role in BZZZ should have its own key pair
|
|
// to enable proper access control and content segmentation.
|
|
//
|
|
// Returns:
|
|
// *config.AgeKeyPair: Structure containing both public and private keys
|
|
// error: Any error during key generation
|
|
//
|
|
// Key Format:
|
|
// - Private key: "AGE-SECRET-KEY-1..." (Age standard format)
|
|
// - Public key: "age1..." (Age recipient format)
|
|
//
|
|
// Security Notes:
|
|
// - Uses X25519 elliptic curve cryptography
|
|
// - Keys are cryptographically random using crypto/rand
|
|
// - Private keys should be stored securely and never shared
|
|
// - Public keys can be distributed freely for encryption
|
|
//
|
|
// Usage:
|
|
// keyPair, err := GenerateAgeKeyPair()
|
|
// if err != nil {
|
|
// return fmt.Errorf("key generation failed: %w", err)
|
|
// }
|
|
// // Store keyPair.PrivateKey securely
|
|
// // Distribute keyPair.PublicKey for encryption
|
|
//
|
|
// Cross-references:
|
|
// - pkg/config/roles.go: AgeKeyPair structure definition
|
|
// - docs/SECURITY.md: Key management best practices
|
|
// - pkg/crypto/shamir.go: Admin key distribution via secret sharing
|
|
func GenerateAgeKeyPair() (*config.AgeKeyPair, error) {
|
|
// Generate X25519 identity using Age's secure random generation
|
|
identity, err := age.GenerateX25519Identity()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate Age identity: %w", err)
|
|
}
|
|
|
|
// Extract public and private key strings in Age format
|
|
return &config.AgeKeyPair{
|
|
PublicKey: identity.Recipient().String(), // "age1..." format for recipients
|
|
PrivateKey: identity.String(), // "AGE-SECRET-KEY-1..." format
|
|
}, nil
|
|
}
|
|
|
|
// ParseAgeIdentity parses an Age private key string into a usable identity.
|
|
//
|
|
// This function converts a private key string (AGE-SECRET-KEY-1...) into
|
|
// an Age identity that can be used for decryption operations.
|
|
//
|
|
// Parameters:
|
|
// privateKey: Age private key string in standard format
|
|
//
|
|
// Returns:
|
|
// age.Identity: Parsed identity for decryption operations
|
|
// error: Parsing error if key format is invalid
|
|
//
|
|
// Key Format Requirements:
|
|
// - Must start with "AGE-SECRET-KEY-1"
|
|
// - Must be properly formatted X25519 private key
|
|
// - Must be base64-encoded as per Age specification
|
|
//
|
|
// Cross-references:
|
|
// - DecryptWithPrivateKey(): Uses parsed identities for decryption
|
|
// - ValidateAgeKey(): Validates key format before parsing
|
|
func ParseAgeIdentity(privateKey string) (age.Identity, error) {
|
|
return age.ParseX25519Identity(privateKey)
|
|
}
|
|
|
|
// ParseAgeRecipient parses an Age public key string into a recipient.
|
|
//
|
|
// This function converts a public key string (age1...) into an Age recipient
|
|
// that can be used for encryption operations.
|
|
//
|
|
// Parameters:
|
|
// publicKey: Age public key string in recipient format
|
|
//
|
|
// Returns:
|
|
// age.Recipient: Parsed recipient for encryption operations
|
|
// error: Parsing error if key format is invalid
|
|
//
|
|
// Key Format Requirements:
|
|
// - Must start with "age1"
|
|
// - Must be properly formatted X25519 public key
|
|
// - Must be base32-encoded as per Age specification
|
|
//
|
|
// Cross-references:
|
|
// - EncryptForRole(): Uses parsed recipients for encryption
|
|
// - ValidateAgeKey(): Validates key format before parsing
|
|
func ParseAgeRecipient(publicKey string) (age.Recipient, error) {
|
|
return age.ParseX25519Recipient(publicKey)
|
|
}
|
|
|
|
// EncryptForRole encrypts content for a specific role using Age encryption
|
|
func (ac *AgeCrypto) EncryptForRole(content []byte, roleName string) ([]byte, error) {
|
|
// Get role definition
|
|
roles := config.GetPredefinedRoles()
|
|
role, exists := roles[roleName]
|
|
if !exists {
|
|
return nil, fmt.Errorf("role '%s' not found", roleName)
|
|
}
|
|
|
|
// Check if role has Age keys configured
|
|
if role.AgeKeys.PublicKey == "" {
|
|
return nil, fmt.Errorf("role '%s' has no Age public key configured", roleName)
|
|
}
|
|
|
|
// Parse the recipient
|
|
recipient, err := ParseAgeRecipient(role.AgeKeys.PublicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse Age recipient for role '%s': %w", roleName, err)
|
|
}
|
|
|
|
// Encrypt the content
|
|
out := &bytes.Buffer{}
|
|
w, err := age.Encrypt(out, recipient)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Age encryptor: %w", err)
|
|
}
|
|
|
|
if _, err := w.Write(content); err != nil {
|
|
return nil, fmt.Errorf("failed to write content to Age encryptor: %w", err)
|
|
}
|
|
|
|
if err := w.Close(); err != nil {
|
|
return nil, fmt.Errorf("failed to close Age encryptor: %w", err)
|
|
}
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// EncryptForMultipleRoles encrypts content for multiple roles
|
|
func (ac *AgeCrypto) EncryptForMultipleRoles(content []byte, roleNames []string) ([]byte, error) {
|
|
if len(roleNames) == 0 {
|
|
return nil, fmt.Errorf("no roles specified")
|
|
}
|
|
|
|
var recipients []age.Recipient
|
|
roles := config.GetPredefinedRoles()
|
|
|
|
// Collect all recipients
|
|
for _, roleName := range roleNames {
|
|
role, exists := roles[roleName]
|
|
if !exists {
|
|
return nil, fmt.Errorf("role '%s' not found", roleName)
|
|
}
|
|
|
|
if role.AgeKeys.PublicKey == "" {
|
|
return nil, fmt.Errorf("role '%s' has no Age public key configured", roleName)
|
|
}
|
|
|
|
recipient, err := ParseAgeRecipient(role.AgeKeys.PublicKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse Age recipient for role '%s': %w", roleName, err)
|
|
}
|
|
|
|
recipients = append(recipients, recipient)
|
|
}
|
|
|
|
// Encrypt for all recipients
|
|
out := &bytes.Buffer{}
|
|
w, err := age.Encrypt(out, recipients...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Age encryptor: %w", err)
|
|
}
|
|
|
|
if _, err := w.Write(content); err != nil {
|
|
return nil, fmt.Errorf("failed to write content to Age encryptor: %w", err)
|
|
}
|
|
|
|
if err := w.Close(); err != nil {
|
|
return nil, fmt.Errorf("failed to close Age encryptor: %w", err)
|
|
}
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// DecryptWithRole decrypts content using the current agent's role key
|
|
func (ac *AgeCrypto) DecryptWithRole(encryptedContent []byte) ([]byte, error) {
|
|
if ac.config.Agent.Role == "" {
|
|
return nil, fmt.Errorf("no role configured for current agent")
|
|
}
|
|
|
|
// Get current role's private key
|
|
roles := config.GetPredefinedRoles()
|
|
role, exists := roles[ac.config.Agent.Role]
|
|
if !exists {
|
|
return nil, fmt.Errorf("current role '%s' not found", ac.config.Agent.Role)
|
|
}
|
|
|
|
if role.AgeKeys.PrivateKey == "" {
|
|
return nil, fmt.Errorf("current role '%s' has no Age private key configured", ac.config.Agent.Role)
|
|
}
|
|
|
|
return ac.DecryptWithPrivateKey(encryptedContent, role.AgeKeys.PrivateKey)
|
|
}
|
|
|
|
// DecryptWithPrivateKey decrypts content using a specific private key
|
|
func (ac *AgeCrypto) DecryptWithPrivateKey(encryptedContent []byte, privateKey string) ([]byte, error) {
|
|
// Parse the identity
|
|
identity, err := ParseAgeIdentity(privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse Age identity: %w", err)
|
|
}
|
|
|
|
// Decrypt the content
|
|
in := bytes.NewReader(encryptedContent)
|
|
r, err := age.Decrypt(in, identity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt content: %w", err)
|
|
}
|
|
|
|
out := &bytes.Buffer{}
|
|
if _, err := io.Copy(out, r); err != nil {
|
|
return nil, fmt.Errorf("failed to read decrypted content: %w", err)
|
|
}
|
|
|
|
return out.Bytes(), nil
|
|
}
|
|
|
|
// CanDecryptContent checks if current role can decrypt content encrypted for a target role
|
|
func (ac *AgeCrypto) CanDecryptContent(targetRole string) (bool, error) {
|
|
return ac.config.CanDecryptRole(targetRole)
|
|
}
|
|
|
|
// GetDecryptableRoles returns list of roles current agent can decrypt
|
|
func (ac *AgeCrypto) GetDecryptableRoles() ([]string, error) {
|
|
if ac.config.Agent.Role == "" {
|
|
return nil, fmt.Errorf("no role configured")
|
|
}
|
|
|
|
roles := config.GetPredefinedRoles()
|
|
currentRole, exists := roles[ac.config.Agent.Role]
|
|
if !exists {
|
|
return nil, fmt.Errorf("current role '%s' not found", ac.config.Agent.Role)
|
|
}
|
|
|
|
return currentRole.CanDecrypt, nil
|
|
}
|
|
|
|
// EncryptUCXLContent encrypts UCXL content based on creator's authority level
|
|
func (ac *AgeCrypto) EncryptUCXLContent(content []byte, creatorRole string) ([]byte, error) {
|
|
// Get roles that should be able to decrypt this content
|
|
decryptableRoles, err := ac.getDecryptableRolesForCreator(creatorRole)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to determine decryptable roles: %w", err)
|
|
}
|
|
|
|
// Encrypt for all decryptable roles
|
|
return ac.EncryptForMultipleRoles(content, decryptableRoles)
|
|
}
|
|
|
|
// getDecryptableRolesForCreator determines which roles should be able to decrypt content from a creator
|
|
func (ac *AgeCrypto) getDecryptableRolesForCreator(creatorRole string) ([]string, error) {
|
|
roles := config.GetPredefinedRoles()
|
|
_, exists := roles[creatorRole]
|
|
if !exists {
|
|
return nil, fmt.Errorf("creator role '%s' not found", creatorRole)
|
|
}
|
|
|
|
// Start with the creator role itself
|
|
decryptableRoles := []string{creatorRole}
|
|
|
|
// Add all roles that have higher or equal authority and can decrypt this role
|
|
for roleName, role := range roles {
|
|
// Skip the creator role (already added)
|
|
if roleName == creatorRole {
|
|
continue
|
|
}
|
|
|
|
// Check if this role can decrypt the creator's content
|
|
for _, decryptableRole := range role.CanDecrypt {
|
|
if decryptableRole == creatorRole || decryptableRole == "*" {
|
|
// Add this role to the list if not already present
|
|
if !contains(decryptableRoles, roleName) {
|
|
decryptableRoles = append(decryptableRoles, roleName)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return decryptableRoles, nil
|
|
}
|
|
|
|
// ValidateAgeKey validates an Age key format
|
|
func ValidateAgeKey(key string, isPrivate bool) error {
|
|
if key == "" {
|
|
return fmt.Errorf("key cannot be empty")
|
|
}
|
|
|
|
if isPrivate {
|
|
// Validate private key format
|
|
if !strings.HasPrefix(key, "AGE-SECRET-KEY-") {
|
|
return fmt.Errorf("invalid Age private key format")
|
|
}
|
|
|
|
// Try to parse it
|
|
_, err := ParseAgeIdentity(key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse Age private key: %w", err)
|
|
}
|
|
} else {
|
|
// Validate public key format
|
|
if !strings.HasPrefix(key, "age1") {
|
|
return fmt.Errorf("invalid Age public key format")
|
|
}
|
|
|
|
// Try to parse it
|
|
_, err := ParseAgeRecipient(key)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse Age public key: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateRoleKeys generates Age key pairs for all roles that don't have them
|
|
func GenerateRoleKeys() (map[string]*config.AgeKeyPair, error) {
|
|
roleKeys := make(map[string]*config.AgeKeyPair)
|
|
roles := config.GetPredefinedRoles()
|
|
|
|
for roleName, role := range roles {
|
|
// Skip if role already has keys
|
|
if role.AgeKeys.PublicKey != "" && role.AgeKeys.PrivateKey != "" {
|
|
continue
|
|
}
|
|
|
|
// Generate new key pair
|
|
keyPair, err := GenerateAgeKeyPair()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate keys for role '%s': %w", roleName, err)
|
|
}
|
|
|
|
roleKeys[roleName] = keyPair
|
|
}
|
|
|
|
return roleKeys, nil
|
|
}
|
|
|
|
// TestAgeEncryption tests Age encryption/decryption with sample data
|
|
func TestAgeEncryption() error {
|
|
// Generate test key pair
|
|
keyPair, err := GenerateAgeKeyPair()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate test key pair: %w", err)
|
|
}
|
|
|
|
// Test content
|
|
testContent := []byte("This is a test UCXL decision node content for Age encryption")
|
|
|
|
// Parse recipient and identity
|
|
recipient, err := ParseAgeRecipient(keyPair.PublicKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse test recipient: %w", err)
|
|
}
|
|
|
|
identity, err := ParseAgeIdentity(keyPair.PrivateKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse test identity: %w", err)
|
|
}
|
|
|
|
// Encrypt
|
|
out := &bytes.Buffer{}
|
|
w, err := age.Encrypt(out, recipient)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create test encryptor: %w", err)
|
|
}
|
|
|
|
if _, err := w.Write(testContent); err != nil {
|
|
return fmt.Errorf("failed to write test content: %w", err)
|
|
}
|
|
|
|
if err := w.Close(); err != nil {
|
|
return fmt.Errorf("failed to close test encryptor: %w", err)
|
|
}
|
|
|
|
encryptedContent := out.Bytes()
|
|
|
|
// Decrypt
|
|
in := bytes.NewReader(encryptedContent)
|
|
r, err := age.Decrypt(in, identity)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decrypt test content: %w", err)
|
|
}
|
|
|
|
decryptedBuffer := &bytes.Buffer{}
|
|
if _, err := io.Copy(decryptedBuffer, r); err != nil {
|
|
return fmt.Errorf("failed to read decrypted test content: %w", err)
|
|
}
|
|
|
|
decryptedContent := decryptedBuffer.Bytes()
|
|
|
|
// Verify
|
|
if !bytes.Equal(testContent, decryptedContent) {
|
|
return fmt.Errorf("test failed: decrypted content doesn't match original")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// contains checks if a string slice contains a value
|
|
func contains(slice []string, value string) bool {
|
|
for _, item := range slice {
|
|
if item == value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
} |