Merge branch 'release/perth_narrows' into devel (3.0.7)

This commit is contained in:
Jakub 2022-12-13 19:37:12 +01:00
commit c39108043b
16 changed files with 312 additions and 100 deletions

View File

@ -2,17 +2,28 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 3.0.7] Perth Narrows
### Fixed
* Other: Increase default UIDVALIDITY.
* GODT-2173: fix: Migrate Bridge passwords from v2.X.
* GODT-2207: Fix encoding of non utf7 mailbox names.
* Other: Increase worker count (2 -> 4).
## [Bridge 3.0.6] Perth Narrows
### Fixed
* GODT-2187: Skip messages during sync that fail to build/parse.
## [Bridge 3.0.5] Perth Narrows
### Fixed
* GODT-2178: Bump go-proton-api to fix drafts.
* GODT-2180: Allow login with FIDO2.
## [Bridge 3.0.4] Perth Narrows
### Changed
@ -26,6 +37,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* GODT-2170: Update draft event means delete old and create new message.
* GODT-2170: User create draft route: first steps.
## [Bridge 3.0.3] Perth Narrows
### Fixed
@ -56,6 +68,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
* Other: Ensure context is string in sentry reports.
* GODT-2160: Ensure we can safely move cache file.
## [Bridge 3.0.1] Perth Narrows
### Changed

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.0.6+git
BRIDGE_APP_VERSION?=3.0.7+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.18
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/gluon v0.14.2-0.20221207071431-0faa318d3c9f
github.com/ProtonMail/gluon v0.14.2-0.20221213102402-d76bb11db6c5
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.2.2-0.20221213121236-3439b3eda101
github.com/ProtonMail/go-rfc5322 v0.11.0

4
go.sum
View File

@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
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/gluon v0.14.2-0.20221207071431-0faa318d3c9f h1:73b28jayIkYr1cJPHSHFMGyFgk1h6iJ127kuYX8UaRo=
github.com/ProtonMail/gluon v0.14.2-0.20221207071431-0faa318d3c9f/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/gluon v0.14.2-0.20221213102402-d76bb11db6c5 h1:OWskNw0tLTR3puf0uizqYVNQPKOmhWXQqrYO0Wm7Hs4=
github.com/ProtonMail/gluon v0.14.2-0.20221213102402-d76bb11db6c5/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=

View File

@ -32,6 +32,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
"github.com/allan-simon/go-singleinstance"
"github.com/hashicorp/go-multierror"
@ -43,6 +44,16 @@ import (
func migrateKeychainHelper(locations *locations.Locations) error {
logrus.Info("Migrating keychain helper")
settings, err := locations.ProvideSettingsPath()
if err != nil {
return fmt.Errorf("failed to get settings path: %w", err)
}
if keychainName, _ := vault.GetHelper(settings); keychainName != "" {
// If uncorupted keychain file is already there do not migrate again.
return nil
}
configDir, err := os.UserConfigDir()
if err != nil {
return fmt.Errorf("failed to get user config dir: %w", err)
@ -63,11 +74,6 @@ func migrateKeychainHelper(locations *locations.Locations) error {
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
}
settings, err := locations.ProvideSettingsPath()
if err != nil {
return fmt.Errorf("failed to get settings path: %w", err)
}
return vault.SetHelper(settings, prefs.Helper)
}
@ -115,26 +121,52 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
return fmt.Errorf("failed to create credentials store: %w", err)
}
var migrationErrors error
for _, userID := range users {
logrus.WithField("userID", userID).Info("Migrating account")
creds, err := store.Get(userID)
if err != nil {
return fmt.Errorf("failed to get user: %w", err)
if err := migrateOldAccount(userID, store, v); err != nil {
migrationErrors = multierror.Append(migrationErrors, err)
}
}
authUID, authRef, err := creds.SplitAPIToken()
if err != nil {
return fmt.Errorf("failed to split api token: %w", err)
}
return migrationErrors
}
user, err := v.AddUser(creds.UserID, creds.EmailList()[0], authUID, authRef, creds.MailboxPassword)
if err != nil {
return fmt.Errorf("failed to add user: %w", err)
}
func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault) error {
creds, err := store.Get(userID)
if err != nil {
return fmt.Errorf("failed to get user %q: %w", userID, err)
}
authUID, authRef, err := creds.SplitAPIToken()
if err != nil {
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
}
user, err := v.AddUser(creds.UserID, creds.EmailList()[0], authUID, authRef, creds.MailboxPassword)
if err != nil {
return fmt.Errorf("failed to add user %q: %w", userID, err)
}
defer func() {
if err := user.Close(); err != nil {
return fmt.Errorf("failed to close user: %w", err)
logrus.WithField("userID", userID).WithError(err).Error("Failed to close vault user after migration")
}
}()
dec, err := algo.B64RawDecode([]byte(creds.BridgePassword))
if err != nil {
return fmt.Errorf("failed to decode bridge password for user %q: %w", userID, err)
}
if err := user.SetBridgePass(dec); err != nil {
return fmt.Errorf("failed to set bridge password to user %q: %w", userID, err)
}
if !creds.IsCombinedAddressMode {
if err := user.SetAddressMode(vault.SplitMode); err != nil {
return fmt.Errorf("failed to set split address mode to user %q: %w", userID, err)
}
}

View File

@ -22,11 +22,19 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
"github.com/stretchr/testify/require"
)
@ -79,3 +87,113 @@ func TestMigratePrefsToVault(t *testing.T) {
// There should be a cookie for the API.
require.NotEmpty(t, cookies.Cookies(url))
}
func TestKeychainMigration(t *testing.T) {
// Migration tested only for linux.
if runtime.GOOS != "linux" {
return
}
tmpDir := t.TempDir()
// Prepare for keychain migration test
{
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", tmpDir))
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
require.NoError(t, err)
require.NoError(t, os.WriteFile(
filepath.Join(oldCacheDir, "prefs.json"),
oldPrefs, 0o600,
))
}
locations := locations.New(bridge.NewTestLocationsProvider(tmpDir), "config-name")
settingsFolder, err := locations.ProvideSettingsPath()
require.NoError(t, err)
// Check that there is nothing yet
keychainName, err := vault.GetHelper(settingsFolder)
require.NoError(t, err)
require.Equal(t, "", keychainName)
// Check migration
require.NoError(t, migrateKeychainHelper(locations))
keychainName, err = vault.GetHelper(settingsFolder)
require.NoError(t, err)
require.Equal(t, "secret-service", keychainName)
// Change the migrated value
require.NoError(t, vault.SetHelper(settingsFolder, "different"))
// Calling migration again will not overwrite existing prefs
require.NoError(t, migrateKeychainHelper(locations))
keychainName, err = vault.GetHelper(settingsFolder)
require.NoError(t, err)
require.Equal(t, "different", keychainName)
}
func TestUserMigration(t *testing.T) {
keychainHelper := keychain.NewTestHelper()
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
kc, err := keychain.NewKeychain("mock", "bridge")
require.NoError(t, err)
require.NoError(t, kc.Put("brokenID", "broken"))
require.NoError(t, kc.Put(
"emptyID",
(&credentials.Credentials{}).Marshal(),
))
wantUID := "uidtoken"
wantRefresh := "refreshtoken"
wantCredentials := credentials.Credentials{
UserID: "validID",
Name: "user@pm.me",
Emails: "user@pm.me;alias@pm.me",
APIToken: wantUID + ":" + wantRefresh,
MailboxPassword: []byte("secret"),
BridgePassword: "bElu2Q1Vusy28J3Wf56cIg",
Version: "v2.3.X",
Timestamp: 100,
IsCombinedAddressMode: true,
}
require.NoError(t, kc.Put(
wantCredentials.UserID,
wantCredentials.Marshal(),
))
tmpDir := t.TempDir()
locations := locations.New(bridge.NewTestLocationsProvider(tmpDir), "config-name")
settingsFolder, err := locations.ProvideSettingsPath()
require.NoError(t, err)
require.NoError(t, vault.SetHelper(settingsFolder, "mock"))
token, err := crypto.RandomToken(32)
require.NoError(t, err)
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token)
require.NoError(t, err)
require.False(t, corrupt)
require.NoError(t, migrateOldAccounts(locations, v))
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
require.Equal(t, wantCredentials.UserID, u.UserID())
require.Equal(t, wantUID, u.AuthUID())
require.Equal(t, wantRefresh, u.AuthRef())
require.Equal(t, wantCredentials.MailboxPassword, u.KeyPass())
require.Equal(t,
[]byte(wantCredentials.BridgePassword),
algo.B64RawEncode(u.BridgePass()),
)
require.Equal(t, vault.CombinedMode, u.AddressMode())
}))
}

View File

@ -73,7 +73,7 @@ func TestBridge_Refresh(t *testing.T) {
for _, name := range names {
status, err := client.Select("Folders/"+name, false)
require.NoError(t, err)
require.Equal(t, uint32(1), status.UidValidity)
require.Equal(t, uint32(1000), status.UidValidity)
}
})
@ -106,7 +106,7 @@ func TestBridge_Refresh(t *testing.T) {
for _, name := range names {
status, err := client.Select("Folders/"+name, false)
require.NoError(t, err)
require.Equal(t, uint32(2), status.UidValidity)
require.Equal(t, uint32(1001), status.UidValidity)
}
})
})

View File

@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
"github.com/bradenaw/juniper/xslices"
)
@ -135,7 +136,7 @@ func newFailedMessageLiteral(
"Error": syncErr.Error(),
}); err != nil {
panic(err)
} else if _, err := buf.Write(lineWrap(b64Encode(b))); err != nil {
} else if _, err := buf.Write(lineWrap(algo.B64Encode(b))); err != nil {
panic(err)
}

View File

@ -18,7 +18,6 @@
package user
import (
"encoding/base64"
"fmt"
"reflect"
"strings"
@ -58,36 +57,6 @@ func groupBy[Key comparable, Value any](items []Value, key func(Value) Key) map[
return groups
}
// b64Encode returns the base64 encoding of the given byte slice.
func b64Encode(b []byte) []byte {
enc := make([]byte, base64.StdEncoding.EncodedLen(len(b)))
base64.StdEncoding.Encode(enc, b)
return enc
}
// b64RawEncode returns the base64 encoding of the given byte slice.
func b64RawEncode(b []byte) []byte {
enc := make([]byte, base64.RawURLEncoding.EncodedLen(len(b)))
base64.RawURLEncoding.Encode(enc, b)
return enc
}
// b64RawDecode returns the bytes represented by the base64 encoding of the given byte slice.
func b64RawDecode(b []byte) ([]byte, error) {
dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(b)))
n, err := base64.RawURLEncoding.Decode(dec, b)
if err != nil {
return nil, err
}
return dec[:n], nil
}
// getAddrID returns the address ID for the given email address.
func getAddrID(apiAddrs map[string]proton.Address, email string) (string, error) {
for _, addr := range apiAddrs {

View File

@ -36,6 +36,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
"github.com/bradenaw/juniper/xslices"
"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
@ -355,7 +356,7 @@ func (user *User) GluonKey() []byte {
// BridgePass returns the user's bridge password, used for authentication over SMTP and IMAP.
func (user *User) BridgePass() []byte {
return b64RawEncode(user.vault.BridgePass())
return algo.B64RawEncode(user.vault.BridgePass())
}
// UsedSpace returns the total space used by the user on the API.
@ -431,7 +432,7 @@ func (user *User) CheckAuth(email string, password []byte) (string, error) {
panic("your wish is my command.. I crash")
}
dec, err := b64RawDecode(password)
dec, err := algo.B64RawDecode(password)
if err != nil {
return "", fmt.Errorf("failed to decode password: %w", err)
}

View File

@ -29,14 +29,16 @@ type Keychain struct {
Helper string
}
func GetHelper(vaultDir string) (string, error) {
filePath := filepath.Clean(filepath.Join(vaultDir, "keychain.json"))
func getKeychainPrefPath(vaultDir string) string {
return filepath.Clean(filepath.Join(vaultDir, "keychain.json"))
}
if _, err := os.Stat(filePath); errors.Is(err, fs.ErrNotExist) {
func GetHelper(vaultDir string) (string, error) {
if _, err := os.Stat(getKeychainPrefPath(vaultDir)); errors.Is(err, fs.ErrNotExist) {
return "", nil
}
b, err := os.ReadFile(filePath)
b, err := os.ReadFile(getKeychainPrefPath(vaultDir))
if err != nil {
return "", err
}
@ -56,5 +58,5 @@ func SetHelper(vaultDir, helper string) error {
return err
}
return os.WriteFile(filepath.Clean(filepath.Join(vaultDir, "keychain.json")), b, 0o600)
return os.WriteFile(getKeychainPrefPath(vaultDir), b, 0o600)
}

View File

@ -28,7 +28,7 @@ type UserData struct {
GluonKey []byte
GluonIDs map[string]string
UIDValidity map[string]imap.UID
BridgePass []byte
BridgePass []byte // raw token represented as byte slice (needs to be encoded)
AddressMode AddressMode
AuthUID string

View File

@ -74,7 +74,7 @@ func (user *User) GetUIDValidity(addrID string) imap.UID {
return validity
}
if err := user.SetUIDValidity(addrID, 1); err != nil {
if err := user.SetUIDValidity(addrID, 1000); err != nil {
panic(err)
}
@ -99,11 +99,18 @@ func (user *User) SetAddressMode(mode AddressMode) error {
})
}
// BridgePass returns the user's bridge password (unencoded).
// BridgePass returns the user's bridge password as raw token bytes (unencoded).
func (user *User) BridgePass() []byte {
return user.vault.getUser(user.userID).BridgePass
}
// SetBridgePass saves bridge password as raw token bytes (unecoded).
func (user *User) SetBridgePass(newPass []byte) error {
return user.vault.modUser(user.userID, func(data *UserData) {
data.BridgePass = newPass
})
}
// AuthUID returns the user's auth UID.
func (user *User) AuthUID() string {
return user.vault.getUser(user.userID).AuthUID

50
pkg/algo/encode.go Normal file
View File

@ -0,0 +1,50 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package algo
import "encoding/base64"
// B64Encode returns the base64 encoding of the given byte slice.
func B64Encode(b []byte) []byte {
enc := make([]byte, base64.StdEncoding.EncodedLen(len(b)))
base64.StdEncoding.Encode(enc, b)
return enc
}
// B64RawEncode returns the base64 encoding of the given byte slice.
func B64RawEncode(b []byte) []byte {
enc := make([]byte, base64.RawURLEncoding.EncodedLen(len(b)))
base64.RawURLEncoding.Encode(enc, b)
return enc
}
// B64RawDecode returns the bytes represented by the base64 encoding of the given byte slice.
func B64RawDecode(b []byte) ([]byte, error) {
dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(b)))
n, err := base64.RawURLEncoding.Decode(dec, b)
if err != nil {
return nil, err
}
return dec[:n], nil
}

View File

@ -21,7 +21,6 @@ import (
"encoding/base64"
"testing"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/stretchr/testify/require"
)
@ -33,7 +32,7 @@ var testData = map[string]string{ //nolint:gochecknoglobals
}
func TestInsertReadRemove(t *testing.T) {
keychain := newKeychain(newTestHelper(), hostURL("bridge"))
keychain := newKeychain(NewTestHelper(), hostURL("bridge"))
for id, secret := range testData {
expectedList, _ := keychain.List()
@ -115,35 +114,3 @@ func TestInsertReadRemove(t *testing.T) {
require.NotContains(t, actualList, id)
}
}
type testHelper map[string]*credentials.Credentials
func newTestHelper() testHelper {
return make(testHelper)
}
func (h testHelper) Add(creds *credentials.Credentials) error {
h[creds.ServerURL] = creds
return nil
}
func (h testHelper) Delete(url string) error {
delete(h, url)
return nil
}
func (h testHelper) Get(url string) (string, string, error) {
creds := h[url]
return creds.Username, creds.Secret, nil
}
func (h testHelper) List() (map[string]string, error) {
list := make(map[string]string)
for url, creds := range h {
list[url] = creds.Username
}
return list, nil
}

View File

@ -0,0 +1,52 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package keychain
import "github.com/docker/docker-credential-helpers/credentials"
type TestHelper map[string]*credentials.Credentials
func NewTestHelper() TestHelper {
return make(TestHelper)
}
func (h TestHelper) Add(creds *credentials.Credentials) error {
h[creds.ServerURL] = creds
return nil
}
func (h TestHelper) Delete(url string) error {
delete(h, url)
return nil
}
func (h TestHelper) Get(url string) (string, string, error) {
creds := h[url]
return creds.Username, creds.Secret, nil
}
func (h TestHelper) List() (map[string]string, error) {
list := make(map[string]string)
for url, creds := range h {
list[url] = creds.Username
}
return list, nil
}