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) } type entry struct { value Value version uint64 reference Reference } // 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]*entry 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]*entry), 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.release() if err := ctx.Err(); err != nil { return Reference{}, err } if current, ok := s.byScope[scope]; ok { if current.value == value { return current.reference, nil } current.value = value current.version++ current.reference.CredentialVersion = versionString(current.version) return current.reference, nil } if len(s.byScope) >= s.capacity { return Reference{}, ErrCapacityExceeded } secretRef, err := s.newUniqueSecretRef() if err != nil { return Reference{}, err } created := &entry{ value: value, version: 1, reference: Reference{ SecretRef: secretRef, CredentialVersion: versionString(1), }, } s.byScope[scope] = created 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.release() 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) 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) release() { 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() }