package credentials import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "strconv" "strings" ) var ( ErrInvalidCapacity = errors.New("invalid credential store capacity") ErrInvalidStore = errors.New("invalid credential store") ErrInvalidScope = errors.New("invalid credential scope") ErrInvalidReference = errors.New("invalid credential reference") ErrCapacityExceeded = errors.New("credential store capacity exceeded") ErrCredentialMissing = errors.New("credential not found") ErrCredentialVersionMismatch = errors.New("credential version mismatch") ErrReferenceCreation = errors.New("credential reference creation failed") ) // Value is resolved credential material. Callers must not log this value. type Value struct { Username string Password string } func (Value) Format(state fmt.State, _ rune) { _, _ = state.Write([]byte("credentials.Value{Username:, Password:}")) } // Reference identifies one exact version of stored credential material. type Reference struct { SecretRef string CredentialVersion string } func (reference Reference) Format(state fmt.State, _ rune) { formatted := "credentials.Reference{SecretRef:, CredentialVersion:" + strconv.Quote(reference.CredentialVersion) + "}" _, _ = state.Write([]byte(formatted)) } type Store interface { Put(context.Context, string, Value) (Reference, error) Resolve(context.Context, Reference) (Value, error) } // Releaser removes transient credential material after the consumer has copied // it into its authoritative storage. Release is idempotent and version fenced. type Releaser interface { Release(context.Context, Reference) error } type CapacityEnsurer interface { EnsureCapacity(context.Context, int) error } type entry struct { scope *scopeState value Value reference Reference } type scopeState struct { name string value Value version uint64 leases int } // MemoryStore keeps credentials in process memory and serializes access with a // context-aware lock. type MemoryStore struct { lock chan struct{} capacity int byScope map[string]*scopeState byRef map[string]*entry } func (s *MemoryStore) Format(state fmt.State, _ rune) { if s == nil { _, _ = state.Write([]byte("credentials.MemoryStore")) return } _, _ = state.Write([]byte("credentials.MemoryStore{capacity:" + strconv.Itoa(s.capacity) + "}")) } var _ Store = (*MemoryStore)(nil) func NewMemoryStore(capacity int) (*MemoryStore, error) { if capacity <= 0 { return nil, ErrInvalidCapacity } lock := make(chan struct{}, 1) lock <- struct{}{} return &MemoryStore{ lock: lock, capacity: capacity, byScope: make(map[string]*scopeState), byRef: make(map[string]*entry), }, nil } func (s *MemoryStore) Put(ctx context.Context, scope string, value Value) (Reference, error) { if err := contextError(ctx); err != nil { return Reference{}, err } if !s.valid() { return Reference{}, ErrInvalidStore } if strings.TrimSpace(scope) == "" { return Reference{}, ErrInvalidScope } if err := s.acquire(ctx); err != nil { return Reference{}, err } defer s.unlock() if err := ctx.Err(); err != nil { return Reference{}, err } if len(s.byRef) >= s.capacity { return Reference{}, ErrCapacityExceeded } current, exists := s.byScope[scope] if !exists && len(s.byScope) >= s.capacity { return Reference{}, ErrCapacityExceeded } secretRef, err := s.newUniqueSecretRef() if err != nil { return Reference{}, err } if !exists { current = &scopeState{name: scope, value: value, version: 1} s.byScope[scope] = current } else if current.value != value { current.value = value current.version++ } created := &entry{ scope: current, value: value, reference: Reference{ SecretRef: secretRef, CredentialVersion: versionString(current.version), }, } current.leases++ s.byRef[secretRef] = created return created.reference, nil } func (s *MemoryStore) Resolve(ctx context.Context, reference Reference) (Value, error) { if err := contextError(ctx); err != nil { return Value{}, err } if !s.valid() { return Value{}, ErrInvalidStore } if reference.SecretRef == "" || !validVersion(reference.CredentialVersion) { return Value{}, ErrInvalidReference } if err := s.acquire(ctx); err != nil { return Value{}, err } defer s.unlock() if err := ctx.Err(); err != nil { return Value{}, err } current, ok := s.byRef[reference.SecretRef] if !ok { return Value{}, ErrCredentialMissing } if current.reference.CredentialVersion != reference.CredentialVersion { return Value{}, ErrCredentialVersionMismatch } return current.value, nil } func (s *MemoryStore) Release(ctx context.Context, reference Reference) error { if err := contextError(ctx); err != nil { return err } if !s.valid() { return ErrInvalidStore } if reference.SecretRef == "" || !validVersion(reference.CredentialVersion) { return ErrInvalidReference } if err := s.acquire(ctx); err != nil { return err } defer s.unlock() if err := ctx.Err(); err != nil { return err } current, ok := s.byRef[reference.SecretRef] if !ok || current.reference != reference { return nil } delete(s.byRef, reference.SecretRef) current.scope.leases-- if current.scope.leases == 0 { delete(s.byScope, current.scope.name) current.scope.value = Value{} } current.value = Value{} return nil } func (s *MemoryStore) EnsureCapacity(ctx context.Context, minimum int) error { if err := contextError(ctx); err != nil { return err } if !s.valid() { return ErrInvalidStore } if minimum <= 0 { return ErrInvalidCapacity } if err := s.acquire(ctx); err != nil { return err } defer s.unlock() if minimum > s.capacity { s.capacity = minimum } return nil } func (s *MemoryStore) acquire(ctx context.Context) error { if err := contextError(ctx); err != nil { return err } select { case <-ctx.Done(): return ctx.Err() case <-s.lock: return nil } } func (s *MemoryStore) unlock() { s.lock <- struct{}{} } func (s *MemoryStore) valid() bool { return s != nil && s.lock != nil && s.capacity > 0 && s.byScope != nil && s.byRef != nil } func (s *MemoryStore) newUniqueSecretRef() (string, error) { for { secretRef, err := newSecretRef() if err != nil { return "", err } if _, exists := s.byRef[secretRef]; !exists { return secretRef, nil } } } func newSecretRef() (string, error) { var random [24]byte if _, err := rand.Read(random[:]); err != nil { return "", ErrReferenceCreation } return "cred_" + hex.EncodeToString(random[:]), nil } func versionString(version uint64) string { return "v" + strconv.FormatUint(version, 10) } func validVersion(version string) bool { if len(version) < 2 || version[0] != 'v' || version[1] == '0' { return false } _, err := strconv.ParseUint(version[1:], 10, 64) return err == nil } func contextError(ctx context.Context) error { if ctx == nil { return context.Canceled } return ctx.Err() }