/* * Copyright (c) 2025 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.Security.Cryptography; using ProtonVPN.Builds.Variables; using ProtonVPN.Client.Logic.Connection.Contracts.GuestHole; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Logging.Contracts; using ProtonVPN.Logging.Contracts.Events.GuestHoleLogs; using ProtonVPN.Serialization.Contracts.Json; namespace ProtonVPN.Client.Logic.Connection.GuestHole; public class GuestHoleServersFileStorage: IGuestHoleServersFileStorage { private const int TAG_SIZE_IN_BYTES = 16; private readonly ILogger _logger; private readonly IStaticConfiguration _config; private readonly IJsonSerializer _jsonSerializer; private List? _cachedList; public GuestHoleServersFileStorage(ILogger logger, IStaticConfiguration config, IJsonSerializer jsonSerializer) { _logger = logger; _config = config; _jsonSerializer = jsonSerializer; } public async Task> GetAsync() { if (_cachedList != null) { return _cachedList; } try { MemoryStream json = await GetJsonAsync(); _cachedList = _jsonSerializer.Deserialize>(json) ?? []; return _cachedList; } catch (Exception e) { _logger.Error("Failed to get guest hole servers.", e); } return []; } private async Task GetJsonAsync() { byte[] encryptedData = await File.ReadAllBytesAsync(_config.GuestHoleServersJsonFilePath); if (encryptedData.Length < TAG_SIZE_IN_BYTES) { throw new CryptographicException( $"Encrypted file '{_config.GuestHoleServersJsonFilePath}' is corrupted or truncated: " + $"expected at least {TAG_SIZE_IN_BYTES} bytes, got {encryptedData.Length}."); } byte[] key1 = HexStringToByteArray(GlobalConfig.GuestHoleKey1); byte[] key2 = HexStringToByteArray(GlobalConfig.GuestHoleKey2); byte[] combinedKey = InterleaveBytes(key1, key2); byte[] nonce = new byte[12]; int cipherTextLength = encryptedData.Length - TAG_SIZE_IN_BYTES; byte[] cipherText = new byte[cipherTextLength]; byte[] tag = new byte[TAG_SIZE_IN_BYTES]; Array.Copy(encryptedData, 0, cipherText, 0, cipherTextLength); Array.Copy(encryptedData, cipherTextLength, tag, 0, TAG_SIZE_IN_BYTES); byte[] plainText = new byte[cipherTextLength]; using (AesGcm aesGcm = new(combinedKey, TAG_SIZE_IN_BYTES)) { aesGcm.Decrypt(nonce, cipherText, tag, plainText); } return new MemoryStream(plainText); } private byte[] InterleaveBytes(byte[] a, byte[] b) { if (a.Length != b.Length) { throw new ArgumentException( $"Cannot interleave arrays of different lengths: a.Length={a.Length}, b.Length={b.Length}", nameof(b)); } byte[] result = new byte[a.Length + b.Length]; for (int i = 0; i < a.Length; i++) { result[2 * i] = a[i]; result[2 * i + 1] = b[i]; } return result; } private byte[] HexStringToByteArray(string hex) { if (hex.Length % 2 != 0) { throw new ArgumentException("Hex string must have an even number of characters.", nameof(hex)); } int length = hex.Length / 2; byte[] bytes = new byte[length]; for (int i = 0; i < length; i++) { bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); } return bytes; } }