Xamarin.Essentials güvenli depolama alanından .NET MAUI güvenli depolama alanına geçiş
Xamarin.Essentials ve .NET Çok Platformlu Uygulama Kullanıcı Arabirimi (.NET MAUI), basit anahtar/değer çiftlerini güvenli bir şekilde depolamanıza yardımcı olan bir SecureStorage
sınıfa sahiptir. Ancak, Xamarin.Essentials ve .NET MAUI sınıfı arasında SecureStorage
uygulama farklılıkları vardır:
Platform | Xamarin.Essentials | .NET MAUI |
---|---|---|
Android | Android KeyStore, {your-app-package-id}.xamarinessentials adlı paylaşılan tercihler nesnesine kaydedilmeden önce bir değeri şifrelemek için kullanılan şifreleme anahtarını depolamak için kullanılır. | Veriler sınıfını sarmalayan SharedPreferences ve anahtarları ve değerleri otomatik olarak şifreleyen sınıfıyla EncryptedSharedPreferences şifrelenir. Kullanılan ad {your-app-package-id}.microsoft.maui.essentials.preferences şeklindedir. |
iOS | KeyChain, değerleri güvenli bir şekilde depolamak için kullanılır. SecRecord Değerleri Service depolamak için kullanılan değeri {your-app-package-id}.xamarinessentials olarak ayarlanmıştır. |
KeyChain, değerleri güvenli bir şekilde depolamak için kullanılır. SecRecord Değerleri Service depolamak için kullanılan değeri {your-app-package-id}.microsoft.maui.essentials.preferences olarak ayarlanmıştır. |
Xamarin.Essentials'taki sınıfı hakkında SecureStorage
daha fazla bilgi için bkz . Xamarin.Essentials: Güvenli depolama. .NET MAUI sınıfı hakkında SecureStorage
daha fazla bilgi için bkz . Güvenli depolama.
sınıfını kullanan SecureStorage
bir Xamarin.Forms uygulamasını .NET MAUI'ye geçirirken, kullanıcılara sorunsuz bir yükseltme deneyimi sağlamak için bu uygulama farklılıklarıyla ilgilenmeniz gerekir. Bu makalede, uygulama farklılıklarıyla başa çıkmak için sınıfını ve yardımcı sınıflarını nasıl kullanabileceğiniz LegacySecureStorage
açıklanmaktadır. sınıfı, LegacySecureStorage
Android ve iOS'ta .NET MAUI uygulamanızın, uygulamanızın önceki bir Xamarin.Forms sürümüyle oluşturulan güvenli depolama verilerini okumasını sağlar.
Eski güvenli depolama verilerine erişme
Aşağıdaki kod, Xamarin.Essentials'tan güvenli depolama uygulaması sağlayan sınıfını gösterir LegacySecureStorage
:
Dekont
Bu kodu kullanmak için .NET MAUI uygulama projenizde adlı LegacySecureStorage
bir sınıfa ekleyin.
#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
Android'de sınıfı, LegacySecureStorage
{your-app-package-id}.xamarinessentials adlı paylaşılan tercihler nesnesine kaydedilmeden önce bir değeri şifrelemek için kullanılan şifreleme anahtarını depolamak için sınıfını kullanır AndroidKeyStore
. Aşağıdaki kod sınıfını AndroidKeyStore
gösterir:
Dekont
Bu kodu kullanmak için.NET MAUI uygulama projenizin Platforms\Android klasöründe adlı AndroidKeyStore
bir sınıfa ekleyin.
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
}
}
Android KeyStore, değeri {your-app-package-id}.xamarinessentials adlı bir Paylaşılan Tercihlerdosyasına kaydedilmeden önce şifrelemek için kullanılan şifreleme anahtarını depolamak için kullanılır. Paylaşılan tercihler dosyasında kullanılan anahtar (şifreleme anahtarı değil, değerin anahtarı), API'lere geçirilen SecureStorage
anahtarın MD5 Karmasıdır.
API 23+ üzerinde, Android KeyStore'dan bir AES anahtarı alınır ve değeri paylaşılan tercihler dosyasında depolanmadan önce şifrelemek için AES/GCM/NoPadding şifrelemesi ile birlikte kullanılır. API 22 ve daha düşük sürümlerde, Android KeyStore yalnızca bir AES anahtarını şifrelemek için RSA/ECB/PKCS1Padding şifrelemesi ile kullanılan ve henüz oluşturulmamışsa Secure Depolama Key anahtarı altındaki paylaşılan tercihler dosyasında depolanan RSA anahtarlarının depolanmasını destekler.
iOS
iOS'ta LegacySecureStorage
sınıfı, değerleri güvenli bir şekilde depolamak için sınıfını KeyChain
kullanır. SecRecord
Değerleri Service
depolamak için kullanılan değeri {your-app-package-id}.xamarinessentials olarak ayarlanmıştır. Aşağıdaki kod sınıfını KeyChain
gösterir:
Dekont
Bu kodu kullanmak için,.NET MAUI uygulama projenizin Platforms\iOS klasöründe adlı KeyChain
bir sınıfa ekleyin.
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;
}
}
Bu kodu kullanmak için, iOS uygulamanız için Anahtarlık yetkilendirme kümesine sahip bir Entitlements.plist dosyanız olmalıdır:
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</array>
Ayrıca, Uygulamanızın Paket İmzalama ayarlarında Entitlements.plist dosyasının Özel Yetkilendirmeler alanı olarak ayarlandığından da emin olmanız gerekir. Daha fazla bilgi için bkz . iOS Yetkilendirmeleri.
Eski güvenli depolama verilerini kullanma
sınıfı LegacySecureStorage
, android ve iOS'ta uygulamanızın önceki bir Xamarin.Forms sürümüyle oluşturulmuş eski güvenli depolama verilerini kullanmak için kullanılabilir:
#if ANDROID || IOS
using MigrationHelpers;
...
string username = await LegacySecureStorage.GetAsync("username");
bool result = LegacySecureStorage.Remove("username");
await SecureStorage.SetAsync("username", username);
#endif
Örnekte, eski güvenli depolama alanından LegacySecureStorage
bir değeri okuyup kaldırmak ve ardından değeri .NET MAUI güvenli depolama alanına yazmak için sınıfının kullanılması gösterilmektedir.