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

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

350 lines
13 KiB
Go
Raw Normal View History

2020-08-19 16:47:18 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
package password
import (
"context"
"crypto/subtle"
"database/sql"
"fmt"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/db"
2020-11-05 16:54:03 -05:00
"github.com/hashicorp/boundary/internal/errors"
2020-08-19 16:47:18 -04:00
"github.com/hashicorp/boundary/internal/kms"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/oplog"
"golang.org/x/crypto/argon2"
)
2020-08-06 13:03:42 -04:00
type authAccount struct {
*Account
*Argon2Credential
*Argon2Configuration
IsCurrentConf bool
}
2020-08-11 17:25:00 -04:00
// Authenticate authenticates loginName and password match for loginName in
// authMethodId. The account for the loginName 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.
2020-08-19 16:47:18 -04:00
func (r *Repository) Authenticate(ctx context.Context, scopeId, authMethodId, loginName, password string) (*Account, error) {
const op = "password.(Repository).Authenticate"
if authMethodId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing authMethodId", errors.WithoutEvent())
}
2020-08-11 17:25:00 -04:00
if loginName == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing loginName", errors.WithoutEvent())
}
if password == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password", errors.WithoutEvent())
}
2020-08-19 16:47:18 -04:00
if scopeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scopeId", errors.WithoutEvent())
2020-08-19 16:47:18 -04:00
}
databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get database wrapper"))
2020-08-19 16:47:18 -04:00
}
2020-08-19 16:47:18 -04:00
acct, err := r.authenticate(ctx, scopeId, authMethodId, loginName, password)
2020-08-06 13:03:42 -04:00
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
2020-08-06 13:03:42 -04:00
if acct == nil {
return nil, nil
}
if !acct.IsCurrentConf {
cc, err := r.currentConfig(ctx, authMethodId)
if err != nil {
return acct.Account, errors.Wrap(ctx, err, op, errors.WithMsg("retrieve current password configuration"))
2020-08-06 13:03:42 -04:00
}
cred, err := newArgon2Credential(ctx, acct.PublicId, password, cc.argon2())
2020-08-06 13:03:42 -04:00
if err != nil {
return acct.Account, errors.Wrap(ctx, err, op, errors.WithCode(errors.PasswordInvalidConfiguration))
2020-08-06 13:03:42 -04:00
}
2020-08-19 16:47:18 -04:00
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog)
if err != nil {
return acct.Account, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplong wrapper"))
2020-08-19 16:47:18 -04:00
}
2020-08-06 13:03:42 -04:00
// do not change the Credential Id
cred.PrivateId = acct.CredentialId
2020-08-19 16:47:18 -04:00
if err := cred.encrypt(ctx, databaseWrapper); err != nil {
return acct.Account, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("update credential"))
2020-08-06 13:03:42 -04:00
}
2022-11-16 17:17:28 -06:00
fields := []string{"CtSalt", "DerivedKey", "PasswordConfId", "KeyId"}
2020-08-06 13:03:42 -04:00
metadata := cred.oplog(oplog.OpType_OP_TYPE_UPDATE)
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
2020-08-19 16:47:18 -04:00
rowsUpdated, err := w.Update(ctx, cred, fields, nil, db.WithOplog(oplogWrapper, metadata))
2021-03-05 06:34:06 -08:00
if err != nil {
return errors.Wrap(ctx, err, op)
2021-03-05 06:34:06 -08:00
}
if rowsUpdated > 1 {
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated")
2020-08-06 13:03:42 -04:00
}
2021-03-05 06:34:06 -08:00
return nil
2020-08-06 13:03:42 -04:00
},
)
if err != nil {
return acct.Account, errors.Wrap(ctx, err, op, errors.WithMsg("update credential"))
2020-08-06 13:03:42 -04:00
}
}
return acct.Account, nil
}
2020-08-17 09:37:15 -07:00
// ChangePassword updates the password for accountId to new if old equals
// the stored password. The account for the accountId is returned with a
2020-08-17 09:37:15 -07:00
// new CredentialId if password is successfully changed.
2020-08-06 13:03:42 -04:00
//
// Returns nil, db.ErrorRecordNotFound if the account doesn't exist.
// Returns nil, nil if old does not match the stored password for accountId.
2021-03-05 06:34:06 -08:00
// Returns nil, error with code PasswordsEqual if old and new are equal.
2020-08-19 16:47:18 -04:00
func (r *Repository) ChangePassword(ctx context.Context, scopeId, accountId, old, new string, version uint32) (*Account, error) {
const op = "password.(Repository).ChangePassword"
2020-08-17 09:37:15 -07:00
if accountId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing account id")
2020-08-06 13:03:42 -04:00
}
if old == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing old password")
2020-08-06 13:03:42 -04:00
}
if new == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing new password")
2020-08-06 13:03:42 -04:00
}
if old == new {
return nil, errors.New(ctx, errors.PasswordsEqual, op, "passwords must not equal")
2020-08-06 13:03:42 -04:00
}
2020-08-17 09:37:15 -07:00
if version == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
2020-08-17 09:37:15 -07:00
}
2020-08-19 16:47:18 -04:00
if scopeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scopeId")
2020-08-19 16:47:18 -04:00
}
2020-08-06 13:03:42 -04:00
2020-08-17 09:37:15 -07:00
authAccount, err := r.LookupAccount(ctx, accountId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
2020-08-17 09:37:15 -07:00
}
if authAccount == nil {
return nil, errors.New(ctx, errors.RecordNotFound, op, "account not found")
2020-08-17 09:37:15 -07:00
}
2020-08-19 16:47:18 -04:00
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplog wrapper"))
2020-08-19 16:47:18 -04:00
}
databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get database wrapper"))
2020-08-19 16:47:18 -04:00
}
acct, err := r.authenticate(ctx, scopeId, authAccount.GetAuthMethodId(), authAccount.GetLoginName(), old)
2020-08-06 13:03:42 -04:00
if err != nil {
return nil, errors.Wrap(ctx, err, op)
2020-08-06 13:03:42 -04:00
}
if acct == nil {
return nil, nil
}
2020-08-17 09:37:15 -07:00
cc, err := r.currentConfig(ctx, authAccount.GetAuthMethodId())
2020-08-06 13:03:42 -04:00
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("retrieve current password configuration"))
2020-08-06 13:03:42 -04:00
}
if cc.MinPasswordLength > len(new) {
return nil, errors.New(ctx, errors.PasswordTooShort, op, fmt.Sprintf("must be at least %d", cc.MinPasswordLength))
2020-08-06 13:03:42 -04:00
}
newCred, err := newArgon2Credential(ctx, accountId, new, cc.argon2())
2020-08-06 13:03:42 -04:00
if err != nil {
return nil, errors.Wrap(ctx, err, op)
2020-08-06 13:03:42 -04:00
}
2020-08-19 16:47:18 -04:00
if err := newCred.encrypt(ctx, databaseWrapper); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
2020-08-06 13:03:42 -04:00
}
oldCred := acct.Argon2Credential
2020-08-17 09:37:15 -07:00
var updatedAccount *Account
2020-08-06 13:03:42 -04:00
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(_ db.Reader, w db.Writer) error {
2020-08-17 09:37:15 -07:00
updatedAccount = allocAccount()
updatedAccount.PublicId = accountId
updatedAccount.Version = version + 1
2020-08-19 16:47:18 -04:00
rowsUpdated, err := w.Update(ctx, updatedAccount, []string{"Version"}, nil, db.WithOplog(oplogWrapper, acct.Account.oplog(oplog.OpType_OP_TYPE_UPDATE)), db.WithVersion(&version))
2020-08-17 09:37:15 -07:00
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update account version"))
2020-08-17 09:37:15 -07:00
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated account and %d rows updated", rowsUpdated))
2020-08-17 09:37:15 -07:00
}
2020-08-19 16:47:18 -04:00
rowsDeleted, err := w.Delete(ctx, oldCred, db.WithOplog(oplogWrapper, oldCred.oplog(oplog.OpType_OP_TYPE_DELETE)))
2020-08-06 13:03:42 -04:00
if err != nil {
return errors.Wrap(ctx, err, op)
2021-03-05 06:34:06 -08:00
}
if rowsDeleted > 1 {
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted")
2021-03-05 06:34:06 -08:00
}
if err = w.Create(ctx, newCred, db.WithOplog(oplogWrapper, newCred.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create new credential"))
2020-08-06 13:03:42 -04:00
}
2021-03-05 06:34:06 -08:00
return nil
2020-08-06 13:03:42 -04:00
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
2020-08-06 13:03:42 -04:00
}
// change the Credential Id
2020-08-17 09:37:15 -07:00
updatedAccount.CredentialId = newCred.PrivateId
return updatedAccount, nil
2020-08-06 13:03:42 -04:00
}
2020-08-19 16:47:18 -04:00
func (r *Repository) authenticate(ctx context.Context, scopeId, authMethodId, loginName, password string) (*authAccount, error) {
const op = "password.(Repository).authenticate"
2020-08-06 13:03:42 -04:00
var accts []authAccount
2022-10-25 18:29:38 -05:00
rows, err := r.reader.Query(ctx, authenticateQuery, []any{sql.Named("auth_method_id", authMethodId), sql.Named("login_name", loginName)})
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
defer rows.Close()
for rows.Next() {
2020-08-06 13:03:42 -04:00
var aa authAccount
if err := r.reader.ScanRows(ctx, rows, &aa); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
accts = append(accts, aa)
}
2024-01-22 14:03:40 -05:00
if err := rows.Err(); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
2020-08-06 13:03:42 -04:00
var acct authAccount
switch {
case len(accts) == 0:
return nil, nil
case len(accts) > 1:
// this should never happen
return nil, errors.New(ctx, errors.Unknown, op, "multiple accounts returned for user name")
default:
acct = accts[0]
}
2020-08-19 16:47:18 -04:00
// We don't pass a wrapper in here because for ecryption we want to indicate the expected key ID
databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase, kms.WithKeyId(acct.GetKeyId()))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get database wrapper"))
2020-08-19 16:47:18 -04:00
}
if err := acct.decrypt(ctx, databaseWrapper); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("unable to decrypt credential"))
}
inputKey := argon2.IDKey([]byte(password), acct.Salt, acct.Iterations, acct.Memory, uint8(acct.Threads), acct.KeyLength)
if subtle.ConstantTimeCompare(inputKey, acct.DerivedKey) == 0 {
2020-08-06 13:03:42 -04:00
// authentication failed, password does not match
return nil, nil
}
2020-08-06 13:03:42 -04:00
return &acct, nil
}
// SetPassword sets the password for accountId to password. If password
// contains an empty string, the password for accountId will be deleted.
2020-08-19 16:47:18 -04:00
func (r *Repository) SetPassword(ctx context.Context, scopeId, accountId, password string, version uint32) (*Account, error) {
const op = "password.(Repository).SetPassword"
if accountId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing accountId")
2020-08-17 09:37:15 -07:00
}
if version == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
2020-08-19 16:47:18 -04:00
if scopeId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scopeId")
2020-08-19 16:47:18 -04:00
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplog wrapper"))
2020-08-19 16:47:18 -04:00
}
databaseWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get database wrapper"))
2020-08-19 16:47:18 -04:00
}
var newCred *Argon2Credential
if password != "" {
cc, err := r.currentConfigForAccount(ctx, accountId)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
2020-08-17 09:37:15 -07:00
}
if cc == nil {
return nil, errors.New(ctx, errors.RecordNotFound, op, "unable to retrieve current configuration")
}
if cc.MinPasswordLength > len(password) {
return nil, errors.New(ctx, errors.PasswordTooShort, op, fmt.Sprintf("password must be at least %v", cc.MinPasswordLength))
}
newCred, err = newArgon2Credential(ctx, accountId, password, cc.argon2())
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
2020-08-19 16:47:18 -04:00
if err := newCred.encrypt(ctx, databaseWrapper); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
}
}
2020-08-17 09:37:15 -07:00
var acct *Account
2020-08-19 16:47:18 -04:00
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
func(rr db.Reader, w db.Writer) error {
2020-08-17 09:37:15 -07:00
updatedAccount := allocAccount()
updatedAccount.PublicId = accountId
updatedAccount.Version = version + 1
2020-08-19 16:47:18 -04:00
rowsUpdated, err := w.Update(ctx, updatedAccount, []string{"Version"}, nil, db.WithOplog(oplogWrapper, updatedAccount.oplog(oplog.OpType_OP_TYPE_UPDATE)), db.WithVersion(&version))
2020-08-17 09:37:15 -07:00
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update account version"))
2020-08-17 09:37:15 -07:00
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated account and %d rows updated", rowsUpdated))
2020-08-17 09:37:15 -07:00
}
acct = updatedAccount
oldCred := allocCredential()
2022-10-25 18:29:38 -05:00
if err := rr.LookupWhere(ctx, &oldCred, "password_account_id = ?", []any{accountId}); err != nil {
if !errors.IsNotFoundError(err) {
return errors.Wrap(ctx, err, op)
}
}
if oldCred.PrivateId != "" {
dCred := oldCred.clone()
2020-08-19 16:47:18 -04:00
rowsDeleted, err := w.Delete(ctx, dCred, db.WithOplog(oplogWrapper, oldCred.oplog(oplog.OpType_OP_TYPE_DELETE)))
if err != nil {
return errors.Wrap(ctx, err, op)
2021-03-05 06:34:06 -08:00
}
if rowsDeleted > 1 {
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted")
}
}
if newCred != nil {
2020-08-19 16:47:18 -04:00
return w.Create(ctx, newCred, db.WithOplog(oplogWrapper, newCred.oplog(oplog.OpType_OP_TYPE_CREATE)))
}
return nil
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
2020-08-17 09:37:15 -07:00
return acct, nil
}