/* * 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 System.Xml; 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.Extensions; using ProtonVPN.Common.Core.Networking; using ProtonVPN.Configurations.Contracts; 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 GlobalSettingsMigrator : IGlobalSettingsMigrator { public const string QUICK_CONNECT_PROFILE_ID_SETTING_KEY = "LegacyQuickConnectProfileId"; public const string PROFILES_SETTING_KEY = "LegacyProfiles"; // ISettings is not used to enforce not writing any user settings private readonly IGlobalSettings _globalSettings; private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly IJsonSerializer _jsonSerializer; private readonly Lazy _xmlDocument; public GlobalSettingsMigrator( IGlobalSettings globalSettings, ILogger logger, IConfiguration configuration, IJsonSerializer jsonSerializer) { _globalSettings = globalSettings; _logger = logger; _configuration = configuration; _jsonSerializer = jsonSerializer; _xmlDocument = new Lazy(() => new XmlDocument()); } public void Migrate() { if (_globalSettings.IsGlobalSettingsMigrationDone) { return; } string? legacyUserConfigFilePath = GetLegacyUserConfigFilePath(); if (legacyUserConfigFilePath is null || !File.Exists(legacyUserConfigFilePath)) { _logger.Info($"The old settings file doesn't exist ({legacyUserConfigFilePath})"); return; } try { _logger.Info($"Migrating the settings file ({legacyUserConfigFilePath})"); MigrateSettingsFile(legacyUserConfigFilePath); if (Directory.Exists(_configuration.LegacyAppLocalData)) { _logger.Info($"Deleting folder ({_configuration.LegacyAppLocalData})"); Directory.Delete(_configuration.LegacyAppLocalData, true); } } catch (Exception e) { _logger.Error("Failed to migrate global settings.", e); } } public string? GetLegacyUserConfigFilePath() { string appDataFolderPath = _configuration.LegacyAppLocalData; if (!Directory.Exists(appDataFolderPath)) { return null; } // v3 string? latestVersionFolderPath = GetOldUserConfigPathByFolderPrefix(appDataFolderPath, "ProtonVPN_Url_"); if (string.IsNullOrEmpty(latestVersionFolderPath)) { // v2 latestVersionFolderPath = GetOldUserConfigPathByFolderPrefix(appDataFolderPath, "ProtonVPN.exe_Url_"); } return string.IsNullOrEmpty(latestVersionFolderPath) ? null : Path.Combine(latestVersionFolderPath, "user.config"); } public string? GetOldUserConfigPathByFolderPrefix(string appDataFolderPath, string prefix) { DateTime latestVersionTime = DateTime.MinValue; string? latestVersionFolderPath = null; foreach (string folderPath in Directory.GetDirectories(appDataFolderPath)) { string? folderName = Path.GetFileName(folderPath); if (folderName != null && folderName.StartsWith(prefix)) { foreach (string versionFolderPath in Directory.GetDirectories(folderPath)) { DateTime versionFolderLastWriteTime = Directory.GetLastWriteTime(versionFolderPath); if (versionFolderLastWriteTime > latestVersionTime) { latestVersionTime = versionFolderLastWriteTime; latestVersionFolderPath = versionFolderPath; } } } } return latestVersionFolderPath; } private void MigrateSettingsFile(string filePath) { _xmlDocument.Value.Load(filePath); List usernames = GetUsernames(); Dictionary> userSettingsByUsername = new(); foreach (string username in usernames) { userSettingsByUsername.Add(username.ToLowerInvariant(), MigrateUser(username)); _logger.Info($"Migrated user '{username}' to temporary global setting."); } _globalSettings.LegacySettingsByUsername = userSettingsByUsername is not null && userSettingsByUsername.Count > 0 ? userSettingsByUsername : null; MigrateGlobalSettings(); _logger.Info("Global migration completed."); _globalSettings.IsGlobalSettingsMigrationDone = true; } private List GetUsernames() { string? jsonUserAccessTokens = GetSettingValue("UserAccessToken"); if (!string.IsNullOrEmpty(jsonUserAccessTokens)) { List? userAccessTokens = _jsonSerializer.DeserializeFromString>(jsonUserAccessTokens); if (userAccessTokens is not null) { return userAccessTokens .Where(uat => !string.IsNullOrEmpty(uat.User)) // Ensure the user with access token is the last, so it remains as the currently logged in user .OrderBy(uat => !string.IsNullOrEmpty(uat.Value?.Decrypt())) .Select(uat => uat.User!.ToLower()) .ToList(); } } return []; } private string? GetSettingValue(string name) { string xPath = $"/configuration/userSettings/ProtonVPN.Properties.Settings/setting[@name='{name}']/value"; return _xmlDocument.Value.SelectSingleNode(xPath)?.InnerText; } private Dictionary MigrateUser(string username) { Dictionary userSettings = new() { { nameof(IUserSettings.IsNotificationEnabled), GetSettingValue("ShowNotifications") }, { nameof(IUserSettings.IsVpnAcceleratorEnabled), GetSettingValue("VpnAcceleratorEnabled") }, { nameof(IUserSettings.IsIpv6LeakProtectionEnabled), GetSettingValue("Ipv6LeakProtection") }, { nameof(IUserSettings.IsNetShieldEnabled), GetUserSetting(username, "UserNetShieldEnabled") }, { nameof(IUserSettings.NetShieldMode), MigrateNetShieldMode(username) }, { nameof(IUserSettings.ConnectionKeyPair), GetConnectionKeyPair(username) }, { nameof(IUserSettings.ConnectionCertificate), GetConnectionCertificate(username) }, { nameof(IUserSettings.IsAutoConnectEnabled), GetSettingValue("ConnectOnAppStart") }, { nameof(IUserSettings.NatType), GetUserSetting(username, "UserModerateNat") }, { nameof(IUserSettings.IsCustomDnsServersEnabled), GetUserSetting(username, "UserCustomDnsEnabled") }, { nameof(IUserSettings.CustomDnsServersList), MigrateCustomDns(username) }, { nameof(IUserSettings.IsPortForwardingNotificationEnabled), GetSettingValue("PortForwardingNotificationsEnabled") }, { nameof(IUserSettings.IsPortForwardingEnabled), GetUserSetting(username, "UserPortForwardingEnabled") }, { nameof(IUserSettings.IsSplitTunnelingEnabled), GetSettingValue("SplitTunnelingEnabled") }, { nameof(IUserSettings.SplitTunnelingInverseAppsList), MigrateSplitTunnelingApps("SplitTunnelingAllowApps") }, { nameof(IUserSettings.SplitTunnelingStandardAppsList), MigrateSplitTunnelingApps("SplitTunnelingBlockApps") }, { nameof(IUserSettings.SplitTunnelingInverseIpAddressesList), MigrateSplitTunnelingIps("SplitTunnelIncludeIps") }, { nameof(IUserSettings.SplitTunnelingStandardIpAddressesList), MigrateSplitTunnelingIps("SplitTunnelExcludeIps") }, { nameof(IUserSettings.OpenVpnAdapter), MigrateOpenVpnNetworkAdapter() }, { nameof(IUserSettings.VpnProtocol), MigrateVpnProtocol() }, { QUICK_CONNECT_PROFILE_ID_SETTING_KEY, GetUserSetting(username, "UserQuickConnect") }, { PROFILES_SETTING_KEY, MigrateProfilesForLaterMigration(username) }, }; MigrateUserAuthData(username); return userSettings; } private string? GetUserSetting(string username, string setting) { return GetUserSetting(username, setting)?.Value; } private T? GetUserSetting(string username, string setting) where T : LegacyUserSettingBase { try { string? userSetting = GetSettingValue(setting); if (string.IsNullOrEmpty(userSetting)) { return default; } return _jsonSerializer.DeserializeFromString>(userSetting)? .Where(s => s.User != null && s.User.EqualsIgnoringCase(username)) .FirstOrDefault(); } catch { return default; } } private string? GetConnectionKeyPair(string username) { string? publicKey = GetUserSetting(username, "UserAuthenticationPublicKey")?.Decrypt(); string? secretKey = GetUserSetting(username, "UserAuthenticationSecretKey")?.Decrypt(); if (publicKey is null || secretKey is null) { return null; } ConnectionAsymmetricKeyPair connectionKeyPair = new() { PublicKey = publicKey, SecretKey = secretKey, }; return _jsonSerializer.SerializeToString(connectionKeyPair).Encrypt(); } private string? GetConnectionCertificate(string username) { try { string? userAuthenticationCertificatePem = GetUserSetting(username, "UserAuthenticationCertificatePem"); return string.IsNullOrWhiteSpace(userAuthenticationCertificatePem) ? null : userAuthenticationCertificatePem; } catch { return null; } } private string? MigrateCustomDns(string username) { try { LegacyUserCustomDnsSetting? customDnsIpsJson = GetUserSetting(username, "UserCustomDnsIps"); if (customDnsIpsJson is null || customDnsIpsJson.Value?.Count == 0) { return null; } List? customDnsServersList = customDnsIpsJson.Value? .Where(lsip => !string.IsNullOrEmpty(lsip.Ip)) .Select(lsip => new CustomDnsServer(lsip.Ip!, lsip.Enabled)) .ToList(); return customDnsServersList is null ? null : _jsonSerializer.SerializeToString(customDnsServersList); } catch { return null; } } private string? MigrateSplitTunnelingApps(string oldSplitTunnelingAppListName) { try { string? splitTunnelingAllowAppsJson = GetSettingValue(oldSplitTunnelingAppListName); if (string.IsNullOrEmpty(splitTunnelingAllowAppsJson)) { return null; } List? splitTunnelingAllowApps = _jsonSerializer.DeserializeFromString>(splitTunnelingAllowAppsJson); if (splitTunnelingAllowApps is null) { return null; } List? splitTunnelingApps = splitTunnelingAllowApps? .Where(sta => !string.IsNullOrEmpty(sta.Path)) .Select(sta => new SplitTunnelingApp(sta.Path!, sta.Enabled)) .ToList(); return splitTunnelingApps is null ? null : _jsonSerializer.SerializeToString(splitTunnelingApps); } catch { return null; } } private string? MigrateSplitTunnelingIps(string oldSplitTunnelingIpsListName) { try { string? splitTunnelingAllowIpsJson = GetSettingValue(oldSplitTunnelingIpsListName); if (string.IsNullOrEmpty(splitTunnelingAllowIpsJson)) { return null; } List? splitTunnelingAllowIps = _jsonSerializer.DeserializeFromString>(splitTunnelingAllowIpsJson); if (splitTunnelingAllowIps is null) { return null; } List? splitTunnelingIpAddresses = splitTunnelingAllowIps? .Where(sti => !string.IsNullOrEmpty(sti.Ip)) .Select(sti => new SplitTunnelingIpAddress(sti.Ip!, sti.Enabled)) .ToList(); return splitTunnelingIpAddresses is null ? null : _jsonSerializer.SerializeToString(splitTunnelingIpAddresses); } catch { return null; } } private string? MigrateOpenVpnNetworkAdapter() { return GetSettingValue("NetworkAdapterType") switch { "0" => nameof(OpenVpnAdapter.Tap), "1" => nameof(OpenVpnAdapter.Tun), _ => nameof(OpenVpnAdapter.Tun), }; } private string? MigrateVpnProtocol() { return GetSettingValue("OvpnProtocol") switch { "auto" => nameof(VpnProtocol.Smart), "udp" => nameof(VpnProtocol.OpenVpnUdp), "tcp" => nameof(VpnProtocol.OpenVpnTcp), "wireguard" => nameof(VpnProtocol.WireGuardUdp), "wireguard_tcp" => nameof(VpnProtocol.WireGuardTcp), "stealth" => nameof(VpnProtocol.WireGuardTls), _ => (string?)null, }; } private string? MigrateNetShieldMode(string username) { return GetUserSetting(username, "UserNetShieldMode") switch { "1" => nameof(NetShieldMode.BlockMalwareOnly), "2" => nameof(NetShieldMode.BlockAdsMalwareTrackers), _ => nameof(NetShieldMode.BlockMalwareOnly), }; } private string? MigrateProfilesForLaterMigration(string username) { try { List? legacyProfiles = GetUserSetting(username, "UserProfiles")?.Value?.Local; if (legacyProfiles is not null && legacyProfiles.Count == 0) { legacyProfiles = null; } return legacyProfiles is null ? null : _jsonSerializer.SerializeToString(legacyProfiles); } catch { return null; } } private void MigrateUserAuthData(string username) { string? accessToken = GetUserSetting(username, "UserAccessToken")?.Decrypt(); string? refreshToken = GetUserSetting(username, "UserRefreshToken")?.Decrypt(); string? uniqueSessionId = GetUserSetting(username, "UserUid")?.Decrypt(); if (string.IsNullOrWhiteSpace(accessToken) || string.IsNullOrWhiteSpace(refreshToken) || string.IsNullOrWhiteSpace(uniqueSessionId)) { _globalSettings.AccessToken = null; _globalSettings.RefreshToken = null; _globalSettings.UniqueSessionId = null; } else { _globalSettings.AccessToken = accessToken; _globalSettings.RefreshToken = refreshToken; _globalSettings.UniqueSessionId = uniqueSessionId; } } private void MigrateGlobalSettings() { bool? isAutoLaunchEnabled = IsGlobalSettingEnabled("StartOnBoot"); if (isAutoLaunchEnabled.HasValue) { _globalSettings.IsAutoLaunchEnabled = isAutoLaunchEnabled.Value; } string? language = GetSettingValue("Language"); if (!string.IsNullOrEmpty(language)) { _globalSettings.Language = language; } bool? isBetaAccessEnabled = IsGlobalSettingEnabled("EarlyAccess"); if (isBetaAccessEnabled.HasValue) { _globalSettings.IsBetaAccessEnabled = isBetaAccessEnabled.Value; } bool? areAutomaticUpdatesEnabled = IsGlobalSettingEnabled("IsToAutoUpdate"); if (areAutomaticUpdatesEnabled.HasValue) { _globalSettings.AreAutomaticUpdatesEnabled = areAutomaticUpdatesEnabled.Value; } bool? isAlternativeRoutingEnabled = IsGlobalSettingEnabled("DoHEnabled"); if (isAlternativeRoutingEnabled.HasValue) { _globalSettings.IsAlternativeRoutingEnabled = isAlternativeRoutingEnabled.Value; } MigrateKillSwitch(); MigrateAutoLaunchMode(); } private bool? IsGlobalSettingEnabled(string settingName) { return GetSettingValue(settingName)?.EqualsIgnoringCase("true"); } private void MigrateKillSwitch() { string? oldKillSwitchMode = GetSettingValue("KillSwitchMode"); if (string.IsNullOrEmpty(oldKillSwitchMode)) { return; } KillSwitchMode newKillSwitchMode = oldKillSwitchMode switch { "1" => KillSwitchMode.Standard, "2" => KillSwitchMode.Advanced, _ => DefaultSettings.KillSwitchMode, }; _globalSettings.IsKillSwitchEnabled = oldKillSwitchMode is "1" or "2"; _globalSettings.KillSwitchMode = newKillSwitchMode; } private void MigrateAutoLaunchMode() { _globalSettings.AutoLaunchMode = GetSettingValue("StartMinimized") switch { "0" => AutoLaunchMode.OpenOnDesktop, // "1" => AutoLaunchMode.MinimizeToTaskbar (not yet supported) "2" => AutoLaunchMode.MinimizeToSystemTray, _ => DefaultSettings.AutoLaunchMode, }; } }