EonaCat.SecretVault/EonaCat.SecretVault.Client/SecretVaultClient.cs

119 lines
4.1 KiB
C#

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
{
/// <summary>
/// Client for securely storing and retrieving secrets from a SecretVault server.
/// </summary>
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<string?> 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();
}
}
}