Ghid Complet de Securitate .NET MAUI: Autentificare Biometrică, Certificate Pinning și Protecția Datelor

Ghid practic de securitate pentru aplicațiile .NET MAUI cu exemple de cod: SecureStorage, autentificare biometrică, certificate pinning, OAuth 2.0 cu PKCE, validarea input-urilor și checklist complet pentru lansare conform OWASP Mobile Top 10.

Introducere: De Ce Contează (Cu Adevărat) Securitatea Aplicațiilor Mobile

Să fim sinceri — trăim într-o lume în care miliarde de utilizatori depind zilnic de aplicațiile mobile pentru tranzacții bancare, comunicare și gestionarea datelor personale. Securitatea nu mai e un "nice to have". E o cerință fundamentală. Și cifrele confirmă asta: peste 75% din aplicațiile mobile prezintă cel puțin o vulnerabilitate critică, iar costul mediu al unei breșe de date a depășit 4,5 milioane de dolari la nivel global.

Destul de înfricoșător, nu-i așa?

.NET MAUI (Multi-platform App UI) oferă dezvoltatorilor un framework puternic pentru construirea aplicațiilor cross-platform, dar securitatea rămâne responsabilitatea echipei de dezvoltare. Odată cu lansarea .NET 10 și .NET MAUI 10, Microsoft a adus îmbunătățiri semnificative privind calitatea, diagnosticarea și urmărirea metricilor — inclusiv suportul edge-to-edge pe Android și namespace-uri XAML globale. Cu toate astea, implementarea corectă a mecanismelor de securitate rămâne în sarcina noastră, a dezvoltatorilor.

OWASP Mobile Top 10 din 2024 evidențiază cele mai critice riscuri pentru aplicațiile mobile:

  • M1: Utilizarea Necorespunzătoare a Credențialelor — stocarea în clar a parolelor și token-urilor
  • M2: Securitate Inadecvată a Lanțului de Aprovizionare — dependențe NuGet compromise
  • M3: Autentificare/Autorizare Nesigură — mecanisme de autentificare slabe
  • M4: Validare Insuficientă a Input-urilor/Output-urilor — lipsa sanitizării datelor
  • M5: Comunicare Nesigură — transmiterea datelor fără criptare
  • M6: Controale Inadecvate ale Confidențialității — expunerea datelor personale

În acest ghid, vom trece prin fiecare aspect important al securității pentru aplicațiile .NET MAUI. De la stocarea securizată a datelor și autentificarea biometrică, până la certificate pinning, protecția împotriva ingineriei inverse și validarea input-urilor — totul cu exemple de cod practice, pe care le puteți folosi direct în proiectele voastre.

Stocarea Securizată a Datelor cu SecureStorage

Una dintre cele mai frecvente greșeli (și am văzut-o de prea multe ori în code review-uri) este stocarea datelor sensibile în format text simplu. .NET MAUI oferă API-ul SecureStorage, care abstractizează mecanismele native de stocare securizată ale fiecărei platforme. Practic, datele sensibile sunt protejate fără să trebuiască să reinventăm roata.

Cum Funcționează SecureStorage pe Fiecare Platformă

SecureStorage utilizează mecanisme diferite de criptare în funcție de platforma țintă:

  • Android (API 23+): Utilizează Android KeyStore pentru generarea unei chei AES, iar datele sunt criptate folosind cifrul AES/GCM/NoPadding. Cheile sunt stocate în hardware-ul securizat al dispozitivului (TEE — Trusted Execution Environment), ceea ce le face extrem de dificil de extras.
  • iOS: Valorile sunt stocate în KeyChain, mecanismul nativ Apple pentru stocarea securizată a credențialelor. KeyChain oferă criptare la nivel de hardware și este protejat de Secure Enclave.
  • Windows: Utilizează ApplicationData.Current.LocalSettings protejat prin DPAPI (Data Protection API), care leagă criptarea de contul utilizatorului Windows.

Important: SecureStorage e conceput doar pentru cantități mici de date text — token-uri de autentificare, chei API sau preferințe sensibile. Nu-l folosiți pentru seturi mari de date sau fișiere binare. Nu e făcut pentru asta.

Implementarea SecureStorage în Practică

Hai să vedem un serviciu complet pentru gestionarea securizată a credențialelor utilizatorului:

// Serviciu pentru gestionarea securizată a credențialelor
public class SecureCredentialService
{
    // Chei constante pentru stocare
    private const string AuthTokenKey = "auth_token";
    private const string RefreshTokenKey = "refresh_token";
    private const string UserIdKey = "user_id";
    private const string TokenExpiryKey = "token_expiry";

    /// <summary>
    /// Salvează token-ul de autentificare în mod securizat
    /// </summary>
    public async Task SaveAuthTokenAsync(string token, string refreshToken, DateTime expiry)
    {
        try
        {
            // Stocăm fiecare valoare separat în SecureStorage
            await SecureStorage.Default.SetAsync(AuthTokenKey, token);
            await SecureStorage.Default.SetAsync(RefreshTokenKey, refreshToken);
            await SecureStorage.Default.SetAsync(TokenExpiryKey,
                expiry.ToString("O")); // Format ISO 8601
        }
        catch (Exception ex)
        {
            // Pe unele dispozitive, SecureStorage poate eșua
            // (de exemplu, dispozitive rootate sau fără hardware securizat)
            System.Diagnostics.Debug.WriteLine(
                $"Eroare la salvarea token-ului: {ex.Message}");
            throw new SecurityException(
                "Nu s-a putut salva token-ul în mod securizat.", ex);
        }
    }

    /// <summary>
    /// Recuperează token-ul de autentificare
    /// </summary>
    public async Task<string?> GetAuthTokenAsync()
    {
        try
        {
            string? token = await SecureStorage.Default.GetAsync(AuthTokenKey);

            // Verificăm dacă token-ul nu a expirat
            string? expiryStr = await SecureStorage.Default.GetAsync(TokenExpiryKey);
            if (expiryStr != null && DateTime.TryParse(expiryStr, out DateTime expiry))
            {
                if (DateTime.UtcNow >= expiry)
                {
                    // Token-ul a expirat, îl ștergem
                    await ClearAllCredentialsAsync();
                    return null;
                }
            }

            return token;
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(
                $"Eroare la citirea token-ului: {ex.Message}");
            return null;
        }
    }

    /// <summary>
    /// Șterge toate credențialele stocate (la deconectare)
    /// </summary>
    public async Task ClearAllCredentialsAsync()
    {
        SecureStorage.Default.Remove(AuthTokenKey);
        SecureStorage.Default.Remove(RefreshTokenKey);
        SecureStorage.Default.Remove(UserIdKey);
        SecureStorage.Default.Remove(TokenExpiryKey);

        await Task.CompletedTask;
    }

    /// <summary>
    /// Verifică dacă utilizatorul are credențiale valide stocate
    /// </summary>
    public async Task<bool> HasValidCredentialsAsync()
    {
        string? token = await GetAuthTokenAsync();
        return !string.IsNullOrEmpty(token);
    }
}

Bune Practici pentru SecureStorage

Câteva reguli de aur pe care le-am învățat din experiență:

  • Stocați doar datele strict necesare — nu salvați parola utilizatorului, ci doar token-ul de sesiune
  • Implementați întotdeauna un mecanism de expirare a token-urilor
  • Gestionați corect excepțiile, deoarece SecureStorage poate eșua pe dispozitive rootate sau fără suport hardware
  • Ștergeți credențialele la deconectare și la detectarea unei posibile compromiteri
  • Nu stocați date mari — SecureStorage e optimizat pentru fragmente mici de text

Autentificarea Biometrică

Autentificarea biometrică (amprenta digitală, Face ID, recunoaștere facială) oferă un nivel suplimentar de securitate și, sincer, o experiență de utilizare mult mai plăcută. Nimeni nu vrea să tasteze o parolă lungă de fiecare dată. În .NET MAUI, implementarea se realizează prin pachetul NuGet Plugin.Maui.Biometric.

Configurare și Cerințe de Platformă

Plugin.Maui.Biometric suportă următoarele platforme:

  • iOS: versiunea 15.0 sau mai recentă
  • macOS: versiunea 12.0 sau mai recentă
  • Android: API 26 (Android 8.0 Oreo) sau mai recent
  • Windows: 10.0.17763.0 sau mai recent

Mai întâi, instalați pachetul și configurați permisiunile necesare:

<!-- În fișierul .csproj, adăugați referința NuGet -->
<ItemGroup>
    <PackageReference Include="Plugin.Maui.Biometric" Version="1.2.0" />
</ItemGroup>

<!-- Android: AndroidManifest.xml - adăugați permisiunea -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />

<!-- iOS: Info.plist - adăugați descrierea pentru Face ID -->
<key>NSFaceIDUsageDescription</key>
<string>Aplicația folosește Face ID pentru autentificare securizată.</string>

Implementarea Completă a Autentificării Biometrice

Iată serviciul complet — și da, merită să gestionați corect toate cazurile de eroare:

// Serviciu de autentificare biometrică
public class BiometricAuthService
{
    private readonly IBiometric _biometric;

    public BiometricAuthService(IBiometric biometric)
    {
        _biometric = biometric;
    }

    /// <summary>
    /// Verifică dacă autentificarea biometrică este disponibilă pe dispozitiv
    /// </summary>
    public async Task<BiometricAvailabilityResult> CheckAvailabilityAsync()
    {
        // Verificăm tipul de biometrie disponibil
        var result = await _biometric.GetAuthenticationStatusAsync();

        return new BiometricAvailabilityResult
        {
            IsAvailable = result == BiometricStatus.Available,
            Status = result
        };
    }

    /// <summary>
    /// Solicită autentificarea biometrică de la utilizator
    /// </summary>
    public async Task<BiometricAuthResult> AuthenticateAsync(
        string title = "Autentificare Necesară",
        string subtitle = "Confirmați identitatea folosind datele biometrice")
    {
        try
        {
            // Verificăm mai întâi disponibilitatea
            var availability = await CheckAvailabilityAsync();
            if (!availability.IsAvailable)
            {
                return new BiometricAuthResult
                {
                    IsSuccess = false,
                    ErrorMessage = "Autentificarea biometrică nu este " +
                        "disponibilă pe acest dispozitiv."
                };
            }

            // Configurăm cererea de autentificare
            var request = new AuthenticationRequest
            {
                Title = title,
                Subtitle = subtitle,
                NegativeText = "Anulare",
                // Permite fallback la PIN/parolă dacă biometria eșuează
                AllowPasswordAuth = true
            };

            // Solicităm autentificarea
            var authResult = await _biometric.AuthenticateAsync(request);

            return new BiometricAuthResult
            {
                IsSuccess = authResult.Status == BiometricResponseStatus.Success,
                ErrorMessage = authResult.Status != BiometricResponseStatus.Success
                    ? GetErrorMessage(authResult.Status)
                    : null
            };
        }
        catch (Exception ex)
        {
            return new BiometricAuthResult
            {
                IsSuccess = false,
                ErrorMessage = $"Eroare la autentificare: {ex.Message}"
            };
        }
    }

    /// <summary>
    /// Traduce codurile de eroare în mesaje pentru utilizator
    /// </summary>
    private string GetErrorMessage(BiometricResponseStatus status)
    {
        return status switch
        {
            BiometricResponseStatus.Failed =>
                "Autentificarea biometrică a eșuat. Încercați din nou.",
            BiometricResponseStatus.Canceled =>
                "Autentificarea a fost anulată de utilizator.",
            BiometricResponseStatus.TooManyAttempts =>
                "Prea multe încercări eșuate. Așteptați înainte de a reîncerca.",
            BiometricResponseStatus.NotAvailable =>
                "Autentificarea biometrică nu este configurată pe acest dispozitiv.",
            _ => "A apărut o eroare necunoscută la autentificare."
        };
    }
}

// Modele de rezultat
public class BiometricAvailabilityResult
{
    public bool IsAvailable { get; set; }
    public BiometricStatus Status { get; set; }
}

public class BiometricAuthResult
{
    public bool IsSuccess { get; set; }
    public string? ErrorMessage { get; set; }
}

Integrarea în Fluxul Aplicației

Acum, partea interesantă — cum integrăm autentificarea biometrică în pagina de login:

// Într-un ViewModel sau code-behind al paginii de autentificare
public class LoginViewModel : ObservableObject
{
    private readonly BiometricAuthService _biometricService;
    private readonly SecureCredentialService _credentialService;

    public LoginViewModel(
        BiometricAuthService biometricService,
        SecureCredentialService credentialService)
    {
        _biometricService = biometricService;
        _credentialService = credentialService;

        // Verificăm dacă putem oferi autentificare biometrică
        CheckBiometricAvailability();
    }

    private bool _isBiometricAvailable;
    public bool IsBiometricAvailable
    {
        get => _isBiometricAvailable;
        set => SetProperty(ref _isBiometricAvailable, value);
    }

    private async void CheckBiometricAvailability()
    {
        var result = await _biometricService.CheckAvailabilityAsync();
        IsBiometricAvailable = result.IsAvailable;
    }

    /// <summary>
    /// Comandă pentru autentificare biometrică, legată de butonul din UI
    /// </summary>
    [RelayCommand]
    private async Task LoginWithBiometricAsync()
    {
        var authResult = await _biometricService.AuthenticateAsync(
            "Autentificare",
            "Folosiți amprenta sau Face ID pentru a vă conecta");

        if (authResult.IsSuccess)
        {
            // Verificăm dacă avem un token valid stocat
            bool hasCredentials = await _credentialService
                .HasValidCredentialsAsync();

            if (hasCredentials)
            {
                // Navigăm la pagina principală
                await Shell.Current.GoToAsync("//MainPage");
            }
            else
            {
                // Token-ul a expirat, necesită autentificare completă
                await Shell.Current.DisplayAlert("Sesiune Expirată",
                    "Vă rugăm să vă autentificați cu email și parolă.", "OK");
            }
        }
        else
        {
            await Shell.Current.DisplayAlert("Eroare",
                authResult.ErrorMessage ?? "Autentificarea a eșuat.", "OK");
        }
    }
}

Certificate Pinning și Comunicare Securizată

Comunicarea dintre aplicația mobilă și serverul backend este, sincer, unul dintre cele mai vulnerabile puncte din întreg lanțul de securitate. Atacurile de tip Man-in-the-Middle (MITM) permit unui atacator să intercepteze, să citească și chiar să modifice traficul dintre aplicație și server — chiar și atunci când se utilizează HTTPS.

Da, ați citit bine. HTTPS singur nu e suficient.

Ce Sunt Atacurile MITM și De Ce Certificate Pinning

Într-un atac MITM, atacatorul se interpune între dispozitivul utilizatorului și server, prezentând un certificat SSL fals. Dacă aplicația acceptă orice certificat valid din perspectiva sistemului de operare, atacatorul poate intercepta tot traficul. Certificate Pinning rezolvă această problemă prin verificarea faptului că certificatul serverului corespunde exact celui așteptat de aplicație.

Recomandare: Fixați hash-ul cheii publice (SPKI — Subject Public Key Info) în loc de certificatul complet. Asta permite rotația certificatelor fără actualizarea aplicației, atâta timp cât cheia publică rămâne aceeași.

Implementarea Certificate Pinning în .NET MAUI

// Handler personalizat pentru validarea certificatelor
public class CertificatePinningHandler : HttpClientHandler
{
    // Hash-urile SHA256 ale cheilor publice acceptate
    // Includeți cel puțin două: certificatul actual și unul de rezervă
    private static readonly string[] PinnedPublicKeyHashes = new[]
    {
        "sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=", // Certificat principal
        "sha256/sRHdihwgkaib1P1gN7akTYqp2Ir2ApHqkWP7WK6BMgM="  // Certificat de rezervă
    };

    // Domeniul serverului nostru
    private const string PinnedDomain = "api.aplicatia-mea.ro";

    public CertificatePinningHandler()
    {
        // Configurăm validarea personalizată a certificatelor
        ServerCertificateCustomValidationCallback = ValidateCertificate;
    }

    /// <summary>
    /// Validează certificatul serverului contra hash-urilor fixate
    /// </summary>
    private bool ValidateCertificate(
        HttpRequestMessage request,
        System.Security.Cryptography.X509Certificates.X509Certificate2? certificate,
        System.Security.Cryptography.X509Certificates.X509Chain? chain,
        System.Net.Security.SslPolicyErrors sslErrors)
    {
        // Verificăm dacă cererea este pentru domeniul nostru
        if (request.RequestUri?.Host != PinnedDomain)
        {
            // Pentru alte domenii, aplicăm validarea standard
            return sslErrors == System.Net.Security.SslPolicyErrors.None;
        }

        // Verificăm mai întâi erorile SSL standard
        if (sslErrors != System.Net.Security.SslPolicyErrors.None)
        {
            System.Diagnostics.Debug.WriteLine(
                $"Eroare SSL detectată: {sslErrors}");
            return false;
        }

        if (certificate == null)
        {
            System.Diagnostics.Debug.WriteLine("Certificat lipsă.");
            return false;
        }

        // Calculăm hash-ul cheii publice din certificat
        byte[] publicKeyBytes = certificate.GetPublicKey();
        using var sha256 = System.Security.Cryptography.SHA256.Create();
        byte[] hashBytes = sha256.ComputeHash(publicKeyBytes);
        string publicKeyHash = "sha256/" + Convert.ToBase64String(hashBytes);

        // Verificăm dacă hash-ul se potrivește cu cel fixat
        bool isPinned = PinnedPublicKeyHashes.Contains(publicKeyHash);

        if (!isPinned)
        {
            System.Diagnostics.Debug.WriteLine(
                $"Certificate pinning eșuat! Hash primit: {publicKeyHash}");
        }

        return isPinned;
    }
}

// Înregistrarea în MauiProgram.cs folosind HttpClientFactory
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>();

        // Configurăm HttpClient cu certificate pinning
        builder.Services.AddHttpClient("SecureApiClient", client =>
        {
            client.BaseAddress = new Uri("https://api.aplicatia-mea.ro");
            client.DefaultRequestHeaders.Add("Accept", "application/json");
            client.Timeout = TimeSpan.FromSeconds(30);
        })
        .ConfigurePrimaryHttpMessageHandler(() => new CertificatePinningHandler());

        // Înregistrăm serviciul API care folosește clientul securizat
        builder.Services.AddTransient<IApiService, ApiService>();

        return builder.Build();
    }
}

Configurare Android cu network_security_config.xml

Pe Android, puteți (și chiar ar trebui) să configurați certificate pinning suplimentar la nivel de platformă:

<!-- Fișier: Platforms/Android/Resources/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- Configurare generală: nu permite trafic în clar -->
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </base-config>

    <!-- Configurare specifică pentru domeniul API-ului nostru -->
    <domain-config>
        <domain includeSubdomains="true">api.aplicatia-mea.ro</domain>
        <pin-set expiration="2025-12-31">
            <!-- Hash-ul SHA256 al cheii publice - certificat principal -->
            <pin digest="SHA-256">
                YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=
            </pin>
            <!-- Hash-ul SHA256 al cheii publice - certificat de rezervă -->
            <pin digest="SHA-256">
                sRHdihwgkaib1P1gN7akTYqp2Ir2ApHqkWP7WK6BMgM=
            </pin>
        </pin-set>
    </domain-config>
</network-security-config>
<!-- În AndroidManifest.xml, referențiați configurația -->
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>
</application>

Protecția Împotriva Ingineriei Inverse

Aplicațiile .NET MAUI, fiind compilate în limbaj intermediar (IL), pot fi decompilate cu ușurință folosind instrumente precum ILSpy sau dnSpy. Fără măsuri suplimentare de protecție, un atacator poate accesa logica de afaceri, cheile API hardcodate și algoritmii proprietari. E un scenariu pe care nu vreți să-l experimentați pe pielea voastră.

Obfuscarea Codului cu dotnet-obfuscator

Obfuscarea transformă codul compilat într-o formă dificil de înțeles, fără a afecta funcționalitatea. Procesul include redenumirea claselor și metodelor, criptarea string-urilor și restructurarea fluxului de control. Gândiți-vă la asta ca la un lacăt suplimentar pe ușa din față — nu e impenetrabil, dar descurajează majoritatea atacatorilor.

// Exemplu de configurare a obfuscării în fișierul .csproj
// Adăugați referința la pachetul de obfuscare
// și configurați regulile de protecție

/*
  În fișierul .csproj, adăugați:

  <PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <EnableObfuscation>true</EnableObfuscation>
    <Obfuscate>true</Obfuscate>
    <DebugSymbols>false</DebugSymbols>
    <DebugType>none</DebugType>
  </PropertyGroup>
*/

// Exemplu: Clasă înainte și după obfuscare
// ÎNAINTE:
public class PaymentProcessor
{
    private string _apiKey = "sk_live_abc123";

    public async Task<bool> ProcessPayment(decimal amount, string cardToken)
    {
        // Logica de procesare a plății
        var client = new HttpClient();
        var response = await client.PostAsync(
            "https://api.payment.com/charge",
            new StringContent($"amount={amount}&token={cardToken}"));
        return response.IsSuccessStatusCode;
    }
}

// DUPĂ OBFUSCARE (aproximare a rezultatului):
// public class a
// {
//     private string b = /* string criptată */;
//     public async Task<bool> c(decimal d, string e) { ... }
// }

// IMPORTANT: Nu hardcodați chei API în cod!
// Folosiți în schimb o abordare securizată:
public class SecureConfigService
{
    /// <summary>
    /// Obține cheia API din SecureStorage sau de la server
    /// Nu stocați chei direct în codul sursă!
    /// </summary>
    public async Task<string> GetApiKeyAsync()
    {
        // Încercăm să obținem cheia din SecureStorage
        string? apiKey = await SecureStorage.Default.GetAsync("payment_api_key");

        if (string.IsNullOrEmpty(apiKey))
        {
            // Obținem cheia de la serverul de configurare
            // (necesită autentificare prealabilă)
            apiKey = await FetchApiKeyFromServerAsync();
            await SecureStorage.Default.SetAsync("payment_api_key", apiKey);
        }

        return apiKey;
    }

    private async Task<string> FetchApiKeyFromServerAsync()
    {
        // Implementare pentru obținerea cheii de la server
        throw new NotImplementedException(
            "Implementați comunicarea cu serverul de configurare.");
    }
}

Dezactivarea Depanării în Versiunile de Release

Asta e ceva ce mulți dezvoltatori uită (sau amână). Nu lăsați depanarea activată în producție:

// În App.xaml.cs sau în punctul de intrare al aplicației
public partial class App : Application
{
    public App()
    {
        InitializeComponent();

#if !DEBUG
        // Prevenim atașarea unui debugger în versiunea de release
        PreventDebugging();
#endif
    }

    private void PreventDebugging()
    {
#if ANDROID
        // Pe Android, verificăm dacă aplicația este depanată
        if (Android.OS.Debug.IsDebuggerConnected)
        {
            // Închideți aplicația sau luați măsuri de protecție
            System.Diagnostics.Process.GetCurrentProcess().Kill();
        }
#endif
        // Verificare generică .NET
        if (System.Diagnostics.Debugger.IsAttached)
        {
            System.Diagnostics.Debug.WriteLine(
                "AVERTISMENT: Debugger detectat în versiunea de release!");
            // Luați măsurile necesare
        }
    }
}

Configurări Specifice de Platformă pentru Protecție

<!-- Configurare .csproj pentru protecție la compilare -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <!-- Dezactivăm simbolurile de depanare -->
    <DebugSymbols>false</DebugSymbols>
    <DebugType>none</DebugType>

    <!-- Activăm compilarea AOT (Ahead of Time) pentru performanță
         și protecție suplimentară -->
    <RunAOTCompilation>true</RunAOTCompilation>

    <!-- Activăm trimming-ul pentru a elimina codul neutilizat -->
    <PublishTrimmed>true</PublishTrimmed>
    <TrimMode>link</TrimMode>

    <!-- Android: activăm R8/ProGuard pentru obfuscare -->
    <AndroidLinkTool>r8</AndroidLinkTool>
    <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
</PropertyGroup>

Gestionarea Securizată a Token-urilor și Sesiunilor

Gestionarea corectă a token-urilor de autentificare e esențială. Punct. Fluxul OAuth 2.0 cu OpenID Connect (OIDC) este standardul recomandat pentru autentificarea în aplicațiile mobile, și pe bună dreptate.

Implementarea Fluxului OAuth 2.0 cu PKCE

PKCE (Proof Key for Code Exchange) e obligatoriu pentru aplicațiile mobile, deoarece acestea nu pot stoca în siguranță un client secret. Dacă nu folosiți PKCE, practic lăsați ușa întredeschisă.

// Serviciu complet de autentificare OAuth 2.0 cu PKCE
public class OAuthService
{
    private readonly HttpClient _httpClient;
    private readonly SecureCredentialService _credentialService;

    // Configurare OAuth
    private const string AuthorizationEndpoint =
        "https://auth.aplicatia-mea.ro/authorize";
    private const string TokenEndpoint =
        "https://auth.aplicatia-mea.ro/token";
    private const string ClientId = "maui-mobile-app";
    private const string RedirectUri = "myapp://callback";
    private const string Scope = "openid profile email offline_access";

    public OAuthService(
        IHttpClientFactory httpClientFactory,
        SecureCredentialService credentialService)
    {
        _httpClient = httpClientFactory.CreateClient("SecureApiClient");
        _credentialService = credentialService;
    }

    /// <summary>
    /// Inițiază fluxul de autentificare OAuth 2.0 cu PKCE
    /// </summary>
    public async Task<AuthResult> LoginAsync()
    {
        // Generăm code_verifier și code_challenge pentru PKCE
        string codeVerifier = GenerateCodeVerifier();
        string codeChallenge = GenerateCodeChallenge(codeVerifier);

        // Construim URL-ul de autorizare
        string authUrl = $"{AuthorizationEndpoint}?" +
            $"response_type=code&" +
            $"client_id={Uri.EscapeDataString(ClientId)}&" +
            $"redirect_uri={Uri.EscapeDataString(RedirectUri)}&" +
            $"scope={Uri.EscapeDataString(Scope)}&" +
            $"code_challenge={codeChallenge}&" +
            $"code_challenge_method=S256&" +
            $"state={Guid.NewGuid()}";

        try
        {
            // Deschidem browser-ul pentru autentificare
            var authResult = await WebAuthenticator.Default.AuthenticateAsync(
                new Uri(authUrl),
                new Uri(RedirectUri));

            // Extragem codul de autorizare din răspuns
            string? code = authResult?.Properties
                .FirstOrDefault(p => p.Key == "code").Value;

            if (string.IsNullOrEmpty(code))
            {
                return new AuthResult { IsSuccess = false,
                    Error = "Nu s-a primit codul de autorizare." };
            }

            // Schimbăm codul de autorizare pe token-uri
            return await ExchangeCodeForTokensAsync(code, codeVerifier);
        }
        catch (TaskCanceledException)
        {
            return new AuthResult { IsSuccess = false,
                Error = "Autentificarea a fost anulată." };
        }
    }

    /// <summary>
    /// Schimbă codul de autorizare pe access_token și refresh_token
    /// </summary>
    private async Task<AuthResult> ExchangeCodeForTokensAsync(
        string code, string codeVerifier)
    {
        var tokenRequest = new Dictionary<string, string>
        {
            ["grant_type"] = "authorization_code",
            ["code"] = code,
            ["redirect_uri"] = RedirectUri,
            ["client_id"] = ClientId,
            ["code_verifier"] = codeVerifier
        };

        var response = await _httpClient.PostAsync(TokenEndpoint,
            new FormUrlEncodedContent(tokenRequest));

        if (!response.IsSuccessStatusCode)
        {
            return new AuthResult { IsSuccess = false,
                Error = "Schimbul de token-uri a eșuat." };
        }

        var tokenResponse = await response.Content
            .ReadFromJsonAsync<TokenResponse>();

        if (tokenResponse == null)
        {
            return new AuthResult { IsSuccess = false,
                Error = "Răspunsul serverului este invalid." };
        }

        // Salvăm token-urile în mod securizat
        DateTime expiry = DateTime.UtcNow
            .AddSeconds(tokenResponse.ExpiresIn);
        await _credentialService.SaveAuthTokenAsync(
            tokenResponse.AccessToken,
            tokenResponse.RefreshToken ?? "",
            expiry);

        return new AuthResult
        {
            IsSuccess = true,
            AccessToken = tokenResponse.AccessToken
        };
    }

    /// <summary>
    /// Reîmprospătează token-ul de acces folosind refresh_token
    /// </summary>
    public async Task<AuthResult> RefreshTokenAsync()
    {
        string? refreshToken = await SecureStorage.Default
            .GetAsync("refresh_token");

        if (string.IsNullOrEmpty(refreshToken))
        {
            return new AuthResult { IsSuccess = false,
                Error = "Nu există refresh token. Autentificare necesară." };
        }

        var tokenRequest = new Dictionary<string, string>
        {
            ["grant_type"] = "refresh_token",
            ["refresh_token"] = refreshToken,
            ["client_id"] = ClientId
        };

        var response = await _httpClient.PostAsync(TokenEndpoint,
            new FormUrlEncodedContent(tokenRequest));

        if (!response.IsSuccessStatusCode)
        {
            // Refresh token-ul a expirat sau a fost revocat
            await _credentialService.ClearAllCredentialsAsync();
            return new AuthResult { IsSuccess = false,
                Error = "Sesiunea a expirat. Autentificare necesară." };
        }

        var tokenResponse = await response.Content
            .ReadFromJsonAsync<TokenResponse>();

        if (tokenResponse != null)
        {
            DateTime expiry = DateTime.UtcNow
                .AddSeconds(tokenResponse.ExpiresIn);
            await _credentialService.SaveAuthTokenAsync(
                tokenResponse.AccessToken,
                tokenResponse.RefreshToken ?? refreshToken,
                expiry);
        }

        return new AuthResult
        {
            IsSuccess = true,
            AccessToken = tokenResponse?.AccessToken
        };
    }

    // Metode helper pentru PKCE
    private string GenerateCodeVerifier()
    {
        byte[] bytes = new byte[32];
        using var rng = System.Security.Cryptography
            .RandomNumberGenerator.Create();
        rng.GetBytes(bytes);
        return Convert.ToBase64String(bytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }

    private string GenerateCodeChallenge(string codeVerifier)
    {
        using var sha256 = System.Security.Cryptography.SHA256.Create();
        byte[] challengeBytes = sha256.ComputeHash(
            System.Text.Encoding.UTF8.GetBytes(codeVerifier));
        return Convert.ToBase64String(challengeBytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }
}

// Modele pentru răspunsuri
public class TokenResponse
{
    [System.Text.Json.Serialization.JsonPropertyName("access_token")]
    public string AccessToken { get; set; } = string.Empty;

    [System.Text.Json.Serialization.JsonPropertyName("refresh_token")]
    public string? RefreshToken { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("expires_in")]
    public int ExpiresIn { get; set; }

    [System.Text.Json.Serialization.JsonPropertyName("token_type")]
    public string TokenType { get; set; } = "Bearer";
}

public class AuthResult
{
    public bool IsSuccess { get; set; }
    public string? AccessToken { get; set; }
    public string? Error { get; set; }
}

DelegatingHandler pentru Reîmprospătarea Automată a Token-urilor

Aceasta e o componentă pe care o consider esențială în orice aplicație mobilă serioasă. Handler-ul de mai jos interceptează răspunsurile 401 și reîmprospătează automat token-ul, fără ca utilizatorul să fie deranjat:

// Handler care reîmprospătează automat token-ul la primirea unui 401
public class TokenRefreshHandler : DelegatingHandler
{
    private readonly OAuthService _oauthService;
    private readonly SecureCredentialService _credentialService;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);

    public TokenRefreshHandler(
        OAuthService oauthService,
        SecureCredentialService credentialService)
    {
        _oauthService = oauthService;
        _credentialService = credentialService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        // Adăugăm token-ul de acces la cerere
        string? token = await _credentialService.GetAuthTokenAsync();
        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization =
                new System.Net.Http.Headers.AuthenticationHeaderValue(
                    "Bearer", token);
        }

        // Trimitem cererea
        var response = await base.SendAsync(request, cancellationToken);

        // Dacă primim 401, încercăm să reîmprospătăm token-ul
        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            // Folosim un semafor pentru a preveni reîmprospătări multiple
            await _refreshLock.WaitAsync(cancellationToken);
            try
            {
                var refreshResult = await _oauthService.RefreshTokenAsync();
                if (refreshResult.IsSuccess)
                {
                    // Retrimitem cererea cu noul token
                    request.Headers.Authorization =
                        new System.Net.Http.Headers.AuthenticationHeaderValue(
                            "Bearer", refreshResult.AccessToken);
                    response = await base.SendAsync(request, cancellationToken);
                }
            }
            finally
            {
                _refreshLock.Release();
            }
        }

        return response;
    }
}

Validarea Input-urilor și Protecția Împotriva Injecțiilor

Validarea datelor introduse de utilizator este, fără exagerare, prima linie de apărare împotriva multor tipuri de atacuri. Vorbim de injecții SQL, Cross-Site Scripting (XSS) și manipularea datelor. Conform OWASP M4, această vulnerabilitate rămâne una dintre cele mai exploatate în aplicațiile mobile.

Am văzut aplicații în producție care concatenau direct input-ul utilizatorului în query-uri SQL. Nu faceți asta. Niciodată.

Modele de Validare a Input-urilor

// Serviciu centralizat de validare a input-urilor
public static class InputValidator
{
    // Expresii regulate pentru validare
    private static readonly Regex EmailRegex = new(
        @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
        RegexOptions.Compiled);

    private static readonly Regex PhoneRegex = new(
        @"^\+?[0-9]{10,15}$",
        RegexOptions.Compiled);

    // Caractere potențial periculoase pentru SQL injection
    private static readonly char[] DangerousChars =
        { '\'', '"', ';', '-', '/', '*', '\\' };

    /// <summary>
    /// Validează și sanitizează adresa de email
    /// </summary>
    public static ValidationResult ValidateEmail(string? email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return ValidationResult.Error(
                "Adresa de email este obligatorie.");

        // Limităm lungimea pentru a preveni atacuri DoS
        if (email.Length > 254)
            return ValidationResult.Error(
                "Adresa de email este prea lungă.");

        // Sanitizăm - eliminăm spațiile din față și din spate
        email = email.Trim().ToLowerInvariant();

        if (!EmailRegex.IsMatch(email))
            return ValidationResult.Error(
                "Formatul adresei de email este invalid.");

        return ValidationResult.Success(email);
    }

    /// <summary>
    /// Validează input-uri text generice, prevenind injecțiile
    /// </summary>
    public static ValidationResult ValidateTextInput(
        string? input,
        string fieldName,
        int maxLength = 500,
        bool allowHtml = false)
    {
        if (string.IsNullOrWhiteSpace(input))
            return ValidationResult.Error(
                $"Câmpul {fieldName} este obligatoriu.");

        if (input.Length > maxLength)
            return ValidationResult.Error(
                $"Câmpul {fieldName} nu poate depăși {maxLength} caractere.");

        // Eliminăm caracterele de control
        input = new string(input.Where(
            c => !char.IsControl(c) || c == '\n' || c == '\r').ToArray());

        if (!allowHtml)
        {
            // Codificăm HTML pentru a preveni XSS
            input = System.Net.WebUtility.HtmlEncode(input);
        }

        return ValidationResult.Success(input);
    }

    /// <summary>
    /// Validează parola conform cerințelor de securitate
    /// </summary>
    public static ValidationResult ValidatePassword(string? password)
    {
        if (string.IsNullOrEmpty(password))
            return ValidationResult.Error("Parola este obligatorie.");

        var errors = new List<string>();

        if (password.Length < 8)
            errors.Add("Parola trebuie să aibă minim 8 caractere.");
        if (!password.Any(char.IsUpper))
            errors.Add("Parola trebuie să conțină cel puțin o literă mare.");
        if (!password.Any(char.IsLower))
            errors.Add("Parola trebuie să conțină cel puțin o literă mică.");
        if (!password.Any(char.IsDigit))
            errors.Add("Parola trebuie să conțină cel puțin o cifră.");
        if (!password.Any(c => !char.IsLetterOrDigit(c)))
            errors.Add(
                "Parola trebuie să conțină cel puțin un caracter special.");

        return errors.Count > 0
            ? ValidationResult.Error(string.Join(" ", errors))
            : ValidationResult.Success(password);
    }
}

// Clasa de rezultat al validării
public class ValidationResult
{
    public bool IsValid { get; private set; }
    public string? SanitizedValue { get; private set; }
    public string? ErrorMessage { get; private set; }

    public static ValidationResult Success(string sanitizedValue) =>
        new() { IsValid = true, SanitizedValue = sanitizedValue };

    public static ValidationResult Error(string errorMessage) =>
        new() { IsValid = false, ErrorMessage = errorMessage };
}

Prevenirea Injecțiilor SQL cu Interogări Parametrizate

Dacă aplicația voastră folosește SQLite local, interogările parametrizate sunt absolut esențiale. Nu e negociabil:

// Serviciu de acces la date cu protecție împotriva injecțiilor SQL
public class SecureDatabaseService
{
    private readonly SQLiteAsyncConnection _database;

    public SecureDatabaseService(string dbPath)
    {
        _database = new SQLiteAsyncConnection(dbPath,
            SQLiteOpenFlags.ReadWrite |
            SQLiteOpenFlags.Create |
            SQLiteOpenFlags.SharedCache);
    }

    /// <summary>
    /// GREȘIT - Vulnerabil la injecții SQL!
    /// Nu folosiți niciodată concatenarea de string-uri în interogări!
    /// </summary>
    public async Task<List<User>> SearchUsersUnsafe_NUFOLOSITI(string name)
    {
        // PERICOL: Dacă name = "'; DROP TABLE Users; --"
        // interogarea devine destructivă!
        string query = $"SELECT * FROM Users WHERE Name = '{name}'";
        return await _database.QueryAsync<User>(query);
    }

    /// <summary>
    /// CORECT - Folosim interogări parametrizate
    /// Parametrii sunt escape-uiți automat de SQLite
    /// </summary>
    public async Task<List<User>> SearchUsersSecure(string name)
    {
        // Validăm mai întâi input-ul
        var validation = InputValidator.ValidateTextInput(
            name, "Nume", maxLength: 100);
        if (!validation.IsValid)
        {
            throw new ArgumentException(validation.ErrorMessage);
        }

        // Interogare parametrizată - sigură împotriva injecțiilor
        return await _database.QueryAsync<User>(
            "SELECT * FROM Users WHERE Name = ?",
            validation.SanitizedValue);
    }

    /// <summary>
    /// Inserare securizată cu validare completă
    /// </summary>
    public async Task<int> InsertUserSecure(string name, string email)
    {
        // Validăm toate input-urile
        var nameValidation = InputValidator.ValidateTextInput(
            name, "Nume", maxLength: 100);
        var emailValidation = InputValidator.ValidateEmail(email);

        if (!nameValidation.IsValid)
            throw new ArgumentException(nameValidation.ErrorMessage);
        if (!emailValidation.IsValid)
            throw new ArgumentException(emailValidation.ErrorMessage);

        // Folosim ORM-ul SQLite-net care parametrizează automat
        var user = new User
        {
            Name = nameValidation.SanitizedValue!,
            Email = emailValidation.SanitizedValue!,
            CreatedAt = DateTime.UtcNow
        };

        return await _database.InsertAsync(user);
    }
}

// Model de date
[SQLite.Table("Users")]
public class User
{
    [SQLite.PrimaryKey, SQLite.AutoIncrement]
    public int Id { get; set; }

    [SQLite.MaxLength(100)]
    public string Name { get; set; } = string.Empty;

    [SQLite.MaxLength(254)]
    public string Email { get; set; } = string.Empty;

    public DateTime CreatedAt { get; set; }
}

Prevenirea XSS în WebView

Dacă aplicația voastră folosește WebView pentru a afișa conținut, sanitizarea e critică. Un singur tag <script> nesanitizat poate compromite întreaga aplicație:

// Serviciu de sanitizare pentru conținut afișat în WebView
public static class XssProtection
{
    /// <summary>
    /// Sanitizează conținutul HTML înainte de afișarea în WebView
    /// Elimină tag-urile și atributele potențial periculoase
    /// </summary>
    public static string SanitizeHtmlContent(string? htmlContent)
    {
        if (string.IsNullOrEmpty(htmlContent))
            return string.Empty;

        // Eliminăm tag-urile script
        htmlContent = Regex.Replace(htmlContent,
            @"<script[^>]*>[\s\S]*?</script>",
            string.Empty,
            RegexOptions.IgnoreCase);

        // Eliminăm event handler-ele inline (onclick, onerror, etc.)
        htmlContent = Regex.Replace(htmlContent,
            @"\s+on\w+\s*=\s*""[^""]*""",
            string.Empty,
            RegexOptions.IgnoreCase);

        // Eliminăm link-urile javascript:
        htmlContent = Regex.Replace(htmlContent,
            @"javascript\s*:",
            string.Empty,
            RegexOptions.IgnoreCase);

        // Eliminăm tag-urile iframe
        htmlContent = Regex.Replace(htmlContent,
            @"<iframe[^>]*>[\s\S]*?</iframe>",
            string.Empty,
            RegexOptions.IgnoreCase);

        return htmlContent;
    }

    /// <summary>
    /// Configurează WebView-ul cu restricții de securitate
    /// </summary>
    public static void ConfigureSecureWebView(WebView webView)
    {
        // Dezactivăm JavaScript dacă nu este strict necesar
        // Activați doar dacă funcționalitatea o cere
#if ANDROID
        var androidWebView = webView.Handler?.PlatformView
            as Android.Webkit.WebView;
        if (androidWebView != null)
        {
            androidWebView.Settings.JavaScriptEnabled = false;
            androidWebView.Settings.AllowFileAccess = false;
            androidWebView.Settings.AllowContentAccess = false;
        }
#endif
    }
}

Securitatea la Nivel de Platformă

Fiecare platformă mobilă are mecanisme specifice de securitate care trebuie configurate corect. Am constatat că mulți dezvoltatori se concentrează pe codul C# și uită complet de configurările specifice platformei. E o greșeală costisitoare.

Android: ProGuard, R8 și Configurări de Securitate

Pe Android, R8 (succesorul ProGuard) oferă obfuscare, optimizare și eliminarea codului nefolosit. Configurarea corectă a R8 e esențială pentru a nu lăsa aplicația expusă:

// În fișierul .csproj, activați R8 pentru Android
/*
<PropertyGroup Condition="$(TargetFramework.Contains('android'))
    and '$(Configuration)' == 'Release'">
    <AndroidLinkTool>r8</AndroidLinkTool>
    <AndroidR8IgnoreWarnings>true</AndroidR8IgnoreWarnings>
    <AndroidEnableProfiledAot>true</AndroidEnableProfiledAot>
    <EnableLLVM>true</EnableLLVM>
</PropertyGroup>
*/
<!-- proguard.cfg - Reguli personalizate pentru ProGuard/R8 -->
<!-- Păstrăm clasele necesare pentru reflexie -->
-keep class com.compania.aplicatia.Models.** { *; }
-keep class com.compania.aplicatia.Services.** { *; }

<!-- Obfuscăm restul codului -->
-optimizationpasses 5
-allowaccessmodification

<!-- Prevenim decompilarea ușoară -->
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
<!-- AndroidManifest.xml - Configurări de securitate esențiale -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:allowBackup="false"
        android:networkSecurityConfig="@xml/network_security_config"
        android:usesCleartextTraffic="false"
        android:debuggable="false">

        <!-- Prevenim exportarea accidentală a componentelor -->
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <!-- Permisiuni minime necesare -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.USE_BIOMETRIC" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

iOS: App Transport Security și Keychain Groups

Pe iOS, App Transport Security (ATS) impune utilizarea HTTPS pentru toate conexiunile. Nu dezactivați ATS în producție — e o tentație pe care am văzut-o des, dar rezistați.

<!-- Info.plist - Configurări de securitate pentru iOS -->
<dict>
    <!-- App Transport Security - NU dezactivați în producție! -->
    <key>NSAppTransportSecurity</key>
    <dict>
        <!-- Forțăm HTTPS pentru toate conexiunile -->
        <key>NSAllowsArbitraryLoads</key>
        <false/>

        <!-- Excepții doar pentru domenii specifice dacă este necesar -->
        <key>NSExceptionDomains</key>
        <dict>
            <key>api.aplicatia-mea.ro</key>
            <dict>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <true/>
                <key>NSExceptionMinimumTLSVersion</key>
                <string>TLSv1.2</string>
                <key>NSRequiresCertificateTransparency</key>
                <true/>
            </dict>
        </dict>
    </dict>

    <!-- Descriere pentru Face ID -->
    <key>NSFaceIDUsageDescription</key>
    <string>Aplicația folosește Face ID pentru autentificare securizată.</string>

    <!-- Keychain Sharing pentru grupuri de aplicații -->
    <key>keychain-access-groups</key>
    <array>
        <string>$(AppIdentifierPrefix)com.compania.aplicatia</string>
    </array>
</dict>
// Configurări specifice iOS pentru securitate avansată
#if IOS
public static class iOSSecurityConfig
{
    /// <summary>
    /// Configurează protecția datelor la nivel de fișier pe iOS
    /// </summary>
    public static void ConfigureDataProtection()
    {
        // Setăm nivelul de protecție pentru fișierele sensibile
        var documentsPath = Environment.GetFolderPath(
            Environment.SpecialFolder.MyDocuments);
        var dbPath = Path.Combine(documentsPath, "app_data.db");

        // NSFileProtectionComplete - fișierul este accesibil
        // doar când dispozitivul este deblocat
        var attributes = new NSDictionary(
            NSFileManager.FileProtectionKey,
            NSFileProtection.Complete);

        NSFileManager.DefaultManager.SetAttributes(
            attributes, dbPath, out NSError error);

        if (error != null)
        {
            System.Diagnostics.Debug.WriteLine(
                $"Eroare la setarea protecției fișierului: {error}");
        }
    }

    /// <summary>
    /// Detectează dacă dispozitivul iOS este jailbroken
    /// </summary>
    public static bool IsDeviceJailbroken()
    {
        // Verificăm existența fișierelor specifice jailbreak
        string[] jailbreakPaths = new[]
        {
            "/Applications/Cydia.app",
            "/Library/MobileSubstrate/MobileSubstrate.dylib",
            "/bin/bash",
            "/usr/sbin/sshd",
            "/etc/apt",
            "/private/var/lib/apt/"
        };

        foreach (var path in jailbreakPaths)
        {
            if (File.Exists(path))
                return true;
        }

        // Verificăm dacă putem scrie în afara sandbox-ului
        try
        {
            File.WriteAllText("/private/test_jb.txt", "test");
            File.Delete("/private/test_jb.txt");
            return true; // Dacă am reușit, dispozitivul este jailbroken
        }
        catch
        {
            return false; // Comportament normal - sandbox activ
        }
    }
}
#endif

Windows: Configurări Specifice

// Configurări de securitate specifice Windows
#if WINDOWS
public static class WindowsSecurityConfig
{
    /// <summary>
    /// Configurează protecția datelor folosind DPAPI pe Windows
    /// </summary>
    public static byte[] ProtectData(byte[] data)
    {
        // DPAPI criptează datele legându-le de contul utilizatorului
        return System.Security.Cryptography.ProtectedData.Protect(
            data,
            optionalEntropy: System.Text.Encoding.UTF8
                .GetBytes("MauiApp_Salt_2024"),
            scope: System.Security.Cryptography.DataProtectionScope
                .CurrentUser);
    }

    /// <summary>
    /// Decriptează datele protejate cu DPAPI
    /// </summary>
    public static byte[] UnprotectData(byte[] encryptedData)
    {
        return System.Security.Cryptography.ProtectedData.Unprotect(
            encryptedData,
            optionalEntropy: System.Text.Encoding.UTF8
                .GetBytes("MauiApp_Salt_2024"),
            scope: System.Security.Cryptography.DataProtectionScope
                .CurrentUser);
    }
}
#endif

Checklist de Securitate pentru Lansare

Înainte de a publica aplicația în magazinele de aplicații, parcurgeți această listă de verificări. Serios, printați-o și bifați fiecare element. Am văzut aplicații care au trecut prin review fără probleme, dar aveau vulnerabilități flagrante la nivel de securitate.

Stocarea Datelor

  1. Toate datele sensibile (token-uri, credențiale, chei API) sunt stocate folosind SecureStorage
  2. Nu există date sensibile hardcodate în codul sursă
  3. Bazele de date locale SQLite sunt criptate sau protejate la nivel de fișier
  4. Datele temporare sunt șterse corespunzător la deconectare
  5. Fișierele de log nu conțin informații sensibile
  6. Backup-ul Android este dezactivat (android:allowBackup="false") sau criptat

Autentificare și Autorizare

  1. Autentificarea biometrică este implementată ca factor suplimentar
  2. Token-urile de acces au o durată de viață scurtă (15-30 minute)
  3. Mecanismul de refresh token funcționează corect
  4. Sesiunile expirate sunt gestionate cu redirecționare la login
  5. Fluxul OAuth 2.0 folosește PKCE (obligatoriu pentru aplicații mobile)
  6. Nu se stochează parole în plaintext, nici măcar local

Comunicare în Rețea

  1. Toate comunicațiile folosesc exclusiv HTTPS/TLS 1.2+
  2. Certificate Pinning este implementat pentru domeniul API principal
  3. Sunt incluse cel puțin două hash-uri de pin (principal + rezervă)
  4. Configurația network_security_config.xml interzice traficul în clar pe Android
  5. ATS (App Transport Security) este activ pe iOS, fără excepții inutile
  6. Timeout-urile de rețea sunt configurate rezonabil

Protecția Codului

  1. Obfuscarea codului este activată pentru build-urile de Release
  2. Simbolurile de depanare sunt eliminate din build-urile de producție
  3. Compilarea AOT (Ahead of Time) este activată
  4. R8/ProGuard este configurat corect pe Android
  5. Trimming-ul este activat pentru eliminarea codului nefolosit
  6. Detectarea root/jailbreak este implementată pentru funcționalitățile critice

Validarea Input-urilor

  1. Toate input-urile utilizatorului sunt validate atât pe client, cât și pe server
  2. Interogările SQLite folosesc parametri, nu concatenare de string-uri
  3. Conținutul HTML afișat în WebView este sanitizat
  4. Lungimile maxime sunt impuse pentru toate câmpurile de text
  5. Validarea expresiilor regulate este aplicată pentru email, telefon și alte formate specifice

Dependențe și Lanțul de Aprovizionare

  1. Toate pachetele NuGet sunt actualizate la ultimele versiuni stabile
  2. Nu există dependențe cu vulnerabilități cunoscute (verificați cu dotnet list package --vulnerable)
  3. Sursele NuGet sunt restricționate la repository-uri de încredere
  4. Hash-urile pachetelor sunt verificate (NuGet package signing)

Confidențialitate și Conformitate

  1. Permisiunile solicitate sunt minime și justificate
  2. Politica de confidențialitate este accesibilă din aplicație
  3. Datele personale sunt criptate în repaus și în tranzit
  4. Mecanismul de ștergere a contului și datelor este funcțional
  5. Colectarea datelor de telemetrie respectă consimțământul utilizatorului

Concluzie

Securitatea aplicațiilor mobile .NET MAUI nu e ceva ce faci o dată și treci mai departe. E un proces continuu, care trebuie integrat în fiecare etapă a ciclului de dezvoltare. De la stocarea securizată a datelor cu SecureStorage și autentificarea biometrică, până la Certificate Pinning, protecția împotriva ingineriei inverse și validarea riguroasă a input-urilor — fiecare strat contează.

Pe măsură ce .NET MAUI 10 aduce îmbunătățiri de calitate, diagnosticare avansată și suport edge-to-edge pe Android, oportunitățile de a construi aplicații mai sigure cresc. Dar responsabilitatea implementării corecte rămâne a noastră.

Iată ce ar trebui să rețineți din acest ghid:

  • Stocarea securizată — folosiți întotdeauna SecureStorage pentru date sensibile, nu Preferences sau fișiere text
  • Autentificarea multi-factor — combinați biometria cu OAuth 2.0/PKCE pentru o securitate robustă
  • Comunicarea criptată — HTTPS obligatoriu, Certificate Pinning implementat, trafic în clar interzis
  • Protecția codului — obfuscare, AOT, eliminarea simbolurilor de debug în producție
  • Validarea constantă — validați fiecare input, parametrizați fiecare interogare, sanitizați fiecare output
  • Securitate specifică platformei — configurați corect ATS pe iOS, network_security_config pe Android și DPAPI pe Windows

Recomandarea mea finală? Adoptați o mentalitate de "security by design" — gândiți securitatea de la prima linie de cod, nu ca o ajustare de ultim moment. Revizuiți periodic dependențele, efectuați audituri de securitate regulate și rămâneți la curent cu actualizările OWASP Mobile Top 10.

Securitatea perfectă nu există, dar cu instrumentele și practicile din acest ghid, aplicațiile voastre .NET MAUI vor fi semnificativ mai rezistente la amenințările din peisajul securității mobile. Și sincer, asta e tot ce putem face — să ridicăm ștacheta suficient de sus încât atacatorii să treacă la ținte mai ușoare.

Despre Autor Editorial Team

Our team of expert writers and editors.