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.