From 38430c43847b2e16fe21e92296af44524bbf6636 Mon Sep 17 00:00:00 2001 From: ukane-philemon Date: Fri, 14 Jan 2022 15:06:48 +0100 Subject: [PATCH] Hash admin password using SHA-256 --- config.go | 1 - go.mod | 2 +- go.sum | 10 ++++++--- prompt.go | 49 ++++++++++++++++++++++++++++++++++++++------ vspd.go | 35 ++++++++++++++++++------------- webapi/admin.go | 4 ++-- webapi/middleware.go | 4 ++-- webapi/webapi.go | 2 +- 8 files changed, 77 insertions(+), 30 deletions(-) diff --git a/config.go b/config.go index 04ff85a8..f9fb4765 100644 --- a/config.go +++ b/config.go @@ -67,7 +67,6 @@ 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. 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. diff --git a/go.mod b/go.mod index 0961ea62..da88c3e6 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ 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/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 ) @@ -54,7 +55,6 @@ require ( github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/ugorji/go/codec v1.1.7 // indirect - golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect google.golang.org/protobuf v1.23.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect diff --git a/go.sum b/go.sum index da482564..3de41f9e 100644 --- a/go.sum +++ b/go.sum @@ -159,8 +159,9 @@ go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -173,8 +174,9 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -195,6 +197,7 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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= @@ -202,8 +205,9 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+ 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= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/prompt.go b/prompt.go index 489f2637..43c45cc1 100644 --- a/prompt.go +++ b/prompt.go @@ -1,10 +1,11 @@ -// Copyright (c) 2021 The Decred developers +// Copyright (c) 2022 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 ( + "bufio" "context" "crypto/sha256" "fmt" @@ -61,21 +62,57 @@ func passwordPrompt(ctx context.Context, prompt string) ([]byte, error) { // 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) { +func passwordHashPrompt(ctx context.Context, prompt string) ([]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 + return nil, err } } - authSHA = sha256.Sum256(passBytes) + authHash := sha256.Sum256(passBytes) // Zero password bytes. clearBytes(passBytes) - return authSHA, nil + return authHash[:], nil +} + +// readPassHashFromFile reads admin password hash from provided file. +func readPassHashFromFile(passwordDir string) ([]byte, error) { + passwordFile, err := os.Open(passwordDir) + if err != nil { + return nil, err + } + defer passwordFile.Close() + + reader := bufio.NewReader(passwordFile) + adminAuthHash, _, err := reader.ReadLine() + if err != nil { + return nil, err + } + + return adminAuthHash, nil +} + +// createPassHashFile prompts user for password, +// hashes the provided password and saves the hashed password to a file. +func createPassHashFile(ctx context.Context, passwordDir string) ([]byte, error) { + adminAuthHash, err := passwordHashPrompt(ctx, "Enter admin Password:") + if err != nil { + return nil, err + } + passwordFile, err := os.Create(passwordDir) + if err != nil { + return nil, err + } + defer passwordFile.Close() + // Length of byte is ignored + _, err = passwordFile.Write(adminAuthHash) + if err != nil { + return nil, err + } + return adminAuthHash, nil } diff --git a/vspd.go b/vspd.go index 75b2704e..5978c1ba 100644 --- a/vspd.go +++ b/vspd.go @@ -6,9 +6,9 @@ package main import ( "context" - "crypto/sha256" "fmt" "os" + "path/filepath" "runtime" "sync" @@ -19,11 +19,15 @@ import ( "github.com/decred/vspd/webapi" ) -// maxVoteChangeRecords defines how many vote change records will be stored for -// each ticket. The limit is in place to mitigate DoS attacks on server storage -// space. When storing a new record breaches this limit, the oldest record in -// the database is deleted. -const maxVoteChangeRecords = 10 +const ( + // maxVoteChangeRecords defines how many vote change records will be stored for + // each ticket. The limit is in place to mitigate DoS attacks on server storage + // space. When storing a new record breaches this limit, the oldest record in + // the database is deleted. + maxVoteChangeRecords = 10 + // passwordHashFileName is the name of the file containing admin password hash. + passwordHashFileName = "password.hash" +) func main() { // Create a context that is cancelled when a shutdown request is received @@ -49,18 +53,21 @@ func run(shutdownCtx context.Context) int { return 1 } - // Request admin password if admin password is not set in config. - var adminAuthSHA [32]byte - if cfg.AdminPass == "" { - adminAuthSHA, err = passwordHashPrompt(shutdownCtx, "Admin password for accessing admin page: ") + // Request admin password if admin password hash file is not found. + var adminAuthHash []byte + passwordDir := filepath.Join(cfg.HomeDir, passwordHashFileName) + if fileExists(passwordDir) { + adminAuthHash, err = readPassHashFromFile(passwordDir) if err != nil { fmt.Fprintf(os.Stderr, "cannot use password: %v\n", err) return 1 } } else { - adminAuthSHA = sha256.Sum256([]byte(cfg.AdminPass)) - // Clear password string - cfg.AdminPass = "" + adminAuthHash, err = createPassHashFile(shutdownCtx, passwordDir) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot use password: %v\n", err) + return 1 + } } // Show version at startup. @@ -118,7 +125,7 @@ func run(shutdownCtx context.Context) int { SupportEmail: cfg.SupportEmail, VspClosed: cfg.VspClosed, VspClosedMsg: cfg.VspClosedMsg, - AdminAuthSHA: adminAuthSHA, + AdminAuthHash: adminAuthHash, Debug: cfg.WebServerDebug, Designation: cfg.Designation, MaxVoteChangeRecords: maxVoteChangeRecords, diff --git a/webapi/admin.go b/webapi/admin.go index dce51c84..e14c8243 100644 --- a/webapi/admin.go +++ b/webapi/admin.go @@ -198,8 +198,8 @@ func (s *Server) ticketSearch(c *gin.Context) { // the current session will be authenticated as an admin. func (s *Server) adminLogin(c *gin.Context) { password := c.PostForm("password") - authSHA := sha256.Sum256([]byte(password)) - if subtle.ConstantTimeCompare(s.cfg.AdminAuthSHA[:], authSHA[:]) != 1 { + passwordHash := sha256.Sum256([]byte(password)) + if subtle.ConstantTimeCompare(s.cfg.AdminAuthHash[:], passwordHash[:]) != 1 { log.Warnf("Failed login attempt from %s", c.ClientIP()) c.HTML(http.StatusUnauthorized, "login.html", gin.H{ "WebApiCache": s.cache.getData(), diff --git a/webapi/middleware.go b/webapi/middleware.go index 96fee3d8..46b414bc 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -357,8 +357,8 @@ func (s *Server) 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[:], s.cfg.AdminAuthSHA[:]) != 1 { + passwordHash := sha256.Sum256([]byte(password)) + if !ok || subtle.ConstantTimeCompare(s.cfg.AdminAuthHash[:], passwordHash[:]) != 1 { // Credentials doesn't match, we return 401 and abort handlers chain. c.Header("WWW-Authenticate", `Basic realm="Authorization Required"`) c.AbortWithStatus(http.StatusUnauthorized) diff --git a/webapi/webapi.go b/webapi/webapi.go index 0f02b901..54f13269 100644 --- a/webapi/webapi.go +++ b/webapi/webapi.go @@ -33,7 +33,7 @@ type Config struct { SupportEmail string VspClosed bool VspClosedMsg string - AdminAuthSHA [32]byte + AdminAuthHash []byte Debug bool Designation string MaxVoteChangeRecords int