diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 1d0413c5d..9ac8a9e81 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -313,6 +313,7 @@ + diff --git a/src/Android/Services/KeyStoreBackedStorageService.cs b/src/Android/Services/KeyStoreBackedStorageService.cs new file mode 100644 index 000000000..004fd39ef --- /dev/null +++ b/src/Android/Services/KeyStoreBackedStorageService.cs @@ -0,0 +1,222 @@ +using System.IO; +using Java.Security; +using Javax.Crypto; +using Android.OS; +using Bit.App.Abstractions; +using System; +using Android.Security; +using Javax.Security.Auth.X500; +using Java.Math; +using Android.Security.Keystore; +using Android.Icu.Util; +using Android.App; +using Plugin.Settings.Abstractions; +using Javax.Crypto.Spec; +using System.Collections.Generic; + +namespace Bit.Android.Services +{ + public class KeyStoreBackedStorageService : ISecureStorageService + { + private const string AndroidKeyStore = "AndroidKeyStore"; + private const string AndroidOpenSSL = "AndroidOpenSSL"; + private const string KeyAlias = "bitwardenKey"; + private const string SettingsFormat = "ksSecured:{0}"; + private const string RsaMode = "RSA/ECB/PKCS1Padding"; + private const string AesMode = "AES/GCM/NoPadding"; + private const string EncryptedKey = "ksSecuredAesKey"; + + private readonly ISettings _settings; + private readonly KeyStore _keyStore; + private readonly bool _oldAndroid = Build.VERSION.SdkInt < BuildVersionCodes.M; + + public KeyStoreBackedStorageService(ISettings settings) + { + _settings = settings; + + _keyStore = KeyStore.GetInstance(AndroidKeyStore); + _keyStore.Load(null); + + GenerateKeys(); + GenerateAesKey(); + } + + public bool Contains(string key) + { + return _settings.Contains(string.Format(SettingsFormat, key)); + } + + public void Delete(string key) + { + _settings.Remove(string.Format(SettingsFormat, key)); + } + + public byte[] Retrieve(string key) + { + var cipherString = _settings.GetValueOrDefault(string.Format(SettingsFormat, key)); + if(cipherString == null) + { + return null; + } + + return AesDecrypt(cipherString); + } + + public void Store(string key, byte[] dataBytes) + { + if(dataBytes == null) + { + _settings.Remove(key); + return; + } + + var cipherString = AesEncrypt(dataBytes); + _settings.AddOrUpdateValue(key, cipherString); + } + + private byte[] RandomBytes(int length) + { + var key = new byte[length]; + var secureRandom = new SecureRandom(); + secureRandom.NextBytes(key); + return key; + } + + private void GenerateKeys() + { + if(_keyStore.ContainsAlias(KeyAlias)) + { + return; + } + + if(_oldAndroid) + { + var start = Calendar.Instance; + var end = Calendar.Instance; + end.Add(CalendarField.Year, 30); + + var gen = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, AndroidKeyStore); + var spec = new KeyPairGeneratorSpec.Builder(Application.Context) + .SetAlias(KeyAlias) + .SetSubject(new X500Principal($"CN={KeyAlias}")) + .SetSerialNumber(BigInteger.Ten) + .SetStartDate(start.Time) + .SetEndDate(end.Time) + .Build(); + + gen.Initialize(spec); + gen.GenerateKeyPair(); + } + else + { + var gen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, AndroidKeyStore); + var spec = new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Decrypt | KeyStorePurpose.Encrypt) + .SetBlockModes(KeyProperties.BlockModeGcm).SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone) + .Build(); + + gen.Init(spec); + gen.GenerateKey(); + } + } + + private void GenerateAesKey() + { + if(!_oldAndroid) + { + return; + } + + if(_settings.Contains(EncryptedKey)) + { + return; + } + + var key = RandomBytes(16); + var encKey = RsaEncrypt(key); + _settings.AddOrUpdateValue(EncryptedKey, Convert.ToBase64String(encKey)); + } + + private IKey GetAesKey() + { + if(_oldAndroid) + { + var encKey = _settings.GetValueOrDefault(EncryptedKey); + var encKeyBytes = Convert.FromBase64String(encKey); + var key = RsaDecrypt(encKeyBytes); + return new SecretKeySpec(key, "AES"); + } + else + { + var entry = _keyStore.GetEntry(KeyAlias, null) as KeyStore.SecretKeyEntry; + return entry.SecretKey; + } + } + + private string AesEncrypt(byte[] input) + { + var cipher = Cipher.GetInstance(AesMode); + var ivBytes = RandomBytes(12); + var spec = new GCMParameterSpec(128, ivBytes); + cipher.Init(CipherMode.EncryptMode, GetAesKey(), spec); + var encBytes = cipher.DoFinal(input); + return $"{Convert.ToBase64String(ivBytes)}|{Convert.ToBase64String(encBytes)}"; + } + + private byte[] AesDecrypt(string cipherString) + { + var parts = cipherString.Split('|'); + var ivBytes = Convert.FromBase64String(parts[0]); + var encBytes = Convert.FromBase64String(parts[1]); + + var cipher = Cipher.GetInstance(AesMode); + var spec = new GCMParameterSpec(128, ivBytes); + cipher.Init(CipherMode.DecryptMode, GetAesKey(), spec); + var decBytes = cipher.DoFinal(encBytes); + return decBytes; + } + + private byte[] RsaEncrypt(byte[] input) + { + var entry = _keyStore.GetEntry(KeyAlias, null) as KeyStore.PrivateKeyEntry; + var inputCipher = Cipher.GetInstance(RsaMode, AndroidOpenSSL); + inputCipher.Init(CipherMode.EncryptMode, entry.Certificate.PublicKey); + + var outputStream = new MemoryStream(); + var cipherStream = new CipherOutputStream(outputStream, inputCipher); + cipherStream.Write(input); + cipherStream.Close(); + + var vals = outputStream.ToArray(); + outputStream.Close(); + return vals; + } + + private byte[] RsaDecrypt(byte[] encInput) + { + var entry = _keyStore.GetEntry(KeyAlias, null) as KeyStore.PrivateKeyEntry; + var outputCipher = Cipher.GetInstance(RsaMode, AndroidOpenSSL); + outputCipher.Init(CipherMode.DecryptMode, entry.PrivateKey); + + var inputStream = new MemoryStream(encInput); + var cipherStream = new CipherInputStream(inputStream, outputCipher); + + var values = new List(); + int nextByte; + while((nextByte = cipherStream.Read()) != -1) + { + values.Add((byte)nextByte); + } + + inputStream.Close(); + cipherStream.Close(); + + var bytes = new byte[values.Count]; + for(var i = 0; i < bytes.Length; i++) + { + bytes[i] = values[i]; + } + + return bytes; + } + } +} \ No newline at end of file