diff --git a/src/AliasVault.Client/Auth/Pages/Login.razor b/src/AliasVault.Client/Auth/Pages/Login.razor index b3e1f8fce..241054029 100644 --- a/src/AliasVault.Client/Auth/Pages/Login.razor +++ b/src/AliasVault.Client/Auth/Pages/Login.razor @@ -385,7 +385,7 @@ else await AuthService.StoreRefreshTokenAsync(validateLoginResponse.Token!.RefreshToken); // Store the encryption key in memory. - AuthService.StoreEncryptionKey(PasswordHash); + await AuthService.StoreEncryptionKeyAsync(PasswordHash); await AuthStateProvider.GetAuthenticationStateAsync(); GlobalNotificationService.ClearMessages(); diff --git a/src/AliasVault.Client/Auth/Pages/Register.razor b/src/AliasVault.Client/Auth/Pages/Register.razor index 50941d5cb..b9982d460 100644 --- a/src/AliasVault.Client/Auth/Pages/Register.razor +++ b/src/AliasVault.Client/Auth/Pages/Register.razor @@ -93,7 +93,7 @@ if (tokenObject != null) { // Store the encryption key in memory. - AuthService.StoreEncryptionKey(passwordHash); + await AuthService.StoreEncryptionKeyAsync(passwordHash); // Store the token as a plain string in local storage await AuthService.StoreAccessTokenAsync(tokenObject.Token); diff --git a/src/AliasVault.Client/Auth/Pages/Unlock.razor b/src/AliasVault.Client/Auth/Pages/Unlock.razor index 428dc0491..129da2ca4 100644 --- a/src/AliasVault.Client/Auth/Pages/Unlock.razor +++ b/src/AliasVault.Client/Auth/Pages/Unlock.razor @@ -101,8 +101,18 @@ // 3. Client derives shared session key. byte[] passwordHash = await Encryption.DeriveKeyFromPasswordAsync(UnlockModel.Password, loginResponse!.Salt); + // Check if the password is correct locally by decrypting the test string. + var validPassword = await AuthService.ValidateEncryptionKeyAsync(passwordHash); + + if (!validPassword) + { + ServerValidationErrors.AddError("The password is incorrect. Please try entering your password again, or logout and login again."); + return; + } + // Store the encryption key in memory. - AuthService.StoreEncryptionKey(passwordHash); + await AuthService.StoreEncryptionKeyAsync(passwordHash); + // Redirect to the page the user was trying to access before if set. var localStorageReturnUrl = await LocalStorage.GetItemAsync("returnUrl"); diff --git a/src/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor b/src/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor index 2851f7a1c..87ff5197e 100644 --- a/src/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor +++ b/src/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor @@ -151,7 +151,7 @@ else // Set new currentPasswordHash locally as it is required for the new database encryption call below so // it is encrypted with the new password hash. - AuthService.StoreEncryptionKey(newPasswordHash); + await AuthService.StoreEncryptionKeyAsync(newPasswordHash); var srpPasswordChange = Srp.PasswordChangeAsync(client, newSalt, username, newPasswordHashString); @@ -182,8 +182,8 @@ else // Clear form. PasswordChangeModel = new PasswordChangeModel(); - // Set currentPasswordHash back to original so we're back to the original state. - AuthService.StoreEncryptionKey(backupPasswordHash); + // Set currentPasswordHash back to original, so we're back to the original state. + await AuthService.StoreEncryptionKeyAsync(backupPasswordHash); GlobalLoadingSpinner.Hide(); StateHasChanged(); diff --git a/src/AliasVault.Client/Services/Auth/AuthService.cs b/src/AliasVault.Client/Services/Auth/AuthService.cs index 5845e2c13..9e461fbf1 100644 --- a/src/AliasVault.Client/Services/Auth/AuthService.cs +++ b/src/AliasVault.Client/Services/Auth/AuthService.cs @@ -21,10 +21,18 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting; /// The local storage service. /// IWebAssemblyHostEnvironment instance. /// IConfiguration instance. -public sealed class AuthService(HttpClient httpClient, ILocalStorageService localStorage, IWebAssemblyHostEnvironment environment, IConfiguration configuration) +/// JSInteropService instance. +public sealed class AuthService(HttpClient httpClient, ILocalStorageService localStorage, IWebAssemblyHostEnvironment environment, IConfiguration configuration, JsInteropService jsInteropService) { private const string AccessTokenKey = "token"; private const string RefreshTokenKey = "refreshToken"; + + /// + /// Test string that is stored in local storage in encrypted state. This is used to validate the encryption key + /// locally during future vault unlocks. + /// + private const string EncryptionTestString = "aliasvault-test-string"; + private byte[] _encryptionKey = new byte[32]; /// @@ -139,9 +147,48 @@ public sealed class AuthService(HttpClient httpClient, ILocalStorageService loca /// Stores the encryption key asynchronously in-memory. /// /// SrpArgonEncryption key. - public void StoreEncryptionKey(byte[] newKey) + /// Task. + public async Task StoreEncryptionKeyAsync(byte[] newKey) { _encryptionKey = newKey; + + // When storing a new encryption key, encrypt a test string and save it to local storage. + // This test string can then be used to locally validate the password during future unlocks. + var encryptedTestString = await jsInteropService.SymmetricEncrypt(EncryptionTestString, GetEncryptionKeyAsBase64Async()); + + // Store the encrypted test string in local storage. + await localStorage.SetItemAsStringAsync("encryptionTestString", encryptedTestString); + } + + /// + /// Validate the encryption locally by attempting to decrypt test string stored in local storage. + /// + /// The encryption key to validate. + /// True if encryption key is valid, false if not. + public async Task ValidateEncryptionKeyAsync(byte[] encryptionKey) + { + // Get the encrypted test string from local storage. + var encryptedTestString = await localStorage.GetItemAsStringAsync("encryptionTestString"); + if (encryptedTestString == null) + { + return false; + } + + var base64EncryptionKey = Convert.ToBase64String(encryptionKey); + + // Decrypt the test string using the provided encryption key. + try + { + var decryptedTestString = await jsInteropService.SymmetricDecrypt(encryptedTestString, base64EncryptionKey); + + // If the decrypted test string is not equal to the test string, the encryption key is invalid. + return decryptedTestString == EncryptionTestString; + } + catch + { + // Ignore errors, if decryption fails the encryption key is invalid. + return false; + } } /// diff --git a/src/AliasVault.Client/Services/Database/DbService.cs b/src/AliasVault.Client/Services/Database/DbService.cs index 9422ed090..cf8ef24d1 100644 --- a/src/AliasVault.Client/Services/Database/DbService.cs +++ b/src/AliasVault.Client/Services/Database/DbService.cs @@ -140,6 +140,7 @@ public sealed class DbService : IDisposable // Make sure a public/private RSA encryption key exists before saving the database. await GetOrCreateEncryptionKeyAsync(); + var encryptedBase64String = await GetEncryptedDatabaseBase64String(); // Save to webapi.