package auth import ( "database/sql" "errors" "fmt" "time" ) // dummyHash is a pre-computed Argon2id hash used for constant-time comparison // when a user is not found. This prevents timing attacks that reveal whether // a username exists. var dummyHash = "$argon2id$v=19$m=65536,t=3,p=4$AAAAAAAAAAAAAAAAAAAAAA$AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" // CreateUser creates a new user with a hashed password. func CreateUser(database *sql.DB, username, password string, params Argon2Params) (int64, error) { hash, err := HashPassword(password, params) if err != nil { return 0, err } now := time.Now().UnixMilli() res, err := database.Exec( "INSERT INTO users (username, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?)", username, hash, now, now, ) if err != nil { return 0, fmt.Errorf("insert user: %w", err) } return res.LastInsertId() } // LookupUserID returns the user ID for a username, or an error if not found. func LookupUserID(database *sql.DB, username string) (int64, error) { var userID int64 err := database.QueryRow("SELECT id FROM users WHERE username = ?", username).Scan(&userID) if err != nil { return 0, fmt.Errorf("user not found: %w", err) } return userID, nil } // AuthenticateUser verifies username/password and returns the user ID. func AuthenticateUser(database *sql.DB, username, password string) (int64, error) { var userID int64 var hash string err := database.QueryRow( "SELECT id, password_hash FROM users WHERE username = ?", username, ).Scan(&userID, &hash) if errors.Is(err, sql.ErrNoRows) { // User not found: verify against dummy hash to consume constant time, // preventing timing attacks that reveal username existence. _, _ = VerifyPassword(password, dummyHash) return 0, fmt.Errorf("invalid credentials") } if err != nil { return 0, fmt.Errorf("invalid credentials") } ok, err := VerifyPassword(password, hash) if err != nil { return 0, fmt.Errorf("invalid credentials") } if !ok { return 0, fmt.Errorf("invalid credentials") } return userID, nil }