mirror of https://github.com/hashicorp/boundary
Add configuration settings for Argon2 password KDF (#217)
* Bump generated proto code * Add argon2 conf sql and proto files * Add generated code for argon2 conf * Add Resources for Argon2 configuration * Add get and set Configuration to repository * Add current config reference to AuthMethod * Fix authmethod_test * Fix TestAccount_New * Add WithConfiguration to options * Add sql views for current configuration * Fix SetTableName * Remove duplicate testAccounts function * Make tests clearer * Fix comment * Rename test variable * Use NewPrivateId function for private Ids * Refactor: rename public_id to private_id for password_conf and password_credential * Remove bad comment * Check if embedded argon2 configuration is nil * Add checks around the WithConfiguration option * Use spaces not tabs in proto file * Add doc to explain choice of oplog ticket * Verify oplog in testpull/229/head
parent
c3c07f1556
commit
c163d790a4
@ -0,0 +1,102 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/auth/password/store"
|
||||
"github.com/hashicorp/watchtower/internal/oplog"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// 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() error {
|
||||
switch {
|
||||
case c == nil, c.Argon2Configuration == nil:
|
||||
return ErrInvalidConfiguration
|
||||
case c.Iterations == 0, c.Memory == 0, c.Threads == 0, c.SaltLength == 0, c.KeyLength == 0:
|
||||
return ErrInvalidConfiguration
|
||||
default:
|
||||
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
|
||||
}
|
||||
|
||||
func (c *Argon2Configuration) whereDup() (string, []interface{}) {
|
||||
var where []string
|
||||
var args []interface{}
|
||||
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,270 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/auth/password/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestArgon2Configuration_New(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
|
||||
authMethods := testAuthMethods(t, conn, 1)
|
||||
authMethod := authMethods[0]
|
||||
authMethodId := authMethod.GetPublicId()
|
||||
ctx := context.Background()
|
||||
|
||||
// There should already be a configuration when an authMethod is created.
|
||||
t.Run("default-configuration", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
var confs []*Argon2Configuration
|
||||
err := rw.SearchWhere(ctx, &confs, "password_method_id = ?", []interface{}{authMethodId})
|
||||
require.NoError(err)
|
||||
require.Equal(1, len(confs))
|
||||
got := confs[0]
|
||||
want := &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
PrivateId: got.GetPrivateId(),
|
||||
CreateTime: got.GetCreateTime(),
|
||||
PasswordMethodId: authMethodId,
|
||||
Iterations: 3,
|
||||
Memory: 64 * 1024,
|
||||
Threads: 1,
|
||||
SaltLength: 32,
|
||||
KeyLength: 32,
|
||||
},
|
||||
}
|
||||
assert.Equal(want, got)
|
||||
})
|
||||
t.Run("no-duplicate-configurations", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got := NewArgon2Configuration()
|
||||
require.NotNil(got)
|
||||
var err error
|
||||
got.PrivateId, err = newArgon2ConfigurationId()
|
||||
require.NoError(err)
|
||||
got.PasswordMethodId = authMethodId
|
||||
err = rw.Create(ctx, got)
|
||||
assert.Error(err)
|
||||
})
|
||||
t.Run("multiple-configurations", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
var confs []*Argon2Configuration
|
||||
err := rw.SearchWhere(ctx, &confs, "password_method_id = ?", []interface{}{authMethodId})
|
||||
require.NoError(err)
|
||||
assert.Equal(1, len(confs))
|
||||
|
||||
c1 := NewArgon2Configuration()
|
||||
require.NotNil(c1)
|
||||
c1.PrivateId, err = newArgon2ConfigurationId()
|
||||
require.NoError(err)
|
||||
c1.PasswordMethodId = authMethodId
|
||||
c1.Iterations = c1.Iterations + 1
|
||||
c1.Threads = c1.Threads + 1
|
||||
err = rw.Create(ctx, c1)
|
||||
assert.NoError(err)
|
||||
|
||||
c2 := NewArgon2Configuration()
|
||||
require.NotNil(c2)
|
||||
c2.PrivateId, err = newArgon2ConfigurationId()
|
||||
require.NoError(err)
|
||||
c2.PasswordMethodId = authMethodId
|
||||
c2.Memory = 32 * 1024
|
||||
c2.SaltLength = 16
|
||||
c2.KeyLength = 16
|
||||
err = rw.Create(ctx, c2)
|
||||
assert.NoError(err)
|
||||
|
||||
confs = nil
|
||||
err = rw.SearchWhere(ctx, &confs, "password_method_id = ?", []interface{}{authMethodId})
|
||||
require.NoError(err)
|
||||
assert.Equal(3, len(confs))
|
||||
})
|
||||
}
|
||||
|
||||
func TestArgon2Configuration_Readonly(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
|
||||
rw := db.New(conn)
|
||||
|
||||
changeIterations := func() func(*Argon2Configuration) (*Argon2Configuration, []string) {
|
||||
return func(c *Argon2Configuration) (*Argon2Configuration, []string) {
|
||||
c.Iterations = c.Iterations + 1
|
||||
return c, []string{"Iterations"}
|
||||
}
|
||||
}
|
||||
changeThreads := func() func(*Argon2Configuration) (*Argon2Configuration, []string) {
|
||||
return func(c *Argon2Configuration) (*Argon2Configuration, []string) {
|
||||
c.Threads = c.Threads + 1
|
||||
return c, []string{"Threads"}
|
||||
}
|
||||
}
|
||||
changeMemory := func() func(*Argon2Configuration) (*Argon2Configuration, []string) {
|
||||
return func(c *Argon2Configuration) (*Argon2Configuration, []string) {
|
||||
c.Memory = c.Memory + 1
|
||||
return c, []string{"Memory"}
|
||||
}
|
||||
}
|
||||
|
||||
authMethods := testAuthMethods(t, conn, 1)
|
||||
authMethod := authMethods[0]
|
||||
authMethodId := authMethod.GetPublicId()
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
chgFn func(*Argon2Configuration) (*Argon2Configuration, []string)
|
||||
}{
|
||||
{
|
||||
name: "iterations",
|
||||
chgFn: changeIterations(),
|
||||
},
|
||||
{
|
||||
name: "threads",
|
||||
chgFn: changeThreads(),
|
||||
},
|
||||
{
|
||||
name: "Memory",
|
||||
chgFn: changeMemory(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
var confs []*Argon2Configuration
|
||||
err := rw.SearchWhere(context.Background(), &confs, "password_method_id = ?", []interface{}{authMethodId})
|
||||
require.NoError(err)
|
||||
assert.Greater(len(confs), 0)
|
||||
orig := confs[0]
|
||||
changed, masks := tt.chgFn(orig)
|
||||
|
||||
require.NotEmpty(changed.GetPrivateId())
|
||||
|
||||
count, err := rw.Update(context.Background(), changed, masks, nil)
|
||||
assert.Error(err)
|
||||
assert.Equal(0, count)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestArgon2Configuration_Validate(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
in *Argon2Configuration
|
||||
want error
|
||||
}{
|
||||
{
|
||||
name: "nil-configuration",
|
||||
in: nil,
|
||||
want: ErrInvalidConfiguration,
|
||||
},
|
||||
{
|
||||
name: "nil-embedded-config",
|
||||
in: &Argon2Configuration{},
|
||||
want: ErrInvalidConfiguration,
|
||||
},
|
||||
{
|
||||
name: "valid-default",
|
||||
in: NewArgon2Configuration(),
|
||||
},
|
||||
{
|
||||
name: "valid-changes",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
Iterations: 3 * 2,
|
||||
Memory: 32 * 1024,
|
||||
Threads: 10,
|
||||
SaltLength: 16,
|
||||
KeyLength: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid-iterations",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
Iterations: 0,
|
||||
Memory: 1,
|
||||
Threads: 1,
|
||||
SaltLength: 1,
|
||||
KeyLength: 1,
|
||||
},
|
||||
},
|
||||
want: ErrInvalidConfiguration,
|
||||
},
|
||||
{
|
||||
name: "invalid-memory",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
Iterations: 1,
|
||||
Memory: 0,
|
||||
Threads: 1,
|
||||
SaltLength: 1,
|
||||
KeyLength: 1,
|
||||
},
|
||||
},
|
||||
want: ErrInvalidConfiguration,
|
||||
},
|
||||
{
|
||||
name: "invalid-threads",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
Iterations: 1,
|
||||
Memory: 1,
|
||||
Threads: 0,
|
||||
SaltLength: 1,
|
||||
KeyLength: 1,
|
||||
},
|
||||
},
|
||||
want: ErrInvalidConfiguration,
|
||||
},
|
||||
{
|
||||
name: "invalid-salt-length",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
Iterations: 1,
|
||||
Memory: 1,
|
||||
Threads: 1,
|
||||
SaltLength: 0,
|
||||
KeyLength: 1,
|
||||
},
|
||||
},
|
||||
want: ErrInvalidConfiguration,
|
||||
},
|
||||
{
|
||||
name: "invalid-key-length",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
Iterations: 1,
|
||||
Memory: 1,
|
||||
Threads: 1,
|
||||
SaltLength: 1,
|
||||
KeyLength: 0,
|
||||
},
|
||||
},
|
||||
want: ErrInvalidConfiguration,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got := tt.in.validate()
|
||||
if tt.want == nil {
|
||||
assert.NoErrorf(got, "valid argon2 configuration: %+v", tt.in)
|
||||
return
|
||||
}
|
||||
require.Error(got)
|
||||
assert.Truef(errors.Is(got, tt.want), "want err: %q got: %q", tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package password
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrUnsupportedConfiguration results from attempting to perform an
|
||||
// operation that sets a password configuration to an unsupported type.
|
||||
ErrUnsupportedConfiguration = errors.New("unsupported configuration")
|
||||
|
||||
// ErrInvalidConfiguration results from attempting to perform an
|
||||
// operation that sets a password configuration with invalid settings.
|
||||
ErrInvalidConfiguration = errors.New("invalid configuration")
|
||||
)
|
||||
@ -0,0 +1,20 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
)
|
||||
|
||||
// Prefixes for private ids for types in the password package.
|
||||
const (
|
||||
argon2ConfigurationPrefix = "arg2conf"
|
||||
)
|
||||
|
||||
func newArgon2ConfigurationId() (string, error) {
|
||||
id, err := db.NewPrivateId(argon2ConfigurationPrefix)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new password argon2 configuration id: %w", err)
|
||||
}
|
||||
return id, err
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_PrivateIds(t *testing.T) {
|
||||
t.Run("argon2Config", func(t *testing.T) {
|
||||
id, err := newArgon2ConfigurationId()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasPrefix(id, argon2ConfigurationPrefix+"_"))
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,134 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/auth/password/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/hashicorp/watchtower/internal/oplog"
|
||||
)
|
||||
|
||||
// A Configuration is an interface holding one of the configuration types
|
||||
// for a specific key derivation function. Argon2Configuration is currently
|
||||
// the only configuration type.
|
||||
type Configuration interface {
|
||||
AuthMethodId() string
|
||||
validate() error
|
||||
}
|
||||
|
||||
// GetConfiguration returns the current configuration for authMethodId.
|
||||
func (r *Repository) GetConfiguration(ctx context.Context, authMethodId string) (Configuration, error) {
|
||||
if authMethodId == "" {
|
||||
return nil, fmt.Errorf("get password configuration: no auth method id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
cc, err := r.currentConfig(ctx, authMethodId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get password configuration: %w", err)
|
||||
}
|
||||
return cc.argon2(), nil
|
||||
}
|
||||
|
||||
// SetConfiguration sets the configuration for c.AuthMethodId to c and
|
||||
// returns a new Configuration. c is not changed. c must contain a valid
|
||||
// AuthMethodId. c.PrivateId is ignored.
|
||||
//
|
||||
// If c contains new settings for c.AuthMethodId, SetConfiguration inserts
|
||||
// c into the repository and updates AuthMethod to use the new
|
||||
// configuration. If c contains settings equal to the current configuration
|
||||
// for c.AuthMethodId, SetConfiguration ignores c. If c contains settings
|
||||
// equal to a previous configuration for c.AuthMethodId, SetConfiguration
|
||||
// updates AuthMethod to use the previous configuration.
|
||||
func (r *Repository) SetConfiguration(ctx context.Context, c Configuration) (Configuration, error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("set password configuration: %w", db.ErrNilParameter)
|
||||
}
|
||||
if c.AuthMethodId() == "" {
|
||||
return nil, fmt.Errorf("set password configuration: no auth method id: %w", db.ErrInvalidParameter)
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, fmt.Errorf("set password configuration: %w", err)
|
||||
}
|
||||
|
||||
switch v := c.(type) {
|
||||
case *Argon2Configuration:
|
||||
out, err := r.setArgon2Conf(ctx, v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("set password configuration: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("set password configuration: %w", ErrUnsupportedConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) setArgon2Conf(ctx context.Context, c *Argon2Configuration) (*Argon2Configuration, error) {
|
||||
c = c.clone()
|
||||
|
||||
id, err := newArgon2ConfigurationId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.PrivateId = id
|
||||
|
||||
a := &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
PublicId: c.PasswordMethodId,
|
||||
},
|
||||
}
|
||||
|
||||
newArgon2Conf := &Argon2Configuration{Argon2Configuration: &store.Argon2Configuration{}}
|
||||
|
||||
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
||||
func(rr db.Reader, w db.Writer) error {
|
||||
where, args := c.whereDup()
|
||||
if err := rr.LookupWhere(ctx, newArgon2Conf, where, args...); err != nil {
|
||||
if err != db.ErrRecordNotFound {
|
||||
return err
|
||||
}
|
||||
newArgon2Conf = c.clone()
|
||||
if err := w.Create(ctx, newArgon2Conf, db.WithOplog(r.wrapper, c.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
a.PasswordConfId = newArgon2Conf.PrivateId
|
||||
rowsUpdated, err := w.Update(ctx, a, []string{"PasswordConfId"}, nil, db.WithOplog(r.wrapper, a.oplog(oplog.OpType_OP_TYPE_UPDATE)))
|
||||
if err == nil && rowsUpdated > 1 {
|
||||
return db.ErrMultipleRecords
|
||||
}
|
||||
return err
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newArgon2Conf, nil
|
||||
}
|
||||
|
||||
type currentConfig struct {
|
||||
ConfType string
|
||||
MinUserNameLength int
|
||||
MinPasswordLength int
|
||||
|
||||
*Argon2Configuration
|
||||
}
|
||||
|
||||
func (c *currentConfig) TableName() string {
|
||||
return "auth_password_current_conf"
|
||||
}
|
||||
|
||||
func (r *Repository) currentConfig(ctx context.Context, authMethodId string) (*currentConfig, error) {
|
||||
var cc currentConfig
|
||||
if err := r.reader.LookupWhere(ctx, &cc, "password_method_id = ?", authMethodId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cc, nil
|
||||
}
|
||||
|
||||
func (c *currentConfig) argon2() *Argon2Configuration {
|
||||
if c.ConfType != "argon2" {
|
||||
return nil
|
||||
}
|
||||
return c.Argon2Configuration
|
||||
}
|
||||
@ -0,0 +1,309 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/watchtower/internal/auth/password/store"
|
||||
"github.com/hashicorp/watchtower/internal/db"
|
||||
"github.com/hashicorp/watchtower/internal/oplog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepository_GetSetConfiguration(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
|
||||
authMethods := testAuthMethods(t, conn, 1)
|
||||
authMethod := authMethods[0]
|
||||
authMethodId := authMethod.GetPublicId()
|
||||
ctx := context.Background()
|
||||
|
||||
// The order of these tests are important. Some tests have a dependency
|
||||
// on prior tests.
|
||||
|
||||
var original string // original configuration ID
|
||||
|
||||
t.Run("has-default-configuration", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := repo.GetConfiguration(ctx, authMethodId)
|
||||
assert.NoError(err)
|
||||
require.NotNil(got)
|
||||
|
||||
conf, ok := got.(*Argon2Configuration)
|
||||
require.True(ok, "want *Argon2Configuration")
|
||||
|
||||
require.NotEmpty(conf.PrivateId, "default configuration PrivateId")
|
||||
original = conf.PrivateId
|
||||
|
||||
want := NewArgon2Configuration()
|
||||
want.PrivateId = original
|
||||
want.CreateTime = conf.CreateTime
|
||||
want.PasswordMethodId = authMethodId
|
||||
require.Equal(want, got)
|
||||
})
|
||||
t.Run("change-configuration", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.NotEmpty(original, "Original ID")
|
||||
|
||||
current, err := repo.GetConfiguration(ctx, authMethodId)
|
||||
assert.NoError(err)
|
||||
require.NotNil(current)
|
||||
|
||||
currentConf, ok := current.(*Argon2Configuration)
|
||||
require.True(ok, "want *Argon2Configuration")
|
||||
assert.Equal(original, currentConf.PrivateId)
|
||||
|
||||
newConf := NewArgon2Configuration()
|
||||
assert.Empty(newConf.PrivateId)
|
||||
newConf.PasswordMethodId = currentConf.PasswordMethodId
|
||||
newConf.Memory = currentConf.Memory * 2
|
||||
|
||||
updated, err := repo.SetConfiguration(ctx, newConf)
|
||||
assert.NoError(err)
|
||||
require.NotNil(updated)
|
||||
assert.NotSame(newConf, updated)
|
||||
|
||||
updatedConf, ok := updated.(*Argon2Configuration)
|
||||
require.True(ok, "want *Argon2Configuration")
|
||||
|
||||
assert.NotSame(newConf, updatedConf)
|
||||
assert.NotEmpty(updatedConf.PrivateId, "updatedConf.PrivateId")
|
||||
assert.NotEqual(original, updatedConf.PrivateId)
|
||||
|
||||
current2, err := repo.GetConfiguration(ctx, authMethodId)
|
||||
assert.NoError(err)
|
||||
require.NotNil(current2)
|
||||
|
||||
current2Conf, ok := current2.(*Argon2Configuration)
|
||||
require.True(ok, "want *Argon2Configuration")
|
||||
assert.Equal(updatedConf.PrivateId, current2Conf.PrivateId)
|
||||
assert.Equal(newConf.Memory, current2Conf.Memory, "changed setting")
|
||||
})
|
||||
t.Run("change-to-old-configuration", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.NotEmpty(original, "Original ID")
|
||||
|
||||
newConf := NewArgon2Configuration()
|
||||
newConf.PasswordMethodId = authMethodId
|
||||
assert.Empty(newConf.PrivateId)
|
||||
|
||||
updated, err := repo.SetConfiguration(ctx, newConf)
|
||||
assert.NoError(err)
|
||||
require.NotNil(updated)
|
||||
assert.NotSame(newConf, updated)
|
||||
|
||||
updatedConf, ok := updated.(*Argon2Configuration)
|
||||
require.True(ok, "want *Argon2Configuration")
|
||||
|
||||
assert.NotSame(newConf, updatedConf)
|
||||
assert.NotEmpty(updatedConf.PrivateId, "updatedConf.PrivateId")
|
||||
assert.Equal(original, updatedConf.PrivateId)
|
||||
|
||||
current, err := repo.GetConfiguration(ctx, authMethodId)
|
||||
assert.NoError(err)
|
||||
require.NotNil(current)
|
||||
|
||||
currentConf, ok := current.(*Argon2Configuration)
|
||||
require.True(ok, "want *Argon2Configuration")
|
||||
assert.Equal(updatedConf.PrivateId, currentConf.PrivateId)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepository_GetConfiguration(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
|
||||
authMethods := testAuthMethods(t, conn, 1)
|
||||
authMethod := authMethods[0]
|
||||
authMethodId := authMethod.GetPublicId()
|
||||
ctx := context.Background()
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
authMethodId string
|
||||
want *Argon2Configuration
|
||||
wantIsErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid-no-authMethodId",
|
||||
authMethodId: "",
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "invalid-authMethodId",
|
||||
authMethodId: "abcdefghijk",
|
||||
wantIsErr: db.ErrRecordNotFound,
|
||||
},
|
||||
{
|
||||
name: "valid-authMethodId",
|
||||
authMethodId: authMethodId,
|
||||
want: NewArgon2Configuration(),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := repo.GetConfiguration(ctx, tt.authMethodId)
|
||||
if tt.wantIsErr != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
|
||||
assert.Nil(got, "returned configuration")
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
gotConf, ok := got.(*Argon2Configuration)
|
||||
require.True(ok, "want *Argon2Configuration")
|
||||
|
||||
tt.want.PasswordMethodId = tt.authMethodId
|
||||
|
||||
assert.Equal(tt.authMethodId, gotConf.AuthMethodId(), "authMethodId")
|
||||
|
||||
assert.Equal(tt.want.PasswordMethodId, gotConf.PasswordMethodId)
|
||||
assert.Equal(tt.want.Iterations, gotConf.Iterations)
|
||||
assert.Equal(tt.want.Memory, gotConf.Memory)
|
||||
assert.Equal(tt.want.Threads, gotConf.Threads)
|
||||
assert.Equal(tt.want.SaltLength, gotConf.SaltLength)
|
||||
assert.Equal(tt.want.KeyLength, gotConf.KeyLength)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type tconf int
|
||||
func (t tconf) AuthMethodId() string { return "abcdefghijk" }
|
||||
func (t tconf) validate() error { return nil }
|
||||
var _ Configuration = tconf(0)
|
||||
|
||||
func TestRepository_SetConfiguration(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
repo, err := NewRepository(rw, rw, wrapper)
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
|
||||
authMethods := testAuthMethods(t, conn, 1)
|
||||
authMethod := authMethods[0]
|
||||
authMethodId := authMethod.GetPublicId()
|
||||
|
||||
var tests = []struct {
|
||||
name string
|
||||
in Configuration
|
||||
want *Argon2Configuration
|
||||
wantUnknownErr bool
|
||||
wantIsErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid-nil-config",
|
||||
wantIsErr: db.ErrNilParameter,
|
||||
},
|
||||
{
|
||||
name: "nil-embedded-config",
|
||||
in: &Argon2Configuration{},
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "invalid-no-authMethodId",
|
||||
in: NewArgon2Configuration(),
|
||||
wantIsErr: db.ErrInvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "unknown-configuration-type",
|
||||
in: tconf(0),
|
||||
wantIsErr: ErrUnsupportedConfiguration,
|
||||
},
|
||||
{
|
||||
name: "invalid-unknown-authMethodId",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
PasswordMethodId: "abcdefghijk",
|
||||
Iterations: 3 * 2,
|
||||
Memory: 64 * 1024,
|
||||
Threads: 1,
|
||||
SaltLength: 32,
|
||||
KeyLength: 32,
|
||||
},
|
||||
},
|
||||
wantUnknownErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid-config-setting",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
PasswordMethodId: authMethodId,
|
||||
Iterations: 0,
|
||||
Memory: 64 * 1024,
|
||||
Threads: 1,
|
||||
SaltLength: 32,
|
||||
KeyLength: 32,
|
||||
},
|
||||
},
|
||||
wantIsErr: ErrInvalidConfiguration,
|
||||
},
|
||||
{
|
||||
name: "valid",
|
||||
in: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
PasswordMethodId: authMethodId,
|
||||
Iterations: 3 * 2,
|
||||
Memory: 64 * 1024,
|
||||
Threads: 1,
|
||||
SaltLength: 32,
|
||||
KeyLength: 32,
|
||||
},
|
||||
},
|
||||
want: &Argon2Configuration{
|
||||
Argon2Configuration: &store.Argon2Configuration{
|
||||
PasswordMethodId: authMethodId,
|
||||
Iterations: 3 * 2,
|
||||
Memory: 64 * 1024,
|
||||
Threads: 1,
|
||||
SaltLength: 32,
|
||||
KeyLength: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := repo.SetConfiguration(context.Background(), tt.in)
|
||||
if tt.wantIsErr != nil {
|
||||
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
|
||||
assert.Nil(got, "returned configuration")
|
||||
return
|
||||
}
|
||||
if tt.wantUnknownErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
assert.NotSame(tt.in, got)
|
||||
|
||||
gotConf, ok := got.(*Argon2Configuration)
|
||||
require.True(ok, "want *Argon2Configuration")
|
||||
|
||||
assert.Equal(tt.want.PasswordMethodId, gotConf.PasswordMethodId)
|
||||
assert.Equal(tt.want.Iterations, gotConf.Iterations)
|
||||
assert.Equal(tt.want.Memory, gotConf.Memory)
|
||||
assert.Equal(tt.want.Threads, gotConf.Threads)
|
||||
assert.Equal(tt.want.SaltLength, gotConf.SaltLength)
|
||||
assert.Equal(tt.want.KeyLength, gotConf.KeyLength)
|
||||
|
||||
assert.NoError(db.TestVerifyOplog(t, rw, gotConf.PrivateId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,264 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.25.0
|
||||
// protoc v3.12.3
|
||||
// source: controller/storage/auth/password/store/v1/argon2.proto
|
||||
|
||||
// Package store provides protobufs for storing types in the password package.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
proto "github.com/golang/protobuf/proto"
|
||||
timestamp "github.com/hashicorp/watchtower/internal/db/timestamp"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// This is a compile-time assertion that a sufficiently up-to-date version
|
||||
// of the legacy proto package is being used.
|
||||
const _ = proto.ProtoPackageIsVersion4
|
||||
|
||||
// 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 {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// @inject_tag: `gorm:"primary_key"`
|
||||
PrivateId string `protobuf:"bytes,1,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty" gorm:"primary_key"`
|
||||
// The create_time is set by the database.
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"`
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
PasswordMethodId string `protobuf:"bytes,3,opt,name=password_method_id,json=passwordMethodId,proto3" json:"password_method_id,omitempty" gorm:"not_null"`
|
||||
// Iterations is the time parameter in the Argon2 specification. It
|
||||
// specifies the number of passes over the memory. Must be > 0.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
Iterations uint32 `protobuf:"varint,4,opt,name=iterations,proto3" json:"iterations,omitempty" gorm:"default:null"`
|
||||
// Memory is the memory parameter in the Argon2 specification. It
|
||||
// specifies the size of the memory in KiB. For example Memory=32*1024
|
||||
// sets the memory cost to ~32 MB. Must be > 0.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
Memory uint32 `protobuf:"varint,5,opt,name=memory,proto3" json:"memory,omitempty" gorm:"default:null"`
|
||||
// Threads is the threads parameter in the Argon2 specification. It can
|
||||
// be adjusted to the number of available CPUs. Must be > 0.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
Threads uint32 `protobuf:"varint,6,opt,name=threads,proto3" json:"threads,omitempty" gorm:"default:null"`
|
||||
// SaltLength is in bytes. Must be >= 16.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
SaltLength uint32 `protobuf:"varint,7,opt,name=salt_length,json=saltLength,proto3" json:"salt_length,omitempty" gorm:"default:null"`
|
||||
// KeyLength is in bytes. Must be >= 16.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
KeyLength uint32 `protobuf:"varint,8,opt,name=key_length,json=keyLength,proto3" json:"key_length,omitempty" gorm:"default:null"`
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) Reset() {
|
||||
*x = Argon2Configuration{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Argon2Configuration) ProtoMessage() {}
|
||||
|
||||
func (x *Argon2Configuration) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Argon2Configuration.ProtoReflect.Descriptor instead.
|
||||
func (*Argon2Configuration) Descriptor() ([]byte, []int) {
|
||||
return file_controller_storage_auth_password_store_v1_argon2_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) GetPrivateId() string {
|
||||
if x != nil {
|
||||
return x.PrivateId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) GetCreateTime() *timestamp.Timestamp {
|
||||
if x != nil {
|
||||
return x.CreateTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) GetPasswordMethodId() string {
|
||||
if x != nil {
|
||||
return x.PasswordMethodId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) GetIterations() uint32 {
|
||||
if x != nil {
|
||||
return x.Iterations
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) GetMemory() uint32 {
|
||||
if x != nil {
|
||||
return x.Memory
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) GetThreads() uint32 {
|
||||
if x != nil {
|
||||
return x.Threads
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) GetSaltLength() uint32 {
|
||||
if x != nil {
|
||||
return x.SaltLength
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Argon2Configuration) GetKeyLength() uint32 {
|
||||
if x != nil {
|
||||
return x.KeyLength
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_controller_storage_auth_password_store_v1_argon2_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc = []byte{
|
||||
0x0a, 0x36, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x61, 0x67, 0x65, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
|
||||
0x72, 0x64, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x72, 0x67, 0x6f,
|
||||
0x6e, 0x32, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x29, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f,
|
||||
0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x61, 0x75, 0x74,
|
||||
0x68, 0x2e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65,
|
||||
0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f,
|
||||
0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||
0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc1, 0x02, 0x0a, 0x13, 0x41, 0x72, 0x67, 0x6f, 0x6e, 0x32, 0x43,
|
||||
0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a,
|
||||
0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63,
|
||||
0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74,
|
||||
0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e,
|
||||
0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72,
|
||||
0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73,
|
||||
0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4d, 0x65,
|
||||
0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x74, 0x65, 0x72, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x69, 0x74, 0x65, 0x72,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79,
|
||||
0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x12, 0x18,
|
||||
0x0a, 0x07, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52,
|
||||
0x07, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x61, 0x6c, 0x74,
|
||||
0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73,
|
||||
0x61, 0x6c, 0x74, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x6b, 0x65, 0x79,
|
||||
0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x6b,
|
||||
0x65, 0x79, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x44, 0x5a, 0x42, 0x67, 0x69, 0x74, 0x68,
|
||||
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70,
|
||||
0x2f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x74, 0x6f, 0x77, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65,
|
||||
0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
|
||||
0x72, 0x64, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_rawDescOnce sync.Once
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData = file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_controller_storage_auth_password_store_v1_argon2_proto_rawDescGZIP() []byte {
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_rawDescOnce.Do(func() {
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData)
|
||||
})
|
||||
return file_controller_storage_auth_password_store_v1_argon2_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_goTypes = []interface{}{
|
||||
(*Argon2Configuration)(nil), // 0: controller.storage.auth.password.store.v1.Argon2Configuration
|
||||
(*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp
|
||||
}
|
||||
var file_controller_storage_auth_password_store_v1_argon2_proto_depIdxs = []int32{
|
||||
1, // 0: controller.storage.auth.password.store.v1.Argon2Configuration.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_controller_storage_auth_password_store_v1_argon2_proto_init() }
|
||||
func file_controller_storage_auth_password_store_v1_argon2_proto_init() {
|
||||
if File_controller_storage_auth_password_store_v1_argon2_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Argon2Configuration); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_controller_storage_auth_password_store_v1_argon2_proto_goTypes,
|
||||
DependencyIndexes: file_controller_storage_auth_password_store_v1_argon2_proto_depIdxs,
|
||||
MessageInfos: file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes,
|
||||
}.Build()
|
||||
File_controller_storage_auth_password_store_v1_argon2_proto = out.File
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc = nil
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_goTypes = nil
|
||||
file_controller_storage_auth_password_store_v1_argon2_proto_depIdxs = nil
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
begin;
|
||||
|
||||
drop table auth_password_argon2_conf;
|
||||
|
||||
commit;
|
||||
@ -0,0 +1,75 @@
|
||||
begin;
|
||||
|
||||
create table auth_password_argon2_conf (
|
||||
private_id wt_private_id primary key
|
||||
references auth_password_conf (private_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
password_method_id wt_public_id not null,
|
||||
create_time wt_timestamp,
|
||||
iterations int not null default 3
|
||||
check(iterations > 0),
|
||||
memory int not null default 65536
|
||||
check(memory > 0),
|
||||
threads int not null default 1
|
||||
check(threads > 0),
|
||||
-- salt_length unit is bytes
|
||||
salt_length int not null default 32
|
||||
-- minimum of 16 bytes (128 bits)
|
||||
check(salt_length >= 16),
|
||||
-- key_length unit is bytes
|
||||
key_length int not null default 32
|
||||
-- minimum of 16 bytes (128 bits)
|
||||
check(key_length >= 16),
|
||||
unique(password_method_id, iterations, memory, threads, salt_length, key_length),
|
||||
unique (password_method_id, private_id),
|
||||
foreign key (password_method_id, private_id)
|
||||
references auth_password_conf (password_method_id, private_id)
|
||||
on delete cascade
|
||||
on update cascade
|
||||
deferrable initially deferred
|
||||
);
|
||||
create or replace function
|
||||
read_only_auth_password_argon2_conf()
|
||||
returns trigger
|
||||
as $$
|
||||
begin
|
||||
raise exception 'auth_password_argon2_conf is read-only';
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger
|
||||
read_only_auth_password_argon2_conf
|
||||
before
|
||||
update on auth_password_argon2_conf
|
||||
for each row execute procedure read_only_auth_password_argon2_conf();
|
||||
|
||||
create trigger
|
||||
insert_auth_password_conf_subtype
|
||||
before insert on auth_password_argon2_conf
|
||||
for each row execute procedure insert_auth_password_conf_subtype();
|
||||
|
||||
--
|
||||
-- triggers for time columns
|
||||
--
|
||||
create trigger
|
||||
immutable_create_time
|
||||
before
|
||||
update on auth_password_argon2_conf
|
||||
for each row execute procedure immutable_create_time_func();
|
||||
|
||||
create trigger
|
||||
default_create_time_column
|
||||
before
|
||||
insert on auth_password_argon2_conf
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
-- The tickets for oplog are the subtypes not the base types because no updates
|
||||
-- are done to any values in the base types.
|
||||
insert into oplog_ticket
|
||||
(name, version)
|
||||
values
|
||||
('auth_password_argon2_conf', 1),
|
||||
('auth_password_argon2_cred', 1);
|
||||
|
||||
commit;
|
||||
@ -0,0 +1,6 @@
|
||||
begin;
|
||||
|
||||
drop view auth_password_current_conf;
|
||||
drop view auth_password_conf_union;
|
||||
|
||||
commit;
|
||||
@ -0,0 +1,25 @@
|
||||
begin;
|
||||
|
||||
-- auth_password_conf_union is a union of the configuration settings
|
||||
-- of all supported key derivation functions.
|
||||
-- It will be updated as new key derivation functions are supported.
|
||||
create or replace view auth_password_conf_union as
|
||||
-- Do not change the order of the columns when adding new configurations.
|
||||
-- Union with new tables appending new columns as needed.
|
||||
select c.password_method_id, c.private_id as password_conf_id, c.private_id,
|
||||
'argon2' as conf_type,
|
||||
c.iterations, c.memory, c.threads, c.salt_length, c.key_length
|
||||
from auth_password_argon2_conf c;
|
||||
|
||||
-- auth_password_current_conf provides a view of the current password
|
||||
-- configuration for each password auth method.
|
||||
-- The view will be updated as new key derivation functions are supported
|
||||
-- but the query to create the view should not need to be updated.
|
||||
create or replace view auth_password_current_conf as
|
||||
-- Rerun this query whenever auth_password_conf_union is updated.
|
||||
select pm.min_user_name_length, pm.min_password_length, c.*
|
||||
from auth_password_method pm
|
||||
inner join auth_password_conf_union c
|
||||
on pm.password_conf_id = c.password_conf_id;
|
||||
|
||||
commit;
|
||||
@ -0,0 +1,52 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// Package store provides protobufs for storing types in the password package.
|
||||
package controller.storage.auth.password.store.v1;
|
||||
option go_package = "github.com/hashicorp/watchtower/internal/auth/password/store;store";
|
||||
|
||||
import "controller/storage/timestamp/v1/timestamp.proto";
|
||||
|
||||
// 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
|
||||
message Argon2Configuration {
|
||||
// @inject_tag: `gorm:"primary_key"`
|
||||
string private_id = 1;
|
||||
|
||||
// The create_time is set by the database.
|
||||
// @inject_tag: `gorm:"default:current_timestamp"`
|
||||
timestamp.v1.Timestamp create_time = 2;
|
||||
|
||||
// @inject_tag: `gorm:"not_null"`
|
||||
string password_method_id = 3;
|
||||
|
||||
// Iterations is the time parameter in the Argon2 specification. It
|
||||
// specifies the number of passes over the memory. Must be > 0.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
uint32 iterations = 4;
|
||||
|
||||
// Memory is the memory parameter in the Argon2 specification. It
|
||||
// specifies the size of the memory in KiB. For example Memory=32*1024
|
||||
// sets the memory cost to ~32 MB. Must be > 0.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
uint32 memory = 5;
|
||||
|
||||
// Threads is the threads parameter in the Argon2 specification. It can
|
||||
// be adjusted to the number of available CPUs. Must be > 0.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
uint32 threads = 6;
|
||||
|
||||
// SaltLength is in bytes. Must be >= 16.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
uint32 salt_length = 7;
|
||||
|
||||
// KeyLength is in bytes. Must be >= 16.
|
||||
// @inject_tag: `gorm:"default:null"`
|
||||
uint32 key_length = 8;
|
||||
}
|
||||
Loading…
Reference in new issue