From 4079ab0383335c36fca75b1479cb9f952dd0dae6 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 | 1 + go.sum | 10 +++++++--- prompt.go | 47 +++++++++++++++++++++++++++++++++++++++----- vspd.go | 34 +++++++++++++++++++------------- webapi/admin.go | 4 ++-- webapi/middleware.go | 4 ++-- webapi/webapi.go | 2 +- 8 files changed, 75 insertions(+), 28 deletions(-) diff --git a/config.go b/config.go index 75f6deff..073469f6 100644 --- a/config.go +++ b/config.go @@ -62,7 +62,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 f7935054..87c5ec41 100644 --- a/go.mod +++ b/go.mod @@ -22,5 +22,6 @@ 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 ) diff --git a/go.sum b/go.sum index c1136473..46a57ab7 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..a9bc0716 100644 --- a/prompt.go +++ b/prompt.go @@ -5,6 +5,7 @@ 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 2eb5cf7a..2a6388d8 100644 --- a/vspd.go +++ b/vspd.go @@ -6,10 +6,10 @@ package main import ( "context" - "crypto/sha256" "errors" "fmt" "os" + "path/filepath" "runtime" "sync" @@ -20,11 +20,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 @@ -52,17 +56,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: ") + // 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 { return fmt.Errorf("cannot use password: %v", err) } } else { - adminAuthSHA = sha256.Sum256([]byte(cfg.AdminPass)) - // Clear password string - cfg.AdminPass = "" + adminAuthHash, err = createPassHashFile(ctx, passwordDir) + if err != nil { + return fmt.Errorf("cannot use password: %v", err) + } } // Show version at startup. @@ -112,7 +118,7 @@ func run(ctx context.Context) error { 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 a06c7911..2026caf1 100644 --- a/webapi/admin.go +++ b/webapi/admin.go @@ -198,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") - authSHA := sha256.Sum256([]byte(password)) - if subtle.ConstantTimeCompare(cfg.AdminAuthSHA[:], authSHA[:]) != 1 { + passwordHash := sha256.Sum256([]byte(password)) + if subtle.ConstantTimeCompare(cfg.AdminAuthHash[:], passwordHash[:]) != 1 { log.Warnf("Failed login attempt from %s", c.ClientIP()) c.HTML(http.StatusUnauthorized, "login.html", gin.H{ "WebApiCache": getCache(), diff --git a/webapi/middleware.go b/webapi/middleware.go index 7356acc9..e488cb28 100644 --- a/webapi/middleware.go +++ b/webapi/middleware.go @@ -392,8 +392,8 @@ 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 { + passwordHash := sha256.Sum256([]byte(password)) + if !ok || subtle.ConstantTimeCompare(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 16755de6..3ce21239 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