dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

login.go (24953B)


      1 package handlers
      2 
      3 import (
      4 	"bytes"
      5 	"dkforest/pkg/cache"
      6 	"dkforest/pkg/captcha"
      7 	"dkforest/pkg/config"
      8 	"dkforest/pkg/database"
      9 	dutils "dkforest/pkg/database/utils"
     10 	"dkforest/pkg/managers"
     11 	"dkforest/pkg/utils"
     12 	hutils "dkforest/pkg/web/handlers/utils"
     13 	"fmt"
     14 	"github.com/asaskevich/govalidator"
     15 	"github.com/labstack/echo"
     16 	"github.com/pquerna/otp/totp"
     17 	"github.com/sirupsen/logrus"
     18 	"golang.org/x/crypto/bcrypt"
     19 	"net/http"
     20 	"strings"
     21 	"time"
     22 )
     23 
     24 const max2faAttempts = 4
     25 
     26 // partialAuthCache keep track of partial auth token -> user id.
     27 // When a user login and have 2fa enabled, we create a "partial" auth cookie.
     28 // The token can be used to complete the 2fa authentication.
     29 var partialAuthCache = cache.New[*PartialAuthItem](2*time.Minute, time.Hour)
     30 
     31 type PartialAuthItem struct {
     32 	UserID          database.UserID
     33 	Step            PartialAuthStep // Inform which type of 2fa the user is supposed to complete
     34 	SessionDuration time.Duration
     35 	Attempt         int
     36 }
     37 
     38 func NewPartialAuthItem(userID database.UserID, step PartialAuthStep, sessionDuration time.Duration) *PartialAuthItem {
     39 	return &PartialAuthItem{UserID: userID, Step: step, SessionDuration: sessionDuration}
     40 }
     41 
     42 type PartialAuthStep string
     43 
     44 const (
     45 	TwoFactorStep PartialAuthStep = "2fa"
     46 	PgpSignStep   PartialAuthStep = "pgp_sign_2fa"
     47 	PgpStep       PartialAuthStep = "pgp_2fa"
     48 )
     49 
     50 // Password recovery flow has 3 steps
     51 // 1- Ask for username & captcha & gpg method
     52 // 2- Validate gpg token/signature
     53 // 3- Reset password
     54 // Since the user is not authenticated in any of these steps, we need to guard each steps and ensure the user can access it legitimately.
     55 // partialRecoveryCache keeps track of users that are in the process of recovering their password and the step they're at.
     56 var (
     57 	partialRecoveryCache = cache.New[PartialRecoveryItem](10*time.Minute, time.Hour)
     58 )
     59 
     60 type PartialRecoveryItem struct {
     61 	UserID database.UserID
     62 	Step   RecoveryStep
     63 }
     64 
     65 type RecoveryStep int64
     66 
     67 const (
     68 	RecoveryCaptchaCompleted RecoveryStep = iota + 1
     69 	RecoveryGpgValidated
     70 )
     71 
     72 func firstUseHandler(c echo.Context) error {
     73 	user := c.Get("authUser").(*database.User)
     74 	db := c.Get("database").(*database.DkfDB)
     75 	var data firstUseData
     76 	if user != nil {
     77 		return c.Redirect(http.StatusFound, "/")
     78 	}
     79 
     80 	if c.Request().Method == http.MethodGet {
     81 		//data.Username = "admin"
     82 		//data.Password = "admin123"
     83 		//data.RePassword = "admin123"
     84 		//data.Email = "admin@admin.admin"
     85 		return c.Render(http.StatusOK, "standalone.first-use", data)
     86 	}
     87 
     88 	data.Username = c.Request().PostFormValue("username")
     89 	data.Password = c.Request().PostFormValue("password")
     90 	data.RePassword = c.Request().PostFormValue("repassword")
     91 	newUser, errs := db.CreateFirstUser(data.Username, data.Password, data.RePassword)
     92 	data.Errors = errs
     93 	if errs.HasError() {
     94 		return c.Render(http.StatusOK, "standalone.first-use", data)
     95 	}
     96 
     97 	_, errs = db.CreateZeroUser()
     98 
     99 	config.IsFirstUse.SetFalse()
    100 
    101 	session := db.DoCreateSession(newUser.ID, c.Request().UserAgent(), time.Hour*24*30)
    102 	c.SetCookie(createSessionCookie(session.Token, time.Hour*24*30))
    103 
    104 	return c.Redirect(http.StatusFound, "/")
    105 }
    106 
    107 func LoginHandler(c echo.Context) error {
    108 
    109 	if config.ProtectHome.IsTrue() {
    110 		return c.NoContent(http.StatusNotFound)
    111 	}
    112 
    113 	return loginHandler(c)
    114 }
    115 
    116 func LoginAttackHandler(c echo.Context) error {
    117 	key := c.Param("loginToken")
    118 	loginLink, found := tempLoginCache.Get("login_link")
    119 	if !found {
    120 		return c.NoContent(http.StatusNotFound)
    121 	}
    122 	// We use the "Dangerous" version of VerifyString, to avoid invalidating the captcha.
    123 	// This way, the captcha can be used multiple times by different users until it's time has expired.
    124 	if err := captcha.VerifyStringDangerous(tempLoginStore, loginLink.ID, key); err != nil {
    125 		// If the captcha was invalid, kill the circuit.
    126 		hutils.KillCircuit(c)
    127 		time.Sleep(utils.RandSec(3, 5))
    128 		return c.NoContent(http.StatusNotFound)
    129 	}
    130 
    131 	return loginHandler(c)
    132 }
    133 
    134 func loginHandler(c echo.Context) error {
    135 	formName := c.Request().PostFormValue("formName")
    136 	if formName == "pgp_2fa" {
    137 		token := c.Request().PostFormValue("token")
    138 		return SessionsGpgTwoFactorHandler(c, false, token)
    139 	} else if formName == "pgp_sign_2fa" {
    140 		token := c.Request().PostFormValue("token")
    141 		return SessionsGpgSignTwoFactorHandler(c, false, token)
    142 	} else if formName == "2fa" {
    143 		token := c.Request().PostFormValue("token")
    144 		return SessionsTwoFactorHandler(c, false, token)
    145 	} else if formName == "2fa_recovery" {
    146 		token := c.Request().PostFormValue("token")
    147 		return SessionsTwoFactorRecoveryHandler(c, token)
    148 	} else if formName == "" {
    149 		return loginFormHandler(c)
    150 	}
    151 	return c.Redirect(http.StatusFound, "/")
    152 }
    153 
    154 // SessionsGpgTwoFactorHandler ...
    155 func SessionsGpgTwoFactorHandler(c echo.Context, step1 bool, token string) error {
    156 	db := c.Get("database").(*database.DkfDB)
    157 	item, found := partialAuthCache.Get(token)
    158 	if !found || item.Step != PgpStep {
    159 		return c.Redirect(http.StatusFound, "/")
    160 	}
    161 
    162 	user, err := db.GetUserByID(item.UserID)
    163 	if err != nil {
    164 		logrus.Errorf("failed to get user %d", item.UserID)
    165 		return c.Redirect(http.StatusFound, "/")
    166 	}
    167 
    168 	cleanup := func() {
    169 		pgpTokenCache.Delete(user.ID)
    170 		partialAuthCache.Delete(token)
    171 	}
    172 
    173 	var data sessionsGpgTwoFactorData
    174 	data.Token = token
    175 
    176 	if step1 {
    177 		msg, err := generatePgpEncryptedTokenMessage(user.ID, user.GPGPublicKey)
    178 		if err != nil {
    179 			data.Error = err.Error()
    180 			return c.Render(http.StatusOK, "sessions-gpg-two-factor", data)
    181 		}
    182 		if expiredTime, _ := utils.GetKeyExpiredTime(user.GPGPublicKey); expiredTime != nil {
    183 			if expiredTime.AddDate(0, -1, 0).Before(time.Now()) {
    184 				chatMsg := fmt.Sprintf("Your PGP key expires in less than a month (%s)", expiredTime.Format("Jan 02, 2006 15:04:05"))
    185 				dutils.ZeroSendMsg(db, user.ID, chatMsg)
    186 			}
    187 		}
    188 		data.EncryptedMessage = msg
    189 		return c.Render(http.StatusOK, "sessions-gpg-two-factor", data)
    190 	}
    191 
    192 	pgpToken, found := pgpTokenCache.Get(user.ID)
    193 	if !found {
    194 		return c.Redirect(http.StatusFound, "/")
    195 	}
    196 	data.EncryptedMessage = c.Request().PostFormValue("encrypted_message")
    197 	data.Code = c.Request().PostFormValue("pgp_code")
    198 	if data.Code != pgpToken.Value {
    199 		item.Attempt++
    200 		if item.Attempt >= max2faAttempts {
    201 			cleanup()
    202 			return c.Redirect(http.StatusFound, "/")
    203 		}
    204 		data.ErrorCode = "invalid code"
    205 		return c.Render(http.StatusOK, "sessions-gpg-two-factor", data)
    206 	}
    207 	cleanup()
    208 
    209 	if user.HasTotpEnabled() {
    210 		token := utils.GenerateToken32()
    211 		partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration))
    212 		return SessionsTwoFactorHandler(c, true, token)
    213 	}
    214 
    215 	return completeLogin(c, user, item.SessionDuration)
    216 }
    217 
    218 // SessionsGpgSignTwoFactorHandler ...
    219 func SessionsGpgSignTwoFactorHandler(c echo.Context, step1 bool, token string) error {
    220 	db := c.Get("database").(*database.DkfDB)
    221 	item, found := partialAuthCache.Get(token)
    222 	if !found || item.Step != PgpSignStep {
    223 		return c.Redirect(http.StatusFound, "/")
    224 	}
    225 
    226 	user, err := db.GetUserByID(item.UserID)
    227 	if err != nil {
    228 		logrus.Errorf("failed to get user %d", item.UserID)
    229 		return c.Redirect(http.StatusFound, "/")
    230 	}
    231 
    232 	cleanup := func() {
    233 		pgpTokenCache.Delete(user.ID)
    234 		partialAuthCache.Delete(token)
    235 	}
    236 
    237 	var data sessionsGpgSignTwoFactorData
    238 	data.Token = token
    239 
    240 	if step1 {
    241 		data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID, user.GPGPublicKey)
    242 		return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data)
    243 	}
    244 
    245 	pgpToken, found := pgpTokenCache.Get(user.ID)
    246 	if !found {
    247 		return c.Redirect(http.StatusFound, "/")
    248 	}
    249 	data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message")
    250 	data.SignedMessage = c.Request().PostFormValue("signed_message")
    251 
    252 	if !utils.PgpCheckSignMessage(pgpToken.PKey, pgpToken.Value, data.SignedMessage) {
    253 		item.Attempt++
    254 		if item.Attempt >= max2faAttempts {
    255 			cleanup()
    256 			return c.Redirect(http.StatusFound, "/")
    257 		}
    258 		data.ErrorSignedMessage = "invalid signature"
    259 		return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data)
    260 	}
    261 	cleanup()
    262 
    263 	if user.HasTotpEnabled() {
    264 		token := utils.GenerateToken32()
    265 		partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration))
    266 		return SessionsTwoFactorHandler(c, true, token)
    267 	}
    268 
    269 	return completeLogin(c, user, item.SessionDuration)
    270 }
    271 
    272 // SessionsTwoFactorHandler ...
    273 func SessionsTwoFactorHandler(c echo.Context, step1 bool, token string) error {
    274 	db := c.Get("database").(*database.DkfDB)
    275 	item, found := partialAuthCache.Get(token)
    276 	if !found || item.Step != TwoFactorStep {
    277 		return c.Redirect(http.StatusFound, "/")
    278 	}
    279 	cleanup := func() { partialAuthCache.Delete(token) }
    280 
    281 	var data sessionsTwoFactorData
    282 	data.Token = token
    283 	if !step1 {
    284 		code := c.Request().PostFormValue("code")
    285 		user, err := db.GetUserByID(item.UserID)
    286 		if err != nil {
    287 			logrus.Errorf("failed to get user %d", item.UserID)
    288 			return c.Redirect(http.StatusFound, "/")
    289 		}
    290 		secret := string(user.TwoFactorSecret)
    291 		if !totp.Validate(code, secret) {
    292 			item.Attempt++
    293 			if item.Attempt >= max2faAttempts {
    294 				cleanup()
    295 				return c.Redirect(http.StatusFound, "/")
    296 			}
    297 			data.Error = "Two-factor authentication failed."
    298 			return c.Render(http.StatusOK, "sessions-two-factor", data)
    299 		}
    300 
    301 		cleanup()
    302 		return completeLogin(c, user, item.SessionDuration)
    303 	}
    304 	return c.Render(http.StatusOK, "sessions-two-factor", data)
    305 }
    306 
    307 // SessionsTwoFactorRecoveryHandler ...
    308 func SessionsTwoFactorRecoveryHandler(c echo.Context, token string) error {
    309 	db := c.Get("database").(*database.DkfDB)
    310 	item, found := partialAuthCache.Get(token)
    311 	if !found {
    312 		return c.Redirect(http.StatusFound, "/")
    313 	}
    314 	cleanup := func() { partialAuthCache.Delete(token) }
    315 
    316 	var data sessionsTwoFactorRecoveryData
    317 	data.Token = token
    318 	recoveryCode := c.Request().PostFormValue("code")
    319 	if recoveryCode != "" {
    320 		user, err := db.GetUserByID(item.UserID)
    321 		if err != nil {
    322 			logrus.Errorf("failed to get user %d", item.UserID)
    323 			return c.Redirect(http.StatusFound, "/")
    324 		}
    325 		if err := bcrypt.CompareHashAndPassword([]byte(user.TwoFactorRecovery), []byte(recoveryCode)); err != nil {
    326 			data.Error = "Recovery code authentication failed"
    327 			return c.Render(http.StatusOK, "sessions-two-factor-recovery", data)
    328 		}
    329 		cleanup()
    330 		return completeLogin(c, user, item.SessionDuration)
    331 	}
    332 	return c.Render(http.StatusOK, "sessions-two-factor-recovery", data)
    333 }
    334 
    335 func loginFormHandler(c echo.Context) error {
    336 	db := c.Get("database").(*database.DkfDB)
    337 	var data loginData
    338 	data.Redirect = c.QueryParam("redirect")
    339 	data.Autofocus = 0
    340 	data.HomeUsersList = config.HomeUsersList.Load()
    341 
    342 	if data.HomeUsersList {
    343 		data.Online = managers.ActiveUsers.GetActiveUsers()
    344 	}
    345 
    346 	actualLogin := func(username, password string, sessionDuration time.Duration, captchaSolved bool) error {
    347 		username = strings.TrimSpace(username)
    348 		user, err := db.GetVerifiedUserByUsername(database.Username(username))
    349 		if err != nil {
    350 			time.Sleep(utils.RandMs(50, 200))
    351 			data.Error = "Invalid username/password"
    352 			return c.Render(http.StatusOK, "standalone.login", data)
    353 		}
    354 
    355 		user.IncrLoginAttempts(db)
    356 
    357 		if user.LoginAttempts > 4 && !captchaSolved {
    358 			data.CaptchaRequired = true
    359 			data.Autofocus = 2
    360 			data.Error = "Captcha required"
    361 			data.CaptchaID, data.CaptchaImg = captcha.New()
    362 			data.Password = password
    363 			captchaID := c.Request().PostFormValue("captcha_id")
    364 			captchaInput := c.Request().PostFormValue("captcha")
    365 			if captchaInput == "" {
    366 				return c.Render(http.StatusOK, "standalone.login", data)
    367 			} else {
    368 				if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
    369 					data.Error = "Invalid captcha"
    370 					return c.Render(http.StatusOK, "standalone.login", data)
    371 				}
    372 			}
    373 		}
    374 
    375 		if !user.CheckPassword(db, password) {
    376 			data.Password = ""
    377 			data.Autofocus = 1
    378 			data.Error = "Invalid username/password"
    379 			return c.Render(http.StatusOK, "standalone.login", data)
    380 		}
    381 
    382 		if user.GpgTwoFactorEnabled || user.HasTotpEnabled() {
    383 			token := utils.GenerateToken32()
    384 			var twoFactorType PartialAuthStep
    385 			var twoFactorClb func(echo.Context, bool, string) error
    386 			if user.GpgTwoFactorEnabled && user.GpgTwoFactorMode {
    387 				twoFactorType = PgpSignStep
    388 				twoFactorClb = SessionsGpgSignTwoFactorHandler
    389 			} else if user.GpgTwoFactorEnabled {
    390 				twoFactorType = PgpStep
    391 				twoFactorClb = SessionsGpgTwoFactorHandler
    392 			} else if user.HasTotpEnabled() {
    393 				twoFactorType = TwoFactorStep
    394 				twoFactorClb = SessionsTwoFactorHandler
    395 			}
    396 			partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, twoFactorType, sessionDuration))
    397 			return twoFactorClb(c, true, token)
    398 		}
    399 
    400 		return completeLogin(c, user, sessionDuration)
    401 	}
    402 
    403 	usernameQuery := c.QueryParam("u")
    404 	passwordQuery := c.QueryParam("p")
    405 	if usernameQuery == "darkforestAdmin" && passwordQuery != "" {
    406 		return actualLogin(usernameQuery, passwordQuery, time.Hour*24, false)
    407 	}
    408 
    409 	if config.ForceLoginCaptcha.IsTrue() {
    410 		data.CaptchaID, data.CaptchaImg = captcha.New()
    411 		data.CaptchaRequired = true
    412 	}
    413 
    414 	if c.Request().Method == http.MethodGet {
    415 		data.SessionDurationSec = 604800
    416 		return c.Render(http.StatusOK, "standalone.login", data)
    417 	}
    418 
    419 	captchaSolved := false
    420 
    421 	data.Username = strings.TrimSpace(c.FormValue("username"))
    422 	password := c.FormValue("password")
    423 	data.SessionDurationSec = utils.Clamp(utils.DoParseInt64(c.Request().PostFormValue("session_duration")), 60, utils.OneMonthSecs)
    424 	sessionDuration := time.Duration(data.SessionDurationSec) * time.Second
    425 
    426 	if config.ForceLoginCaptcha.IsTrue() {
    427 		data.CaptchaRequired = true
    428 		captchaID := c.Request().PostFormValue("captcha_id")
    429 		captchaInput := c.Request().PostFormValue("captcha")
    430 		if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
    431 			data.ErrCaptcha = err.Error()
    432 			return c.Render(http.StatusOK, "standalone.login", data)
    433 		}
    434 		captchaSolved = true
    435 	}
    436 
    437 	return actualLogin(data.Username, password, sessionDuration, captchaSolved)
    438 }
    439 
    440 func completeLogin(c echo.Context, user database.User, sessionDuration time.Duration) error {
    441 	db := c.Get("database").(*database.DkfDB)
    442 	user.ResetLoginAttempts(db)
    443 
    444 	for _, session := range db.GetActiveUserSessions(user.ID) {
    445 		msg := fmt.Sprintf(`New login`)
    446 		db.CreateSessionNotification(msg, session.Token)
    447 	}
    448 
    449 	session := db.DoCreateSession(user.ID, c.Request().UserAgent(), sessionDuration)
    450 	db.CreateSecurityLog(user.ID, database.LoginSecurityLog)
    451 	c.SetCookie(createSessionCookie(session.Token, sessionDuration))
    452 
    453 	redirectURL := "/"
    454 	redir := c.QueryParam("redirect")
    455 	if redir != "" && strings.HasPrefix(redir, "/") {
    456 		redirectURL = redir
    457 	}
    458 	return c.Redirect(http.StatusFound, redirectURL)
    459 }
    460 
    461 func LoginCompletedHandler(c echo.Context) error {
    462 	authUser := c.Get("authUser").(*database.User)
    463 	var data loginCompletedData
    464 	data.SecretPhrase = string(authUser.SecretPhrase)
    465 	data.RedirectURL = "/"
    466 	redir := c.QueryParam("redirect")
    467 	if redir != "" && strings.HasPrefix(redir, "/") {
    468 		data.RedirectURL = redir
    469 	}
    470 	return c.Render(http.StatusOK, "login-completed", data)
    471 }
    472 
    473 // LogoutHandler for logout route
    474 func LogoutHandler(ctx echo.Context) error {
    475 	authUser := ctx.Get("authUser").(*database.User)
    476 	db := ctx.Get("database").(*database.DkfDB)
    477 	c, _ := ctx.Cookie(hutils.AuthCookieName)
    478 	if err := db.DeleteSessionByToken(c.Value); err != nil {
    479 		logrus.Error("Failed to remove session from db : ", err)
    480 	}
    481 	if authUser.TerminateAllSessionsOnLogout {
    482 		// Delete active user sessions
    483 		if err := db.DeleteUserSessions(authUser.ID); err != nil {
    484 			logrus.Error("failed to delete user sessions : ", err)
    485 		}
    486 	}
    487 	db.CreateSecurityLog(authUser.ID, database.LogoutSecurityLog)
    488 	ctx.SetCookie(hutils.DeleteCookie(hutils.AuthCookieName))
    489 	managers.ActiveUsers.RemoveUser(authUser.ID)
    490 	if authUser.Temp {
    491 		if err := db.DB().Where("id = ?", authUser.ID).Unscoped().Delete(&database.User{}).Error; err != nil {
    492 			logrus.Error(err)
    493 		}
    494 	}
    495 	return ctx.Redirect(http.StatusFound, "/")
    496 }
    497 
    498 // ForgotPasswordHandler ...
    499 func ForgotPasswordHandler(c echo.Context) error {
    500 	return waitPageWrapper(c, forgotPasswordHandler, hutils.WaitCookieName)
    501 }
    502 
    503 func forgotPasswordHandler(c echo.Context) error {
    504 	db := c.Get("database").(*database.DkfDB)
    505 	var data forgotPasswordData
    506 	data.Redirect = c.QueryParam("redirect")
    507 	const (
    508 		usernameCaptchaStep = iota + 1
    509 		gpgCodeSignatureStep
    510 		resetPasswordStep
    511 
    512 		forgotPasswordTmplName = "standalone.forgot-password"
    513 	)
    514 	data.Step = usernameCaptchaStep
    515 
    516 	data.CaptchaSec = 120
    517 	data.Frames = generateCssFrames(data.CaptchaSec, nil, true)
    518 
    519 	data.CaptchaID, data.CaptchaImg = captcha.New()
    520 
    521 	if c.Request().Method == http.MethodGet {
    522 		return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    523 	}
    524 
    525 	// POST
    526 
    527 	formName := c.Request().PostFormValue("form_name")
    528 
    529 	if formName == "step1" {
    530 		// Receive and validate Username/Captcha
    531 		data.Step = usernameCaptchaStep
    532 		data.Username = database.Username(c.Request().PostFormValue("username"))
    533 		captchaID := c.Request().PostFormValue("captcha_id")
    534 		captchaInput := c.Request().PostFormValue("captcha")
    535 		data.GpgMode = utils.DoParseBool(c.Request().PostFormValue("gpg_mode"))
    536 
    537 		if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
    538 			data.ErrCaptcha = err.Error()
    539 			return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    540 		}
    541 		user, err := db.GetUserByUsername(data.Username)
    542 		if err != nil {
    543 			data.UsernameError = "no such user"
    544 			return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    545 		}
    546 		userGPGPublicKey := user.GPGPublicKey
    547 		if userGPGPublicKey == "" {
    548 			data.UsernameError = "user has no gpg public key"
    549 			return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    550 		}
    551 		if user.GpgTwoFactorEnabled {
    552 			data.UsernameError = "user has gpg two-factors enabled"
    553 			return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    554 		}
    555 
    556 		if data.GpgMode {
    557 			data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID, userGPGPublicKey)
    558 
    559 		} else {
    560 			msg, err := generatePgpEncryptedTokenMessage(user.ID, userGPGPublicKey)
    561 			if err != nil {
    562 				data.Error = err.Error()
    563 				return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    564 			}
    565 			data.EncryptedMessage = msg
    566 		}
    567 
    568 		token := utils.GenerateToken32()
    569 		partialRecoveryCache.SetD(token, PartialRecoveryItem{user.ID, RecoveryCaptchaCompleted})
    570 
    571 		data.Token = token
    572 		data.Step = gpgCodeSignatureStep
    573 		return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    574 
    575 	} else if formName == "step2" {
    576 		// Receive and validate GPG code/signature
    577 		data.Step = gpgCodeSignatureStep
    578 
    579 		// Step2 is guarded by the "token" that must be valid
    580 		data.Token = c.Request().PostFormValue("token")
    581 		item, found := partialRecoveryCache.Get(data.Token)
    582 		if !found || item.Step != RecoveryCaptchaCompleted {
    583 			return c.Redirect(http.StatusFound, "/")
    584 		}
    585 		userID := item.UserID
    586 
    587 		pgpToken, found := pgpTokenCache.Get(userID)
    588 		if !found {
    589 			return c.Redirect(http.StatusFound, "/")
    590 		}
    591 
    592 		data.GpgMode = utils.DoParseBool(c.Request().PostFormValue("gpg_mode"))
    593 		if data.GpgMode {
    594 			data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message")
    595 			data.SignedMessage = c.Request().PostFormValue("signed_message")
    596 			if !utils.PgpCheckSignMessage(pgpToken.PKey, pgpToken.Value, data.SignedMessage) {
    597 				data.ErrorSignedMessage = "invalid signature"
    598 				return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    599 			}
    600 
    601 		} else {
    602 			data.EncryptedMessage = c.Request().PostFormValue("encrypted_message")
    603 			data.Code = c.Request().PostFormValue("pgp_code")
    604 			if data.Code != pgpToken.Value {
    605 				data.ErrorCode = "invalid code"
    606 				return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    607 			}
    608 		}
    609 
    610 		pgpTokenCache.Delete(userID)
    611 		partialRecoveryCache.SetD(data.Token, PartialRecoveryItem{userID, RecoveryGpgValidated})
    612 
    613 		data.Step = resetPasswordStep
    614 		return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    615 
    616 	} else if formName == "step3" {
    617 		// Receive and validate new password
    618 		data.Step = resetPasswordStep
    619 
    620 		// Step3 is guarded by the "token" that must be valid
    621 		token := c.Request().PostFormValue("token")
    622 		item, found := partialRecoveryCache.Get(token)
    623 		if !found || item.Step != RecoveryGpgValidated {
    624 			return c.Redirect(http.StatusFound, "/")
    625 		}
    626 		userID := item.UserID
    627 		user, err := db.GetUserByID(userID)
    628 		if err != nil {
    629 			return c.Redirect(http.StatusFound, "/")
    630 		}
    631 
    632 		newPassword := c.Request().PostFormValue("newPassword")
    633 		rePassword := c.Request().PostFormValue("rePassword")
    634 		data.NewPassword = newPassword
    635 		data.RePassword = rePassword
    636 
    637 		hashedPassword, err := database.NewPasswordValidator(db, newPassword).CompareWith(rePassword).Hash()
    638 		if err != nil {
    639 			data.ErrorNewPassword = err.Error()
    640 			return c.Render(http.StatusOK, forgotPasswordTmplName, data)
    641 		}
    642 
    643 		if err := user.ChangePassword(db, hashedPassword); err != nil {
    644 			logrus.Error(err)
    645 		}
    646 		db.CreateSecurityLog(user.ID, database.PasswordRecoverySecurityLog)
    647 
    648 		partialRecoveryCache.Delete(token)
    649 		c.SetCookie(hutils.DeleteCookie(hutils.WaitCookieName))
    650 
    651 		return c.Render(http.StatusFound, "flash", FlashResponse{Message: "Password reset done", Redirect: "/login"})
    652 	}
    653 
    654 	return c.Render(http.StatusOK, "flash", FlashResponse{"should not go here", "/login", "alert-danger"})
    655 }
    656 
    657 func protectHomeHandler(c echo.Context) error {
    658 	if c.Request().Method == http.MethodPost {
    659 		return c.NoContent(http.StatusNotFound)
    660 	}
    661 	captchaQuery := c.QueryParam("captcha")
    662 	loginQuery := c.QueryParam("login")
    663 	signupQuery := c.QueryParam("signup")
    664 	if captchaQuery != "" {
    665 		if len(captchaQuery) > 6 || len(loginQuery) > 1 || len(signupQuery) > 1 ||
    666 			!govalidator.IsASCII(captchaQuery) || !govalidator.IsASCII(loginQuery) || !govalidator.IsASCII(signupQuery) {
    667 			time.Sleep(utils.RandSec(3, 7))
    668 			return c.NoContent(http.StatusOK)
    669 		}
    670 		redirectTo := "/login/" + captchaQuery
    671 		if signupQuery == "1" {
    672 			redirectTo = "/signup/" + captchaQuery
    673 		}
    674 		time.Sleep(utils.RandSec(1, 2))
    675 		return c.Redirect(http.StatusFound, redirectTo)
    676 	}
    677 	loginLink, found := tempLoginCache.Get("login_link")
    678 	if !found {
    679 		loginLink.ID, loginLink.Img = captcha.NewWithParams(captcha.Params{Store: tempLoginStore})
    680 		loginLink.ValidUntil = time.Now().Add(3 * time.Minute)
    681 		tempLoginCache.SetD("login_link", loginLink)
    682 	}
    683 
    684 	waitTime := int64(time.Until(loginLink.ValidUntil).Seconds())
    685 
    686 	// Generate css frames
    687 	frames := generateCssFrames(waitTime, func(i int64) string {
    688 		return utils.ShortDur(time.Duration(i) * time.Second)
    689 	}, true)
    690 
    691 	time.Sleep(utils.RandSec(1, 2))
    692 	bufTmp := make([]byte, 0, 1024*4)
    693 	buf := bytes.NewBuffer(bufTmp)
    694 	buf.Write([]byte(`<!DOCTYPE html><html lang="en"><head>
    695     <link href="/public/img/favicon.ico" rel="icon" type="image/x-icon" />
    696     <meta charset="UTF-8" />
    697     <meta name="author" content="n0tr1v">
    698     <meta name="language" content="English">
    699     <meta name="revisit-after" content="1 days">
    700     <meta http-equiv="expires" content="0">
    701     <meta http-equiv="pragma" content="no-cache">
    702     <title>DarkForest</title>
    703     <style>
    704         body, html { height: 100%; width:100%; display:table; background-color: #222; color: white; line-height: 25px;
    705         font-family: Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; }
    706         body { display:table-cell; vertical-align:middle; }
    707         #parent { display: table; width: 100%; }
    708         #form_login { display: table; margin: auto; }
    709         .captcha-img { transition: transform .2s; }
    710         .captcha-img:hover { transform: scale(2.5); }
    711         #timer_countdown:before {
    712             content: "`))
    713 	buf.Write([]byte(utils.ShortDur(time.Duration(waitTime) * time.Second)))
    714 	buf.Write([]byte(`";
    715             animation: `))
    716 	buf.Write([]byte(utils.FormatInt64(waitTime)))
    717 	buf.Write([]byte(`s 1s forwards timer_countdown_frames;
    718         }
    719         @keyframes timer_countdown_frames {`))
    720 	for _, frame := range frames {
    721 		buf.Write([]byte(frame))
    722 	}
    723 	buf.Write([]byte(`
    724         }
    725     </style>
    726 </head>
    727 <body class="bg">
    728 
    729 <div id="parent">
    730     <div id="form_login">
    731         <div class="text-center">
    732             <p>
    733                 To login go to <code>/login/XXXXXX</code><br />
    734                 To register go to <code>/signup/XXXXXX</code><br />
    735                 (replace X by the numbers in the image)<br />
    736                 Link valid for <strong><span id="timer_countdown"></span></strong>
    737             </p>
    738             <img src="data:image/png;base64,`))
    739 	buf.Write([]byte(loginLink.Img))
    740 	buf.Write([]byte(`" style="background-color: hsl(0, 0%, 90%);" class="captcha-img" />
    741             <form method="get">
    742                 <input type="text" name="captcha" maxlength="6" autofocus />
    743                 <button name="login" value="1" type="submit">Login</button>
    744                 <button name="signup" value="1" type="submit">Register</button>
    745             </form>
    746         </div>
    747     </div>
    748 </div>
    749 
    750 </body>
    751 </html>`))
    752 	return c.HTMLBlob(http.StatusOK, buf.Bytes())
    753 }