Migrar do armazenamento seguro Xamarin.Essentials para o armazenamento seguro .NET MAUI
O Xamarin.Essentials e o .NET Multi-platform App UI (.NET MAUI) têm uma SecureStorage
classe que ajuda você a armazenar com segurança pares de chave/valor simples. No entanto, há diferenças de implementação entre a classe no Xamarin.Essentials e . SecureStorage
NET MAUI:
Plataforma | Xamarin.Essentials | .NET MAUI |
---|---|---|
Android | O Android KeyStore é usado para armazenar a chave de codificação usada para criptografar um valor antes que ele seja salvo em um objeto de preferências compartilhadas com um nome de {your-app-package-id}.xamarinessentials. | Os dados são criptografados com a classe, que encapsula a EncryptedSharedPreferences SharedPreferences classe e criptografa automaticamente chaves e valores. O nome usado é {your-app-package-id}.microsoft.maui.essentials.preferences. |
iOS | KeyChain é usado para armazenar valores com segurança. O SecRecord usado para armazenar valores tem um Service valor definido como {your-app-package-id}.xamarinessentials. |
KeyChain é usado para armazenar valores com segurança. O SecRecord usado para armazenar valores tem um Service valor definido como {your-app-package-id}.microsoft.maui.essentials.preferences. |
Para obter mais informações sobre a SecureStorage
classe no Xamarin.Essentials, consulte Xamarin.Essentials: armazenamento seguro. Para obter mais informações sobre a SecureStorage
classe no .NET MAUI, consulte Armazenamento seguro.
Ao migrar um aplicativo Xamarin.Forms que usa a classe para . SecureStorage
NET MAUI, você deve lidar com essas diferenças de implementação para fornecer aos usuários uma experiência de atualização suave. Este artigo descreve como você pode usar as LegacySecureStorage
classes de classe e auxiliar para lidar com as diferenças de implementação. A LegacySecureStorage
classe permite que seu aplicativo .NET MAUI no Android e iOS leia dados de armazenamento seguro que foram criados com uma versão anterior do Xamarin.Forms do seu aplicativo.
Acessar dados de armazenamento seguro herdados
O código a seguir mostra a classe, que fornece a LegacySecureStorage
implementação de armazenamento seguro do Xamarin.Essentials:
Observação
Para usar esse código, adicione-o a uma classe nomeada LegacySecureStorage
em seu projeto de aplicativo .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
No Android, a classe usa a classe para armazenar a LegacySecureStorage
AndroidKeyStore
chave de codificação usada para criptografar um valor antes que ele seja salvo em um objeto de preferências compartilhadas com um nome de {your-app-package-id}.xamarinessentials. O código a seguir mostra a classe AndroidKeyStore
:
Observação
Para usar esse código, adicione-o a uma classe nomeada AndroidKeyStore
na pasta Platforms\Android do seu projeto de aplicativo .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
}
}
O Android KeyStore é usado para armazenar a chave de codificação usada para criptografar o valor antes de ser salvo em um arquivo de Preferências Compartilhadascom um nome de {your-app-package-id}.xamarinessentials. A chave (não uma chave criptográfica, a chave para o valor) usada no arquivo de preferências compartilhadas é um Hash MD5 da chave passada para as SecureStorage
APIs.
Na API 23+, uma chave AES é obtida do Android KeyStore e usada com uma cifra AES/GCM/NoPadding para criptografar o valor antes de ser armazenado no arquivo de preferências compartilhadas. Na API 22 e inferior, o Android KeyStore oferece suporte apenas ao armazenamento de chaves RSA, que é usado com uma cifra RSA/ECB/PKCS1Padding para criptografar uma chave AES (gerada aleatoriamente em tempo de execução) e armazenada no arquivo de preferências compartilhadas sob a chave SecureStorageKey, se ainda não tiver sido gerada.
iOS
No iOS, a classe usa a LegacySecureStorage
KeyChain
classe para armazenar valores com segurança. O SecRecord
usado para armazenar valores tem um Service
valor definido como {your-app-package-id}.xamarinessentials. O código a seguir mostra a classe KeyChain
:
Observação
Para usar esse código, adicione-o a uma classe nomeada KeyChain
na pasta Platforms\iOS do seu projeto de aplicativo .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;
}
}
Para usar esse código, você deve ter um arquivo Entitlements.plist para seu aplicativo iOS com o conjunto de direitos Keychain:
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
</array>
Você também deve garantir que o arquivo Entitlements.plist esteja definido como o campo Direitos Personalizados nas configurações de Assinatura de Pacote para seu aplicativo. Para obter mais informações, consulte Direitos do iOS.
Consumir dados de armazenamento seguro herdados
A LegacySecureStorage
classe pode ser usada para consumir dados de armazenamento seguro herdados, no Android e iOS, que foram criados com uma versão anterior do Xamarin.Forms do seu aplicativo:
#if ANDROID || IOS
using MigrationHelpers;
...
string username = await LegacySecureStorage.GetAsync("username");
bool result = LegacySecureStorage.Remove("username");
await SecureStorage.SetAsync("username", username);
#endif
O exemplo mostra o uso da classe para ler e remover um valor do armazenamento seguro herdado e, em seguida, gravar o LegacySecureStorage
valor no armazenamento seguro do .NET MAUI.