//----------------------------------------------------------------------- // // Copyright (c) lanedirt. All rights reserved. // Licensed under the MIT license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- namespace AliasVault.Client.Services; using System.Security.Cryptography; using System.Text.Json; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.JSInterop; /// /// JavaScript interop service for calling JavaScript functions from C#. /// /// IJSRuntime. public sealed class JsInteropService(IJSRuntime jsRuntime) { /// /// Symmetrically encrypts a string using the provided encryption key. /// /// Plain text to encrypt. /// Encryption key to use. /// Encrypted ciphertext. public async Task SymmetricEncrypt(string plaintext, string encryptionKey) { if (string.IsNullOrEmpty(plaintext)) { return plaintext; } return await jsRuntime.InvokeAsync("cryptoInterop.encrypt", plaintext, encryptionKey); } /// /// Symmetrically decrypts a string using the provided encryption key. /// /// Cipher text to decrypt. /// Encryption key to use. /// Encrypted ciphertext. public async Task SymmetricDecrypt(string ciphertext, string encryptionKey) { if (string.IsNullOrEmpty(ciphertext)) { return ciphertext; } return await jsRuntime.InvokeAsync("cryptoInterop.decrypt", ciphertext, encryptionKey); } /// /// Downloads a file from a stream. /// /// Filename of the download. /// Blob byte array to download. /// Task. public async Task DownloadFileFromStream(string filename, byte[] blob) => await jsRuntime.InvokeVoidAsync("downloadFileFromStream", filename, blob); /// /// Focus an element by its ID. /// /// The element ID to focus. /// Task. public async Task FocusElementById(string elementId) => await jsRuntime.InvokeVoidAsync("focusElement", elementId); /// /// Blur (defocus) an element by its ID. /// /// The element ID to focus. /// Task. public async Task BlurElementById(string elementId) => await jsRuntime.InvokeVoidAsync("blurElement", elementId); /// /// Copy a string to the browser's clipboard. /// /// Value to copy to clipboard. /// Task. public async Task CopyToClipboard(string value) => await jsRuntime.InvokeVoidAsync("navigator.clipboard.writeText", value); /// /// Initializes the top menu. /// /// Task. public async Task InitTopMenu() => await jsRuntime.InvokeVoidAsync("window.initTopMenu"); /// /// Registers a click outside handler for the top menu. /// /// Component type. /// DotNetObjectReference. /// Task. public async Task TopMenuClickOutsideHandler(DotNetObjectReference objRef) where TComponent : class => await jsRuntime.InvokeVoidAsync("window.topMenuClickOutsideHandler", objRef); /// /// Generates a new RSA key pair. /// /// Tuple with public and private key. public async Task<(string PublicKey, string PrivateKey)> GenerateRsaKeyPair() { var result = await jsRuntime.InvokeAsync("rsaInterop.generateRsaKeyPair"); return (result.GetProperty("publicKey").GetString()!, result.GetProperty("privateKey").GetString()!); } /// /// Encrypts a plaintext with a public key. /// /// Plain text to encrypt. /// Public key to use for encryption. /// Encrypted ciphertext. public async Task EncryptWithPublicKey(string plaintext, string publicKey) => await jsRuntime.InvokeAsync("rsaInterop.encryptWithPublicKey", plaintext, publicKey); /// /// Decrypts a ciphertext with a private key. /// /// Ciphertext to decrypt. /// Private key to use for decryption. /// Decrypted 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); return result; } catch (JSException ex) { await Console.Error.WriteLineAsync($"JavaScript decryption error: {ex.Message}"); throw new CryptographicException("Decryption failed", ex); } } /// /// Generates a QR code. /// /// Element ID that contains data-url attribute which to generate QR code for. /// Task. public async Task GenerateQrCode(string elementId) { try { // Invoke the JavaScript function and get the result as a byte array await jsRuntime.InvokeVoidAsync("generateQrCode", "authenticator-uri"); } catch (JSException ex) { await Console.Error.WriteLineAsync($"JavaScript error: {ex.Message}"); } } /// /// Gets the WebAuthn credential derived key used to encrypt or decrypt the persisted vault encryption key. /// /// The credential ID to use. /// The salt to use. /// Derived key as base64 string. /// Thrown when the authenticator does not support the PRF extension. /// Thrown when the PRF key derivation fails. /// Thrown when there's an error getting the WebAuthn credential or when decryption fails. public async Task GetWebAuthnCredentialDerivedKey(string credentialId, string salt) { try { var result = await jsRuntime.InvokeAsync("getWebAuthnCredentialAndDeriveKey", credentialId, salt); if (result.Error is null) { return result.DerivedKey ?? throw new CryptographicException("Derived key is null"); } throw result.Error switch { "PRF_NOT_SUPPORTED" => new NotSupportedException("Authenticator does not support the PRF extension."), "PRF_DERIVATION_FAILED" => new InvalidOperationException("Failed to derive key using PRF extension."), "WEBAUTHN_CREATE_ERROR" => new CryptographicException($"Failed to create WebAuthn credential: {result.Message}"), _ => new CryptographicException($"Unknown error occurred: {result.Error ?? "No error specified"}"), }; } catch (JSException ex) { await Console.Error.WriteLineAsync($"JavaScript error: {ex.Message}"); throw new CryptographicException("Decryption failed", ex); } } /// /// Gets or creates the WebAuthn credential derived key used to encrypt or decrypt the persisted vault encryption key. /// /// The username for the credential. /// A tuple containing the credential ID, salt and the derived key. /// Thrown when decryption fails due to a JavaScript error. /// Thrown when the authenticator does not support the PRF extension. /// Thrown when the PRF key derivation fails. public async Task<(string CredentialId, string Salt, string DerivedKey)> CreateWebAuthnCredentialDerivedKey(string username) { try { var result = await jsRuntime.InvokeAsync("createWebAuthnCredentialAndDeriveKey", "AliasVault | " + username); if (result.CredentialId is not null && result.Salt is not null && result.DerivedKey is not null) { return (result.CredentialId, result.Salt, result.DerivedKey); } throw result.Error switch { "PRF_NOT_SUPPORTED" => new NotSupportedException("Authenticator does not support the PRF extension."), "PRF_DERIVATION_FAILED" => new InvalidOperationException("Failed to derive key using PRF extension."), "WEBAUTHN_CREATE_ERROR" => new CryptographicException($"Failed to create WebAuthn credential: {result.Message}"), _ => new CryptographicException($"Unknown error occurred: {result.Error ?? "No error specified"}"), }; } catch (JSException ex) { await Console.Error.WriteLineAsync($"JavaScript error: {ex.Message}"); throw new CryptographicException("Decryption failed", ex); } } /// /// Scrolls to the top of the page. /// /// Task. public async Task ScrollToTop() { await jsRuntime.InvokeVoidAsync("window.scrollTo", 0, 0); } /// /// Registers a visibility callback which is invoked when the visibility of component changes in client. /// /// Component type. /// DotNetObjectReference. /// Task. public async Task RegisterVisibilityCallback(DotNetObjectReference objRef) where TComponent : class => await jsRuntime.InvokeVoidAsync("window.registerVisibilityCallback", objRef); /// /// Unregisters the visibility callback to prevent memory leaks. /// /// Component type. /// DotNetObjectReference. /// Task. public async Task UnregisterVisibilityCallback(DotNetObjectReference objRef) where TComponent : class => await jsRuntime.InvokeVoidAsync("window.unregisterVisibilityCallback", objRef); /// /// Symmetrically decrypts a byte array using the provided encryption key. /// /// Cipher bytes to decrypt. /// Encryption key to use. /// Decrypted bytes. public async Task SymmetricDecryptBytes(byte[] cipherBytes, string encryptionKey) { if (cipherBytes == null || cipherBytes.Length == 0) { return []; } var base64Ciphertext = Convert.ToBase64String(cipherBytes); return await jsRuntime.InvokeAsync("cryptoInterop.decryptBytes", base64Ciphertext, encryptionKey); } /// /// Represents the result of a WebAuthn get credential operation. /// private sealed class WebAuthnGetCredentialResult { /// /// Gets the derived key. /// public string? DerivedKey { get; init; } /// /// Gets the optional error message. /// public string? Error { get; init; } /// /// Gets the optional additional error details. /// public string? Message { get; init; } } /// /// Represents the result of a WebAuthn credential operation. /// private sealed class WebAuthnCreateCredentialResult { /// /// Gets the credential ID as a base64 string. /// public string? CredentialId { get; init; } /// /// Gets the salt as a base64 string. /// public string? Salt { get; init; } /// /// Gets the derived key as a base64 string. /// public string? DerivedKey { get; init; } /// /// Gets the optional error message. /// public string? Error { get; init; } /// /// Gets the optional additional error details. /// public string? Message { get; init; } } }