using System; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; // This file is part of the EonaCat project(s) which is released under the Apache License. // See the LICENSE file or go to https://EonaCat.com/License for full license details. namespace EonaCat.SecretVault.Client { /// /// Client for securely storing and retrieving secrets from a SecretVault server. /// public class SecretVaultClient : IDisposable { private readonly HttpClient _http; private readonly string _baseUrl; public SecretVaultClient(string baseUrl, string apiKey, bool isHttps = true, X509Certificate2? expectedCert = null) { if (isHttps) { if (!baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Base URL must use HTTPS for secure communication."); } } _baseUrl = baseUrl.TrimEnd('/') + "/"; var handler = new HttpClientHandler(); if (expectedCert != null) { handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return cert != null && cert.GetCertHashString() == expectedCert.GetCertHashString(); }; } _http = new HttpClient(handler) { BaseAddress = new Uri(_baseUrl), Timeout = TimeSpan.FromSeconds(10) }; _http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); } public SecretVaultClient(HttpClient httpClient, string apiKey, bool isHttps = true, X509Certificate2? expectedCert = null) { _http = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); if (isHttps) { if (!_http.BaseAddress.AbsoluteUri.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("Base URL must use HTTPS for secure communication."); } } if (string.IsNullOrWhiteSpace(apiKey)) { throw new ArgumentException("API key cannot be null or empty", nameof(apiKey)); } _http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); if (expectedCert != null) { var handler = new HttpClientHandler(); handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => { return cert != null && cert.GetCertHashString() == expectedCert.GetCertHashString(); }; // Dispose the old HttpClient if it exists _http?.Dispose(); _http = new HttpClient(handler) { BaseAddress = _http.BaseAddress, Timeout = _http.Timeout, MaxResponseContentBufferSize = _http.MaxResponseContentBufferSize, }; _http.DefaultRequestHeaders.Add("X-Api-Key", apiKey); } } public async Task StoreAsync(string key, string secret) { var content = new StringContent($"\"{secret}\""); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); await _http.PostAsync($"api/secrets/store?key={Uri.EscapeDataString(key)}", content); } public async Task DeleteAsync(string key) { await _http.DeleteAsync($"api/secrets/delete?key={Uri.EscapeDataString(key)}"); } public async Task RetrieveAsync(string key) { var res = await _http.GetAsync($"api/secrets/retrieve?key={Uri.EscapeDataString(key)}"); if (!res.IsSuccessStatusCode) { return null; } return await res.Content.ReadAsStringAsync(); } public void Dispose() { _http.Dispose(); } } }