FruitBankHybridApp/FruitBankHybrid.Web.Client/Services/WebSecureCredentialService.cs

123 lines
3.8 KiB
C#

using System.Text;
using System.Text.Json;
using FruitBank.Common.Services;
using Microsoft.JSInterop;
namespace FruitBankHybrid.Web.Client.Services;
/// <summary>
/// WebAssembly implementation of ISecureCredentialService using obfuscated localStorage.
/// Note: WebAssembly has limited cryptography support, so we use Base64 + XOR obfuscation.
/// This prevents casual inspection but is not cryptographically secure.
/// For true security, consider server-side token storage or authentication cookies.
/// </summary>
public sealed class WebSecureCredentialService : ISecureCredentialService
{
private const string CredentialsKey = "FruitBank_UserCredentials";
private static readonly TimeSpan ExpirationDuration = TimeSpan.FromDays(2);
private static readonly byte[] ObfuscationKey = "FruitBank_Secure_v1_2025"u8.ToArray();
private readonly IJSRuntime _jsRuntime;
public WebSecureCredentialService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async Task SaveCredentialsAsync(string email, string password)
{
var data = new SecureCredentialData
{
Email = email,
Password = password,
ExpiresAtUtc = DateTime.UtcNow.Add(ExpirationDuration)
};
var json = JsonSerializer.Serialize(data);
var obfuscated = Obfuscate(json);
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", CredentialsKey, obfuscated);
}
public async Task<StoredCredentials?> GetCredentialsAsync()
{
try
{
var obfuscated = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", CredentialsKey);
if (string.IsNullOrEmpty(obfuscated))
return null;
var json = Deobfuscate(obfuscated);
if (string.IsNullOrEmpty(json))
{
await ClearCredentialsAsync();
return null;
}
var data = JsonSerializer.Deserialize<SecureCredentialData>(json);
if (data == null)
return null;
// Check expiration
if (DateTime.UtcNow > data.ExpiresAtUtc)
{
await ClearCredentialsAsync();
return null;
}
return new StoredCredentials(data.Email, data.Password);
}
catch
{
// If any error occurs (corrupted data, etc.), clear and return null
await ClearCredentialsAsync();
return null;
}
}
public async Task ClearCredentialsAsync()
{
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", CredentialsKey);
}
private static string Obfuscate(string plainText)
{
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var obfuscatedBytes = new byte[plainBytes.Length];
for (int i = 0; i < plainBytes.Length; i++)
{
obfuscatedBytes[i] = (byte)(plainBytes[i] ^ ObfuscationKey[i % ObfuscationKey.Length]);
}
return Convert.ToBase64String(obfuscatedBytes);
}
private static string? Deobfuscate(string obfuscated)
{
try
{
var obfuscatedBytes = Convert.FromBase64String(obfuscated);
var plainBytes = new byte[obfuscatedBytes.Length];
for (int i = 0; i < obfuscatedBytes.Length; i++)
{
plainBytes[i] = (byte)(obfuscatedBytes[i] ^ ObfuscationKey[i % ObfuscationKey.Length]);
}
return Encoding.UTF8.GetString(plainBytes);
}
catch
{
return null;
}
}
private sealed class SecureCredentialData
{
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public DateTime ExpiresAtUtc { get; set; }
}
}