using System.Text; using System.Text.Json; using FruitBank.Common.Services; using Microsoft.JSInterop; namespace FruitBankHybrid.Web.Client.Services; /// /// 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. /// 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 GetCredentialsAsync() { try { var obfuscated = await _jsRuntime.InvokeAsync("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(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; } } }