Безопасность приложений .NET MAUI: от SecureStorage до Certificate Pinning

Руководство по безопасности .NET MAUI: SecureStorage, certificate pinning, обфускация кода через Dotfuscator и NativeAOT, детекция root/jailbreak, шифрование SQLite и управление секретами. С примерами на C# под .NET 10.

Введение: почему безопасность мобильных приложений — это не опция

Если вы пишете мобильное приложение на .NET MAUI, то наверняка уже ловили себя на мысли: «А правильно ли я храню токены? Можно ли вообще доверять SecureStorage? Нужен ли мне certificate pinning или это паранойя?» Честно говоря, когда я впервые задумался над этим всерьёз, масштаб проблемы меня впечатлил.

Дело вот в чём. Мобильное приложение — это не серверный код за файрволом. Ваш бинарник физически лежит на устройстве пользователя. Его можно декомпилировать, перехватить трафик, вытащить сохранённые данные. По данным OWASP Mobile Top 10 (который, кстати, впервые за 8 лет обновили в 2024 году), некорректное обращение с учётными данными — это уязвимость номер один в мобильных приложениях.

В этом руководстве разберём все ключевые аспекты безопасности .NET MAUI-приложений — от SecureStorage и сетевых коммуникаций до обфускации кода и обнаружения рутированных устройств. Всё с рабочим кодом под .NET 10.

1. OWASP Mobile Top 10 2024 через призму .NET MAUI

Итак, в 2024 году OWASP наконец обновил свой список Mobile Top 10. Для нас, .NET MAUI-разработчиков, самые критичные риски выглядят так:

  • M1 — Некорректное обращение с учётными данными — захардкоженные API-ключи, токены в открытом виде, отсутствие ротации
  • M2 — Недостаточная безопасность цепочки поставок — уязвимые NuGet-пакеты, непроверенные сторонние библиотеки
  • M5 — Незащищённые коммуникации — HTTP вместо HTTPS, отсутствие certificate pinning, игнорирование ошибок TLS
  • M7 — Недостаточная защита бинарных файлов — нет обфускации, лёгкая декомпиляция через ILSpy
  • M9 — Небезопасное хранение данных — чувствительные данные в SharedPreferences, открытые SQLite-базы

Каждый из этих рисков мы закроем конкретными решениями дальше по тексту.

2. SecureStorage: безопасное хранение чувствительных данных

2.1 Как работает SecureStorage под капотом

.NET MAUI предоставляет интерфейс ISecureStorage для безопасного хранения пар ключ-значение. Но прежде чем использовать его, стоит разобраться, что именно происходит на каждой платформе — иначе можно попасть в неприятные ловушки:

  • Android — под капотом используется EncryptedSharedPreferences из Android Security Library. Ключи шифруются детерминировано, значения — с помощью AES-256 GCM. Ключи шифрования живут в Android KeyStore
  • iOS — данные уходят в Keychain, аппаратно защищённое хранилище Apple. Service-идентификатор записи: [Bundle-ID].microsoft.maui.essentials.preferences
  • Windows — применяется DataProtectionProvider, данные сохраняются в ApplicationDataContainer

2.2 Базовое использование SecureStorage

Сам по себе API предельно простой:

// Сохранение токена
await SecureStorage.Default.SetAsync("auth_token", accessToken);

// Чтение токена
string token = await SecureStorage.Default.GetAsync("auth_token");

// Удаление конкретного значения
SecureStorage.Default.Remove("auth_token");

// Очистка всего хранилища (например, при logout)
SecureStorage.Default.RemoveAll();

2.3 Правильная архитектура: обёртка над SecureStorage

В реальных проектах я настоятельно рекомендую не дёргать SecureStorage.Default напрямую. Лучше сделать сервис-обёртку — это даст вам тестируемость и единую точку обработки ошибок (а ошибки с SecureStorage на Android бывают, поверьте):

public interface ISecureStorageService
{
    Task<string?> GetTokenAsync();
    Task SaveTokenAsync(string token);
    Task<bool> HasValidTokenAsync();
    Task ClearAllAsync();
}

public class SecureStorageService : ISecureStorageService
{
    private const string AuthTokenKey = "auth_token";
    private const string TokenExpiryKey = "token_expiry";
    private readonly ISecureStorage _secureStorage;

    public SecureStorageService(ISecureStorage secureStorage)
    {
        _secureStorage = secureStorage;
    }

    public async Task<string?> GetTokenAsync()
    {
        try
        {
            return await _secureStorage.GetAsync(AuthTokenKey);
        }
        catch (Exception ex)
        {
            // На Android при сбросе KeyStore данные
            // могут стать нечитаемыми
            System.Diagnostics.Debug.WriteLine(
                $"SecureStorage read failed: {ex.Message}");
            _secureStorage.Remove(AuthTokenKey);
            return null;
        }
    }

    public async Task SaveTokenAsync(string token)
    {
        await _secureStorage.SetAsync(AuthTokenKey, token);
        await _secureStorage.SetAsync(TokenExpiryKey,
            DateTime.UtcNow.AddHours(1).ToString("O"));
    }

    public async Task<bool> HasValidTokenAsync()
    {
        var token = await GetTokenAsync();
        if (string.IsNullOrEmpty(token)) return false;

        var expiryStr = await _secureStorage.GetAsync(TokenExpiryKey);
        if (DateTime.TryParse(expiryStr, out var expiry))
            return DateTime.UtcNow < expiry;

        return false;
    }

    public Task ClearAllAsync()
    {
        _secureStorage.RemoveAll();
        return Task.CompletedTask;
    }
}

Регистрация в DI-контейнере — стандартная:

// MauiProgram.cs
builder.Services.AddSingleton<ISecureStorage>(SecureStorage.Default);
builder.Services.AddSingleton<ISecureStorageService, SecureStorageService>();

2.4 Ловушка Android Auto Backup

Вот эта штука реально может подпортить жизнь. На Android 6.0+ по умолчанию включён Auto Backup, который копирует данные приложения, включая EncryptedSharedPreferences. Проблема? При восстановлении на новом устройстве ключи шифрования из KeyStore не переносятся — и все данные в SecureStorage превращаются в мусор.

.NET MAUI автоматически обрабатывает этот случай, удаляя повреждённые записи. Но лучше перестраховаться и явно исключить файлы SecureStorage из бэкапа:

<!-- Platforms/Android/Resources/xml/backup_rules.xml -->
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
  <cloud-backup>
    <exclude domain="sharedpref"
      path="__androidx_security_crypto_encrypted_prefs__" />
  </cloud-backup>
</data-extraction-rules>
<!-- AndroidManifest.xml -->
<application
    android:dataExtractionRules="@xml/backup_rules"
    ...>

3. Безопасные сетевые коммуникации

3.1 Принудительное использование HTTPS

Все сетевые запросы должны идти по HTTPS — тут без вариантов. На Android начиная с API 28 открытый HTTP-трафик заблокирован по умолчанию, а вот на iOS нужно отдельно настроить App Transport Security (ATS):

<!-- Platforms/iOS/Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
    <!-- НЕ добавляйте NSAllowsArbitraryLoads = true в продакшене! -->
    <key>NSExceptionDomains</key>
    <dict>
        <key>api.yourserver.com</key>
        <dict>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.3</string>
            <key>NSExceptionRequiresForwardSecrecy</key>
            <true/>
        </dict>
    </dict>
</dict>

3.2 Certificate Pinning: защита от MITM-атак

HTTPS защитит от пассивного перехвата, но не от man-in-the-middle с подменой сертификата. Тут на помощь приходит certificate pinning — вы привязываете приложение к конкретному серверному сертификату (или его публичному ключу), отсекая все остальные. Даже те, что выданы вполне легитимными центрами сертификации.

Вариант 1: Android Network Security Config

На Android проще всего использовать встроенный механизм:

<!-- Platforms/Android/Resources/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.yourserver.com</domain>
        <pin-set expiration="2027-01-01">
            <!-- SHA-256 хеш публичного ключа сертификата -->
            <pin digest="SHA-256">
                AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEE=
            </pin>
            <!-- Резервный пин (обязательно!) -->
            <pin digest="SHA-256">
                FFFFFFFFGGGGGGGGHHHHHHHHIIIIIIIIJJJJJJJJ=
            </pin>
        </pin-set>
    </domain-config>
</network-security-config>
<!-- AndroidManifest.xml -->
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

Вариант 2: Программная реализация через HttpClientHandler

Если нужен кросс-платформенный вариант с более гибким контролем, можно реализовать pinning программно:

public class CertificatePinningHandler : HttpClientHandler
{
    private readonly HashSet<string> _validPins;

    public CertificatePinningHandler(IEnumerable<string> pins)
    {
        _validPins = new HashSet<string>(pins);
        ServerCertificateCustomValidationCallback = ValidateCertificate;
    }

    private bool ValidateCertificate(
        HttpRequestMessage request,
        X509Certificate2? certificate,
        X509Chain? chain,
        SslPolicyErrors sslErrors)
    {
        if (certificate is null) return false;

        // Проверяем стандартные ошибки SSL
        if (sslErrors != SslPolicyErrors.None) return false;

        // Вычисляем SHA-256 хеш публичного ключа
        using var sha256 = SHA256.Create();
        var publicKeyBytes = certificate.GetPublicKey();
        var hash = sha256.ComputeHash(publicKeyBytes);
        var pin = Convert.ToBase64String(hash);

        return _validPins.Contains(pin);
    }
}

// Регистрация в DI
builder.Services.AddTransient<HttpClient>(sp =>
{
    var handler = new CertificatePinningHandler(new[]
    {
        "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEE=",
        "FFFFFFFFGGGGGGGGHHHHHHHHIIIIIIIIJJJJJJJJ="
    });
    return new HttpClient(handler)
    {
        BaseAddress = new Uri("https://api.yourserver.com")
    };
});

Важно: всегда указывайте минимум два пина — основной и резервный. Если основной сертификат отзовут или заменят, а у вас один-единственный пин, пользователи просто потеряют доступ к серверу. Я видел такие случаи — ничего весёлого.

4. Аутентификация и управление сессиями

4.1 OAuth 2.0 и PKCE в .NET MAUI

Для аутентификации в мобильных приложениях используйте OAuth 2.0 с расширением PKCE (Proof Key for Code Exchange). .NET MAUI предоставляет встроенный WebAuthenticator, который берёт на себя всю работу с OAuth-потоками:

public class AuthService
{
    private readonly ISecureStorageService _secureStorage;

    public AuthService(ISecureStorageService secureStorage)
    {
        _secureStorage = secureStorage;
    }

    public async Task<bool> LoginAsync()
    {
        try
        {
            var authResult = await WebAuthenticator.Default
                .AuthenticateAsync(
                    new Uri("https://auth.yourserver.com/authorize" +
                        "?response_type=code" +
                        "&client_id=your_client_id" +
                        "&redirect_uri=myapp://callback" +
                        "&code_challenge=YOUR_CHALLENGE" +
                        "&code_challenge_method=S256" +
                        "&scope=openid+profile"),
                    new Uri("myapp://callback"));

            var authCode = authResult.Properties["code"];

            // Обмениваем код на токен на сервере
            var token = await ExchangeCodeForTokenAsync(authCode);
            await _secureStorage.SaveTokenAsync(token);

            return true;
        }
        catch (TaskCanceledException)
        {
            // Пользователь отменил аутентификацию
            return false;
        }
    }

    private async Task<string> ExchangeCodeForTokenAsync(
        string authCode)
    {
        // Обмен кода на токен должен происходить
        // через ваш backend-сервер, а НЕ напрямую
        // с мобильного клиента
        using var client = new HttpClient();
        var response = await client.PostAsync(
            "https://api.yourserver.com/auth/token",
            new FormUrlEncodedContent(new Dictionary<string, string>
            {
                ["code"] = authCode,
                ["grant_type"] = "authorization_code",
                ["redirect_uri"] = "myapp://callback"
            }));

        var json = await response.Content.ReadAsStringAsync();
        var result = JsonSerializer.Deserialize<TokenResponse>(json);
        return result!.AccessToken;
    }
}

4.2 Биометрическая аутентификация

Для дополнительного уровня защиты стоит задействовать биометрию. На Android это BiometricPrompt, на iOS — LocalAuthentication. Вот пример для Android:

#if ANDROID
public async Task<bool> AuthenticateWithBiometricsAsync()
{
    var context = Platform.CurrentActivity
        ?? throw new InvalidOperationException(
            "No current activity");

    var executor = ContextCompat.GetMainExecutor(context);
    var callback = new BiometricCallback();

    var promptInfo = new BiometricPrompt.PromptInfo.Builder()
        .SetTitle("Подтвердите личность")
        .SetSubtitle("Используйте отпечаток пальца или Face ID")
        .SetNegativeButtonText("Отмена")
        .SetAllowedAuthenticators(
            BiometricManager.Authenticators.BiometricStrong)
        .Build();

    var biometricPrompt = new BiometricPrompt(
        (FragmentActivity)context, executor, callback);

    biometricPrompt.Authenticate(promptInfo);
    return await callback.TaskCompletionSource.Task;
}
#endif

5. Защита кода от реверс-инжиниринга

5.1 Почему .NET MAUI-приложения уязвимы

.NET-приложения компилируются в промежуточный язык MSIL (CIL), который декомпилируется обратно в читаемый C# буквально в пару кликов — достаточно открыть ILSpy или dnSpy. Для .NET MAUI это особенно неприятно: приложение ставится на устройство вместе со всеми DLL-файлами.

Злоумышленник может извлечь APK или IPA, распаковать его и получить доступ к вашей бизнес-логике, алгоритмам лицензирования и захардкоженным секретам. Буквально за минуты.

5.2 NativeAOT как первая линия защиты

Если вы уже используете NativeAOT (а мы рассказывали о нём в предыдущей статье), то получаете приятный бонус к безопасности. Приложение компилируется в нативный машинный код, а не в MSIL — и это серьёзно усложняет декомпиляцию:

<!-- В .csproj для включения NativeAOT -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <PublishAot>true</PublishAot>
    <StripSymbols>true</StripSymbols>
</PropertyGroup>

Правда, есть нюанс: NativeAOT не поддерживается для iOS (Apple использует собственный AOT) и требует тщательного тестирования на совместимость с Reflection-зависимыми библиотеками. Так что перед переходом — протестируйте как следует.

5.3 Обфускация с помощью Dotfuscator

Dotfuscator Community Edition идёт прямо в комплекте с Visual Studio и даёт многоуровневую защиту:

  • Переименование — заменяет названия классов, методов и полей на нечитаемые символы
  • Control Flow Obfuscation — запутывает логику выполнения, затрудняя анализ
  • String Encryption — шифрует строковые литералы в коде
  • Anti-Tamper — обнаруживает модификацию бинарников и завершает приложение
  • Anti-Debug — обнаруживает подключение отладчика

Удобство .NET MAUI в том, что проект единый для всех платформ. Интегрируете Dotfuscator один раз — и защита работает везде.

5.4 R8/ProGuard для Android

Для Android-сборок дополнительно стоит включить R8 — минификатор и обфускатор от Google, встроенный в Android build toolchain:

<!-- В .csproj -->
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
    <AndroidLinkMode>Full</AndroidLinkMode>
    <EnableProguard>true</EnableProguard>
    <AndroidR8IgnoreWarnings>true</AndroidR8IgnoreWarnings>
</PropertyGroup>

6. Обнаружение скомпрометированного окружения

6.1 Детекция Root/Jailbreak

Приложения на рутированных (Android) или взломанных (iOS) устройствах подвержены дополнительным рискам: перехват системных вызовов, подмена ответов API, извлечение данных из Keychain/KeyStore. Для банковских и медицинских приложений проверка окружения — по сути, обязательна:

public static class DeviceSecurityChecker
{
#if ANDROID
    public static bool IsDeviceRooted()
    {
        // Проверка наличия su-бинарника
        string[] rootPaths = {
            "/system/bin/su",
            "/system/xbin/su",
            "/sbin/su",
            "/data/local/xbin/su",
            "/data/local/bin/su",
            "/system/sd/xbin/su",
            "/system/bin/failsafe/su",
            "/data/local/su"
        };

        if (rootPaths.Any(File.Exists))
            return true;

        // Проверка свойств системы
        try
        {
            using var process = Java.Lang.Runtime.GetRuntime()
                ?.Exec(new[] { "which", "su" });
            if (process?.InputStream?.Read() != -1)
                return true;
        }
        catch { /* su не найден */ }

        // Проверка на наличие Magisk
        try
        {
            var pm = Android.App.Application.Context.PackageManager;
            pm?.GetPackageInfo("com.topjohnwu.magisk", 0);
            return true;
        }
        catch { /* Magisk не установлен */ }

        return false;
    }
#elif IOS
    public static bool IsDeviceRooted()
    {
        // Проверка на наличие Cydia и других маркеров
        string[] jailbreakPaths = {
            "/Applications/Cydia.app",
            "/Library/MobileSubstrate/MobileSubstrate.dylib",
            "/bin/bash",
            "/usr/sbin/sshd",
            "/etc/apt",
            "/usr/bin/ssh",
            "/private/var/lib/apt/"
        };

        if (jailbreakPaths.Any(File.Exists))
            return true;

        // Проверка записи за пределами песочницы
        try
        {
            File.WriteAllText(
                "/private/jailbreak_test.txt", "test");
            File.Delete("/private/jailbreak_test.txt");
            return true; // Если запись удалась — устройство взломано
        }
        catch { /* Нормальное поведение */ }

        return false;
    }
#endif
}

6.2 Детекция эмулятора и отладчика

Злоумышленники часто используют эмуляторы и отладчики для анализа приложений. Вот базовая детекция:

public static class EnvironmentChecker
{
#if ANDROID
    public static bool IsRunningOnEmulator()
    {
        return Android.OS.Build.Fingerprint?.StartsWith("generic") == true
            || Android.OS.Build.Fingerprint?.StartsWith("unknown") == true
            || Android.OS.Build.Model?.Contains("google_sdk") == true
            || Android.OS.Build.Model?.Contains("Emulator") == true
            || Android.OS.Build.Model?.Contains("Android SDK") == true
            || Android.OS.Build.Brand?.StartsWith("generic") == true
            || Android.OS.Build.Device?.StartsWith("generic") == true
            || Android.OS.Build.Product == "sdk"
            || Android.OS.Build.Product == "sdk_gphone64_arm64"
            || Android.OS.Build.Hardware?.Contains("goldfish") == true
            || Android.OS.Build.Hardware?.Contains("ranchu") == true;
    }
#endif

    public static bool IsDebuggerAttached()
    {
        return System.Diagnostics.Debugger.IsAttached;
    }
}

Важный момент: ни одна из этих проверок не даёт стопроцентной гарантии. Опытный атакующий с Frida может перехватить и подменить результат любой проверки. Суть этих мер — поднять планку атаки, а не сделать её невозможной. Для действительно критичных приложений стоит смотреть в сторону коммерческих RASP-решений (Runtime Application Self-Protection).

7. Управление секретами и конфигурацией

7.1 Никогда не храните секреты в коде

Правило номер один. И оно же — самое часто нарушаемое. API-ключи, connection strings, секреты OAuth — ничего из этого не должно быть в исходном коде. Казалось бы, очевидно, но на GitHub регулярно находят захардкоженные ключи даже в крупных проектах:

// ❌ НИКОГДА ТАК НЕ ДЕЛАЙТЕ
public static class ApiConfig
{
    public const string ApiKey = "sk-12345abcdef";
    public const string DbConnectionString =
        "Server=prod.db.com;Password=hunter2";
}

// ✅ Правильный подход — получение с сервера
public class ConfigService
{
    private readonly HttpClient _httpClient;
    private readonly ISecureStorageService _storage;

    public ConfigService(
        HttpClient httpClient,
        ISecureStorageService storage)
    {
        _httpClient = httpClient;
        _storage = storage;
    }

    public async Task<string?> GetApiKeyAsync()
    {
        // Сначала проверяем кэш
        var cached = await _storage.GetTokenAsync();
        if (!string.IsNullOrEmpty(cached))
            return cached;

        // Получаем с сервера (после аутентификации)
        var response = await _httpClient.GetAsync("/config/api-key");
        if (response.IsSuccessStatusCode)
        {
            var key = await response.Content.ReadAsStringAsync();
            await _storage.SaveTokenAsync(key);
            return key;
        }

        return null;
    }
}

7.2 Конфигурация через appsettings.json для не-секретных данных

Для настроек без секретов (URL-адреса API, feature flags, таймауты) подойдёт стандартный механизм конфигурации .NET:

// MauiProgram.cs
using var stream = await FileSystem
    .OpenAppPackageFileAsync("appsettings.json");
var config = new ConfigurationBuilder()
    .AddJsonStream(stream)
    .Build();

builder.Configuration.AddConfiguration(config);

// Использование
builder.Services.Configure<ApiSettings>(
    config.GetSection("Api"));

// В ViewModel или сервисе
public class MyService
{
    private readonly ApiSettings _settings;

    public MyService(IOptions<ApiSettings> settings)
    {
        _settings = settings.Value;
    }
}

8. Защита данных в состоянии покоя

8.1 Шифрование SQLite-базы данных

Если приложение использует локальный SQLite для хранения данных, то по умолчанию база лежит в открытом виде — любой может её прочитать, имея доступ к файловой системе. Для шифрования берём Microsoft.Data.Sqlite с поддержкой SQLCipher:

// Установите NuGet-пакет: SQLitePCLRaw.bundle_e_sqlcipher

public class EncryptedDatabaseService
{
    private readonly string _dbPath;
    private readonly ISecureStorageService _secureStorage;

    public EncryptedDatabaseService(
        ISecureStorageService secureStorage)
    {
        _secureStorage = secureStorage;
        _dbPath = Path.Combine(
            FileSystem.AppDataDirectory,
            "app_data.db");
    }

    public async Task<SqliteConnection> GetConnectionAsync()
    {
        var passphrase = await GetOrCreatePassphraseAsync();

        var connectionString = new SqliteConnectionStringBuilder
        {
            DataSource = _dbPath,
            Password = passphrase,
            Mode = SqliteOpenMode.ReadWriteCreate
        }.ToString();

        var connection = new SqliteConnection(connectionString);
        await connection.OpenAsync();
        return connection;
    }

    private async Task<string> GetOrCreatePassphraseAsync()
    {
        const string key = "db_passphrase";
        var passphrase = await SecureStorage.Default
            .GetAsync(key);

        if (string.IsNullOrEmpty(passphrase))
        {
            // Генерируем криптографически стойкий пароль
            var bytes = new byte[32];
            using var rng = RandomNumberGenerator
                .Create();
            rng.GetBytes(bytes);
            passphrase = Convert.ToBase64String(bytes);
            await SecureStorage.Default
                .SetAsync(key, passphrase);
        }

        return passphrase;
    }
}

8.2 Очистка данных при выходе

Для приложений с повышенными требованиями безопасности не забудьте про очистку данных при logout:

public async Task SecureLogoutAsync()
{
    // Очистка всех токенов
    await _secureStorage.ClearAllAsync();

    // Очистка кэша HTTP
    _httpClient.DefaultRequestHeaders.Authorization = null;

    // Очистка навигационного стека
    // (предотвращает возврат на защищённые страницы)
    await Shell.Current.GoToAsync("//login");
}

9. Чеклист безопасности .NET MAUI-приложения

Этот список стоит прогонять перед каждым релизом. Серьёзно, распечатайте и повесьте на стену:

  1. Хранение данных — токены и пароли лежат только в SecureStorage, а не в Preferences или файлах
  2. Сетевые коммуникации — все запросы идут по HTTPS, certificate pinning настроен для критичных эндпоинтов
  3. Аутентификация — используется OAuth 2.0 + PKCE, токены имеют ограниченный срок жизни
  4. Код — включена обфускация (Dotfuscator или аналог), NativeAOT для Android
  5. Секреты — в коде нет захардкоженных API-ключей, паролей и connection strings
  6. Окружение — есть базовая детекция root/jailbreak для критичных сценариев
  7. Логирование — в Release-сборке отключён вывод чувствительных данных в логи
  8. Бэкап — настроены правила исключения SecureStorage из Android Auto Backup
  9. Зависимости — все NuGet-пакеты обновлены, выполнен dotnet list package --vulnerable
  10. Скриншоты — на экранах с чувствительными данными включена защита от скриншотов

FAQ: часто задаваемые вопросы

Безопасен ли SecureStorage в .NET MAUI для хранения паролей?

SecureStorage использует платформенные механизмы шифрования: Android KeyStore с AES-256 GCM, iOS Keychain и Windows DataProtectionProvider. Для хранения токенов и коротких секретов — вполне надёжное решение. Но для паролей пользователей лучше использовать серверную аутентификацию (OAuth 2.0) и вообще не хранить пароли на устройстве. Храните refresh-токены с ограниченным сроком жизни — так безопаснее.

Как реализовать certificate pinning в .NET MAUI для Android и iOS?

На Android используйте network_security_config.xml с SHA-256 хешами публичных ключей ваших сертификатов. На iOS — библиотеку TrustKit через .NET MAUI Bindings или программную проверку через ServerCertificateCustomValidationCallback в HttpClientHandler. И обязательно — минимум два пина (основной и резервный) плюс дата истечения.

Нужна ли обфускация кода при использовании NativeAOT?

NativeAOT компилирует код в нативные инструкции, что сильно усложняет декомпиляцию по сравнению с MSIL. Но NativeAOT не работает на iOS (там Apple со своим AOT-компилятором) и не защищает строковые литералы. Поэтому для максимальной защиты имеет смысл комбинировать NativeAOT (для Android/Windows) с шифрованием строк и anti-tamper проверками.

Как защитить .NET MAUI-приложение от работы на рутированном устройстве?

Реализуйте проверки наличия su-бинарников, Magisk, Cydia (на iOS) и других маркеров при запуске. Но имейте в виду: все клиентские проверки можно обойти через Frida или аналоги. Для финансовых и медицинских приложений берите коммерческие RASP-решения (Dotfuscator, Appdome, Guardsquare) или серверную верификацию целостности (Google Play Integrity API, Apple DeviceCheck).

Какие NuGet-пакеты помогают с безопасностью .NET MAUI-приложений?

Основные пакеты: Microsoft.Data.Sqlite с SQLitePCLRaw.bundle_e_sqlcipher для шифрования баз данных, System.Security.Cryptography для криптографических операций, TrustKit .NET Bindings для certificate pinning, плюс Dotfuscator Community Edition (идёт в Visual Studio) для обфускации. И не забывайте регулярно проверять уязвимости: dotnet list package --vulnerable.

Об авторе Editorial Team

Our team of expert writers and editors.