Add password authentication using Argon2 (#227)

* Add argon2 credentials

* Verify oplog in tests

* Refactor: rename field

* Update internal/auth/password/repository_account.go

Co-authored-by: Todd Knight <T.Alan.Knight@gmail.com>

* Add inline comments to SQL query to doc mapping to structure

* Refactor: move authentication query to query.go file

* Replace immutable_create_time_func with immutable_columns

Co-authored-by: Todd Knight <T.Alan.Knight@gmail.com>
This commit is contained in:
Michael GaffneyandTodd Knight authored and GitHub committed 2020-07-31 11:18:41 -04:00
1 parent 87492816cb
commit 6c679a9939
18 files changed
+930 -18

No files matched your search

+3 -3
View File
@@ -14,9 +14,9 @@ type Account struct {
*store.Account
tableName string
// CredentialID is included when Authenticate or ChangePassword is
// called. A new CredentialID is generated when a password is changed.
CredentialID string `gorm:"-"`
// CredentialId is included when Authenticate or ChangePassword is
// called. A new CredentialId is generated when a password is changed.
CredentialId string `gorm:"-"`
}
func allocAccount() Account {
+94
View File
@@ -1,10 +1,17 @@
package password
import (
"context"
"crypto/rand"
"fmt"
"strings"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-kms-wrapping/structwrapping"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/oplog"
"golang.org/x/crypto/argon2"
"google.golang.org/protobuf/proto"
)
@@ -100,3 +107,90 @@ func (c *Argon2Configuration) whereDup() (string, []interface{}) {
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(accountId string, password string, conf *Argon2Configuration) (*Argon2Credential, error) {
if accountId == "" {
return nil, fmt.Errorf("new: password argon2 credential: no accountId: %w", db.ErrInvalidParameter)
}
if password == "" {
return nil, fmt.Errorf("new: password argon2 credential: no password: %w", db.ErrInvalidParameter)
}
if conf == nil {
return nil, fmt.Errorf("new: password argon2 credential: no argon2 configuration: %w", db.ErrNilParameter)
}
id, err := newArgon2CredentialId()
if err != nil {
return nil, fmt.Errorf("new: password argon2 credential: %w", err)
}
c := &Argon2Credential{
Argon2Credential: &store.Argon2Credential{
PrivateId: id,
PasswordAccountId: accountId,
PasswordConfId: conf.PrivateId,
},
}
salt := make([]byte, conf.SaltLength)
if _, err := rand.Read(salt); err != nil {
return nil, fmt.Errorf("new: password argon2 credential: %w", err)
}
c.Salt = salt
c.DerivedKey = argon2.IDKey([]byte(password), c.Salt, conf.Iterations, conf.Memory, uint8(conf.Threads), conf.KeyLength)
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 {
if err := structwrapping.WrapStruct(ctx, cipher, c.Argon2Credential, nil); err != nil {
return fmt.Errorf("error encrypting argon2 credential: %w", err)
}
return nil
}
func (c *Argon2Credential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
if err := structwrapping.UnwrapStruct(ctx, cipher, c.Argon2Credential, nil); err != nil {
return fmt.Errorf("error decrypting argon2 credential: %w", err)
}
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
}
+155
View File
@@ -7,6 +7,7 @@ import (
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -268,3 +269,157 @@ func TestArgon2Configuration_Validate(t *testing.T) {
})
}
}
func testArgon2Confs(t *testing.T, conn *gorm.DB, authMethodId string, count int) []*Argon2Configuration {
t.Helper()
assert, require := assert.New(t), require.New(t)
rw := db.New(conn)
var confs []*Argon2Configuration
err := rw.SearchWhere(context.Background(), &confs, "password_method_id = ?", []interface{}{authMethodId})
require.NoError(err)
assert.Equal(1, len(confs))
base := confs[0]
for i := 0; i < count; i++ {
conf := NewArgon2Configuration()
require.NotNil(conf)
conf.PasswordMethodId = authMethodId
conf.PrivateId, err = newArgon2ConfigurationId()
require.NoError(err)
conf.Iterations = base.Iterations + uint32(i+1)
conf.Threads = base.Threads + uint32(i+1)
err = rw.Create(context.Background(), conf)
require.NoError(err)
confs = append(confs, conf)
}
return confs
}
func TestArgon2Credential_New(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
auts := testAuthMethods(t, conn, 1)
aut := auts[0]
accts := testAccounts(t, conn, aut.ScopeId, aut.PublicId, 5)
confs := testArgon2Confs(t, conn, accts[0].AuthMethodId, 1)
type args struct {
accountId string
password string
conf *Argon2Configuration
}
var tests = []struct {
name string
args args
want *Argon2Credential
wantIsErr error
}{
{
name: "blank-accountId",
args: args{
accountId: "",
password: "foobarcity",
conf: confs[0],
},
want: nil,
wantIsErr: db.ErrInvalidParameter,
},
{
name: "blank-password",
args: args{
accountId: accts[0].PublicId,
password: "",
conf: confs[0],
},
want: nil,
wantIsErr: db.ErrInvalidParameter,
},
{
name: "nil-configuration",
args: args{
accountId: accts[0].PublicId,
password: "foobarcity",
conf: nil,
},
want: nil,
wantIsErr: db.ErrNilParameter,
},
{
name: "valid-password",
args: args{
accountId: accts[0].PublicId,
password: "foobarcity",
conf: confs[0],
},
want: &Argon2Credential{
Argon2Credential: &store.Argon2Credential{
PasswordAccountId: accts[0].PublicId,
PasswordConfId: confs[0].PrivateId,
},
},
},
{
name: "traditional-chinese-characters-password",
args: args{
accountId: accts[1].PublicId,
password: "漢字𫝆𫝑𫝜𫝓𫝶𫝼𫞉𫞔𫞩𫞬",
conf: confs[0],
},
want: &Argon2Credential{
Argon2Credential: &store.Argon2Credential{
PasswordAccountId: accts[1].PublicId,
PasswordConfId: confs[0].PrivateId,
},
},
},
{
name: "emoji-password",
args: args{
accountId: accts[2].PublicId,
password: "😃 😀 😅 😘🤔 😑🤢🤠😰",
conf: confs[0],
},
want: &Argon2Credential{
Argon2Credential: &store.Argon2Credential{
PasswordAccountId: accts[2].PublicId,
PasswordConfId: confs[0].PrivateId,
},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := newArgon2Credential(tt.args.accountId, tt.args.password, tt.args.conf)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(got)
return
}
require.NoError(err)
require.NotNil(got)
got.encrypt(context.Background(), wrapper)
err = rw.Create(context.Background(), got)
assert.NoError(err)
if tt.want == nil {
return
}
gotCred := &Argon2Credential{
Argon2Credential: &store.Argon2Credential{
PrivateId: got.PrivateId,
},
}
err = rw.LookupById(context.Background(), gotCred)
require.NoError(err)
assert.Equal(tt.want.PasswordAccountId, gotCred.PasswordAccountId)
assert.Equal(tt.want.PasswordConfId, gotCred.PasswordConfId)
})
}
}
+4
View File
@@ -3,6 +3,10 @@ package password
import "errors"
var (
// ErrTooShort results from attempting to set a password which is to
// short.
ErrTooShort = errors.New("password to short")
// ErrUnsupportedConfiguration results from attempting to perform an
// operation that sets a password configuration to an unsupported type.
ErrUnsupportedConfiguration = errors.New("unsupported configuration")
+10
View File
@@ -18,6 +18,8 @@ type options struct {
withDescription string
withLimit int
withConfig Configuration
password string
withPassword bool
}
func getDefaultOptions() options {
@@ -49,6 +51,14 @@ func WithLimit(l int) Option {
}
}
// WithPassword provides an optional password.
func WithPassword(password string) Option {
return func(o *options) {
o.password = password
o.withPassword = true
}
}
// WithConfiguration provides an optional configuration.
func WithConfiguration(config Configuration) Option {
return func(o *options) {
+7
View File
@@ -27,6 +27,13 @@ func Test_GetOpts(t *testing.T) {
testOpts.withLimit = 5
assert.Equal(t, opts, testOpts)
})
t.Run("WithPassword", func(t *testing.T) {
opts := getOpts(WithPassword("test password"))
testOpts := getDefaultOptions()
testOpts.password = "test password"
testOpts.withPassword = true
assert.Equal(t, opts, testOpts)
})
t.Run("WithConfiguration", func(t *testing.T) {
conf := NewArgon2Configuration()
conf.KeyLength = conf.KeyLength * 2
+9
View File
@@ -9,6 +9,7 @@ import (
// Prefixes for private ids for types in the password package.
const (
argon2ConfigurationPrefix = "arg2conf"
argon2CredentialPrefix = "arg2cred"
)
func newArgon2ConfigurationId() (string, error) {
@@ -18,3 +19,11 @@ func newArgon2ConfigurationId() (string, error) {
}
return id, err
}
func newArgon2CredentialId() (string, error) {
id, err := db.NewPrivateId(argon2CredentialPrefix)
if err != nil {
return "", fmt.Errorf("new password argon2 configuration id: %w", err)
}
return id, err
}
@@ -14,4 +14,9 @@ func Test_PrivateIds(t *testing.T) {
require.NoError(t, err)
assert.True(t, strings.HasPrefix(id, argon2ConfigurationPrefix+"_"))
})
t.Run("argon2Cred", func(t *testing.T) {
id, err := newArgon2CredentialId()
require.NoError(t, err)
assert.True(t, strings.HasPrefix(id, argon2CredentialPrefix+"_"))
})
}
+29
View File
@@ -0,0 +1,29 @@
package password
const (
authenticateQuery = `
select acct.name, -- Account.Name
acct.description, -- Account.Description
acct.user_name, -- Account.UserName
acct.public_id, -- Account.PublicId
acct.auth_method_id, -- Account.AuthMethodId
acct.scope_id, -- Account.ScopeId
acct.create_time, -- Account.CreateTime
acct.update_time, -- Account.UpdateTime
cred.private_id as credential_id, -- Account.CredentialId
cred.password_conf_id, -- Argon2Credential.PasswordConfId
cred.salt, -- Argon2Credential.CtSalt/Salt
cred.derived_key, -- Argon2Credential.DerivedKey
conf.key_length, -- Argon2Configuration.KeyLength
conf.iterations, -- Argon2Configuration.Iterations
conf.memory, -- Argon2Configuration.Memory
conf.threads -- Argon2Configuration.Threads
from auth_password_argon2_cred cred,
auth_password_argon2_conf conf,
auth_password_account acct
where acct.auth_method_id = $1
and acct.user_name = $2
and cred.password_conf_id = conf.private_id
and cred.password_account_id = acct.public_id;
`
)
@@ -15,6 +15,8 @@ import (
// contain a valid AuthMethodId. a must not contain a PublicId. The PublicId is
// generated and assigned by this method.
//
// WithPassword is the only valid option. All other options are ignored.
//
// Both a.Name and a.Description are optional. If a.Name is set, it must be
// unique within a.AuthMethodId.
func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Option) (*Account, error) {
@@ -34,6 +36,15 @@ func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Optio
return nil, fmt.Errorf("create: password account: invalid user name: %w", db.ErrInvalidParameter)
}
cc, err := r.currentConfig(ctx, a.AuthMethodId)
if err != nil {
return nil, fmt.Errorf("create: password account: retrieve current configuration: %w", err)
}
if cc.MinUserNameLength > len(a.UserName) {
return nil, fmt.Errorf("create: password account: user name %q: %w", a.UserName, ErrTooShort)
}
a = a.clone()
id, err := newAccountId()
if err != nil {
@@ -41,6 +52,19 @@ func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Optio
}
a.PublicId = id
opts := getOpts(opt...)
var cred *Argon2Credential
if opts.withPassword {
if cc.MinPasswordLength > len(opts.password) {
return nil, fmt.Errorf("create: password account: password: %w", ErrTooShort)
}
if cred, err = newArgon2Credential(id, opts.password, cc.argon2()); err != nil {
return nil, fmt.Errorf("create: password account: %w", err)
}
}
var newCred *Argon2Credential
var newAccount *Account
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
@@ -49,6 +73,15 @@ func (r *Repository) CreateAccount(ctx context.Context, a *Account, opt ...Optio
return err
}
if cred != nil {
newCred = cred.clone()
if err := newCred.encrypt(ctx, r.wrapper); err != nil {
return err
}
if err := w.Create(ctx, newCred, db.WithOplog(r.wrapper, cred.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil {
return err
}
}
return nil
},
)
@@ -5,9 +5,11 @@ import (
"errors"
"fmt"
"testing"
"time"
"github.com/hashicorp/watchtower/internal/auth/password/store"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/oplog"
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -143,6 +145,29 @@ func TestRepository_CreateAccount(t *testing.T) {
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-username-to-short",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kaz",
},
},
wantIsErr: ErrTooShort,
},
{
name: "invalid-password-to-short",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kazmierczak123",
},
},
opts: []Option{
WithPassword("a"),
},
wantIsErr: ErrTooShort,
},
{
name: "valid-no-options",
in: &Account{
@@ -192,6 +217,24 @@ func TestRepository_CreateAccount(t *testing.T) {
},
},
},
{
name: "valid-with-password",
in: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kazmierczak3",
},
},
opts: []Option{
WithPassword("1234567890"),
},
want: &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kazmierczak3",
},
},
},
}
for _, tt := range tests {
@@ -215,6 +258,15 @@ func TestRepository_CreateAccount(t *testing.T) {
assert.Equal(tt.want.Name, got.Name)
assert.Equal(tt.want.Description, got.Description)
assert.Equal(got.CreateTime, got.UpdateTime)
assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
opts := getOpts(tt.opts...)
if opts.withPassword {
authAcct, err := repo.Authenticate(context.Background(), tt.in.AuthMethodId, tt.in.UserName, opts.password)
require.NoError(err)
assert.NoError(db.TestVerifyOplog(t, rw, authAcct.CredentialId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)))
}
})
}
@@ -0,0 +1,91 @@
package password
import (
"context"
"crypto/subtle"
"fmt"
"github.com/hashicorp/watchtower/internal/db"
"golang.org/x/crypto/argon2"
)
// Authenticate authenticates userName and password match for userName in
// authMethodId. The account for the userName is returned if authentication
// is successful. Returns nil if authentication fails.
//
// The CredentialId in the returned account represents a user's current
// password. A new CredentialId is generated when a user's password is
// changed and the old one is deleted.
//
// Authenticate will update the stored values for password to the current
// password settings for authMethodId if authentication is successful and
// the stored values are not using the current password settings.
func (r *Repository) Authenticate(ctx context.Context, authMethodId string, userName string, password string) (*Account, error) {
if authMethodId == "" {
return nil, fmt.Errorf("password authenticate: no authMethodId: %w", db.ErrInvalidParameter)
}
if userName == "" {
return nil, fmt.Errorf("password authenticate: no userName: %w", db.ErrInvalidParameter)
}
if password == "" {
return nil, fmt.Errorf("password authenticate: no password: %w", db.ErrInvalidParameter)
}
type AuthAccount struct {
*Account
*Argon2Credential
*Argon2Configuration
}
var accts []AuthAccount
tx, err := r.reader.DB()
if err != nil {
return nil, err
}
rows, err := tx.Query(authenticateQuery, authMethodId, userName)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var aa AuthAccount
if err := r.reader.ScanRows(rows, &aa); err != nil {
return nil, err
}
accts = append(accts, aa)
}
var acct AuthAccount
switch {
case len(accts) == 0:
return nil, nil
case len(accts) > 1:
// this should never happen
return nil, fmt.Errorf("authenticate: multiple accounts returned for user name")
default:
acct = accts[0]
}
if err := acct.decrypt(ctx, r.wrapper); err != nil {
return nil, fmt.Errorf("authenticate: credential: cannot decrypt value: %w", err)
}
inputKey := argon2.IDKey([]byte(password), acct.Salt, acct.Iterations, acct.Memory, uint8(acct.Threads), acct.KeyLength)
if subtle.ConstantTimeCompare(inputKey, acct.DerivedKey) == 0 {
return nil, nil
}
// TODO(mgaffney) 07/2020: update stored credential if acct config is
// not the current config for authMethodId
return acct.Account, nil
}
// ChangePassword updates the password for userName in authMethodId to new
// if old equals the stored password. The account for the userName is
// returned with a new CredentialId if password is successfully changed.
//
// Returns nil if old does not match the stored password for userName.
func (r *Repository) ChangePassword(ctx context.Context, authMethodId string, userName string, old, new string) (*Account, error) {
panic("not implemented")
}
@@ -0,0 +1,117 @@
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 TestRepository_Authenticate(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
authMethods := testAuthMethods(t, conn, 1)
authMethod := authMethods[0]
inAcct := &Account{
Account: &store.Account{
AuthMethodId: authMethod.PublicId,
UserName: "kazmierczak",
},
}
passwd := "12345678"
repo, err := NewRepository(rw, rw, wrapper)
assert.NoError(t, err)
require.NotNil(t, repo)
outAcct, err := repo.CreateAccount(context.Background(), inAcct, WithPassword(passwd))
assert.NoError(t, err)
require.NotNil(t, outAcct)
type args struct {
authMethodId string
userName string
password string
}
var tests = []struct {
name string
args args
want *Account
wantIsErr error
}{
{
name: "invalid-no-authMethodId",
args: args{
authMethodId: "",
userName: inAcct.UserName,
password: passwd,
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-no-userName",
args: args{
authMethodId: inAcct.AuthMethodId,
userName: "",
password: passwd,
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "invalid-no-password",
args: args{
authMethodId: inAcct.AuthMethodId,
userName: inAcct.UserName,
password: "",
},
wantIsErr: db.ErrInvalidParameter,
},
{
name: "valid-authenticate",
args: args{
authMethodId: inAcct.AuthMethodId,
userName: inAcct.UserName,
password: passwd,
},
want: outAcct,
},
{
name: "wrong-password",
args: args{
authMethodId: inAcct.AuthMethodId,
userName: inAcct.UserName,
password: "foobar",
},
want: nil,
wantIsErr: nil,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
authAcct, err := repo.Authenticate(context.Background(), tt.args.authMethodId, tt.args.userName, tt.args.password)
if tt.wantIsErr != nil {
assert.Truef(errors.Is(err, tt.wantIsErr), "want err: %q got: %q", tt.wantIsErr, err)
assert.Nil(authAcct, "returned account")
return
}
require.NoError(err)
if tt.want == nil {
assert.Nil(authAcct)
return
}
require.NotNil(authAcct, "returned account")
assert.NotEmpty(authAcct.CredentialId, "CredentialId")
assert.Equal(tt.args.authMethodId, authAcct.AuthMethodId, "authMethodId")
assert.Equal(tt.args.userName, authAcct.UserName, "UserName")
})
}
}
+182 -15
View File
@@ -158,6 +158,132 @@ func (x *Argon2Configuration) GetKeyLength() uint32 {
return 0
}
type Argon2Credential 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"`
// The update_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"`
// @inject_tag: `gorm:"not_null"`
PasswordAccountId string `protobuf:"bytes,4,opt,name=password_account_id,json=passwordAccountId,proto3" json:"password_account_id,omitempty" gorm:"not_null"`
// @inject_tag: `gorm:"not_null"`
PasswordConfId string `protobuf:"bytes,5,opt,name=password_conf_id,json=passwordConfId,proto3" json:"password_conf_id,omitempty" gorm:"not_null"`
// @inject_tag: `gorm:"not_null"`
PasswordMethodId string `protobuf:"bytes,6,opt,name=password_method_id,json=passwordMethodId,proto3" json:"password_method_id,omitempty" gorm:"not_null"`
// ct_salt is the encrypted salt which is stored in the database.
// @inject_tag: `gorm:"column:salt;not_null" wrapping:"ct,entry_salt"`
CtSalt []byte `protobuf:"bytes,7,opt,name=ct_salt,json=ctSalt,proto3" json:"ct_salt,omitempty" gorm:"column:salt;not_null" wrapping:"ct,entry_salt"`
// salt is the unencrypted salt which is not stored in the database.
// @inject_tag: `gorm:"-" wrapping:"pt,entry_salt"`
Salt []byte `protobuf:"bytes,8,opt,name=salt,proto3" json:"salt,omitempty" gorm:"-" wrapping:"pt,entry_salt"`
// derived_key is the derived key produced by the Argon2id key
// derivation function.
// @inject_tag: `gorm:"not_null"`
DerivedKey []byte `protobuf:"bytes,9,opt,name=derived_key,json=derivedKey,proto3" json:"derived_key,omitempty" gorm:"not_null"`
}
func (x *Argon2Credential) Reset() {
*x = Argon2Credential{}
if protoimpl.UnsafeEnabled {
mi := &file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Argon2Credential) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Argon2Credential) ProtoMessage() {}
func (x *Argon2Credential) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[1]
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 Argon2Credential.ProtoReflect.Descriptor instead.
func (*Argon2Credential) Descriptor() ([]byte, []int) {
return file_controller_storage_auth_password_store_v1_argon2_proto_rawDescGZIP(), []int{1}
}
func (x *Argon2Credential) GetPrivateId() string {
if x != nil {
return x.PrivateId
}
return ""
}
func (x *Argon2Credential) GetCreateTime() *timestamp.Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *Argon2Credential) GetUpdateTime() *timestamp.Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
func (x *Argon2Credential) GetPasswordAccountId() string {
if x != nil {
return x.PasswordAccountId
}
return ""
}
func (x *Argon2Credential) GetPasswordConfId() string {
if x != nil {
return x.PasswordConfId
}
return ""
}
func (x *Argon2Credential) GetPasswordMethodId() string {
if x != nil {
return x.PasswordMethodId
}
return ""
}
func (x *Argon2Credential) GetCtSalt() []byte {
if x != nil {
return x.CtSalt
}
return nil
}
func (x *Argon2Credential) GetSalt() []byte {
if x != nil {
return x.Salt
}
return nil
}
func (x *Argon2Credential) GetDerivedKey() []byte {
if x != nil {
return x.DerivedKey
}
return nil
}
var File_controller_storage_auth_password_store_v1_argon2_proto protoreflect.FileDescriptor
var file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc = []byte{
@@ -190,12 +316,38 @@ var file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc = []byte
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,
0x65, 0x79, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xa1, 0x03, 0x0a, 0x10, 0x41, 0x72, 0x67,
0x6f, 0x6e, 0x32, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 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, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64,
0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 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, 0x75, 0x70, 0x64, 0x61,
0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x63, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x49, 0x64,
0x12, 0x2c, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6d, 0x65, 0x74,
0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x17,
0x0a, 0x07, 0x63, 0x74, 0x5f, 0x73, 0x61, 0x6c, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x06, 0x63, 0x74, 0x53, 0x61, 0x6c, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x18,
0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x73, 0x61, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x64,
0x65, 0x72, 0x69, 0x76, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x0a, 0x64, 0x65, 0x72, 0x69, 0x76, 0x65, 0x64, 0x4b, 0x65, 0x79, 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 (
@@ -210,18 +362,21 @@ func file_controller_storage_auth_password_store_v1_argon2_proto_rawDescGZIP() [
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_msgTypes = make([]protoimpl.MessageInfo, 2)
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
(*Argon2Credential)(nil), // 1: controller.storage.auth.password.store.v1.Argon2Credential
(*timestamp.Timestamp)(nil), // 2: 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
2, // 0: controller.storage.auth.password.store.v1.Argon2Configuration.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
2, // 1: controller.storage.auth.password.store.v1.Argon2Credential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
2, // 2: controller.storage.auth.password.store.v1.Argon2Credential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_controller_storage_auth_password_store_v1_argon2_proto_init() }
@@ -242,6 +397,18 @@ func file_controller_storage_auth_password_store_v1_argon2_proto_init() {
return nil
}
}
file_controller_storage_auth_password_store_v1_argon2_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Argon2Credential); 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{
@@ -249,7 +416,7 @@ func file_controller_storage_auth_password_store_v1_argon2_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_controller_storage_auth_password_store_v1_argon2_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
+53
View File
@@ -1873,6 +1873,7 @@ commit;
bytes: []byte(`
begin;
drop table auth_password_argon2_cred;
drop table auth_password_argon2_conf;
commit;
@@ -1913,6 +1914,7 @@ begin;
on update cascade
deferrable initially deferred
);
create or replace function
read_only_auth_password_argon2_conf()
returns trigger
@@ -1933,9 +1935,42 @@ begin;
before insert on auth_password_argon2_conf
for each row execute procedure insert_auth_password_conf_subtype();
create table auth_password_argon2_cred (
private_id wt_private_id primary key
references auth_password_credential (private_id)
on delete cascade
on update cascade,
password_account_id wt_public_id not null,
password_conf_id wt_private_id not null,
-- NOTE(mgaffney): The password_method_id type is not wt_public_id because
-- the domain check is executed before the insert trigger which retrieves
-- the password_method_id causing an insert to fail.
password_method_id text not null,
create_time wt_timestamp,
update_time wt_timestamp,
salt bytea not null -- cannot be changed unless derived_key is changed too
check(length(salt) > 0),
derived_key bytea not null
check(length(derived_key) > 0),
foreign key (password_method_id, password_conf_id)
references auth_password_argon2_conf (password_method_id, private_id)
on delete cascade
on update cascade,
foreign key (password_method_id, password_conf_id, password_account_id)
references auth_password_credential (password_method_id, password_conf_id, password_account_id)
on delete cascade
on update cascade
);
create trigger
insert_auth_password_credential_subtype
before insert on auth_password_argon2_cred
for each row execute procedure insert_auth_password_credential_subtype();
--
-- triggers for time columns
--
create trigger
immutable_columns
before
@@ -1948,6 +1983,24 @@ begin;
insert on auth_password_argon2_conf
for each row execute procedure default_create_time();
create trigger
update_time_column
before
update on auth_password_argon2_cred
for each row execute procedure update_time_column();
create trigger
immutable_columns
before
update on auth_password_argon2_cred
for each row execute procedure immutable_columns('create_time');
create trigger
default_create_time_column
before
insert on auth_password_argon2_cred
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
@@ -1,5 +1,6 @@
begin;
drop table auth_password_argon2_cred;
drop table auth_password_argon2_conf;
commit;
@@ -29,6 +29,7 @@ begin;
on update cascade
deferrable initially deferred
);
create or replace function
read_only_auth_password_argon2_conf()
returns trigger
@@ -49,9 +50,42 @@ begin;
before insert on auth_password_argon2_conf
for each row execute procedure insert_auth_password_conf_subtype();
create table auth_password_argon2_cred (
private_id wt_private_id primary key
references auth_password_credential (private_id)
on delete cascade
on update cascade,
password_account_id wt_public_id not null,
password_conf_id wt_private_id not null,
-- NOTE(mgaffney): The password_method_id type is not wt_public_id because
-- the domain check is executed before the insert trigger which retrieves
-- the password_method_id causing an insert to fail.
password_method_id text not null,
create_time wt_timestamp,
update_time wt_timestamp,
salt bytea not null -- cannot be changed unless derived_key is changed too
check(length(salt) > 0),
derived_key bytea not null
check(length(derived_key) > 0),
foreign key (password_method_id, password_conf_id)
references auth_password_argon2_conf (password_method_id, private_id)
on delete cascade
on update cascade,
foreign key (password_method_id, password_conf_id, password_account_id)
references auth_password_credential (password_method_id, password_conf_id, password_account_id)
on delete cascade
on update cascade
);
create trigger
insert_auth_password_credential_subtype
before insert on auth_password_argon2_cred
for each row execute procedure insert_auth_password_credential_subtype();
--
-- triggers for time columns
--
create trigger
immutable_columns
before
@@ -64,6 +98,24 @@ begin;
insert on auth_password_argon2_conf
for each row execute procedure default_create_time();
create trigger
update_time_column
before
update on auth_password_argon2_cred
for each row execute procedure update_time_column();
create trigger
immutable_columns
before
update on auth_password_argon2_cred
for each row execute procedure immutable_columns('create_time');
create trigger
default_create_time_column
before
insert on auth_password_argon2_cred
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
@@ -50,3 +50,36 @@ message Argon2Configuration {
// @inject_tag: `gorm:"default:null"`
uint32 key_length = 8;
}
message Argon2Credential {
// @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;
// The update_time is set by the database.
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp update_time = 3;
// @inject_tag: `gorm:"not_null"`
string password_account_id = 4;
// @inject_tag: `gorm:"not_null"`
string password_conf_id = 5;
// @inject_tag: `gorm:"not_null"`
string password_method_id = 6;
// ct_salt is the encrypted salt which is stored in the database.
// @inject_tag: `gorm:"column:salt;not_null" wrapping:"ct,entry_salt"`
bytes ct_salt = 7;
// salt is the unencrypted salt which is not stored in the database.
// @inject_tag: `gorm:"-" wrapping:"pt,entry_salt"`
bytes salt = 8;
// derived_key is the derived key produced by the Argon2id key
// derivation function.
// @inject_tag: `gorm:"not_null"`
bytes derived_key = 9;
}