From 12ac47e949c64dbd76ab7525a2534ba1b4fb8c2b Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Mon, 10 May 2021 15:51:47 +0200 Subject: [PATCH 01/15] Other: fix typos regarding listener --- internal/serverutil/server.go | 4 ++-- pkg/listener/listener.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/serverutil/server.go b/internal/serverutil/server.go index 1a07805d..6542194e 100644 --- a/internal/serverutil/server.go +++ b/internal/serverutil/server.go @@ -72,7 +72,7 @@ func redirectInternetEventsToOneChannel(l listener.Listener) (isInternetOn chan const ( recheckPortAfter = 50 * time.Millisecond stopPortChecksAfter = 15 * time.Second - retryListnerAfter = 5 * time.Second + retryListenerAfter = 5 * time.Second ) func monitorInternetConnection(s Server, l listener.Listener) { @@ -89,7 +89,7 @@ func monitorInternetConnection(s Server, l listener.Listener) { // blocked our port for a bit after we closed IMAP server // due to connection issues. // Restart always helped, so we do retry to not bother user. - s.ListenRetryAndServe(10, retryListnerAfter) + s.ListenRetryAndServe(10, retryListenerAfter) }() expectedIsPortFree = false } else { diff --git a/pkg/listener/listener.go b/pkg/listener/listener.go index 32d91561..48164d87 100644 --- a/pkg/listener/listener.go +++ b/pkg/listener/listener.go @@ -90,7 +90,7 @@ func (l *listener) Add(eventName string, channel chan<- string) { log := log.WithField("name", eventName).WithField("i", len(l.channels[eventName])) l.channels[eventName] = append(l.channels[eventName], channel) - log.Debug("Added event listner") + log.Debug("Added event listener") } // Remove removes an event listener. From 00146e747471ec70cd2adb7bc1e3c28dcac2ac43 Mon Sep 17 00:00:00 2001 From: Andrzej Szafranski Date: Mon, 10 May 2021 08:43:59 +0000 Subject: [PATCH 02/15] Other: Bridge James 1.8.0 release notes --- release-notes/bridge_early.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/release-notes/bridge_early.md b/release-notes/bridge_early.md index f4074672..a76b2e43 100644 --- a/release-notes/bridge_early.md +++ b/release-notes/bridge_early.md @@ -1,3 +1,17 @@ +## v1.8.0 +- 2021-05-10 + +### New + +- Implemented connection manager to improve performance during weak connection, better handling of connection loss and other connectivity issues +- Prompt profile installation during Apple Mail auto-configuration on MacOS Big Sur + +### Fixed + +- Bugs with building of message bodies/headers +- Incorrect naming format of some of the attachments + + ## v1.7.1 - 2021-04-27 From 3dadad51315ac99ad6b762bfcaf1134b0dd27e60 Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Wed, 12 May 2021 16:29:43 +0200 Subject: [PATCH 03/15] GODT-1161: Guarantee order of responses when creating new message --- go.mod | 3 -- go.sum | 7 --- internal/imap/idle/extension.go | 85 +++++++++++++++++++++++++++++++ internal/imap/server.go | 4 +- internal/imap/updates.go | 6 +-- internal/store/mailbox_message.go | 28 +++++++--- 6 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 internal/imap/idle/extension.go diff --git a/go.mod b/go.mod index 7834e12e..205dabf7 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,6 @@ require ( github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/cucumber/godog v0.8.1 github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a - github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c github.com/emersion/go-imap-unselect v0.0.0-20171113212723-b985794e5f26 @@ -59,8 +58,6 @@ require ( github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/stretchr/testify v1.6.1 github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e - github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d // indirect - github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d // indirect github.com/urfave/cli/v2 v2.2.0 github.com/vmihailenco/msgpack/v5 v5.1.3 go.etcd.io/bbolt v1.3.5 diff --git a/go.sum b/go.sum index 3d4d4a44..09da5138 100644 --- a/go.sum +++ b/go.sum @@ -73,8 +73,6 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a h1:bMdSPm6sssuOFpIaveu3XGAijMS3Tq2S3EqFZmZxidc= github.com/emersion/go-imap-appendlimit v0.0.0-20190308131241-25671c986a6a/go.mod h1:ikgISoP7pRAolqsVP64yMteJa2FIpS6ju88eBT6K1yQ= -github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4 h1:/JIALzmCduf5o8TWJSiOBzTb9+R0SChwElUrJLlp2po= -github.com/emersion/go-imap-idle v0.0.0-20200601154248-f05f54664cc4/go.mod h1:o14zPKCmEH5WC1vU5SdPoZGgNvQx7zzKSnxPQlobo78= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342 h1:5p1t3e1PomYgLWwEwhwEU5kVBwcyAcVrOpexv8AeZx0= github.com/emersion/go-imap-move v0.0.0-20190710073258-6e5a51a5b342/go.mod h1:QuMaZcKFDVI0yCrnAbPLfbwllz1wtOrZH8/vZ5yzp4w= github.com/emersion/go-imap-quota v0.0.0-20210203125329-619074823f3c h1:khcEdu1yFiZjBgi7gGnQiLhpSgghJ0YTnKD0l4EUqqc= @@ -262,11 +260,6 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e h1:G0DQ/TRQyrEZjtLlLwevFjaRiG8eeCMlq9WXQ2OO2bk= github.com/therecipe/qt v0.0.0-20200701200531-7f61353ee73e/go.mod h1:SUUR2j3aE1z6/g76SdD6NwACEpvCxb3fvG82eKbD6us= -github.com/therecipe/qt v0.0.0-20200904063919-c0c124a5770d h1:T+d8FnaLSvM/1BdlDXhW4d5dr2F07bAbB+LpgzMxx+o= -github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d h1:hAZyEG2swPRWjF0kqqdGERXUazYnRJdAk4a58f14z7Y= -github.com/therecipe/qt/internal/binding/files/docs/5.12.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:7m8PDYDEtEVqfjoUQc2UrFqhG0CDmoVJjRlQxexndFc= -github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d h1:AJRoBel/g9cDS+yE8BcN3E+TDD/xNAguG21aoR8DAIE= -github.com/therecipe/qt/internal/binding/files/docs/5.13.0 v0.0.0-20200904063919-c0c124a5770d/go.mod h1:mH55Ek7AZcdns5KPp99O0bg+78el64YCYWHiQKrOdt4= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= diff --git a/internal/imap/idle/extension.go b/internal/imap/idle/extension.go new file mode 100644 index 00000000..4590409e --- /dev/null +++ b/internal/imap/idle/extension.go @@ -0,0 +1,85 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package idle + +import ( + "bufio" + "errors" + "strings" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/server" +) + +const ( + idleCommand = "IDLE" // Capability and Command identificator + doneLine = "DONE" +) + +// Handler for IDLE extension. +type Handler struct{} + +// Command for IDLE handler. +func (h *Handler) Command() *imap.Command { + return &imap.Command{Name: idleCommand} +} + +// Parse for IDLE handler. +func (h *Handler) Parse(fields []interface{}) error { + return nil +} + +// Handle the IDLE request. +func (h *Handler) Handle(conn server.Conn) error { + cont := &imap.ContinuationReq{Info: "idling"} + if err := conn.WriteResp(cont); err != nil { + return err + } + + // Wait for DONE + scanner := bufio.NewScanner(conn) + scanner.Scan() + if err := scanner.Err(); err != nil { + return err + } + + if strings.ToUpper(scanner.Text()) != doneLine { + return errors.New("expected DONE") + } + return nil +} + +type extension struct{} + +func (ext *extension) Capabilities(c server.Conn) []string { + return []string{idleCommand} +} + +func (ext *extension) Command(name string) server.HandlerFactory { + if name != idleCommand { + return nil + } + + return func() server.Handler { + return &Handler{} + } +} + +func NewExtension() server.Extension { + return &extension{} +} diff --git a/internal/imap/server.go b/internal/imap/server.go index 70161af4..7dfb3caf 100644 --- a/internal/imap/server.go +++ b/internal/imap/server.go @@ -31,12 +31,12 @@ import ( "github.com/ProtonMail/proton-bridge/internal/config/useragent" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/imap/id" + "github.com/ProtonMail/proton-bridge/internal/imap/idle" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/serverutil" "github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/emersion/go-imap" imapappendlimit "github.com/emersion/go-imap-appendlimit" - imapidle "github.com/emersion/go-imap-idle" imapmove "github.com/emersion/go-imap-move" imapquota "github.com/emersion/go-imap-quota" imapunselect "github.com/emersion/go-imap-unselect" @@ -94,7 +94,7 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por }) s.Enable( - imapidle.NewExtension(), + idle.NewExtension(), imapmove.NewExtension(), id.NewExtension(serverID, userAgent), imapquota.NewExtension(), diff --git a/internal/imap/updates.go b/internal/imap/updates.go index 0e77d084..83fd87b0 100644 --- a/internal/imap/updates.go +++ b/internal/imap/updates.go @@ -188,10 +188,10 @@ func (iu *imapUpdates) MailboxStatus(address, mailboxName string, total, unread, update.MailboxStatus.Messages = total update.MailboxStatus.Unseen = unread update.MailboxStatus.UnseenSeqNum = unreadSeqNum - iu.sendIMAPUpdate(update, false) + iu.sendIMAPUpdate(update, true) } -func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, block bool) { +func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, isBlocking bool) { if iu.ch == nil { log.Trace("IMAP IDLE unavailable") return @@ -207,7 +207,7 @@ func (iu *imapUpdates) sendIMAPUpdate(update goIMAPBackend.Update, block bool) { } }() - if !block { + if !isBlocking { return } diff --git a/internal/store/mailbox_message.go b/internal/store/mailbox_message.go index 57d129c0..914e720a 100644 --- a/internal/store/mailbox_message.go +++ b/internal/store/mailbox_message.go @@ -355,6 +355,10 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi // Buckets are not initialized right away because it's a heavy operation. // The best option is to get the same bucket only once and only when needed. var apiBucket, imapBucket, deletedBucket *bolt.Bucket + + // Collect updates to send them later, after possibly sending the status/EXISTS update. + updates := make([]func(), 0, len(msgs)) + for _, msg := range msgs { if storeMailbox.txSkipAndRemoveFromMailbox(tx, msg) { continue @@ -417,14 +421,18 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi if err != nil { return errors.Wrap(err, "cannot get sequence number from UID") } - storeMailbox.store.notifyUpdateMessage( - storeMailbox.storeAddress.address, - storeMailbox.labelName, - uid, - seqNum, - msg, - false, // new message is never marked as deleted - ) + + updates = append(updates, func() { + storeMailbox.store.notifyUpdateMessage( + storeMailbox.storeAddress.address, + storeMailbox.labelName, + uid, + seqNum, + msg, + false, // new message is never marked as deleted + ) + }) + shouldSendMailboxUpdate = true } @@ -434,6 +442,10 @@ func (storeMailbox *Mailbox) txCreateOrUpdateMessages(tx *bolt.Tx, msgs []*pmapi } } + for _, update := range updates { + update() + } + return nil } From 8496c9e181881046def69c17a65f8410dcae2102 Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Tue, 18 May 2021 16:55:03 +0200 Subject: [PATCH 04/15] Other: bump SMTP test timeout time from 1 to 2 seconds, fingers crossed --- test/features/bridge/no_internet.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/features/bridge/no_internet.feature b/test/features/bridge/no_internet.feature index 470856b6..febfb11e 100644 --- a/test/features/bridge/no_internet.feature +++ b/test/features/bridge/no_internet.feature @@ -5,7 +5,7 @@ Feature: Servers are closed when no internet And there is IMAP client "i1" logged in as "user" And there is SMTP client "s1" logged in as "user" When there is no internet connection - And 1 second pass + And 2 second pass Then IMAP client "i1" is logged out And SMTP client "s1" is logged out Given the internet connection is restored @@ -16,7 +16,7 @@ Feature: Servers are closed when no internet Then IMAP response to "i2" is "OK" Then SMTP response to "s2" is "OK" When there is no internet connection - And 1 second pass + And 2 second pass Then IMAP client "i2" is logged out And SMTP client "s2" is logged out Given the internet connection is restored From 41d82e10f9457eb834adf425af90d0f146e8c335 Mon Sep 17 00:00:00 2001 From: Andrzej Szafranski Date: Mon, 17 May 2021 13:46:30 +0000 Subject: [PATCH 05/15] Other: Bridge James v1.8.0 release notes --- release-notes/bridge_stable.md | 18 ++++++++++++++++++ release-notes/ie_stable.md | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/release-notes/bridge_stable.md b/release-notes/bridge_stable.md index 057dc2e2..d1d0872c 100644 --- a/release-notes/bridge_stable.md +++ b/release-notes/bridge_stable.md @@ -1,3 +1,21 @@ +## v1.8.0 +- 2021-05-17 + +### New +- Refactor of message builder to achieve greater RFC compliance +- Implemented connection manager to improve performance during weak connection, better handling of connection loss and other connectivity issues +- Increased the number of message fetchers to allow more parallel requests - performance improvement +- Log changes for easier debugging (update-related) +- Prompt profile installation during Apple Mail auto-configuration on MacOS Big Sur + +### Fixed + +- Bugs with building of message bodies/headers +- Incorrect naming format of some of the attachments +- Removed html-wrappig of non-decriptable messages - to facilitate decryption outside Bridge and/or allow to store such messages as they are +- Tray icon issues with multiple displays on MacOS + + ## v1.6.9 - 2021-04-01 diff --git a/release-notes/ie_stable.md b/release-notes/ie_stable.md index 142556ff..2528efa3 100644 --- a/release-notes/ie_stable.md +++ b/release-notes/ie_stable.md @@ -1,3 +1,11 @@ +## v1.3.3 +- 2021-05-17 + +### Fixed +- Fixed potential security vulnerability related to rpath +- Improved parsing of embedded messages + + ## v1.3.1 - 2021-03-11 From cb30dd91e3616093cbb12ce0b36cc0431dd15005 Mon Sep 17 00:00:00 2001 From: Jakub Date: Tue, 18 May 2021 18:25:01 +0200 Subject: [PATCH 06/15] Other: fix no internet integration test --- test/features/bridge/no_internet.feature | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/features/bridge/no_internet.feature b/test/features/bridge/no_internet.feature index febfb11e..5abb1841 100644 --- a/test/features/bridge/no_internet.feature +++ b/test/features/bridge/no_internet.feature @@ -5,10 +5,11 @@ Feature: Servers are closed when no internet And there is IMAP client "i1" logged in as "user" And there is SMTP client "s1" logged in as "user" When there is no internet connection - And 2 second pass + And 1 second pass Then IMAP client "i1" is logged out And SMTP client "s1" is logged out Given the internet connection is restored + And 1 second pass And there is IMAP client "i2" logged in as "user" And there is SMTP client "s2" logged in as "user" When IMAP client "i2" gets info of "INBOX" @@ -16,10 +17,11 @@ Feature: Servers are closed when no internet Then IMAP response to "i2" is "OK" Then SMTP response to "s2" is "OK" When there is no internet connection - And 2 second pass + And 1 second pass Then IMAP client "i2" is logged out And SMTP client "s2" is logged out Given the internet connection is restored + And 1 second pass And there is IMAP client "i3" logged in as "user" And there is SMTP client "s3" logged in as "user" When IMAP client "i3" gets info of "INBOX" From 233c55ab19a0a5dd317fadc8d50162b2a82f890a Mon Sep 17 00:00:00 2001 From: Andrzej Szafranski Date: Wed, 19 May 2021 16:07:41 +0000 Subject: [PATCH 07/15] Other: release notes Bridge James v1.8.1 --- release-notes/bridge_early.md | 7 +++++++ release-notes/bridge_stable.md | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/release-notes/bridge_early.md b/release-notes/bridge_early.md index a76b2e43..2be2306e 100644 --- a/release-notes/bridge_early.md +++ b/release-notes/bridge_early.md @@ -1,3 +1,10 @@ +## v1.8.1 +- 2021-05-19 + +### Fixed + +- Hotfix for crash when listing empty folder + ## v1.8.0 - 2021-05-10 diff --git a/release-notes/bridge_stable.md b/release-notes/bridge_stable.md index d1d0872c..d2686968 100644 --- a/release-notes/bridge_stable.md +++ b/release-notes/bridge_stable.md @@ -1,3 +1,10 @@ +## v1.8.1 +- 2021-05-19 + +### Fixed + +- Hotfix for crash when listing empty folder + ## v1.8.0 - 2021-05-17 From c37a0338c5095be8f7c87cd42e72930012cd7cef Mon Sep 17 00:00:00 2001 From: Andrzej Szafranski Date: Thu, 20 May 2021 20:03:15 +0000 Subject: [PATCH 08/15] Other: Release notes 1.8.2 --- release-notes/bridge_early.md | 9 +++++++++ release-notes/bridge_stable.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/release-notes/bridge_early.md b/release-notes/bridge_early.md index 2be2306e..9e3738fd 100644 --- a/release-notes/bridge_early.md +++ b/release-notes/bridge_early.md @@ -1,3 +1,11 @@ +## v1.8.2 +- 2021-05-21 + +### Fixed + +- Hotfix for error during bug reporting + + ## v1.8.1 - 2021-05-19 @@ -5,6 +13,7 @@ - Hotfix for crash when listing empty folder + ## v1.8.0 - 2021-05-10 diff --git a/release-notes/bridge_stable.md b/release-notes/bridge_stable.md index d2686968..e2328c9b 100644 --- a/release-notes/bridge_stable.md +++ b/release-notes/bridge_stable.md @@ -1,3 +1,11 @@ +## v1.8.2 +- 2021-05-21 + +### Fixed + +- Hotfix for error during bug reporting + + ## v1.8.1 - 2021-05-19 @@ -5,6 +13,7 @@ - Hotfix for crash when listing empty folder + ## v1.8.0 - 2021-05-17 From 509ba52ba2ecf1f0b233d90a360ff0e04b523778 Mon Sep 17 00:00:00 2001 From: Jakub Date: Thu, 13 May 2021 07:47:27 +0200 Subject: [PATCH 09/15] GODT-1162: Fix wrong section 1 error when email has no MIME parts --- pkg/message/section.go | 22 +++++++++---- pkg/message/section_test.go | 62 +++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/pkg/message/section.go b/pkg/message/section.go index 515b68ca..2e99927b 100644 --- a/pkg/message/section.go +++ b/pkg/message/section.go @@ -212,6 +212,13 @@ func (bs *BodyStructure) hasInfo(sectionPath []int) bool { return err == nil } +func (bs *BodyStructure) getInfoCheckSection(sectionPath []int) (sectionInfo *SectionInfo, err error) { + if len(*bs) == 1 && len(sectionPath) == 1 && sectionPath[0] == 1 { + sectionPath = []int{} + } + return bs.getInfo(sectionPath) +} + func (bs *BodyStructure) getInfo(sectionPath []int) (sectionInfo *SectionInfo, err error) { path := stringPathFromInts(sectionPath) sectionInfo, ok := (*bs)[path] @@ -223,7 +230,7 @@ func (bs *BodyStructure) getInfo(sectionPath []int) (sectionInfo *SectionInfo, e // GetSection returns bytes of section including MIME header. func (bs *BodyStructure) GetSection(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) { - info, err := bs.getInfo(sectionPath) + info, err := bs.getInfoCheckSection(sectionPath) if err != nil { return } @@ -232,7 +239,7 @@ func (bs *BodyStructure) GetSection(wholeMail io.ReadSeeker, sectionPath []int) // GetSectionContent returns bytes of section content (excluding MIME header). func (bs *BodyStructure) GetSectionContent(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) { - info, err := bs.getInfo(sectionPath) + info, err := bs.getInfoCheckSection(sectionPath) if err != nil { return } @@ -251,8 +258,11 @@ func (bs *BodyStructure) GetMailHeaderBytes(wholeMail io.ReadSeeker) (header []b } func goToOffsetAndReadNBytes(wholeMail io.ReadSeeker, offset, length int) ([]byte, error) { - if length < 1 { - return nil, errors.New("requested non positive length") + if length == 0 { + return []byte{}, nil + } + if length < 0 { + return nil, errors.New("requested negative length") } if offset > 0 { if _, err := wholeMail.Seek(int64(offset), io.SeekStart); err != nil { @@ -266,7 +276,7 @@ func goToOffsetAndReadNBytes(wholeMail io.ReadSeeker, offset, length int) ([]byt // GetSectionHeader returns the mime header of specified section. func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (header textproto.MIMEHeader, err error) { - info, err := bs.getInfo(sectionPath) + info, err := bs.getInfoCheckSection(sectionPath) if err != nil { return } @@ -275,7 +285,7 @@ func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (header textproto.M } func (bs *BodyStructure) GetSectionHeaderBytes(wholeMail io.ReadSeeker, sectionPath []int) (header []byte, err error) { - info, err := bs.getInfo(sectionPath) + info, err := bs.getInfoCheckSection(sectionPath) if err != nil { return } diff --git a/pkg/message/section_test.go b/pkg/message/section_test.go index baf5129f..69eaebee 100644 --- a/pkg/message/section_test.go +++ b/pkg/message/section_test.go @@ -82,6 +82,14 @@ func TestGetSection(t *testing.T) { structReader := strings.NewReader(sampleMail) bs, err := NewBodyStructure(structReader) require.NoError(t, err) + + // Bad paths + wantPaths := [][]int{{0}, {-1}, {3, 2, 3}} + for _, wantPath := range wantPaths { + _, err = bs.getInfo(wantPath) + require.Error(t, err, "path %v", wantPath) + } + // Whole section. for _, try := range testPaths { mailReader := strings.NewReader(sampleMail) @@ -108,6 +116,60 @@ func TestGetSection(t *testing.T) { } } +func TestGetSecionNoMIMEParts(t *testing.T) { + wantBody := "This is just a simple mail with no multipart structure.\n" + wantHeader := `Subject: Sample mail +From: John Doe +To: Mary Smith +Date: Fri, 21 Nov 1997 09:55:06 -0600 +Content-Type: plain/text + +` + wantMail := wantHeader + wantBody + + r := require.New(t) + bs, err := NewBodyStructure(strings.NewReader(wantMail)) + r.NoError(err) + + // Bad parts + wantPaths := [][]int{{0}, {2}, {1, 2, 3}} + for _, wantPath := range wantPaths { + _, err = bs.getInfoCheckSection(wantPath) + r.Error(err, "path %v: %d %d\n__\n%s\n", wantPath) + } + + debug := func(wantPath []int, info *SectionInfo, section []byte) string { + if info == nil { + info = &SectionInfo{} + } + return fmt.Sprintf("path %v %q: %d %d\n___\n%s\n‾‾‾\n", + wantPath, stringPathFromInts(wantPath), info.Start, info.Size, + string(section), + ) + } + + // Ok Parts + wantPaths = [][]int{{}, {1}} + for _, p := range wantPaths { + wantPath := append([]int{}, p...) + + info, err := bs.getInfoCheckSection(wantPath) + r.NoError(err, debug(wantPath, info, []byte{})) + + section, err := bs.GetSection(strings.NewReader(wantMail), wantPath) + r.NoError(err, debug(wantPath, info, section)) + r.Equal(wantMail, string(section), debug(wantPath, info, section)) + + haveBody, err := bs.GetSectionContent(strings.NewReader(wantMail), wantPath) + r.NoError(err, debug(wantPath, info, haveBody)) + r.Equal(wantBody, string(haveBody), debug(wantPath, info, haveBody)) + + haveHeader, err := bs.GetSectionHeaderBytes(strings.NewReader(wantMail), wantPath) + r.NoError(err, debug(wantPath, info, haveHeader)) + r.Equal(wantHeader, string(haveHeader), debug(wantPath, info, haveHeader)) + } +} + func TestGetMainHeaderBytes(t *testing.T) { wantHeader := []byte(`Subject: Sample mail From: John Doe From e01dc77a61dd2136bed69ce2f978be08911a27d2 Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Mon, 17 May 2021 15:56:22 +0200 Subject: [PATCH 10/15] GODT-1044: lite parser --- internal/imap/mailbox_append.go | 227 +++++++++++++++------------ internal/imap/mailbox_header.go | 37 +---- internal/imap/store.go | 2 +- internal/store/mailbox_message.go | 23 ++- pkg/message/encrypt.go | 244 ++++++++++++++++++++++++++++++ pkg/message/encrypt_test.go | 101 +++++++++++++ pkg/message/flags.go | 27 ---- pkg/message/header.go | 123 +++++++++++++++ pkg/message/header_test.go | 76 ++++++++++ pkg/message/scanner.go | 96 ++++++++++++ pkg/message/scanner_test.go | 136 +++++++++++++++++ pkg/message/writer.go | 48 ++++++ 12 files changed, 966 insertions(+), 174 deletions(-) create mode 100644 pkg/message/encrypt.go create mode 100644 pkg/message/encrypt_test.go create mode 100644 pkg/message/header.go create mode 100644 pkg/message/header_test.go create mode 100644 pkg/message/scanner.go create mode 100644 pkg/message/scanner_test.go create mode 100644 pkg/message/writer.go diff --git a/internal/imap/mailbox_append.go b/internal/imap/mailbox_append.go index a4f9f6d8..6cbb68fe 100644 --- a/internal/imap/mailbox_append.go +++ b/internal/imap/mailbox_append.go @@ -18,7 +18,9 @@ package imap import ( - "io" + "bufio" + "bytes" + "io/ioutil" "net/mail" "strings" "time" @@ -28,6 +30,7 @@ import ( "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/emersion/go-imap" + "github.com/emersion/go-message/textproto" "github.com/pkg/errors" ) @@ -43,11 +46,15 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L }, "APPEND", flags, date) } -func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.Literal) error { //nolint[funlen] +func (im *imapMailbox) createMessage(imapFlags []string, date time.Time, r imap.Literal) error { //nolint[funlen] // Called from go-imap in goroutines - we need to handle panics for each function. defer im.panicHandler.HandlePanic() - m, _, _, readers, err := message.Parse(body) + // NOTE: Is this lock meant to be here? + im.user.appendExpungeLock.Lock() + defer im.user.appendExpungeLock.Unlock() + + body, err := ioutil.ReadAll(r) if err != nil { return err } @@ -56,113 +63,88 @@ func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.L if addr == nil { return errors.New("no available address for encryption") } - m.AddressID = addr.ID kr, err := im.user.client().KeyRingForAddressID(addr.ID) if err != nil { return err } - // Handle imported messages which have no "Sender" address. - // This sometimes occurs with outlook which reports errors as imported emails or for drafts. - if m.Sender == nil { - im.log.Warning("Append: Missing email sender. Will use main address") - m.Sender = &mail.Address{ - Name: "", - Address: addr.Email, - } - } - - // "Drafts" needs to call special API routes. - // Clients always append the whole message again and remove the old one. if im.storeMailbox.LabelID() == pmapi.DraftLabel { - // Sender address needs to be sanitised (drafts need to match cases exactly). - m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, addr.Email) - - draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "") - if err != nil { - return errors.Wrap(err, "failed to create draft") - } - - targetSeq := im.storeMailbox.GetUIDList([]string{draft.ID}) - return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq) + return im.createDraftMessage(kr, addr.Email, body) } - // We need to make sure this is an import, and not a sent message from this account - // (sent messages from the account will be added by the event loop). if im.storeMailbox.LabelID() == pmapi.SentLabel { - sanitizedSender := pmapi.SanitizeEmail(m.Sender.Address) + m, _, _, _, err := message.Parse(bytes.NewReader(body)) + if err != nil { + return err + } - // Check whether this message was sent by a bridge user. - user, err := im.user.backend.bridge.GetUser(sanitizedSender) - if err == nil && user.ID() == im.storeUser.UserID() { - logEntry := im.log.WithField("addr", sanitizedSender).WithField("extID", m.Header.Get("Message-Id")) + if m.Sender == nil { + m.Sender = &mail.Address{Address: addr.Email} + } + + if user, err := im.user.backend.bridge.GetUser(pmapi.SanitizeEmail(m.Sender.Address)); err == nil && user.ID() == im.storeUser.UserID() { + logEntry := im.log.WithField("sender", m.Sender).WithField("extID", m.Header.Get("Message-Id")).WithField("date", date) - // If we find the message in the store already, we can skip importing it. if foundUID := im.storeMailbox.GetUIDByHeader(&m.Header); foundUID != uint32(0) { logEntry.Info("Ignoring APPEND of duplicate to Sent folder") return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), &uidplus.OrderedSeq{foundUID}) } - // We didn't find the message in the store, so we are currently sending it. - logEntry.WithField("time", date).Info("No matching UID, continuing APPEND to Sent") + logEntry.Info("No matching UID, continuing APPEND to Sent") } } - message.ParseFlags(m, flags) - if !date.IsZero() { - m.Time = date.Unix() - } - - internalID := m.Header.Get("X-Pm-Internal-Id") - references := m.Header.Get("References") - referenceList := strings.Fields(references) - - // In case there is a mail client which corrupts headers, try - // "References" too. - if internalID == "" && len(referenceList) > 0 { - lastReference := referenceList[len(referenceList)-1] - match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(lastReference) - if len(match) == 2 { - internalID = match[1] - } - } - - im.user.appendExpungeLock.Lock() - defer im.user.appendExpungeLock.Unlock() - - // Avoid appending a message which is already on the server. Apply the - // new label instead. This always happens with Outlook (it uses APPEND - // instead of COPY). - if internalID != "" { - // Check to see if this belongs to a different address in split mode or another ProtonMail account. - msg, err := im.storeMailbox.GetMessage(internalID) - if err == nil && (im.user.user.IsCombinedAddressMode() || (im.storeAddress.AddressID() == msg.Message().AddressID)) { - IDs := []string{internalID} - - // See the comment bellow. - if msg.IsMarkedDeleted() { - if err := im.storeMailbox.MarkMessagesUndeleted(IDs); err != nil { - log.WithError(err).Error("Failed to undelete re-imported internal message") - } - } - - err = im.storeMailbox.LabelMessages(IDs) - if err != nil { - return err - } - - targetSeq := im.storeMailbox.GetUIDList(IDs) - return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq) - } - } - - im.log.Info("Importing external message") - if err := im.importMessage(m, readers, kr); err != nil { - im.log.Error("Import failed: ", err) + hdr, err := textproto.ReadHeader(bufio.NewReader(bytes.NewReader(body))) + if err != nil { return err } + // Avoid appending a message which is already on the server. Apply the new label instead. + // This always happens with Outlook because it uses APPEND instead of COPY. + internalID := hdr.Get("X-Pm-Internal-Id") + + // In case there is a mail client which corrupts headers, try "References" too. + if internalID == "" { + if references := strings.Fields(hdr.Get("References")); len(references) > 0 { + if match := pmapi.RxInternalReferenceFormat.FindStringSubmatch(references[len(references)-1]); len(match) == 2 { + internalID = match[1] + } + } + } + + if internalID != "" { + if msg, err := im.storeMailbox.GetMessage(internalID); err == nil { + if im.user.user.IsCombinedAddressMode() || im.storeAddress.AddressID() == msg.Message().AddressID { + return im.labelExistingMessage(msg.ID(), msg.IsMarkedDeleted()) + } + } + } + + return im.importMessage(kr, hdr, body, imapFlags, date) +} + +func (im *imapMailbox) createDraftMessage(kr *crypto.KeyRing, email string, body []byte) error { + im.log.Info("Creating draft message") + + m, _, _, readers, err := message.Parse(bytes.NewReader(body)) + if err != nil { + return err + } + + m.Sender.Address = pmapi.ConstructAddress(m.Sender.Address, email) + + draft, _, err := im.user.storeUser.CreateDraft(kr, m, readers, "", "", "") + if err != nil { + return errors.Wrap(err, "failed to create draft") + } + + return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{draft.ID})) +} + +func (im *imapMailbox) labelExistingMessage(messageID string, isDeleted bool) error { + im.log.Info("Labelling existing message") + // IMAP clients can move message to local folder (setting \Deleted flag) // and then move it back (IMAP client does not remember the message, // so instead removing the flag it imports duplicate message). @@ -170,29 +152,76 @@ func (im *imapMailbox) createMessage(flags []string, date time.Time, body imap.L // not delete the message (EXPUNGE would delete the original message and // the new duplicate one would stay). API detects duplicates; therefore // we need to remove \Deleted flag if IMAP client re-imports. - msg, err := im.storeMailbox.GetMessage(m.ID) - if err == nil && msg.IsMarkedDeleted() { - if err := im.storeMailbox.MarkMessagesUndeleted([]string{m.ID}); err != nil { + if isDeleted { + if err := im.storeMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil { log.WithError(err).Error("Failed to undelete re-imported message") } } - targetSeq := im.storeMailbox.GetUIDList([]string{m.ID}) - return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), targetSeq) + if err := im.storeMailbox.LabelMessages([]string{messageID}); err != nil { + return err + } + + return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID})) } -func (im *imapMailbox) importMessage(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) (err error) { - body, err := message.BuildEncrypted(m, readers, kr) +func (im *imapMailbox) importMessage(kr *crypto.KeyRing, hdr textproto.Header, body []byte, imapFlags []string, date time.Time) error { + im.log.Info("Importing external message") + + var ( + seen bool + flags int64 + labelIDs []string + time int64 + ) + + if hdr.Get("received") == "" { + flags = pmapi.FlagSent + } else { + flags = pmapi.FlagReceived + } + + for _, flag := range imapFlags { + switch flag { + case imap.DraftFlag: + flags &= ^pmapi.FlagSent + flags &= ^pmapi.FlagReceived + + case imap.SeenFlag: + seen = true + + case imap.FlaggedFlag: + labelIDs = append(labelIDs, pmapi.StarredLabel) + + case imap.AnsweredFlag: + flags |= pmapi.FlagReplied + } + } + + if !date.IsZero() { + time = date.Unix() + } + + enc, err := message.EncryptRFC822(kr, bytes.NewReader(body)) if err != nil { return err } - labels := []string{} - for _, l := range m.LabelIDs { - if l == pmapi.StarredLabel { - labels = append(labels, pmapi.StarredLabel) + messageID, err := im.storeMailbox.ImportMessage(enc, seen, labelIDs, flags, time) + if err != nil { + return err + } + + msg, err := im.storeMailbox.GetMessage(messageID) + if err != nil { + return err + } + + if msg.IsMarkedDeleted() { + if err := im.storeMailbox.MarkMessagesUndeleted([]string{messageID}); err != nil { + log.WithError(err).Error("Failed to undelete re-imported message") } } - return im.storeMailbox.ImportMessage(m, body, labels) + return uidplus.AppendResponse(im.storeMailbox.UIDValidity(), im.storeMailbox.GetUIDList([]string{messageID})) } diff --git a/internal/imap/mailbox_header.go b/internal/imap/mailbox_header.go index fd5d3fe0..a5978ae7 100644 --- a/internal/imap/mailbox_header.go +++ b/internal/imap/mailbox_header.go @@ -18,13 +18,11 @@ package imap import ( - "bufio" "bytes" - "io" "strings" + "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/emersion/go-imap" - "github.com/pkg/errors" ) func filterHeader(header []byte, section *imap.BodySectionName) []byte { @@ -53,7 +51,7 @@ func filterHeader(header []byte, section *imap.BodySectionName) []byte { func filterHeaderLines(header []byte, wantField func(string) bool) []byte { var res []byte - for _, line := range headerLines(header) { + for _, line := range message.HeaderLines(header) { if len(bytes.TrimSpace(line)) == 0 { res = append(res, line...) } else { @@ -71,34 +69,3 @@ func filterHeaderLines(header []byte, wantField func(string) bool) []byte { return res } - -// NOTE: This sucks because we trim and split stuff here already, only to do it again when we use this function! -func headerLines(header []byte) [][]byte { - var lines [][]byte - - r := bufio.NewReader(bytes.NewReader(header)) - - for { - b, err := r.ReadBytes('\n') - if err != nil { - if err != io.EOF { - panic(errors.Wrap(err, "failed to read header line")) - } - - break - } - - switch { - case len(bytes.TrimSpace(b)) == 0: - lines = append(lines, b) - - case len(bytes.SplitN(b, []byte(": "), 2)) != 2: - lines[len(lines)-1] = append(lines[len(lines)-1], b...) - - default: - lines = append(lines, b) - } - } - - return lines -} diff --git a/internal/imap/store.go b/internal/imap/store.go index 7088cd87..918976be 100644 --- a/internal/imap/store.go +++ b/internal/imap/store.go @@ -89,7 +89,7 @@ type storeMailboxProvider interface { MarkMessagesUnstarred(apiID []string) error MarkMessagesDeleted(apiID []string) error MarkMessagesUndeleted(apiID []string) error - ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error + ImportMessage(enc []byte, seen bool, labelIDs []string, flags, time int64) (string, error) RemoveDeleted(apiIDs []string) error } diff --git a/internal/store/mailbox_message.go b/internal/store/mailbox_message.go index 914e720a..cbdda7c3 100644 --- a/internal/store/mailbox_message.go +++ b/internal/store/mailbox_message.go @@ -48,9 +48,7 @@ func (storeMailbox *Mailbox) FetchMessage(apiID string) (*Message, error) { return newStoreMessage(storeMailbox, msg), nil } -// ImportMessage imports the message by calling an API. -// It has to be propagated to all mailboxes which is done by the event loop. -func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labelIDs []string) error { +func (storeMailbox *Mailbox) ImportMessage(enc []byte, seen bool, labelIDs []string, flags, time int64) (string, error) { defer storeMailbox.pollNow() if storeMailbox.labelID != pmapi.AllMailLabel { @@ -59,24 +57,25 @@ func (storeMailbox *Mailbox) ImportMessage(msg *pmapi.Message, body []byte, labe importReqs := &pmapi.ImportMsgReq{ Metadata: &pmapi.ImportMetadata{ - AddressID: msg.AddressID, - Unread: msg.Unread, - Flags: msg.Flags, - Time: msg.Time, + AddressID: storeMailbox.storeAddress.addressID, + Unread: pmapi.Boolean(!seen), + Flags: flags, + Time: time, LabelIDs: labelIDs, }, - Message: body, + Message: append(enc, "\r\n"...), } res, err := storeMailbox.client().Import(exposeContextForIMAP(), pmapi.ImportMsgReqs{importReqs}) if err != nil { - return err + return "", err } + if len(res) == 0 { - return errors.New("no import response") + return "", errors.New("no import response") } - msg.ID = res[0].MessageID - return res[0].Error + + return res[0].MessageID, res[0].Error } // LabelMessages adds the label by calling an API. diff --git a/pkg/message/encrypt.go b/pkg/message/encrypt.go new file mode 100644 index 00000000..7324d629 --- /dev/null +++ b/pkg/message/encrypt.go @@ -0,0 +1,244 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package message + +import ( + "bytes" + "encoding/base64" + "io" + "io/ioutil" + "mime" + "mime/quotedprintable" + "strings" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + pmmime "github.com/ProtonMail/proton-bridge/pkg/mime" + "github.com/emersion/go-message/textproto" +) + +func EncryptRFC822(kr *crypto.KeyRing, r io.Reader) ([]byte, error) { + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + + header, body, err := readHeaderBody(b) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + + result, err := writeEncryptedPart(kr, header, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + if err := textproto.WriteHeader(buf, *header); err != nil { + return nil, err + } + + if _, err := result.WriteTo(buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func writeEncryptedPart(kr *crypto.KeyRing, header *textproto.Header, r io.Reader) (io.WriterTo, error) { + decoder := getTransferDecoder(r, header.Get("Content-Transfer-Encoding")) + encoded := new(bytes.Buffer) + + contentType, contentParams, err := parseContentType(header.Get("Content-Type")) + if err != nil { + return nil, err + } + + switch { + case contentType == "", strings.HasPrefix(contentType, "text/"), strings.HasPrefix(contentType, "message/"): + header.Del("Content-Transfer-Encoding") + + if charset, ok := contentParams["charset"]; ok { + if reader, err := pmmime.CharsetReader(charset, decoder); err == nil { + decoder = reader + + // We can decode the charset to utf-8 so let's set that as the content type charset parameter. + contentParams["charset"] = "utf-8" + + header.Set("Content-Type", mime.FormatMediaType(contentType, contentParams)) + } + } + + if err := encode(&writeCloser{encoded}, func(w io.Writer) error { + return writeEncryptedTextPart(w, decoder, kr) + }); err != nil { + return nil, err + } + + case contentType == "multipart/encrypted": + if _, err := encoded.ReadFrom(decoder); err != nil { + return nil, err + } + + case strings.HasPrefix(contentType, "multipart/"): + if err := encode(&writeCloser{encoded}, func(w io.Writer) error { + return writeEncryptedMultiPart(kr, w, header, decoder) + }); err != nil { + return nil, err + } + + default: + header.Set("Content-Transfer-Encoding", "base64") + + if err := encode(base64.NewEncoder(base64.StdEncoding, encoded), func(w io.Writer) error { + return writeEncryptedAttachmentPart(w, decoder, kr) + }); err != nil { + return nil, err + } + } + + return encoded, nil +} + +func writeEncryptedTextPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error { + dec, err := ioutil.ReadAll(r) + if err != nil { + return err + } + + var arm string + + if msg, err := crypto.NewPGPMessageFromArmored(string(dec)); err != nil { + enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr) + if err != nil { + return err + } + + if arm, err = enc.GetArmored(); err != nil { + return err + } + } else if arm, err = msg.GetArmored(); err != nil { + return err + } + + if _, err := io.WriteString(w, arm); err != nil { + return err + } + + return nil +} + +func writeEncryptedAttachmentPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error { + dec, err := ioutil.ReadAll(r) + if err != nil { + return err + } + + enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr) + if err != nil { + return err + } + + if _, err := w.Write(enc.GetBinary()); err != nil { + return err + } + + return nil +} + +func writeEncryptedMultiPart(kr *crypto.KeyRing, w io.Writer, header *textproto.Header, r io.Reader) error { + _, contentParams, err := parseContentType(header.Get("Content-Type")) + if err != nil { + return err + } + + scanner, err := newPartScanner(r, contentParams["boundary"]) + if err != nil { + return err + } + + parts, err := scanner.scanAll() + if err != nil { + return err + } + + writer := newPartWriter(w, contentParams["boundary"]) + + for _, part := range parts { + header, body, err := readHeaderBody(part.b) + if err != nil { + return err + } + + result, err := writeEncryptedPart(kr, header, bytes.NewReader(body)) + if err != nil { + return err + } + + if err := writer.createPart(func(w io.Writer) error { + if err := textproto.WriteHeader(w, *header); err != nil { + return err + } + + if _, err := result.WriteTo(w); err != nil { + return err + } + + return nil + }); err != nil { + return err + } + } + + return writer.done() +} + +func getTransferDecoder(r io.Reader, encoding string) io.Reader { + switch strings.ToLower(encoding) { + case "base64": + return base64.NewDecoder(base64.StdEncoding, r) + + case "quoted-printable": + return quotedprintable.NewReader(r) + + default: + return r + } +} + +func encode(wc io.WriteCloser, fn func(io.Writer) error) error { + if err := fn(wc); err != nil { + return err + } + + return wc.Close() +} + +type writeCloser struct { + io.Writer +} + +func (writeCloser) Close() error { return nil } + +func parseContentType(val string) (string, map[string]string, error) { + if val == "" { + val = "text/plain" + } + + return pmmime.ParseMediaType(val) +} diff --git a/pkg/message/encrypt_test.go b/pkg/message/encrypt_test.go new file mode 100644 index 00000000..df256350 --- /dev/null +++ b/pkg/message/encrypt_test.go @@ -0,0 +1,101 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package message + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/stretchr/testify/require" +) + +func TestEncryptRFC822(t *testing.T) { + literal, err := ioutil.ReadFile("testdata/text_plain_latin1.eml") + require.NoError(t, err) + + key, err := crypto.GenerateKey("name", "email", "rsa", 2048) + require.NoError(t, err) + + kr, err := crypto.NewKeyRing(key) + require.NoError(t, err) + + enc, err := EncryptRFC822(kr, bytes.NewReader(literal)) + require.NoError(t, err) + + section(t, enc). + expectContentType(is(`text/plain`)). + expectContentTypeParam(`charset`, is(`utf-8`)). + expectBody(decryptsTo(kr, `ééééééé`)) +} + +func TestEncryptRFC822Multipart(t *testing.T) { + literal, err := ioutil.ReadFile("testdata/multipart_alternative_nested.eml") + require.NoError(t, err) + + key, err := crypto.GenerateKey("name", "email", "rsa", 2048) + require.NoError(t, err) + + kr, err := crypto.NewKeyRing(key) + require.NoError(t, err) + + enc, err := EncryptRFC822(kr, bytes.NewReader(literal)) + require.NoError(t, err) + + section(t, enc). + expectContentType(is(`multipart/alternative`)) + + section(t, enc, 1). + expectContentType(is(`multipart/alternative`)) + + section(t, enc, 1, 1). + expectContentType(is(`text/plain`)). + expectBody(decryptsTo(kr, "*multipart 1.1*\n\n")) + + section(t, enc, 1, 2). + expectContentType(is(`text/html`)). + expectBody(decryptsTo(kr, ` + + + + + multipart 1.2 + + +`)) + + section(t, enc, 2). + expectContentType(is(`multipart/alternative`)) + + section(t, enc, 2, 1). + expectContentType(is(`text/plain`)). + expectBody(decryptsTo(kr, "*multipart 2.1*\n\n")) + + section(t, enc, 2, 2). + expectContentType(is(`text/html`)). + expectBody(decryptsTo(kr, ` + + + + + multipart 2.2 + + +`)) +} diff --git a/pkg/message/flags.go b/pkg/message/flags.go index 5ed2c2c6..9b4cf1b0 100644 --- a/pkg/message/flags.go +++ b/pkg/message/flags.go @@ -59,30 +59,3 @@ func GetFlags(m *pmapi.Message) (flags []string) { return } - -// ParseFlags sets attributes to pmapi messages based on imap flags. -func ParseFlags(m *pmapi.Message, flags []string) { - if m.Header.Get("received") == "" { - m.Flags = pmapi.FlagSent - } else { - m.Flags = pmapi.FlagReceived - } - - m.Unread = true - for _, f := range flags { - switch f { - case imap.SeenFlag: - m.Unread = false - case imap.DraftFlag: - m.Flags &= ^pmapi.FlagSent - m.Flags &= ^pmapi.FlagReceived - m.LabelIDs = append(m.LabelIDs, pmapi.DraftLabel) - case imap.FlaggedFlag: - m.LabelIDs = append(m.LabelIDs, pmapi.StarredLabel) - case imap.AnsweredFlag: - m.Flags |= pmapi.FlagReplied - case AppleMailJunkFlag, ThunderbirdJunkFlag: - m.LabelIDs = append(m.LabelIDs, pmapi.SpamLabel) - } - } -} diff --git a/pkg/message/header.go b/pkg/message/header.go new file mode 100644 index 00000000..c4cf9659 --- /dev/null +++ b/pkg/message/header.go @@ -0,0 +1,123 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package message + +import ( + "bufio" + "bytes" + "io" + "io/ioutil" + + "github.com/emersion/go-message/textproto" + "github.com/pkg/errors" +) + +// HeaderLines returns each line in the given header. +func HeaderLines(header []byte) [][]byte { + var ( + lines [][]byte + quote int + ) + + forEachLine(bufio.NewReader(bytes.NewReader(header)), func(line []byte) { + switch { + case len(bytes.TrimSpace(line)) == 0: + lines = append(lines, line) + + case quote%2 != 0, len(bytes.SplitN(line, []byte(`: `), 2)) != 2: + if len(lines) > 0 { + lines[len(lines)-1] = append(lines[len(lines)-1], line...) + } else { + lines = append(lines, line) + } + + default: + lines = append(lines, line) + } + + quote += bytes.Count(line, []byte(`"`)) + }) + + return lines +} + +func forEachLine(br *bufio.Reader, fn func([]byte)) { + for { + b, err := br.ReadBytes('\n') + if err != nil { + if !errors.Is(err, io.EOF) { + panic(err) + } + + if len(b) > 0 { + fn(b) + } + + return + } + + fn(b) + } +} + +func readHeaderBody(b []byte) (*textproto.Header, []byte, error) { + rawHeader, body, err := splitHeaderBody(b) + if err != nil { + return nil, nil, err + } + + var header textproto.Header + + for _, line := range HeaderLines(rawHeader) { + if len(bytes.TrimSpace(line)) > 0 { + header.AddRaw(line) + } + } + + return &header, body, nil +} + +func splitHeaderBody(b []byte) ([]byte, []byte, error) { + br := bufio.NewReader(bytes.NewReader(b)) + + var header []byte + + for { + b, err := br.ReadBytes('\n') + if err != nil { + if !errors.Is(err, io.EOF) { + panic(err) + } + + break + } + + header = append(header, b...) + + if len(bytes.TrimSpace(b)) == 0 { + break + } + } + + body, err := ioutil.ReadAll(br) + if err != nil && !errors.Is(err, io.EOF) { + return nil, nil, err + } + + return header, body, nil +} diff --git a/pkg/message/header_test.go b/pkg/message/header_test.go new file mode 100644 index 00000000..89968740 --- /dev/null +++ b/pkg/message/header_test.go @@ -0,0 +1,76 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package message + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHeaderLines(t *testing.T) { + const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n\r\n" + + assert.Equal(t, [][]byte{ + []byte("To: somebody\r\n"), + []byte("From: somebody else\r\n"), + []byte("Subject: this is\r\n\ta multiline field\r\n"), + []byte("\r\n"), + }, HeaderLines([]byte(header))) +} + +func TestHeaderLinesMultilineFilename(t *testing.T) { + const header = "Content-Type: application/msword; name=\"this is a very long\nfilename.doc\"" + + assert.Equal(t, [][]byte{ + []byte("Content-Type: application/msword; name=\"this is a very long\nfilename.doc\""), + }, HeaderLines([]byte(header))) +} + +func TestHeaderLinesMultilineFilenameWithColon(t *testing.T) { + const header = "Content-Type: application/msword; name=\"this is a very long\nfilename: too long.doc\"" + + assert.Equal(t, [][]byte{ + []byte("Content-Type: application/msword; name=\"this is a very long\nfilename: too long.doc\""), + }, HeaderLines([]byte(header))) +} + +func TestHeaderLinesMultilineFilenameWithColonAndNewline(t *testing.T) { + const header = "Content-Type: application/msword; name=\"this is a very long\nfilename: too long.doc\"\n" + + assert.Equal(t, [][]byte{ + []byte("Content-Type: application/msword; name=\"this is a very long\nfilename: too long.doc\"\n"), + }, HeaderLines([]byte(header))) +} + +func TestHeaderLinesMultipleMultilineFilenames(t *testing.T) { + const header = `Content-Type: application/msword; name="=E5=B8=B6=E6=9C=89=E5=A4=96=E5=9C=8B=E5=AD=97=E7=AC=A6=E7=9A=84=E9=99=84=E4= +=BB=B6.DOC" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="=E5=B8=B6=E6=9C=89=E5=A4=96=E5=9C=8B=E5=AD=97=E7=AC=A6=E7=9A=84=E9=99=84=E4= +=BB=B6.DOC" +Content-ID: <> +` + + assert.Equal(t, [][]byte{ + []byte("Content-Type: application/msword; name=\"=E5=B8=B6=E6=9C=89=E5=A4=96=E5=9C=8B=E5=AD=97=E7=AC=A6=E7=9A=84=E9=99=84=E4=\n=BB=B6.DOC\"\n"), + []byte("Content-Transfer-Encoding: base64\n"), + []byte("Content-Disposition: attachment; filename=\"=E5=B8=B6=E6=9C=89=E5=A4=96=E5=9C=8B=E5=AD=97=E7=AC=A6=E7=9A=84=E9=99=84=E4=\n=BB=B6.DOC\"\n"), + []byte("Content-ID: <>\n"), + }, HeaderLines([]byte(header))) +} diff --git a/pkg/message/scanner.go b/pkg/message/scanner.go new file mode 100644 index 00000000..657c3fe5 --- /dev/null +++ b/pkg/message/scanner.go @@ -0,0 +1,96 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package message + +import ( + "bufio" + "bytes" + "errors" + "io" +) + +type partScanner struct { + r *bufio.Reader + + boundary string + progress int +} + +type part struct { + b []byte + offset int +} + +func newPartScanner(r io.Reader, boundary string) (*partScanner, error) { + scanner := &partScanner{r: bufio.NewReader(r), boundary: boundary} + + if _, _, err := scanner.readToBoundary(); err != nil { + return nil, err + } + + return scanner, nil +} + +func (s *partScanner) scanAll() ([]part, error) { + var parts []part + + for { + offset := s.progress + + b, more, err := s.readToBoundary() + if err != nil { + return nil, err + } + + if !more { + return parts, nil + } + + parts = append(parts, part{b: b, offset: offset}) + } +} + +func (s *partScanner) readToBoundary() ([]byte, bool, error) { + var res []byte + + for { + line, err := s.r.ReadBytes('\n') + if err != nil { + if !errors.Is(err, io.EOF) { + return nil, false, err + } + + if len(line) == 0 { + return nil, false, nil + } + } + + s.progress += len(line) + + switch { + case bytes.HasPrefix(bytes.TrimSpace(line), []byte("--"+s.boundary)): + return bytes.TrimSuffix(bytes.TrimSuffix(res, []byte("\n")), []byte("\r")), true, nil + + case bytes.HasSuffix(bytes.TrimSpace(line), []byte(s.boundary+"--")): + return bytes.TrimSuffix(bytes.TrimSuffix(res, []byte("\n")), []byte("\r")), false, nil + + default: + res = append(res, line...) + } + } +} diff --git a/pkg/message/scanner_test.go b/pkg/message/scanner_test.go new file mode 100644 index 00000000..918e91cd --- /dev/null +++ b/pkg/message/scanner_test.go @@ -0,0 +1,136 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package message + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScanner(t *testing.T) { + const literal = `this part of the text should be ignored + +--longrandomstring + +body1 + +--longrandomstring + +body2 + +--longrandomstring-- +` + + scanner, err := newPartScanner(strings.NewReader(literal), "longrandomstring") + require.NoError(t, err) + + parts, err := scanner.scanAll() + require.NoError(t, err) + + assert.Equal(t, "\nbody1\n", string(parts[0].b)) + assert.Equal(t, "\nbody2\n", string(parts[1].b)) + + assert.Equal(t, "\nbody1\n", literal[parts[0].offset:parts[0].offset+len(parts[0].b)]) + assert.Equal(t, "\nbody2\n", literal[parts[1].offset:parts[1].offset+len(parts[1].b)]) +} + +func TestScannerNested(t *testing.T) { + const literal = `This is the preamble. It is to be ignored, though it +is a handy place for mail composers to include an +explanatory note to non-MIME compliant readers. +--simple boundary +Content-type: multipart/mixed; boundary="nested boundary" + +This is the preamble. It is to be ignored, though it +is a handy place for mail composers to include an +explanatory note to non-MIME compliant readers. +--nested boundary +Content-type: text/plain; charset=us-ascii + +This part does not end with a linebreak. +--nested boundary +Content-type: text/plain; charset=us-ascii + +This part does end with a linebreak. + +--nested boundary-- +--simple boundary +Content-type: text/plain; charset=us-ascii + +This part does end with a linebreak. + +--simple boundary-- +This is the epilogue. It is also to be ignored. +` + + scanner, err := newPartScanner(strings.NewReader(literal), "simple boundary") + require.NoError(t, err) + + parts, err := scanner.scanAll() + require.NoError(t, err) + + assert.Equal(t, `Content-type: multipart/mixed; boundary="nested boundary" + +This is the preamble. It is to be ignored, though it +is a handy place for mail composers to include an +explanatory note to non-MIME compliant readers. +--nested boundary +Content-type: text/plain; charset=us-ascii + +This part does not end with a linebreak. +--nested boundary +Content-type: text/plain; charset=us-ascii + +This part does end with a linebreak. + +--nested boundary--`, string(parts[0].b)) + assert.Equal(t, `Content-type: text/plain; charset=us-ascii + +This part does end with a linebreak. +`, string(parts[1].b)) +} + +func TestScannerNoFinalLinebreak(t *testing.T) { + const literal = `--nested boundary +Content-type: text/plain; charset=us-ascii + +This part does not end with a linebreak. +--nested boundary +Content-type: text/plain; charset=us-ascii + +This part does end with a linebreak. + +--nested boundary--` + + scanner, err := newPartScanner(strings.NewReader(literal), "nested boundary") + require.NoError(t, err) + + parts, err := scanner.scanAll() + require.NoError(t, err) + + assert.Equal(t, `Content-type: text/plain; charset=us-ascii + +This part does not end with a linebreak.`, string(parts[0].b)) + assert.Equal(t, `Content-type: text/plain; charset=us-ascii + +This part does end with a linebreak. +`, string(parts[1].b)) +} diff --git a/pkg/message/writer.go b/pkg/message/writer.go new file mode 100644 index 00000000..ed910519 --- /dev/null +++ b/pkg/message/writer.go @@ -0,0 +1,48 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package message + +import ( + "fmt" + "io" +) + +type partWriter struct { + w io.Writer + boundary string +} + +func newPartWriter(w io.Writer, boundary string) *partWriter { + return &partWriter{w: w, boundary: boundary} +} + +func (w *partWriter) createPart(fn func(io.Writer) error) error { + if _, err := fmt.Fprintf(w.w, "\r\n--%v\r\n", w.boundary); err != nil { + return err + } + + return fn(w.w) +} + +func (w *partWriter) done() error { + if _, err := fmt.Fprintf(w.w, "\r\n--%v--\r\n", w.boundary); err != nil { + return err + } + + return nil +} From d0a97a3f4abdbdd1dd7ebcfd938c5e4d73140871 Mon Sep 17 00:00:00 2001 From: Jakub Date: Wed, 26 May 2021 14:25:34 +0200 Subject: [PATCH 11/15] GODT-1044: fix header lines parsing --- pkg/message/header.go | 6 +++++- pkg/message/header_test.go | 15 ++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/message/header.go b/pkg/message/header.go index c4cf9659..a911e827 100644 --- a/pkg/message/header.go +++ b/pkg/message/header.go @@ -35,11 +35,15 @@ func HeaderLines(header []byte) [][]byte { ) forEachLine(bufio.NewReader(bytes.NewReader(header)), func(line []byte) { + l := bytes.SplitN(line, []byte(`: `), 2) + isLineContinuation := quote%2 != 0 || // no quotes opened + len(l) != 2 || // it doesn't have colon + (len(l) == 2 && !bytes.Equal(bytes.TrimSpace(l[0]), l[0])) // has white space in front of header field switch { case len(bytes.TrimSpace(line)) == 0: lines = append(lines, line) - case quote%2 != 0, len(bytes.SplitN(line, []byte(`: `), 2)) != 2: + case isLineContinuation: if len(lines) > 0 { lines[len(lines)-1] = append(lines[len(lines)-1], line...) } else { diff --git a/pkg/message/header_test.go b/pkg/message/header_test.go index 89968740..0b9e911c 100644 --- a/pkg/message/header_test.go +++ b/pkg/message/header_test.go @@ -24,14 +24,19 @@ import ( ) func TestHeaderLines(t *testing.T) { - const header = "To: somebody\r\nFrom: somebody else\r\nSubject: this is\r\n\ta multiline field\r\n\r\n" - - assert.Equal(t, [][]byte{ + want := [][]byte{ []byte("To: somebody\r\n"), []byte("From: somebody else\r\n"), - []byte("Subject: this is\r\n\ta multiline field\r\n"), + []byte("Subject: RE: this is\r\n\ta multiline field: with colon\r\n\tor: many: more: colons\r\n"), + []byte("X-Special: \r\n\tNothing on the first line\r\n\tbut has something on the other lines\r\n"), []byte("\r\n"), - }, HeaderLines([]byte(header))) + } + var header []byte + for _, line := range want { + header = append(header, line...) + } + + assert.Equal(t, want, HeaderLines(header)) } func TestHeaderLinesMultilineFilename(t *testing.T) { From e10aa89313910514829eeb867321fe78e687d9ba Mon Sep 17 00:00:00 2001 From: Jakub Date: Wed, 26 May 2021 17:10:30 +0200 Subject: [PATCH 12/15] Other: Bridge James 1.8.3 --- Changelog.md | 12 ++++++++++++ Makefile | 2 +- release-notes/bridge_early.md | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index f0479e8b..b3b0734a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,16 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) +## [Bridge 1.8.3] James + +## Added +GODT-1044: Lite parser for appended messages. + +## Fixed +GODT-1161: Guarantee order of responses when creating new message. +GODT-1162: Fix wrong section 1 error when email has no MIME parts. + + ## [Bridge 1.8.2] James ### Fixed @@ -20,11 +30,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) * GODT-1056 Check encrypted size of the message before upload. * GODT-1143 Turn off SMTP server while no connection. * GODT-1089 Explicitly open system preferences window on BigSur. +* GODT-35: Connection manager with resty. ### Fixed * GODT-1159 SMTP server not restarting after restored internet. * GODT-1146 Refactor handling of fetching BODY[HEADER] (and similar) regarding trailing newline. * GODT-1152 Correctly resolve wildcard sequence/UID set. +* GODT-876 Set default from if empty for importing draft. * Other: Avoid API jail. diff --git a/Makefile b/Makefile index d5117395..70f9de81 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ TARGET_OS?=${GOOS} .PHONY: build build-ie build-nogui build-ie-nogui build-launcher build-launcher-ie versioner hasher # Keep version hardcoded so app build works also without Git repository. -BRIDGE_APP_VERSION?=1.8.2+git +BRIDGE_APP_VERSION?=1.8.3+git IE_APP_VERSION?=1.3.3+git APP_VERSION:=${BRIDGE_APP_VERSION} SRC_ICO:=logo.ico diff --git a/release-notes/bridge_early.md b/release-notes/bridge_early.md index 9e3738fd..0d44e25a 100644 --- a/release-notes/bridge_early.md +++ b/release-notes/bridge_early.md @@ -1,3 +1,17 @@ +## v1.8.3 +- 2021-05-31 + +### New + +- Improved moving messages from other accounts to ProtonMail - implemented new parser for processing such messages +- Performance improvements + +### Fixed + +- Sync issue with Microsoft Outlook (changed the order of processing requests) +- Fetching the bodies of non-multipart messages + + ## v1.8.2 - 2021-05-21 From c69239ca1639ff0c867954c6ce4981679bfb5c5a Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Thu, 27 May 2021 11:03:49 +0200 Subject: [PATCH 13/15] Other: bump go-rfc5322 dependency to v0.8.0 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 205dabf7..a6a1e4d5 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/Masterminds/semver/v3 v3.1.0 github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde - github.com/ProtonMail/go-rfc5322 v0.5.0 + github.com/ProtonMail/go-rfc5322 v0.8.0 github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 github.com/ProtonMail/gopenpgp/v2 v2.1.3 github.com/PuerkitoBio/goquery v1.5.1 diff --git a/go.sum b/go.sum index 09da5138..8741377b 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDE github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:795VPXcRUIQ9JyMNHP4el582VokQfippgjkQP3Gk0r0= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= -github.com/ProtonMail/go-rfc5322 v0.5.0 h1:LbKWjgfvumYZCr8BgGyTUk3ETGkFLAjQdkuSUpZ5CcE= -github.com/ProtonMail/go-rfc5322 v0.5.0/go.mod h1:mzZWlMWnQJuYLL7JpzuPF5+FimV2lZ9f0jeq24kJjpU= +github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU= +github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= github.com/ProtonMail/gopenpgp/v2 v2.1.3 h1:4+nFDJ9WtcUQTip/je2Ll3P21XhAUl4asWsafLrw97c= From a2029002c43e9ffa75d1ae1b7f8a780b292f054e Mon Sep 17 00:00:00 2001 From: Jakub Date: Fri, 14 May 2021 09:36:48 +0200 Subject: [PATCH 14/15] GODT-1155 Update gopenpgp and use go-srp --- go.mod | 11 +- go.sum | 35 +-- internal/frontend/cli-ie/accounts.go | 4 +- internal/frontend/cli/accounts.go | 4 +- internal/frontend/qt-common/accounts.go | 4 +- internal/frontend/qt/accounts.go | 4 +- internal/frontend/types/types.go | 8 +- internal/users/credentials/credentials.go | 28 +-- .../users/credentials/credentials_test.go | 4 +- internal/users/credentials/store.go | 4 +- internal/users/credentials/store_test.go | 2 +- internal/users/mocks/mocks.go | 4 +- internal/users/types.go | 4 +- internal/users/user.go | 4 +- internal/users/user_credentials_test.go | 2 +- internal/users/user_new_test.go | 2 +- internal/users/users.go | 10 +- internal/users/users_login_test.go | 8 +- internal/users/users_new_test.go | 2 +- internal/users/users_test.go | 10 +- pkg/pmapi/attachments_test.go | 135 ++++++----- pkg/pmapi/keyring_test.go | 25 +- pkg/pmapi/manager_auth.go | 8 +- pkg/pmapi/manager_types.go | 2 +- pkg/pmapi/mocks/mocks.go | 2 +- pkg/pmapi/passwords.go | 18 +- pkg/pmapi/passwords_test.go | 44 ++++ pkg/srp/hash.go | 107 --------- pkg/srp/srp.go | 219 ------------------ pkg/srp/srp_test.go | 111 --------- test/accounts/account.go | 8 +- test/context/credentials.go | 6 +- test/context/pmapi_controller.go | 2 +- test/context/users.go | 4 +- test/fakeapi/controller_control.go | 2 +- test/fakeapi/controller_session.go | 5 +- test/fakeapi/controller_user.go | 2 +- test/fakeapi/manager.go | 2 +- test/liveapi/users.go | 2 +- test/users_actions_test.go | 2 +- 40 files changed, 257 insertions(+), 603 deletions(-) create mode 100644 pkg/pmapi/passwords_test.go delete mode 100644 pkg/srp/hash.go delete mode 100644 pkg/srp/srp.go delete mode 100644 pkg/srp/srp_test.go diff --git a/go.mod b/go.mod index a6a1e4d5..a7bffb69 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,7 @@ go 1.13 require ( github.com/docker/docker-credential-helpers v0.6.3 github.com/emersion/go-imap v1.0.6 - github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + github.com/jameskeane/bcrypt v0.0.0-20170924085257-7509ea014998 // indirect ) require ( @@ -17,8 +16,9 @@ require ( github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde github.com/ProtonMail/go-rfc5322 v0.8.0 + github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01 github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 - github.com/ProtonMail/gopenpgp/v2 v2.1.3 + github.com/ProtonMail/gopenpgp/v2 v2.1.9 github.com/PuerkitoBio/goquery v1.5.1 github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect @@ -61,13 +61,14 @@ require ( github.com/urfave/cli/v2 v2.2.0 github.com/vmihailenco/msgpack/v5 v5.1.3 go.etcd.io/bbolt v1.3.5 + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 golang.org/x/text v0.3.5-0.20201125200606-c27b9fd57aec + ) replace ( github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac - github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 - golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c + github.com/jameskeane/bcrypt => github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57 ) diff --git a/go.sum b/go.sum index 8741377b..02ee7bff 100644 --- a/go.sum +++ b/go.sum @@ -8,16 +8,14 @@ github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMd github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998 h1:YT2uVwQiRQZxCaaahwfcgTq2j3j66w00n/27gb/zubs= -github.com/ProtonMail/bcrypt v0.0.0-20170924085257-7509ea014998/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= -github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c h1:iaVbEOnskSGgcH7XQWHG6VPirHDRoYe+Idd0/dl4m8A= -github.com/ProtonMail/crypto v0.0.0-20201112115411-41db4ea0dd1c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= +github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57 h1:pHA4K54ifoogVLunGGHi3xyF5Nz4x+Uh3dJuy3NwGQQ= +github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk= github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a h1:fXK2KsfnkBV9Nh+9SKzHchYjuE9s0vI20JG1mbtEAcc= github.com/ProtonMail/go-autostart v0.0.0-20181114175602-c5272053443a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4= -github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2 h1:pQkjJELHayW59jp7r4G5Dlmnicr5McejDfwsjcwI1SU= -github.com/ProtonMail/go-crypto v0.0.0-20201208171014-cdb7591792e2/go.mod h1:HTM9X7e9oLwn7RiqLG0UVwVRJenLs3wN+tQ0NPAfwMQ= +github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= +github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs= github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= github.com/ProtonMail/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:5koQozTDELymYOyFbQ/VSubexAEXzDR8qGM5mO8GRdw= @@ -26,10 +24,12 @@ github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1 github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= github.com/ProtonMail/go-rfc5322 v0.8.0 h1:7emrf75n3CDIduQflx7aT1nJa5h/kGsiFKUYX/+IAkU= github.com/ProtonMail/go-rfc5322 v0.8.0/go.mod h1:BwpTbkJxkMGkc+pC84AXZnwuWOisEULBpfPIyIKS/Us= +github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01 h1:sRxNvPGnJFh6yWlSr9BpGsSrshFkZLClSm5oIi++a0I= +github.com/ProtonMail/go-srp v0.0.0-20210514134713-bd9454f3fa01/go.mod h1:jOXzdvWTILIJzl83yzi/EZcnnhpI+A/5EyflaeVfi/0= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5 h1:Uga1DHFN4GUxuDQr0F71tpi8I9HqPIlZodZAI1lR6VQ= github.com/ProtonMail/go-vcard v0.0.0-20180326232728-33aaa0a0c8a5/go.mod h1:oeP9CMN+ajWp5jKp1kue5daJNwMMxLF+ujPaUIoJWlA= -github.com/ProtonMail/gopenpgp/v2 v2.1.3 h1:4+nFDJ9WtcUQTip/je2Ll3P21XhAUl4asWsafLrw97c= -github.com/ProtonMail/gopenpgp/v2 v2.1.3/go.mod h1:WeYndoqEcRR4/QbgRL24z6OwYX5T1RWerRk8NfZ6rJM= +github.com/ProtonMail/gopenpgp/v2 v2.1.9 h1:MdvkFBP8ldOHYOoaVct9LO+Zv5rl6VdeN1QurntRmkc= +github.com/ProtonMail/gopenpgp/v2 v2.1.9/go.mod h1:CHIXesUdnPxIxtJTg2P/cxoA0cvUwIBpZIS8SsY82QA= github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= @@ -285,10 +285,21 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190910184405-b558ed863381/go.mod h1:p895TfNkDgPEmEQrNiOtIl3j98d/tGU95djDj7NfyjQ= golang.org/x/mobile v0.0.0-20200801112145-973feb4309de/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -305,13 +316,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -320,6 +329,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -327,13 +337,10 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -347,8 +354,8 @@ golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/frontend/cli-ie/accounts.go b/internal/frontend/cli-ie/accounts.go index 8a877142..b98c0e19 100644 --- a/internal/frontend/cli-ie/accounts.go +++ b/internal/frontend/cli-ie/accounts.go @@ -68,7 +68,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen] } f.Println("Authenticating ... ") - client, auth, err := f.ie.Login(loginName, password) + client, auth, err := f.ie.Login(loginName, []byte(password)) if err != nil { f.processAPIError(err) return @@ -96,7 +96,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen] } f.Println("Adding account ...") - user, err := f.ie.FinishLogin(client, auth, mailboxPassword) + user, err := f.ie.FinishLogin(client, auth, []byte(mailboxPassword)) if err != nil { log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful") f.Println("Adding account was unsuccessful:", err) diff --git a/internal/frontend/cli/accounts.go b/internal/frontend/cli/accounts.go index cc71a386..a1b7152a 100644 --- a/internal/frontend/cli/accounts.go +++ b/internal/frontend/cli/accounts.go @@ -115,7 +115,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen] } f.Println("Authenticating ... ") - client, auth, err := f.bridge.Login(loginName, password) + client, auth, err := f.bridge.Login(loginName, []byte(password)) if err != nil { f.processAPIError(err) return @@ -143,7 +143,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { // nolint[funlen] } f.Println("Adding account ...") - user, err := f.bridge.FinishLogin(client, auth, mailboxPassword) + user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword)) if err != nil { log.WithField("username", loginName).WithError(err).Error("Login was unsuccessful") f.Println("Adding account was unsuccessful:", err) diff --git a/internal/frontend/qt-common/accounts.go b/internal/frontend/qt-common/accounts.go index 264aa946..29609a65 100644 --- a/internal/frontend/qt-common/accounts.go +++ b/internal/frontend/qt-common/accounts.go @@ -186,7 +186,7 @@ func (a *Accounts) showLoginError(err error, scope string) bool { // 2: when has no 2FA but have MBOX func (a *Accounts) Login(login, password string) int { var err error - a.authClient, a.auth, err = a.um.Login(login, password) + a.authClient, a.auth, err = a.um.Login(login, []byte(password)) if a.showLoginError(err, "login") { return -1 } @@ -230,7 +230,7 @@ func (a *Accounts) AddAccount(mailboxPassword string) int { return -1 } - user, err := a.um.FinishLogin(a.authClient, a.auth, mailboxPassword) + user, err := a.um.FinishLogin(a.authClient, a.auth, []byte(mailboxPassword)) if err != nil { log.WithError(err).Error("Login was unsuccessful") a.qml.SetAddAccountWarning("Failure: "+err.Error(), -2) diff --git a/internal/frontend/qt/accounts.go b/internal/frontend/qt/accounts.go index 0244f6ff..cb0162c0 100644 --- a/internal/frontend/qt/accounts.go +++ b/internal/frontend/qt/accounts.go @@ -152,7 +152,7 @@ func (s *FrontendQt) showLoginError(err error, scope string) bool { // 2: when has no 2FA but have MBOX func (s *FrontendQt) login(login, password string) int { var err error - s.authClient, s.auth, err = s.bridge.Login(login, password) + s.authClient, s.auth, err = s.bridge.Login(login, []byte(password)) if s.showLoginError(err, "login") { return -1 } @@ -195,7 +195,7 @@ func (s *FrontendQt) addAccount(mailboxPassword string) int { return -1 } - user, err := s.bridge.FinishLogin(s.authClient, s.auth, mailboxPassword) + user, err := s.bridge.FinishLogin(s.authClient, s.auth, []byte(mailboxPassword)) if err != nil { log.WithError(err).Error("Login was unsuccessful") s.Qml.SetAddAccountWarning("Failure: "+err.Error(), -2) diff --git a/internal/frontend/types/types.go b/internal/frontend/types/types.go index 131e8cfa..a3edcdb7 100644 --- a/internal/frontend/types/types.go +++ b/internal/frontend/types/types.go @@ -49,8 +49,8 @@ type Updater interface { // UserManager is an interface of users needed by frontend. type UserManager interface { - Login(username, password string) (pmapi.Client, *pmapi.Auth, error) - FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) + Login(username string, password []byte) (pmapi.Client, *pmapi.Auth, error) + FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) GetUsers() []User GetUser(query string) (User, error) DeleteUser(userID string, clearCache bool) error @@ -94,7 +94,7 @@ func NewBridgeWrap(bridge *bridge.Bridge) *bridgeWrap { //nolint[golint] return &bridgeWrap{Bridge: bridge} } -func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) { +func (b *bridgeWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) { return b.Bridge.FinishLogin(client, auth, mailboxPassword) } @@ -134,7 +134,7 @@ func NewImportExportWrap(ie *importexport.ImportExport) *importExportWrap { //no return &importExportWrap{ImportExport: ie} } -func (b *importExportWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword string) (User, error) { +func (b *importExportWrap) FinishLogin(client pmapi.Client, auth *pmapi.Auth, mailboxPassword []byte) (User, error) { return b.ImportExport.FinishLogin(client, auth, mailboxPassword) } diff --git a/internal/users/credentials/credentials.go b/internal/users/credentials/credentials.go index a9dfc784..32d9e482 100644 --- a/internal/users/credentials/credentials.go +++ b/internal/users/credentials/credentials.go @@ -47,8 +47,8 @@ type Credentials struct { UserID, // Do not marshal; used as a key. Name, Emails, - APIToken, - MailboxPassword, + APIToken string + MailboxPassword []byte BridgePassword, Version string Timestamp int64 @@ -58,15 +58,15 @@ type Credentials struct { func (s *Credentials) Marshal() string { items := []string{ - s.Name, // 0 - s.Emails, // 1 - s.APIToken, // 2 - s.MailboxPassword, // 3 - s.BridgePassword, // 4 - s.Version, // 5 - "", // 6 - "", // 7 - "", // 8 + s.Name, // 0 + s.Emails, // 1 + s.APIToken, // 2 + string(s.MailboxPassword), // 3 + s.BridgePassword, // 4 + s.Version, // 5 + "", // 6 + "", // 7 + "", // 8 } items[6] = fmt.Sprint(s.Timestamp) @@ -97,7 +97,7 @@ func (s *Credentials) Unmarshal(secret string) error { s.Name = items[0] s.Emails = items[1] s.APIToken = items[2] - s.MailboxPassword = items[3] + s.MailboxPassword = []byte(items[3]) switch len(items) { case itemLengthBridge: @@ -143,11 +143,11 @@ func (s *Credentials) CheckPassword(password string) error { func (s *Credentials) Logout() { s.APIToken = "" - s.MailboxPassword = "" + s.MailboxPassword = []byte{} } func (s *Credentials) IsConnected() bool { - return s.APIToken != "" && s.MailboxPassword != "" + return s.APIToken != "" && len(s.MailboxPassword) != 0 } func (s *Credentials) SplitAPIToken() (string, string, error) { diff --git a/internal/users/credentials/credentials_test.go b/internal/users/credentials/credentials_test.go index fefe68ee..5120fceb 100644 --- a/internal/users/credentials/credentials_test.go +++ b/internal/users/credentials/credentials_test.go @@ -32,7 +32,7 @@ var wantCredentials = Credentials{ Name: "name", Emails: "email1;email2", APIToken: "token", - MailboxPassword: "mailbox pass", + MailboxPassword: []byte("mailbox pass"), BridgePassword: "bridge pass", Version: "k11", Timestamp: time.Now().Unix(), @@ -52,7 +52,7 @@ func TestUnmarshallImportExport(t *testing.T) { wantCredentials.Name, wantCredentials.Emails, wantCredentials.APIToken, - wantCredentials.MailboxPassword, + string(wantCredentials.MailboxPassword), "k11", fmt.Sprint(wantCredentials.Timestamp), } diff --git a/internal/users/credentials/store.go b/internal/users/credentials/store.go index b1599f8d..b1610d7f 100644 --- a/internal/users/credentials/store.go +++ b/internal/users/credentials/store.go @@ -39,7 +39,7 @@ func NewStore(keychain *keychain.Keychain) *Store { return &Store{secrets: keychain} } -func (s *Store) Add(userID, userName, uid, ref, mailboxPassword string, emails []string) (*Credentials, error) { +func (s *Store) Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*Credentials, error) { storeLocker.Lock() defer storeLocker.Unlock() @@ -108,7 +108,7 @@ func (s *Store) UpdateEmails(userID string, emails []string) (*Credentials, erro return credentials, s.saveCredentials(credentials) } -func (s *Store) UpdatePassword(userID, password string) (*Credentials, error) { +func (s *Store) UpdatePassword(userID string, password []byte) (*Credentials, error) { storeLocker.Lock() defer storeLocker.Unlock() diff --git a/internal/users/credentials/store_test.go b/internal/users/credentials/store_test.go index bc94ccc1..658098fb 100644 --- a/internal/users/credentials/store_test.go +++ b/internal/users/credentials/store_test.go @@ -277,7 +277,7 @@ func TestMarshal(t *testing.T) { Name: "007", Emails: "ja@pm.me;aj@cus.tom", APIToken: "sdfdsfsdfsdfsdf", - MailboxPassword: "cdcdcdcd", + MailboxPassword: []byte("cdcdcdcd"), BridgePassword: "wew123", Version: "k11", Timestamp: 152469263742, diff --git a/internal/users/mocks/mocks.go b/internal/users/mocks/mocks.go index 3304b3af..8293f297 100644 --- a/internal/users/mocks/mocks.go +++ b/internal/users/mocks/mocks.go @@ -108,7 +108,7 @@ func (m *MockCredentialsStorer) EXPECT() *MockCredentialsStorerMockRecorder { } // Add mocks base method -func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3, arg4 string, arg5 []string) (*credentials.Credentials, error) { +func (m *MockCredentialsStorer) Add(arg0, arg1, arg2, arg3 string, arg4 []byte, arg5 []string) (*credentials.Credentials, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Add", arg0, arg1, arg2, arg3, arg4, arg5) ret0, _ := ret[0].(*credentials.Credentials) @@ -212,7 +212,7 @@ func (mr *MockCredentialsStorerMockRecorder) UpdateEmails(arg0, arg1 interface{} } // UpdatePassword mocks base method -func (m *MockCredentialsStorer) UpdatePassword(arg0, arg1 string) (*credentials.Credentials, error) { +func (m *MockCredentialsStorer) UpdatePassword(arg0 string, arg1 []byte) (*credentials.Credentials, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdatePassword", arg0, arg1) ret0, _ := ret[0].(*credentials.Credentials) diff --git a/internal/users/types.go b/internal/users/types.go index 9c09985f..19c5d422 100644 --- a/internal/users/types.go +++ b/internal/users/types.go @@ -32,11 +32,11 @@ type PanicHandler interface { type CredentialsStorer interface { List() (userIDs []string, err error) - Add(userID, userName, uid, ref, mailboxPassword string, emails []string) (*credentials.Credentials, error) + Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*credentials.Credentials, error) Get(userID string) (*credentials.Credentials, error) SwitchAddressMode(userID string) (*credentials.Credentials, error) UpdateEmails(userID string, emails []string) (*credentials.Credentials, error) - UpdatePassword(userID, password string) (*credentials.Credentials, error) + UpdatePassword(userID string, password []byte) (*credentials.Credentials, error) UpdateToken(userID, uid, ref string) (*credentials.Credentials, error) Logout(userID string) (*credentials.Credentials, error) Delete(userID string) error diff --git a/internal/users/user.go b/internal/users/user.go index 5641526e..ce5e7aba 100644 --- a/internal/users/user.go +++ b/internal/users/user.go @@ -227,7 +227,7 @@ func (u *User) unlockIfNecessary() error { // client. Unlock should only finish unlocking when connection is back up. // That means it should try it fast enough and not retry if connection // is still down. - err := u.client.Unlock(pmapi.ContextWithoutRetry(context.Background()), []byte(u.creds.MailboxPassword)) + err := u.client.Unlock(pmapi.ContextWithoutRetry(context.Background()), u.creds.MailboxPassword) if err == nil { return nil } @@ -364,7 +364,7 @@ func (u *User) UpdateUser(ctx context.Context) error { return err } - if err := u.client.ReloadKeys(ctx, []byte(u.creds.MailboxPassword)); err != nil { + if err := u.client.ReloadKeys(ctx, u.creds.MailboxPassword); err != nil { return errors.Wrap(err, "failed to reload keys") } diff --git a/internal/users/user_credentials_test.go b/internal/users/user_credentials_test.go index b3a13946..a38b2734 100644 --- a/internal/users/user_credentials_test.go +++ b/internal/users/user_credentials_test.go @@ -37,7 +37,7 @@ func TestUpdateUser(t *testing.T) { gomock.InOrder( m.pmapiClient.EXPECT().UpdateUser(gomock.Any()).Return(nil, nil), - m.pmapiClient.EXPECT().ReloadKeys(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(nil), + m.pmapiClient.EXPECT().ReloadKeys(gomock.Any(), testCredentials.MailboxPassword).Return(nil), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.credentialsStore.EXPECT().UpdateEmails("user", []string{testPMAPIAddress.Email}).Return(testCredentials, nil), diff --git a/internal/users/user_new_test.go b/internal/users/user_new_test.go index 50e42c8d..3b766180 100644 --- a/internal/users/user_new_test.go +++ b/internal/users/user_new_test.go @@ -46,7 +46,7 @@ func TestNewUserUnlockFails(t *testing.T) { m.credentialsStore.EXPECT().Get("user").Return(testCredentials, nil), m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()), m.pmapiClient.EXPECT().IsUnlocked().Return(false), - m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(errors.New("bad password")), + m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("bad password")), // Handle of unlock error. m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), diff --git a/internal/users/users.go b/internal/users/users.go index e819d53f..9e621d04 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -200,14 +200,14 @@ func (u *Users) closeAllConnections() { // Login authenticates a user by username/password, returning an authorised client and an auth object. // The authorisation scope may not yet be full if the user has 2FA enabled. -func (u *Users) Login(username, password string) (authClient pmapi.Client, auth *pmapi.Auth, err error) { +func (u *Users) Login(username string, password []byte) (authClient pmapi.Client, auth *pmapi.Auth, err error) { u.crashBandicoot(username) return u.clientManager.NewClientWithLogin(context.Background(), username, password) } // FinishLogin finishes the login procedure and adds the user into the credentials store. -func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password string) (user *User, err error) { //nolint[funlen] +func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password []byte) (user *User, err error) { //nolint[funlen] apiUser, passphrase, err := getAPIUser(context.Background(), client, password) if err != nil { return nil, err @@ -228,7 +228,7 @@ func (u *Users) FinishLogin(client pmapi.Client, auth *pmapi.Auth, password stri } // Update the password in case the user changed it. - creds, err := u.credStorer.UpdatePassword(apiUser.ID, string(passphrase)) + creds, err := u.credStorer.UpdatePassword(apiUser.ID, passphrase) if err != nil { return nil, errors.Wrap(err, "failed to update password of user in credentials store") } @@ -264,7 +264,7 @@ func (u *Users) addNewUser(client pmapi.Client, apiUser *pmapi.User, auth *pmapi emails = client.Addresses().AllEmails() } - if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, string(passphrase), emails); err != nil { + if _, err := u.credStorer.Add(apiUser.ID, apiUser.Name, auth.UID, auth.RefreshToken, passphrase, emails); err != nil { return errors.Wrap(err, "failed to add user credentials to credentials store") } @@ -286,7 +286,7 @@ func (u *Users) addNewUser(client pmapi.Client, apiUser *pmapi.User, auth *pmapi return nil } -func getAPIUser(ctx context.Context, client pmapi.Client, password string) (*pmapi.User, []byte, error) { +func getAPIUser(ctx context.Context, client pmapi.Client, password []byte) (*pmapi.User, []byte, error) { salt, err := client.AuthSalt(ctx) if err != nil { return nil, nil, errors.Wrap(err, "failed to get salt") diff --git a/internal/users/users_login_test.go b/internal/users/users_login_test.go index 9fc6015c..8c24a9c6 100644 --- a/internal/users/users_login_test.go +++ b/internal/users/users_login_test.go @@ -37,7 +37,7 @@ func TestUsersFinishLoginBadMailboxPassword(t *testing.T) { // Set up mocks for FinishLogin. m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil) - m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(errors.New("no keys could be unlocked")) + m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("no keys could be unlocked")) checkUsersFinishLogin(t, m, testAuthRefresh, testCredentials.MailboxPassword, "", ErrWrongMailboxPassword) } @@ -69,7 +69,7 @@ func TestUsersFinishLoginExistingDisconnectedUser(t *testing.T) { // Mock process of FinishLogin of already added user. gomock.InOrder( m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil), - m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(nil), + m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil), m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUserDisconnected, nil), m.credentialsStore.EXPECT().UpdateToken(testCredentialsDisconnected.UserID, testAuthRefresh.UID, testAuthRefresh.RefreshToken).Return(testCredentials, nil), m.credentialsStore.EXPECT().UpdatePassword(testCredentialsDisconnected.UserID, testCredentials.MailboxPassword).Return(testCredentials, nil), @@ -101,7 +101,7 @@ func TestUsersFinishLoginConnectedUser(t *testing.T) { // Mock process of FinishLogin of already connected user. gomock.InOrder( m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil), - m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(nil), + m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil), m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUser, nil), m.pmapiClient.EXPECT().AuthDelete(gomock.Any()).Return(nil), ) @@ -113,7 +113,7 @@ func TestUsersFinishLoginConnectedUser(t *testing.T) { r.EqualError(t, err, "user is already connected") } -func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword string, expectedUserID string, expectedErr error) { +func checkUsersFinishLogin(t *testing.T, m mocks, auth *pmapi.Auth, mailboxPassword []byte, expectedUserID string, expectedErr error) { users := testNewUsers(t, m) defer cleanUpUsersData(users) diff --git a/internal/users/users_new_test.go b/internal/users/users_new_test.go index ae3e4ff7..00a7b886 100644 --- a/internal/users/users_new_test.go +++ b/internal/users/users_new_test.go @@ -84,7 +84,7 @@ func TestNewUsersWithConnectedUserWithBadToken(t *testing.T) { m.clientManager.EXPECT().NewClient("uid", "", "acc", time.Time{}).Return(m.pmapiClient) m.pmapiClient.EXPECT().AddAuthRefreshHandler(gomock.Any()) m.pmapiClient.EXPECT().IsUnlocked().Return(false) - m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(errors.New("not authorized")) + m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(errors.New("not authorized")) m.pmapiClient.EXPECT().AuthDelete(gomock.Any()) m.credentialsStore.EXPECT().List().Return([]string{"user"}, nil) diff --git a/internal/users/users_test.go b/internal/users/users_test.go index 08971d95..962c8f7b 100644 --- a/internal/users/users_test.go +++ b/internal/users/users_test.go @@ -63,7 +63,7 @@ var ( Name: "username", Emails: "user@pm.me", APIToken: "uid:acc", - MailboxPassword: "pass", + MailboxPassword: []byte("pass"), BridgePassword: "0123456789abcdef", Version: "v1", Timestamp: 123456789, @@ -76,7 +76,7 @@ var ( Name: "usersname", Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me", APIToken: "uid:acc", - MailboxPassword: "pass", + MailboxPassword: []byte("pass"), BridgePassword: "0123456789abcdef", Version: "v1", Timestamp: 123456789, @@ -89,7 +89,7 @@ var ( Name: "username", Emails: "user@pm.me", APIToken: "", - MailboxPassword: "", + MailboxPassword: []byte{}, BridgePassword: "0123456789abcdef", Version: "v1", Timestamp: 123456789, @@ -102,7 +102,7 @@ var ( Name: "usersname", Emails: "users@pm.me;anotheruser@pm.me;alsouser@pm.me", APIToken: "", - MailboxPassword: "", + MailboxPassword: []byte{}, BridgePassword: "0123456789abcdef", Version: "v1", Timestamp: 123456789, @@ -249,7 +249,7 @@ func mockAddingConnectedUser(m mocks) { gomock.InOrder( // Mock of users.FinishLogin. m.pmapiClient.EXPECT().AuthSalt(gomock.Any()).Return("", nil), - m.pmapiClient.EXPECT().Unlock(gomock.Any(), []byte(testCredentials.MailboxPassword)).Return(nil), + m.pmapiClient.EXPECT().Unlock(gomock.Any(), testCredentials.MailboxPassword).Return(nil), m.pmapiClient.EXPECT().CurrentUser(gomock.Any()).Return(testPMAPIUser, nil), m.pmapiClient.EXPECT().Addresses().Return([]*pmapi.Address{testPMAPIAddress}), m.credentialsStore.EXPECT().Add("user", "username", testAuthRefresh.UID, testAuthRefresh.RefreshToken, testCredentials.MailboxPassword, []string{testPMAPIAddress.Email}).Return(testCredentials, nil), diff --git a/pkg/pmapi/attachments_test.go b/pkg/pmapi/attachments_test.go index 0791d3a4..6a78fd7b 100644 --- a/pkg/pmapi/attachments_test.go +++ b/pkg/pmapi/attachments_test.go @@ -33,16 +33,24 @@ import ( pmmime "github.com/ProtonMail/proton-bridge/pkg/mime" - a "github.com/stretchr/testify/assert" - r "github.com/stretchr/testify/require" + "github.com/stretchr/testify/require" ) +const testAttachmentCleartext = `cc, +dille. +` + +// Attachment cleartext encrypted with testPrivateKeyRing. +const testKeyPacket = `wcBMA0fcZ7XLgmf2AQf/cHhfDRM9zlIuBi+h2W6DKjbbyIHMkgF6ER3JEvn/tSruUH8KTGt0N7Z+a80FFMCuXn1Y1I/nW7MVrNhGuJZAF4OymD8ugvuoAMIQX0eCYEpPXzRIWJBZg82AuowmFMsv8Dgvq4bTZq4cttI3CZcxKUNXuAearmNpmgplUKWj5USmRXK4iGB3VFGjidXkxbElrP4fD5A/rfEZ5aJgCsegqcXxX3MEjWXi9pFzgd/9phOvl1ZFm9U9hNoVAW3QsgmVeihnKaDZUyf2Qsigij21QKAUxw9U3y89eTUIqZAcmIgqeDujA3RWBgJwjtY/lOyhEmkf3AWKzehvf1xtJmCWDg==` +const testDataPacket = `0ksB6S4f4l8C1NB8yzmd/jNi0xqEZsyTDLdTP+N4Qxh3NZjla+yGRvC9rGmoUL7XVyowsG/GKTf2LXF/5E5FkX/3WMYwIv1n11ExyAE=` + var testAttachment = &Attachment{ ID: "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw==", Name: "croutonmail.txt", Size: 77, MIMEType: "text/plain", - KeyPackets: "wcBMA0fcZ7XLgmf2AQgAiRsOlnm1kSB4/lr7tYe6pBsRGn10GqwUhrwU5PMKOHdCgnO12jO3y3CzP0Yl/jGhAYja9wLDqH8X0sk3tY32u4Sb1Qe5IuzggAiCa4dwOJj5gEFMTHMzjIMPHR7A70XqUxMhmILye8V4KRm/j4c1sxbzA1rM3lYBumQuB5l/ck0Kgt4ZqxHVXHK5Q1l65FHhSXRj8qnunasHa30TYNzP8nmBA8BinnJxpiQ7FGc2umnUhgkFtjm5ixu9vyjr9ukwDTbwAXXfmY+o7tK7kqIXJcmTL6k2UeC6Mz1AagQtRCRtU+bv/3zGojq/trZo9lom3naIeQYa36Ketmcpj2Qwjg==", + KeyPackets: testKeyPacket, + Header: textproto.MIMEHeader{ "Content-Description": {"You'll never believe what's in this text file"}, "X-Mailer": {"Microsoft Outlook 15.0", "Microsoft Live Mail 42.0"}, @@ -50,12 +58,13 @@ var testAttachment = &Attachment{ MessageID: "h3CD-DT7rLoAw1vmpcajvIPAl-wwDfXR2MHtWID3wuQURDBKTiGUAwd6E2WBbS44QQKeXImW-axm6X0hAfcVCA==", } +// Part of GET /mail/messages/{id} response from server. const testAttachmentJSON = `{ "ID": "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw==", "Name": "croutonmail.txt", "Size": 77, "MIMEType": "text/plain", - "KeyPackets": "wcBMA0fcZ7XLgmf2AQgAiRsOlnm1kSB4/lr7tYe6pBsRGn10GqwUhrwU5PMKOHdCgnO12jO3y3CzP0Yl/jGhAYja9wLDqH8X0sk3tY32u4Sb1Qe5IuzggAiCa4dwOJj5gEFMTHMzjIMPHR7A70XqUxMhmILye8V4KRm/j4c1sxbzA1rM3lYBumQuB5l/ck0Kgt4ZqxHVXHK5Q1l65FHhSXRj8qnunasHa30TYNzP8nmBA8BinnJxpiQ7FGc2umnUhgkFtjm5ixu9vyjr9ukwDTbwAXXfmY+o7tK7kqIXJcmTL6k2UeC6Mz1AagQtRCRtU+bv/3zGojq/trZo9lom3naIeQYa36Ketmcpj2Qwjg==", + "KeyPackets": "` + testKeyPacket + `", "Headers": { "content-description": "You'll never believe what's in this text file", "x-mailer": [ @@ -66,68 +75,66 @@ const testAttachmentJSON = `{ } ` -const testAttachmentCleartext = `cc, -dille. -` - -const testAttachmentEncrypted = `wcBMA0fcZ7XLgmf2AQf/cHhfDRM9zlIuBi+h2W6DKjbbyIHMkgF6ER3JEvn/tSruUH8KTGt0N7Z+a80FFMCuXn1Y1I/nW7MVrNhGuJZAF4OymD8ugvuoAMIQX0eCYEpPXzRIWJBZg82AuowmFMsv8Dgvq4bTZq4cttI3CZcxKUNXuAearmNpmgplUKWj5USmRXK4iGB3VFGjidXkxbElrP4fD5A/rfEZ5aJgCsegqcXxX3MEjWXi9pFzgd/9phOvl1ZFm9U9hNoVAW3QsgmVeihnKaDZUyf2Qsigij21QKAUxw9U3y89eTUIqZAcmIgqeDujA3RWBgJwjtY/lOyhEmkf3AWKzehvf1xtJmCWDtJLAekuH+JfAtTQfMs5nf4zYtMahGbMkwy3Uz/jeEMYdzWY5WvshkbwvaxpqFC+11cqMLBvxik39i1xf+RORZF/91jGMCL9Z9dRMcgB` - -const testCreateAttachmentBody = `{ +// POST /mail/attachment/ response from server. +const testCreatedAttachmentBody = `{ "Code": 1000, "Attachment": {"ID": "y6uKIlc2HdoHPAwPSrvf7dXoZNMYvBgxshYUN67cY5DJjL2O8NYewuvGHcYvCfd8LpEoAI_GdymO0Jr0mHlsEw=="} }` func TestAttachment_UnmarshalJSON(t *testing.T) { + r := require.New(t) att := new(Attachment) err := json.Unmarshal([]byte(testAttachmentJSON), att) - r.NoError(t, err) + r.NoError(err) - att.MessageID = testAttachment.MessageID // This isn't in the JSON object + att.MessageID = testAttachment.MessageID // This isn't in the server response - r.Equal(t, testAttachment, att) + r.Equal(testAttachment, att) } func TestClient_CreateAttachment(t *testing.T) { + r := require.New(t) s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "POST", "/mail/v4/attachments")) + r.NoError(checkMethodAndPath(req, "POST", "/mail/v4/attachments")) contentType, params, err := pmmime.ParseMediaType(req.Header.Get("Content-Type")) - r.NoError(t, err) - r.Equal(t, "multipart/form-data", contentType) + r.NoError(err) + r.Equal("multipart/form-data", contentType) mr := multipart.NewReader(req.Body, params["boundary"]) form, err := mr.ReadForm(10 * 1024) - r.NoError(t, err) - defer r.NoError(t, form.RemoveAll()) + r.NoError(err) + defer r.NoError(form.RemoveAll()) - r.Equal(t, testAttachment.Name, form.Value["Filename"][0]) - r.Equal(t, testAttachment.MessageID, form.Value["MessageID"][0]) - r.Equal(t, testAttachment.MIMEType, form.Value["MIMEType"][0]) + r.Equal(testAttachment.Name, form.Value["Filename"][0]) + r.Equal(testAttachment.MessageID, form.Value["MessageID"][0]) + r.Equal(testAttachment.MIMEType, form.Value["MIMEType"][0]) dataFile, err := form.File["DataPacket"][0].Open() - r.NoError(t, err) - defer r.NoError(t, dataFile.Close()) + r.NoError(err) + defer r.NoError(dataFile.Close()) b, err := ioutil.ReadAll(dataFile) - r.NoError(t, err) - r.Equal(t, testAttachmentCleartext, string(b)) + r.NoError(err) + r.Equal(testAttachmentCleartext, string(b)) w.Header().Set("Content-Type", "application/json") - fmt.Fprint(w, testCreateAttachmentBody) + fmt.Fprint(w, testCreatedAttachmentBody) })) defer s.Close() reader := strings.NewReader(testAttachmentCleartext) // In reality, this thing is encrypted created, err := c.CreateAttachment(context.Background(), testAttachment, reader, strings.NewReader("")) - r.NoError(t, err) + r.NoError(err) - r.Equal(t, testAttachment.ID, created.ID) + r.Equal(testAttachment.ID, created.ID) } func TestClient_GetAttachment(t *testing.T) { + r := require.New(t) s, c := newTestClient(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - r.NoError(t, checkMethodAndPath(req, "GET", "/mail/v4/attachments/"+testAttachment.ID)) + r.NoError(checkMethodAndPath(req, "GET", "/mail/v4/attachments/"+testAttachment.ID)) w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, testAttachmentCleartext) @@ -135,39 +142,61 @@ func TestClient_GetAttachment(t *testing.T) { defer s.Close() att, err := c.GetAttachment(context.Background(), testAttachment.ID) - r.NoError(t, err) + r.NoError(err) defer att.Close() //nolint[errcheck] // In reality, r contains encrypted data b, err := ioutil.ReadAll(att) - r.NoError(t, err) + r.NoError(err) - r.Equal(t, testAttachmentCleartext, string(b)) + r.Equal(testAttachmentCleartext, string(b)) } -func TestAttachment_Encrypt(t *testing.T) { - data := bytes.NewBufferString(testAttachmentCleartext) - r, err := testAttachment.Encrypt(testPublicKeyRing, data) - a.Nil(t, err) - b, err := ioutil.ReadAll(r) - a.Nil(t, err) +func TestAttachmentDecrypt(t *testing.T) { + r := require.New(t) - // Result is always different, so the best way is to test it by decrypting again. - // Another test for decrypting will help us to be sure it's working. - dataEnc := bytes.NewBuffer(b) - decryptAndCheck(t, dataEnc) + rawKeyPacket, err := base64.StdEncoding.DecodeString(testKeyPacket) + r.NoError(err) + + rawDataPacket, err := base64.StdEncoding.DecodeString(testDataPacket) + r.NoError(err) + + decryptAndCheck(r, bytes.NewBuffer(append(rawKeyPacket, rawDataPacket...))) } -func TestAttachment_Decrypt(t *testing.T) { - dataBytes, _ := base64.StdEncoding.DecodeString(testAttachmentEncrypted) - dataReader := bytes.NewBuffer(dataBytes) - decryptAndCheck(t, dataReader) +func TestAttachmentEncrypt(t *testing.T) { + r := require.New(t) + + encryptedReader, err := testAttachment.Encrypt( + testPublicKeyRing, + bytes.NewBufferString(testAttachmentCleartext), + ) + r.NoError(err) + + // The result is always different due to session key. The best way is to + // test result of encryption by decrypting again acn coparet to cleartext. + decryptAndCheck(r, encryptedReader) } -func decryptAndCheck(t *testing.T, data io.Reader) { - r, err := testAttachment.Decrypt(data, testPrivateKeyRing) - a.Nil(t, err) - b, err := ioutil.ReadAll(r) - a.Nil(t, err) - a.Equal(t, testAttachmentCleartext, string(b)) +func decryptAndCheck(r *require.Assertions, data io.Reader) { + // First separate KeyPacket from encrypted data. In our case keypacket + // has 271 bytes. + raw, err := ioutil.ReadAll(data) + r.NoError(err) + rawKeyPacket := raw[:271] + rawDataPacket := raw[271:] + + // KeyPacket is retrieve by get GET /mail/messages/{id} + haveAttachment := &Attachment{ + KeyPackets: base64.StdEncoding.EncodeToString(rawKeyPacket), + } + + // DataPacket is received from GET /mail/attachments/{id} + decryptedReader, err := haveAttachment.Decrypt(bytes.NewBuffer(rawDataPacket), testPrivateKeyRing) + r.NoError(err) + + b, err := ioutil.ReadAll(decryptedReader) + r.NoError(err) + + r.Equal(testAttachmentCleartext, string(b)) } diff --git a/pkg/pmapi/keyring_test.go b/pkg/pmapi/keyring_test.go index 93fe8ccd..cd01c804 100644 --- a/pkg/pmapi/keyring_test.go +++ b/pkg/pmapi/keyring_test.go @@ -22,7 +22,7 @@ import ( "testing" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func loadPMKeys(jsonKeys string) (keys *PMKeys) { @@ -31,6 +31,7 @@ func loadPMKeys(jsonKeys string) (keys *PMKeys) { } func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) { + r := require.New(t) addrKeysWithTokens := loadPMKeys(readTestFile("keyring_addressKeysWithTokens_JSON", false)) addrKeysWithoutTokens := loadPMKeys(readTestFile("keyring_addressKeysWithoutTokens_JSON", false)) addrKeysPrimaryHasToken := loadPMKeys(readTestFile("keyring_addressKeysPrimaryHasToken_JSON", false)) @@ -42,7 +43,7 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) { } userKey, err := crypto.NewKeyRing(key) - assert.NoError(t, err, "Expected not to receive an error unlocking user key") + r.NoError(err, "Expected not to receive an error unlocking user key") type args struct { userKeyring *crypto.KeyRing @@ -77,9 +78,7 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { kr, err := tt.keys.UnlockAll(tt.args.passphrase, tt.args.userKeyring) // nolint[scopelint] - if !assert.NoError(t, err) { - return - } + r.NoError(err) // assert at least one key has been decrypted atLeastOneDecrypted := false @@ -96,7 +95,21 @@ func TestPMKeys_GetKeyRingAndUnlock(t *testing.T) { } } - assert.True(t, atLeastOneDecrypted) + r.True(atLeastOneDecrypted) }) } } + +func TestGopenpgpEncryptAttachment(t *testing.T) { + r := require.New(t) + + wantMessage := crypto.NewPlainMessage([]byte(testAttachmentCleartext)) + + pgpSplitMessage, err := testPublicKeyRing.EncryptAttachment(wantMessage, "") + r.NoError(err) + + haveMessage, err := testPrivateKeyRing.DecryptAttachment(pgpSplitMessage) + r.NoError(err) + + r.Equal(wantMessage.Data, haveMessage.Data) +} diff --git a/pkg/pmapi/manager_auth.go b/pkg/pmapi/manager_auth.go index 7f47e34f..d99d8289 100644 --- a/pkg/pmapi/manager_auth.go +++ b/pkg/pmapi/manager_auth.go @@ -22,7 +22,7 @@ import ( "encoding/base64" "time" - "github.com/ProtonMail/proton-bridge/pkg/srp" + "github.com/ProtonMail/go-srp" ) func (m *manager) NewClient(uid, acc, ref string, exp time.Time) Client { @@ -44,7 +44,7 @@ func (m *manager) NewClientWithRefresh(ctx context.Context, uid, ref string) (Cl return c.withAuth(auth.AccessToken, auth.RefreshToken, expiresIn(auth.ExpiresIn)), auth, nil } -func (m *manager) NewClientWithLogin(ctx context.Context, username, password string) (Client, *Auth, error) { +func (m *manager) NewClientWithLogin(ctx context.Context, username string, password []byte) (Client, *Auth, error) { log.Trace("New client with login") info, err := m.getAuthInfo(ctx, GetAuthInfoReq{Username: username}) @@ -52,12 +52,12 @@ func (m *manager) NewClientWithLogin(ctx context.Context, username, password str return nil, nil, err } - srpAuth, err := srp.NewSrpAuth(info.Version, username, password, info.Salt, info.Modulus, info.ServerEphemeral) + srpAuth, err := srp.NewAuth(info.Version, username, password, info.Salt, info.Modulus, info.ServerEphemeral) if err != nil { return nil, nil, err } - proofs, err := srpAuth.GenerateSrpProofs(2048) + proofs, err := srpAuth.GenerateProofs(2048) if err != nil { return nil, nil, err } diff --git a/pkg/pmapi/manager_types.go b/pkg/pmapi/manager_types.go index 0a9e5f0d..1902be8e 100644 --- a/pkg/pmapi/manager_types.go +++ b/pkg/pmapi/manager_types.go @@ -29,7 +29,7 @@ import ( type Manager interface { NewClient(string, string, string, time.Time) Client NewClientWithRefresh(context.Context, string, string) (Client, *AuthRefresh, error) - NewClientWithLogin(context.Context, string, string) (Client, *Auth, error) + NewClientWithLogin(context.Context, string, []byte) (Client, *Auth, error) DownloadAndVerify(kr *crypto.KeyRing, url, sig string) ([]byte, error) ReportBug(context.Context, ReportBugReq) error diff --git a/pkg/pmapi/mocks/mocks.go b/pkg/pmapi/mocks/mocks.go index ac6cdaaf..78492681 100644 --- a/pkg/pmapi/mocks/mocks.go +++ b/pkg/pmapi/mocks/mocks.go @@ -670,7 +670,7 @@ func (mr *MockManagerMockRecorder) NewClient(arg0, arg1, arg2, arg3 interface{}) } // NewClientWithLogin mocks base method -func (m *MockManager) NewClientWithLogin(arg0 context.Context, arg1, arg2 string) (pmapi.Client, *pmapi.Auth, error) { +func (m *MockManager) NewClientWithLogin(arg0 context.Context, arg1 string, arg2 []byte) (pmapi.Client, *pmapi.Auth, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NewClientWithLogin", arg0, arg1, arg2) ret0, _ := ret[0].(pmapi.Client) diff --git a/pkg/pmapi/passwords.go b/pkg/pmapi/passwords.go index f4fcc708..2547b391 100644 --- a/pkg/pmapi/passwords.go +++ b/pkg/pmapi/passwords.go @@ -20,13 +20,14 @@ package pmapi import ( "encoding/base64" - "github.com/jameskeane/bcrypt" + "github.com/ProtonMail/go-srp" "github.com/pkg/errors" ) -func HashMailboxPassword(password, salt string) ([]byte, error) { +// HashMailboxPassword expectects 128bit long salt encoded by standard base64. +func HashMailboxPassword(password []byte, salt string) ([]byte, error) { if salt == "" { - return []byte(password), nil + return password, nil } decodedSalt, err := base64.StdEncoding.DecodeString(salt) @@ -34,15 +35,10 @@ func HashMailboxPassword(password, salt string) ([]byte, error) { return nil, errors.Wrap(err, "failed to decode salt") } - encodedSalt := base64.NewEncoding("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").WithPadding(base64.NoPadding).EncodeToString(decodedSalt) - hashResult, err := bcrypt.Hash(password, "$2y$10$"+encodedSalt) + hash, err := srp.MailboxPassword(password, decodedSalt) if err != nil { - return nil, errors.Wrap(err, "failed to bcrypt-hash password") + return nil, errors.Wrap(err, "failed to hash password") } - if len(hashResult) != 60 { - return nil, errors.New("pmapi: invalid mailbox password hash") - } - - return []byte(hashResult[len(hashResult)-31:]), nil + return hash[len(hash)-31:], nil } diff --git a/pkg/pmapi/passwords_test.go b/pkg/pmapi/passwords_test.go new file mode 100644 index 00000000..39e139b5 --- /dev/null +++ b/pkg/pmapi/passwords_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2021 Proton Technologies AG +// +// This file is part of ProtonMail Bridge. +// +// ProtonMail Bridge is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ProtonMail Bridge is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with ProtonMail Bridge. If not, see . + +package pmapi + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMailboxPassword(t *testing.T) { + // wantHash was generated with passprase and salt defined below. It + // should not change when changing implementation of the function. + wantHash := []byte("B5nwpsJQSTJ16ldr64Vdq6oeCCn32Fi") + + // Valid salt is 128bit long (16bytes) + // $echo aaaabbbbccccdddd | base64 + salt := "YWFhYWJiYmJjY2NjZGRkZAo=" + + passphrase := []byte("random") + + r := require.New(t) + _, err := HashMailboxPassword(passphrase, "badsalt") + r.Error(err) + + haveHash, err := HashMailboxPassword(passphrase, salt) + r.NoError(err) + r.Equal(wantHash, haveHash) +} diff --git a/pkg/srp/hash.go b/pkg/srp/hash.go deleted file mode 100644 index 296a9a05..00000000 --- a/pkg/srp/hash.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// -// This file is part of ProtonMail Bridge. -// -// ProtonMail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// ProtonMail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with ProtonMail Bridge. If not, see . - -package srp - -import ( - "bytes" - "crypto/md5" //nolint[gosec] - "crypto/sha512" - "encoding/base64" - "encoding/hex" - "errors" - "strings" - - "github.com/jameskeane/bcrypt" -) - -// BCryptHash function bcrypt algorithm to hash password with salt. -func BCryptHash(password string, salt string) (string, error) { - return bcrypt.Hash(password, salt) -} - -// ExpandHash extends the byte data for SRP flow. -func ExpandHash(data []byte) []byte { - part0 := sha512.Sum512(append(data, 0)) - part1 := sha512.Sum512(append(data, 1)) - part2 := sha512.Sum512(append(data, 2)) - part3 := sha512.Sum512(append(data, 3)) - return bytes.Join([][]byte{ - part0[:], - part1[:], - part2[:], - part3[:], - }, []byte{}) -} - -// HashPassword returns the hash of password argument. Based on version number -// following arguments are used in addition to password: -// * 0, 1, 2: userName and modulus -// * 3, 4: salt and modulus. -func HashPassword(authVersion int, password, userName string, salt, modulus []byte) ([]byte, error) { - switch authVersion { - case 4, 3: - return hashPasswordVersion3(password, salt, modulus) - case 2: - return hashPasswordVersion2(password, userName, modulus) - case 1: - return hashPasswordVersion1(password, userName, modulus) - case 0: - return hashPasswordVersion0(password, userName, modulus) - default: - return nil, errors.New("pmapi: unsupported auth version") - } -} - -// CleanUserName returns the input string in lower-case without characters `_`, -// `.` and `-`. -func CleanUserName(userName string) string { - userName = strings.ReplaceAll(userName, "-", "") - userName = strings.ReplaceAll(userName, ".", "") - userName = strings.ReplaceAll(userName, "_", "") - return strings.ToLower(userName) -} - -func hashPasswordVersion3(password string, salt, modulus []byte) (res []byte, err error) { - encodedSalt := base64.NewEncoding("./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").WithPadding(base64.NoPadding).EncodeToString(append(salt, []byte("proton")...)) - crypted, err := BCryptHash(password, "$2y$10$"+encodedSalt) - if err != nil { - return - } - - return ExpandHash(append([]byte(crypted), modulus...)), nil -} - -func hashPasswordVersion2(password, userName string, modulus []byte) (res []byte, err error) { - return hashPasswordVersion1(password, CleanUserName(userName), modulus) -} - -func hashPasswordVersion1(password, userName string, modulus []byte) (res []byte, err error) { - prehashed := md5.Sum([]byte(strings.ToLower(userName))) //nolint[gosec] - encodedSalt := hex.EncodeToString(prehashed[:]) - crypted, err := BCryptHash(password, "$2y$10$"+encodedSalt) - if err != nil { - return - } - - return ExpandHash(append([]byte(crypted), modulus...)), nil -} - -func hashPasswordVersion0(password, userName string, modulus []byte) (res []byte, err error) { - prehashed := sha512.Sum512([]byte(password)) - return hashPasswordVersion1(base64.StdEncoding.EncodeToString(prehashed[:]), userName, modulus) -} diff --git a/pkg/srp/srp.go b/pkg/srp/srp.go deleted file mode 100644 index 70e2ea39..00000000 --- a/pkg/srp/srp.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// -// This file is part of ProtonMail Bridge. -// -// ProtonMail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// ProtonMail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with ProtonMail Bridge. If not, see . - -package srp - -import ( - "bytes" - "crypto/rand" - "encoding/base64" - "errors" - "math/big" - - "golang.org/x/crypto/openpgp" - "golang.org/x/crypto/openpgp/clearsign" -) - -//nolint[gochecknoglobals] -var ( - ErrDataAfterModulus = errors.New("pm-srp: extra data after modulus") - ErrInvalidSignature = errors.New("pm-srp: invalid modulus signature") - RandReader = rand.Reader -) - -// Store random reader in a variable to be able to overwrite it in tests - -// Amored pubkey for modulus verification. -const modulusPubkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- - -xjMEXAHLgxYJKwYBBAHaRw8BAQdAFurWXXwjTemqjD7CXjXVyKf0of7n9Ctm -L8v9enkzggHNEnByb3RvbkBzcnAubW9kdWx1c8J3BBAWCgApBQJcAcuDBgsJ -BwgDAgkQNQWFxOlRjyYEFQgKAgMWAgECGQECGwMCHgEAAPGRAP9sauJsW12U -MnTQUZpsbJb53d0Wv55mZIIiJL2XulpWPQD/V6NglBd96lZKBmInSXX/kXat -Sv+y0io+LR8i2+jV+AbOOARcAcuDEgorBgEEAZdVAQUBAQdAeJHUz1c9+KfE -kSIgcBRE3WuXC4oj5a2/U3oASExGDW4DAQgHwmEEGBYIABMFAlwBy4MJEDUF -hcTpUY8mAhsMAAD/XQD8DxNI6E78meodQI+wLsrKLeHn32iLvUqJbVDhfWSU -WO4BAMcm1u02t4VKw++ttECPt+HUgPUq5pqQWe5Q2cW4TMsE -=Y4Mw ------END PGP PUBLIC KEY BLOCK-----` - -// ReadClearSignedMessage reads the clear text from signed message and verifies -// signature. There must be no data appended after signed message in input string. -// The message must be sign by key corresponding to `modulusPubkey`. -func ReadClearSignedMessage(signedMessage string) (string, error) { - modulusBlock, rest := clearsign.Decode([]byte(signedMessage)) - if len(rest) != 0 { - return "", ErrDataAfterModulus - } - - modulusKeyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(modulusPubkey))) - if err != nil { - return "", errors.New("pm-srp: can not read modulus pubkey") - } - - _, err = openpgp.CheckDetachedSignature(modulusKeyring, bytes.NewReader(modulusBlock.Bytes), modulusBlock.ArmoredSignature.Body, nil) - if err != nil { - return "", ErrInvalidSignature - } - - return string(modulusBlock.Bytes), nil -} - -// SrpProofs object. -type SrpProofs struct { //nolint[golint] - ClientProof, ClientEphemeral, ExpectedServerProof []byte -} - -// SrpAuth stores byte data for the calculation of SRP proofs. -type SrpAuth struct { //nolint[golint] - Modulus, ServerEphemeral, HashedPassword []byte -} - -// NewSrpAuth creates new SrpAuth from strings input. Salt and server ephemeral are in -// base64 format. Modulus is base64 with signature attached. The signature is -// verified against server key. The version controls password hash algorithm. -func NewSrpAuth(version int, username, password, salt, signedModulus, serverEphemeral string) (auth *SrpAuth, err error) { - data := &SrpAuth{} - - // Modulus - var modulus string - modulus, err = ReadClearSignedMessage(signedModulus) - if err != nil { - return - } - data.Modulus, err = base64.StdEncoding.DecodeString(modulus) - if err != nil { - return - } - - // Password - var decodedSalt []byte - if version >= 3 { - decodedSalt, err = base64.StdEncoding.DecodeString(salt) - if err != nil { - return - } - } - data.HashedPassword, err = HashPassword(version, password, username, decodedSalt, data.Modulus) - if err != nil { - return - } - - // Server ephermeral - data.ServerEphemeral, err = base64.StdEncoding.DecodeString(serverEphemeral) - if err != nil { - return - } - - return data, nil -} - -// GenerateSrpProofs calculates SPR proofs. -func (s *SrpAuth) GenerateSrpProofs(length int) (res *SrpProofs, err error) { //nolint[funlen] - toInt := func(arr []byte) *big.Int { - var reversed = make([]byte, len(arr)) - for i := 0; i < len(arr); i++ { - reversed[len(arr)-i-1] = arr[i] - } - return big.NewInt(0).SetBytes(reversed) - } - - fromInt := func(num *big.Int) []byte { - var arr = num.Bytes() - var reversed = make([]byte, length/8) - for i := 0; i < len(arr); i++ { - reversed[len(arr)-i-1] = arr[i] - } - return reversed - } - - generator := big.NewInt(2) - multiplier := toInt(ExpandHash(append(fromInt(generator), s.Modulus...))) - - modulus := toInt(s.Modulus) - serverEphemeral := toInt(s.ServerEphemeral) - hashedPassword := toInt(s.HashedPassword) - - modulusMinusOne := big.NewInt(0).Sub(modulus, big.NewInt(1)) - - if modulus.BitLen() != length { - return nil, errors.New("pm-srp: SRP modulus has incorrect size") - } - - multiplier = multiplier.Mod(multiplier, modulus) - - if multiplier.Cmp(big.NewInt(1)) <= 0 || multiplier.Cmp(modulusMinusOne) >= 0 { - return nil, errors.New("pm-srp: SRP multiplier is out of bounds") - } - - if generator.Cmp(big.NewInt(1)) <= 0 || generator.Cmp(modulusMinusOne) >= 0 { - return nil, errors.New("pm-srp: SRP generator is out of bounds") - } - - if serverEphemeral.Cmp(big.NewInt(1)) <= 0 || serverEphemeral.Cmp(modulusMinusOne) >= 0 { - return nil, errors.New("pm-srp: SRP server ephemeral is out of bounds") - } - - // Check primality - // Doing exponentiation here is faster than a full call to ProbablyPrime while - // still perfectly accurate by Pocklington's theorem - if big.NewInt(0).Exp(big.NewInt(2), modulusMinusOne, modulus).Cmp(big.NewInt(1)) != 0 { - return nil, errors.New("pm-srp: SRP modulus is not prime") - } - - // Check safe primality - if !big.NewInt(0).Rsh(modulus, 1).ProbablyPrime(10) { - return nil, errors.New("pm-srp: SRP modulus is not a safe prime") - } - - var clientSecret, clientEphemeral, scramblingParam *big.Int - for { - for { - clientSecret, err = rand.Int(RandReader, modulusMinusOne) - if err != nil { - return - } - - if clientSecret.Cmp(big.NewInt(int64(length*2))) > 0 { // Very likely - break - } - } - - clientEphemeral = big.NewInt(0).Exp(generator, clientSecret, modulus) - scramblingParam = toInt(ExpandHash(append(fromInt(clientEphemeral), fromInt(serverEphemeral)...))) - if scramblingParam.Cmp(big.NewInt(0)) != 0 { // Very likely - break - } - } - - subtracted := big.NewInt(0).Sub(serverEphemeral, big.NewInt(0).Mod(big.NewInt(0).Mul(big.NewInt(0).Exp(generator, hashedPassword, modulus), multiplier), modulus)) - if subtracted.Cmp(big.NewInt(0)) < 0 { - subtracted.Add(subtracted, modulus) - } - exponent := big.NewInt(0).Mod(big.NewInt(0).Add(big.NewInt(0).Mul(scramblingParam, hashedPassword), clientSecret), modulusMinusOne) - sharedSession := big.NewInt(0).Exp(subtracted, exponent, modulus) - - clientProof := ExpandHash(bytes.Join([][]byte{fromInt(clientEphemeral), fromInt(serverEphemeral), fromInt(sharedSession)}, []byte{})) - serverProof := ExpandHash(bytes.Join([][]byte{fromInt(clientEphemeral), clientProof, fromInt(sharedSession)}, []byte{})) - - return &SrpProofs{ClientEphemeral: fromInt(clientEphemeral), ClientProof: clientProof, ExpectedServerProof: serverProof}, nil -} - -// GenerateVerifier verifier for update pwds and create accounts. -func (s *SrpAuth) GenerateVerifier(length int) ([]byte, error) { - return nil, errors.New("pm-srp: the client doesn't need SRP GenerateVerifier") -} diff --git a/pkg/srp/srp_test.go b/pkg/srp/srp_test.go deleted file mode 100644 index df9152c8..00000000 --- a/pkg/srp/srp_test.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) 2021 Proton Technologies AG -// -// This file is part of ProtonMail Bridge. -// -// ProtonMail Bridge is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// ProtonMail Bridge is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with ProtonMail Bridge. If not, see . - -package srp - -import ( - "bytes" - "encoding/base64" - "math/rand" - "testing" -) - -const ( - testServerEphemeral = "l13IQSVFBEV0ZZREuRQ4ZgP6OpGiIfIjbSDYQG3Yp39FkT2B/k3n1ZhwqrAdy+qvPPFq/le0b7UDtayoX4aOTJihoRvifas8Hr3icd9nAHqd0TUBbkZkT6Iy6UpzmirCXQtEhvGQIdOLuwvy+vZWh24G2ahBM75dAqwkP961EJMh67/I5PA5hJdQZjdPT5luCyVa7BS1d9ZdmuR0/VCjUOdJbYjgtIH7BQoZs+KacjhUN8gybu+fsycvTK3eC+9mCN2Y6GdsuCMuR3pFB0RF9eKae7cA6RbJfF1bjm0nNfWLXzgKguKBOeF3GEAsnCgK68q82/pq9etiUDizUlUBcA==" - testServerProof = "ffYFIhnhZJAflFJr9FfXbtdsBLkDGH+TUR5sj98wg0iVHyIhIVT6BeZD8tZA75tYlz7uYIanswweB3bjrGfITXfxERgQysQSoPUB284cX4VQm1IfTB/9LPma618MH8OULNluXVu2eizPWnvIn9VLXCaIX+38Xd6xOjmCQgfkpJy3Sh3ndikjqNCGWiKyvERVJi0nTmpAbHmcdeEp1K++ZRbebRhm2d018o/u4H2gu+MF39Hx12zMzEGNMwkNkgKSEQYlqmj57S6tW9JuB30zVZFnw6Krftg1QfJR6zCT1/J57OGp0A/7X/lC6Xz/I33eJvXOpG9GCRCbNiozFg9IXQ==" - - testClientProof = "8dQtp6zIeEmu3D93CxPdEiCWiAE86uDmK33EpxyqReMwUrm/bTL+zCkWa/X7QgLNrt2FBAriyROhz5TEONgZq/PqZnBEBym6Rvo708KHu6S4LFdZkVc0+lgi7yQpNhU8bqB0BCqdSWd3Fjd3xbOYgO7/vnFK+p9XQZKwEh2RmGv97XHwoxefoyXK6BB+VVMkELd4vL7vdqBiOBU3ufOlSp+0XBMVltQ4oi5l1y21pzOA9cw5WTPIPMcQHffNFq/rReHYnqbBqiLlSLyw6K0PcVuN3bvr3rVYfdS1CsM/Rv1DzXlBUl39B2j82y6hdyGcTeplGyAnAcu0CimvynKBvQ==" - testModulus = "W2z5HBi8RvsfYzZTS7qBaUxxPhsfHJFZpu3Kd6s1JafNrCCH9rfvPLrfuqocxWPgWDH2R8neK7PkNvjxto9TStuY5z7jAzWRvFWN9cQhAKkdWgy0JY6ywVn22+HFpF4cYesHrqFIKUPDMSSIlWjBVmEJZ/MusD44ZT29xcPrOqeZvwtCffKtGAIjLYPZIEbZKnDM1Dm3q2K/xS5h+xdhjnndhsrkwm9U9oyA2wxzSXFL+pdfj2fOdRwuR5nW0J2NFrq3kJjkRmpO/Genq1UW+TEknIWAb6VzJJJA244K/H8cnSx2+nSNZO3bbo6Ys228ruV9A8m6DhxmS+bihN3ttQ==" - testModulusClearSign = `-----BEGIN PGP SIGNED MESSAGE----- -Hash: SHA256 - -W2z5HBi8RvsfYzZTS7qBaUxxPhsfHJFZpu3Kd6s1JafNrCCH9rfvPLrfuqocxWPgWDH2R8neK7PkNvjxto9TStuY5z7jAzWRvFWN9cQhAKkdWgy0JY6ywVn22+HFpF4cYesHrqFIKUPDMSSIlWjBVmEJZ/MusD44ZT29xcPrOqeZvwtCffKtGAIjLYPZIEbZKnDM1Dm3q2K/xS5h+xdhjnndhsrkwm9U9oyA2wxzSXFL+pdfj2fOdRwuR5nW0J2NFrq3kJjkRmpO/Genq1UW+TEknIWAb6VzJJJA244K/H8cnSx2+nSNZO3bbo6Ys228ruV9A8m6DhxmS+bihN3ttQ== ------BEGIN PGP SIGNATURE----- -Version: ProtonMail -Comment: https://protonmail.com - -wl4EARYIABAFAlwB1j0JEDUFhcTpUY8mAAD8CgEAnsFnF4cF0uSHKkXa1GIa -GO86yMV4zDZEZcDSJo0fgr8A/AlupGN9EdHlsrZLmTA1vhIx+rOgxdEff28N -kvNM7qIK -=q6vu ------END PGP SIGNATURE-----` -) - -func init() { - // Only for tests, replace the default random reader by something that always - // return the same thing - RandReader = rand.New(rand.NewSource(42)) -} - -func TestReadClearSigned(t *testing.T) { - cleartext, err := ReadClearSignedMessage(testModulusClearSign) - if err != nil { - t.Fatal("Expected no error but have ", err) - } - if cleartext != testModulus { - t.Fatalf("Expected message\n\t'%s'\nbut have\n\t'%s'", testModulus, cleartext) - } - - lastChar := len(testModulusClearSign) - wrongSignature := testModulusClearSign[:lastChar-100] - wrongSignature += "c" - wrongSignature += testModulusClearSign[lastChar-99:] - _, err = ReadClearSignedMessage(wrongSignature) - if err != ErrInvalidSignature { - t.Fatal("Expected the ErrInvalidSignature but have ", err) - } - - wrongSignature = testModulusClearSign + "data after modulus" - _, err = ReadClearSignedMessage(wrongSignature) - if err != ErrDataAfterModulus { - t.Fatal("Expected the ErrDataAfterModulus but have ", err) - } -} - -func TestSRPauth(t *testing.T) { - srp, err := NewSrpAuth(4, "bridgetest", "test", "yKlc5/CvObfoiw==", testModulusClearSign, testServerEphemeral) - if err != nil { - t.Fatal("Expected no error but have ", err) - } - - proofs, err := srp.GenerateSrpProofs(2048) - if err != nil { - t.Fatal("Expected no error but have ", err) - } - - expectedProof, err := base64.StdEncoding.DecodeString(testServerProof) - if err != nil { - t.Fatal("Expected no error but have ", err) - } - if !bytes.Equal(proofs.ExpectedServerProof, expectedProof) { - t.Fatalf("Expected server proof\n\t'%s'\nbut have\n\t'%s'", - testServerProof, - base64.StdEncoding.EncodeToString(proofs.ExpectedServerProof), - ) - } - - expectedProof, err = base64.StdEncoding.DecodeString(testClientProof) - if err != nil { - t.Fatal("Expected no error but have ", err) - } - if !bytes.Equal(proofs.ClientProof, expectedProof) { - t.Fatalf("Expected client proof\n\t'%s'\nbut have\n\t'%s'", - testClientProof, - base64.StdEncoding.EncodeToString(proofs.ClientProof), - ) - } -} diff --git a/test/accounts/account.go b/test/accounts/account.go index 9fedcaea..975ae1f2 100644 --- a/test/accounts/account.go +++ b/test/accounts/account.go @@ -177,12 +177,12 @@ func (a *TestAccount) EnsureAddress(addressOrAddressTestID string) string { return addressOrAddressTestID } -func (a *TestAccount) Password() string { - return a.password +func (a *TestAccount) Password() []byte { + return []byte(a.password) } -func (a *TestAccount) MailboxPassword() string { - return a.mailboxPassword +func (a *TestAccount) MailboxPassword() []byte { + return []byte(a.mailboxPassword) } func (a *TestAccount) IsTwoFAEnabled() bool { diff --git a/test/context/credentials.go b/test/context/credentials.go index 0fc409cc..500bdff3 100644 --- a/test/context/credentials.go +++ b/test/context/credentials.go @@ -51,7 +51,7 @@ func (c *fakeCredStore) List() (userIDs []string, err error) { return keys, nil } -func (c *fakeCredStore) Add(userID, userName, uid, ref, mailboxPassword string, emails []string) (*credentials.Credentials, error) { +func (c *fakeCredStore) Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*credentials.Credentials, error) { bridgePassword := bridgePassword if c, ok := c.credentials[userID]; ok { bridgePassword = c.BridgePassword @@ -80,7 +80,7 @@ func (c *fakeCredStore) UpdateEmails(userID string, emails []string) (*credentia return c.credentials[userID], nil } -func (c *fakeCredStore) UpdatePassword(userID, password string) (*credentials.Credentials, error) { +func (c *fakeCredStore) UpdatePassword(userID string, password []byte) (*credentials.Credentials, error) { creds, err := c.Get(userID) if err != nil { return nil, err @@ -100,7 +100,7 @@ func (c *fakeCredStore) UpdateToken(userID, uid, ref string) (*credentials.Crede func (c *fakeCredStore) Logout(userID string) (*credentials.Credentials, error) { c.credentials[userID].APIToken = "" - c.credentials[userID].MailboxPassword = "" + c.credentials[userID].MailboxPassword = []byte{} return c.credentials[userID], nil } diff --git a/test/context/pmapi_controller.go b/test/context/pmapi_controller.go index bc768496..17c5a1b9 100644 --- a/test/context/pmapi_controller.go +++ b/test/context/pmapi_controller.go @@ -30,7 +30,7 @@ import ( type PMAPIController interface { TurnInternetConnectionOff() TurnInternetConnectionOn() - AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error + AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) error AddUserLabel(username string, label *pmapi.Label) error GetLabelIDs(username string, labelNames []string) ([]string, error) AddUserMessage(username string, message *pmapi.Message) (string, error) diff --git a/test/context/users.go b/test/context/users.go index 8beaafbe..9fa66ec0 100644 --- a/test/context/users.go +++ b/test/context/users.go @@ -24,9 +24,9 @@ import ( "path/filepath" "time" + "github.com/ProtonMail/go-srp" "github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/users" - "github.com/ProtonMail/proton-bridge/pkg/srp" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) @@ -37,7 +37,7 @@ func (ctx *TestContext) GetUsers() *users.Users { } // LoginUser logs in the user with the given username, password, and mailbox password. -func (ctx *TestContext) LoginUser(username, password, mailboxPassword string) error { +func (ctx *TestContext) LoginUser(username string, password, mailboxPassword []byte) error { srp.RandReader = rand.New(rand.NewSource(42)) //nolint[gosec] It is OK to use weaker random number generator here client, auth, err := ctx.users.Login(username, password) diff --git a/test/fakeapi/controller_control.go b/test/fakeapi/controller_control.go index 4d2f92cf..35a9e184 100644 --- a/test/fakeapi/controller_control.go +++ b/test/fakeapi/controller_control.go @@ -61,7 +61,7 @@ func (ctl *Controller) ReorderAddresses(user *pmapi.User, addressIDs []string) e return api.ReorderAddresses(context.Background(), addressIDs) } -func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error { +func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) error { ctl.usersByUsername[user.Name] = &fakeUser{ user: user, password: password, diff --git a/test/fakeapi/controller_session.go b/test/fakeapi/controller_session.go index 6a22b18a..87de705f 100644 --- a/test/fakeapi/controller_session.go +++ b/test/fakeapi/controller_session.go @@ -18,6 +18,7 @@ package fakeapi import ( + "bytes" "errors" "github.com/ProtonMail/proton-bridge/pkg/pmapi" @@ -49,10 +50,10 @@ func (ctl *Controller) checkScope(uid string) bool { return session.hasFullScope } -func (ctl *Controller) createSessionIfAuthorized(username, password string) (*fakeSession, error) { +func (ctl *Controller) createSessionIfAuthorized(username string, password []byte) (*fakeSession, error) { // get user user, ok := ctl.usersByUsername[username] - if !ok || user.password != password { + if !ok || !bytes.Equal(user.password, password) { return nil, errWrongNameOrPassword } diff --git a/test/fakeapi/controller_user.go b/test/fakeapi/controller_user.go index 3b73a542..5524e55e 100644 --- a/test/fakeapi/controller_user.go +++ b/test/fakeapi/controller_user.go @@ -21,6 +21,6 @@ import "github.com/ProtonMail/proton-bridge/pkg/pmapi" type fakeUser struct { user *pmapi.User - password string + password []byte has2FA bool } diff --git a/test/fakeapi/manager.go b/test/fakeapi/manager.go index 71da9d05..14eca80e 100644 --- a/test/fakeapi/manager.go +++ b/test/fakeapi/manager.go @@ -94,7 +94,7 @@ func (m *fakePMAPIManager) NewClientWithRefresh(_ context.Context, uid, ref stri return client, auth, nil } -func (m *fakePMAPIManager) NewClientWithLogin(_ context.Context, username string, password string) (pmapi.Client, *pmapi.Auth, error) { +func (m *fakePMAPIManager) NewClientWithLogin(_ context.Context, username string, password []byte) (pmapi.Client, *pmapi.Auth, error) { if err := m.controller.checkAndRecordCall(POST, "/auth/info", &pmapi.GetAuthInfoReq{Username: username}); err != nil { return nil, nil, err } diff --git a/test/liveapi/users.go b/test/liveapi/users.go index 7f328329..eac77afa 100644 --- a/test/liveapi/users.go +++ b/test/liveapi/users.go @@ -25,7 +25,7 @@ import ( "github.com/pkg/errors" ) -func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password string, twoFAEnabled bool) error { +func (ctl *Controller) AddUser(user *pmapi.User, addresses *pmapi.AddressList, password []byte, twoFAEnabled bool) error { if twoFAEnabled { return godog.ErrPending } diff --git a/test/users_actions_test.go b/test/users_actions_test.go index 0d6e8634..1f7f983c 100644 --- a/test/users_actions_test.go +++ b/test/users_actions_test.go @@ -45,7 +45,7 @@ func userLogsInWithBadPassword(bddUserID string) error { if account == nil { return godog.ErrPending } - ctx.SetLastError(ctx.LoginUser(account.Username(), "you shall not pass!", "123")) + ctx.SetLastError(ctx.LoginUser(account.Username(), []byte("you shall not pass!"), []byte("123"))) return nil } From 0a9ce5f526025330af08f34bd02a89c3d6dfa3c0 Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Thu, 27 May 2021 16:48:45 +0200 Subject: [PATCH 15/15] GODT-1155: zero out mailbox password during logout --- internal/users/credentials/credentials.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/users/credentials/credentials.go b/internal/users/credentials/credentials.go index 32d9e482..4e6e968b 100644 --- a/internal/users/credentials/credentials.go +++ b/internal/users/credentials/credentials.go @@ -143,6 +143,11 @@ func (s *Credentials) CheckPassword(password string) error { func (s *Credentials) Logout() { s.APIToken = "" + + for i := range s.MailboxPassword { + s.MailboxPassword[i] = 0 + } + s.MailboxPassword = []byte{} }