mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2026-02-12 03:48:34 +00:00
GODT-1779: Remove go-imap
This commit is contained in:
19
internal/vault/certs.go
Normal file
19
internal/vault/certs.go
Normal file
@ -0,0 +1,19 @@
|
||||
package vault
|
||||
|
||||
func (vault *Vault) GetBridgeTLSCert() []byte {
|
||||
return vault.get().Certs.Bridge.Cert
|
||||
}
|
||||
|
||||
func (vault *Vault) GetBridgeTLSKey() []byte {
|
||||
return vault.get().Certs.Bridge.Key
|
||||
}
|
||||
|
||||
func (vault *Vault) GetCertsInstalled() bool {
|
||||
return vault.get().Certs.Installed
|
||||
}
|
||||
|
||||
func (vault *Vault) SetCertsInstalled(installed bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Certs.Installed = installed
|
||||
})
|
||||
}
|
||||
25
internal/vault/certs_test.go
Normal file
25
internal/vault/certs_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package vault_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVault_TLSCerts(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default bridge TLS certs.
|
||||
require.NotEmpty(t, s.GetBridgeTLSCert())
|
||||
require.NotEmpty(t, s.GetBridgeTLSKey())
|
||||
|
||||
// Check the certificates are not installed.
|
||||
require.False(t, s.GetCertsInstalled())
|
||||
|
||||
// Install the certificates.
|
||||
require.NoError(t, s.SetCertsInstalled(true))
|
||||
|
||||
// Check the certificates are installed.
|
||||
require.True(t, s.GetCertsInstalled())
|
||||
}
|
||||
11
internal/vault/cookies.go
Normal file
11
internal/vault/cookies.go
Normal file
@ -0,0 +1,11 @@
|
||||
package vault
|
||||
|
||||
func (vault *Vault) GetCookies() ([]byte, error) {
|
||||
return vault.get().Cookies, nil
|
||||
}
|
||||
|
||||
func (vault *Vault) SetCookies(cookies []byte) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Cookies = cookies
|
||||
})
|
||||
}
|
||||
25
internal/vault/cookies_test.go
Normal file
25
internal/vault/cookies_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
package vault_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVault_Cookies(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default cookies are empty.
|
||||
cookies, err := s.GetCookies()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cookies)
|
||||
|
||||
// Set some cookies.
|
||||
require.NoError(t, s.SetCookies([]byte("something")))
|
||||
|
||||
// Check the cookies are as set.
|
||||
newCookies, err := s.GetCookies()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []byte("something"), newCookies)
|
||||
}
|
||||
41
internal/vault/helper.go
Normal file
41
internal/vault/helper.go
Normal file
@ -0,0 +1,41 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Keychain struct {
|
||||
Helper string
|
||||
}
|
||||
|
||||
func GetHelper(vaultDir string) (string, error) {
|
||||
var keychain Keychain
|
||||
|
||||
if _, err := os.Stat(filepath.Join(vaultDir, "keychain.json")); errors.Is(err, fs.ErrNotExist) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(filepath.Join(vaultDir, "keychain.json"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &keychain); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return keychain.Helper, nil
|
||||
}
|
||||
|
||||
func SetHelper(vaultDir, helper string) error {
|
||||
b, err := json.MarshalIndent(Keychain{Helper: helper}, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Join(vaultDir, "keychain.json"), b, 0o600)
|
||||
}
|
||||
186
internal/vault/settings.go
Normal file
186
internal/vault/settings.go
Normal file
@ -0,0 +1,186 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
)
|
||||
|
||||
// GetIMAPPort sets the port that the IMAP server should listen on.
|
||||
func (vault *Vault) GetIMAPPort() int {
|
||||
return vault.get().Settings.IMAPPort
|
||||
}
|
||||
|
||||
// SetIMAPPort sets the port that the IMAP server should listen on.
|
||||
func (vault *Vault) SetIMAPPort(port int) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.IMAPPort = port
|
||||
})
|
||||
}
|
||||
|
||||
// GetSMTPPort sets the port that the SMTP server should listen on.
|
||||
func (vault *Vault) GetSMTPPort() int {
|
||||
return vault.get().Settings.SMTPPort
|
||||
}
|
||||
|
||||
// SetSMTPPort sets the port that the SMTP server should listen on.
|
||||
func (vault *Vault) SetSMTPPort(port int) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.SMTPPort = port
|
||||
})
|
||||
}
|
||||
|
||||
// GetIMAPSSL sets whether the IMAP server should use SSL.
|
||||
func (vault *Vault) GetIMAPSSL() bool {
|
||||
return vault.get().Settings.IMAPSSL
|
||||
}
|
||||
|
||||
// SetIMAPSSL sets whether the IMAP server should use SSL.
|
||||
func (vault *Vault) SetIMAPSSL(ssl bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.IMAPSSL = ssl
|
||||
})
|
||||
}
|
||||
|
||||
// GetSMTPSSL sets whether the SMTP server should use SSL.
|
||||
func (vault *Vault) GetSMTPSSL() bool {
|
||||
return vault.get().Settings.SMTPSSL
|
||||
}
|
||||
|
||||
// SetSMTPSSL sets whether the SMTP server should use SSL.
|
||||
func (vault *Vault) SetSMTPSSL(ssl bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.SMTPSSL = ssl
|
||||
})
|
||||
}
|
||||
|
||||
// GetGluonDir sets the directory where the gluon should store its data.
|
||||
func (vault *Vault) GetGluonDir() string {
|
||||
return vault.get().Settings.GluonDir
|
||||
}
|
||||
|
||||
// SetGluonDir sets the directory where the gluon should store its data.
|
||||
func (vault *Vault) SetGluonDir(dir string) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.GluonDir = dir
|
||||
})
|
||||
}
|
||||
|
||||
// GetUpdateChannel sets the update channel.
|
||||
func (vault *Vault) GetUpdateChannel() updater.Channel {
|
||||
return vault.get().Settings.UpdateChannel
|
||||
}
|
||||
|
||||
// SetUpdateChannel sets the update channel.
|
||||
func (vault *Vault) SetUpdateChannel(channel updater.Channel) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.UpdateChannel = channel
|
||||
})
|
||||
}
|
||||
|
||||
// GetUpdateRollout sets the update rollout.
|
||||
func (vault *Vault) GetUpdateRollout() float64 {
|
||||
return vault.get().Settings.UpdateRollout
|
||||
}
|
||||
|
||||
// SetUpdateRollout sets the update rollout.
|
||||
func (vault *Vault) SetUpdateRollout(rollout float64) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.UpdateRollout = rollout
|
||||
})
|
||||
}
|
||||
|
||||
// GetColorScheme sets the color scheme to be used by the bridge GUI.
|
||||
func (vault *Vault) GetColorScheme() string {
|
||||
return vault.get().Settings.ColorScheme
|
||||
}
|
||||
|
||||
// SetColorScheme sets the color scheme to be used by the bridge GUI.
|
||||
func (vault *Vault) SetColorScheme(colorScheme string) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.ColorScheme = colorScheme
|
||||
})
|
||||
}
|
||||
|
||||
// GetProxyAllowed sets whether the bridge is allowed to use alternative routing.
|
||||
func (vault *Vault) GetProxyAllowed() bool {
|
||||
return vault.get().Settings.ProxyAllowed
|
||||
}
|
||||
|
||||
// SetProxyAllowed sets whether the bridge is allowed to use alternative routing.
|
||||
func (vault *Vault) SetProxyAllowed(allowed bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.ProxyAllowed = allowed
|
||||
})
|
||||
}
|
||||
|
||||
// GetShowAllMail sets whether the bridge should show the All Mail folder.
|
||||
func (vault *Vault) GetShowAllMail() bool {
|
||||
return vault.get().Settings.ShowAllMail
|
||||
}
|
||||
|
||||
// SetShowAllMail sets whether the bridge should show the All Mail folder.
|
||||
func (vault *Vault) SetShowAllMail(showAllMail bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.ShowAllMail = showAllMail
|
||||
})
|
||||
}
|
||||
|
||||
// GetAutostart sets whether the bridge should autostart.
|
||||
func (vault *Vault) GetAutostart() bool {
|
||||
return vault.get().Settings.Autostart
|
||||
}
|
||||
|
||||
// SetAutostart sets whether the bridge should autostart.
|
||||
func (vault *Vault) SetAutostart(autostart bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.Autostart = autostart
|
||||
})
|
||||
}
|
||||
|
||||
// GetAutoUpdate sets whether the bridge should automatically update.
|
||||
func (vault *Vault) GetAutoUpdate() bool {
|
||||
return vault.get().Settings.AutoUpdate
|
||||
}
|
||||
|
||||
// SetAutoUpdate sets whether the bridge should automatically update.
|
||||
func (vault *Vault) SetAutoUpdate(autoUpdate bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.AutoUpdate = autoUpdate
|
||||
})
|
||||
}
|
||||
|
||||
// GetLastVersion returns the last version of the bridge that was run.
|
||||
func (vault *Vault) GetLastVersion() *semver.Version {
|
||||
return vault.get().Settings.LastVersion
|
||||
}
|
||||
|
||||
// SetLastVersion sets the last version of the bridge that was run.
|
||||
func (vault *Vault) SetLastVersion(version *semver.Version) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.LastVersion = version
|
||||
})
|
||||
}
|
||||
|
||||
// GetFirstStart sets whether this is the first time the bridge has been started.
|
||||
func (vault *Vault) GetFirstStart() bool {
|
||||
return vault.get().Settings.FirstStart
|
||||
}
|
||||
|
||||
// SetFirstStart sets whether this is the first time the bridge has been started.
|
||||
func (vault *Vault) SetFirstStart(firstStart bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.FirstStart = firstStart
|
||||
})
|
||||
}
|
||||
|
||||
// GetFirstStartGUI sets whether this is the first time the bridge GUI has been started.
|
||||
func (vault *Vault) GetFirstStartGUI() bool {
|
||||
return vault.get().Settings.FirstStartGUI
|
||||
}
|
||||
|
||||
// SetFirstStartGUI sets whether this is the first time the bridge GUI has been started.
|
||||
func (vault *Vault) SetFirstStartGUI(firstStartGUI bool) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
data.Settings.FirstStartGUI = firstStartGUI
|
||||
})
|
||||
}
|
||||
201
internal/vault/settings_test.go
Normal file
201
internal/vault/settings_test.go
Normal file
@ -0,0 +1,201 @@
|
||||
package vault_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVault_Settings_IMAP(t *testing.T) {
|
||||
// Create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default IMAP port and SSL setting.
|
||||
require.Equal(t, 1143, s.GetIMAPPort())
|
||||
require.Equal(t, false, s.GetIMAPSSL())
|
||||
|
||||
// Modify the IMAP port and SSL setting.
|
||||
require.NoError(t, s.SetIMAPPort(1234))
|
||||
require.NoError(t, s.SetIMAPSSL(true))
|
||||
|
||||
// Check the new IMAP port and SSL setting.
|
||||
require.Equal(t, 1234, s.GetIMAPPort())
|
||||
require.Equal(t, true, s.GetIMAPSSL())
|
||||
}
|
||||
|
||||
func TestVault_Settings_SMTP(t *testing.T) {
|
||||
// Create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default SMTP port and SSL setting.
|
||||
require.Equal(t, 1025, s.GetSMTPPort())
|
||||
require.Equal(t, false, s.GetSMTPSSL())
|
||||
|
||||
// Modify the SMTP port and SSL setting.
|
||||
require.NoError(t, s.SetSMTPPort(1234))
|
||||
require.NoError(t, s.SetSMTPSSL(true))
|
||||
|
||||
// Check the new SMTP port and SSL setting.
|
||||
require.Equal(t, 1234, s.GetSMTPPort())
|
||||
require.Equal(t, true, s.GetSMTPSSL())
|
||||
}
|
||||
|
||||
func TestVault_Settings_GluonDir(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s, corrupt, err := vault.New(t.TempDir(), "/path/to/gluon", []byte("my secret key"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
// Check the default gluon dir.
|
||||
require.Equal(t, "/path/to/gluon", s.GetGluonDir())
|
||||
|
||||
// Modify the gluon dir.
|
||||
require.NoError(t, s.SetGluonDir("/tmp/gluon"))
|
||||
|
||||
// Check the new gluon dir.
|
||||
require.Equal(t, "/tmp/gluon", s.GetGluonDir())
|
||||
}
|
||||
|
||||
func TestVault_Settings_UpdateChannel(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default update channel.
|
||||
require.Equal(t, updater.StableChannel, s.GetUpdateChannel())
|
||||
|
||||
// Modify the update channel.
|
||||
require.NoError(t, s.SetUpdateChannel(updater.EarlyChannel))
|
||||
|
||||
// Check the new update channel.
|
||||
require.Equal(t, updater.EarlyChannel, s.GetUpdateChannel())
|
||||
}
|
||||
|
||||
func TestVault_Settings_UpdateRollout(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default update rollout.
|
||||
require.GreaterOrEqual(t, s.GetUpdateRollout(), float64(0))
|
||||
require.LessOrEqual(t, s.GetUpdateRollout(), float64(1))
|
||||
|
||||
// Modify the update rollout.
|
||||
require.NoError(t, s.SetUpdateRollout(0.5))
|
||||
|
||||
// Check the new update rollout.
|
||||
require.Equal(t, float64(0.5), s.GetUpdateRollout())
|
||||
}
|
||||
|
||||
func TestVault_Settings_ColorScheme(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default color scheme.
|
||||
require.Equal(t, "", s.GetColorScheme())
|
||||
|
||||
// Modify the color scheme.
|
||||
require.NoError(t, s.SetColorScheme("dark"))
|
||||
|
||||
// Check the new color scheme.
|
||||
require.Equal(t, "dark", s.GetColorScheme())
|
||||
}
|
||||
|
||||
func TestVault_Settings_ProxyAllowed(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default proxy allowed setting.
|
||||
require.Equal(t, true, s.GetProxyAllowed())
|
||||
|
||||
// Modify the proxy allowed setting.
|
||||
require.NoError(t, s.SetProxyAllowed(false))
|
||||
|
||||
// Check the new proxy allowed setting.
|
||||
require.Equal(t, false, s.GetProxyAllowed())
|
||||
}
|
||||
|
||||
func TestVault_Settings_ShowAllMail(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default show all mail setting.
|
||||
require.Equal(t, true, s.GetShowAllMail())
|
||||
|
||||
// Modify the show all mail setting.
|
||||
require.NoError(t, s.SetShowAllMail(false))
|
||||
|
||||
// Check the new show all mail setting.
|
||||
require.Equal(t, false, s.GetShowAllMail())
|
||||
}
|
||||
|
||||
func TestVault_Settings_Autostart(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default autostart setting.
|
||||
require.Equal(t, false, s.GetAutostart())
|
||||
|
||||
// Modify the autostart setting.
|
||||
require.NoError(t, s.SetAutostart(true))
|
||||
|
||||
// Check the new autostart setting.
|
||||
require.Equal(t, true, s.GetAutostart())
|
||||
}
|
||||
|
||||
func TestVault_Settings_AutoUpdate(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default auto update setting.
|
||||
require.Equal(t, true, s.GetAutoUpdate())
|
||||
|
||||
// Modify the auto update setting.
|
||||
require.NoError(t, s.SetAutoUpdate(false))
|
||||
|
||||
// Check the new auto update setting.
|
||||
require.Equal(t, false, s.GetAutoUpdate())
|
||||
}
|
||||
|
||||
func TestVault_Settings_LastVersion(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default first start value.
|
||||
require.True(t, semver.MustParse("0.0.0").Equal(s.GetLastVersion()))
|
||||
|
||||
// Modify the first start value.
|
||||
require.NoError(t, s.SetLastVersion(semver.MustParse("1.2.3")))
|
||||
|
||||
// Check the new first start value.
|
||||
require.True(t, semver.MustParse("1.2.3").Equal(s.GetLastVersion()))
|
||||
}
|
||||
|
||||
func TestVault_Settings_FirstStart(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default first start value.
|
||||
require.Equal(t, true, s.GetFirstStart())
|
||||
|
||||
// Modify the first start value.
|
||||
require.NoError(t, s.SetFirstStart(false))
|
||||
|
||||
// Check the new first start value.
|
||||
require.Equal(t, false, s.GetFirstStart())
|
||||
}
|
||||
|
||||
func TestVault_Settings_FirstStartGUI(t *testing.T) {
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Check the default first start value.
|
||||
require.Equal(t, true, s.GetFirstStartGUI())
|
||||
|
||||
// Modify the first start value.
|
||||
require.NoError(t, s.SetFirstStartGUI(false))
|
||||
|
||||
// Check the new first start value.
|
||||
require.Equal(t, false, s.GetFirstStartGUI())
|
||||
}
|
||||
264
internal/vault/store.go
Normal file
264
internal/vault/store.go
Normal file
@ -0,0 +1,264 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/certs"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInsecure = errors.New("the vault is insecure")
|
||||
ErrCorrupt = errors.New("the vault is corrupt")
|
||||
)
|
||||
|
||||
type Vault struct {
|
||||
path string
|
||||
enc []byte
|
||||
gcm cipher.AEAD
|
||||
}
|
||||
|
||||
// New constructs a new encrypted data vault at the given filepath using the given encryption key.
|
||||
func New(vaultDir, gluonDir string, key []byte) (*Vault, bool, error) {
|
||||
if err := os.MkdirAll(vaultDir, 0o700); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
hash256 := sha256.Sum256(key)
|
||||
|
||||
aes, err := aes.NewCipher(hash256[:])
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(aes)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
vault, corrupt, err := newVault(filepath.Join(vaultDir, "vault.enc"), gluonDir, gcm)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return vault, corrupt, nil
|
||||
}
|
||||
|
||||
// GetUserIDs returns the user IDs and usernames of all users in the vault.
|
||||
func (vault *Vault) GetUserIDs() []string {
|
||||
return xslices.Map(vault.get().Users, func(user UserData) string {
|
||||
return user.UserID
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserIDs returns the user IDs and usernames of all users in the vault.
|
||||
func (vault *Vault) GetUser(userID string) (*User, error) {
|
||||
if idx := xslices.IndexFunc(vault.get().Users, func(user UserData) bool {
|
||||
return user.UserID == userID
|
||||
}); idx < 0 {
|
||||
return nil, errors.New("no such user")
|
||||
}
|
||||
|
||||
return &User{
|
||||
vault: vault,
|
||||
userID: userID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddUser creates a new user in the vault with the given ID and username.
|
||||
// A bridge password is generated using the package's token generator.
|
||||
func (vault *Vault) AddUser(userID, username, authUID, authRef string, keyPass []byte) (*User, error) {
|
||||
if idx := xslices.IndexFunc(vault.get().Users, func(user UserData) bool {
|
||||
return user.UserID == userID
|
||||
}); idx >= 0 {
|
||||
return nil, errors.New("user already exists")
|
||||
}
|
||||
|
||||
tok, err := RandomToken(16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := vault.mod(func(data *Data) {
|
||||
data.Users = append(data.Users, UserData{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
BridgePass: hex.EncodeToString(tok),
|
||||
|
||||
AuthUID: authUID,
|
||||
AuthRef: authRef,
|
||||
KeyPass: keyPass,
|
||||
})
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vault.GetUser(userID)
|
||||
}
|
||||
|
||||
// DeleteUser removes the given user from the vault.
|
||||
func (vault *Vault) DeleteUser(userID string) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
idx := xslices.IndexFunc(data.Users, func(user UserData) bool {
|
||||
return user.UserID == userID
|
||||
})
|
||||
|
||||
if idx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
data.Users = append(data.Users[:idx], data.Users[idx+1:]...)
|
||||
})
|
||||
}
|
||||
|
||||
func newVault(path, gluonDir string, gcm cipher.AEAD) (*Vault, bool, error) {
|
||||
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
if _, err := initVault(path, gluonDir, gcm); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
enc, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var corrupt bool
|
||||
|
||||
if _, err := decrypt(gcm, enc); err != nil {
|
||||
corrupt = true
|
||||
|
||||
newEnc, err := initVault(path, gluonDir, gcm)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
enc = newEnc
|
||||
}
|
||||
|
||||
return &Vault{path: path, enc: enc, gcm: gcm}, corrupt, nil
|
||||
}
|
||||
|
||||
func (vault *Vault) get() Data {
|
||||
dec, err := decrypt(vault.gcm, vault.enc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var data Data
|
||||
|
||||
if err := json.Unmarshal(dec, &data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (vault *Vault) mod(fn func(data *Data)) error {
|
||||
data := vault.get()
|
||||
|
||||
fn(&data)
|
||||
|
||||
return vault.set(data)
|
||||
}
|
||||
|
||||
func (vault *Vault) set(data Data) error {
|
||||
dec, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
enc, err := encrypt(vault.gcm, dec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vault.enc = enc
|
||||
|
||||
return os.WriteFile(vault.path, vault.enc, 0o600)
|
||||
}
|
||||
|
||||
func (vault *Vault) getUser(userID string) UserData {
|
||||
return vault.get().Users[xslices.IndexFunc(vault.get().Users, func(user UserData) bool {
|
||||
return user.UserID == userID
|
||||
})]
|
||||
}
|
||||
|
||||
func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
|
||||
return vault.mod(func(data *Data) {
|
||||
idx := xslices.IndexFunc(data.Users, func(user UserData) bool {
|
||||
return user.UserID == userID
|
||||
})
|
||||
|
||||
fn(&data.Users[idx])
|
||||
})
|
||||
}
|
||||
|
||||
func initVault(path, gluonDir string, gcm cipher.AEAD) ([]byte, error) {
|
||||
bridgeCert, err := newTLSCert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dec, err := json.Marshal(Data{
|
||||
Settings: newDefaultSettings(gluonDir),
|
||||
|
||||
Certs: Certs{
|
||||
Bridge: bridgeCert,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enc, err := encrypt(gcm, dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, enc, 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return enc, nil
|
||||
}
|
||||
|
||||
func decrypt(gcm cipher.AEAD, enc []byte) ([]byte, error) {
|
||||
return gcm.Open(nil, enc[:gcm.NonceSize()], enc[gcm.NonceSize():], nil)
|
||||
}
|
||||
|
||||
func encrypt(gcm cipher.AEAD, data []byte) ([]byte, error) {
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gcm.Seal(nonce, nonce, data, nil), nil
|
||||
}
|
||||
|
||||
func newTLSCert() (Cert, error) {
|
||||
template, err := certs.NewTLSTemplate()
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
certPEM, keyPEM, err := certs.GenerateCert(template)
|
||||
if err != nil {
|
||||
return Cert{}, err
|
||||
}
|
||||
|
||||
return Cert{
|
||||
Cert: certPEM,
|
||||
Key: keyPEM,
|
||||
}, nil
|
||||
}
|
||||
40
internal/vault/store_test.go
Normal file
40
internal/vault/store_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
package vault_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVaultCorrupt(t *testing.T) {
|
||||
vaultDir, gluonDir := t.TempDir(), t.TempDir()
|
||||
|
||||
{
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
}
|
||||
|
||||
{
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("my secret key"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
}
|
||||
|
||||
{
|
||||
_, corrupt, err := vault.New(vaultDir, gluonDir, []byte("bad key"))
|
||||
require.NoError(t, err)
|
||||
require.True(t, corrupt)
|
||||
}
|
||||
}
|
||||
|
||||
func newVault(t *testing.T) *vault.Vault {
|
||||
t.Helper()
|
||||
|
||||
s, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key"))
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
return s
|
||||
}
|
||||
13
internal/vault/token.go
Normal file
13
internal/vault/token.go
Normal file
@ -0,0 +1,13 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
)
|
||||
|
||||
// RandomToken is a function that returns a random token.
|
||||
var RandomToken func(size int) ([]byte, error)
|
||||
|
||||
// By default, we use crypto.RandomToken to generate tokens.
|
||||
func init() {
|
||||
RandomToken = crypto.RandomToken
|
||||
}
|
||||
88
internal/vault/types.go
Normal file
88
internal/vault/types.go
Normal file
@ -0,0 +1,88 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
Settings Settings
|
||||
Users []UserData
|
||||
Cookies []byte
|
||||
Certs Certs
|
||||
}
|
||||
|
||||
type Certs struct {
|
||||
Bridge Cert
|
||||
Installed bool
|
||||
}
|
||||
|
||||
type Cert struct {
|
||||
Cert, Key []byte
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
GluonDir string
|
||||
|
||||
IMAPPort int
|
||||
SMTPPort int
|
||||
IMAPSSL bool
|
||||
SMTPSSL bool
|
||||
|
||||
UpdateChannel updater.Channel
|
||||
UpdateRollout float64
|
||||
|
||||
ColorScheme string
|
||||
ProxyAllowed bool
|
||||
ShowAllMail bool
|
||||
Autostart bool
|
||||
AutoUpdate bool
|
||||
|
||||
LastVersion *semver.Version
|
||||
FirstStart bool
|
||||
FirstStartGUI bool
|
||||
}
|
||||
|
||||
// UserData holds information about a single bridge user.
|
||||
// The user may or may not be logged in.
|
||||
type UserData struct {
|
||||
UserID string
|
||||
Username string
|
||||
|
||||
GluonID string
|
||||
GluonKey []byte
|
||||
BridgePass string
|
||||
|
||||
AuthUID string
|
||||
AuthRef string
|
||||
KeyPass []byte
|
||||
|
||||
EventID string
|
||||
HasSync bool
|
||||
}
|
||||
|
||||
func newDefaultSettings(gluonDir string) Settings {
|
||||
return Settings{
|
||||
GluonDir: gluonDir,
|
||||
|
||||
IMAPPort: 1143,
|
||||
SMTPPort: 1025,
|
||||
IMAPSSL: false,
|
||||
SMTPSSL: false,
|
||||
|
||||
UpdateChannel: updater.DefaultUpdateChannel,
|
||||
UpdateRollout: rand.Float64(),
|
||||
|
||||
ColorScheme: "",
|
||||
ProxyAllowed: true,
|
||||
ShowAllMail: true,
|
||||
Autostart: false,
|
||||
AutoUpdate: true,
|
||||
|
||||
LastVersion: semver.MustParse("0.0.0"),
|
||||
FirstStart: true,
|
||||
FirstStartGUI: true,
|
||||
}
|
||||
}
|
||||
91
internal/vault/user.go
Normal file
91
internal/vault/user.go
Normal file
@ -0,0 +1,91 @@
|
||||
package vault
|
||||
|
||||
type User struct {
|
||||
vault *Vault
|
||||
userID string
|
||||
}
|
||||
|
||||
func (user *User) UserID() string {
|
||||
return user.vault.getUser(user.userID).UserID
|
||||
}
|
||||
|
||||
func (user *User) Username() string {
|
||||
return user.vault.getUser(user.userID).Username
|
||||
}
|
||||
|
||||
func (user *User) GluonID() string {
|
||||
return user.vault.getUser(user.userID).GluonID
|
||||
}
|
||||
|
||||
func (user *User) GluonKey() []byte {
|
||||
return user.vault.getUser(user.userID).GluonKey
|
||||
}
|
||||
|
||||
func (user *User) BridgePass() string {
|
||||
return user.vault.getUser(user.userID).BridgePass
|
||||
}
|
||||
|
||||
func (user *User) AuthUID() string {
|
||||
return user.vault.getUser(user.userID).AuthUID
|
||||
}
|
||||
|
||||
func (user *User) AuthRef() string {
|
||||
return user.vault.getUser(user.userID).AuthRef
|
||||
}
|
||||
|
||||
func (user *User) KeyPass() []byte {
|
||||
return user.vault.getUser(user.userID).KeyPass
|
||||
}
|
||||
|
||||
func (user *User) EventID() string {
|
||||
return user.vault.getUser(user.userID).EventID
|
||||
}
|
||||
|
||||
func (user *User) HasSync() bool {
|
||||
return user.vault.getUser(user.userID).HasSync
|
||||
}
|
||||
|
||||
func (user *User) UpdateKeyPass(keyPass []byte) error {
|
||||
return user.vault.modUser(user.userID, func(data *UserData) {
|
||||
data.KeyPass = keyPass
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateAuth updates the auth secrets for the given user.
|
||||
func (user *User) UpdateAuth(authUID, authRef string) error {
|
||||
return user.vault.modUser(user.userID, func(data *UserData) {
|
||||
data.AuthUID = authUID
|
||||
data.AuthRef = authRef
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateGluonData updates the gluon ID and key for the given user.
|
||||
func (user *User) UpdateGluonData(gluonID string, gluonKey []byte) error {
|
||||
return user.vault.modUser(user.userID, func(data *UserData) {
|
||||
data.GluonID = gluonID
|
||||
data.GluonKey = gluonKey
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateEventID updates the event ID for the given user.
|
||||
func (user *User) UpdateEventID(eventID string) error {
|
||||
return user.vault.modUser(user.userID, func(data *UserData) {
|
||||
data.EventID = eventID
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSync updates the sync state for the given user.
|
||||
func (user *User) UpdateSync(hasSync bool) error {
|
||||
return user.vault.modUser(user.userID, func(data *UserData) {
|
||||
data.HasSync = hasSync
|
||||
})
|
||||
}
|
||||
|
||||
// Clear clears the secrets for the given user.
|
||||
func (user *User) Clear() error {
|
||||
return user.vault.modUser(user.userID, func(data *UserData) {
|
||||
data.AuthUID = ""
|
||||
data.AuthRef = ""
|
||||
data.KeyPass = nil
|
||||
})
|
||||
}
|
||||
84
internal/vault/user_test.go
Normal file
84
internal/vault/user_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
package vault_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
// Replace the token generator with a dummy one.
|
||||
vault.RandomToken = func(size int) ([]byte, error) {
|
||||
return []byte("token"), nil
|
||||
}
|
||||
|
||||
// create a new test vault.
|
||||
s := newVault(t)
|
||||
|
||||
// Set auth information for user 1 and 2.
|
||||
user1, err := s.AddUser("userID1", "user1", "authUID1", "authRef1", []byte("keyPass1"))
|
||||
require.NoError(t, err)
|
||||
user2, err := s.AddUser("userID2", "user2", "authUID2", "authRef2", []byte("keyPass2"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set event IDs for user 1 and 2.
|
||||
require.NoError(t, user1.UpdateEventID("eventID1"))
|
||||
require.NoError(t, user2.UpdateEventID("eventID2"))
|
||||
|
||||
// Set sync state for user 1 and 2.
|
||||
require.NoError(t, user1.UpdateSync(true))
|
||||
require.NoError(t, user2.UpdateSync(false))
|
||||
|
||||
// Set gluon data for user 1 and 2.
|
||||
require.NoError(t, user1.UpdateGluonData("gluonID1", []byte("gluonKey1")))
|
||||
require.NoError(t, user2.UpdateGluonData("gluonID2", []byte("gluonKey2")))
|
||||
|
||||
// List available users.
|
||||
require.ElementsMatch(t, []string{"userID1", "userID2"}, s.GetUserIDs())
|
||||
|
||||
// Get auth information for user 1.
|
||||
require.Equal(t, "userID1", user1.UserID())
|
||||
require.Equal(t, "user1", user1.Username())
|
||||
require.Equal(t, "gluonID1", user1.GluonID())
|
||||
require.Equal(t, []byte("gluonKey1"), user1.GluonKey())
|
||||
require.Equal(t, hex.EncodeToString([]byte("token")), user1.BridgePass())
|
||||
require.Equal(t, "authUID1", user1.AuthUID())
|
||||
require.Equal(t, "authRef1", user1.AuthRef())
|
||||
require.Equal(t, []byte("keyPass1"), user1.KeyPass())
|
||||
require.Equal(t, "eventID1", user1.EventID())
|
||||
require.Equal(t, true, user1.HasSync())
|
||||
|
||||
// Get auth information for user 2.
|
||||
require.Equal(t, "userID2", user2.UserID())
|
||||
require.Equal(t, "user2", user2.Username())
|
||||
require.Equal(t, "gluonID2", user2.GluonID())
|
||||
require.Equal(t, []byte("gluonKey2"), user2.GluonKey())
|
||||
require.Equal(t, hex.EncodeToString([]byte("token")), user2.BridgePass())
|
||||
require.Equal(t, "authUID2", user2.AuthUID())
|
||||
require.Equal(t, "authRef2", user2.AuthRef())
|
||||
require.Equal(t, []byte("keyPass2"), user2.KeyPass())
|
||||
require.Equal(t, "eventID2", user2.EventID())
|
||||
require.Equal(t, false, user2.HasSync())
|
||||
|
||||
// Clear the users.
|
||||
require.NoError(t, user1.Clear())
|
||||
require.NoError(t, user2.Clear())
|
||||
|
||||
// Their secrets should now be cleared.
|
||||
require.Equal(t, "", user1.AuthUID())
|
||||
require.Equal(t, "", user1.AuthRef())
|
||||
require.Empty(t, user1.KeyPass())
|
||||
|
||||
// Get auth information for user 2.
|
||||
require.Equal(t, "", user2.AuthUID())
|
||||
require.Equal(t, "", user2.AuthRef())
|
||||
require.Empty(t, user2.KeyPass())
|
||||
|
||||
// Delete auth information for user 1.
|
||||
require.NoError(t, s.DeleteUser("userID1"))
|
||||
|
||||
// List available userIDs. User 1 should be gone.
|
||||
require.ElementsMatch(t, []string{"userID2"}, s.GetUserIDs())
|
||||
}
|
||||
Reference in New Issue
Block a user