Migrer du stockage sécurisé Xamarin.Essentials vers le stockage sécurisé .NET MAUI
L’interface utilisateur de l’application multiplateforme Xamarin.Essentials et .NET (.NET MAUI) possèdent toutes deux une SecureStorage
classe qui vous permet de stocker en toute sécurité des paires clé/valeur simples. Toutefois, il existe des différences d’implémentation entre la SecureStorage
classe dans Xamarin.Essentials et .NET MAUI :
Plate-forme | Xamarin.Essentials | .NET MAUI |
---|---|---|
Android | Android KeyStore est utilisé pour stocker la clé de chiffrement utilisée pour chiffrer une valeur avant son enregistrement dans un objet de préférences partagées avec le nom {your-app-package-id}.xamarinessentials. | Les données sont chiffrées avec la EncryptedSharedPreferences classe, qui encapsule la SharedPreferences classe, et chiffre automatiquement les clés et les valeurs. Le nom utilisé est {your-app-package-id}.microsoft.maui.essentials.preferences. |
iOS | KeyChain est utilisé pour stocker les valeurs de manière sécurisée. La SecRecord valeur utilisée pour stocker les valeurs a une Service valeur définie sur {your-app-package-id}.xamarinessentials. |
KeyChain est utilisé pour stocker les valeurs de manière sécurisée. La SecRecord valeur utilisée pour stocker les valeurs a la Service valeur {your-app-package-id}.microsoft.maui.essentials.preferences. |
Pour plus d’informations sur la SecureStorage
classe dans Xamarin.Essentials, consultez Xamarin.Essentials : Stockage sécurisé. Pour plus d’informations sur la SecureStorage
classe dans .NET MAUI, consultez Stockage sécurisé.
Lors de la migration d’une application Xamarin.Forms qui utilise la SecureStorage
classe vers .NET MAUI, vous devez gérer ces différences d’implémentation pour fournir aux utilisateurs une expérience de mise à niveau fluide. Cet article explique comment utiliser les LegacySecureStorage
classes de classe et d’assistance pour gérer les différences d’implémentation. La LegacySecureStorage
classe permet à votre application .NET MAUI sur Android et iOS de lire des données de stockage sécurisées créées avec une version précédente de Xamarin.Forms de votre application.
Accéder aux données de stockage sécurisée héritées
Le code suivant montre la LegacySecureStorage
classe, qui fournit l’implémentation de stockage sécurisée à partir de Xamarin.Essentials :
Remarque
Pour utiliser ce code, ajoutez-le à une classe nommée LegacySecureStorage
dans votre projet d’application .NET MAUI.
#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
Sur Android, la LegacySecureStorage
classe utilise la AndroidKeyStore
classe pour stocker la clé de chiffrement utilisée pour chiffrer une valeur avant son enregistrement dans un objet de préférences partagées avec le nom {your-app-package-id}.xamarinessentials. Le code suivant illustre la classe AndroidKeyStore
:
Remarque
Pour utiliser ce code, ajoutez-le à une classe nommée AndroidKeyStore
dans le dossier Platforms\Android de votre projet d’application .NET MAUI.
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 est utilisé pour stocker la clé de chiffrement utilisée pour chiffrer la valeur avant son enregistrement dans un fichier Préférencespartagées avec le nom { your-app-package-id}.xamarinessentials. La clé (et non une clé de chiffrement, la clé à la valeur) utilisée dans le fichier de préférences partagées est un hachage MD5 de la clé passée dans les SecureStorage
API.
Sur l’API 23+, une clé AES est obtenue à partir du magasin de clés Android et utilisée avec un chiffrement AES/GCM/NoPadding pour chiffrer la valeur avant qu’elle ne soit stockée dans le fichier de préférences partagées. Sur l’API 22 et inférieure, android KeyStore prend uniquement en charge le stockage de clés RSA, qui est utilisé avec un chiffrement RSA/BCE/PKCS1Padding pour chiffrer une clé AES (générée de manière aléatoire au moment de l’exécution) et stocké dans le fichier de préférences partagées sous la clé Secure Stockage Key, si aucune clé n’a déjà été générée.
iOS
Sur iOS, la LegacySecureStorage
classe utilise la KeyChain
classe pour stocker les valeurs en toute sécurité. La SecRecord
valeur utilisée pour stocker les valeurs a une Service
valeur définie sur {your-app-package-id}.xamarinessentials. Le code suivant illustre la classe KeyChain
:
Remarque
Pour utiliser ce code, ajoutez-le à une classe nommée KeyChain
dans le dossier Platforms\iOS de votre projet d’application .NET MAUI.
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;
}
}
Pour utiliser ce code, vous devez disposer d’un fichier Entitlements.plist pour votre application iOS avec le jeu de droits du trousseau :
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</array>
Vous devez également vous assurer que le fichier Entitlements.plist est défini en tant que champ Droits personnalisés dans les paramètres de signature de bundle pour votre application. Pour plus d’informations, consultez droits iOS.
Consommer des données de stockage sécurisée héritées
La LegacySecureStorage
classe peut être utilisée pour consommer des données de stockage sécurisée héritées, sur Android et iOS, créées avec une version précédente de Xamarin.Forms de votre application :
#if ANDROID || IOS
using MigrationHelpers;
...
string username = await LegacySecureStorage.GetAsync("username");
bool result = LegacySecureStorage.Remove("username");
await SecureStorage.SetAsync("username", username);
#endif
L’exemple montre comment utiliser la LegacySecureStorage
classe pour lire et supprimer une valeur du stockage sécurisé hérité, puis écrire la valeur dans le stockage sécurisé .NET MAUI.