Skip to content

Commit

Permalink
Implement Admin Password hashing using sha256
Browse files Browse the repository at this point in the history
  • Loading branch information
ukane-philemon committed Jan 14, 2022
1 parent 0b78c9c commit ae8a407
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 15 deletions.
8 changes: 1 addition & 7 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ type config struct {
BackupInterval time.Duration `long:"backupinterval" ini-name:"backupinterval" description:"Time period between automatic database backups. Valid time units are {s,m,h}. Minimum 30 seconds."`
VspClosed bool `long:"vspclosed" ini-name:"vspclosed" description:"Closed prevents the VSP from accepting new tickets."`
VspClosedMsg string `long:"vspclosedmsg" ini-name:"vspclosedmsg" description:"A short message displayed on the webpage and returned by the status API endpoint if vspclosed is true."`
AdminPass string `long:"adminpass" ini-name:"adminpass" description:"Password for accessing admin page."`
AdminPass string `long:"adminpass" ini-name:"adminpass" description:"Password for accessing admin page. INSECURE. Do not set unless absolutely necessary."`
Designation string `long:"designation" ini-name:"designation" description:"Short name for the VSP. Customizes the logo in the top toolbar."`

// The following flags should be set on CLI only, not via config file.
Expand Down Expand Up @@ -163,7 +163,6 @@ func normalizeAddress(addr, defaultPort string) string {
// while still allowing the user to override settings with config files and
// command line options. Command line options always take precedence.
func loadConfig() (*config, error) {

// Default config.
cfg := config{
Listen: defaultListen,
Expand Down Expand Up @@ -302,11 +301,6 @@ func loadConfig() (*config, error) {
return nil, errors.New("the supportemail option is not set")
}

// Ensure the administrator password is set.
if cfg.AdminPass == "" {
return nil, errors.New("the adminpass option is not set")
}

// Ensure the dcrd RPC username is set.
if cfg.DcrdUser == "" {
return nil, errors.New("the dcrduser option is not set")
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ require (
github.com/jrick/logrotate v1.0.0
github.com/jrick/wsrpc/v2 v2.3.4
go.etcd.io/bbolt v1.3.6
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
Expand Down
81 changes: 81 additions & 0 deletions prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) 2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package main

import (
"context"
"crypto/sha256"
"fmt"
"os"

"golang.org/x/term"
)

type passwordReadResponse struct {
password []byte
err error
}

// clearBytes zeroes the byte slice.
func clearBytes(b []byte) {
for i := range b {
b[i] = 0
}
}

// passwordPrompt prompts the user to enter a password. Password must not be an
// empty string.
func passwordPrompt(ctx context.Context, prompt string) ([]byte, error) {
// Get the initial state of the terminal.
initialTermState, err := term.GetState(int(os.Stdin.Fd()))
if err != nil {
return nil, err
}

passwordReadChan := make(chan passwordReadResponse, 1)

go func() {
fmt.Print(prompt)
pass, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
passwordReadChan <- passwordReadResponse{
password: pass,
err: err,
}
}()

select {
case <-ctx.Done():
_ = term.Restore(int(os.Stdin.Fd()), initialTermState)
return nil, ctx.Err()

case res := <-passwordReadChan:
if res.err != nil {
return nil, res.err
}
return res.password, nil
}
}

// passwordHashPrompt prompts the user to enter a password and returns its
// SHA256 hash. Password must not be an empty string.
func passwordHashPrompt(ctx context.Context, prompt string) ([sha256.Size]byte, error) {
var passBytes []byte
var err error
var authSHA [sha256.Size]byte

// Ensure passBytes is not empty.
for len(passBytes) == 0 {
passBytes, err = passwordPrompt(ctx, prompt)
if err != nil {
return authSHA, err
}
}

authSHA = sha256.Sum256(passBytes)
// Zero password bytes.
clearBytes(passBytes)
return authSHA, nil
}
Binary file added vspd.exe
Binary file not shown.
18 changes: 16 additions & 2 deletions vspd.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// Copyright (c) 2020 The Decred developers
// Copyright (c) 2020-2021 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package main

import (
"context"
"crypto/sha256"
"errors"
"fmt"
"os"
Expand Down Expand Up @@ -51,6 +52,19 @@ func run(ctx context.Context) error {
return err
}

// Request admin password if admin password is not set in config.
var adminAuthSHA [32]byte
if cfg.AdminPass == "" {
adminAuthSHA, err = passwordHashPrompt(ctx, "Admin password for accessing admin page: ")
if err != nil {
return fmt.Errorf("cannot use password: %v", err)
}
} else {
adminAuthSHA = sha256.Sum256([]byte(cfg.AdminPass))
// Clear password string
cfg.AdminPass = ""
}

// Show version at startup.
log.Infof("Version %s (Go version %s %s/%s)", version.String(), runtime.Version(),
runtime.GOOS, runtime.GOARCH)
Expand Down Expand Up @@ -98,7 +112,7 @@ func run(ctx context.Context) error {
SupportEmail: cfg.SupportEmail,
VspClosed: cfg.VspClosed,
VspClosedMsg: cfg.VspClosedMsg,
AdminPass: cfg.AdminPass,
AdminAuthSHA: adminAuthSHA,
Debug: cfg.WebServerDebug,
Designation: cfg.Designation,
MaxVoteChangeRecords: maxVoteChangeRecords,
Expand Down
6 changes: 4 additions & 2 deletions webapi/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package webapi

import (
"crypto/sha256"
"crypto/subtle"
"net/http"

"github.com/decred/vspd/database"
Expand Down Expand Up @@ -196,8 +198,8 @@ func ticketSearch(c *gin.Context) {
// the current session will be authenticated as an admin.
func adminLogin(c *gin.Context) {
password := c.PostForm("password")

if password != cfg.AdminPass {
authSHA := sha256.Sum256([]byte(password))
if subtle.ConstantTimeCompare(cfg.AdminAuthSHA[:], authSHA[:]) != 1 {
log.Warnf("Failed login attempt from %s", c.ClientIP())
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"WebApiCache": getCache(),
Expand Down
17 changes: 17 additions & 0 deletions webapi/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package webapi

import (
"bytes"
"crypto/sha256"
"crypto/subtle"
"errors"
"io"
"net/http"
Expand Down Expand Up @@ -384,3 +386,18 @@ func vspAuth() gin.HandlerFunc {
}

}

// authMiddleware checks incoming requests for authentication.
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// User is ignored
_, password, ok := c.Request.BasicAuth()
passAuthSHA := sha256.Sum256([]byte(password))
if !ok || subtle.ConstantTimeCompare(passAuthSHA[:], cfg.AdminAuthSHA[:]) != 1 {
// Credentials doesn't match, we return 401 and abort handlers chain.
c.Header("WWW-Authenticate", `Basic realm="Authorization Required"`)
c.AbortWithStatus(http.StatusUnauthorized)
return
}
}
}
6 changes: 2 additions & 4 deletions webapi/webapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type Config struct {
SupportEmail string
VspClosed bool
VspClosedMsg string
AdminPass string
AdminAuthSHA [32]byte
Debug bool
Designation string
MaxVoteChangeRecords int
Expand Down Expand Up @@ -253,9 +253,7 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r

// Require Basic HTTP Auth on /admin/status endpoint.
basic := router.Group("/admin").Use(
withDcrdClient(dcrd), withWalletClients(wallets), gin.BasicAuth(gin.Accounts{
"admin": cfg.AdminPass,
}),
withDcrdClient(dcrd), withWalletClients(wallets), authMiddleware(),
)
basic.GET("/status", statusJSON)

Expand Down

0 comments on commit ae8a407

Please sign in to comment.