From 070a6ffc76f8d3fea9d2521b116ec5a434c02b28 Mon Sep 17 00:00:00 2001 From: Aine Date: Thu, 22 Sep 2022 18:21:17 +0300 Subject: [PATCH 01/11] use postmoogle as general purpose SMTP server and allow other apps or scripts to send emails through it --- .gitignore | 1 + README.md | 2 ++ bot/access.go | 15 +++++++++++++++ bot/command.go | 11 +++++++++++ bot/email.go | 13 +++++++++++-- bot/settings_room.go | 5 +++++ smtp/msa.go | 22 +++++++++++++++++++--- smtp/msasession.go | 38 ++++++++++++++++++++++++-------------- smtp/mta.go | 3 ++- smtp/server.go | 2 ++ utils/email.go | 16 ++++++++++++++++ 11 files changed, 108 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index a1cf83c..8bcb162 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /local.db /local.db-journal /cover.out +/e2e/main.go diff --git a/README.md b/README.md index ee00a53..df88abd 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ It can't be used with arbitrary email providers, but setup your own provider "wi ### Send - [x] SMTP client +- [x] SMTP server (you can use Postmoogle as general purpose SMTP server to send emails from your scripts or apps) - [x] Send a message to matrix room with special format to send a new email - [ ] Reply to matrix thread sends reply into email thread @@ -237,6 +238,7 @@ If you want to change them - check available options in the help message (`!pm h * **!pm mailbox** - Get or set mailbox of the room * **!pm owner** - Get or set owner of the room +* **!pm password** - Get or set SMTP password of the room's mailbox --- diff --git a/bot/access.go b/bot/access.go index 6bb987b..5121b64 100644 --- a/bot/access.go +++ b/bot/access.go @@ -65,3 +65,18 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool { return !cfg.NoSend() } + +// AllowAuth check if SMTP login (mailbox) and password are valid +func (b *Bot) AllowAuth(mailbox, password string) bool { + roomID, ok := b.GetMapping(mailbox) + if !ok { + return false + } + cfg, err := b.getRoomSettings(roomID) + if err != nil { + b.log.Error("failed to retrieve settings: %v", err) + return false + } + + return cfg.Password() != "" && cfg.Password() == password +} diff --git a/bot/command.go b/bot/command.go index c986bbe..26242e5 100644 --- a/bot/command.go +++ b/bot/command.go @@ -73,6 +73,12 @@ func (b *Bot) initCommands() commandList { sanitizer: func(s string) string { return s }, allowed: b.allowOwner, }, + { + key: roomOptionPassword, + description: "Get or set SMTP password of the room's mailbox", + sanitizer: func(s string) string { return strings.TrimSpace(s) }, + allowed: b.allowOwner, + }, {allowed: b.allowOwner}, // delimiter { key: roomOptionNoSend, @@ -293,6 +299,11 @@ func (b *Bot) runSend(ctx context.Context) { return } + if !utils.AddressValid(to) { + b.Error(ctx, evt.RoomID, "email address is not valid") + return + } + cfg, err := b.getRoomSettings(evt.RoomID) if err != nil { b.Error(ctx, evt.RoomID, "failed to retrieve room settings: %v", err) diff --git a/bot/email.go b/bot/email.go index 6b391af..a4d5877 100644 --- a/bot/email.go +++ b/bot/email.go @@ -46,8 +46,8 @@ func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) { } // Send email to matrix room -func (b *Bot) Send2Matrix(ctx context.Context, email *utils.Email) error { - roomID, ok := b.GetMapping(utils.Mailbox(email.To)) +func (b *Bot) Send2Matrix(ctx context.Context, email *utils.Email, local bool) error { + roomID, ok := b.GetMapping(email.Mailbox(local)) if !ok { return errors.New("room not found") } @@ -59,6 +59,10 @@ func (b *Bot) Send2Matrix(ctx context.Context, email *utils.Email) error { b.Error(ctx, roomID, "cannot get settings: %v", err) } + if !local && cfg.NoSend() { + return errors.New("that mailbox is receive-only") + } + var threadID id.EventID if email.InReplyTo != "" && !cfg.NoThreads() { threadID = b.getThreadID(roomID, email.InReplyTo) @@ -81,6 +85,11 @@ func (b *Bot) Send2Matrix(ctx context.Context, email *utils.Email) error { if !cfg.NoFiles() { b.sendFiles(ctx, roomID, email.Files, cfg.NoThreads(), threadID) } + + if !local { + email.MessageID = fmt.Sprintf("<%s@%s>", eventID, b.domain) + return b.mta.Send(email.From, email.To, email.Compose(b.getBotSettings().DKIMPrivateKey())) + } return nil } diff --git a/bot/settings_room.go b/bot/settings_room.go index f2a6b6c..fe471d4 100644 --- a/bot/settings_room.go +++ b/bot/settings_room.go @@ -21,6 +21,7 @@ const ( roomOptionNoHTML = "nohtml" roomOptionNoThreads = "nothreads" roomOptionNoFiles = "nofiles" + roomOptionPassword = "password" ) type roomSettings map[string]string @@ -43,6 +44,10 @@ func (s roomSettings) Owner() string { return s.Get(roomOptionOwner) } +func (s roomSettings) Password() string { + return s.Get(roomOptionPassword) +} + func (s roomSettings) NoSend() bool { return utils.Bool(s.Get(roomOptionNoSend)) } diff --git a/smtp/msa.go b/smtp/msa.go index dd18f92..a683ce3 100644 --- a/smtp/msa.go +++ b/smtp/msa.go @@ -2,10 +2,13 @@ package smtp import ( "context" + "errors" "github.com/emersion/go-smtp" "github.com/getsentry/sentry-go" "gitlab.com/etke.cc/go/logger" + + "gitlab.com/etke.cc/postmoogle/utils" ) // msa is mail submission agent, implements smtp.Backend @@ -13,11 +16,15 @@ type msa struct { log *logger.Logger domain string bot Bot + mta utils.MTA } -func (m *msa) newSession() *msasession { +func (m *msa) newSession(from string, local bool) *msasession { return &msasession{ ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()), + mta: m.mta, + from: from, + local: local, log: m.log, bot: m.bot, domain: m.domain, @@ -25,9 +32,18 @@ func (m *msa) newSession() *msasession { } func (m *msa) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { - return nil, smtp.ErrAuthUnsupported + if !utils.AddressValid(username) { + return nil, errors.New("please, provide email address") + } + + mailbox := utils.Mailbox(username) + if !m.bot.AllowAuth(mailbox, password) { + return nil, errors.New("email or password is invalid") + } + + return m.newSession(username, false), nil } func (m *msa) AnonymousLogin(state *smtp.ConnectionState) (smtp.Session, error) { - return m.newSession(), nil + return m.newSession("", true), nil } diff --git a/smtp/msasession.go b/smtp/msasession.go index 746568a..aac0b24 100644 --- a/smtp/msasession.go +++ b/smtp/msasession.go @@ -2,6 +2,7 @@ package smtp import ( "context" + "errors" "io" "github.com/emersion/go-smtp" @@ -15,32 +16,41 @@ import ( type msasession struct { log *logger.Logger bot Bot + mta utils.MTA domain string - ctx context.Context - to string - from string + ctx context.Context + local bool + to string + from string } func (s *msasession) Mail(from string, opts smtp.MailOptions) error { sentry.GetHubFromContext(s.ctx).Scope().SetTag("from", from) - s.from = from - s.log.Debug("mail from %s, options: %+v", from, opts) + if !utils.AddressValid(from) { + return errors.New("please, provide email address") + } + if s.local { + s.from = from + s.log.Debug("mail from %s, options: %+v", from, opts) + } return nil } func (s *msasession) Rcpt(to string) error { sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to) - if utils.Hostname(to) != s.domain { - s.log.Debug("wrong domain of %s", to) - return smtp.ErrAuthRequired - } + if s.local { + if utils.Hostname(to) != s.domain { + s.log.Debug("wrong domain of %s", to) + return smtp.ErrAuthRequired + } - _, ok := s.bot.GetMapping(utils.Mailbox(to)) - if !ok { - s.log.Debug("mapping for %s not found", to) - return smtp.ErrAuthRequired + _, ok := s.bot.GetMapping(utils.Mailbox(to)) + if !ok { + s.log.Debug("mapping for %s not found", to) + return smtp.ErrAuthRequired + } } s.to = to @@ -80,7 +90,7 @@ func (s *msasession) Data(r io.Reader) error { eml.HTML, files) - return s.bot.Send2Matrix(s.ctx, email) + return s.bot.Send2Matrix(s.ctx, email, s.local) } func (s *msasession) Reset() {} diff --git a/smtp/mta.go b/smtp/mta.go index 0ad6fac..b63a9b7 100644 --- a/smtp/mta.go +++ b/smtp/mta.go @@ -17,8 +17,9 @@ import ( // Bot interface to send emails into matrix type Bot interface { + AllowAuth(string, string) bool GetMapping(string) (id.RoomID, bool) - Send2Matrix(ctx context.Context, email *utils.Email) error + Send2Matrix(ctx context.Context, email *utils.Email, local bool) error SetMTA(mta utils.MTA) } diff --git a/smtp/server.go b/smtp/server.go index 864dde5..bbb8024 100644 --- a/smtp/server.go +++ b/smtp/server.go @@ -40,6 +40,7 @@ func NewServer(cfg *Config) *Server { sender := NewMTA(cfg.LogLevel) receiver := &msa{ log: log, + mta: sender, bot: cfg.Bot, domain: cfg.Domain, } @@ -51,6 +52,7 @@ func NewServer(cfg *Config) *Server { s.WriteTimeout = 10 * time.Second s.MaxMessageBytes = cfg.MaxSize * 1024 * 1024 s.EnableREQUIRETLS = cfg.TLSRequired + s.AllowInsecureAuth = !cfg.TLSRequired if log.GetLevel() == "DEBUG" || log.GetLevel() == "TRACE" { s.Debug = os.Stdout } diff --git a/utils/email.go b/utils/email.go index 8d5a33e..3efcb49 100644 --- a/utils/email.go +++ b/utils/email.go @@ -4,6 +4,7 @@ import ( "crypto" "crypto/x509" "encoding/pem" + "regexp" "strings" "time" @@ -13,6 +14,8 @@ import ( "maunium.net/go/mautrix/id" ) +var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + // MTA is mail transfer agent type MTA interface { Send(from, to, data string) error @@ -46,6 +49,11 @@ type ContentOptions struct { FromKey string } +// AddressValid checks if email address is valid +func AddressValid(email string) bool { + return !emailRegex.MatchString(email) +} + // NewEmail constructs Email object func NewEmail(messageID, inReplyTo, subject, from, to, text, html string, files []*File) *Email { email := &Email{ @@ -71,6 +79,14 @@ func NewEmail(messageID, inReplyTo, subject, from, to, text, html string, files return email } +// Mailbox returns postmoogle's mailbox, parsing it from FROM (if local=false) or TO (local=true) +func (e *Email) Mailbox(local bool) string { + if local { + return Mailbox(e.To) + } + return Mailbox(e.From) +} + // Content converts the email object to a Matrix event content func (e *Email) Content(threadID id.EventID, options *ContentOptions) *event.Content { var text strings.Builder From 1dc552686df2d264e8286dcc5e12418d263f565f Mon Sep 17 00:00:00 2001 From: Aine Date: Thu, 22 Sep 2022 18:26:56 +0300 Subject: [PATCH 02/11] reflect smtp auth changes in radme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index df88abd..7ff12e3 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ An Email to Matrix bridge. 1 room = 1 mailbox. -Postmoogle is an actual SMTP server that allows you to receive emails on your matrix server. -It can't be used with arbitrary email providers, but setup your own provider "with matrix interface" instead. +Postmoogle is an actual SMTP server that allows you to send and receive emails on your matrix server. +It can't be used with arbitrary email providers, because it acts as an actual email provider itself, +so you can use it to send emails from your apps and scripts as well. ## Roadmap From 5a19ffad08b98547b4c70db9e2901a7324b3b068 Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 23 Sep 2022 10:19:25 +0300 Subject: [PATCH 03/11] securely compare passwords, add notice about message removal --- bot/access.go | 4 +++- bot/command.go | 1 - bot/command_owner.go | 14 ++++++++++++-- utils/utils.go | 22 ++++++++++++++++++++++ 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/bot/access.go b/bot/access.go index 5121b64..74fc61e 100644 --- a/bot/access.go +++ b/bot/access.go @@ -6,6 +6,8 @@ import ( "gitlab.com/etke.cc/go/mxidwc" "maunium.net/go/mautrix/id" + + "gitlab.com/etke.cc/postmoogle/utils" ) func parseMXIDpatterns(patterns []string, defaultPattern string) ([]*regexp.Regexp, error) { @@ -78,5 +80,5 @@ func (b *Bot) AllowAuth(mailbox, password string) bool { return false } - return cfg.Password() != "" && cfg.Password() == password + return utils.Compare(password, cfg.Password()) } diff --git a/bot/command.go b/bot/command.go index 26242e5..710c0b2 100644 --- a/bot/command.go +++ b/bot/command.go @@ -76,7 +76,6 @@ func (b *Bot) initCommands() commandList { { key: roomOptionPassword, description: "Get or set SMTP password of the room's mailbox", - sanitizer: func(s string) string { return strings.TrimSpace(s) }, allowed: b.allowOwner, }, {allowed: b.allowOwner}, // delimiter diff --git a/bot/command_owner.go b/bot/command_owner.go index 6d979a6..3d1490a 100644 --- a/bot/command_owner.go +++ b/bot/command_owner.go @@ -62,12 +62,17 @@ func (b *Bot) getOption(ctx context.Context, name string) { msg := fmt.Sprintf("`%s` of this room is `%s`\n"+ "To set it to a new value, send a `%s %s VALUE` command.", name, value, b.prefix, name) + if name == roomOptionPassword { + msg = msg + "\n\n---\n\n" + + "**Please, remove that message after reading.**" + } b.SendNotice(ctx, evt.RoomID, msg) } +//nolint:gocognit func (b *Bot) setOption(ctx context.Context, name, value string) { cmd := b.commands.get(name) - if cmd != nil { + if cmd != nil && cmd.sanitizer != nil { value = cmd.sanitizer(value) } @@ -104,5 +109,10 @@ func (b *Bot) setOption(ctx context.Context, name, value string) { return } - b.SendNotice(ctx, evt.RoomID, fmt.Sprintf("`%s` of this room set to `%s`", name, value)) + msg := fmt.Sprintf("`%s` of this room set to `%s`", name, value) + if name == roomOptionPassword { + msg = msg + "\n\n---\n\n" + + "**Please, remove that message and the previous one.**" + } + b.SendNotice(ctx, evt.RoomID, msg) } diff --git a/utils/utils.go b/utils/utils.go index 93271da..a002cd4 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "crypto/subtle" "strconv" "strings" ) @@ -33,3 +34,24 @@ func Bool(str string) bool { func SanitizeBoolString(str string) string { return strconv.FormatBool(Bool(str)) } + +// Compare strings with constant time to prevent timing attacks +func Compare(actual, expected string) bool { + actualb := []byte(actual) + expectedb := []byte(expected) + + if expected == "" { + // Just to keep constant time + _ = subtle.ConstantTimeCompare(expectedb, expectedb) == 1 + return false + } + + // actual comparison + if subtle.ConstantTimeEq(int32(len(actual)), int32(len(expected))) == 1 { + return subtle.ConstantTimeCompare(actualb, expectedb) == 1 + } + + // Just to keep constant time + _ = subtle.ConstantTimeCompare(expectedb, expectedb) == 1 + return false +} From d50b79a80163ac4fab7bf289eaee31404e09005e Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 23 Sep 2022 10:29:37 +0300 Subject: [PATCH 04/11] switch email address validation to mail.ParseAddress --- utils/email.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/utils/email.go b/utils/email.go index 3efcb49..9463dfb 100644 --- a/utils/email.go +++ b/utils/email.go @@ -4,7 +4,7 @@ import ( "crypto" "crypto/x509" "encoding/pem" - "regexp" + "net/mail" "strings" "time" @@ -14,8 +14,6 @@ import ( "maunium.net/go/mautrix/id" ) -var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - // MTA is mail transfer agent type MTA interface { Send(from, to, data string) error @@ -51,7 +49,8 @@ type ContentOptions struct { // AddressValid checks if email address is valid func AddressValid(email string) bool { - return !emailRegex.MatchString(email) + _, err := mail.ParseAddress(email) + return err == nil } // NewEmail constructs Email object From 3f5a1cd915a27cc3d1b22721fc6c66541ff275b7 Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 23 Sep 2022 10:33:25 +0300 Subject: [PATCH 05/11] rename local to incoming --- bot/email.go | 8 ++++---- smtp/msa.go | 16 ++++++++-------- smtp/msasession.go | 14 +++++++------- smtp/mta.go | 2 +- utils/email.go | 6 +++--- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/bot/email.go b/bot/email.go index a4d5877..311701b 100644 --- a/bot/email.go +++ b/bot/email.go @@ -46,8 +46,8 @@ func (b *Bot) GetMapping(mailbox string) (id.RoomID, bool) { } // Send email to matrix room -func (b *Bot) Send2Matrix(ctx context.Context, email *utils.Email, local bool) error { - roomID, ok := b.GetMapping(email.Mailbox(local)) +func (b *Bot) Send2Matrix(ctx context.Context, email *utils.Email, incoming bool) error { + roomID, ok := b.GetMapping(email.Mailbox(incoming)) if !ok { return errors.New("room not found") } @@ -59,7 +59,7 @@ func (b *Bot) Send2Matrix(ctx context.Context, email *utils.Email, local bool) e b.Error(ctx, roomID, "cannot get settings: %v", err) } - if !local && cfg.NoSend() { + if !incoming && cfg.NoSend() { return errors.New("that mailbox is receive-only") } @@ -86,7 +86,7 @@ func (b *Bot) Send2Matrix(ctx context.Context, email *utils.Email, local bool) e b.sendFiles(ctx, roomID, email.Files, cfg.NoThreads(), threadID) } - if !local { + if !incoming { email.MessageID = fmt.Sprintf("<%s@%s>", eventID, b.domain) return b.mta.Send(email.From, email.To, email.Compose(b.getBotSettings().DKIMPrivateKey())) } diff --git a/smtp/msa.go b/smtp/msa.go index a683ce3..bd03a18 100644 --- a/smtp/msa.go +++ b/smtp/msa.go @@ -19,15 +19,15 @@ type msa struct { mta utils.MTA } -func (m *msa) newSession(from string, local bool) *msasession { +func (m *msa) newSession(from string, incoming bool) *msasession { return &msasession{ - ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()), - mta: m.mta, - from: from, - local: local, - log: m.log, - bot: m.bot, - domain: m.domain, + ctx: sentry.SetHubOnContext(context.Background(), sentry.CurrentHub().Clone()), + mta: m.mta, + from: from, + incoming: incoming, + log: m.log, + bot: m.bot, + domain: m.domain, } } diff --git a/smtp/msasession.go b/smtp/msasession.go index aac0b24..d2ab78b 100644 --- a/smtp/msasession.go +++ b/smtp/msasession.go @@ -19,10 +19,10 @@ type msasession struct { mta utils.MTA domain string - ctx context.Context - local bool - to string - from string + ctx context.Context + incoming bool + to string + from string } func (s *msasession) Mail(from string, opts smtp.MailOptions) error { @@ -30,7 +30,7 @@ func (s *msasession) Mail(from string, opts smtp.MailOptions) error { if !utils.AddressValid(from) { return errors.New("please, provide email address") } - if s.local { + if s.incoming { s.from = from s.log.Debug("mail from %s, options: %+v", from, opts) } @@ -40,7 +40,7 @@ func (s *msasession) Mail(from string, opts smtp.MailOptions) error { func (s *msasession) Rcpt(to string) error { sentry.GetHubFromContext(s.ctx).Scope().SetTag("to", to) - if s.local { + if s.incoming { if utils.Hostname(to) != s.domain { s.log.Debug("wrong domain of %s", to) return smtp.ErrAuthRequired @@ -90,7 +90,7 @@ func (s *msasession) Data(r io.Reader) error { eml.HTML, files) - return s.bot.Send2Matrix(s.ctx, email, s.local) + return s.bot.Send2Matrix(s.ctx, email, s.incoming) } func (s *msasession) Reset() {} diff --git a/smtp/mta.go b/smtp/mta.go index b63a9b7..dcf6475 100644 --- a/smtp/mta.go +++ b/smtp/mta.go @@ -19,7 +19,7 @@ import ( type Bot interface { AllowAuth(string, string) bool GetMapping(string) (id.RoomID, bool) - Send2Matrix(ctx context.Context, email *utils.Email, local bool) error + Send2Matrix(ctx context.Context, email *utils.Email, incoming bool) error SetMTA(mta utils.MTA) } diff --git a/utils/email.go b/utils/email.go index 9463dfb..187594e 100644 --- a/utils/email.go +++ b/utils/email.go @@ -78,9 +78,9 @@ func NewEmail(messageID, inReplyTo, subject, from, to, text, html string, files return email } -// Mailbox returns postmoogle's mailbox, parsing it from FROM (if local=false) or TO (local=true) -func (e *Email) Mailbox(local bool) string { - if local { +// Mailbox returns postmoogle's mailbox, parsing it from FROM (if incoming=false) or TO (incoming=true) +func (e *Email) Mailbox(incoming bool) string { + if incoming { return Mailbox(e.To) } return Mailbox(e.From) From bd2237d717ca00a6eb1437ce11d46c3e73681824 Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 23 Sep 2022 10:34:25 +0300 Subject: [PATCH 06/11] fix typo --- smtp/msa.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtp/msa.go b/smtp/msa.go index bd03a18..b146854 100644 --- a/smtp/msa.go +++ b/smtp/msa.go @@ -33,7 +33,7 @@ func (m *msa) newSession(from string, incoming bool) *msasession { func (m *msa) Login(state *smtp.ConnectionState, username, password string) (smtp.Session, error) { if !utils.AddressValid(username) { - return nil, errors.New("please, provide email address") + return nil, errors.New("please, provide an email address") } mailbox := utils.Mailbox(username) From 9129f8e38c84a1bf91c73760d1dbb2510290ee3d Mon Sep 17 00:00:00 2001 From: Slavi Pantaleev Date: Fri, 23 Sep 2022 07:35:35 +0000 Subject: [PATCH 07/11] Apply 1 suggestion(s) to 1 file(s) --- bot/access.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/access.go b/bot/access.go index 74fc61e..31064c2 100644 --- a/bot/access.go +++ b/bot/access.go @@ -68,9 +68,13 @@ func (b *Bot) allowSend(actorID id.UserID, targetRoomID id.RoomID) bool { return !cfg.NoSend() } -// AllowAuth check if SMTP login (mailbox) and password are valid -func (b *Bot) AllowAuth(mailbox, password string) bool { - roomID, ok := b.GetMapping(mailbox) +// AllowAuth check if SMTP login (email) and password are valid +func (b *Bot) AllowAuth(email, password string) bool { + if !strings.HasSuffix(email, "@"+b.domain) { + return false + } + + roomID, ok := b.GetMapping(utils.Mailbox(email)) if !ok { return false } From e368d26fc15d0f76dd03ea0caeee71695a628b7c Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 23 Sep 2022 10:37:08 +0300 Subject: [PATCH 08/11] check full email in AllowAuth --- bot/access.go | 1 + smtp/msa.go | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/access.go b/bot/access.go index 31064c2..5fc04a1 100644 --- a/bot/access.go +++ b/bot/access.go @@ -3,6 +3,7 @@ package bot import ( "context" "regexp" + "strings" "gitlab.com/etke.cc/go/mxidwc" "maunium.net/go/mautrix/id" diff --git a/smtp/msa.go b/smtp/msa.go index b146854..71d50b2 100644 --- a/smtp/msa.go +++ b/smtp/msa.go @@ -36,8 +36,7 @@ func (m *msa) Login(state *smtp.ConnectionState, username, password string) (smt return nil, errors.New("please, provide an email address") } - mailbox := utils.Mailbox(username) - if !m.bot.AllowAuth(mailbox, password) { + if !m.bot.AllowAuth(username, password) { return nil, errors.New("email or password is invalid") } From 236b23470df103b68d9bc406e56893a78f41ae50 Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 23 Sep 2022 10:42:17 +0300 Subject: [PATCH 09/11] add comment --- smtp/msasession.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/smtp/msasession.go b/smtp/msasession.go index d2ab78b..93b293f 100644 --- a/smtp/msasession.go +++ b/smtp/msasession.go @@ -13,6 +13,10 @@ import ( "gitlab.com/etke.cc/postmoogle/utils" ) +// msasession represents an SMTP-submission session. +// This can be used in 2 directions: +// - receiving emails from remote servers, in which case: `incoming = true` +// - sending emails from local users, in which case: `incoming = false` type msasession struct { log *logger.Logger bot Bot From 4bf0f0dee3853501d17fcb34680db04e4aa5bab1 Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 23 Sep 2022 11:17:34 +0300 Subject: [PATCH 10/11] switch to password hashes --- bot/access.go | 7 ++++++- bot/command_owner.go | 17 +++++++++++++++-- go.mod | 3 ++- go.sum | 2 ++ utils/utils.go | 22 ---------------------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/bot/access.go b/bot/access.go index 5fc04a1..0e05c7f 100644 --- a/bot/access.go +++ b/bot/access.go @@ -5,6 +5,7 @@ import ( "regexp" "strings" + "github.com/raja/argon2pw" "gitlab.com/etke.cc/go/mxidwc" "maunium.net/go/mautrix/id" @@ -85,5 +86,9 @@ func (b *Bot) AllowAuth(email, password string) bool { return false } - return utils.Compare(password, cfg.Password()) + allow, err := argon2pw.CompareHashWithPassword(cfg.Password(), password) + if err != nil { + b.log.Warn("Password for %s is not valid: %v", email, err) + } + return allow } diff --git a/bot/command_owner.go b/bot/command_owner.go index 3d1490a..709bcce 100644 --- a/bot/command_owner.go +++ b/bot/command_owner.go @@ -3,6 +3,8 @@ package bot import ( "context" "fmt" + + "github.com/raja/argon2pw" ) func (b *Bot) runStop(ctx context.Context) { @@ -63,8 +65,11 @@ func (b *Bot) getOption(ctx context.Context, name string) { "To set it to a new value, send a `%s %s VALUE` command.", name, value, b.prefix, name) if name == roomOptionPassword { - msg = msg + "\n\n---\n\n" + - "**Please, remove that message after reading.**" + msg = fmt.Sprintf("Password hash of this room is `%s`\n"+ + "To set it to a new value, send a `%s %s VALUE` command.\n\n"+ + "---\n\n"+ + "**Please, remove that message after reading.**", + value, b.prefix, name) } b.SendNotice(ctx, evt.RoomID, msg) } @@ -91,6 +96,14 @@ func (b *Bot) setOption(ctx context.Context, name, value string) { return } + if name == roomOptionPassword { + value, err = argon2pw.GenerateSaltedHash(value) + if err != nil { + b.Error(ctx, evt.RoomID, "failed to hash password: %v", err) + return + } + } + old := cfg.Get(name) cfg.Set(name, value) diff --git a/go.mod b/go.mod index 143822a..ff8d7a0 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,14 @@ go 1.18 require ( git.sr.ht/~xn/cache/v2 v2.0.0 github.com/emersion/go-msgauth v0.6.6 + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 github.com/emersion/go-smtp v0.15.0 github.com/gabriel-vasile/mimetype v1.4.1 github.com/getsentry/sentry-go v0.13.0 github.com/jhillyerd/enmime v0.10.0 github.com/lib/pq v1.10.6 github.com/mattn/go-sqlite3 v1.14.15 + github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 gitlab.com/etke.cc/go/env v1.0.0 gitlab.com/etke.cc/go/logger v1.1.0 gitlab.com/etke.cc/go/mxidwc v1.0.0 @@ -22,7 +24,6 @@ require ( require ( github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect - github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect github.com/google/go-cmp v0.5.8 // indirect github.com/gorilla/mux v1.8.0 // indirect diff --git a/go.sum b/go.sum index 4db88fe..e5efcb0 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 h1:2by0+lF6NfaNWhlpsv1DfBQzwbAyYUPIsMWYapek/Sk= +github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39/go.mod h1:idX/fPqwjX31YMTF2iIpEpNApV2YbQhSFr4iIhJaqp4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/utils/utils.go b/utils/utils.go index a002cd4..93271da 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,7 +1,6 @@ package utils import ( - "crypto/subtle" "strconv" "strings" ) @@ -34,24 +33,3 @@ func Bool(str string) bool { func SanitizeBoolString(str string) string { return strconv.FormatBool(Bool(str)) } - -// Compare strings with constant time to prevent timing attacks -func Compare(actual, expected string) bool { - actualb := []byte(actual) - expectedb := []byte(expected) - - if expected == "" { - // Just to keep constant time - _ = subtle.ConstantTimeCompare(expectedb, expectedb) == 1 - return false - } - - // actual comparison - if subtle.ConstantTimeEq(int32(len(actual)), int32(len(expected))) == 1 { - return subtle.ConstantTimeCompare(actualb, expectedb) == 1 - } - - // Just to keep constant time - _ = subtle.ConstantTimeCompare(expectedb, expectedb) == 1 - return false -} From c56c740c1d89d1e4b23178fece0ddd11d22b6f69 Mon Sep 17 00:00:00 2001 From: Aine Date: Fri, 23 Sep 2022 11:28:15 +0300 Subject: [PATCH 11/11] add password option messages --- bot/command_owner.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bot/command_owner.go b/bot/command_owner.go index 709bcce..e282f0e 100644 --- a/bot/command_owner.go +++ b/bot/command_owner.go @@ -65,11 +65,11 @@ func (b *Bot) getOption(ctx context.Context, name string) { "To set it to a new value, send a `%s %s VALUE` command.", name, value, b.prefix, name) if name == roomOptionPassword { - msg = fmt.Sprintf("Password hash of this room is `%s`\n"+ - "To set it to a new value, send a `%s %s VALUE` command.\n\n"+ - "---\n\n"+ - "**Please, remove that message after reading.**", - value, b.prefix, name) + msg = fmt.Sprintf("There is an SMTP password already set for this room/mailbox. "+ + "It's stored in a secure hashed manner, so we can't tell you what the original raw password was. "+ + "To find the raw password, try to find your old message which had originally set it, "+ + "or just set a new one with `%s %s NEW_PASSWORD`.", + b.prefix, name) } b.SendNotice(ctx, evt.RoomID, msg) } @@ -124,8 +124,7 @@ func (b *Bot) setOption(ctx context.Context, name, value string) { msg := fmt.Sprintf("`%s` of this room set to `%s`", name, value) if name == roomOptionPassword { - msg = msg + "\n\n---\n\n" + - "**Please, remove that message and the previous one.**" + msg = "SMTP password has been set" } b.SendNotice(ctx, evt.RoomID, msg) }