Re-hash user's passwords if necessary when they log in

This commit is contained in:
Ben Visness 2021-03-28 10:32:30 -05:00
parent 8f2958594a
commit f7ac023c44
4 changed files with 80 additions and 2 deletions

View File

@ -2,15 +2,18 @@ package auth
import ( import (
"bytes" "bytes"
"context"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"strings" "strings"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
"github.com/jackc/pgx/v4/pgxpool"
"golang.org/x/crypto/argon2" "golang.org/x/crypto/argon2"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
@ -54,6 +57,10 @@ func (p HashedPassword) String() string {
return fmt.Sprintf("%s$%s$%s$%s", p.Algorithm, p.AlgoConfig, p.Salt, p.Hash) return fmt.Sprintf("%s$%s$%s$%s", p.Algorithm, p.AlgoConfig, p.Salt, p.Hash)
} }
func (p HashedPassword) IsOutdated() bool {
return p.Algorithm != Argon2id
}
type Argon2idConfig struct { type Argon2idConfig struct {
Time uint32 Time uint32
Memory uint32 Memory uint32
@ -152,7 +159,7 @@ func HashPassword(password string) (HashedPassword, error) {
cfg := Argon2idConfig{ cfg := Argon2idConfig{
Time: 1, Time: 1,
Memory: 40 * 1024 * 1024, Memory: 40 * 1024, // this is in KiB for some reason
Threads: 1, Threads: 1,
KeyLength: keyLength, KeyLength: keyLength,
} }
@ -167,3 +174,16 @@ func HashPassword(password string) (HashedPassword, error) {
Hash: keyEnc, Hash: keyEnc,
}, nil }, nil
} }
var ErrUserDoesNotExist = errors.New("user does not exist")
func UpdatePassword(ctx context.Context, conn *pgxpool.Pool, username string, hp HashedPassword) error {
tag, err := conn.Exec(ctx, "UPDATE auth_user SET password = $1 WHERE username = $2", hp.String(), username)
if err != nil {
return oops.New(err, "failed to update password")
} else if tag.RowsAffected() < 1 {
return ErrUserDoesNotExist
}
return nil
}

View File

@ -49,7 +49,8 @@ func init() {
Short: "Create a new database migration file", Short: "Create a new database migration file",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if len(args) < 2 { if len(args) < 2 {
fmt.Println("You must provide a name and a description.") fmt.Printf("You must provide a name and a description.\n\n")
cmd.Usage()
os.Exit(1) os.Exit(1)
} }

View File

@ -0,0 +1,43 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v4"
)
func init() {
registerMigration(AllowLongerPasswordHashes{})
}
type AllowLongerPasswordHashes struct{}
func (m AllowLongerPasswordHashes) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2021, 3, 28, 15, 26, 30, 0, time.UTC))
}
func (m AllowLongerPasswordHashes) Name() string {
return "AllowLongerPasswordHashes"
}
func (m AllowLongerPasswordHashes) Description() string {
return "Increase the storage size limit on hashed passwords"
}
func (m AllowLongerPasswordHashes) Up(tx pgx.Tx) error {
_, err := tx.Exec(context.Background(), `
ALTER TABLE auth_user
ALTER COLUMN password TYPE VARCHAR(256)
`)
return err
}
func (m AllowLongerPasswordHashes) Down(tx pgx.Tx) error {
_, err := tx.Exec(context.Background(), `
ALTER TABLE auth_user
ALTER COLUMN password TYPE VARCHAR(128)
`)
return err
}

View File

@ -183,6 +183,20 @@ func (s *websiteRoutes) Login(c *RequestContext, p httprouter.Params) {
} }
if passwordsMatch { if passwordsMatch {
// re-hash and save the user's password if necessary
if hashed.IsOutdated() {
newHashed, err := auth.HashPassword(password)
if err == nil {
err := auth.UpdatePassword(c.Context(), s.conn, username, newHashed)
if err != nil {
c.Logger.Error().Err(err).Msg("failed to update user's password")
}
} else {
c.Logger.Error().Err(err).Msg("failed to re-hash password")
}
// If errors happen here, we can still continue with logging them in
}
session, err := auth.CreateSession(c.Context(), s.conn, username) session, err := auth.CreateSession(c.Context(), s.conn, username)
if err != nil { if err != nil {
c.Errored(http.StatusInternalServerError, oops.New(err, "failed to create session")) c.Errored(http.StatusInternalServerError, oops.New(err, "failed to create session"))