From 79589b07fceaab8a55caba77b75645274b30b1c5 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Mon, 14 Jun 2021 14:39:34 -0700 Subject: [PATCH] Use 2 iterations for local password hashing (#1423) * Add HashPurpose parameter to HashPasswordAsync * Use 2 iterations for local password hashing * Force logout if user has old keyHash stored * Revert "Force logout if user has old keyHash stored" This reverts commit 497d4928fa7e1f2814b455d90a7a788e15d8ec98. * Add backwards compatability with existing keyHash --- src/App/Pages/Accounts/LockPageViewModel.cs | 39 +++++++++---------- .../Accounts/SetPasswordPageViewModel.cs | 5 ++- .../Settings/ExportVaultPageViewModel.cs | 5 +-- .../Services/MobilePasswordRepromptService.cs | 10 +---- src/Core/Abstractions/ICryptoService.cs | 3 +- src/Core/Enums/HashPurpose.cs | 8 ++++ src/Core/Services/AuthService.cs | 18 +++++---- src/Core/Services/CryptoService.cs | 27 ++++++++++++- .../Controllers/LockPasswordViewController.cs | 9 +++-- 9 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 src/Core/Enums/HashPurpose.cs diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index fcd148858..69225e471 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -241,32 +241,31 @@ namespace Bit.App.Pages else { var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdf, kdfIterations); - var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key); + var storedKeyHash = await _cryptoService.GetKeyHashAsync(); var passwordValid = false; - if (keyHash != null) + + if (storedKeyHash != null) { - var storedKeyHash = await _cryptoService.GetKeyHashAsync(); - if (storedKeyHash != null) + passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, key); + } + else + { + await _deviceActionService.ShowLoadingAsync(AppResources.Loading); + var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization); + var request = new PasswordVerificationRequest(); + request.MasterPasswordHash = keyHash; + try { - passwordValid = storedKeyHash == keyHash; + await _apiService.PostAccountVerifyPasswordAsync(request); + passwordValid = true; + var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization); + await _cryptoService.SetKeyHashAsync(localKeyHash); } - else + catch (Exception e) { - await _deviceActionService.ShowLoadingAsync(AppResources.Loading); - var request = new PasswordVerificationRequest(); - request.MasterPasswordHash = keyHash; - try - { - await _apiService.PostAccountVerifyPasswordAsync(request); - passwordValid = true; - await _cryptoService.SetKeyHashAsync(keyHash); - } - catch (Exception e) - { - System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace); - } - await _deviceActionService.HideLoadingAsync(); + System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace); } + await _deviceActionService.HideLoadingAsync(); } if (passwordValid) { diff --git a/src/App/Pages/Accounts/SetPasswordPageViewModel.cs b/src/App/Pages/Accounts/SetPasswordPageViewModel.cs index e523e7354..78f850017 100644 --- a/src/App/Pages/Accounts/SetPasswordPageViewModel.cs +++ b/src/App/Pages/Accounts/SetPasswordPageViewModel.cs @@ -138,7 +138,8 @@ namespace Bit.App.Pages var kdfIterations = 100000; var email = await _userService.GetEmailAsync(); var key = await _cryptoService.MakeKeyAsync(MasterPassword, email, kdf, kdfIterations); - var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key); + var masterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization); + var localMasterPasswordHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization); Tuple encKey; var existingEncKey = await _cryptoService.GetEncKeyAsync(); @@ -174,7 +175,7 @@ namespace Bit.App.Pages await _userService.SetInformationAsync(await _userService.GetUserIdAsync(), await _userService.GetEmailAsync(), kdf, kdfIterations); await _cryptoService.SetKeyAsync(key); - await _cryptoService.SetKeyHashAsync(masterPasswordHash); + await _cryptoService.SetKeyHashAsync(localMasterPasswordHash); await _cryptoService.SetEncKeyAsync(encKey.Item2.EncryptedString); await _cryptoService.SetEncPrivateKeyAsync(keys.Item2.EncryptedString); await _deviceActionService.HideLoadingAsync(); diff --git a/src/App/Pages/Settings/ExportVaultPageViewModel.cs b/src/App/Pages/Settings/ExportVaultPageViewModel.cs index cafda15da..918a97df9 100644 --- a/src/App/Pages/Settings/ExportVaultPageViewModel.cs +++ b/src/App/Pages/Settings/ExportVaultPageViewModel.cs @@ -103,11 +103,10 @@ namespace Bit.App.Pages return; } - var keyHash = await _cryptoService.HashPasswordAsync(_masterPassword, null); MasterPassword = string.Empty; - var storedKeyHash = await _cryptoService.GetKeyHashAsync(); - if (storedKeyHash == null || keyHash == null || storedKeyHash != keyHash) + var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(_masterPassword, null); + if (passwordValid) { await _platformUtilsService.ShowDialogAsync(_i18nService.T("InvalidMasterPassword")); return; diff --git a/src/App/Services/MobilePasswordRepromptService.cs b/src/App/Services/MobilePasswordRepromptService.cs index f6cab89e7..e12edf94d 100644 --- a/src/App/Services/MobilePasswordRepromptService.cs +++ b/src/App/Services/MobilePasswordRepromptService.cs @@ -29,15 +29,7 @@ namespace Bit.App.Services return false; }; - var keyHash = await _cryptoService.HashPasswordAsync(password, null); - var storedKeyHash = await _cryptoService.GetKeyHashAsync(); - - if (storedKeyHash == null || keyHash == null || storedKeyHash != keyHash) - { - return false; - } - - return true; + return await _cryptoService.CompareAndUpdateKeyHashAsync(password, null); }; return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, validator); diff --git a/src/Core/Abstractions/ICryptoService.cs b/src/Core/Abstractions/ICryptoService.cs index f1dc19692..fc9376e63 100644 --- a/src/Core/Abstractions/ICryptoService.cs +++ b/src/Core/Abstractions/ICryptoService.cs @@ -30,8 +30,9 @@ namespace Bit.Core.Abstractions Task> GetOrgKeysAsync(); Task GetPrivateKeyAsync(); Task GetPublicKeyAsync(); + Task CompareAndUpdateKeyHashAsync(string masterPassword, SymmetricCryptoKey key); Task HasEncKeyAsync(); - Task HashPasswordAsync(string password, SymmetricCryptoKey key); + Task HashPasswordAsync(string password, SymmetricCryptoKey key, HashPurpose hashPurpose = HashPurpose.ServerAuthorization); Task HasKeyAsync(); Task> MakeEncKeyAsync(SymmetricCryptoKey key); Task MakeKeyAsync(string password, string salt, KdfType? kdf, int? kdfIterations); diff --git a/src/Core/Enums/HashPurpose.cs b/src/Core/Enums/HashPurpose.cs new file mode 100644 index 000000000..c37779d14 --- /dev/null +++ b/src/Core/Enums/HashPurpose.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum HashPurpose : byte + { + ServerAuthorization = 1, + LocalAuthorization = 2, + } +} diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index 0cf15f90f..7a34110ed 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -93,6 +93,7 @@ namespace Bit.Core.Services public string Email { get; set; } public string MasterPasswordHash { get; set; } + public string LocalMasterPasswordHash { get; set; } public string Code { get; set; } public string CodeVerifier { get; set; } public string SsoRedirectUrl { get; set; } @@ -123,7 +124,8 @@ namespace Bit.Core.Services SelectedTwoFactorProviderType = null; var key = await MakePreloginKeyAsync(masterPassword, email); var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key); - return await LogInHelperAsync(email, hashedPassword, null, null, null, key, null, null, null); + var localHashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization); + return await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, null, null, null); } public async Task LogInSsoAsync(string code, string codeVerifier, string redirectUrl) @@ -135,7 +137,7 @@ namespace Bit.Core.Services public Task LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null) { - return LogInHelperAsync(Email, MasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key, + return LogInHelperAsync(Email, MasterPasswordHash, LocalMasterPasswordHash, Code, CodeVerifier, SsoRedirectUrl, _key, twoFactorProvider, twoFactorToken, remember); } @@ -145,7 +147,8 @@ namespace Bit.Core.Services SelectedTwoFactorProviderType = null; var key = await MakePreloginKeyAsync(masterPassword, email); var hashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key); - return await LogInHelperAsync(email, hashedPassword, null, null, null, key, twoFactorProvider, + var localHashedPassword = await _cryptoService.HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization); + return await LogInHelperAsync(email, hashedPassword, localHashedPassword, null, null, null, key, twoFactorProvider, twoFactorToken, remember); } @@ -153,7 +156,7 @@ namespace Bit.Core.Services TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null) { SelectedTwoFactorProviderType = null; - return await LogInHelperAsync(null, null, code, codeVerifier, redirectUrl, null, twoFactorProvider, + return await LogInHelperAsync(null, null, null, code, codeVerifier, redirectUrl, null, twoFactorProvider, twoFactorToken, remember); } @@ -266,8 +269,8 @@ namespace Bit.Core.Services return await _cryptoService.MakeKeyAsync(masterPassword, email, kdf, kdfIterations); } - private async Task LogInHelperAsync(string email, string hashedPassword, string code, - string codeVerifier, string redirectUrl, SymmetricCryptoKey key, + private async Task LogInHelperAsync(string email, string hashedPassword, string localHashedPassword, + string code, string codeVerifier, string redirectUrl, SymmetricCryptoKey key, TwoFactorProviderType? twoFactorProvider = null, string twoFactorToken = null, bool? remember = null) { var storedTwoFactorToken = await _tokenService.GetTwoFactorTokenAsync(email); @@ -318,6 +321,7 @@ namespace Bit.Core.Services var twoFactorResponse = response.Item2; Email = email; MasterPasswordHash = hashedPassword; + LocalMasterPasswordHash = localHashedPassword; Code = code; CodeVerifier = codeVerifier; SsoRedirectUrl = redirectUrl; @@ -339,7 +343,7 @@ namespace Bit.Core.Services if (_setCryptoKeys) { await _cryptoService.SetKeyAsync(key); - await _cryptoService.SetKeyHashAsync(hashedPassword); + await _cryptoService.SetKeyHashAsync(localHashedPassword); await _cryptoService.SetEncKeyAsync(tokenResponse.Key); // User doesn't have a key pair yet (old account), let's generate one for them. diff --git a/src/Core/Services/CryptoService.cs b/src/Core/Services/CryptoService.cs index 3c21925cc..375f6d467 100644 --- a/src/Core/Services/CryptoService.cs +++ b/src/Core/Services/CryptoService.cs @@ -281,6 +281,28 @@ namespace Bit.Core.Services return orgKeys[orgId]; } + public async Task CompareAndUpdateKeyHashAsync(string masterPassword, SymmetricCryptoKey key) + { + var storedKeyHash = await GetKeyHashAsync(); + if (masterPassword != null && storedKeyHash != null) + { + var localKeyHash = await HashPasswordAsync(masterPassword, key, HashPurpose.LocalAuthorization); + if (localKeyHash != null && storedKeyHash == localKeyHash) + { + return true; + } + + var serverKeyHash = await HashPasswordAsync(masterPassword, key, HashPurpose.ServerAuthorization); + if (serverKeyHash != null & storedKeyHash == serverKeyHash) + { + await SetKeyHashAsync(localKeyHash); + return true; + } + } + + return false; + } + public async Task HasKeyAsync() { var key = await GetKeyAsync(); @@ -433,7 +455,7 @@ namespace Bit.Core.Services return new SymmetricCryptoKey(sendKey); } - public async Task HashPasswordAsync(string password, SymmetricCryptoKey key) + public async Task HashPasswordAsync(string password, SymmetricCryptoKey key, HashPurpose hashPurpose = HashPurpose.ServerAuthorization) { if (key == null) { @@ -443,7 +465,8 @@ namespace Bit.Core.Services { throw new Exception("Invalid parameters."); } - var hash = await _cryptoFunctionService.Pbkdf2Async(key.Key, password, CryptoHashAlgorithm.Sha256, 1); + var iterations = hashPurpose == HashPurpose.LocalAuthorization ? 2 : 1; + var hash = await _cryptoFunctionService.Pbkdf2Async(key.Key, password, CryptoHashAlgorithm.Sha256, iterations); return Convert.ToBase64String(hash); } diff --git a/src/iOS.Core/Controllers/LockPasswordViewController.cs b/src/iOS.Core/Controllers/LockPasswordViewController.cs index edb285e83..6d3bc6049 100644 --- a/src/iOS.Core/Controllers/LockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/LockPasswordViewController.cs @@ -190,19 +190,20 @@ namespace Bit.iOS.Core.Controllers else { var key2 = await _cryptoService.MakeKeyAsync(inputtedValue, email, kdf, kdfIterations); - var keyHash = await _cryptoService.HashPasswordAsync(inputtedValue, key2); + var storedKeyHash = await _cryptoService.GetKeyHashAsync(); if (storedKeyHash == null) { var oldKey = await _secureStorageService.GetAsync("oldKey"); if (key2.KeyB64 == oldKey) { + var localKeyHash = await _cryptoService.HashPasswordAsync(inputtedValue, key2, HashPurpose.LocalAuthorization); await _secureStorageService.RemoveAsync("oldKey"); - await _cryptoService.SetKeyHashAsync(keyHash); - storedKeyHash = keyHash; + await _cryptoService.SetKeyHashAsync(localKeyHash); } } - if (storedKeyHash != null && keyHash != null && storedKeyHash == keyHash) + var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(inputtedValue, key2); + if (passwordValid) { if (_pinSet.Item1) {