// 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 . package app import ( "encoding/json" "fmt" "io/fs" "os" "path/filepath" "strconv" "strings" "time" "github.com/Masterminds/semver/v3" "github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials" "github.com/ProtonMail/proton-bridge/v3/internal/locations" "github.com/ProtonMail/proton-bridge/v3/internal/logging" "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" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) // nolint:gosec 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 keychain helper file is already there do not migrate again. if keychainName, _ := vault.GetHelper(settings); keychainName != "" { return nil } configDir, err := os.UserConfigDir() if err != nil { return fmt.Errorf("failed to get user config dir: %w", err) } b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json")) if errors.Is(err, fs.ErrNotExist) { return nil } else if err != nil { return fmt.Errorf("failed to read old prefs file: %w", err) } var prefs struct { Helper string `json:"preferred_keychain"` } if err := json.Unmarshal(b, &prefs); err != nil { return fmt.Errorf("failed to unmarshal old prefs file: %w", err) } return vault.SetHelper(settings, prefs.Helper) } // nolint:gosec func migrateOldSettings(v *vault.Vault) error { logrus.Info("Migrating settings") configDir, err := os.UserConfigDir() if err != nil { return fmt.Errorf("failed to get user config dir: %w", err) } return migrateOldSettingsWithDir(configDir, v) } // nolint:gosec func migrateOldSettingsWithDir(configDir string, v *vault.Vault) error { b, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "prefs.json")) if errors.Is(err, fs.ErrNotExist) { return nil } else if err != nil { return fmt.Errorf("failed to read old prefs file: %w", err) } if err := migratePrefsToVault(v, b); err != nil { return fmt.Errorf("failed to migrate prefs to vault: %w", err) } logrus.Info("Migrating TLS certificate") certPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "cert.pem")) if errors.Is(err, fs.ErrNotExist) { return nil } else if err != nil { return fmt.Errorf("failed to read old cert file: %w", err) } keyPEM, err := os.ReadFile(filepath.Join(configDir, "protonmail", "bridge", "key.pem")) if errors.Is(err, fs.ErrNotExist) { return nil } else if err != nil { return fmt.Errorf("failed to read old key file: %w", err) } return v.SetBridgeTLSCertKey(certPEM, keyPEM) } func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error { logrus.Info("Migrating accounts") settings, err := locations.ProvideSettingsPath() if err != nil { return fmt.Errorf("failed to get settings path: %w", err) } helper, err := vault.GetHelper(settings) if err != nil { return fmt.Errorf("failed to get helper: %w", err) } keychain, err := keychain.NewKeychain(helper, "bridge") if err != nil { return fmt.Errorf("failed to create keychain: %w", err) } store := credentials.NewStore(keychain) users, err := store.List() if err != nil { return fmt.Errorf("failed to create credentials store: %w", err) } var migrationErrors error for _, userID := range users { if err := migrateOldAccount(userID, store, v); err != nil { migrationErrors = multierror.Append(migrationErrors, err) } } return migrationErrors } func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault) error { l := logrus.WithField("userID", userID) l.Info("Migrating account") 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) } var primaryEmail string if len(creds.EmailList()) > 0 { primaryEmail = creds.EmailList()[0] } user, err := v.AddUser(creds.UserID, creds.Name, primaryEmail, authUID, authRef, creds.MailboxPassword) if err != nil { return fmt.Errorf("failed to add user %q: %w", userID, err) } l = l.WithField("username", logging.Sensitive(user.Username())) l.Info("Migrated account with random bridge password") defer func() { if err := user.Close(); err != nil { 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 for user %q: %w", userID, err) } l = l.WithField("password", logging.Sensitive(string(algo.B64RawEncode(dec)))) l.Info("Migrated existing bridge password") 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) } } return nil } // nolint:funlen func migratePrefsToVault(vault *vault.Vault, b []byte) error { var prefs struct { IMAPPort int `json:"user_port_imap,,string"` SMTPPort int `json:"user_port_smtp,,string"` SMTPSSL bool `json:"user_ssl_smtp,,string"` AutoUpdate bool `json:"autoupdate,,string"` UpdateChannel updater.Channel `json:"update_channel"` UpdateRollout float64 `json:"rollout,,string"` FirstStart bool `json:"first_time_start,,string"` ColorScheme string `json:"color_scheme"` LastVersion *semver.Version `json:"last_used_version"` Autostart bool `json:"autostart,,string"` AllowProxy bool `json:"allow_proxy,,string"` FetchWorkers int `json:"fetch_workers,,string"` AttachmentWorkers int `json:"attachment_workers,,string"` ShowAllMail bool `json:"is_all_mail_visible,,string"` Cookies string `json:"cookies"` } if err := json.Unmarshal(b, &prefs); err != nil { return fmt.Errorf("failed to unmarshal old prefs file: %w", err) } var errs error if err := vault.SetIMAPPort(prefs.IMAPPort); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate IMAP port: %w", err)) } if err := vault.SetSMTPPort(prefs.SMTPPort); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate SMTP port: %w", err)) } if err := vault.SetSMTPSSL(prefs.SMTPSSL); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate SMTP SSL: %w", err)) } if err := vault.SetAutoUpdate(prefs.AutoUpdate); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate auto update: %w", err)) } if err := vault.SetUpdateChannel(prefs.UpdateChannel); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate update channel: %w", err)) } if err := vault.SetUpdateRollout(prefs.UpdateRollout); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate rollout: %w", err)) } if err := vault.SetFirstStart(prefs.FirstStart); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate first start: %w", err)) } if err := vault.SetColorScheme(prefs.ColorScheme); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate color scheme: %w", err)) } if err := vault.SetLastVersion(prefs.LastVersion); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate last version: %w", err)) } if err := vault.SetAutostart(prefs.Autostart); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate autostart: %w", err)) } if err := vault.SetProxyAllowed(prefs.AllowProxy); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate allow proxy: %w", err)) } if err := vault.SetShowAllMail(prefs.ShowAllMail); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate show all mail: %w", err)) } if err := vault.SetSyncWorkers(prefs.FetchWorkers); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync workers: %w", err)) } if err := vault.SetSyncAttPool(prefs.AttachmentWorkers); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate sync attachment pool: %w", err)) } if err := vault.SetCookies([]byte(prefs.Cookies)); err != nil { errs = multierror.Append(errs, fmt.Errorf("failed to migrate cookies: %w", err)) } return errs } func migrateOldVersions() (allErrors error) { cacheDir, cacheError := os.UserCacheDir() if cacheError != nil { allErrors = multierror.Append(allErrors, errors.Wrap(cacheError, "cannot get os cache")) return // not need to continue for now (with more migrations might be still ok to continue) } if err := killV2AppAndRemoveV2LockFiles(filepath.Join(cacheDir, "protonmail", "bridge", "bridge.lock")); err != nil { allErrors = multierror.Append(allErrors, errors.Wrap(err, "cannot migrate lockfiles")) } return } func killV2AppAndRemoveV2LockFiles(lockFilePathV2 string) error { l := logrus.WithField("path", lockFilePathV2) if _, err := os.Stat(lockFilePathV2); os.IsNotExist(err) { l.Debug("no v2 lockfile") return nil } lock, err := singleinstance.CreateLockFile(lockFilePathV2) if err == nil { l.Debug("no other v2 instance is running") if errClose := lock.Close(); errClose != nil { l.WithError(errClose).Error("Cannot close lock file") } return os.Remove(lockFilePathV2) } // The other instance is an older version, so we should kill it. pid, err := getPID(lockFilePathV2) if err != nil { return errors.Wrap(err, "cannot get v2 pid") } if err := killPID(pid); err != nil { return errors.Wrapf(err, "cannot kill v2 app (PID %d)", pid) } // Need to wait some time to release file lock time.Sleep(time.Second) return nil } func getPID(lockFilePath string) (int, error) { file, err := os.Open(filepath.Clean(lockFilePath)) if err != nil { return 0, err } defer func() { _ = file.Close() }() rawPID := make([]byte, 10) // PID is probably up to 7 digits long, 10 should be enough n, err := file.Read(rawPID) if err != nil { return 0, err } return strconv.Atoi(strings.TrimSpace(string(rawPID[:n]))) } func killPID(pid int) error { p, err := os.FindProcess(pid) if err != nil { return err } return p.Kill() }