mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-10 04:36:43 +00:00
feat(GODT-2610): re-use previous password when removing and adding back account.
This commit is contained in:
46
internal/vault/password_archive.go
Normal file
46
internal/vault/password_archive.go
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2023 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 vault
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// set archives the password for an email address, overwriting any existing archived value.
|
||||
func (p *PasswordArchive) set(emailAddress string, password []byte) {
|
||||
if p.Archive == nil {
|
||||
p.Archive = make(map[string][]byte)
|
||||
}
|
||||
|
||||
p.Archive[emailHashString(emailAddress)] = password
|
||||
}
|
||||
|
||||
// get retrieves the archived password for an email address, or nil if not found.
|
||||
func (p *PasswordArchive) get(emailAddress string) []byte {
|
||||
if p.Archive == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.Archive[emailHashString(emailAddress)]
|
||||
}
|
||||
|
||||
// emailHashString returns a hash string for an email address as a hexadecimal string.
|
||||
func emailHashString(emailAddress string) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(emailAddress)))
|
||||
}
|
||||
@ -238,3 +238,30 @@ func TestVault_Settings_LastUserAgent(t *testing.T) {
|
||||
// Check the default first start value.
|
||||
require.Equal(t, vault.DefaultUserAgent, s.GetLastUserAgent())
|
||||
}
|
||||
|
||||
func Test_Settings_PasswordArchive(t *testing.T) {
|
||||
// Create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// The store should have no users.
|
||||
require.Empty(t, s.GetUserIDs())
|
||||
|
||||
// Create a new user.
|
||||
user, err := s.AddUser("userID1", "username1", "username1@pm.me", "authUID1", "authRef1", []byte("keyPass1"))
|
||||
require.NoError(t, err)
|
||||
bridgePass := user.BridgePass()
|
||||
|
||||
// Remove the user.
|
||||
require.NoError(t, user.Close())
|
||||
require.NoError(t, s.DeleteUser("userID1"))
|
||||
|
||||
// Add a different user. Another password is generated.
|
||||
user, err = s.AddUser("userID2", "username2", "username2@pm.me", "authUID2", "authRef2", []byte("keyPass2"))
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, user.BridgePass(), bridgePass)
|
||||
|
||||
// Add the first user again. The password is restored.
|
||||
user, err = s.AddUser("userID1", "username1", "username1@pm.me", "authUID1", "authRef1", []byte("keyPass1"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, user.BridgePass(), bridgePass)
|
||||
}
|
||||
|
||||
25
internal/vault/types_password_archive.go
Normal file
25
internal/vault/types_password_archive.go
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2023 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 vault
|
||||
|
||||
// PasswordArchive maps a list email address hashes to passwords.
|
||||
// The type is not defined as a map alias to prevent having to handle nil default values when vault was created by an older version of the application.
|
||||
type PasswordArchive struct {
|
||||
// we store the SHA-256 sum as string for readability and JSON marshalling of map[[32]byte][]byte will not be allowed, thus breaking vault-editor.
|
||||
Archive map[string][]byte
|
||||
}
|
||||
@ -53,6 +53,8 @@ type Settings struct {
|
||||
|
||||
LastHeartbeatSent time.Time
|
||||
|
||||
PasswordArchive PasswordArchive
|
||||
|
||||
// **WARNING**: These entry can't be removed until they vault has proper migration support.
|
||||
SyncWorkers int
|
||||
SyncAttPool int
|
||||
@ -105,5 +107,7 @@ func newDefaultSettings(gluonDir string) Settings {
|
||||
|
||||
LastUserAgent: DefaultUserAgent,
|
||||
LastHeartbeatSent: time.Time{},
|
||||
|
||||
PasswordArchive: PasswordArchive{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ func (status SyncStatus) IsComplete() bool {
|
||||
return status.HasLabels && status.HasMessages
|
||||
}
|
||||
|
||||
func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) UserData {
|
||||
func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, keyPass, bridgePass []byte) UserData {
|
||||
return UserData{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
@ -82,7 +82,7 @@ func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, key
|
||||
GluonKey: newRandomToken(32),
|
||||
GluonIDs: make(map[string]string),
|
||||
UIDValidity: make(map[string]imap.UID),
|
||||
BridgePass: newRandomToken(16),
|
||||
BridgePass: bridgePass,
|
||||
AddressMode: CombinedMode,
|
||||
|
||||
AuthUID: authUID,
|
||||
|
||||
@ -133,7 +133,8 @@ func (vault *Vault) ForUser(parallelism int, fn func(*User) error) error {
|
||||
}
|
||||
|
||||
// AddUser creates a new user in the vault with the given ID, username and password.
|
||||
// A bridge password and gluon key are generated using the package's token generator.
|
||||
// A gluon key is generated using the package's token generator. If a password is found in the password archive for this user,
|
||||
// it is restored, otherwise a new bridge password is generated using the package's token generator.
|
||||
func (vault *Vault) AddUser(userID, username, primaryEmail, authUID, authRef string, keyPass []byte) (*User, error) {
|
||||
logrus.WithField("userID", userID).Info("Adding vault user")
|
||||
|
||||
@ -145,7 +146,12 @@ func (vault *Vault) AddUser(userID, username, primaryEmail, authUID, authRef str
|
||||
}); idx >= 0 {
|
||||
exists = true
|
||||
} else {
|
||||
data.Users = append(data.Users, newDefaultUser(userID, username, primaryEmail, authUID, authRef, keyPass))
|
||||
bridgePass := data.Settings.PasswordArchive.get(primaryEmail)
|
||||
if len(bridgePass) == 0 {
|
||||
bridgePass = newRandomToken(16)
|
||||
}
|
||||
|
||||
data.Users = append(data.Users, newDefaultUser(userID, username, primaryEmail, authUID, authRef, keyPass, bridgePass))
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@ -177,7 +183,7 @@ func (vault *Vault) DeleteUser(userID string) error {
|
||||
if idx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
data.Settings.PasswordArchive.set(data.Users[idx].PrimaryEmail, data.Users[idx].BridgePass)
|
||||
data.Users = append(data.Users[:idx], data.Users[idx+1:]...)
|
||||
})
|
||||
}
|
||||
|
||||
@ -180,6 +180,8 @@ func TestFeatures(testingT *testing.T) {
|
||||
ctx.Step(`^user "([^"]*)" is not listed$`, s.userIsNotListed)
|
||||
ctx.Step(`^user "([^"]*)" finishes syncing$`, s.userFinishesSyncing)
|
||||
ctx.Step(`^user "([^"]*)" has telemetry set to (\d+)$`, s.userHasTelemetrySetTo)
|
||||
ctx.Step(`^the bridge password of user "([^"]*)" is changed to "([^"]*)"`, s.bridgePasswordOfUserIsChangedTo)
|
||||
ctx.Step(`^the bridge password of user "([^"]*)" is equal to "([^"]*)"`, s.bridgePasswordOfUserIsEqualTo)
|
||||
|
||||
// ==== IMAP ====
|
||||
ctx.Step(`^user "([^"]*)" connects IMAP client "([^"]*)"$`, s.userConnectsIMAPClient)
|
||||
|
||||
@ -114,6 +114,7 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) {
|
||||
} else if corrupt {
|
||||
return nil, fmt.Errorf("vault is corrupt")
|
||||
}
|
||||
t.vault = vault
|
||||
|
||||
// Create the underlying cookie jar.
|
||||
jar, err := cookiejar.New(nil)
|
||||
|
||||
@ -36,6 +36,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
frontend "github.com/ProtonMail/proton-bridge/v3/internal/frontend/grpc"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/cucumber/godog"
|
||||
"github.com/emersion/go-imap/client"
|
||||
@ -135,6 +136,7 @@ type testCtx struct {
|
||||
|
||||
// bridge holds the bridge app under test.
|
||||
bridge *bridge.Bridge
|
||||
vault *vault.Vault
|
||||
|
||||
// service holds the gRPC frontend service under test.
|
||||
service *frontend.Service
|
||||
|
||||
@ -13,3 +13,12 @@ Feature: A logged out user can login again
|
||||
Scenario: Cannot login to removed account
|
||||
When user "[user:user]" is deleted
|
||||
Then user "[user:user]" is not listed
|
||||
|
||||
Scenario: Bridge password persists after logout/login
|
||||
Given there exists an account with username "testUser" and password "password"
|
||||
And the user logs in with username "testUser" and password "password"
|
||||
And the bridge password of user "testUser" is changed to "YnJpZGdlcGFzc3dvcmQK"
|
||||
And user "testUser" is deleted
|
||||
And the user logs in with username "testUser" and password "password"
|
||||
Then user "testUser" is eventually listed and connected
|
||||
And the bridge password of user "testUser" is equal to "YnJpZGdlcGFzc3dvcmQK"
|
||||
|
||||
@ -28,6 +28,8 @@ import (
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||
"github.com/bradenaw/juniper/iterator"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/cucumber/godog"
|
||||
@ -426,6 +428,37 @@ func (s *scenario) userHasTelemetrySetTo(username string, telemetry int) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *scenario) bridgePasswordOfUserIsChangedTo(username, bridgePassword string) error {
|
||||
b, err := algo.B64RawDecode([]byte(bridgePassword))
|
||||
if err != nil {
|
||||
return errors.New("the password is not base64 encoded")
|
||||
}
|
||||
|
||||
var setErr error
|
||||
if err := s.t.vault.GetUser(
|
||||
s.t.getUserByName(username).getUserID(),
|
||||
func(user *vault.User) { setErr = user.SetBridgePass(b) },
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return setErr
|
||||
}
|
||||
|
||||
func (s *scenario) bridgePasswordOfUserIsEqualTo(username, bridgePassword string) error {
|
||||
userInfo, err := s.t.bridge.QueryUserInfo(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
readPassword := string(userInfo.BridgePass)
|
||||
if readPassword != bridgePassword {
|
||||
return fmt.Errorf("bridge password mismatch, expected '%v', got '%v'", bridgePassword, readPassword)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *scenario) addAdditionalAddressToAccount(username, address string, disabled bool) error {
|
||||
userID := s.t.getUserByName(username).getUserID()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user