diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 47e3863a4..2f20f1023 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -110,6 +110,7 @@ + diff --git a/src/Android/Migration/AndroidKeyStoreStorageService.cs b/src/Android/Migration/AndroidKeyStoreStorageService.cs new file mode 100644 index 000000000..453b0f12a --- /dev/null +++ b/src/Android/Migration/AndroidKeyStoreStorageService.cs @@ -0,0 +1,371 @@ +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.App; +using Java.Util; +using Javax.Crypto.Spec; +using Android.Preferences; +using Bit.App.Migration; + +namespace Bit.Droid.Migration +{ + public class AndroidKeyStoreStorageService + { + private const string AndroidKeyStore = "AndroidKeyStore"; + private const string AesMode = "AES/GCM/NoPadding"; + + private const string KeyAlias = "bitwardenKey2"; + private const string KeyAliasV1 = "bitwardenKey"; + + private const string SettingsFormat = "ksSecured2:{0}"; + private const string SettingsFormatV1 = "ksSecured:{0}"; + + private const string AesKey = "ksSecured2:aesKeyForService"; + private const string AesKeyV1 = "ksSecured:aesKeyForService"; + + private readonly string _rsaMode; + private readonly bool _oldAndroid; + private readonly SettingsShim _settings; + private readonly KeyStore _keyStore; + + public AndroidKeyStoreStorageService() + { + _oldAndroid = Build.VERSION.SdkInt < BuildVersionCodes.M; + _rsaMode = _oldAndroid ? "RSA/ECB/PKCS1Padding" : "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; + + _settings = new SettingsShim(); + + _keyStore = KeyStore.GetInstance(AndroidKeyStore); + _keyStore.Load(null); + + /* + try + { + GenerateStoreKey(true); + } + catch + { + GenerateStoreKey(false); + } + + GenerateAesKey(); + */ + } + + public bool Contains(string key) + { + return _settings.Contains(string.Format(SettingsFormat, key)) || + _settings.Contains(string.Format(SettingsFormatV1, key)); + } + + public void Delete(string key) + { + CleanupOld(key); + + var formattedKey = string.Format(SettingsFormat, key); + if(_settings.Contains(formattedKey)) + { + _settings.Remove(formattedKey); + } + } + + public byte[] Retrieve(string key) + { + var formattedKey = string.Format(SettingsFormat, key); + if(!_settings.Contains(formattedKey)) + { + return TryGetAndMigrate(key); + } + + var cs = _settings.GetValueOrDefault(formattedKey, null); + if(string.IsNullOrWhiteSpace(cs)) + { + return null; + } + + var aesKey = GetAesKey(); + if(aesKey == null) + { + return null; + } + + try + { + return App.Migration.Crypto.AesCbcDecrypt(new App.Migration.Models.CipherString(cs), aesKey); + } + catch + { + Console.WriteLine("Failed to decrypt from secure storage."); + _settings.Remove(formattedKey); + //Utilities.SendCrashEmail(e); + //Utilities.SaveCrashFile(e); + return null; + } + } + + public void Store(string key, byte[] dataBytes) + { + var formattedKey = string.Format(SettingsFormat, key); + CleanupOld(key); + if(dataBytes == null) + { + _settings.Remove(formattedKey); + return; + } + + var aesKey = GetAesKey(); + if(aesKey == null) + { + return; + } + + try + { + var cipherString = App.Migration.Crypto.AesCbcEncrypt(dataBytes, aesKey); + _settings.AddOrUpdateValue(formattedKey, cipherString.EncryptedString); + } + catch + { + Console.WriteLine("Failed to encrypt to secure storage."); + //Utilities.SendCrashEmail(e); + //Utilities.SaveCrashFile(e); + } + } + + private void GenerateStoreKey(bool withDate) + { + if(_keyStore.ContainsAlias(KeyAlias)) + { + return; + } + + ClearSettings(); + + var end = Calendar.Instance; + end.Add(CalendarField.Year, 99); + + if(_oldAndroid) + { + var subject = new X500Principal($"CN={KeyAlias}"); + + var builder = new KeyPairGeneratorSpec.Builder(Application.Context) + .SetAlias(KeyAlias) + .SetSubject(subject) + .SetSerialNumber(BigInteger.Ten); + + if(withDate) + { + builder.SetStartDate(new Date(0)).SetEndDate(end.Time); + } + + var spec = builder.Build(); + var gen = KeyPairGenerator.GetInstance(KeyProperties.KeyAlgorithmRsa, AndroidKeyStore); + gen.Initialize(spec); + gen.GenerateKeyPair(); + } + else + { + var builder = new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Decrypt | KeyStorePurpose.Encrypt) + .SetBlockModes(KeyProperties.BlockModeGcm) + .SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone); + + if(withDate) + { + builder.SetKeyValidityStart(new Date(0)).SetKeyValidityEnd(end.Time); + } + + var spec = builder.Build(); + var gen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, AndroidKeyStore); + gen.Init(spec); + gen.GenerateKey(); + } + } + + private KeyStore.PrivateKeyEntry GetRsaKeyEntry(string alias) + { + return _keyStore.GetEntry(alias, null) as KeyStore.PrivateKeyEntry; + } + + private void GenerateAesKey() + { + if(_settings.Contains(AesKey)) + { + return; + } + + var key = App.Migration.Crypto.RandomBytes(512 / 8); + var encKey = _oldAndroid ? RsaEncrypt(key) : AesEncrypt(key); + _settings.AddOrUpdateValue(AesKey, encKey); + } + + private App.Migration.Models.SymmetricCryptoKey GetAesKey(bool v1 = false) + { + try + { + var aesKey = v1 ? AesKeyV1 : AesKey; + if(!_settings.Contains(aesKey)) + { + return null; + } + + var encKey = _settings.GetValueOrDefault(aesKey, null); + if(string.IsNullOrWhiteSpace(encKey)) + { + return null; + } + + if(_oldAndroid || v1) + { + var encKeyBytes = Convert.FromBase64String(encKey); + var key = RsaDecrypt(encKeyBytes, v1); + return new App.Migration.Models.SymmetricCryptoKey(key); + } + else + { + var parts = encKey.Split('|'); + if(parts.Length < 2) + { + return null; + } + + var ivBytes = Convert.FromBase64String(parts[0]); + var encKeyBytes = Convert.FromBase64String(parts[1]); + var key = AesDecrypt(ivBytes, encKeyBytes); + return new App.Migration.Models.SymmetricCryptoKey(key); + } + } + catch + { + Console.WriteLine("Cannot get AesKey."); + _keyStore.DeleteEntry(KeyAlias); + _settings.Remove(AesKey); + if(!v1) + { + //Utilities.SendCrashEmail(e); + //Utilities.SaveCrashFile(e); + } + return null; + } + } + + private string AesEncrypt(byte[] input) + { + using(var entry = _keyStore.GetKey(KeyAlias, null)) + using(var cipher = Cipher.GetInstance(AesMode)) + { + cipher.Init(CipherMode.EncryptMode, entry); + var encBytes = cipher.DoFinal(input); + var ivBytes = cipher.GetIV(); + return $"{Convert.ToBase64String(ivBytes)}|{Convert.ToBase64String(encBytes)}"; + } + } + + private byte[] AesDecrypt(byte[] iv, byte[] encData) + { + using(var entry = _keyStore.GetKey(KeyAlias, null)) + using(var cipher = Cipher.GetInstance(AesMode)) + { + var spec = new GCMParameterSpec(128, iv); + cipher.Init(CipherMode.DecryptMode, entry, spec); + var decBytes = cipher.DoFinal(encData); + return decBytes; + } + } + + private string RsaEncrypt(byte[] data) + { + using(var entry = GetRsaKeyEntry(KeyAlias)) + using(var cipher = Cipher.GetInstance(_rsaMode)) + { + cipher.Init(CipherMode.EncryptMode, entry.Certificate.PublicKey); + var cipherText = cipher.DoFinal(data); + return Convert.ToBase64String(cipherText); + } + } + + private byte[] RsaDecrypt(byte[] encData, bool v1) + { + using(var entry = GetRsaKeyEntry(v1 ? KeyAliasV1 : KeyAlias)) + using(var cipher = Cipher.GetInstance(_rsaMode)) + { + if(_oldAndroid) + { + cipher.Init(CipherMode.DecryptMode, entry.PrivateKey); + } + else + { + cipher.Init(CipherMode.DecryptMode, entry.PrivateKey, OAEPParameterSpec.Default); + } + + var plainText = cipher.DoFinal(encData); + return plainText; + } + } + + private byte[] TryGetAndMigrate(string key) + { + var formattedKeyV1 = string.Format(SettingsFormatV1, key); + if(_settings.Contains(formattedKeyV1)) + { + var aesKeyV1 = GetAesKey(true); + if(aesKeyV1 != null) + { + try + { + var cs = _settings.GetValueOrDefault(formattedKeyV1, null); + var value = App.Migration.Crypto.AesCbcDecrypt(new App.Migration.Models.CipherString(cs), aesKeyV1); + Store(key, value); + return value; + } + catch + { + Console.WriteLine("Failed to decrypt v1 from secure storage."); + } + } + + _settings.Remove(formattedKeyV1); + } + + return null; + } + + private void CleanupOld(string key) + { + var formattedKeyV1 = string.Format(SettingsFormatV1, key); + if(_settings.Contains(formattedKeyV1)) + { + _settings.Remove(formattedKeyV1); + } + } + + private void ClearSettings(string format = SettingsFormat) + { + var prefix = string.Format(format, string.Empty); + + using(var sharedPreferences = PreferenceManager.GetDefaultSharedPreferences(Application.Context)) + using(var sharedPreferencesEditor = sharedPreferences.Edit()) + { + var removed = false; + foreach(var pref in sharedPreferences.All) + { + if(pref.Key.StartsWith(prefix)) + { + removed = true; + sharedPreferencesEditor.Remove(pref.Key); + } + } + + if(removed) + { + sharedPreferencesEditor.Commit(); + } + } + } + } +} \ No newline at end of file diff --git a/src/App/Migration/Crypto.cs b/src/App/Migration/Crypto.cs new file mode 100644 index 000000000..d17ca639a --- /dev/null +++ b/src/App/Migration/Crypto.cs @@ -0,0 +1,199 @@ +using Bit.App.Migration.Models; +using Bit.Core.Enums; +using PCLCrypto; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.App.Migration +{ + public static class Crypto + { + public static CipherString AesCbcEncrypt(byte[] plainBytes, SymmetricCryptoKey key) + { + var parts = AesCbcEncryptToParts(plainBytes, key); + return new CipherString(parts.Item1, Convert.ToBase64String(parts.Item2), + Convert.ToBase64String(parts.Item4), parts.Item3 != null ? Convert.ToBase64String(parts.Item3) : null); + } + + public static byte[] AesCbcEncryptToBytes(byte[] plainBytes, SymmetricCryptoKey key) + { + var parts = AesCbcEncryptToParts(plainBytes, key); + var macLength = parts.Item3?.Length ?? 0; + + var encBytes = new byte[1 + parts.Item2.Length + macLength + parts.Item4.Length]; + encBytes[0] = (byte)parts.Item1; + parts.Item2.CopyTo(encBytes, 1); + if(parts.Item3 != null) + { + parts.Item3.CopyTo(encBytes, 1 + parts.Item2.Length); + } + parts.Item4.CopyTo(encBytes, 1 + parts.Item2.Length + macLength); + return encBytes; + } + + private static Tuple AesCbcEncryptToParts(byte[] plainBytes, + SymmetricCryptoKey key) + { + if(key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if(plainBytes == null) + { + throw new ArgumentNullException(nameof(plainBytes)); + } + + var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); + var cryptoKey = provider.CreateSymmetricKey(key.EncKey); + var iv = RandomBytes(provider.BlockLength); + var ct = WinRTCrypto.CryptographicEngine.Encrypt(cryptoKey, plainBytes, iv); + var mac = key.MacKey != null ? ComputeMac(ct, iv, key.MacKey) : null; + + return new Tuple(key.EncryptionType, iv, mac, ct); + } + + public static byte[] AesCbcDecrypt(CipherString encyptedValue, SymmetricCryptoKey key) + { + if(encyptedValue == null) + { + throw new ArgumentNullException(nameof(encyptedValue)); + } + + return AesCbcDecrypt(encyptedValue.EncryptionType, encyptedValue.CipherTextBytes, + encyptedValue.InitializationVectorBytes, encyptedValue.MacBytes, key); + } + + public static byte[] AesCbcDecrypt(EncryptionType type, byte[] ct, byte[] iv, byte[] mac, + SymmetricCryptoKey key) + { + if(key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if(ct == null) + { + throw new ArgumentNullException(nameof(ct)); + } + + if(iv == null) + { + throw new ArgumentNullException(nameof(iv)); + } + + if(key.MacKey != null && mac == null) + { + throw new ArgumentNullException(nameof(mac)); + } + + if(key.EncryptionType != type) + { + throw new InvalidOperationException(nameof(type)); + } + + if(key.MacKey != null && mac != null) + { + var computedMacBytes = ComputeMac(ct, iv, key.MacKey); + if(!MacsEqual(computedMacBytes, mac)) + { + throw new InvalidOperationException("MAC failed."); + } + } + + var provider = WinRTCrypto.SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); + var cryptoKey = provider.CreateSymmetricKey(key.EncKey); + var decryptedBytes = WinRTCrypto.CryptographicEngine.Decrypt(cryptoKey, ct, iv); + return decryptedBytes; + } + + public static byte[] RandomBytes(int length) + { + return WinRTCrypto.CryptographicBuffer.GenerateRandom(length); + } + + public static byte[] ComputeMac(byte[] ctBytes, byte[] ivBytes, byte[] macKey) + { + if(ctBytes == null) + { + throw new ArgumentNullException(nameof(ctBytes)); + } + + if(ivBytes == null) + { + throw new ArgumentNullException(nameof(ivBytes)); + } + + return ComputeMac(ivBytes.Concat(ctBytes), macKey); + } + + public static byte[] ComputeMac(IEnumerable dataBytes, byte[] macKey) + { + if(macKey == null) + { + throw new ArgumentNullException(nameof(macKey)); + } + + if(dataBytes == null) + { + throw new ArgumentNullException(nameof(dataBytes)); + } + + var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256); + var hasher = algorithm.CreateHash(macKey); + hasher.Append(dataBytes.ToArray()); + var mac = hasher.GetValueAndReset(); + return mac; + } + + // Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification). + // ref: https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/february/double-hmac-verification/ + // ref: https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy + public static bool MacsEqual(byte[] mac1, byte[] mac2) + { + var algorithm = WinRTCrypto.MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256); + var hasher = algorithm.CreateHash(RandomBytes(32)); + + hasher.Append(mac1); + mac1 = hasher.GetValueAndReset(); + + hasher.Append(mac2); + mac2 = hasher.GetValueAndReset(); + + if(mac1.Length != mac2.Length) + { + return false; + } + + for(int i = 0; i < mac2.Length; i++) + { + if(mac1[i] != mac2[i]) + { + return false; + } + } + + return true; + } + + // ref: https://tools.ietf.org/html/rfc5869 + public static byte[] HkdfExpand(byte[] prk, byte[] info, int size) + { + var hashLen = 32; // sha256 + var okm = new byte[size]; + var previousT = new byte[0]; + var n = (int)Math.Ceiling((double)size / hashLen); + for(int i = 0; i < n; i++) + { + var t = new byte[previousT.Length + info.Length + 1]; + previousT.CopyTo(t, 0); + info.CopyTo(t, previousT.Length); + t[t.Length - 1] = (byte)(i + 1); + previousT = ComputeMac(t, prk); + previousT.CopyTo(okm, i * hashLen); + } + return okm; + } + } +} diff --git a/src/App/Migration/Models/CipherString.cs b/src/App/Migration/Models/CipherString.cs new file mode 100644 index 000000000..e9c61d74b --- /dev/null +++ b/src/App/Migration/Models/CipherString.cs @@ -0,0 +1,117 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.App.Migration.Models +{ + public class CipherString + { + private string _decryptedValue; + + public CipherString(string encryptedString) + { + if(string.IsNullOrWhiteSpace(encryptedString)) + { + throw new ArgumentException(nameof(encryptedString)); + } + + var headerPieces = encryptedString.Split('.'); + string[] encPieces; + + EncryptionType encType; + if(headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out encType)) + { + EncryptionType = encType; + encPieces = headerPieces[1].Split('|'); + } + else if(headerPieces.Length == 1) + { + encPieces = headerPieces[0].Split('|'); + EncryptionType = encPieces.Length == 3 ? EncryptionType.AesCbc128_HmacSha256_B64 : + EncryptionType.AesCbc256_B64; + } + else + { + throw new ArgumentException("Malformed header."); + } + + switch(EncryptionType) + { + case EncryptionType.AesCbc256_B64: + if(encPieces.Length != 2) + { + throw new ArgumentException("Malformed encPieces."); + } + InitializationVector = encPieces[0]; + CipherText = encPieces[1]; + break; + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if(encPieces.Length != 3) + { + throw new ArgumentException("Malformed encPieces."); + } + InitializationVector = encPieces[0]; + CipherText = encPieces[1]; + Mac = encPieces[2]; + break; + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha1_B64: + if(encPieces.Length != 1) + { + throw new ArgumentException("Malformed encPieces."); + } + CipherText = encPieces[0]; + break; + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + if(encPieces.Length != 2) + { + throw new ArgumentException("Malformed encPieces."); + } + CipherText = encPieces[0]; + Mac = encPieces[1]; + break; + default: + throw new ArgumentException("Unknown encType."); + } + + EncryptedString = encryptedString; + } + + public CipherString(EncryptionType encryptionType, string initializationVector, string cipherText, + string mac = null) + { + if(string.IsNullOrWhiteSpace(initializationVector)) + { + throw new ArgumentNullException(nameof(initializationVector)); + } + + if(string.IsNullOrWhiteSpace(cipherText)) + { + throw new ArgumentNullException(nameof(cipherText)); + } + + EncryptionType = encryptionType; + EncryptedString = string.Format("{0}.{1}|{2}", (byte)EncryptionType, initializationVector, cipherText); + + if(!string.IsNullOrWhiteSpace(mac)) + { + EncryptedString = string.Format("{0}|{1}", EncryptedString, mac); + } + + CipherText = cipherText; + InitializationVector = initializationVector; + Mac = mac; + } + + public EncryptionType EncryptionType { get; private set; } + public string EncryptedString { get; private set; } + public string InitializationVector { get; private set; } + public string CipherText { get; private set; } + public string Mac { get; private set; } + public byte[] InitializationVectorBytes => string.IsNullOrWhiteSpace(InitializationVector) ? + null : Convert.FromBase64String(InitializationVector); + public byte[] CipherTextBytes => Convert.FromBase64String(CipherText); + public byte[] MacBytes => Mac == null ? null : Convert.FromBase64String(Mac); + } +} diff --git a/src/App/Migration/Models/SymmetricCryptoKey.cs b/src/App/Migration/Models/SymmetricCryptoKey.cs new file mode 100644 index 000000000..2a0572dba --- /dev/null +++ b/src/App/Migration/Models/SymmetricCryptoKey.cs @@ -0,0 +1,62 @@ +using Bit.Core.Enums; +using System; +using System.Linq; + +namespace Bit.App.Migration.Models +{ + public class SymmetricCryptoKey + { + public SymmetricCryptoKey(byte[] rawBytes, EncryptionType? encType = null) + { + if(rawBytes == null || rawBytes.Length == 0) + { + throw new Exception("Must provide keyBytes."); + } + + if(encType == null) + { + if(rawBytes.Length == 32) + { + encType = EncryptionType.AesCbc256_B64; + } + else if(rawBytes.Length == 64) + { + encType = EncryptionType.AesCbc256_HmacSha256_B64; + } + else + { + throw new Exception("Unable to determine encType."); + } + } + + EncryptionType = encType.Value; + Key = rawBytes; + + if(EncryptionType == EncryptionType.AesCbc256_B64 && Key.Length == 32) + { + EncKey = Key; + MacKey = null; + } + else if(EncryptionType == EncryptionType.AesCbc128_HmacSha256_B64 && Key.Length == 32) + { + EncKey = Key.Take(16).ToArray(); + MacKey = Key.Skip(16).Take(16).ToArray(); + } + else if(EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64 && Key.Length == 64) + { + EncKey = Key.Take(32).ToArray(); + MacKey = Key.Skip(32).Take(32).ToArray(); + } + else + { + throw new Exception("Unsupported encType/key length."); + } + } + + public byte[] Key { get; set; } + public string B64Key => Convert.ToBase64String(Key); + public byte[] EncKey { get; set; } + public byte[] MacKey { get; set; } + public EncryptionType EncryptionType { get; set; } + } +} diff --git a/src/App/Migration/SettingsShim.cs b/src/App/Migration/SettingsShim.cs new file mode 100644 index 000000000..cf88a3caa --- /dev/null +++ b/src/App/Migration/SettingsShim.cs @@ -0,0 +1,67 @@ +using System; + +namespace Bit.App.Migration +{ + public class SettingsShim + { + public bool Contains(string key) + { + return Xamarin.Essentials.Preferences.ContainsKey(key); + } + + public string GetValueOrDefault(string key, string defaultValue) + { + return Xamarin.Essentials.Preferences.Get(key, defaultValue); + } + + public DateTime GetValueOrDefault(string key, DateTime defaultValue) + { + return Xamarin.Essentials.Preferences.Get(key, defaultValue); + } + + public bool GetValueOrDefault(string key, bool defaultValue) + { + return Xamarin.Essentials.Preferences.Get(key, defaultValue); + } + + public int GetValueOrDefault(string key, int defaultValue) + { + return Xamarin.Essentials.Preferences.Get(key, defaultValue); + } + + public long GetValueOrDefault(string key, long defaultValue) + { + return Xamarin.Essentials.Preferences.Get(key, defaultValue); + } + + public void AddOrUpdateValue(string key, string value) + { + Xamarin.Essentials.Preferences.Set(key, value); + } + + public void AddOrUpdateValue(string key, DateTime value) + { + Xamarin.Essentials.Preferences.Set(key, value); + } + + public void AddOrUpdateValue(string key, bool value) + { + Xamarin.Essentials.Preferences.Set(key, value); + } + + public void AddOrUpdateValue(string key, long value) + { + Xamarin.Essentials.Preferences.Set(key, value); + } + + public void AddOrUpdateValue(string key, int value) + { + Xamarin.Essentials.Preferences.Set(key, value); + } + + public void Remove(string key) + { + Xamarin.Essentials.Preferences.Remove(key); + } + } +}