Skip to content

Commit

Permalink
Merge pull request #51 from mtlynch/hash-password
Browse files Browse the repository at this point in the history
Check passwords based on hashes rather than plaintext
  • Loading branch information
0x2E authored Jan 13, 2025
2 parents 28d360a + bfd4e8c commit c672aae
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 17 deletions.
7 changes: 4 additions & 3 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"time"

"github.com/0x2e/fusion/auth"
"github.com/0x2e/fusion/conf"
"github.com/0x2e/fusion/frontend"
"github.com/0x2e/fusion/pkg/logx"
Expand All @@ -26,7 +27,7 @@ import (
type Params struct {
Host string
Port int
Password string
PasswordHash auth.HashedPassword
UseSecureCookie bool
TLSCert string
TLSKey string
Expand Down Expand Up @@ -70,7 +71,7 @@ func Run(params Params) {
r.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
Timeout: 30 * time.Second,
}))
r.Use(session.Middleware(sessions.NewCookieStore([]byte(params.Password))))
r.Use(session.Middleware(sessions.NewCookieStore(params.PasswordHash.Bytes())))
r.Pre(middleware.RemoveTrailingSlash())
r.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
Expand All @@ -88,7 +89,7 @@ func Run(params Params) {
}))

loginAPI := Session{
Password: params.Password,
PasswordHash: params.PasswordHash,
UseSecureCookie: params.UseSecureCookie,
}
r.POST("/api/sessions", loginAPI.Create)
Expand Down
10 changes: 8 additions & 2 deletions api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package api
import (
"net/http"

"github.com/0x2e/fusion/auth"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)

type Session struct {
Password string
PasswordHash auth.HashedPassword
UseSecureCookie bool
}

Expand All @@ -25,7 +26,12 @@ func (s Session) Create(c echo.Context) error {
return err
}

if req.Password != s.Password {
attemptedPasswordHash, err := auth.HashPassword(req.Password)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid password")
}

if correctPasswordHash := s.PasswordHash; !attemptedPasswordHash.Equals(correctPasswordHash) {
return echo.NewHTTPError(http.StatusUnauthorized, "Wrong password")
}

Expand Down
43 changes: 43 additions & 0 deletions auth/password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package auth

import (
"crypto/sha256"
"crypto/subtle"
"errors"

"golang.org/x/crypto/pbkdf2"
)

var ErrPasswordTooShort = errors.New("password must be non-empty")

type HashedPassword struct {
hash []byte
}

func (hp HashedPassword) Bytes() []byte {
return hp.hash
}

func (hp HashedPassword) Equals(other HashedPassword) bool {
return subtle.ConstantTimeCompare(hp.hash, other.hash) != 0
}

func HashPassword(password string) (HashedPassword, error) {
if len(password) == 0 {
return HashedPassword{}, ErrPasswordTooShort
}

// These bytes are chosen at random. It's insecure to use a static salt to
// hash a set of passwords, but since we're only ever hashing a single
// password, using a static salt is fine. The salt prevents an attacker from
// using a rainbow table to retrieve the plaintext password from the hashed
// version, and that's all that's necessary for fusion's needs.
staticSalt := []byte{36, 129, 1, 54}
iter := 100
keyLen := 32
hash := pbkdf2.Key([]byte(password), staticSalt, iter, keyLen, sha256.New)

return HashedPassword{
hash: hash,
}, nil
}
71 changes: 71 additions & 0 deletions auth/password_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package auth_test

import (
"testing"

"github.com/0x2e/fusion/auth"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestHashPassword(t *testing.T) {
for _, tt := range []struct {
explanation string
input string
wantErr error
}{
{
explanation: "valid password succeeds",
input: "mypassword",
wantErr: nil,
},
{
explanation: "empty password returns ErrPasswordTooShort",
input: "",
wantErr: auth.ErrPasswordTooShort,
},
} {
t.Run(tt.explanation, func(t *testing.T) {
got, err := auth.HashPassword(tt.input)
require.Equal(t, tt.wantErr, err)
if tt.wantErr == nil {
assert.NotEmpty(t, got.Bytes())
}
})
}
}

func TestHashedPasswordEquals(t *testing.T) {
for _, tt := range []struct {
explanation string
hashedPassword1 auth.HashedPassword
hashedPassword2 auth.HashedPassword
want bool
}{
{
explanation: "same passwords match",
hashedPassword1: mustHashPassword("password1"),
hashedPassword2: mustHashPassword("password1"),
want: true,
},
{
explanation: "different passwords don't match",
hashedPassword1: mustHashPassword("password1"),
hashedPassword2: mustHashPassword("password2"),
want: false,
},
} {
t.Run(tt.explanation, func(t *testing.T) {
assert.Equal(t, tt.want, tt.hashedPassword1.Equals(tt.hashedPassword2))
})
}
}

func mustHashPassword(password string) auth.HashedPassword {
hashedPassword, err := auth.HashPassword(password)
if err != nil {
panic(err)
}
return hashedPassword
}
2 changes: 1 addition & 1 deletion cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func main() {
api.Run(api.Params{
Host: config.Host,
Port: config.Port,
Password: config.Password,
PasswordHash: config.PasswordHash,
UseSecureCookie: config.SecureCookie,
TLSCert: config.TLSCert,
TLSKey: config.TLSKey,
Expand Down
40 changes: 29 additions & 11 deletions conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"os"

"github.com/0x2e/fusion/auth"
"github.com/caarlos0/env/v11"
"github.com/joho/godotenv"
)
Expand All @@ -17,13 +18,13 @@ const (
)

type Conf struct {
Host string `env:"HOST" envDefault:"0.0.0.0"`
Port int `env:"PORT" envDefault:"8080"`
Password string `env:"PASSWORD"`
DB string `env:"DB" envDefault:"fusion.db"`
SecureCookie bool `env:"SECURE_COOKIE" envDefault:"false"`
TLSCert string `env:"TLS_CERT"`
TLSKey string `env:"TLS_KEY"`
Host string
Port int
PasswordHash auth.HashedPassword
DB string
SecureCookie bool
TLSCert string
TLSKey string
}

func Load() (Conf, error) {
Expand All @@ -35,16 +36,25 @@ func Load() (Conf, error) {
} else {
log.Printf("read configuration from %s", dotEnvFilename)
}
var conf Conf
var conf struct {
Host string `env:"HOST" envDefault:"0.0.0.0"`
Port int `env:"PORT" envDefault:"8080"`
Password string `env:"PASSWORD"`
DB string `env:"DB" envDefault:"fusion.db"`
SecureCookie bool `env:"SECURE_COOKIE" envDefault:"false"`
TLSCert string `env:"TLS_CERT"`
TLSKey string `env:"TLS_KEY"`
}
if err := env.Parse(&conf); err != nil {
panic(err)
}
if Debug {
fmt.Println(conf)
}

if conf.Password == "" {
return Conf{}, errors.New("password is required")
pwHash, err := auth.HashPassword(conf.Password)
if err != nil {
return Conf{}, err
}

if (conf.TLSCert == "") != (conf.TLSKey == "") {
Expand All @@ -54,5 +64,13 @@ func Load() (Conf, error) {
conf.SecureCookie = true
}

return conf, nil
return Conf{
Host: conf.Host,
Port: conf.Port,
PasswordHash: pwHash,
DB: conf.DB,
SecureCookie: conf.SecureCookie,
TLSCert: conf.TLSCert,
TLSKey: conf.TLSKey,
}, nil
}

0 comments on commit c672aae

Please sign in to comment.