Skip to content

Commit c672aae

Browse files
authored
Merge pull request #51 from mtlynch/hash-password
Check passwords based on hashes rather than plaintext
2 parents 28d360a + bfd4e8c commit c672aae

File tree

6 files changed

+156
-17
lines changed

6 files changed

+156
-17
lines changed

api/api.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88
"time"
99

10+
"github.com/0x2e/fusion/auth"
1011
"github.com/0x2e/fusion/conf"
1112
"github.com/0x2e/fusion/frontend"
1213
"github.com/0x2e/fusion/pkg/logx"
@@ -26,7 +27,7 @@ import (
2627
type Params struct {
2728
Host string
2829
Port int
29-
Password string
30+
PasswordHash auth.HashedPassword
3031
UseSecureCookie bool
3132
TLSCert string
3233
TLSKey string
@@ -70,7 +71,7 @@ func Run(params Params) {
7071
r.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
7172
Timeout: 30 * time.Second,
7273
}))
73-
r.Use(session.Middleware(sessions.NewCookieStore([]byte(params.Password))))
74+
r.Use(session.Middleware(sessions.NewCookieStore(params.PasswordHash.Bytes())))
7475
r.Pre(middleware.RemoveTrailingSlash())
7576
r.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
7677
return func(c echo.Context) error {
@@ -88,7 +89,7 @@ func Run(params Params) {
8889
}))
8990

9091
loginAPI := Session{
91-
Password: params.Password,
92+
PasswordHash: params.PasswordHash,
9293
UseSecureCookie: params.UseSecureCookie,
9394
}
9495
r.POST("/api/sessions", loginAPI.Create)

api/session.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package api
33
import (
44
"net/http"
55

6+
"github.com/0x2e/fusion/auth"
67
"github.com/labstack/echo-contrib/session"
78
"github.com/labstack/echo/v4"
89
)
910

1011
type Session struct {
11-
Password string
12+
PasswordHash auth.HashedPassword
1213
UseSecureCookie bool
1314
}
1415

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

28-
if req.Password != s.Password {
29+
attemptedPasswordHash, err := auth.HashPassword(req.Password)
30+
if err != nil {
31+
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid password")
32+
}
33+
34+
if correctPasswordHash := s.PasswordHash; !attemptedPasswordHash.Equals(correctPasswordHash) {
2935
return echo.NewHTTPError(http.StatusUnauthorized, "Wrong password")
3036
}
3137

auth/password.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package auth
2+
3+
import (
4+
"crypto/sha256"
5+
"crypto/subtle"
6+
"errors"
7+
8+
"golang.org/x/crypto/pbkdf2"
9+
)
10+
11+
var ErrPasswordTooShort = errors.New("password must be non-empty")
12+
13+
type HashedPassword struct {
14+
hash []byte
15+
}
16+
17+
func (hp HashedPassword) Bytes() []byte {
18+
return hp.hash
19+
}
20+
21+
func (hp HashedPassword) Equals(other HashedPassword) bool {
22+
return subtle.ConstantTimeCompare(hp.hash, other.hash) != 0
23+
}
24+
25+
func HashPassword(password string) (HashedPassword, error) {
26+
if len(password) == 0 {
27+
return HashedPassword{}, ErrPasswordTooShort
28+
}
29+
30+
// These bytes are chosen at random. It's insecure to use a static salt to
31+
// hash a set of passwords, but since we're only ever hashing a single
32+
// password, using a static salt is fine. The salt prevents an attacker from
33+
// using a rainbow table to retrieve the plaintext password from the hashed
34+
// version, and that's all that's necessary for fusion's needs.
35+
staticSalt := []byte{36, 129, 1, 54}
36+
iter := 100
37+
keyLen := 32
38+
hash := pbkdf2.Key([]byte(password), staticSalt, iter, keyLen, sha256.New)
39+
40+
return HashedPassword{
41+
hash: hash,
42+
}, nil
43+
}

auth/password_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package auth_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/0x2e/fusion/auth"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestHashPassword(t *testing.T) {
13+
for _, tt := range []struct {
14+
explanation string
15+
input string
16+
wantErr error
17+
}{
18+
{
19+
explanation: "valid password succeeds",
20+
input: "mypassword",
21+
wantErr: nil,
22+
},
23+
{
24+
explanation: "empty password returns ErrPasswordTooShort",
25+
input: "",
26+
wantErr: auth.ErrPasswordTooShort,
27+
},
28+
} {
29+
t.Run(tt.explanation, func(t *testing.T) {
30+
got, err := auth.HashPassword(tt.input)
31+
require.Equal(t, tt.wantErr, err)
32+
if tt.wantErr == nil {
33+
assert.NotEmpty(t, got.Bytes())
34+
}
35+
})
36+
}
37+
}
38+
39+
func TestHashedPasswordEquals(t *testing.T) {
40+
for _, tt := range []struct {
41+
explanation string
42+
hashedPassword1 auth.HashedPassword
43+
hashedPassword2 auth.HashedPassword
44+
want bool
45+
}{
46+
{
47+
explanation: "same passwords match",
48+
hashedPassword1: mustHashPassword("password1"),
49+
hashedPassword2: mustHashPassword("password1"),
50+
want: true,
51+
},
52+
{
53+
explanation: "different passwords don't match",
54+
hashedPassword1: mustHashPassword("password1"),
55+
hashedPassword2: mustHashPassword("password2"),
56+
want: false,
57+
},
58+
} {
59+
t.Run(tt.explanation, func(t *testing.T) {
60+
assert.Equal(t, tt.want, tt.hashedPassword1.Equals(tt.hashedPassword2))
61+
})
62+
}
63+
}
64+
65+
func mustHashPassword(password string) auth.HashedPassword {
66+
hashedPassword, err := auth.HashPassword(password)
67+
if err != nil {
68+
panic(err)
69+
}
70+
return hashedPassword
71+
}

cmd/server/server.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func main() {
3232
api.Run(api.Params{
3333
Host: config.Host,
3434
Port: config.Port,
35-
Password: config.Password,
35+
PasswordHash: config.PasswordHash,
3636
UseSecureCookie: config.SecureCookie,
3737
TLSCert: config.TLSCert,
3838
TLSKey: config.TLSKey,

conf/conf.go

+29-11
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"log"
77
"os"
88

9+
"github.com/0x2e/fusion/auth"
910
"github.com/caarlos0/env/v11"
1011
"github.com/joho/godotenv"
1112
)
@@ -17,13 +18,13 @@ const (
1718
)
1819

1920
type Conf struct {
20-
Host string `env:"HOST" envDefault:"0.0.0.0"`
21-
Port int `env:"PORT" envDefault:"8080"`
22-
Password string `env:"PASSWORD"`
23-
DB string `env:"DB" envDefault:"fusion.db"`
24-
SecureCookie bool `env:"SECURE_COOKIE" envDefault:"false"`
25-
TLSCert string `env:"TLS_CERT"`
26-
TLSKey string `env:"TLS_KEY"`
21+
Host string
22+
Port int
23+
PasswordHash auth.HashedPassword
24+
DB string
25+
SecureCookie bool
26+
TLSCert string
27+
TLSKey string
2728
}
2829

2930
func Load() (Conf, error) {
@@ -35,16 +36,25 @@ func Load() (Conf, error) {
3536
} else {
3637
log.Printf("read configuration from %s", dotEnvFilename)
3738
}
38-
var conf Conf
39+
var conf struct {
40+
Host string `env:"HOST" envDefault:"0.0.0.0"`
41+
Port int `env:"PORT" envDefault:"8080"`
42+
Password string `env:"PASSWORD"`
43+
DB string `env:"DB" envDefault:"fusion.db"`
44+
SecureCookie bool `env:"SECURE_COOKIE" envDefault:"false"`
45+
TLSCert string `env:"TLS_CERT"`
46+
TLSKey string `env:"TLS_KEY"`
47+
}
3948
if err := env.Parse(&conf); err != nil {
4049
panic(err)
4150
}
4251
if Debug {
4352
fmt.Println(conf)
4453
}
4554

46-
if conf.Password == "" {
47-
return Conf{}, errors.New("password is required")
55+
pwHash, err := auth.HashPassword(conf.Password)
56+
if err != nil {
57+
return Conf{}, err
4858
}
4959

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

57-
return conf, nil
67+
return Conf{
68+
Host: conf.Host,
69+
Port: conf.Port,
70+
PasswordHash: pwHash,
71+
DB: conf.DB,
72+
SecureCookie: conf.SecureCookie,
73+
TLSCert: conf.TLSCert,
74+
TLSKey: conf.TLSKey,
75+
}, nil
5876
}

0 commit comments

Comments
 (0)