feat: Aggiunto sistema crittografia credenziali portabile e migrazione
Sostituita Windows ProtectedData con AES-256-GCM per compatibilità multi-macchina. Aggiunta interfaccia migrazione guidata per credenziali legacy e gestione errori completa. - Nuovo: Servizio crittografia AES con derivazione chiavi PBKDF2 - Nuovo: Interfaccia Blazor migrazione con rilevamento credenziali - Nuovo: Documentazione utente per risoluzione problemi - Fix: Errori compilazione e problemi binding componenti - Miglioramento: Credenziali portabili funzionano su qualsiasi macchina dopo migrazione una-tantum Completamente retrocompatibile - credenziali
This commit is contained in:
@@ -666,8 +666,7 @@ public class CredentialService : ICredentialService
|
||||
return await query.Select(c => c.Name).ToListAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore nel recuperare i nomi delle credenziali");
|
||||
{ _logger.LogError(ex, "Errore nel recuperare i nomi delle credenziali");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -677,7 +676,8 @@ public class CredentialService : ICredentialService
|
||||
#region Private Mapping Methods
|
||||
|
||||
private DatabaseCredential MapToDatabaseCredential(CredentialEntity entity)
|
||||
{ var credential = new DatabaseCredential
|
||||
{
|
||||
var credential = new DatabaseCredential
|
||||
{
|
||||
Name = entity.Name,
|
||||
DatabaseType = Enum.Parse<DatabaseType>(entity.DatabaseType!),
|
||||
@@ -685,7 +685,7 @@ public class CredentialService : ICredentialService
|
||||
Port = entity.Port ?? 0,
|
||||
DatabaseName = entity.DatabaseName ?? string.Empty,
|
||||
Username = entity.Username ?? string.Empty,
|
||||
Password = _encryptionService.Decrypt(entity.EncryptedPassword!),
|
||||
Password = DecryptSafely(entity.EncryptedPassword, entity.Name, "password"),
|
||||
ConnectionString = entity.ConnectionString,
|
||||
CommandTimeout = entity.CommandTimeout,
|
||||
IgnoreSslErrors = entity.IgnoreSslErrors
|
||||
@@ -715,15 +715,14 @@ public class CredentialService : ICredentialService
|
||||
? serviceType
|
||||
: RestServiceType.Generic,
|
||||
BaseUrl = entity.Host ?? string.Empty,
|
||||
Username = entity.Username,
|
||||
Password = !string.IsNullOrEmpty(entity.EncryptedPassword)
|
||||
? _encryptionService.Decrypt(entity.EncryptedPassword)
|
||||
Username = entity.Username, Password = !string.IsNullOrEmpty(entity.EncryptedPassword)
|
||||
? DecryptSafely(entity.EncryptedPassword, entity.Name, "password")
|
||||
: null,
|
||||
ApiKey = !string.IsNullOrEmpty(entity.EncryptedApiKey)
|
||||
? _encryptionService.Decrypt(entity.EncryptedApiKey)
|
||||
? DecryptSafely(entity.EncryptedApiKey, entity.Name, "API key")
|
||||
: null,
|
||||
AuthToken = !string.IsNullOrEmpty(entity.EncryptedAuthToken)
|
||||
? _encryptionService.Decrypt(entity.EncryptedAuthToken)
|
||||
? DecryptSafely(entity.EncryptedAuthToken, entity.Name, "auth token")
|
||||
: null,
|
||||
TimeoutSeconds = entity.TimeoutSeconds,
|
||||
IgnoreSslErrors = entity.IgnoreSslErrors
|
||||
@@ -919,4 +918,47 @@ public class CredentialService : ICredentialService
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Utility Methods /// <summary>
|
||||
/// Decrittografa in modo sicuro gestendo i fallimenti dovuti a migrazione tra macchine
|
||||
/// </summary>
|
||||
/// <param name="encryptedValue">Valore crittografato</param>
|
||||
/// <param name="credentialName">Nome della credenziale per logging</param>
|
||||
/// <param name="fieldName">Nome del campo per logging</param>
|
||||
/// <returns>Valore decrittografato o stringa speciale per indicare che serve re-inserimento</returns>
|
||||
private string DecryptSafely(string? encryptedValue, string credentialName, string fieldName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(encryptedValue))
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
return _encryptionService.Decrypt(encryptedValue);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message.Contains("Impossibile decrittografare"))
|
||||
{
|
||||
_logger.LogWarning("Impossibile decrittografare {FieldName} per la credenziale {CredentialName}. Probabile migrazione tra macchine diverse.",
|
||||
fieldName, credentialName);
|
||||
return "*** CREDENZIALI NON DISPONIBILI - REINSERIRE ***";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Problema nella decrittografia di {FieldName} per la credenziale {CredentialName}: {Message}",
|
||||
fieldName, credentialName, ex.Message);
|
||||
return "*** ERRORE DECRITTOGRAFIA ***";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se una credenziale ha bisogno di essere re-inserita
|
||||
/// </summary>
|
||||
/// <param name="credentialValue">Valore della credenziale</param>
|
||||
/// <returns>True se la credenziale deve essere re-inserita</returns>
|
||||
private bool NeedsReentry(string credentialValue)
|
||||
{
|
||||
return credentialValue.Contains("*** CREDENZIALI NON DISPONIBILI") ||
|
||||
credentialValue.Contains("*** ERRORE DECRITTOGRAFIA ***");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -11,19 +11,24 @@ public interface IEncryptionService
|
||||
{
|
||||
string Encrypt(string plainText);
|
||||
string Decrypt(string encryptedText);
|
||||
bool CanDecrypt(string encryptedText);
|
||||
string MigrateEncryptedText(string oldEncryptedText, string newPlainText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Servizio per la crittografia delle password cross-platform
|
||||
/// Servizio per la crittografia delle password cross-platform con supporto per migrazione
|
||||
/// </summary>
|
||||
public class EncryptionService : IEncryptionService
|
||||
{
|
||||
private readonly byte[] _key;
|
||||
private readonly byte[] _iv;
|
||||
private const string ENTROPY_STRING = "CredentialManager2025";
|
||||
private const string AES_PREFIX = "AES:";
|
||||
private const string PROTECTED_PREFIX = "PROTECTED:";
|
||||
|
||||
public EncryptionService()
|
||||
{
|
||||
// Chiave e IV derivati da una stringa fissa (in produzione dovrebbero essere configurabili)
|
||||
// Chiave e IV derivati da una stringa fissa
|
||||
var keySource = "CredentialManager2025KeyForEncryption!";
|
||||
var ivSource = "CredMgr2025IV!";
|
||||
|
||||
@@ -40,14 +45,8 @@ public class EncryptionService : IEncryptionService
|
||||
|
||||
try
|
||||
{
|
||||
// Su Windows, usa ProtectedData se disponibile
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return EncryptWithProtectedData(plainText);
|
||||
}
|
||||
|
||||
// Su altre piattaforme, usa AES
|
||||
return EncryptWithAes(plainText);
|
||||
// Sempre usa AES per nuove crittografie per garantire portabilità
|
||||
return AES_PREFIX + EncryptWithAes(plainText);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -62,23 +61,90 @@ public class EncryptionService : IEncryptionService
|
||||
|
||||
try
|
||||
{
|
||||
// Su Windows, usa ProtectedData se disponibile
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
// Determina il metodo di crittografia utilizzato
|
||||
if (encryptedText.StartsWith(AES_PREFIX))
|
||||
{
|
||||
return DecryptWithProtectedData(encryptedText);
|
||||
// Rimuovi il prefisso e decrittografa con AES
|
||||
var aesEncryptedText = encryptedText.Substring(AES_PREFIX.Length);
|
||||
return DecryptWithAes(aesEncryptedText);
|
||||
}
|
||||
else if (encryptedText.StartsWith(PROTECTED_PREFIX))
|
||||
{
|
||||
// Rimuovi il prefisso e decrittografa con ProtectedData
|
||||
var protectedEncryptedText = encryptedText.Substring(PROTECTED_PREFIX.Length);
|
||||
return DecryptWithProtectedData(protectedEncryptedText);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Formato legacy - prova prima ProtectedData (se su Windows), poi AES
|
||||
return DecryptLegacyFormat(encryptedText);
|
||||
}
|
||||
|
||||
// Su altre piattaforme, usa AES
|
||||
return DecryptWithAes(encryptedText);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("Errore durante la decrittografia", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanDecrypt(string encryptedText)
|
||||
{
|
||||
if (string.IsNullOrEmpty(encryptedText))
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
Decrypt(encryptedText);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string MigrateEncryptedText(string oldEncryptedText, string newPlainText)
|
||||
{
|
||||
// Cripta il nuovo testo in chiaro con il metodo attuale (AES)
|
||||
return Encrypt(newPlainText);
|
||||
}
|
||||
|
||||
private string DecryptLegacyFormat(string encryptedText)
|
||||
{
|
||||
// Su Windows, prova prima ProtectedData
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
try
|
||||
{
|
||||
return DecryptWithProtectedData(encryptedText);
|
||||
}
|
||||
catch (CryptographicException ex) when (ex.Message.Contains("Chiave non utilizzabile") ||
|
||||
ex.Message.Contains("Key not valid for use in specified state") ||
|
||||
ex.Message.Contains("The data is invalid"))
|
||||
{
|
||||
// ProtectedData non riesce (probabilmente diversa macchina/utente)
|
||||
// Prova con AES come fallback
|
||||
try
|
||||
{
|
||||
return DecryptWithAes(encryptedText);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Impossibile decrittografare le credenziali. " +
|
||||
"Le credenziali potrebbero essere state create su una macchina/utente diverso. " +
|
||||
"È necessario re-inserire le credenziali.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Su altre piattaforme, usa AES
|
||||
return DecryptWithAes(encryptedText);
|
||||
}
|
||||
} private string EncryptWithProtectedData(string plainText)
|
||||
{
|
||||
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
|
||||
byte[] entropy = Encoding.UTF8.GetBytes("CredentialManager2025");
|
||||
byte[] entropy = Encoding.UTF8.GetBytes(ENTROPY_STRING);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
@@ -95,7 +161,7 @@ public class EncryptionService : IEncryptionService
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
|
||||
byte[] entropy = Encoding.UTF8.GetBytes("CredentialManager2025");
|
||||
byte[] entropy = Encoding.UTF8.GetBytes(ENTROPY_STRING);
|
||||
byte[] decryptedBytes = ProtectedData.Unprotect(encryptedBytes, entropy, DataProtectionScope.CurrentUser);
|
||||
return Encoding.UTF8.GetString(decryptedBytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user