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 Verschlüsselungsschlüssel zu speichern, der zum Verschlüsseln eines Werts verwendet wird, bevor er in einem freigegebenen Einstellungsobjekt mit dem Namen {your-app-package-id}.xamarinessentials gespeichert wird. 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 Der Schlüsselbund wird verwendet, um Werte sicher zu speichern. Die SecRecord Komponente, die zum Speichern von Werten verwendet wird, hat einen Service Wert, der auf {your-app-package-id}.xamarinessentials festgelegt ist. Der Schlüsselbund wird verwendet, um Werte sicher zu speichern. Der SecRecord, der zum Speichern von Werten verwendet wird, 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: Sicherer Speicher. Weitere Informationen zur SecureStorage Klasse in .NET MAUI finden Sie unter Secure Storage.

Beim Migrieren einer Xamarin.Forms-App, die die SecureStorage Klasse zu .NET MAUI verwendet, müssen Sie mit diesen Implementierungsunterschieden umgehen, um Benutzern eine reibungslose Upgradeerfahrung zu bieten. In diesem Artikel wird beschrieben, wie Sie die LegacySecureStorage Klassen und Hilfsklassen verwenden können, um die Implementierungsunterschiede zu behandeln. Die LegacySecureStorage Klasse ermöglicht Es Ihrer .NET MAUI-App unter Android und iOS, sichere Speicherdaten zu lesen, die mit einer früheren Xamarin.Forms-Version Ihrer App erstellt wurden.

Zugreifen auf ältere sichere 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 benannt ist LegacySecureStorage .

#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 verwendet die LegacySecureStorage Klasse die AndroidKeyStore Klasse, um den Verschlüsselungsschlüssel zu speichern, der zum Verschlüsseln eines Werts verwendet wird, bevor er in einem freigegebenen Einstellungsobjekt mit dem Namen {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 im Ordner AndroidKeyStore" 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 Verschlüsselungsschlüssel zu speichern, der zum Verschlüsseln des Werts verwendet wird, bevor er in einer Freigegebenen Einstellungsdateimit dem Namen {your-app-package-id}.xamarinessentials gespeichert wird. Der Schlüssel (nicht ein kryptografischer Schlüssel, der Schlüssel für den Wert), der in der freigegebenen Einstellungsdatei verwendet wird, ist ein MD5-Hash des Schlüssels, der an die SecureStorage APIs übergeben wird.

Bei API 23+ wird ein AES-Schlüssel aus dem 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. In API 22 und niedriger unterstützt der Android KeyStore nur das Speichern von RSA-Schlüsseln , die mit einer RSA/ECB/PKCS1Padding-Verschlüsselung verwendet wird, um einen AES-Schlüssel (zufällig generiert zur Laufzeit) zu verschlüsseln und in der freigegebenen Einstellungsdatei unter dem Schlüssel SecureStorageKey gespeichert zu werden, sofern noch kein Schlüssel generiert wurde.

Ios

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

Hinweis

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

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 eine 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ündelsignatureinstellungen für Ihre App als Feld "Benutzerdefinierte Berechtigungen" festgelegt ist. Weitere Informationen finden Sie unter iOS-Berechtigungen.

Nutzen von älteren sicheren Speicherdaten

Die LegacySecureStorage Klasse kann verwendet werden, um ältere sichere 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 älteren sicheren Speicher und anschließendes Schreiben des Werts in .NET MAUI secure storage.