package authorization import ( "bytes" "context" crand "crypto/rand" "crypto/sha256" "io" "sync" "github.com/influxdata/influxdb/v2" "github.com/influxdata/influxdb/v2/kit/platform" ) // An implementation of influxdb.PasswordsService that will perform // ComparePassword requests at a reduced cost under certain // conditions. See ComparePassword for further information. // // The cache is only valid for the duration of the process. type CachingPasswordsService struct { inner influxdb.PasswordsService mu sync.RWMutex // protects concurrent access to authCache authCache map[platform.ID]authUser } func NewCachingPasswordsService(inner influxdb.PasswordsService) *CachingPasswordsService { return &CachingPasswordsService{inner: inner, authCache: make(map[platform.ID]authUser)} } var _ influxdb.PasswordsService = (*CachingPasswordsService)(nil) func (c *CachingPasswordsService) SetPassword(ctx context.Context, id platform.ID, password string) error { err := c.inner.SetPassword(ctx, id, password) if err == nil { c.mu.Lock() delete(c.authCache, id) c.mu.Unlock() } return err } // ComparePassword will attempt to perform the comparison using a lower cost hashing function // if influxdb.ContextHasPasswordCacheOption returns true for ctx. func (c *CachingPasswordsService) ComparePassword(ctx context.Context, id platform.ID, password string) error { c.mu.RLock() au, ok := c.authCache[id] c.mu.RUnlock() if ok { // verify the password using the cached salt and hash if bytes.Equal(c.hashWithSalt(au.salt, password), au.hash) { return nil } // fall through to requiring a full bcrypt hash for invalid passwords } err := c.inner.ComparePassword(ctx, id, password) if err != nil { return err } if salt, hashed, err := c.saltedHash(password); err == nil { c.mu.Lock() c.authCache[id] = authUser{salt: salt, hash: hashed} c.mu.Unlock() } return nil } func (c *CachingPasswordsService) CompareAndSetPassword(ctx context.Context, id platform.ID, old, new string) error { err := c.inner.CompareAndSetPassword(ctx, id, old, new) if err == nil { c.mu.Lock() delete(c.authCache, id) c.mu.Unlock() } return err } // NOTE(sgc): This caching implementation was lifted from the 1.x source // https://github.com/influxdata/influxdb/blob/c1e11e732e145fc1a356535ddf3dcb9fb732a22b/services/meta/client.go#L390-L406 const ( // SaltBytes is the number of bytes used for salts. SaltBytes = 32 ) type authUser struct { salt []byte hash []byte } // hashWithSalt returns a salted hash of password using salt. func (c *CachingPasswordsService) hashWithSalt(salt []byte, password string) []byte { hasher := sha256.New() hasher.Write(salt) hasher.Write([]byte(password)) return hasher.Sum(nil) } // saltedHash returns a salt and salted hash of password. func (c *CachingPasswordsService) saltedHash(password string) (salt, hash []byte, err error) { salt = make([]byte, SaltBytes) if _, err := io.ReadFull(crand.Reader, salt); err != nil { return nil, nil, err } return salt, c.hashWithSalt(salt, password), nil }