diff --git a/CREDENTIAL_TROUBLESHOOTING.md b/CREDENTIAL_TROUBLESHOOTING.md new file mode 100644 index 0000000..95d2826 --- /dev/null +++ b/CREDENTIAL_TROUBLESHOOTING.md @@ -0,0 +1,51 @@ +# Risoluzione Problemi Credenziali + +## Problema: "Errore durante la decrittografia" + +Questo errore si verifica quando l'applicazione non riesce a decrittografare le credenziali salvate. + +### Cause Comuni + +1. **Esecuzione su macchina diversa**: Le credenziali sono state salvate su una macchina diversa da quella attuale +2. **Utente diverso**: L'applicazione viene eseguita con un utente diverso da quello che ha creato le credenziali +3. **Sistema operativo diverso**: Le credenziali sono state create su un sistema operativo diverso + +### Soluzioni + +#### Opzione 1: Migrazione Automatica delle Credenziali + +1. Vai alla pagina [Migrazione Credenziali](/credential-migration) +2. Segui le istruzioni per re-inserire le credenziali che non possono essere decrittografate +3. Le credenziali verranno automaticamente ri-crittografate per la macchina/utente corrente + +#### Opzione 2: Backup e Ripristino Manuale + +1. Esporta le credenziali dalla macchina originale (se disponibile) +2. Re-inserisci manualmente le credenziali nella pagina di [Gestione Credenziali](/credentials) +3. Elimina le credenziali problematiche + +#### Opzione 3: Reset Completo + +1. Vai alla cartella dati dell'applicazione: + - Windows: `%LOCALAPPDATA%\\DataCoupler\\Credentials` + - Linux/Mac: `~/.local/share/DataCoupler/Credentials` +2. Elimina il file `credential_profiles.json` +3. Riavvia l'applicazione +4. Re-inserisci tutte le credenziali + +### Prevenzione + +Per evitare questo problema in futuro: + +1. **Backup regolari**: Esporta periodicamente le credenziali +2. **Documentazione**: Mantieni una documentazione delle credenziali in un luogo sicuro +3. **Configurazione centralizzata**: Considera l'uso di un sistema di gestione credenziali centralizzato per ambienti enterprise + +### Contatti + +Se il problema persiste, contatta il supporto tecnico con i seguenti dettagli: + +- Sistema operativo +- Utente corrente +- Messaggio di errore completo +- Log dell'applicazione diff --git a/CredentialManager/Services/CredentialService.cs b/CredentialManager/Services/CredentialService.cs index e976a7c..c449b24 100644 --- a/CredentialManager/Services/CredentialService.cs +++ b/CredentialManager/Services/CredentialService.cs @@ -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(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 /// + /// Decrittografa in modo sicuro gestendo i fallimenti dovuti a migrazione tra macchine + /// + /// Valore crittografato + /// Nome della credenziale per logging + /// Nome del campo per logging + /// Valore decrittografato o stringa speciale per indicare che serve re-inserimento + 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 ***"; + } + } + + /// + /// Verifica se una credenziale ha bisogno di essere re-inserita + /// + /// Valore della credenziale + /// True se la credenziale deve essere re-inserita + private bool NeedsReentry(string credentialValue) + { + return credentialValue.Contains("*** CREDENZIALI NON DISPONIBILI") || + credentialValue.Contains("*** ERRORE DECRITTOGRAFIA ***"); + } + + #endregion } diff --git a/CredentialManager/Services/EncryptionService.cs b/CredentialManager/Services/EncryptionService.cs index 3e7393e..93bce50 100644 --- a/CredentialManager/Services/EncryptionService.cs +++ b/CredentialManager/Services/EncryptionService.cs @@ -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); } /// -/// Servizio per la crittografia delle password cross-platform +/// Servizio per la crittografia delle password cross-platform con supporto per migrazione /// 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); } diff --git a/Data_Coupler/Pages/CredentialManagement.razor b/Data_Coupler/Pages/CredentialManagement.razor index 7abb04a..1086922 100644 --- a/Data_Coupler/Pages/CredentialManagement.razor +++ b/Data_Coupler/Pages/CredentialManagement.razor @@ -6,11 +6,34 @@ @using Microsoft.JSInterop @inject IDataConnectionCredentialService CredentialService @inject IJSRuntime JSRuntime +@inject NavigationManager Navigation Gestione Credenziali

Gestione Credenziali

+@* Controllo per credenziali problematiche *@ +@if (hasProblematicCredentials && !loading) +{ + +} +
@@ -519,6 +542,7 @@ else private bool loading = true; private string? errorMessage = null; private bool testingConnection = false; + private bool hasProblematicCredentials = false; // Modal state private bool showDatabaseModal = false; @@ -529,16 +553,16 @@ else private RestApiCredential currentRestApiCredential = new(); protected override async Task OnInitializedAsync() - { - await RefreshCredentials(); + { await RefreshCredentials(); + CheckForProblematicCredentials(); } private async Task RefreshCredentials() { loading = true; errorMessage = null; try - { - databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync(); + { databaseCredentials = await CredentialService.GetAllDatabaseCredentialsAsync(); restApiCredentials = await CredentialService.GetAllRestApiCredentialsAsync(); + CheckForProblematicCredentials(); } catch (Exception ex) { @@ -957,6 +981,56 @@ else }; return IsFieldRequired(fieldName, serviceType) ? $"{label} *" : label; + } /// + /// Verifica se ci sono credenziali che non possono essere decrittografate + /// + private void CheckForProblematicCredentials() + { + try + { + hasProblematicCredentials = false; + + // Verifica credenziali database + foreach (var dbCred in databaseCredentials) + { + if (HasProblematicPassword(dbCred.Password)) + { + hasProblematicCredentials = true; + break; + } + } + + // Verifica credenziali REST API se non trovate problematiche + if (!hasProblematicCredentials) + { + foreach (var restCred in restApiCredentials) + { + if (HasProblematicPassword(restCred.Password) || + HasProblematicPassword(restCred.ApiKey) || + HasProblematicPassword(restCred.AuthToken) || + HasProblematicPassword(restCred.ClientSecret)) + { + hasProblematicCredentials = true; + break; + } + } + } + + StateHasChanged(); + } + catch (Exception ex) + { + // Log dell'errore, ma non bloccare l'interfaccia + Console.WriteLine($"Errore nella verifica delle credenziali problematiche: {ex.Message}"); + } + } /// + /// Verifica se una password indica un problema di decrittografia + /// + private bool HasProblematicPassword(string? password) + { + return !string.IsNullOrEmpty(password) && + (password.Contains("*** CREDENZIALI NON DISPONIBILI") || + password.Contains("*** ERRORE DECRITTOGRAFIA ***")); } #endregion diff --git a/Data_Coupler/Pages/CredentialMigration.razor b/Data_Coupler/Pages/CredentialMigration.razor new file mode 100644 index 0000000..72d1d0b --- /dev/null +++ b/Data_Coupler/Pages/CredentialMigration.razor @@ -0,0 +1,333 @@ +@page "/credential-migration" +@using CredentialManager.Services +@using CredentialManager.Models +@inject ICredentialService CredentialService +@inject NavigationManager Navigation +@inject IJSRuntime JSRuntime +@inject ILogger Logger + +Migrazione Credenziali + +
+
+
+
+
+

Migrazione Credenziali Richiesta

+
+
+ + + @if (isLoading) + { +
+
+ Caricamento... +
+
+ } + else if (problematicCredentials.Any()) + { +
Credenziali che richiedono re-inserimento:
+ + @foreach (var credGroup in problematicCredentials.GroupBy(c => c.Type)) + { +
+
+
@GetCredentialTypeDisplayName(credGroup.Key)
+
+
+ @foreach (var cred in credGroup) + { +
+
@cred.Name
+

@cred.Description

@if (cred.Type == CredentialType.Database) + { + var editModel = GetEditModel(cred.Name); + + + + +
+
+
+ + +
+
+
+ + +
+ } else if (cred.Type == CredentialType.RestApi) + { + var editModel = GetEditModel(cred.Name); + + + + +
+
+
+ + +
+
+
+
+ + +
+
+
+ + @if (IsSpecificServiceType(cred, "Salesforce")) + { +
+
+
+ + +
+
+
+
+ + +
+
+
+ } + + +
+ } +
+ } +
+
+ } + } + else + { + + } +
+
+
+
+
+ +@code { + private bool isLoading = true; + private List problematicCredentials = new(); + private Dictionary editModels = new(); + + protected override async Task OnInitializedAsync() + { + await LoadProblematicCredentials(); + isLoading = false; + } + + private async Task LoadProblematicCredentials() + { + try + { + problematicCredentials.Clear(); + + // Carica tutte le credenziali e verifica quelle problematiche + var allCredentials = new List(); + + // Database credentials + try + { + var dbCreds = await CredentialService.GetAllDatabaseCredentialsAsync(); + allCredentials.AddRange(dbCreds.Select(c => new CredentialSummary + { + Name = c.Name, + Type = CredentialType.Database, + Description = $"Database: {c.DatabaseName} su {c.Host}", + HasProblematicPassword = NeedsReentry(c.Password) + })); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento credenziali database"); + } + + // REST API credentials + try + { + var restCreds = await CredentialService.GetAllRestApiCredentialsAsync(); + allCredentials.AddRange(restCreds.Select(c => new CredentialSummary + { + Name = c.Name, + Type = CredentialType.RestApi, + Description = $"Servizio: {c.ServiceType} - {c.BaseUrl}", + ServiceType = c.ServiceType.ToString(), + HasProblematicPassword = NeedsReentry(c.Password ?? ""), + HasProblematicApiKey = NeedsReentry(c.ApiKey ?? ""), + HasProblematicClientSecret = NeedsReentry(c.ClientSecret ?? "") + })); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento credenziali REST API"); + } + + problematicCredentials = allCredentials + .Where(c => c.HasProblematicPassword || c.HasProblematicApiKey || c.HasProblematicClientSecret) + .ToList(); + + // Inizializza i modelli di editing + foreach (var cred in problematicCredentials) + { + editModels[cred.Name] = new CredentialEditModel(); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore generale nel caricamento delle credenziali"); + } + } + + private bool NeedsReentry(string credentialValue) + { + return !string.IsNullOrEmpty(credentialValue) && + (credentialValue.Contains("*** CREDENZIALI NON DISPONIBILI") || + credentialValue.Contains("*** ERRORE DECRITTOGRAFIA ***")); + } + + private string GetCredentialTypeDisplayName(CredentialType type) + { + return type switch + { + CredentialType.Database => "Credenziali Database", + CredentialType.RestApi => "Credenziali REST API", + _ => type.ToString() + }; + } + + private bool IsSpecificServiceType(CredentialSummary cred, string serviceType) + { + return cred.ServiceType?.Contains(serviceType, StringComparison.OrdinalIgnoreCase) == true; + } + + private CredentialEditModel GetEditModel(string credentialName) + { + if (!editModels.ContainsKey(credentialName)) + editModels[credentialName] = new CredentialEditModel(); + return editModels[credentialName]; + } + + private async Task UpdateDatabaseCredential(string credentialName) + { + try + { + var editModel = GetEditModel(credentialName); + if (string.IsNullOrEmpty(editModel.Password)) + { + await JSRuntime.InvokeVoidAsync("alert", "La password è obbligatoria"); + return; + } + + // Trova la credenziale esistente + var existingCred = (await CredentialService.GetAllDatabaseCredentialsAsync()) + .FirstOrDefault(c => c.Name == credentialName); + + if (existingCred != null) + { + // Aggiorna solo la password + existingCred.Password = editModel.Password; + await CredentialService.SaveDatabaseCredentialAsync(existingCred); + + await JSRuntime.InvokeVoidAsync("alert", "Credenziali aggiornate con successo!"); + await LoadProblematicCredentials(); + StateHasChanged(); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'aggiornamento delle credenziali database: {Name}", credentialName); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nell'aggiornamento: {ex.Message}"); + } + } + + private async Task UpdateRestCredential(string credentialName) + { + try + { + var editModel = GetEditModel(credentialName); + + // Trova la credenziale esistente + var existingCred = (await CredentialService.GetAllRestApiCredentialsAsync()) + .FirstOrDefault(c => c.Name == credentialName); + + if (existingCred != null) + { + // Aggiorna i campi forniti + if (!string.IsNullOrEmpty(editModel.Password)) + existingCred.Password = editModel.Password; + + if (!string.IsNullOrEmpty(editModel.ApiKey)) + existingCred.ApiKey = editModel.ApiKey; + + if (!string.IsNullOrEmpty(editModel.ClientSecret)) + existingCred.ClientSecret = editModel.ClientSecret; + + if (!string.IsNullOrEmpty(editModel.SecurityToken)) + existingCred.SecurityToken = editModel.SecurityToken; + + await CredentialService.SaveRestApiCredentialAsync(existingCred); + + await JSRuntime.InvokeVoidAsync("alert", "Credenziali aggiornate con successo!"); + await LoadProblematicCredentials(); + StateHasChanged(); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'aggiornamento delle credenziali REST: {Name}", credentialName); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nell'aggiornamento: {ex.Message}"); + } + } + + public class CredentialSummary + { + public string Name { get; set; } = ""; + public CredentialType Type { get; set; } + public string Description { get; set; } = ""; + public string? ServiceType { get; set; } + public bool HasProblematicPassword { get; set; } + public bool HasProblematicApiKey { get; set; } + public bool HasProblematicClientSecret { get; set; } + } + + public class CredentialEditModel + { + public string? Password { get; set; } + public string? ApiKey { get; set; } + public string? ClientSecret { get; set; } + public string? SecurityToken { get; set; } + } +} diff --git a/Data_Coupler/wwwroot/data/credentials.db b/Data_Coupler/wwwroot/data/credentials.db index 8b37ffd..7e7649c 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db and b/Data_Coupler/wwwroot/data/credentials.db differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm index 3ffe6e3..6e84bd6 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-shm and b/Data_Coupler/wwwroot/data/credentials.db-shm differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal index 73e9b15..e69de29 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-wal and b/Data_Coupler/wwwroot/data/credentials.db-wal differ