Files
threshold/internal/auth/password/argon2.go
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

250 lines
7.8 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
package password
import (
"context"
"crypto/rand"
"strings"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/auth/password/store"
2020-11-05 16:54:03 -05:00
"github.com/hashicorp/boundary/internal/errors"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/oplog"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
"golang.org/x/crypto/argon2"
"google.golang.org/protobuf/proto"
)
2025-01-22 13:31:42 -08:00
// hashingPermitPool is the global permit pool used to restrict concurrent
// password hashing. It can be resized with SetHashingPermits.
var hashingPermitPool *resizablePermitPool
func init() {
hashingPermitPool = newResizablePermitPool(1)
}
// SetHashingPermits sets the number of concurrent password hashing operations permitted.
func SetHashingPermits(n int) error {
const op = "password.SetHashingPermits"
if n <= 0 {
return errors.New(context.Background(), errors.InvalidParameter, op, "n must be greater than 0")
}
if err := hashingPermitPool.SetPermits(n); err != nil {
return err
}
return nil
}
// Argon2Configuration is a configuration for using the argon2id key
// derivation function. It is owned by an AuthMethod.
//
// Iterations, Memory, and Threads are the cost parameters. The cost
// parameters should be increased as memory latency and CPU parallelism
// increases.
//
// For a detailed specification of Argon2 see:
// https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
type Argon2Configuration struct {
*store.Argon2Configuration
tableName string
}
// NewArgon2Configuration creates a new in memory Argon2Configuration with
// reasonable default settings.
func NewArgon2Configuration() *Argon2Configuration {
return &Argon2Configuration{
Argon2Configuration: &store.Argon2Configuration{
Iterations: 3,
Memory: 64 * 1024,
Threads: 1,
SaltLength: 32,
KeyLength: 32,
},
}
}
func (c *Argon2Configuration) validate(ctx context.Context) error {
2021-03-05 06:34:06 -08:00
const op = "password.(Argon2Configuration).validate"
if c == nil {
return errors.New(ctx, errors.PasswordInvalidConfiguration, op, "missing config")
}
2021-03-05 06:34:06 -08:00
if c.Argon2Configuration == nil {
return errors.New(ctx, errors.PasswordInvalidConfiguration, op, "missing embedded config")
2021-03-05 06:34:06 -08:00
}
if c.Iterations == 0 {
return errors.New(ctx, errors.PasswordInvalidConfiguration, op, "missing iterations")
2021-03-05 06:34:06 -08:00
}
if c.Memory == 0 {
return errors.New(ctx, errors.PasswordInvalidConfiguration, op, "missing memory")
2021-03-05 06:34:06 -08:00
}
if c.Threads == 0 {
return errors.New(ctx, errors.PasswordInvalidConfiguration, op, "missing threads")
2021-03-05 06:34:06 -08:00
}
if c.SaltLength == 0 {
return errors.New(ctx, errors.PasswordInvalidConfiguration, op, "missing salt length")
2021-03-05 06:34:06 -08:00
}
if c.KeyLength == 0 {
return errors.New(ctx, errors.PasswordInvalidConfiguration, op, "missing key length")
2021-03-05 06:34:06 -08:00
}
return nil
}
// AuthMethodId returns the Id of the AuthMethod which owns c.
func (c *Argon2Configuration) AuthMethodId() string {
if c != nil && c.Argon2Configuration != nil {
return c.PasswordMethodId
}
return ""
}
func (c *Argon2Configuration) clone() *Argon2Configuration {
cp := proto.Clone(c.Argon2Configuration)
return &Argon2Configuration{
Argon2Configuration: cp.(*store.Argon2Configuration),
}
}
// TableName returns the table name.
func (c *Argon2Configuration) TableName() string {
if c != nil && c.tableName != "" {
return c.tableName
}
return "auth_password_argon2_conf"
}
// SetTableName sets the table name.
func (c *Argon2Configuration) SetTableName(n string) {
c.tableName = n
}
func (c *Argon2Configuration) oplog(op oplog.OpType) oplog.Metadata {
metadata := oplog.Metadata{
"resource-public-id": []string{c.PrivateId},
"resource-type": []string{"password argon2 conf"},
"op-type": []string{op.String()},
}
if c.PasswordMethodId != "" {
metadata["password-method-id"] = []string{c.PasswordMethodId}
}
return metadata
}
2022-10-25 18:29:38 -05:00
func (c *Argon2Configuration) whereDup() (string, []any) {
var where []string
2022-10-25 18:29:38 -05:00
var args []any
where, args = append(where, "password_method_id = ?"), append(args, c.PasswordMethodId)
where, args = append(where, "iterations = ?"), append(args, c.Iterations)
where, args = append(where, "memory = ?"), append(args, c.Memory)
where, args = append(where, "threads = ?"), append(args, c.Threads)
where, args = append(where, "key_length = ?"), append(args, c.KeyLength)
where, args = append(where, "salt_length = ?"), append(args, c.SaltLength)
return strings.Join(where, " and "), args
}
// A Argon2Credential contains a key derived from a password and the salt
// used in the key derivation. It is owned by an Account.
type Argon2Credential struct {
*store.Argon2Credential
tableName string
}
func newArgon2Credential(ctx context.Context, accountId string, password string, conf *Argon2Configuration) (*Argon2Credential, error) {
const op = "password.newArgon2Credential"
if accountId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing accountId")
}
if password == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password")
}
if conf == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing argon2 configuration")
}
id, err := newArgon2CredentialId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
c := &Argon2Credential{
Argon2Credential: &store.Argon2Credential{
PrivateId: id,
PasswordAccountId: accountId,
PasswordConfId: conf.PrivateId,
2020-08-06 13:03:42 -04:00
PasswordMethodId: conf.PasswordMethodId,
},
}
salt := make([]byte, conf.SaltLength)
if _, err := rand.Read(salt); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Io))
}
c.Salt = salt
2025-01-22 13:31:42 -08:00
// Limit the number of concurrent calls to the argon2 hashing function,
// since each call consumes a significant amount of CPU and memory.
if err := hashingPermitPool.Do(ctx, func() {
c.DerivedKey = argon2.IDKey([]byte(password), c.Salt, conf.Iterations, conf.Memory, uint8(conf.Threads), conf.KeyLength)
}); err != nil {
// Context was canceled while waiting for a permit
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("context canceled while waiting for hashing permit"))
}
return c, nil
}
func (c *Argon2Credential) clone() *Argon2Credential {
cp := proto.Clone(c.Argon2Credential)
return &Argon2Credential{
Argon2Credential: cp.(*store.Argon2Credential),
}
}
// TableName returns the table name.
func (c *Argon2Credential) TableName() string {
if c != nil && c.tableName != "" {
return c.tableName
}
return "auth_password_argon2_cred"
}
// SetTableName sets the table name.
func (c *Argon2Credential) SetTableName(n string) {
c.tableName = n
}
func (c *Argon2Credential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "password.(Argon2Credential).encrypt"
if err := structwrapping.WrapStruct(ctx, cipher, c.Argon2Credential, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
keyId, err := cipher.KeyId(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("error reading cipher key id"))
}
c.KeyId = keyId
return nil
}
func (c *Argon2Credential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
const op = "password.(Argon2Credential).decrypt"
if err := structwrapping.UnwrapStruct(ctx, cipher, c.Argon2Credential, nil); err != nil {
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
}
return nil
}
func (c *Argon2Credential) oplog(op oplog.OpType) oplog.Metadata {
metadata := oplog.Metadata{
"resource-public-id": []string{c.PrivateId},
"resource-type": []string{"argon2 credential"},
"op-type": []string{op.String()},
"password-account-id": []string{c.PasswordAccountId},
}
if c.PasswordMethodId != "" {
metadata["password-method-id"] = []string{c.PasswordMethodId}
}
return metadata
}