dkforest

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

commit 50acf96c5080afad2b5042fc6e3819672c5951db
parent 499c0a585f7a7d3bb7608c2a7b61918f74f83561
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Thu,  1 Dec 2022 21:57:01 -0500

gpg 2fa signature mode

Diffstat:
Mpkg/database/tableUsers.go | 1+
Apkg/migrations/114.sql | 4++++
Mpkg/utils/utils.go | 11+++++++++++
Mpkg/web/handlers/data.go | 14+++++++++++---
Mpkg/web/handlers/handlers.go | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apkg/web/public/views/pages/sessions-gpg-sign-two-factor.gohtml | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/web/public/views/pages/two-factor-authentication-gpg.gohtml | 6++++++
Mpkg/web/web.go | 2++
8 files changed, 153 insertions(+), 3 deletions(-)

diff --git a/pkg/database/tableUsers.go b/pkg/database/tableUsers.go @@ -26,6 +26,7 @@ type User struct { TwoFactorRecovery string `json:"-"` SecretPhrase EncryptedString `json:"-"` GpgTwoFactorEnabled bool + GpgTwoFactorMode bool // false -> decrypt; true -> sign CreatedAt time.Time UpdatedAt time.Time DeletedAt *time.Time diff --git a/pkg/migrations/114.sql b/pkg/migrations/114.sql @@ -0,0 +1,4 @@ +-- +migrate Up +ALTER TABLE users ADD COLUMN gpg_two_factor_mode TINYINT(1) DEFAULT 0; + +-- +migrate Down diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go @@ -373,6 +373,17 @@ func getGCMKeyBytes(keyBytes []byte) (cipher.AEAD, int, error) { return gcm, nonceSize, nil } +func PgpCheckSignMessage(pkey, msg, signature string) error { + keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(pkey)) + if err != nil { + return errors.New("invalid public key") + } + if _, err = openpgp.CheckArmoredDetachedSignature(keyring, strings.NewReader(msg), strings.NewReader(signature), nil); err != nil { + return err + } + return nil +} + func PgpDecryptMessage(secretKey, msg string) (string, error) { readerMsg := bytes.NewReader([]byte(msg)) block, err := armor.Decode(readerMsg) diff --git a/pkg/web/handlers/data.go b/pkg/web/handlers/data.go @@ -47,6 +47,13 @@ type sessionsGpgTwoFactorData struct { ErrorCode string } +type sessionsGpgSignTwoFactorData struct { + ToBeSignedMessage string + SignedMessage string + Error string + ErrorSignedMessage string +} + type sessionsTwoFactorRecoveryData struct { Error string } @@ -517,9 +524,10 @@ type diableTotpData struct { } type gpgTwoFactorAuthenticationVerifyData struct { - IsEnabled bool - Password string - ErrorPassword string + IsEnabled bool + GpgTwoFactorMode bool + Password string + ErrorPassword string } type twoFactorAuthenticationVerifyData struct { diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go @@ -301,6 +301,9 @@ func loginHandler(c echo.Context) error { partialAuthCache.Set(token, user.ID, cache1.DefaultExpiration) c.SetCookie(createPartialSessionCookie(token, 10*utils.OneMinuteSecs)) redirectURL := "/sessions/gpg-two-factor" + if user.GpgTwoFactorMode { + redirectURL = "/sessions/gpg-sign-two-factor" + } if redir != "" { redirectURL += "?redirect=" + redir } @@ -449,6 +452,64 @@ func SessionsGpgTwoFactorHandler(c echo.Context) error { return completeLogin(c, user, c.RealIP()) } +// SessionsGpgSignTwoFactorHandler ... +func SessionsGpgSignTwoFactorHandler(c echo.Context) error { + partialAuthCookie, err := c.Cookie("partial-auth-token") + if err != nil { + return c.Redirect(http.StatusFound, "/") + } + userID, found := partialAuthCache.Get(partialAuthCookie.Value) + if !found { + return c.Redirect(http.StatusFound, "/") + } + + user, err := database.GetUserByID(userID) + if err != nil { + logrus.Errorf("failed to get user %d", userID) + return c.Redirect(http.StatusFound, "/") + } + + var data sessionsGpgSignTwoFactorData + + if c.Request().Method == http.MethodGet { + data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID) + return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data) + } + + token, found := pgpTokenCache.Get(user.ID) + if !found { + return c.Redirect(http.StatusFound, "/") + } + data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message") + data.SignedMessage = c.Request().PostFormValue("signed_message") + + // Text editors often add an extra line break, so let's check with and without it. + if err := utils.PgpCheckSignMessage(user.GPGPublicKey, token+"\n", data.SignedMessage); err != nil { + if err := utils.PgpCheckSignMessage(user.GPGPublicKey, token, data.SignedMessage); err != nil { + logrus.Error(err) + data.ErrorSignedMessage = "invalid signature" + return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data) + } + } + pgpTokenCache.Delete(user.ID) + partialAuthCache.Delete(partialAuthCookie.Value) + c.SetCookie(createPartialSessionCookie("", -1)) + + if string(user.TwoFactorSecret) != "" { + token := utils.GenerateToken32() + partialAuthCache.Set(token, user.ID, cache1.DefaultExpiration) + c.SetCookie(createPartialSessionCookie(token, 10*utils.OneMinuteSecs)) + redirectURL := "/sessions/two-factor" + redir := c.QueryParam("redirect") + if redir != "" { + redirectURL += "?redirect=" + redir + } + return c.Redirect(http.StatusFound, redirectURL) + } + + return completeLogin(c, user, c.RealIP()) +} + // SessionsTwoFactorHandler ... func SessionsTwoFactorHandler(c echo.Context) error { partialAuthCookie, err := c.Cookie("partial-auth-token") @@ -3277,6 +3338,13 @@ func generatePgpEncryptedTokenMessage(userID int64, pkey string) (string, error) return utils.GeneratePgpEncryptedMessage(pkey, msg) } +func generatePgpToBeSignedTokenMessage(userID int64) string { + token := utils.GenerateToken10() + msg := fmt.Sprintf("Signed message for darkforest 2fa\n%s\n%s", token, time.Now().UTC().Format("Jan 02, 2006 - 15:04:05")) + pgpTokenCache.Set(userID, msg, 10*time.Minute) + return msg +} + func AddPGPHandler(c echo.Context) error { authUser := c.Get("authUser").(*database.User) var data addPGPData @@ -3364,6 +3432,7 @@ func GpgTwoFactorAuthenticationToggleHandler(c echo.Context) error { var data gpgTwoFactorAuthenticationVerifyData data.IsEnabled = authUser.GpgTwoFactorEnabled + data.GpgTwoFactorMode = authUser.GpgTwoFactorMode if c.Request().Method == http.MethodGet { return c.Render(http.StatusOK, "two-factor-authentication-gpg", data) @@ -3393,6 +3462,7 @@ func GpgTwoFactorAuthenticationToggleHandler(c echo.Context) error { } c.SetCookie(createSessionCookie("", -1)) authUser.GpgTwoFactorEnabled = true + authUser.GpgTwoFactorMode = utils.DoParseBool(c.Request().PostFormValue("gpg_two_factor_mode")) authUser.DoSave() database.CreateSecurityLog(authUser.ID, database.Gpg2faEnabledSecurityLog) return c.Render(http.StatusOK, "flash", FlashResponse{"GPG Two-factor authentication enabled", "/settings/account", "alert-success"}) diff --git a/pkg/web/public/views/pages/sessions-gpg-sign-two-factor.gohtml b/pkg/web/public/views/pages/sessions-gpg-sign-two-factor.gohtml @@ -0,0 +1,47 @@ +{{ define "extra-head" }} +<style> + textarea { + white-space: pre; + overflow-wrap: normal; + overflow-x: scroll; + } +</style> +{{ end }} + +{{ define "content" }} +<div class="container"> + <div class="row"> + <div class="col-8 offset-2 col-md-8 offset-md-2 col-sm-8 col-lg-6 offset-lg-3 col-xl-6 offset-xl-3"> + {{ if .Data.Error }} + <div class="alert alert-danger">{{ .Data.Error }}</div> + {{ end }} + <div class="card mb-3"> + <div class="card-header"> + {{ t "PGP signature 2fa" . }} + </div> + <div class="card-body"> + <form method="post" novalidate> + <input type="hidden" name="csrf" value="{{ .CSRF }}" /> + <input type="hidden" name="formName" value="pgp_step2" /> + <div class="form-group"> + <label for="encrypted_message">{{ t "Please sign the following message with your private key and send the signed message" . }}</label> + <textarea name="to_be_signed_message" id="to_be_signed_message" rows="4" class="form-control" style="font-family: SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;" readonly>{{ .Data.ToBeSignedMessage }}</textarea> + </div> + <div class="form-group"> + <label for="signed_message">{{ t "Your signed message" . }}</label> + <textarea name="signed_message" id="signed_message" rows="10" class="form-control{{ if .Data.ErrorSignedMessage }} is-invalid{{ end }}" style="font-family: SFMono-Regular,Menlo,Monaco,Consolas,'Liberation Mono','Courier New',monospace;" autofocus>{{ .Data.SignedMessage }}</textarea> + {{ if .Data.ErrorSignedMessage }} + <div class="invalid-feedback">{{ .Data.ErrorSignedMessage }}</div> + {{ end }} + </div> + <div class="form-group"> + <input type="submit" value="{{ t "Continue login" . }}" class="btn btn-primary" /> + <a href="/settings/pgp" class="btn btn-secondary">{{ t "Cancel" . }}</a> + </div> + </form> + </div> + </div> + </div> + </div> +</div> +{{ end }} +\ No newline at end of file diff --git a/pkg/web/public/views/pages/two-factor-authentication-gpg.gohtml b/pkg/web/public/views/pages/two-factor-authentication-gpg.gohtml @@ -3,6 +3,12 @@ <h3>{{ if .Data.IsEnabled }}{{ t "Disable" . }}{{ else }}{{ t "Enable" . }}{{ end }} GPG two-factor authentication</h3> <form method="post"> <input type="hidden" name="csrf" value="{{ .CSRF }}" /> + {{ if not .Data.IsEnabled }} + <div class="form-group"> + <input id="gpg_two_factor_mode_decrypt" name="gpg_two_factor_mode" value="0" type="radio"{{ if not .Data.GpgTwoFactorMode }} checked{{ end }} /><label for="gpg_two_factor_mode_decrypt">Decrypt code</label> + <input id="gpg_two_factor_mode_sign" name="gpg_two_factor_mode" value="1" type="radio"{{ if .Data.GpgTwoFactorMode }} checked{{ end }} /><label for="gpg_two_factor_mode_sign">Sign message</label> + </div> + {{ end }} <div class="form-group"> <input type="password" name="password" value="{{ .Data.Password }}" class="form-control" placeholder="Current password" style="{{ if .Data.ErrorPassword }}is-invalid{{ end }}" autocomplete="off" autocapitalize="none" /> {{ if .Data.ErrorPassword }}<div class="invalid-feedback d-block">{{ .Data.ErrorPassword }}</div>{{ end }} diff --git a/pkg/web/web.go b/pkg/web/web.go @@ -72,6 +72,8 @@ func getMainServer() echo.HandlerFunc { e.POST("/login/:loginToken", handlers.LoginAttackHandler, middlewares.CircuitRateLimitMiddleware(1*time.Second, 2, false)) e.GET("/sessions/gpg-two-factor", handlers.SessionsGpgTwoFactorHandler, middlewares.CircuitRateLimitMiddleware(1*time.Second, 4, false)) e.POST("/sessions/gpg-two-factor", handlers.SessionsGpgTwoFactorHandler, middlewares.CircuitRateLimitMiddleware(1*time.Second, 2, false)) + e.GET("/sessions/gpg-sign-two-factor", handlers.SessionsGpgSignTwoFactorHandler, middlewares.CircuitRateLimitMiddleware(1*time.Second, 4, false)) + e.POST("/sessions/gpg-sign-two-factor", handlers.SessionsGpgSignTwoFactorHandler, middlewares.CircuitRateLimitMiddleware(1*time.Second, 2, false)) e.GET("/sessions/two-factor", handlers.SessionsTwoFactorHandler, middlewares.CircuitRateLimitMiddleware(1*time.Second, 4, false)) e.POST("/sessions/two-factor", handlers.SessionsTwoFactorHandler, middlewares.CircuitRateLimitMiddleware(1*time.Second, 2, false)) e.GET("/sessions/two-factor/recovery", handlers.SessionsTwoFactorRecoveryHandler, middlewares.CircuitRateLimitMiddleware(1*time.Second, 4, false))