dkforest

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

commit 4a6b8ad450dff8143005ea9544684882e2ad7eac
parent bc408f802a4e20b24a244ca1060b7c673449326c
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Tue, 24 Jan 2023 01:25:41 -0800

allow clearsign forum messages

Diffstat:
Acmd/dkf/migrations/121.sql | 4++++
Mpkg/database/table_forum_threads.go | 17++++++++++++++++-
Mpkg/utils/utils.go | 16++++++++++++++++
Mpkg/web/handlers/handlers.go | 15+++++++++++++++
Mpkg/web/public/views/pages/thread.gohtml | 7++++++-
Mpkg/web/web.go | 1+
6 files changed, 58 insertions(+), 2 deletions(-)

diff --git a/cmd/dkf/migrations/121.sql b/cmd/dkf/migrations/121.sql @@ -0,0 +1,4 @@ +-- +migrate Up +ALTER TABLE forum_messages ADD COLUMN is_signed TINYINT(1) NOT NULL DEFAULT 0; + +-- +migrate Down diff --git a/pkg/database/table_forum_threads.go b/pkg/database/table_forum_threads.go @@ -2,6 +2,7 @@ package database import ( "dkforest/pkg/utils" + "github.com/ProtonMail/go-crypto/openpgp/clearsign" html2 "html" "regexp" "strings" @@ -61,6 +62,7 @@ type ForumMessage struct { Message string UserID UserID ThreadID ForumThreadID + IsSigned bool CreatedAt time.Time User User } @@ -94,7 +96,13 @@ func (u *ForumMessage) DoSave() { } func (m *ForumMessage) Escape() string { - res := strings.Replace(m.Message, "\r", "", -1) + msg := m.Message + if m.IsSigned { + if b, _ := clearsign.Decode([]byte(msg)); b != nil { + msg = string(b.Plaintext) + } + } + res := strings.Replace(msg, "\r", "", -1) res = html2.EscapeString(res) resBytes := bf.Run([]byte(res), bf.WithRenderer(utils.MyRenderer(true, true)), bf.WithExtensions(bf.CommonExtensions|bf.HardLineBreak)) res = string(resBytes) @@ -135,6 +143,13 @@ func (m *ForumMessage) CanEdit() bool { return true } +func (m *ForumMessage) ValidateSignature(pkey string) bool { + if pkey == "" { + return false + } + return utils.PgpCheckClearSignMessage(pkey, m.Message) +} + func GetForumThread(threadID ForumThreadID) (out ForumThread, err error) { err = DB.First(&out, "id = ? AND is_club = 1", threadID).Error return diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/ProtonMail/go-crypto/openpgp/clearsign" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/alecthomas/chroma/formatters/html" "github.com/asaskevich/govalidator" @@ -391,6 +392,21 @@ func getGCMKeyBytes(keyBytes []byte) (cipher.AEAD, int, error) { return gcm, nonceSize, nil } +func PgpCheckClearSignMessage(pkey, msg string) bool { + keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(pkey)) + if err != nil { + return false + } + b, _ := clearsign.Decode([]byte(msg)) + if b == nil { + return false + } + if _, err = b.VerifySignature(keyring, nil); err != nil { + return false + } + return true +} + func PgpCheckSignMessage(pkey, msg, signature string) bool { keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(pkey)) if err != nil { diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go @@ -1785,6 +1785,7 @@ func ThreadReplyHandler(c echo.Context) error { return c.Render(http.StatusOK, "thread-reply", data) } message := database.ForumMessage{UUID: database.ForumMessageUUID(uuid.New().String()), Message: data.Message, UserID: authUser.ID, ThreadID: thread.ID} + message.IsSigned = message.ValidateSignature(authUser.GPGPublicKey) if err := database.DB.Create(&message).Error; err != nil { logrus.Error(err) } @@ -2044,6 +2045,7 @@ func ThreadEditMessageHandler(c echo.Context) error { return c.Render(http.StatusOK, "thread-reply", data) } msg.Message = data.Message + msg.IsSigned = msg.ValidateSignature(authUser.GPGPublicKey) msg.DoSave() return c.Redirect(http.StatusFound, "/t/"+string(thread.UUID)) } @@ -2051,6 +2053,18 @@ func ThreadEditMessageHandler(c echo.Context) error { return c.Render(http.StatusOK, "thread-reply", data) } +func ThreadRawMessageHandler(c echo.Context) error { + if config.ForumEnabled.IsFalse() { + return c.Render(http.StatusOK, "flash", FlashResponse{Message: "Forum is temporarily disabled", Redirect: "/", Type: "alert-danger"}) + } + messageUUID := database.ForumMessageUUID(c.Param("messageUUID")) + msg, err := database.GetForumMessageByUUID(messageUUID) + if err != nil { + return c.Redirect(http.StatusFound, "/") + } + return c.String(http.StatusOK, msg.Message) +} + func ClubThreadEditMessageHandler(c echo.Context) error { authUser := c.Get("authUser").(*database.User) threadID := database.ForumThreadID(utils.DoParseInt64(c.Param("threadID"))) @@ -2110,6 +2124,7 @@ func NewThreadHandler(c echo.Context) error { thread := database.ForumThread{UUID: database.ForumThreadUUID(uuid.New().String()), Name: data.ThreadName, UserID: authUser.ID, CategoryID: 1} database.DB.Create(&thread) message := database.ForumMessage{UUID: database.ForumMessageUUID(uuid.New().String()), Message: data.Message, UserID: authUser.ID, ThreadID: thread.ID} + message.IsSigned = message.ValidateSignature(authUser.GPGPublicKey) database.DB.Create(&message) _ = database.SubscribeToForumThread(authUser.ID, thread.ID) return c.Redirect(http.StatusFound, "/t/"+string(thread.UUID)) diff --git a/pkg/web/public/views/pages/thread.gohtml b/pkg/web/public/views/pages/thread.gohtml @@ -70,7 +70,12 @@ {{ end }} </div> <div style="display: flex; flex-direction: column;"> - <div style="flex: 1;"><a {{ .User.GenerateChatStyle | attr }} href="/u/{{ .User.Username }}">{{ .User.Username }}</a></div> + <div style="flex: 1;"> + <a {{ .User.GenerateChatStyle | attr }} href="/u/{{ .User.Username }}">{{ .User.Username }}</a> + {{- if .IsSigned -}} + <a href="/t/{{ $.Data.Thread.UUID }}/messages/{{ .UUID }}/raw" class="ml-2" title="PGP signed" rel="noopener noreferrer" target="_blank">✅</a> + {{- end -}} + </div> <div style="flex: 1;"><a href="#{{ .UUID }}">{{ .CreatedAt.Format "Jan 02, 2006 15:04:05" }}</a></div> <div> {{ if $.AuthUser }} diff --git a/pkg/web/web.go b/pkg/web/web.go @@ -189,6 +189,7 @@ func getMainServer() echo.HandlerFunc { authGroup.POST("/t/:threadUUID/delete", handlers.ThreadDeleteHandler, middlewares.AuthRateLimitMiddleware(1*time.Second, 2)) authGroup.GET("/t/:threadUUID/reply", handlers.ThreadReplyHandler) authGroup.POST("/t/:threadUUID/reply", handlers.ThreadReplyHandler, middlewares.AuthRateLimitMiddleware(1*time.Second, 2)) + authGroup.GET("/t/:threadUUID/messages/:messageUUID/raw", handlers.ThreadRawMessageHandler) authGroup.GET("/t/:threadUUID/messages/:messageUUID/edit", handlers.ThreadEditMessageHandler) authGroup.POST("/t/:threadUUID/messages/:messageUUID/edit", handlers.ThreadEditMessageHandler, middlewares.AuthRateLimitMiddleware(1*time.Second, 2)) authGroup.GET("/t/:threadUUID/messages/:messageUUID/delete", handlers.ThreadDeleteMessageHandler)