From 4e7acd909119bf1add8e4cdd8ca85b8fea7fa582 Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Mon, 20 Mar 2023 14:55:49 +0100 Subject: [PATCH] feat(GODT-2509): Migrate TLS cert from v1/v2 location during upgrade to v3 --- internal/app/migration.go | 27 +++++- internal/app/migration_test.go | 94 ++++++++++++------- .../with_keys/protonmail/bridge/cert.pem | 1 + .../with_keys/protonmail/bridge/key.pem | 1 + .../protonmail/bridge}/prefs.json | 0 .../without_keys/protonmail/bridge/prefs.json | 31 ++++++ internal/vault/certs.go | 8 ++ 7 files changed, 128 insertions(+), 34 deletions(-) create mode 100644 internal/app/testdata/with_keys/protonmail/bridge/cert.pem create mode 100644 internal/app/testdata/with_keys/protonmail/bridge/key.pem rename internal/app/testdata/{ => with_keys/protonmail/bridge}/prefs.json (100%) create mode 100644 internal/app/testdata/without_keys/protonmail/bridge/prefs.json diff --git a/internal/app/migration.go b/internal/app/migration.go index 1124de9b..3534964f 100644 --- a/internal/app/migration.go +++ b/internal/app/migration.go @@ -87,6 +87,11 @@ func migrateOldSettings(v *vault.Vault) error { 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 @@ -94,7 +99,27 @@ func migrateOldSettings(v *vault.Vault) error { return fmt.Errorf("failed to read old prefs file: %w", err) } - return migratePrefsToVault(v, b) + 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 { diff --git a/internal/app/migration_test.go b/internal/app/migration_test.go index a9e128e9..83b6e7aa 100644 --- a/internal/app/migration_test.go +++ b/internal/app/migration_test.go @@ -38,53 +38,44 @@ import ( "github.com/stretchr/testify/require" ) -func TestMigratePrefsToVault(t *testing.T) { +func TestMigratePrefsToVaultWithKeys(t *testing.T) { // Create a new vault. vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key")) require.NoError(t, err) require.False(t, corrupt) // load the old prefs file. - b, err := os.ReadFile(filepath.Join("testdata", "prefs.json")) - require.NoError(t, err) + configDir := filepath.Join("testdata", "with_keys") // Migrate the old prefs file to the new vault. - require.NoError(t, migratePrefsToVault(vault, b)) + require.NoError(t, migrateOldSettingsWithDir(configDir, vault)) - // Check that the IMAP and SMTP prefs are migrated. - require.Equal(t, 2143, vault.GetIMAPPort()) - require.Equal(t, 2025, vault.GetSMTPPort()) - require.True(t, vault.GetSMTPSSL()) + // Check Json Settings + validateJSONPrefs(t, vault) - // Check that the update channel is migrated. - require.True(t, vault.GetAutoUpdate()) - require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel()) - require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout()) + // Check the keys were found and collected. + require.Equal(t, "-----BEGIN CERTIFICATE-----", string(vault.GetBridgeTLSCert())) + require.Equal(t, "-----BEGIN RSA PRIVATE KEY-----", string(vault.GetBridgeTLSKey())) +} - // Check that the app settings have been migrated. - require.False(t, vault.GetFirstStart()) - require.Equal(t, "blablabla", vault.GetColorScheme()) - require.Equal(t, "2.3.0+git", vault.GetLastVersion().String()) - require.True(t, vault.GetAutostart()) - - // Check that the other app settings have been migrated. - require.Equal(t, 16, vault.SyncWorkers()) - require.Equal(t, 16, vault.SyncAttPool()) - require.False(t, vault.GetProxyAllowed()) - require.False(t, vault.GetShowAllMail()) - - // Check that the cookies have been migrated. - jar, err := cookiejar.New(nil) +func TestMigratePrefsToVaultWithoutKeys(t *testing.T) { + // Create a new vault. + vault, corrupt, err := vault.New(t.TempDir(), t.TempDir(), []byte("my secret key")) require.NoError(t, err) + require.False(t, corrupt) - cookies, err := cookies.NewCookieJar(jar, vault) - require.NoError(t, err) + // load the old prefs file. + configDir := filepath.Join("testdata", "without_keys") - url, err := url.Parse("https://api.protonmail.ch") - require.NoError(t, err) + // Migrate the old prefs file to the new vault. + require.NoError(t, migrateOldSettingsWithDir(configDir, vault)) - // There should be a cookie for the API. - require.NotEmpty(t, cookies.Cookies(url)) + // Check Json Settings + validateJSONPrefs(t, vault) + + // Check the keys were found and collected. + require.NotEqual(t, []byte("-----BEGIN CERTIFICATE-----"), vault.GetBridgeTLSCert()) + require.NotEqual(t, []byte("-----BEGIN RSA PRIVATE KEY-----"), vault.GetBridgeTLSKey()) } func TestKeychainMigration(t *testing.T) { @@ -101,7 +92,7 @@ func TestKeychainMigration(t *testing.T) { oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge") require.NoError(t, os.MkdirAll(oldCacheDir, 0o700)) - oldPrefs, err := os.ReadFile(filepath.Join("testdata", "prefs.json")) + oldPrefs, err := os.ReadFile(filepath.Join("testdata", "without_keys", "protonmail", "bridge", "prefs.json")) require.NoError(t, err) require.NoError(t, os.WriteFile( @@ -196,3 +187,40 @@ func TestUserMigration(t *testing.T) { require.Equal(t, vault.CombinedMode, u.AddressMode()) })) } + +func validateJSONPrefs(t *testing.T, vault *vault.Vault) { + // Check that the IMAP and SMTP prefs are migrated. + require.Equal(t, 2143, vault.GetIMAPPort()) + require.Equal(t, 2025, vault.GetSMTPPort()) + require.True(t, vault.GetSMTPSSL()) + + // Check that the update channel is migrated. + require.True(t, vault.GetAutoUpdate()) + require.Equal(t, updater.EarlyChannel, vault.GetUpdateChannel()) + require.Equal(t, 0.4849529004202015, vault.GetUpdateRollout()) + + // Check that the app settings have been migrated. + require.False(t, vault.GetFirstStart()) + require.Equal(t, "blablabla", vault.GetColorScheme()) + require.Equal(t, "2.3.0+git", vault.GetLastVersion().String()) + require.True(t, vault.GetAutostart()) + + // Check that the other app settings have been migrated. + require.Equal(t, 16, vault.SyncWorkers()) + require.Equal(t, 16, vault.SyncAttPool()) + require.False(t, vault.GetProxyAllowed()) + require.False(t, vault.GetShowAllMail()) + + // Check that the cookies have been migrated. + jar, err := cookiejar.New(nil) + require.NoError(t, err) + + cookies, err := cookies.NewCookieJar(jar, vault) + require.NoError(t, err) + + url, err := url.Parse("https://api.protonmail.ch") + require.NoError(t, err) + + // There should be a cookie for the API. + require.NotEmpty(t, cookies.Cookies(url)) +} diff --git a/internal/app/testdata/with_keys/protonmail/bridge/cert.pem b/internal/app/testdata/with_keys/protonmail/bridge/cert.pem new file mode 100644 index 00000000..75f3c322 --- /dev/null +++ b/internal/app/testdata/with_keys/protonmail/bridge/cert.pem @@ -0,0 +1 @@ +-----BEGIN CERTIFICATE----- \ No newline at end of file diff --git a/internal/app/testdata/with_keys/protonmail/bridge/key.pem b/internal/app/testdata/with_keys/protonmail/bridge/key.pem new file mode 100644 index 00000000..f05debd2 --- /dev/null +++ b/internal/app/testdata/with_keys/protonmail/bridge/key.pem @@ -0,0 +1 @@ +-----BEGIN RSA PRIVATE KEY----- \ No newline at end of file diff --git a/internal/app/testdata/prefs.json b/internal/app/testdata/with_keys/protonmail/bridge/prefs.json similarity index 100% rename from internal/app/testdata/prefs.json rename to internal/app/testdata/with_keys/protonmail/bridge/prefs.json diff --git a/internal/app/testdata/without_keys/protonmail/bridge/prefs.json b/internal/app/testdata/without_keys/protonmail/bridge/prefs.json new file mode 100644 index 00000000..b6a16d46 --- /dev/null +++ b/internal/app/testdata/without_keys/protonmail/bridge/prefs.json @@ -0,0 +1,31 @@ +{ + "allow_proxy": "false", + "attachment_workers": "16", + "autostart": "true", + "autoupdate": "true", + "cache_compression": "true", + "cache_concurrent_read": "16", + "cache_concurrent_write": "16", + "cache_enabled": "true", + "cache_location": "/home/user/.config/protonmail/bridge/cache/c11/messages", + "cache_min_free_abs": "250000000", + "cache_min_free_rat": "", + "color_scheme": "blablabla", + "cookies": "{\"https://api.protonmail.ch\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.ch\",\"Expires\":\"2023-02-19T00:20:40.269424437+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=blablablablablablablablabla; Domain=protonmail.ch; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"default\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:40.269428627+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=default; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}],\"https://protonmail.com\":[{\"Name\":\"Session-Id\",\"Value\":\"blablablablablablablablabla\",\"Path\":\"/\",\"Domain\":\"protonmail.com\",\"Expires\":\"2023-02-19T00:20:18.315084712+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":true,\"SameSite\":0,\"Raw\":\"Session-Id=Y3q2Mh-ClvqL6LWeYdfyPgAAABI; Domain=protonmail.com; Path=/; HttpOnly; Secure; Max-Age=7776000\",\"Unparsed\":null},{\"Name\":\"Tag\",\"Value\":\"redirect\",\"Path\":\"/\",\"Domain\":\"\",\"Expires\":\"2023-02-19T00:20:18.315087646+01:00\",\"RawExpires\":\"\",\"MaxAge\":7776000,\"Secure\":true,\"HttpOnly\":false,\"SameSite\":0,\"Raw\":\"Tag=redirect; Path=/; Secure; Max-Age=7776000\",\"Unparsed\":null}]}", + "fetch_workers": "16", + "first_time_start": "false", + "first_time_start_gui": "true", + "imap_workers": "16", + "is_all_mail_visible": "false", + "last_heartbeat": "325", + "last_used_version": "2.3.0+git", + "preferred_keychain": "secret-service", + "rebranding_migrated": "true", + "report_outgoing_email_without_encryption": "false", + "rollout": "0.4849529004202015", + "user_port_api": "1042", + "update_channel": "early", + "user_port_imap": "2143", + "user_port_smtp": "2025", + "user_ssl_smtp": "true" +} \ No newline at end of file diff --git a/internal/vault/certs.go b/internal/vault/certs.go index 77086f9b..dee02017 100644 --- a/internal/vault/certs.go +++ b/internal/vault/certs.go @@ -25,6 +25,14 @@ func (vault *Vault) GetBridgeTLSKey() []byte { return vault.get().Certs.Bridge.Key } +// SetBridgeTLSCertKey sets the path to PEM-encoded certificates for the bridge. +func (vault *Vault) SetBridgeTLSCertKey(cert, key []byte) error { + return vault.mod(func(data *Data) { + data.Certs.Bridge.Cert = cert + data.Certs.Bridge.Key = key + }) +} + func (vault *Vault) GetCertsInstalled() bool { return vault.get().Certs.Installed }