Add shared libraries to AliasVault.Client (#896)

This commit is contained in:
Leendert de Borst
2025-06-04 21:58:45 +02:00
committed by Leendert de Borst
parent a7ffc33d56
commit 260aec34ce
10 changed files with 4266 additions and 9 deletions

View File

@@ -14,15 +14,13 @@ using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using AliasClientDb;
using AliasVault.Generators.Identity.Implementations.Factories;
using AliasVault.Generators.Identity.Models;
using AliasVault.Shared.Models.WebApi.Favicon;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// Service class for credential operations.
/// </summary>
public sealed class CredentialService(HttpClient httpClient, DbService dbService, Config config)
public sealed class CredentialService(HttpClient httpClient, DbService dbService, Config config, JsInteropService jsInteropService)
{
/// <summary>
/// The default service URL used as placeholder in forms. When this value is set, the URL field is considered empty
@@ -73,15 +71,15 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
do
{
// Generate a random identity using the IIdentityGenerator implementation
var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(dbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
// Generate a random identity using the TypeScript library
var identity = await jsInteropService.GenerateRandomIdentityAsync(dbService.Settings.DefaultIdentityLanguage);
// Generate random values for the Identity properties
credential.Username = identity.NickName;
credential.Alias.FirstName = identity.FirstName;
credential.Alias.LastName = identity.LastName;
credential.Alias.NickName = identity.NickName;
credential.Alias.Gender = identity.Gender == Gender.Male ? "Male" : "Female";
credential.Alias.Gender = identity.Gender;
credential.Alias.BirthDate = identity.BirthDate;
// Set the email
@@ -105,9 +103,9 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
}
while (isEmailTaken && attempts < MaxAttempts);
// Generate password
// Generate password using the TypeScript library
var passwordSettings = dbService.Settings.PasswordSettings;
credential.Passwords.First().Value = GenerateRandomPassword(passwordSettings);
credential.Passwords.First().Value = await jsInteropService.GenerateRandomPasswordAsync(passwordSettings);
return credential;
}

View File

@@ -9,7 +9,6 @@ namespace AliasVault.Client.Services;
using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.JSInterop;
/// <summary>
@@ -18,6 +17,19 @@ using Microsoft.JSInterop;
/// <param name="jsRuntime">IJSRuntime.</param>
public sealed class JsInteropService(IJSRuntime jsRuntime)
{
private IJSObjectReference? _identityGeneratorModule;
private IJSObjectReference? _passwordGeneratorModule;
/// <summary>
/// Initialize the identity generator module.
/// </summary>
/// <returns>A task representing the asynchronous operation.</returns>
public async Task InitializeAsync()
{
_identityGeneratorModule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/shared/identity-generator/index.mjs");
_passwordGeneratorModule = await jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/shared/password-generator/index.mjs");
}
/// <summary>
/// Symmetrically encrypts a string using the provided encryption key.
/// </summary>
@@ -275,6 +287,73 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
return await jsRuntime.InvokeAsync<byte[]>("cryptoInterop.decryptBytes", base64Ciphertext, encryptionKey);
}
/// <summary>
/// Generates a random identity using the specified language.
/// </summary>
/// <param name="language">The language to use for generating the identity (e.g. "en", "nl").</param>
/// <returns>A tuple containing the generated identity information.</returns>
public async Task<(string FirstName, string LastName, string NickName, string EmailPrefix, string Gender, DateTime BirthDate)> GenerateRandomIdentityAsync(string language)
{
try
{
if (_identityGeneratorModule == null)
{
await InitializeAsync();
if (_identityGeneratorModule == null)
{
throw new InvalidOperationException("Failed to initialize identity generator module");
}
}
var generatorInstance = await _identityGeneratorModule.InvokeAsync<IJSObjectReference>("createGenerator", language);
var result = await generatorInstance.InvokeAsync<JsonElement>("generateRandomIdentity");
return (
result.GetProperty("firstName").GetString()!,
result.GetProperty("lastName").GetString()!,
result.GetProperty("nickName").GetString()!,
result.GetProperty("emailPrefix").GetString()!,
result.GetProperty("gender").GetString()!,
result.GetProperty("birthDate").GetDateTime());
}
catch (JSException ex)
{
await Console.Error.WriteLineAsync($"JavaScript error generating identity: {ex.Message}");
throw new InvalidOperationException("Failed to generate random identity", ex);
}
}
/// <summary>
/// Generates a random password using the specified settings.
/// </summary>
/// <param name="settings">The password settings to use.</param>
/// <returns>The generated password.</returns>
public async Task<string> GenerateRandomPasswordAsync(PasswordSettings settings)
{
try
{
if (_passwordGeneratorModule == null)
{
await InitializeAsync();
if (_passwordGeneratorModule == null)
{
throw new InvalidOperationException("Failed to initialize password generator module");
}
}
var generatorInstance = await _passwordGeneratorModule.InvokeAsync<IJSObjectReference>("createPasswordGenerator", settings);
var result = await generatorInstance.InvokeAsync<string>("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);
}
}
/// <summary>
/// Represents the result of a WebAuthn get credential operation.
/// </summary>

View File

@@ -0,0 +1,9 @@
# ⚠️ Auto-Generated Files
This folder contains the output of the shared `identity-generator` module from the `/shared` directory in the AliasVault project.
**Do not edit any of these files manually.**
To make changes:
1. Update the source files in `packages/identity-generator/src`
2. Run the `build-and-distribute.sh` script at the root of the project to regenerate the outputs and copy them here.

View File

@@ -0,0 +1,142 @@
declare enum Gender {
Male = "Male",
Female = "Female",
Other = "Other"
}
/**
* Identity.
*/
type Identity = {
firstName: string;
lastName: string;
gender: Gender;
birthDate: Date;
emailPrefix: string;
nickName: string;
};
/**
* Generate a username or email prefix.
*/
declare class UsernameEmailGenerator {
private static readonly MIN_LENGTH;
private static readonly MAX_LENGTH;
private readonly symbols;
/**
* Generate a username based on an identity.
*/
generateUsername(identity: Identity): string;
/**
* Generate an email prefix based on an identity.
*/
generateEmailPrefix(identity: Identity): string;
/**
* Sanitize an email prefix.
*/
private sanitizeEmailPrefix;
/**
* Get a random symbol.
*/
private getRandomSymbol;
/**
* Generate a random string.
*/
private generateRandomString;
/**
* Generate a secure random integer between 0 (inclusive) and max (exclusive)
*/
private getSecureRandom;
}
interface IIdentityGenerator {
generateRandomIdentity(): Identity;
}
/**
* Base identity generator.
*/
declare abstract class BaseIdentityGenerator implements IIdentityGenerator {
protected firstNamesMale: string[];
protected firstNamesFemale: string[];
protected lastNames: string[];
private readonly random;
/**
* Constructor.
*/
constructor();
protected abstract getFirstNamesMaleJson(): string[];
protected abstract getFirstNamesFemaleJson(): string[];
protected abstract getLastNamesJson(): string[];
/**
* Generate a random date of birth.
*/
protected generateRandomDateOfBirth(): Date;
/**
* Generate a random identity.
*/
generateRandomIdentity(): Identity;
}
/**
* Identity generator for English language using English word dictionaries.
*/
declare class IdentityGeneratorEn extends BaseIdentityGenerator {
/**
* Get the male first names.
*/
protected getFirstNamesMaleJson(): string[];
/**
* Get the female first names.
*/
protected getFirstNamesFemaleJson(): string[];
/**
* Get the last names.
*/
protected getLastNamesJson(): string[];
}
/**
* Identity generator for Dutch language using Dutch word dictionaries.
*/
declare class IdentityGeneratorNl extends BaseIdentityGenerator {
/**
* Get the male first names.
*/
protected getFirstNamesMaleJson(): string[];
/**
* Get the female first names.
*/
protected getFirstNamesFemaleJson(): string[];
/**
* Get the last names.
*/
protected getLastNamesJson(): string[];
}
/**
* Helper utilities for identity generation that can be used by multiple client applications.
*/
declare class IdentityHelperUtils {
/**
* Normalize a birth date for display.
*/
static normalizeBirthDateForDisplay(birthDate: string | undefined): string;
/**
* Normalize a birth date for database.
*/
static normalizeBirthDateForDb(input: string | undefined): string;
/**
* Check if a birth date is valid.
*/
static isValidBirthDate(input: string | undefined): boolean;
}
/**
* Creates a new identity generator based on the language.
* @param language - The language to use for generating the identity (e.g. "en", "nl").
* @returns A new identity generator instance.
*/
declare const createGenerator: (language: string) => IIdentityGenerator;
export { BaseIdentityGenerator, Gender, type Identity, IdentityGeneratorEn, IdentityGeneratorNl, IdentityHelperUtils, UsernameEmailGenerator, createGenerator };

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
# ⚠️ Auto-Generated Files
This folder contains the output of the shared `password-generator` module from the `/shared` directory in the AliasVault project.
**Do not edit any of these files manually.**
To make changes:
1. Update the source files in `packages/password-generator/src`
2. Run the `build-and-distribute.sh` script at the root of the project to regenerate the outputs and copy them here.

View File

@@ -0,0 +1,119 @@
/**
* Settings for password generation stored in SQLite database settings table as string.
*/
type PasswordSettings = {
/**
* The length of the password.
*/
Length: number;
/**
* Whether to use lowercase letters.
*/
UseLowercase: boolean;
/**
* Whether to use uppercase letters.
*/
UseUppercase: boolean;
/**
* Whether to use numbers.
*/
UseNumbers: boolean;
/**
* Whether to use special characters.
*/
UseSpecialChars: boolean;
/**
* Whether to use non-ambiguous characters.
*/
UseNonAmbiguousChars: boolean;
};
/**
* Generate a random password.
*/
declare class PasswordGenerator {
private readonly lowercaseChars;
private readonly uppercaseChars;
private readonly numberChars;
private readonly specialChars;
private readonly ambiguousChars;
private length;
private useLowercase;
private useUppercase;
private useNumbers;
private useSpecial;
private useNonAmbiguous;
/**
* Create a new instance of PasswordGenerator.
* @param settings Optional password settings to initialize with.
*/
constructor(settings?: PasswordSettings);
/**
* Apply password settings to this generator.
*/
applySettings(settings: PasswordSettings): this;
/**
* Set the length of the password.
*/
setLength(length: number): this;
/**
* Set if lowercase letters should be used.
*/
useLowercaseLetters(use: boolean): this;
/**
* Set if uppercase letters should be used.
*/
useUppercaseLetters(use: boolean): this;
/**
* Set if numeric characters should be used.
*/
useNumericCharacters(use: boolean): this;
/**
* Set if special characters should be used.
*/
useSpecialCharacters(use: boolean): this;
/**
* Set if only non-ambiguous characters should be used.
*/
useNonAmbiguousCharacters(use: boolean): this;
/**
* Get a random index from the crypto module.
*/
private getUnbiasedRandomIndex;
/**
* Generate a random password.
*/
generateRandomPassword(): string;
/**
* Build character set based on selected options.
*/
private buildCharacterSet;
/**
* Remove ambiguous characters from a character set.
*/
private removeAmbiguousCharacters;
/**
* Generate initial random password.
*/
private generateInitialPassword;
/**
* Ensure the generated password meets all specified requirements.
*/
private ensureRequirements;
/**
* Get a character set with ambiguous characters removed if needed.
*/
private getSafeCharacterSet;
/**
* Add a character from the given set at a random position in the password.
*/
private addCharacterFromSet;
}
/**
* Creates a new password generator.
* @returns A new password generator instance.
*/
declare const createPasswordGenerator: () => PasswordGenerator;
export { PasswordGenerator, type PasswordSettings, createPasswordGenerator };

View File

@@ -0,0 +1,244 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
PasswordGenerator: () => PasswordGenerator,
createPasswordGenerator: () => createPasswordGenerator
});
module.exports = __toCommonJS(index_exports);
// src/utils/PasswordGenerator.ts
var PasswordGenerator = class {
/**
* Create a new instance of PasswordGenerator.
* @param settings Optional password settings to initialize with.
*/
constructor(settings) {
this.lowercaseChars = "abcdefghijklmnopqrstuvwxyz";
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
this.numberChars = "0123456789";
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
this.ambiguousChars = "Il1O0";
this.length = 18;
this.useLowercase = true;
this.useUppercase = true;
this.useNumbers = true;
this.useSpecial = true;
this.useNonAmbiguous = false;
if (settings) {
this.applySettings(settings);
}
}
/**
* Apply password settings to this generator.
*/
applySettings(settings) {
this.length = settings.Length;
this.useLowercase = settings.UseLowercase;
this.useUppercase = settings.UseUppercase;
this.useNumbers = settings.UseNumbers;
this.useSpecial = settings.UseSpecialChars;
this.useNonAmbiguous = settings.UseNonAmbiguousChars;
return this;
}
/**
* Set the length of the password.
*/
setLength(length) {
this.length = length;
return this;
}
/**
* Set if lowercase letters should be used.
*/
useLowercaseLetters(use) {
this.useLowercase = use;
return this;
}
/**
* Set if uppercase letters should be used.
*/
useUppercaseLetters(use) {
this.useUppercase = use;
return this;
}
/**
* Set if numeric characters should be used.
*/
useNumericCharacters(use) {
this.useNumbers = use;
return this;
}
/**
* Set if special characters should be used.
*/
useSpecialCharacters(use) {
this.useSpecial = use;
return this;
}
/**
* Set if only non-ambiguous characters should be used.
*/
useNonAmbiguousCharacters(use) {
this.useNonAmbiguous = use;
return this;
}
/**
* Get a random index from the crypto module.
*/
getUnbiasedRandomIndex(max) {
const limit = Math.floor(2 ** 32 / max) * max;
while (true) {
const array = new Uint32Array(1);
crypto.getRandomValues(array);
const value = array[0];
if (value < limit) {
return value % max;
}
}
}
/**
* Generate a random password.
*/
generateRandomPassword() {
const chars = this.buildCharacterSet();
let password = this.generateInitialPassword(chars);
password = this.ensureRequirements(password);
return password;
}
/**
* Build character set based on selected options.
*/
buildCharacterSet() {
let chars = "";
if (this.useLowercase) {
chars += this.lowercaseChars;
}
if (this.useUppercase) {
chars += this.uppercaseChars;
}
if (this.useNumbers) {
chars += this.numberChars;
}
if (this.useSpecial) {
chars += this.specialChars;
}
if (chars.length === 0) {
chars = this.lowercaseChars;
}
if (this.useNonAmbiguous) {
chars = this.removeAmbiguousCharacters(chars);
}
return chars;
}
/**
* Remove ambiguous characters from a character set.
*/
removeAmbiguousCharacters(chars) {
for (const ambChar of this.ambiguousChars) {
chars = chars.replace(ambChar, "");
}
return chars;
}
/**
* Generate initial random password.
*/
generateInitialPassword(chars) {
let password = "";
for (let i = 0; i < this.length; i++) {
password += chars[this.getUnbiasedRandomIndex(chars.length)];
}
return password;
}
/**
* Ensure the generated password meets all specified requirements.
*/
ensureRequirements(password) {
if (this.useLowercase && !/[a-z]/.exec(password)) {
password = this.addCharacterFromSet(
password,
this.getSafeCharacterSet(this.lowercaseChars, true)
);
}
if (this.useUppercase && !/[A-Z]/.exec(password)) {
password = this.addCharacterFromSet(
password,
this.getSafeCharacterSet(this.uppercaseChars, true)
);
}
if (this.useNumbers && !/\d/.exec(password)) {
password = this.addCharacterFromSet(
password,
this.getSafeCharacterSet(this.numberChars, false)
);
}
if (this.useSpecial && !/[!@#$%^&*()_+\-=[\]{}|;:,.<>?]/.exec(password)) {
password = this.addCharacterFromSet(
password,
this.specialChars
);
}
return password;
}
/**
* Get a character set with ambiguous characters removed if needed.
*/
getSafeCharacterSet(charSet, isAlpha) {
if (!this.useNonAmbiguous) {
return charSet;
}
let safeSet = charSet;
for (const ambChar of this.ambiguousChars) {
if (!isAlpha && !/\d/.test(ambChar)) {
continue;
}
let charToRemove = ambChar;
if (isAlpha) {
if (charSet === this.lowercaseChars) {
charToRemove = ambChar.toLowerCase();
} else {
charToRemove = ambChar.toUpperCase();
}
}
safeSet = safeSet.replace(charToRemove, "");
}
return safeSet;
}
/**
* Add a character from the given set at a random position in the password.
*/
addCharacterFromSet(password, charSet) {
const pos = this.getUnbiasedRandomIndex(this.length);
const char = charSet[this.getUnbiasedRandomIndex(charSet.length)];
return password.substring(0, pos) + char + password.substring(pos + 1);
}
};
// src/factories/PasswordGeneratorFactory.ts
var createPasswordGenerator = () => {
return new PasswordGenerator();
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
PasswordGenerator,
createPasswordGenerator
});
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1,216 @@
// src/utils/PasswordGenerator.ts
var PasswordGenerator = class {
/**
* Create a new instance of PasswordGenerator.
* @param settings Optional password settings to initialize with.
*/
constructor(settings) {
this.lowercaseChars = "abcdefghijklmnopqrstuvwxyz";
this.uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
this.numberChars = "0123456789";
this.specialChars = "!@#$%^&*()_+-=[]{}|;:,.<>?";
this.ambiguousChars = "Il1O0";
this.length = 18;
this.useLowercase = true;
this.useUppercase = true;
this.useNumbers = true;
this.useSpecial = true;
this.useNonAmbiguous = false;
if (settings) {
this.applySettings(settings);
}
}
/**
* Apply password settings to this generator.
*/
applySettings(settings) {
this.length = settings.Length;
this.useLowercase = settings.UseLowercase;
this.useUppercase = settings.UseUppercase;
this.useNumbers = settings.UseNumbers;
this.useSpecial = settings.UseSpecialChars;
this.useNonAmbiguous = settings.UseNonAmbiguousChars;
return this;
}
/**
* Set the length of the password.
*/
setLength(length) {
this.length = length;
return this;
}
/**
* Set if lowercase letters should be used.
*/
useLowercaseLetters(use) {
this.useLowercase = use;
return this;
}
/**
* Set if uppercase letters should be used.
*/
useUppercaseLetters(use) {
this.useUppercase = use;
return this;
}
/**
* Set if numeric characters should be used.
*/
useNumericCharacters(use) {
this.useNumbers = use;
return this;
}
/**
* Set if special characters should be used.
*/
useSpecialCharacters(use) {
this.useSpecial = use;
return this;
}
/**
* Set if only non-ambiguous characters should be used.
*/
useNonAmbiguousCharacters(use) {
this.useNonAmbiguous = use;
return this;
}
/**
* Get a random index from the crypto module.
*/
getUnbiasedRandomIndex(max) {
const limit = Math.floor(2 ** 32 / max) * max;
while (true) {
const array = new Uint32Array(1);
crypto.getRandomValues(array);
const value = array[0];
if (value < limit) {
return value % max;
}
}
}
/**
* Generate a random password.
*/
generateRandomPassword() {
const chars = this.buildCharacterSet();
let password = this.generateInitialPassword(chars);
password = this.ensureRequirements(password);
return password;
}
/**
* Build character set based on selected options.
*/
buildCharacterSet() {
let chars = "";
if (this.useLowercase) {
chars += this.lowercaseChars;
}
if (this.useUppercase) {
chars += this.uppercaseChars;
}
if (this.useNumbers) {
chars += this.numberChars;
}
if (this.useSpecial) {
chars += this.specialChars;
}
if (chars.length === 0) {
chars = this.lowercaseChars;
}
if (this.useNonAmbiguous) {
chars = this.removeAmbiguousCharacters(chars);
}
return chars;
}
/**
* Remove ambiguous characters from a character set.
*/
removeAmbiguousCharacters(chars) {
for (const ambChar of this.ambiguousChars) {
chars = chars.replace(ambChar, "");
}
return chars;
}
/**
* Generate initial random password.
*/
generateInitialPassword(chars) {
let password = "";
for (let i = 0; i < this.length; i++) {
password += chars[this.getUnbiasedRandomIndex(chars.length)];
}
return password;
}
/**
* Ensure the generated password meets all specified requirements.
*/
ensureRequirements(password) {
if (this.useLowercase && !/[a-z]/.exec(password)) {
password = this.addCharacterFromSet(
password,
this.getSafeCharacterSet(this.lowercaseChars, true)
);
}
if (this.useUppercase && !/[A-Z]/.exec(password)) {
password = this.addCharacterFromSet(
password,
this.getSafeCharacterSet(this.uppercaseChars, true)
);
}
if (this.useNumbers && !/\d/.exec(password)) {
password = this.addCharacterFromSet(
password,
this.getSafeCharacterSet(this.numberChars, false)
);
}
if (this.useSpecial && !/[!@#$%^&*()_+\-=[\]{}|;:,.<>?]/.exec(password)) {
password = this.addCharacterFromSet(
password,
this.specialChars
);
}
return password;
}
/**
* Get a character set with ambiguous characters removed if needed.
*/
getSafeCharacterSet(charSet, isAlpha) {
if (!this.useNonAmbiguous) {
return charSet;
}
let safeSet = charSet;
for (const ambChar of this.ambiguousChars) {
if (!isAlpha && !/\d/.test(ambChar)) {
continue;
}
let charToRemove = ambChar;
if (isAlpha) {
if (charSet === this.lowercaseChars) {
charToRemove = ambChar.toLowerCase();
} else {
charToRemove = ambChar.toUpperCase();
}
}
safeSet = safeSet.replace(charToRemove, "");
}
return safeSet;
}
/**
* Add a character from the given set at a random position in the password.
*/
addCharacterFromSet(password, charSet) {
const pos = this.getUnbiasedRandomIndex(this.length);
const char = charSet[this.getUnbiasedRandomIndex(charSet.length)];
return password.substring(0, pos) + char + password.substring(pos + 1);
}
};
// src/factories/PasswordGeneratorFactory.ts
var createPasswordGenerator = () => {
return new PasswordGenerator();
};
export {
PasswordGenerator,
createPasswordGenerator
};
//# sourceMappingURL=index.mjs.map