- Implement ThemeManager for dynamic light/dark theme switching in the application. - Create Wait class for cancellable delays without exceptions for smoother user experience. - Introduce WatchedProductsStore to manage and persist watched products in JSON format. - Add WindowsNotifier for system notifications to inform users of important events. - Develop ProductViewModel to encapsulate product data and manage UI interactions effectively.
174 lines
6.5 KiB
C#
174 lines
6.5 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using AutoBidder.Models;
|
|
|
|
namespace AutoBidder.Services
|
|
{
|
|
/// <summary>
|
|
/// Persistenza della sessione Bidoo.
|
|
///
|
|
/// Il cookie salvato qui è la credenziale piena del conto: chi lo legge entra come te.
|
|
/// Va quindi protetto con <b>DPAPI</b> (<see cref="ProtectedData"/>, ambito utente
|
|
/// corrente): Windows lega il testo cifrato al tuo account, e l'applicazione non deve
|
|
/// custodire nessuna chiave.
|
|
///
|
|
/// <para>La versione precedente usava AES con chiave derivata da una stringa compilata
|
|
/// nell'eseguibile e IV costante: chiunque avesse l'exe poteva decifrare qualunque
|
|
/// <c>session.dat</c>. Quel formato viene ancora <i>letto</i> per non perdere le
|
|
/// sessioni esistenti, e al primo salvataggio il file viene riscritto con DPAPI.</para>
|
|
/// </summary>
|
|
public class SessionManager
|
|
{
|
|
// Nella radice fissa: DPAPI lega comunque il file a questo utente Windows,
|
|
// quindi spostarlo su un disco condiviso non lo renderebbe piu' utilizzabile.
|
|
private static string SessionFilePath => Utilities.AppPaths.SessionFile;
|
|
|
|
/// <summary>Marcatore iniziale dei file protetti con DPAPI.</summary>
|
|
private static readonly byte[] DpapiMagic = "ABDP1\0"u8.ToArray();
|
|
|
|
/// <summary>
|
|
/// Entropia aggiuntiva: senza, qualunque altro programma in esecuzione con il tuo
|
|
/// account potrebbe decifrare il file semplicemente chiamando DPAPI.
|
|
/// </summary>
|
|
private static readonly byte[] Entropy = SHA256.HashData(
|
|
Encoding.UTF8.GetBytes("AutoBidder.Session.v2"));
|
|
|
|
// ── Formato precedente (solo lettura, per non perdere le sessioni salvate) ──
|
|
private static readonly byte[] LegacyKey =
|
|
SHA256.HashData(Encoding.UTF8.GetBytes("AutoBidder_Session_Key_V1_2025"));
|
|
private static readonly byte[] LegacyIV =
|
|
MD5.HashData(Encoding.UTF8.GetBytes("AutoBidder_IV_V1"));
|
|
|
|
/// <summary>Salva la sessione protetta con DPAPI.</summary>
|
|
public static bool SaveSession(BidooSession session)
|
|
{
|
|
try
|
|
{
|
|
var directory = Path.GetDirectoryName(SessionFilePath);
|
|
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
var json = JsonSerializer.Serialize(session, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = true
|
|
});
|
|
|
|
var plain = Encoding.UTF8.GetBytes(json);
|
|
var protectedBytes = ProtectedData.Protect(plain, Entropy, DataProtectionScope.CurrentUser);
|
|
|
|
var output = new byte[DpapiMagic.Length + protectedBytes.Length];
|
|
Buffer.BlockCopy(DpapiMagic, 0, output, 0, DpapiMagic.Length);
|
|
Buffer.BlockCopy(protectedBytes, 0, output, DpapiMagic.Length, protectedBytes.Length);
|
|
|
|
File.WriteAllBytes(SessionFilePath, output);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[SESSION ERROR] Salvataggio non riuscito: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Carica la sessione. Riconosce il formato dal marcatore iniziale e, se trova
|
|
/// ancora il vecchio AES, la riscrive subito con DPAPI.
|
|
/// </summary>
|
|
public static BidooSession? LoadSession()
|
|
{
|
|
try
|
|
{
|
|
if (!File.Exists(SessionFilePath)) return null;
|
|
|
|
var raw = File.ReadAllBytes(SessionFilePath);
|
|
if (raw.Length == 0) return null;
|
|
|
|
var isDpapi = HasDpapiMagic(raw);
|
|
|
|
byte[] plain;
|
|
if (isDpapi)
|
|
{
|
|
var payload = new byte[raw.Length - DpapiMagic.Length];
|
|
Buffer.BlockCopy(raw, DpapiMagic.Length, payload, 0, payload.Length);
|
|
plain = ProtectedData.Unprotect(payload, Entropy, DataProtectionScope.CurrentUser);
|
|
}
|
|
else
|
|
{
|
|
plain = DecryptLegacyAes(raw);
|
|
}
|
|
|
|
var json = Encoding.UTF8.GetString(plain);
|
|
var session = JsonSerializer.Deserialize<BidooSession>(json);
|
|
|
|
if (session == null || !session.IsValid) return null;
|
|
|
|
// Migrazione silenziosa: da qui in poi il file è legato all'account Windows.
|
|
if (!isDpapi)
|
|
{
|
|
SaveSession(session);
|
|
Console.WriteLine("[SESSION] Sessione migrata alla protezione DPAPI");
|
|
}
|
|
|
|
return session;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[SESSION ERROR] Caricamento non riuscito: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static bool ClearSession()
|
|
{
|
|
try
|
|
{
|
|
if (File.Exists(SessionFilePath)) File.Delete(SessionFilePath);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[SESSION ERROR] Cancellazione non riuscita: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static bool HasSavedSession() => File.Exists(SessionFilePath);
|
|
|
|
public static (bool exists, DateTime? lastModified) GetSessionInfo()
|
|
{
|
|
if (!File.Exists(SessionFilePath)) return (false, null);
|
|
return (true, new FileInfo(SessionFilePath).LastWriteTime);
|
|
}
|
|
|
|
private static bool HasDpapiMagic(byte[] raw)
|
|
{
|
|
if (raw.Length < DpapiMagic.Length) return false;
|
|
|
|
for (var i = 0; i < DpapiMagic.Length; i++)
|
|
{
|
|
if (raw[i] != DpapiMagic[i]) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Legge il vecchio formato AES. Solo lettura: non si scrive più così.</summary>
|
|
private static byte[] DecryptLegacyAes(byte[] encrypted)
|
|
{
|
|
using var aes = Aes.Create();
|
|
aes.Key = LegacyKey;
|
|
aes.IV = LegacyIV;
|
|
aes.Mode = CipherMode.CBC;
|
|
aes.Padding = PaddingMode.PKCS7;
|
|
|
|
using var decryptor = aes.CreateDecryptor();
|
|
return decryptor.TransformFinalBlock(encrypted, 0, encrypted.Length);
|
|
}
|
|
}
|
|
}
|