diff --git a/cmd/subspace/config.go b/cmd/subspace/config.go index da0b1d15..73b2230c 100644 --- a/cmd/subspace/config.go +++ b/cmd/subspace/config.go @@ -17,6 +17,8 @@ import ( "sort" "sync" "time" + + "github.com/pquerna/otp/totp" ) var ( @@ -61,6 +63,7 @@ type Info struct { Email string `json:"email"` Password []byte `json:"password"` Secret string `json:"secret"` + TotpKey string `json:"totp_key"` Configured bool `json:"configure"` Domain string `json:"domain"` HashKey string `json:"hash_key"` @@ -99,6 +102,7 @@ func NewConfig(filename string) (*Config, error) { // Create new config with defaults if os.IsNotExist(err) { c.Info = &Info{ + Email: "null", HashKey: RandomString(32), BlockKey: RandomString(32), } @@ -422,3 +426,32 @@ func (c *Config) save() error { } return Overwrite(c.filename, b, 0644) } + +func (c *Config) ResetTotp() error { + c.Lock() + defer c.Unlock() + + c.Info.TotpKey = "" + + if err := c.save(); err != nil { + return err + } + + return c.GenerateTOTP() +} + +func (c *Config) GenerateTOTP() error { + key, err := totp.Generate( + totp.GenerateOpts{ + Issuer: httpHost, + AccountName: c.Info.Email, + }, + ) + if err != nil { + return err + } + + tempTotpKey = key + + return nil +} diff --git a/cmd/subspace/handlers.go b/cmd/subspace/handlers.go index 5f621f75..229417be 100644 --- a/cmd/subspace/handlers.go +++ b/cmd/subspace/handlers.go @@ -1,7 +1,9 @@ package main import ( + "bytes" "fmt" + "image/png" "io/ioutil" "net/http" "os" @@ -10,6 +12,7 @@ import ( "github.com/crewjam/saml/samlsp" "github.com/julienschmidt/httprouter" + "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" qrcode "github.com/skip2/go-qrcode" @@ -240,6 +243,7 @@ func signinHandler(w *Web) { email := strings.ToLower(strings.TrimSpace(w.r.FormValue("email"))) password := w.r.FormValue("password") + passcode := w.r.FormValue("totp") if email != config.FindInfo().Email { w.Redirect("/signin?error=invalid") @@ -250,6 +254,13 @@ func signinHandler(w *Web) { w.Redirect("/signin?error=invalid") return } + + if config.FindInfo().TotpKey != "" && !totp.Validate(passcode, config.FindInfo().TotpKey) { + // Totp has been configured and the provided code doesn't match + w.Redirect("/signin?error=invalid") + return + } + if err := w.SigninSession(true, ""); err != nil { Error(w.w, err) return @@ -258,6 +269,36 @@ func signinHandler(w *Web) { w.Redirect("/") } +func totpQRHandler(w *Web) { + if !w.Admin { + Error(w.w, fmt.Errorf("failed to view config: permission denied")) + return + } + + if config.Info.TotpKey != "" { + // TOTP is already configured, don't allow the current one to be leaked + w.Redirect("/") + return + } + + var buf bytes.Buffer + img, err := tempTotpKey.Image(200, 200) + if err != nil { + Error(w.w, err) + return + } + + png.Encode(&buf, img) + + w.w.Header().Set("Content-Type", "image/png") + w.w.Header().Set("Content-Length", fmt.Sprintf("%d", len(buf.Bytes()))) + if _, err := w.w.Write(buf.Bytes()); err != nil { + Error(w.w, err) + return + } + +} + func userEditHandler(w *Web) { userID := w.ps.ByName("user") if userID == "" { @@ -569,6 +610,9 @@ func settingsHandler(w *Web) { currentPassword := w.r.FormValue("current_password") newPassword := w.r.FormValue("new_password") + resetTotp := w.r.FormValue("reset_totp") + totpCode := w.r.FormValue("totp_code") + config.UpdateInfo(func(i *Info) error { i.SAML.IDPMetadata = samlMetadata i.Email = email @@ -608,6 +652,26 @@ func settingsHandler(w *Web) { }) } + if resetTotp == "true" { + err := config.ResetTotp() + if err != nil { + w.Redirect("/settings?error=totp") + return + } + + w.Redirect("/settings?success=totp") + return + } + + if config.Info.TotpKey == "" && totpCode != "" { + if !totp.Validate(totpCode, tempTotpKey.Secret()) { + w.Redirect("/settings?error=totp") + return + } + config.Info.TotpKey = tempTotpKey.Secret() + config.save() + } + w.Redirect("/settings?success=settings") } diff --git a/cmd/subspace/main.go b/cmd/subspace/main.go index aef13fb6..1956ac9d 100644 --- a/cmd/subspace/main.go +++ b/cmd/subspace/main.go @@ -18,6 +18,7 @@ import ( "time" "github.com/julienschmidt/httprouter" + "github.com/pquerna/otp" "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" @@ -79,6 +80,9 @@ var ( // theme semanticTheme string + + // Totp + tempTotpKey *otp.Key ) func init() { @@ -144,6 +148,12 @@ func main() { logger.Fatal(err) } + // TOTP + err = config.GenerateTOTP() + if err != nil { + logger.Fatal(err) + } + // Secure token securetoken = securecookie.New([]byte(config.FindInfo().HashKey), []byte(config.FindInfo().BlockKey)) @@ -170,6 +180,7 @@ func main() { r.GET("/saml/acs", Log(samlHandler)) r.POST("/saml/acs", Log(samlHandler)) + r.GET("/totp/image", Log(WebHandler(totpQRHandler, "totp/image"))) r.GET("/signin", Log(WebHandler(signinHandler, "signin"))) r.GET("/signout", Log(WebHandler(signoutHandler, "signout"))) r.POST("/signin", Log(WebHandler(signinHandler, "signin"))) diff --git a/cmd/subspace/web.go b/cmd/subspace/web.go index 3a02ce13..81c55686 100644 --- a/cmd/subspace/web.go +++ b/cmd/subspace/web.go @@ -13,6 +13,7 @@ import ( "github.com/crewjam/saml" "github.com/crewjam/saml/samlsp" + "github.com/pquerna/otp" "golang.org/x/net/publicsuffix" @@ -58,6 +59,7 @@ type Web struct { TargetProfiles []Profile SemanticTheme string + TempTotpKey *otp.Key } func init() { @@ -159,6 +161,7 @@ func WebHandler(h func(*Web), section string) httprouter.Handle { Info: config.FindInfo(), SAML: samlSP, SemanticTheme: semanticTheme, + TempTotpKey: tempTotpKey, } if section == "signin" || section == "forgot" || section == "configure" { diff --git a/go.mod b/go.mod index 66f90e27..1dcf240e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/jteeuwen/go-bindata v3.0.8-0.20180305030458-6025e8de665b+incompatible github.com/julienschmidt/httprouter v1.3.0 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/pquerna/otp v1.2.0 // indirect github.com/sirupsen/logrus v1.6.0 github.com/skip2/go-qrcode v0.0.0-20200519171959-a3b48390827e golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 diff --git a/go.sum b/go.sum index 2994a35b..b555c3a6 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/beevik/etree v1.0.1 h1:lWzdj5v/Pj1X360EV7bUudox5SRipy4qZLjY0rhb0ck= github.com/beevik/etree v1.0.1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/crewjam/httperr v0.0.0-20190612203328-a946449404da h1:WXnT88cFG2davqSFqvaFfzkSMC0lqh/8/rKZ+z7tYvI= @@ -44,6 +46,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok= +github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7 h1:J4AOUcOh/t1XbQcJfkEqhzgvMJ2tDxdCVvmHxW5QXao= github.com/russellhaering/goxmldsig v0.0.0-20180430223755-7acd5e4a6ef7/go.mod h1:Oz4y6ImuOQZxynhbSXk7btjEfNBtGlj2dcaOvXl2FSM= github.com/russellhaering/goxmldsig v1.1.0 h1:lK/zeJie2sqG52ZAlPNn1oBBqsIsEKypUUBGpYYF6lk= @@ -55,6 +59,7 @@ github.com/skip2/go-qrcode v0.0.0-20200519171959-a3b48390827e/go.mod h1:XV66xRDq github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= diff --git a/web/templates/settings.html b/web/templates/settings.html index 0e027f14..be750201 100644 --- a/web/templates/settings.html +++ b/web/templates/settings.html @@ -13,6 +13,8 @@ Device removed successfully {{else if eq $success "configured"}} Admin account is setup. Configure SAML for SSO (optional). + {{else if eq $success "totp"}} + TOTP reset for default user, please reconfigure for improved security. {{end}} @@ -26,6 +28,8 @@