Add local encryption key validation to unlock page (#200)

This commit is contained in:
Leendert de Borst
2024-09-02 22:51:36 +02:00
parent 81ec09a2ed
commit 06f09cdbf1
6 changed files with 66 additions and 8 deletions

View File

@@ -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();

View File

@@ -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);

View File

@@ -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<string>("returnUrl");

View File

@@ -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();

View File

@@ -21,10 +21,18 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
/// <param name="localStorage">The local storage service.</param>
/// <param name="environment">IWebAssemblyHostEnvironment instance.</param>
/// <param name="configuration">IConfiguration instance.</param>
public sealed class AuthService(HttpClient httpClient, ILocalStorageService localStorage, IWebAssemblyHostEnvironment environment, IConfiguration configuration)
/// <param name="jsInteropService">JSInteropService instance.</param>
public sealed class AuthService(HttpClient httpClient, ILocalStorageService localStorage, IWebAssemblyHostEnvironment environment, IConfiguration configuration, JsInteropService jsInteropService)
{
private const string AccessTokenKey = "token";
private const string RefreshTokenKey = "refreshToken";
/// <summary>
/// Test string that is stored in local storage in encrypted state. This is used to validate the encryption key
/// locally during future vault unlocks.
/// </summary>
private const string EncryptionTestString = "aliasvault-test-string";
private byte[] _encryptionKey = new byte[32];
/// <summary>
@@ -139,9 +147,48 @@ public sealed class AuthService(HttpClient httpClient, ILocalStorageService loca
/// Stores the encryption key asynchronously in-memory.
/// </summary>
/// <param name="newKey">SrpArgonEncryption key.</param>
public void StoreEncryptionKey(byte[] newKey)
/// <returns>Task.</returns>
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);
}
/// <summary>
/// Validate the encryption locally by attempting to decrypt test string stored in local storage.
/// </summary>
/// <param name="encryptionKey">The encryption key to validate.</param>
/// <returns>True if encryption key is valid, false if not.</returns>
public async Task<bool> 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;
}
}
/// <summary>

View File

@@ -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.