GODT-2173: fix: Migrate Bridge password from v2.X.
This commit is contained in:
parent
57d563d488
commit
2b25fe1fa4
|
@ -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"
|
||||
|
@ -120,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,11 +25,16 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
@ -84,46 +89,111 @@ func TestMigratePrefsToVault(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestKeychainMigration(t *testing.T) {
|
||||
// migration needed only for linux
|
||||
// Migration tested only for linux.
|
||||
if runtime.GOOS != "linux" {
|
||||
return
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
os.Setenv("XDG_CONFIG_HOME", tmpDir)
|
||||
|
||||
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
||||
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
|
||||
// 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)
|
||||
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,
|
||||
))
|
||||
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
|
||||
// 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())
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -34,13 +34,11 @@ func getKeychainPrefPath(vaultDir string) string {
|
|||
}
|
||||
|
||||
func GetHelper(vaultDir string) (string, error) {
|
||||
filePath := getKeychainPrefPath(vaultDir)
|
||||
|
||||
if _, err := os.Stat(filePath); errors.Is(err, fs.ErrNotExist) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue