Migrasi dari penyimpanan aman Xamarin.Essentials ke penyimpanan aman .NET MAUI
Xamarin.Essentials dan .NET Multi-platform App UI (.NET MAUI) keduanya memiliki SecureStorage
kelas yang membantu Anda menyimpan pasangan kunci/nilai sederhana dengan aman. Namun, ada perbedaan implementasi antara SecureStorage
kelas di Xamarin.Essentials dan .NET MAUI:
Platform | Xamarin.Essentials | .NET MAUI |
---|---|---|
Android | Android KeyStore digunakan untuk menyimpan kunci cipher yang digunakan untuk mengenkripsi nilai sebelum disimpan ke dalam objek preferensi bersama dengan nama {your-app-package-id}.xamarinessentials. | Data dienkripsi dengan EncryptedSharedPreferences kelas , yang membungkus SharedPreferences kelas, dan secara otomatis mengenkripsi kunci dan nilai. Nama yang digunakan adalah {your-app-package-id}.microsoft.maui.essentials.preferences. |
iOS | Rantai Kunci digunakan untuk menyimpan nilai dengan aman. Nilai SecRecord yang digunakan untuk menyimpan memiliki nilai yang Service diatur ke {your-app-package-id}.xamarinessentials. |
Rantai Kunci digunakan untuk menyimpan nilai dengan aman. Nilai SecRecord yang digunakan untuk menyimpan memiliki nilai yang Service diatur ke {your-app-package-id}.microsoft.maui.essentials.preferences. |
Untuk informasi selengkapnya tentang SecureStorage
kelas di Xamarin.Essentials, lihat Xamarin.Essentials: Penyimpanan aman. Untuk informasi selengkapnya tentang SecureStorage
kelas di .NET MAUI, lihat Penyimpanan aman.
Saat memigrasikan aplikasi Xamarin.Forms yang menggunakan SecureStorage
kelas ke .NET MAUI, Anda harus menangani perbedaan implementasi ini untuk memberi pengguna pengalaman peningkatan yang lancar. Artikel ini menjelaskan bagaimana Anda dapat menggunakan LegacySecureStorage
kelas dan kelas pembantu untuk menangani perbedaan implementasi. Kelas ini LegacySecureStorage
memungkinkan aplikasi .NET MAUI Anda di Android dan iOS membaca data penyimpanan aman yang dibuat dengan versi Xamarin.Forms sebelumnya dari aplikasi Anda.
Mengakses data penyimpanan aman warisan
Kode berikut menunjukkan LegacySecureStorage
kelas , yang menyediakan implementasi penyimpanan aman dari Xamarin.Essentials:
Catatan
Untuk menggunakan kode ini, tambahkan ke kelas bernama LegacySecureStorage
di proyek aplikasi .NET MAUI Anda.
#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
Di Android, LegacySecureStorage
kelas menggunakan AndroidKeyStore
kelas untuk menyimpan kunci cipher yang digunakan untuk mengenkripsi nilai sebelum disimpan ke dalam objek preferensi bersama dengan nama {your-app-package-id}.xamarinessentials. Kode berikut menunjukkan AndroidKeyStore
kelas:
Catatan
Untuk menggunakan kode ini, tambahkan ke kelas bernama AndroidKeyStore
di folder Platforms\Android dari proyek aplikasi .NET MAUI Anda.
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 digunakan untuk menyimpan kunci cipher yang digunakan untuk mengenkripsi nilai sebelum disimpan ke dalam file PreferensiBersama dengan nama {your-app-package-id}.xamarinessentials. Kunci (bukan kunci kriptografi, kunci ke nilai) yang digunakan dalam file preferensi bersama adalah Hash MD5 dari kunci yang diteruskan ke SecureStorage
API.
Pada API 23+, kunci AES diperoleh dari Android KeyStore dan digunakan dengan cipher AES/GCM/NoPadding untuk mengenkripsi nilai sebelum disimpan dalam file preferensi bersama. Pada API 22 dan yang lebih rendah, Android KeyStore hanya mendukung penyimpanan kunci RSA , yang digunakan dengan cipher RSA/ECB/PKCS1Padding untuk mengenkripsi kunci AES (dihasilkan secara acak saat runtime) dan disimpan dalam file preferensi bersama di bawah kunci SecureStorageKey, jika belum dibuat.
iOS
Di iOS, LegacySecureStorage
kelas menggunakan KeyChain
kelas untuk menyimpan nilai dengan aman. Nilai SecRecord
yang digunakan untuk menyimpan memiliki nilai yang Service
diatur ke {your-app-package-id}.xamarinessentials. Kode berikut menunjukkan KeyChain
kelas:
Catatan
Untuk menggunakan kode ini, tambahkan ke kelas bernama KeyChain
di folder Platforms\iOS dari proyek aplikasi .NET MAUI Anda.
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;
}
}
Untuk menggunakan kode ini, Anda harus memiliki file Entitlements.plist untuk aplikasi iOS Anda dengan set pemberian izin Rantai Kunci:
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</array>
Anda juga harus memastikan bahwa file Entitlements.plist diatur sebagai bidang Pemberian Izin Kustom di pengaturan Penandatanganan Bundel untuk aplikasi Anda. Untuk informasi selengkapnya, lihat Pemberian Izin iOS.
Mengonsumsi data penyimpanan aman warisan
Kelas LegacySecureStorage
ini dapat digunakan untuk menggunakan data penyimpanan aman warisan, di Android dan iOS, yang dibuat dengan versi Xamarin.Forms sebelumnya dari aplikasi Anda:
#if ANDROID || IOS
using MigrationHelpers;
...
string username = await LegacySecureStorage.GetAsync("username");
bool result = LegacySecureStorage.Remove("username");
await SecureStorage.SetAsync("username", username);
#endif
Contoh menunjukkan penggunaan LegacySecureStorage
kelas untuk membaca dan menghapus nilai dari penyimpanan aman warisan, lalu menulis nilai ke penyimpanan aman .NET MAUI.
Saran dan Komentar
https://aka.ms/ContentUserFeedback.
Segera hadir: Sepanjang tahun 2024 kami akan menghentikan penggunaan GitHub Issues sebagai mekanisme umpan balik untuk konten dan menggantinya dengan sistem umpan balik baru. Untuk mengetahui informasi selengkapnya, lihat:Kirim dan lihat umpan balik untuk