/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN 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. * * ProtonVPN 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 ProtonVPN. If not, see . */ using ProtonVPN.Client.Logic.Auth.Contracts.Models; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Client.Settings.Contracts.Enums; using ProtonVPN.Client.Settings.Contracts.Migrations; using ProtonVPN.Client.Settings.Contracts.Models; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Crypto.Contracts.Extensions; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; using ProtonVPN.Serialization.Contracts.Json; namespace ProtonVPN.Client.Settings.Migrations; public class UserSettingsMigrator : IUserSettingsMigrator { private readonly ISettings _settings; private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; private readonly ISettingsCorrector _settingsCorrector; private readonly IProfilesMigrator _profilesMigrator; public UserSettingsMigrator(ISettings settings, ILogger logger, IJsonSerializer jsonSerializer, ISettingsCorrector settingsCorrector, IProfilesMigrator profilesMigrator) { _settings = settings; _logger = logger; _jsonSerializer = jsonSerializer; _settingsCorrector = settingsCorrector; _profilesMigrator = profilesMigrator; } public void Migrate() { string? username = _settings.Username; if (_settings.IsUserSettingsMigrationDone || string.IsNullOrWhiteSpace(username)) { return; } Dictionary>? settingsByUsername = _settings.LegacySettingsByUsername; if (settingsByUsername is not null && settingsByUsername.Count > 0) { KeyValuePair>? userSettingsPair = FindSettingsByUsername(username, settingsByUsername); if (userSettingsPair is null) { _logger.Info($"No user settings to migrate for username '{username}'."); } else { _logger.Info($"Migrating user settings for username '{username}'."); MigrateUserSettings(userSettingsPair.Value.Value); _logger.Info($"Finished migrating user settings for username '{username}'. Removing previous settings."); RemoveMigratedUserSettings(settingsByUsername, userSettingsPair.Value.Key); _logger.Info($"Removed previous settings."); } } else { _logger.Info("No user settings to migrate."); } _settings.IsUserSettingsMigrationDone = true; _settingsCorrector.Correct(); } private void RemoveMigratedUserSettings(Dictionary> settingsByUsername, string username) { if (!settingsByUsername.Remove(username)) { _logger.Warn("The old user settings were not removed."); } if (settingsByUsername.Count > 0) { _logger.Info($"{settingsByUsername.Count} user settings left to migrate."); _settings.LegacySettingsByUsername = settingsByUsername; } else { _logger.Info("No more user settings left to migrate."); _settings.LegacySettingsByUsername = null; } } private KeyValuePair>? FindSettingsByUsername(string username, Dictionary> settingsByUsername) { IEnumerable usernameComparisonAttempts = GetUsernameComparisonAttempt(username); foreach (string usernameComparisonAttempt in usernameComparisonAttempts) { if (settingsByUsername.TryGetValue(usernameComparisonAttempt, out Dictionary? userSettings) && userSettings is not null) { return new KeyValuePair>(usernameComparisonAttempt, userSettings); } } return null; } private IEnumerable GetUsernameComparisonAttempt(string username) { yield return username.ToLowerInvariant(); yield return username.ToLowerInvariant().Replace(" ", "."); yield return username.ToLowerInvariant().Replace(".", " "); int indexOfEmailAt = username.IndexOf('@'); if (indexOfEmailAt >= 0) { string usernameWithoutEmailSuffix = username.Substring(0, indexOfEmailAt); yield return usernameWithoutEmailSuffix.ToLowerInvariant(); yield return usernameWithoutEmailSuffix.ToLowerInvariant().Replace(".", " "); } } private void MigrateUserSettings(Dictionary userSettings) { MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsNotificationEnabled), val => { _settings.IsNotificationEnabled = val; }); MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsVpnAcceleratorEnabled), val => { _settings.IsVpnAcceleratorEnabled = val; }); MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsIpv6LeakProtectionEnabled), val => { _settings.IsIpv6LeakProtectionEnabled = val; }); MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsNetShieldEnabled), val => { _settings.IsNetShieldEnabled = val; }); MigrateNetShieldMode(userSettings); MigrateConnectionKeyPair(userSettings); MigrateConnectionCertificate(userSettings); MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsAutoConnectEnabled), val => { _settings.IsAutoConnectEnabled = val; }); MigrateNatType(userSettings); MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsCustomDnsServersEnabled), val => { _settings.IsCustomDnsServersEnabled = val; }); MigrateJsonUserSetting>(userSettings, nameof(IUserSettings.CustomDnsServersList), val => { _settings.CustomDnsServersList = val; }); MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsPortForwardingNotificationEnabled), val => { _settings.IsPortForwardingNotificationEnabled = val; }); MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsPortForwardingEnabled), val => { _settings.IsPortForwardingEnabled = val; }); MigrateBoolUserSetting(userSettings, nameof(IUserSettings.IsSplitTunnelingEnabled), val => { _settings.IsSplitTunnelingEnabled = val; }); MigrateJsonUserSetting>(userSettings, nameof(IUserSettings.SplitTunnelingInverseAppsList), val => { _settings.SplitTunnelingInverseAppsList = val; }); MigrateJsonUserSetting>(userSettings, nameof(IUserSettings.SplitTunnelingStandardAppsList), val => { _settings.SplitTunnelingStandardAppsList = val; }); MigrateJsonUserSetting>(userSettings, nameof(IUserSettings.SplitTunnelingInverseIpAddressesList), val => { _settings.SplitTunnelingInverseIpAddressesList = val; }); MigrateJsonUserSetting>(userSettings, nameof(IUserSettings.SplitTunnelingStandardIpAddressesList), val => { _settings.SplitTunnelingStandardIpAddressesList = val; }); MigrateOpenVpnAdapter(userSettings); MigrateVpnProtocol(userSettings); MigrateProfilesAndQuickConnectProfileId(userSettings); ConfigureWelcomeOverlays(); } private void MigrateConnectionKeyPair(Dictionary userSettings) { if (userSettings.TryGetValue(nameof(IUserSettings.ConnectionKeyPair), out string? rawSettingValue) && !string.IsNullOrWhiteSpace(rawSettingValue)) { try { ConnectionAsymmetricKeyPair? deserializedValue = _jsonSerializer.DeserializeFromString(rawSettingValue.Decrypt()); if (deserializedValue is not null) { _settings.ConnectionKeyPair = deserializedValue; } } catch (Exception e) { _logger.Error($"Error while migrating connection key pairs.", e); } } } private void MigrateConnectionCertificate(Dictionary userSettings) { if (_settings.ConnectionKeyPair is null) { return; // If no connection key pair exists, the certificate is worthless as both are necessary in the connection process } if (userSettings.TryGetValue(nameof(IUserSettings.ConnectionCertificate), out string? rawSettingValue) && !string.IsNullOrWhiteSpace(rawSettingValue)) { try { _settings.ConnectionCertificate = new ConnectionCertificate() { Pem = rawSettingValue.Decrypt(), RequestUtcDate = DateTimeOffset.MinValue, RefreshUtcDate = DateTimeOffset.MinValue, ExpirationUtcDate = DateTimeOffset.MinValue, }; } catch (Exception e) { _logger.Error($"Error while migrating connection certificate.", e); } } } private void MigrateBoolUserSetting(Dictionary userSettings, string settingName, Action setter) { if (userSettings.TryGetValue(settingName, out string? rawSettingValue) && rawSettingValue is not null) { if (bool.TryParse(rawSettingValue, out bool parseResult)) { setter(parseResult); } } } private void MigrateJsonUserSetting(Dictionary userSettings, string settingName, Action setter) where T : class { if (userSettings.TryGetValue(settingName, out string? rawSettingValue) && rawSettingValue is not null) { try { T? deserializedValue = _jsonSerializer.DeserializeFromString(rawSettingValue); if (deserializedValue is not null) { setter(deserializedValue); } } catch (Exception e) { _logger.Error($"Error while migrating user settings.", e); } } } private void MigrateNatType(Dictionary userSettings) { if (userSettings.TryGetValue(nameof(IUserSettings.NatType), out string? rawSettingValue) && rawSettingValue is not null) { _settings.NatType = rawSettingValue switch { "true" => NatType.Moderate, _ => DefaultSettings.NatType, }; } } private void MigrateOpenVpnAdapter(Dictionary userSettings) { if (userSettings.TryGetValue(nameof(IUserSettings.OpenVpnAdapter), out string? rawSettingValue) && rawSettingValue is not null && Enum.TryParse(rawSettingValue, out OpenVpnAdapter result)) { _settings.OpenVpnAdapter = result; } } private void MigrateVpnProtocol(Dictionary userSettings) { if (userSettings.TryGetValue(nameof(IUserSettings.VpnProtocol), out string? rawSettingValue) && rawSettingValue is not null && Enum.TryParse(rawSettingValue, out VpnProtocol result)) { _settings.VpnProtocol = result; } } private void MigrateNetShieldMode(Dictionary userSettings) { if (userSettings.TryGetValue(nameof(IUserSettings.NetShieldMode), out string? rawSettingValue) && rawSettingValue is not null && Enum.TryParse(rawSettingValue, out NetShieldMode result)) { _settings.NetShieldMode = result; } } private void MigrateProfilesAndQuickConnectProfileId(Dictionary userSettings) { try { _logger.Info("Migrating profiles."); userSettings.TryGetValue(GlobalSettingsMigrator.QUICK_CONNECT_PROFILE_ID_SETTING_KEY, out string? quickConnectProfileId); userSettings.TryGetValue(GlobalSettingsMigrator.PROFILES_SETTING_KEY, out string? rawProfiles); List legacyProfiles = rawProfiles is not null && _jsonSerializer.DeserializeFromString?>(rawProfiles) is List deserializedProfiles ? deserializedProfiles : []; _profilesMigrator.Migrate(legacyProfiles, quickConnectProfileId); } catch (Exception e) { _logger.Error($"Error while migrating profiles.", e); } } private void ConfigureWelcomeOverlays() { _settings.WasWelcomeOverlayDisplayed = true; _settings.WasWelcomeB2BOverlayDisplayed = true; _settings.LastSeenWhatsNewOverlayVersion = 0; } }