// Package engine defines the Engine interface and mount registry. // Phase 1: interface and registry only, no concrete implementations. package engine import ( "context" "encoding/json" "errors" "fmt" "log/slog" "regexp" "strings" "sync" "git.wntrmute.dev/mc/metacrypt/internal/barrier" ) // EngineType identifies a cryptographic engine type. type EngineType string const ( EngineTypeCA EngineType = "ca" EngineTypeSSHCA EngineType = "sshca" EngineTypeTransit EngineType = "transit" EngineTypeUser EngineType = "user" ) var ( ErrMountExists = errors.New("engine: mount already exists") ErrMountNotFound = errors.New("engine: mount not found") ErrUnknownType = errors.New("engine: unknown engine type") ErrInvalidName = errors.New("engine: invalid name") ) var validName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) // ValidateName checks that a user-provided name is safe for use in barrier // paths. Names must be 1-128 characters, start with an alphanumeric, and // contain only alphanumerics, dots, hyphens, and underscores. func ValidateName(name string) error { if name == "" || len(name) > 128 || !validName.MatchString(name) { return fmt.Errorf("%w: %q", ErrInvalidName, name) } return nil } // CallerInfo carries authentication context into engines. type CallerInfo struct { Username string AccountType string // "human" or "system" Roles []string IsAdmin bool } // IsUser returns true if the caller is authorized to perform user-level // operations. Admins, system (service) accounts, and humans with the // "user" role all qualify. Authenticated callers with no roles are treated // as service accounts (MCIAS issues service tokens with nil roles). func (c *CallerInfo) IsUser() bool { if c.IsAdmin { return true } if c.AccountType == "system" { return true } for _, r := range c.Roles { if r == "user" { return true } if r == "guest" { return false } } // Authenticated caller with no roles — service account. if c.Username != "" && len(c.Roles) == 0 { return true } return false } // PolicyChecker evaluates whether the caller has access to a specific resource. // Returns the policy effect ("allow" or "deny") and whether a matching rule was found. // When matched is false, the caller should fall back to default access rules. type PolicyChecker func(resource, action string) (effect string, matched bool) // Request is a request to an engine. type Request struct { Data map[string]interface{} CallerInfo *CallerInfo CheckPolicy PolicyChecker Operation string Path string } // Response is a response from an engine. type Response struct { Data map[string]interface{} } // Engine is the interface that all cryptographic engines must implement. type Engine interface { // Type returns the engine type. Type() EngineType // Initialize sets up the engine for first use. Initialize(ctx context.Context, b barrier.Barrier, mountPath string, config map[string]interface{}) error // Unseal opens the engine using state from the barrier. Unseal(ctx context.Context, b barrier.Barrier, mountPath string) error // Seal closes the engine and zeroizes key material. Seal() error // HandleRequest processes a request. HandleRequest(ctx context.Context, req *Request) (*Response, error) } // Factory creates a new engine instance of a given type. type Factory func() Engine // Mount represents a mounted engine instance. type Mount struct { Engine Engine `json:"-"` Name string `json:"name"` Type EngineType `json:"type"` MountPath string `json:"mount_path"` } // Registry manages mounted engine instances. type Registry struct { barrier barrier.Barrier mounts map[string]*Mount factories map[EngineType]Factory logger *slog.Logger mu sync.RWMutex } // NewRegistry creates a new engine registry. func NewRegistry(b barrier.Barrier, logger *slog.Logger) *Registry { return &Registry{ mounts: make(map[string]*Mount), factories: make(map[EngineType]Factory), barrier: b, logger: logger, } } // RegisterFactory registers a factory for the given engine type. func (r *Registry) RegisterFactory(t EngineType, f Factory) { r.mu.Lock() defer r.mu.Unlock() r.logger.Debug("registering engine factory", "type", t) r.factories[t] = f } // mountMeta is persisted to the barrier so mounts survive seal/unseal cycles. type mountMeta struct { Name string `json:"name"` Type EngineType `json:"type"` } const mountsPrefix = "engine/_mounts/" // Mount creates and initializes a new engine mount. func (r *Registry) Mount(ctx context.Context, name string, engineType EngineType, config map[string]interface{}) error { if err := ValidateName(name); err != nil { return err } r.mu.Lock() defer r.mu.Unlock() if _, exists := r.mounts[name]; exists { return ErrMountExists } factory, ok := r.factories[engineType] if !ok { return fmt.Errorf("%w: %s", ErrUnknownType, engineType) } r.logger.Debug("mounting engine", "name", name, "type", engineType) eng := factory() mountPath := fmt.Sprintf("engine/%s/%s/", engineType, name) // Create a per-engine DEK in the barrier for this mount. if aesBarrier, ok := r.barrier.(*barrier.AESGCMBarrier); ok { dekKeyID := fmt.Sprintf("engine/%s/%s", engineType, name) if err := aesBarrier.CreateKey(ctx, dekKeyID); err != nil { return fmt.Errorf("engine: create DEK %q: %w", dekKeyID, err) } } if err := eng.Initialize(ctx, r.barrier, mountPath, config); err != nil { return fmt.Errorf("engine: initialize %q: %w", name, err) } // Persist mount metadata so it can be reloaded after unseal. meta, err := json.Marshal(mountMeta{Name: name, Type: engineType}) if err != nil { return fmt.Errorf("engine: marshal mount metadata: %w", err) } if err := r.barrier.Put(ctx, mountsPrefix+name+".json", meta); err != nil { return fmt.Errorf("engine: persist mount metadata: %w", err) } r.mounts[name] = &Mount{ Name: name, Type: engineType, MountPath: mountPath, Engine: eng, } r.logger.Debug("engine mounted", "name", name, "type", engineType, "mount_path", mountPath) return nil } // GetEngine returns the engine for the given mount name. func (r *Registry) GetEngine(name string) (Engine, error) { r.mu.RLock() defer r.mu.RUnlock() mount, exists := r.mounts[name] if !exists { return nil, ErrMountNotFound } return mount.Engine, nil } // GetMount returns the mount for the given name. func (r *Registry) GetMount(name string) (*Mount, error) { r.mu.RLock() defer r.mu.RUnlock() mount, exists := r.mounts[name] if !exists { return nil, ErrMountNotFound } return mount, nil } // Unmount removes and seals an engine mount. func (r *Registry) Unmount(ctx context.Context, name string) error { r.mu.Lock() defer r.mu.Unlock() mount, exists := r.mounts[name] if !exists { return ErrMountNotFound } r.logger.Debug("unmounting engine", "name", name, "type", mount.Type) if err := mount.Engine.Seal(); err != nil { return fmt.Errorf("engine: seal %q: %w", name, err) } // Remove persisted mount metadata. if err := r.barrier.Delete(ctx, mountsPrefix+name+".json"); err != nil { return fmt.Errorf("engine: delete mount metadata %q: %w", name, err) } delete(r.mounts, name) r.logger.Debug("engine unmounted", "name", name) return nil } // UnsealAll discovers persisted mounts from the barrier and reloads them into // memory. It must be called after the barrier is unsealed. // // It first loads mounts recorded in engine/_mounts/, then does a discovery // pass over each registered engine type to pick up any mounts that predate // the metadata mechanism (one-time migration). func (r *Registry) UnsealAll(ctx context.Context) error { r.mu.Lock() defer r.mu.Unlock() // Load mounts from persisted metadata. if err := r.loadFromMetadata(ctx); err != nil { return err } // Discovery pass: scan each registered engine type for mounts that exist // in the barrier but have no metadata entry yet (pre-migration mounts). for engineType, factory := range r.factories { prefix := fmt.Sprintf("engine/%s/", engineType) paths, err := r.barrier.List(ctx, prefix) if err != nil { continue // no entries for this type } // Extract unique mount names from paths like "myca/config.json". seen := make(map[string]bool) for _, p := range paths { parts := strings.SplitN(p, "/", 2) if len(parts) == 0 || parts[0] == "" || parts[0] == "_mounts" { continue } seen[parts[0]] = true } for name := range seen { if _, exists := r.mounts[name]; exists { continue // already loaded } r.logger.Debug("discovered pre-migration engine mount", "name", name, "type", engineType) eng := factory() mountPath := fmt.Sprintf("engine/%s/%s/", engineType, name) if err := eng.Unseal(ctx, r.barrier, mountPath); err != nil { return fmt.Errorf("engine: unseal (discovered) %q: %w", name, err) } // Write metadata so future restarts don't need the discovery pass. meta, _ := json.Marshal(mountMeta{Name: name, Type: engineType}) _ = r.barrier.Put(ctx, mountsPrefix+name+".json", meta) r.mounts[name] = &Mount{ Name: name, Type: engineType, MountPath: mountPath, Engine: eng, } } } return nil } // loadFromMetadata loads mounts recorded under engine/_mounts/. Caller must hold r.mu. func (r *Registry) loadFromMetadata(ctx context.Context) error { paths, err := r.barrier.List(ctx, mountsPrefix) if err != nil { return nil // no metadata written yet } for _, p := range paths { if !strings.HasSuffix(p, ".json") { continue } data, err := r.barrier.Get(ctx, mountsPrefix+p) if err != nil { return fmt.Errorf("engine: read mount metadata %q: %w", p, err) } var meta mountMeta if err := json.Unmarshal(data, &meta); err != nil { return fmt.Errorf("engine: parse mount metadata %q: %w", p, err) } factory, ok := r.factories[meta.Type] if !ok { return fmt.Errorf("%w: %s (mount %q)", ErrUnknownType, meta.Type, meta.Name) } r.logger.Debug("unsealing engine from metadata", "name", meta.Name, "type", meta.Type) eng := factory() mountPath := fmt.Sprintf("engine/%s/%s/", meta.Type, meta.Name) if err := eng.Unseal(ctx, r.barrier, mountPath); err != nil { return fmt.Errorf("engine: unseal %q: %w", meta.Name, err) } r.mounts[meta.Name] = &Mount{ Name: meta.Name, Type: meta.Type, MountPath: mountPath, Engine: eng, } } return nil } // ListMounts returns all current mounts. func (r *Registry) ListMounts() []Mount { r.mu.RLock() defer r.mu.RUnlock() mounts := make([]Mount, 0, len(r.mounts)) for _, m := range r.mounts { mounts = append(mounts, Mount{ Name: m.Name, Type: m.Type, MountPath: m.MountPath, }) } return mounts } // HandleRequest routes a request to the appropriate engine. func (r *Registry) HandleRequest(ctx context.Context, mountName string, req *Request) (*Response, error) { r.mu.RLock() mount, exists := r.mounts[mountName] r.mu.RUnlock() if !exists { return nil, ErrMountNotFound } r.logger.Debug("routing engine request", "mount", mountName, "operation", req.Operation, "path", req.Path) return mount.Engine.HandleRequest(ctx, req) } // SealAll seals all mounted engines. func (r *Registry) SealAll() error { r.mu.Lock() defer r.mu.Unlock() r.logger.Debug("sealing all engines", "count", len(r.mounts)) for name, mount := range r.mounts { if err := mount.Engine.Seal(); err != nil { return fmt.Errorf("engine: seal %q: %w", name, err) } } return nil }