// Package backend defines the resolver interface every credential source // implements. Backends receive opaque refs (mpk-) and return key // material; material is held in memory only, never persisted, never // written to logs. Errors identify the ref, the backend, and a // machine-readable reason — never material. package backend import ( "context" "errors" "fmt" "sort" ) // Ref is one configured mpk- entry from keyproxy.toml. Source and // Key name WHERE material lives; they are locations, not material. type Ref struct { Name string `json:"ref"` // consumer-facing opaque ref, e.g. "mpk-redmine" Backend string `json:"backend"` // file | env | bitwarden | vault Source string `json:"-"` // backend-specific source (file path, env var name, vault path) Key string `json:"-"` // key inside the source (env-file key, secret name) Mode string `json:"-"` // file backend: allowed permission mask, default "0600" } // Reason is a machine-readable failure class. Reason strings are the only // failure detail that may cross the wire or reach logs: they are fixed // enums and can never embed source contents. type Reason string const ( ReasonUnknownRef Reason = "unknown_ref" ReasonNotImplemented Reason = "not_implemented" ReasonUnreadableSource Reason = "unreadable_source" ReasonInsecureMode Reason = "insecure_source_mode" ReasonMalformedSource Reason = "malformed_source" ReasonMissingKey Reason = "missing_key" ReasonEmptyValue Reason = "empty_value" ) // ResolveError names the ref, the backend and a reason. It never carries // material; its Error() output is safe to log. type ResolveError struct { Ref string Backend string Reason Reason } func (e *ResolveError) Error() string { return fmt.Sprintf("ref %s: backend %s: %s", e.Ref, e.Backend, e.Reason) } // Err wraps err as a ResolveError for ref/backend, deduplicating if it // already is one. func Err(ref Ref, backend string, reason Reason) error { return &ResolveError{Ref: ref.Name, Backend: backend, Reason: reason} } // AsResolveError extracts a *ResolveError, or wraps an unexpected error // as a generic unreadable_source failure (the underlying text is dropped: // it is never safe to assume it is material-free). func AsResolveError(err error, ref Ref, backend string) *ResolveError { var re *ResolveError if err != nil && errors.As(err, &re) { return re } return &ResolveError{Ref: ref.Name, Backend: backend, Reason: ReasonUnreadableSource} } // Backend resolves one Ref to its material. Implementations must not // persist material, cache it to disk, or write it to any log. type Backend interface { // Name is the backend's config-facing name (file, env, ...). Name() string // Resolve returns the material for ref. Errors must be (or wrap) a // *ResolveError; returned strings exist in memory only. Resolve(ctx context.Context, ref Ref) (string, error) } // Registry maps backend names to implementations. type Registry struct { m map[string]Backend } // NewRegistry builds a registry from the given backends. func NewRegistry(backends ...Backend) *Registry { r := &Registry{m: make(map[string]Backend, len(backends))} for _, b := range backends { r.m[b.Name()] = b } return r } // Get returns the backend registered under name. func (r *Registry) Get(name string) (Backend, bool) { b, ok := r.m[name] return b, ok } // Names lists registered backend names, sorted. func (r *Registry) Names() []string { out := make([]string, 0, len(r.m)) for n := range r.m { out = append(out, n) } sort.Strings(out) return out }