123 lines
3.8 KiB
C#
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; }
|
|
}
|
|
}
|