202 lines
5.8 KiB
Go
202 lines
5.8 KiB
Go
package kv
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"github.com/influxdata/influxdb"
|
|
)
|
|
|
|
// MinPasswordLength is the shortest password we allow into the system.
|
|
const MinPasswordLength = 8
|
|
|
|
var (
|
|
// EIncorrectPassword is returned when any password operation fails in which
|
|
// we do not want to leak information.
|
|
EIncorrectPassword = &influxdb.Error{
|
|
Code: influxdb.EForbidden,
|
|
Msg: "your username or password is incorrect",
|
|
}
|
|
|
|
// EShortPassword is used when a password is less than the minimum
|
|
// acceptable password length.
|
|
EShortPassword = &influxdb.Error{
|
|
Code: influxdb.EInvalid,
|
|
Msg: "passwords must be at least 8 characters long",
|
|
}
|
|
)
|
|
|
|
// UnavailablePasswordServiceError is used if we aren't able to add the
|
|
// password to the store, it means the store is not available at the moment
|
|
// (e.g. network).
|
|
func UnavailablePasswordServiceError(err error) *influxdb.Error {
|
|
return &influxdb.Error{
|
|
Code: influxdb.EUnavailable,
|
|
Msg: fmt.Sprintf("Unable to connect to password service. Please try again; Err: %v", err),
|
|
Op: "kv/setPassword",
|
|
}
|
|
}
|
|
|
|
// CorruptUserIDError is used when the ID was encoded incorrectly previously.
|
|
// This is some sort of internal server error.
|
|
func CorruptUserIDError(name string, err error) *influxdb.Error {
|
|
return &influxdb.Error{
|
|
Code: influxdb.EInternal,
|
|
Msg: fmt.Sprintf("User ID for %s has been corrupted; Err: %v", name, err),
|
|
Op: "kv/setPassword",
|
|
}
|
|
}
|
|
|
|
// InternalPasswordHashError is used if the hasher is unable to generate
|
|
// a hash of the password. This is some sort of internal server error.
|
|
func InternalPasswordHashError(err error) *influxdb.Error {
|
|
return &influxdb.Error{
|
|
Code: influxdb.EInternal,
|
|
Msg: fmt.Sprintf("Unable to generate password; Err: %v", err),
|
|
Op: "kv/setPassword",
|
|
}
|
|
}
|
|
|
|
var (
|
|
userpasswordBucket = []byte("userspasswordv1")
|
|
)
|
|
|
|
var _ influxdb.PasswordsService = (*Service)(nil)
|
|
|
|
func (s *Service) initializePasswords(ctx context.Context, tx Tx) error {
|
|
_, err := tx.Bucket(userpasswordBucket)
|
|
return err
|
|
}
|
|
|
|
// CompareAndSetPassword checks the password and if they match
|
|
// updates to the new password.
|
|
func (s *Service) CompareAndSetPassword(ctx context.Context, name string, old string, new string) error {
|
|
return s.kv.Update(ctx, func(tx Tx) error {
|
|
if err := s.comparePassword(ctx, tx, name, old); err != nil {
|
|
return err
|
|
}
|
|
return s.setPassword(ctx, tx, name, new)
|
|
})
|
|
}
|
|
|
|
// SetPassword overrides the password of a known user.
|
|
func (s *Service) SetPassword(ctx context.Context, name string, password string) error {
|
|
return s.kv.Update(ctx, func(tx Tx) error {
|
|
return s.setPassword(ctx, tx, name, password)
|
|
})
|
|
}
|
|
|
|
// ComparePassword checks if the password matches the password recorded.
|
|
// Passwords that do not match return errors.
|
|
func (s *Service) ComparePassword(ctx context.Context, name string, password string) error {
|
|
return s.kv.View(ctx, func(tx Tx) error {
|
|
return s.comparePassword(ctx, tx, name, password)
|
|
})
|
|
}
|
|
|
|
func (s *Service) setPassword(ctx context.Context, tx Tx, name string, password string) error {
|
|
if len(password) < MinPasswordLength {
|
|
return EShortPassword
|
|
}
|
|
|
|
u, err := s.findUserByName(ctx, tx, name)
|
|
if err != nil {
|
|
return EIncorrectPassword
|
|
}
|
|
|
|
encodedID, err := u.ID.Encode()
|
|
if err != nil {
|
|
return CorruptUserIDError(name, err)
|
|
}
|
|
|
|
b, err := tx.Bucket(userpasswordBucket)
|
|
if err != nil {
|
|
return UnavailablePasswordServiceError(err)
|
|
}
|
|
|
|
hasher := s.Hash
|
|
if hasher == nil {
|
|
hasher = &Bcrypt{}
|
|
}
|
|
|
|
hash, err := hasher.GenerateFromPassword([]byte(password), DefaultCost)
|
|
if err != nil {
|
|
return InternalPasswordHashError(err)
|
|
}
|
|
|
|
if err := b.Put(encodedID, hash); err != nil {
|
|
return UnavailablePasswordServiceError(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) comparePassword(ctx context.Context, tx Tx, name string, password string) error {
|
|
u, err := s.findUserByName(ctx, tx, name)
|
|
if err != nil {
|
|
return EIncorrectPassword
|
|
}
|
|
|
|
encodedID, err := u.ID.Encode()
|
|
if err != nil {
|
|
return CorruptUserIDError(name, err)
|
|
}
|
|
|
|
b, err := tx.Bucket(userpasswordBucket)
|
|
if err != nil {
|
|
return UnavailablePasswordServiceError(err)
|
|
}
|
|
|
|
hash, err := b.Get(encodedID)
|
|
if err != nil {
|
|
// User exists but has no password has been set.
|
|
return EIncorrectPassword
|
|
}
|
|
|
|
hasher := s.Hash
|
|
if hasher == nil {
|
|
hasher = &Bcrypt{}
|
|
}
|
|
|
|
if err := hasher.CompareHashAndPassword(hash, []byte(password)); err != nil {
|
|
// User exists but the password was incorrect
|
|
return EIncorrectPassword
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DefaultCost is the cost that will actually be set if a cost below MinCost
|
|
// is passed into GenerateFromPassword
|
|
var DefaultCost = bcrypt.DefaultCost
|
|
|
|
// Crypt represents a cryptographic hashing function.
|
|
type Crypt interface {
|
|
// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
|
|
// Returns nil on success, or an error on failure.
|
|
CompareHashAndPassword(hashedPassword, password []byte) error
|
|
// GenerateFromPassword returns the hash of the password at the given cost.
|
|
// If the cost given is less than MinCost, the cost will be set to DefaultCost, instead.
|
|
GenerateFromPassword(password []byte, cost int) ([]byte, error)
|
|
}
|
|
|
|
var _ Crypt = (*Bcrypt)(nil)
|
|
|
|
// Bcrypt implements Crypt using golang.org/x/crypto/bcrypt
|
|
type Bcrypt struct{}
|
|
|
|
// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
|
|
// Returns nil on success, or an error on failure.
|
|
func (b *Bcrypt) CompareHashAndPassword(hashedPassword, password []byte) error {
|
|
return bcrypt.CompareHashAndPassword(hashedPassword, password)
|
|
}
|
|
|
|
// GenerateFromPassword returns the hash of the password at the given cost.
|
|
// If the cost given is less than MinCost, the cost will be set to DefaultCost, instead.
|
|
func (b *Bcrypt) GenerateFromPassword(password []byte, cost int) ([]byte, error) {
|
|
if cost < bcrypt.MinCost {
|
|
cost = DefaultCost
|
|
}
|
|
return bcrypt.GenerateFromPassword(password, cost)
|
|
}
|