Files
Mimante/Mimante/Services/SessionManager.cs
T
Alby96 7ca504a70a Add utility classes for theme management, waiting, watched products, and notifications
- 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.
2026-08-04 21:49:53 +02:00

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);
}
}
}