commit 50acf96c5080afad2b5042fc6e3819672c5951db
parent 499c0a585f7a7d3bb7608c2a7b61918f74f83561
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Thu, 1 Dec 2022 21:57:01 -0500
gpg 2fa signature mode
Diffstat:
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))