pkg/utils/cryptutil/hasher.go (63 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package cryptutil
import (
"bytes"
lru "github.com/hashicorp/golang-lru/v2"
"golang.org/x/crypto/bcrypt"
)
// NewPasswordHasher returns a bcrypt hash generator.
// If size is greater than 0 the hashes are cached using a cache of the provided size.
func NewPasswordHasher(size int) (PasswordHasher, error) {
if size > 0 {
lruCache, err := lru.New[string, []byte](size)
if err != nil {
return nil, err
}
return &lruHashCache{
generateFromPassword: bcrypt.GenerateFromPassword,
compareHashAndPassword: bcrypt.CompareHashAndPassword,
hashCache: lruCache,
}, nil
}
return &passwordHashProvider{}, nil
}
type PasswordHasher interface {
ReuseOrGenerateHash(password, existingHash []byte) ([]byte, error)
}
type generateFromPassword func(password []byte, cost int) ([]byte, error)
type compareHashAndPassword func(hashedPassword, password []byte) error
type lruHashCache struct {
hashCache *lru.Cache[string, []byte]
// only to be changed for unit tests
generateFromPassword
compareHashAndPassword
}
func (h *lruHashCache) ReuseOrGenerateHash(password, existingHash []byte) ([]byte, error) {
key := string(password)
if len(existingHash) > 0 {
// Check if we have the hash in cache
cachedHash := h.get(key)
if cachedHash != nil && bytes.Equal(cachedHash, existingHash) {
return existingHash, nil
}
// Check if the existing hash is valid
if h.compareHashAndPassword(existingHash, password) == nil {
// existing hash is valid, save it and return
h.hashCache.Add(key, existingHash)
return existingHash, nil
}
}
// No existing hash or existing hash is not valid
hash, err := h.generateFromPassword(password, bcrypt.DefaultCost)
if err != nil {
return nil, err
}
h.hashCache.Add(key, hash)
return hash, nil
}
func (h *lruHashCache) get(key string) []byte {
cachedHash, exists := h.hashCache.Get(key)
if !exists {
return nil
}
return cachedHash
}
type passwordHashProvider struct{}
func (n *passwordHashProvider) ReuseOrGenerateHash(password, existingHash []byte) ([]byte, error) {
if len(existingHash) > 0 && bcrypt.CompareHashAndPassword(existingHash, password) == nil {
return existingHash, nil
}
return bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
}