Freigeben über


Migrieren von sicherem Xamarin.Essentials-Speicher zu sicherem .NET MAUI-Speicher

Xamarin.Essentials und .NET Multi-Platform App UI (.NET MAUI) verfügen beide über eine SecureStorage-Klasse, mit der Sie einfache Schlüssel-Wert-Paare sicher speichern können. Es gibt jedoch Implementierungsunterschiede zwischen der SecureStorage-Klasse in Xamarin.Essentials und .NET MAUI:

Plattform Xamarin.Essentials .NET MAUI
Android Der Android-KeyStore wird verwendet, um den Chiffrierschlüssel zu speichern, mit dem der Wert verschlüsselt wurde, bevor er in einem freigegebenen Einstellungsobjekt mit einem Namen von {your-app-package-id}.xamarinessentials gespeichert wird. Die Daten werden mit der EncryptedSharedPreferences-Klasse verschlüsselt, die die SharedPreferences-Klasse umschließt, und verschlüsselt automatisch Schlüssel und Werte. Der verwendete Name lautet {your-app-package-id}.microsoft.maui.essentials.preferences.
iOS KeyChain wird verwendet, um Werte sicher zu speichern. Das zum Speichern von Werten verwendete SecRecord-Element hat einen Service-Wert, der auf {your-app-package-id}.xamarinessentials festgelegt ist. KeyChain wird verwendet, um Werte sicher zu speichern. Das zum Speichern von Werten verwendete SecRecord-Element hat einen Service-Wert, der auf {your-app-package-id}.microsoft.maui.essentials.preferences festgelegt ist.

Weitere Informationen zur SecureStorage-Klasse in Xamarin.Essentials finden Sie unter Xamarin.Essentials: Sichere Speicherung. Weitere Informationen zur SecureStorage-Klasse in .NET MAUI finden Sie unter Sichere Speicherung.

Bei der Migration einer Xamarin.Forms-App, die die SecureStorage-Klasse für .NET MAUI verwendet, müssen Sie sich mit diesen Implementierungsunterschieden befassen, um Benutzern eine reibungslose Upgradeerfahrung zu bieten. In diesem Artikel wird beschrieben, wie Sie die LegacySecureStorage-Klasse und entsprechenden Hilfsklassen verwenden können, um die Implementierungsunterschiede zu verwalten. Die LegacySecureStorage-Klasse ermöglicht Ihrer .NET MAUI-App unter Android und iOS das Lesen von sicheren Speicherdaten, die mit einer früheren Xamarin.Forms-Version Ihrer App erstellt wurden.

Zugreifen auf sichere Legacy-Speicherdaten

Der folgende Code zeigt die LegacySecureStorage-Klasse, die die sichere Speicherimplementierung von Xamarin.Essentials bereitstellt:

Hinweis

Um diesen Code zu verwenden, fügen Sie ihn einer Klasse hinzu, die in Ihrem .NET MAUI-App-Projekt LegacySecureStorage benannt ist.

#nullable enable
#if ANDROID || IOS

namespace MigrationHelpers;

public class LegacySecureStorage
{
    internal static readonly string Alias = $"{AppInfo.PackageName}.xamarinessentials";

    public static Task<string> GetAsync(string key)
    {
        if (string.IsNullOrWhiteSpace(key))
            throw new ArgumentNullException(nameof(key));

        string result = string.Empty;

#if ANDROID
        object locker = new object();
        string? encVal = Preferences.Get(key, null, Alias);

        if (!string.IsNullOrEmpty(encVal))
        {
            byte[] encData = Convert.FromBase64String(encVal);
            lock (locker)
            {
                AndroidKeyStore keyStore = new AndroidKeyStore(Platform.AppContext, Alias, false);
                result = keyStore.Decrypt(encData);
            }
        }
#elif IOS
        KeyChain keyChain = new KeyChain();
        result = keyChain.ValueForKey(key, Alias);
#endif
        return Task.FromResult(result);
    }

    public static bool Remove(string key)
    {
        bool result = false;

#if ANDROID
        Preferences.Remove(key, Alias);
        result = true;
#elif IOS
        KeyChain keyChain = new KeyChain();
        result = keyChain.Remove(key, Alias);
#endif
        return result;
    }

    public static void RemoveAll()
    {
#if ANDROID
        Preferences.Clear(Alias);
#elif IOS
        KeyChain keyChain = new KeyChain();
        keyChain.RemoveAll(Alias);
#endif
    }
}
#endif

Android

Unter Android nutzt die LegacySecureStorage-Klasse die AndroidKeyStore-Klasse, um den Chiffrierschlüssel zu speichern, mit dem der Wert verschlüsselt wurde, bevor er in einem freigegebenen Einstellungsobjekt mit einem Namen von {your-app-package-id}.xamarinessentials gespeichert wird. Der folgende Code zeigt die AndroidKeyStore-Klasse:

Hinweis

Um diesen Code zu verwenden, fügen Sie ihn zu einer Klasse hinzu, die AndroidKeyStore im Ordner Platforms\Android Ihres .NET MAUI-App-Projekts benannt ist.

using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Security;
using Android.Security.Keystore;
using Java.Security;
using Javax.Crypto;
using Javax.Crypto.Spec;
using System.Text;

namespace MigrationHelpers;

class AndroidKeyStore
{
    const string androidKeyStore = "AndroidKeyStore"; // this is an Android const value
    const string aesAlgorithm = "AES";
    const string cipherTransformationAsymmetric = "RSA/ECB/PKCS1Padding";
    const string cipherTransformationSymmetric = "AES/GCM/NoPadding";
    const string prefsMasterKey = "SecureStorageKey";
    const int initializationVectorLen = 12; // Android supports an IV of 12 for AES/GCM

    internal AndroidKeyStore(Context context, string keystoreAlias, bool alwaysUseAsymmetricKeyStorage)
    {
        alwaysUseAsymmetricKey = alwaysUseAsymmetricKeyStorage;
        appContext = context;
        alias = keystoreAlias;

        keyStore = KeyStore.GetInstance(androidKeyStore);
        keyStore.Load(null);
    }

    readonly Context appContext;
    readonly string alias;
    readonly bool alwaysUseAsymmetricKey;
    readonly string useSymmetricPreferenceKey = "essentials_use_symmetric";

    KeyStore keyStore;
    bool useSymmetric = false;

    ISecretKey GetKey()
    {
        // check to see if we need to get our key from past-versions or newer versions.
        // we want to use symmetric if we are >= 23 or we didn't set it previously.
        var hasApiLevel = Build.VERSION.SdkInt >= BuildVersionCodes.M;

        useSymmetric = Preferences.Get(useSymmetricPreferenceKey, hasApiLevel, alias);

        // If >= API 23 we can use the KeyStore's symmetric key
        if (useSymmetric && !alwaysUseAsymmetricKey)
            return GetSymmetricKey();

        // NOTE: KeyStore in < API 23 can only store asymmetric keys
        // specifically, only RSA/ECB/PKCS1Padding
        // So we will wrap our symmetric AES key we just generated
        // with this and save the encrypted/wrapped key out to
        // preferences for future use.
        // ECB should be fine in this case as the AES key should be
        // contained in one block.

        // Get the asymmetric key pair
        var keyPair = GetAsymmetricKeyPair();

        var existingKeyStr = Preferences.Get(prefsMasterKey, null, alias);

        if (!string.IsNullOrEmpty(existingKeyStr))
        {
            try
            {
                var wrappedKey = Convert.FromBase64String(existingKeyStr);

                var unwrappedKey = UnwrapKey(wrappedKey, keyPair.Private);
                var kp = unwrappedKey.JavaCast<ISecretKey>();

                return kp;
            }
            catch (InvalidKeyException ikEx)
            {
                System.Diagnostics.Debug.WriteLine($"Unable to unwrap key: Invalid Key. This may be caused by system backup or upgrades. All secure storage items will now be removed. {ikEx.Message}");
            }
            catch (IllegalBlockSizeException ibsEx)
            {
                System.Diagnostics.Debug.WriteLine($"Unable to unwrap key: Illegal Block Size. This may be caused by system backup or upgrades. All secure storage items will now be removed. {ibsEx.Message}");
            }
            catch (BadPaddingException paddingEx)
            {
                System.Diagnostics.Debug.WriteLine($"Unable to unwrap key: Bad Padding. This may be caused by system backup or upgrades. All secure storage items will now be removed. {paddingEx.Message}");
            }
            LegacySecureStorage.RemoveAll();
        }

        var keyGenerator = KeyGenerator.GetInstance(aesAlgorithm);
        var defSymmetricKey = keyGenerator.GenerateKey();

        var newWrappedKey = WrapKey(defSymmetricKey, keyPair.Public);

        Preferences.Set(prefsMasterKey, Convert.ToBase64String(newWrappedKey), alias);

        return defSymmetricKey;
    }

    // API 23+ Only
#pragma warning disable CA1416
    ISecretKey GetSymmetricKey()
    {
        Preferences.Set(useSymmetricPreferenceKey, true, alias);

        var existingKey = keyStore.GetKey(alias, null);

        if (existingKey != null)
        {
            var existingSecretKey = existingKey.JavaCast<ISecretKey>();
            return existingSecretKey;
        }

        var keyGenerator = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, androidKeyStore);
        var builder = new KeyGenParameterSpec.Builder(alias, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
            .SetBlockModes(KeyProperties.BlockModeGcm)
            .SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone)
            .SetRandomizedEncryptionRequired(false);

        keyGenerator.Init(builder.Build());

        return keyGenerator.GenerateKey();
    }
#pragma warning restore CA1416

    KeyPair GetAsymmetricKeyPair()
    {
        // set that we generated keys on pre-m device.
        Preferences.Set(useSymmetricPreferenceKey, false, alias);

        var asymmetricAlias = $"{alias}.asymmetric";

        var privateKey = keyStore.GetKey(asymmetricAlias, null)?.JavaCast<IPrivateKey>();
        var publicKey = keyStore.GetCertificate(asymmetricAlias)?.PublicKey;

        // Return the existing key if found
        if (privateKey != null && publicKey != null)
            return new KeyPair(publicKey, privateKey);

        var originalLocale = Java.Util.Locale.Default;
        try
        {
            // Force to english for known bug in date parsing:
            // https://issuetracker.google.com/issues/37095309
            SetLocale(Java.Util.Locale.English);

            // Otherwise we create a new key
#pragma warning disable CA1416
            var generator = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, androidKeyStore);
#pragma warning restore CA1416

            var end = DateTime.UtcNow.AddYears(20);
            var startDate = new Java.Util.Date();
#pragma warning disable CS0618 // Type or member is obsolete
            var endDate = new Java.Util.Date(end.Year, end.Month, end.Day);
#pragma warning restore CS0618 // Type or member is obsolete

#pragma warning disable CS0618
            var builder = new KeyPairGeneratorSpec.Builder(Platform.AppContext)
                .SetAlias(asymmetricAlias)
                .SetSerialNumber(Java.Math.BigInteger.One)
                .SetSubject(new Javax.Security.Auth.X500.X500Principal($"CN={asymmetricAlias} CA Certificate"))
                .SetStartDate(startDate)
                .SetEndDate(endDate);

            generator.Initialize(builder.Build());
#pragma warning restore CS0618

            return generator.GenerateKeyPair();
        }
        finally
        {
            SetLocale(originalLocale);
        }
    }

    byte[] WrapKey(IKey keyToWrap, IKey withKey)
    {
        var cipher = Cipher.GetInstance(cipherTransformationAsymmetric);
        cipher.Init(CipherMode.WrapMode, withKey);
        return cipher.Wrap(keyToWrap);
    }

#pragma warning disable CA1416
    IKey UnwrapKey(byte[] wrappedData, IKey withKey)
    {
        var cipher = Cipher.GetInstance(cipherTransformationAsymmetric);
        cipher.Init(CipherMode.UnwrapMode, withKey);
        var unwrapped = cipher.Unwrap(wrappedData, KeyProperties.KeyAlgorithmAes, KeyType.SecretKey);
        return unwrapped;
    }
#pragma warning restore CA1416

    internal string Decrypt(byte[] data)
    {
        if (data.Length < initializationVectorLen)
            return null;

        var key = GetKey();

        // IV will be the first 16 bytes of the encrypted data
        var iv = new byte[initializationVectorLen];
        Buffer.BlockCopy(data, 0, iv, 0, initializationVectorLen);

        Cipher cipher;

        // Attempt to use GCMParameterSpec by default
        try
        {
            cipher = Cipher.GetInstance(cipherTransformationSymmetric);
            cipher.Init(CipherMode.DecryptMode, key, new GCMParameterSpec(128, iv));
        }
        catch (InvalidAlgorithmParameterException)
        {
            // If we encounter this error, it's likely an old bouncycastle provider version
            // is being used which does not recognize GCMParameterSpec, but should work
            // with IvParameterSpec, however we only do this as a last effort since other
            // implementations will error if you use IvParameterSpec when GCMParameterSpec
            // is recognized and expected.
            cipher = Cipher.GetInstance(cipherTransformationSymmetric);
            cipher.Init(CipherMode.DecryptMode, key, new IvParameterSpec(iv));
        }

        // Decrypt starting after the first 16 bytes from the IV
        var decryptedData = cipher.DoFinal(data, initializationVectorLen, data.Length - initializationVectorLen);

        return Encoding.UTF8.GetString(decryptedData);
    }

    internal void SetLocale(Java.Util.Locale locale)
    {
        Java.Util.Locale.Default = locale;
        var resources = appContext.Resources;
        var config = resources.Configuration;

        if (Build.VERSION.SdkInt >= BuildVersionCodes.N)
            config.SetLocale(locale);
        else
#pragma warning disable CS0618 // Type or member is obsolete
            config.Locale = locale;
#pragma warning restore CS0618 // Type or member is obsolete

#pragma warning disable CS0618 // Type or member is obsolete
        resources.UpdateConfiguration(config, resources.DisplayMetrics);
#pragma warning restore CS0618 // Type or member is obsolete
    }
}

Der Android-KeyStore wird verwendet, um den Chiffrierschlüssel zu speichern, sodass der Wert verschlüsselt wird, bevor er in der Datei Freigegebene Einstellungen mit einem Namen von {your-app-package-id}.xamarinessentials gespeichert wird. Der in der freigegebenen Einstellungsdatei verwendete Schlüssel (kein kryptografischer Schlüssel, sondern der -Schlüssel für den -Wert) ist ein MD5-Hash des Schlüssels, der an die SecureStorage-APIs übergeben wird.

Unter API 23+ wird ein AES-Schlüssel im Android-KeyStore abgerufen und mit einer AES/GCM/NoPadding-Verschlüsselung verwendet, um den Wert zu verschlüsseln, bevor er in der freigegebenen Einstellungsdatei gespeichert wird. Unter API 22 und älteren Versionen unterstützt der Android-KeyStore nur das Speichern von RSA-Schlüsseln, die mit einer RSA/ECB/PKCS1Padding-Verschlüsselung verwendet werden, um einen AES-Schlüssel zu verschlüsseln (zufällig zur Laufzeit erzeugt), und in der freigegebenen Einstellungsdatei unter dem Schlüssel SecureStorageKey gespeichert werden, wenn noch keiner generiert wurde.

iOS

Unter iOS verwendet die LegacySecureStorage-Klasse die KeyChain-Klasse, um Werte sicher zu speichern. Das zum Speichern von Werten verwendete SecRecord-Element hat einen Service-Wert, der auf {your-app-package-id}.xamarinessentials festgelegt ist. Der folgende Code zeigt die KeyChain-Klasse:

Hinweis

Um diesen Code zu verwenden, fügen Sie ihn zu einer Klasse namens KeyChain im Ordner Platforms\iOS Ihres .NET MAUI-App-Projekts hinzu.

using Foundation;
using Security;

namespace MigrationHelpers;

class KeyChain
{
    SecRecord ExistingRecordForKey(string key, string service)
    {
        return new SecRecord(SecKind.GenericPassword)
        {
            Account = key,
            Service = service
        };
    }

    internal string ValueForKey(string key, string service)
    {
        using (var record = ExistingRecordForKey(key, service))
        using (var match = SecKeyChain.QueryAsRecord(record, out var resultCode))
        {
            if (resultCode == SecStatusCode.Success)
                return NSString.FromData(match.ValueData, NSStringEncoding.UTF8);
            else
                return null;
        }
    }

    internal bool Remove(string key, string service)
    {
        using (var record = ExistingRecordForKey(key, service))
        using (var match = SecKeyChain.QueryAsRecord(record, out var resultCode))
        {
            if (resultCode == SecStatusCode.Success)
            {
                RemoveRecord(record);
                return true;
            }
        }
        return false;
    }

    internal void RemoveAll(string service)
    {
        using (var query = new SecRecord(SecKind.GenericPassword) { Service = service })
        {
            SecKeyChain.Remove(query);
        }
    }

    bool RemoveRecord(SecRecord record)
    {
        var result = SecKeyChain.Remove(record);
        if (result != SecStatusCode.Success && result != SecStatusCode.ItemNotFound)
            throw new Exception($"Error removing record: {result}");

        return true;
    }
}

Um diesen Code zu verwenden, müssen Sie über die Datei Entitlements.plist für Ihre iOS-App mit dem Schlüsselbundberechtigungssatz verfügen:

<key>keychain-access-groups</key>
<array>
  <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</array>

Sie müssen auch sicherstellen, dass die Datei Entitlements.plist in den Bündelsignierungseinstellungen für Ihre App als Feld „Benutzerdefinierte Berechtigungen“ festgelegt ist. Weitere Informationen finden Sie in unter iOS-Berechtigungen.

Nutzen von sicheren Legacy-Speicherdaten

Die LegacySecureStorage-Klasse kann verwendet werden, um sichere Legacy-Speicherdaten unter Android und iOS zu nutzen, die mit einer früheren Xamarin.Forms-Version Ihrer App erstellt wurde:

#if ANDROID || IOS
using MigrationHelpers;
...

string username = await LegacySecureStorage.GetAsync("username");
bool result = LegacySecureStorage.Remove("username");
await SecureStorage.SetAsync("username", username);
#endif

Das Beispiel zeigt die Verwendung der LegacySecureStorage-Klasse zum Lesen und Entfernen eines Werts aus dem sicheren Legacy-Speicher und anschließendes Schreiben des Werts in den sicheren .NET MAUI-Speicher.