diff --git a/apps/server/AliasVault.Client/Services/EmailService.cs b/apps/server/AliasVault.Client/Services/EmailService.cs index b50d5d898..2d66432ee 100644 --- a/apps/server/AliasVault.Client/Services/EmailService.cs +++ b/apps/server/AliasVault.Client/Services/EmailService.cs @@ -80,12 +80,12 @@ public sealed class EmailService(DbService dbService, JsInteropService jsInterop try { - var decryptedSymmetricKey = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey); - email.Subject = await jsInteropService.SymmetricDecrypt(email.Subject, Convert.ToBase64String(decryptedSymmetricKey)); - email.FromDisplay = await jsInteropService.SymmetricDecrypt(email.FromDisplay, Convert.ToBase64String(decryptedSymmetricKey)); - email.FromLocal = await jsInteropService.SymmetricDecrypt(email.FromLocal, Convert.ToBase64String(decryptedSymmetricKey)); - email.FromDomain = await jsInteropService.SymmetricDecrypt(email.FromDomain, Convert.ToBase64String(decryptedSymmetricKey)); - email.MessagePreview = await jsInteropService.SymmetricDecrypt(email.MessagePreview, Convert.ToBase64String(decryptedSymmetricKey)); + var decryptedSymmetricKeyBase64 = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey); + email.Subject = await jsInteropService.SymmetricDecrypt(email.Subject, decryptedSymmetricKeyBase64); + email.FromDisplay = await jsInteropService.SymmetricDecrypt(email.FromDisplay, decryptedSymmetricKeyBase64); + email.FromLocal = await jsInteropService.SymmetricDecrypt(email.FromLocal, decryptedSymmetricKeyBase64); + email.FromDomain = await jsInteropService.SymmetricDecrypt(email.FromDomain, decryptedSymmetricKeyBase64); + email.MessagePreview = await jsInteropService.SymmetricDecrypt(email.MessagePreview, decryptedSymmetricKeyBase64); } catch (Exception ex) { @@ -110,8 +110,8 @@ public sealed class EmailService(DbService dbService, JsInteropService jsInterop try { - var decryptedSymmetricKey = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey); - var decryptedBase64 = await jsInteropService.SymmetricDecryptBytes(encryptedBytes, Convert.ToBase64String(decryptedSymmetricKey)); + var decryptedSymmetricKeyBase64 = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey); + var decryptedBase64 = await jsInteropService.SymmetricDecryptBytes(encryptedBytes, decryptedSymmetricKeyBase64); return decryptedBase64; } catch (Exception ex) @@ -133,21 +133,21 @@ public sealed class EmailService(DbService dbService, JsInteropService jsInterop try { - var decryptedSymmetricKey = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey); - email.Subject = await jsInteropService.SymmetricDecrypt(email.Subject, Convert.ToBase64String(decryptedSymmetricKey)); + var decryptedSymmetricKeyBase64 = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey); + email.Subject = await jsInteropService.SymmetricDecrypt(email.Subject, decryptedSymmetricKeyBase64); if (email.MessageHtml is not null) { - email.MessageHtml = await jsInteropService.SymmetricDecrypt(email.MessageHtml, Convert.ToBase64String(decryptedSymmetricKey)); + email.MessageHtml = await jsInteropService.SymmetricDecrypt(email.MessageHtml, decryptedSymmetricKeyBase64); } if (email.MessagePlain is not null) { - email.MessagePlain = await jsInteropService.SymmetricDecrypt(email.MessagePlain, Convert.ToBase64String(decryptedSymmetricKey)); + email.MessagePlain = await jsInteropService.SymmetricDecrypt(email.MessagePlain, decryptedSymmetricKeyBase64); } - email.FromDisplay = await jsInteropService.SymmetricDecrypt(email.FromDisplay, Convert.ToBase64String(decryptedSymmetricKey)); - email.FromLocal = await jsInteropService.SymmetricDecrypt(email.FromLocal, Convert.ToBase64String(decryptedSymmetricKey)); - email.FromDomain = await jsInteropService.SymmetricDecrypt(email.FromDomain, Convert.ToBase64String(decryptedSymmetricKey)); + email.FromDisplay = await jsInteropService.SymmetricDecrypt(email.FromDisplay, decryptedSymmetricKeyBase64); + email.FromLocal = await jsInteropService.SymmetricDecrypt(email.FromLocal, decryptedSymmetricKeyBase64); + email.FromDomain = await jsInteropService.SymmetricDecrypt(email.FromDomain, decryptedSymmetricKeyBase64); } catch (Exception ex) { diff --git a/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs b/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs index 0f12323f3..4fc60280b 100644 --- a/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs +++ b/apps/server/AliasVault.Client/Services/JsInterop/JsInteropService.cs @@ -199,13 +199,13 @@ public sealed class JsInteropService(IJSRuntime jsRuntime) /// /// Ciphertext to decrypt. /// Private key to use for decryption. - /// Decrypted string. - public async Task DecryptWithPrivateKey(string base64Ciphertext, string privateKey) + /// Decrypted bytes as base64 string. + public async Task DecryptWithPrivateKey(string base64Ciphertext, string privateKey) { try { - // Invoke the JavaScript function and get the result as a byte array - byte[] result = await jsRuntime.InvokeAsync("rsaInterop.decryptWithPrivateKey", base64Ciphertext, privateKey); + // Invoke the JavaScript function and get the result as a base64 string + string result = await jsRuntime.InvokeAsync("rsaInterop.decryptWithPrivateKey", base64Ciphertext, privateKey); return result; } catch (JSException ex) diff --git a/apps/server/AliasVault.Client/wwwroot/js/crypto.js b/apps/server/AliasVault.Client/wwwroot/js/crypto.js index 7610ba8af..ecdd21190 100644 --- a/apps/server/AliasVault.Client/wwwroot/js/crypto.js +++ b/apps/server/AliasVault.Client/wwwroot/js/crypto.js @@ -28,7 +28,7 @@ function checkCryptoAvailable() { window.cryptoInterop = { encrypt: async function (plaintext, base64Key) { checkCryptoAvailable(); - + const key = await window.crypto.subtle.importKey( "raw", Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)), @@ -62,7 +62,7 @@ window.cryptoInterop = { }, decrypt: async function (base64Ciphertext, base64Key) { checkCryptoAvailable(); - + const key = await window.crypto.subtle.importKey( "raw", Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)), @@ -89,7 +89,7 @@ window.cryptoInterop = { }, decryptBytes: async function (base64Ciphertext, base64Key) { checkCryptoAvailable(); - + const key = await window.crypto.subtle.importKey( "raw", Uint8Array.from(atob(base64Key), c => c.charCodeAt(0)), @@ -126,7 +126,7 @@ window.rsaInterop = { */ generateRsaKeyPair : async function() { checkCryptoAvailable(); - + const keyPair = await window.crypto.subtle.generateKey( { name: "RSA-OAEP", @@ -154,7 +154,7 @@ window.rsaInterop = { */ encryptWithPublicKey : async function(plaintext, publicKey) { checkCryptoAvailable(); - + const publicKeyObj = await window.crypto.subtle.importKey( "jwk", JSON.parse(publicKey), @@ -181,11 +181,11 @@ window.rsaInterop = { * Decrypts a ciphertext string using an RSA private key. * @param {string} ciphertext - The base64-encoded ciphertext to decrypt. * @param {string} privateKey - The private key in JWK format. - * @returns {Promise} A promise that resolves to the decrypted data as a Uint8Array. + * @returns {Promise} A promise that resolves to the decrypted data as a base64 string. */ decryptWithPrivateKey: async function(ciphertext, privateKey) { checkCryptoAvailable(); - + try { // Parse the private key let parsedPrivateKey = JSON.parse(privateKey); @@ -215,8 +215,9 @@ window.rsaInterop = { cipherBuffer ); - // Return the decrypted data as a Uint8Array - return new Uint8Array(plaintextBuffer); + // Convert to base64 string instead of returning Uint8Array to avoid Blazor serialization issues, see https://github.com/dotnet/aspnetcore/issues/59837 + const decryptedBytes = new Uint8Array(plaintextBuffer); + return btoa(String.fromCharCode.apply(null, Array.from(decryptedBytes))); } catch (error) { throw new Error(`Failed to decrypt: ${error.message}`); }