//----------------------------------------------------------------------- // // Copyright (c) lanedirt. All rights reserved. // Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- namespace AliasVault.Client.Services.JsInterop; using System.Security.Cryptography; using System.Text.Json; using AliasVault.Client.Services.JsInterop.Models; using Microsoft.JSInterop; /// /// JavaScript interop service for calling JavaScript functions from C#. /// /// IJSRuntime. public sealed class JsInteropService(IJSRuntime jsRuntime) { private const string _DEFAULT_VERSION = "0.0.0"; private const string _VAULT_SQL_GENERATOR_FACTORY_FUNCTION = "CreateVaultSqlGenerator"; private IJSObjectReference? _identityGeneratorModule; private IJSObjectReference? _passwordGeneratorModule; private IJSObjectReference? _vaultSqlInteropModule; /// /// Initialize the identity generator module. /// /// A task representing the asynchronous operation. public async Task InitializeAsync() { _identityGeneratorModule = await jsRuntime.InvokeAsync("import", "./js/dist/shared/identity-generator/index.mjs"); if (_identityGeneratorModule == null) { throw new InvalidOperationException("Failed to initialize identity generator module"); } _passwordGeneratorModule = await jsRuntime.InvokeAsync("import", "./js/dist/shared/password-generator/index.mjs"); if (_passwordGeneratorModule == null) { throw new InvalidOperationException("Failed to initialize password generator module"); } _vaultSqlInteropModule = await jsRuntime.InvokeAsync("import", "./js/dist/shared/vault-sql/index.mjs"); if (_vaultSqlInteropModule == null) { throw new InvalidOperationException("Failed to initialize vault SQL generator module"); } } /// /// 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); } /// /// Generates a random identity using the specified language. /// /// The language to use for generating the identity (e.g. "en", "nl"). /// The gender preference for generating the identity (optional, defaults to random). /// An AliasVaultIdentity containing the generated identity information. public async Task GenerateRandomIdentityAsync(string language, string? gender = null) { try { if (_identityGeneratorModule == null) { await InitializeAsync(); } var generatorInstance = await _identityGeneratorModule!.InvokeAsync("CreateIdentityGenerator", language); var result = string.IsNullOrEmpty(gender) || gender == "random" ? await generatorInstance.InvokeAsync("generateRandomIdentity") : await generatorInstance.InvokeAsync("generateRandomIdentity", gender); return result; } catch (JSException ex) { await Console.Error.WriteLineAsync($"JavaScript error generating identity: {ex.Message}"); throw new InvalidOperationException("Failed to generate random identity", ex); } } /// /// Generates a random username. /// /// The identity to use for generating the username. /// The generated username. public async Task GenerateRandomUsernameAsync(AliasVaultIdentity identity) { try { if (_identityGeneratorModule == null) { await InitializeAsync(); } Console.WriteLine($"Generating username for identity: {identity.FirstName} {identity.LastName} {identity.BirthDate} {identity.Gender} {identity.NickName}"); var generatorInstance = await _identityGeneratorModule!.InvokeAsync("CreateUsernameEmailGenerator"); var result = await generatorInstance.InvokeAsync("generateUsername", identity); return result; } catch (JSException ex) { await Console.Error.WriteLineAsync($"JavaScript error generating username: {ex.Message}"); throw new InvalidOperationException("Failed to generate random username", ex); } } /// /// Generates a random email prefix. /// /// The identity to use for generating the email prefix. /// The generated email prefix. public async Task GenerateRandomEmailPrefixAsync(AliasVaultIdentity identity) { try { if (_identityGeneratorModule == null) { await InitializeAsync(); } var generatorInstance = await _identityGeneratorModule!.InvokeAsync("CreateUsernameEmailGenerator"); var result = await generatorInstance.InvokeAsync("generateEmailPrefix", identity); return result; } catch (JSException ex) { await Console.Error.WriteLineAsync($"JavaScript error generating email prefix: {ex.Message}"); throw new InvalidOperationException("Failed to generate random email prefix", ex); } } /// /// Generates a random password using the specified settings. /// /// The password settings to use. /// The generated password. public async Task GenerateRandomPasswordAsync(PasswordSettings settings) { try { if (_passwordGeneratorModule == null) { await InitializeAsync(); } var generatorInstance = await _passwordGeneratorModule!.InvokeAsync("CreatePasswordGenerator", settings); var result = await generatorInstance.InvokeAsync("generateRandomPassword"); return result; } catch (JSException ex) { await Console.Error.WriteLineAsync($"JavaScript error generating password: {ex.Message}"); throw new InvalidOperationException("Failed to generate random password", ex); } } /// /// Gets SQL commands to create a new vault with the latest schema. /// /// SQL generation result with commands to execute. public async Task GetCreateVaultSqlAsync() { try { if (_vaultSqlInteropModule == null) { await InitializeAsync(); } var vaultGenerator = await _vaultSqlInteropModule!.InvokeAsync(_VAULT_SQL_GENERATOR_FACTORY_FUNCTION); var result = await vaultGenerator.InvokeAsync("getCreateVaultSql"); return new SqlGenerationResult { Success = result.GetProperty("success").GetBoolean(), SqlCommands = [.. result.GetProperty("sqlCommands").EnumerateArray() .Select(x => x.GetString() ?? string.Empty) .Where(x => !string.IsNullOrEmpty(x))], Version = result.GetProperty("version").GetString() ?? _DEFAULT_VERSION, MigrationNumber = result.GetProperty("migrationNumber").GetInt32(), Error = result.TryGetProperty("error", out var errorElement) ? errorElement.GetString() : null, }; } catch (JSException ex) { return new SqlGenerationResult { Success = false, SqlCommands = [], Version = _DEFAULT_VERSION, MigrationNumber = 0, Error = $"JavaScript error: {ex.Message}", }; } } /// /// Gets all available vault versions. /// /// List of vault versions. public async Task> GetAllVaultVersionsAsync() { if (_vaultSqlInteropModule == null) { await InitializeAsync(); } var vaultGenerator = await _vaultSqlInteropModule!.InvokeAsync(_VAULT_SQL_GENERATOR_FACTORY_FUNCTION); var result = await vaultGenerator.InvokeAsync("getAllVersions"); return result.EnumerateArray().Select(x => new SqlVaultVersion { Revision = x.GetProperty("revision").GetInt32(), Version = x.GetProperty("version").GetString() ?? string.Empty, Description = x.GetProperty("description").GetString() ?? string.Empty, ReleaseVersion = x.GetProperty("releaseVersion").GetString() ?? string.Empty, }).ToList(); } /// /// Gets SQL commands to check current vault version. /// /// Array of SQL commands to execute. public async Task GetLatestVaultVersionAsync() { if (_vaultSqlInteropModule == null) { await InitializeAsync(); } var vaultGenerator = await _vaultSqlInteropModule!.InvokeAsync(_VAULT_SQL_GENERATOR_FACTORY_FUNCTION); var result = await vaultGenerator.InvokeAsync("getLatestVersion"); return new SqlVaultVersion { Revision = result.GetProperty("revision").GetInt32(), Version = result.GetProperty("version").GetString() ?? string.Empty, Description = result.GetProperty("description").GetString() ?? string.Empty, ReleaseVersion = result.GetProperty("releaseVersion").GetString() ?? string.Empty, }; } /// /// Gets SQL commands to upgrade vault from current to target migration. /// /// Current migration number. /// Target migration number (optional, defaults to latest). /// SQL generation result with commands to execute. public async Task GetUpgradeVaultSqlAsync(int currentMigrationNumber, int? targetMigrationNumber = null) { try { if (_vaultSqlInteropModule == null) { await InitializeAsync(); } var vaultGenerator = await _vaultSqlInteropModule!.InvokeAsync(_VAULT_SQL_GENERATOR_FACTORY_FUNCTION); var result = targetMigrationNumber.HasValue ? await vaultGenerator.InvokeAsync("getUpgradeVaultSql", currentMigrationNumber, targetMigrationNumber.Value) : await vaultGenerator.InvokeAsync("getUpgradeToLatestSql", currentMigrationNumber); return new SqlGenerationResult { Success = result.GetProperty("success").GetBoolean(), SqlCommands = [.. result.GetProperty("sqlCommands").EnumerateArray() .Select(x => x.GetString() ?? string.Empty) .Where(x => !string.IsNullOrEmpty(x))], Version = result.GetProperty("version").GetString() ?? _DEFAULT_VERSION, MigrationNumber = result.GetProperty("migrationNumber").GetInt32(), Error = result.TryGetProperty("error", out var errorElement) ? errorElement.GetString() : null, }; } catch (JSException ex) { return new SqlGenerationResult { Success = false, SqlCommands = [], Version = _DEFAULT_VERSION, MigrationNumber = 0, Error = $"JavaScript error: {ex.Message}", }; } } /// /// Validates vault structure from table names. /// /// List of table names found in database. /// True if vault structure is valid. public async Task ValidateVaultStructureAsync(string[] tableNames) { try { if (_vaultSqlInteropModule == null) { await InitializeAsync(); } var vaultGenerator = await _vaultSqlInteropModule!.InvokeAsync(_VAULT_SQL_GENERATOR_FACTORY_FUNCTION); return await vaultGenerator.InvokeAsync("validateVaultStructure", tableNames); } catch (JSException) { return false; } } /// /// 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; } } }