Add password generator settings awareness to browser extension (#167)

This commit is contained in:
Leendert de Borst
2025-03-18 16:30:41 +01:00
parent dffa651512
commit 79950ab9fc
7 changed files with 160 additions and 6 deletions

View File

@@ -2,7 +2,7 @@ import { browser } from "wxt/browser";
import { defineBackground } from 'wxt/sandbox';
import { onMessage } from "webext-bridge/background";
import { setupContextMenus, handleContextMenuClick } from './background/ContextMenu';
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetPasswordSettings, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
import { handleOpenPopup, handlePopupWithCredential } from './background/PopupMessageHandler';
export default defineBackground({
@@ -12,7 +12,7 @@ export default defineBackground({
main() {
// Set up context menus
setupContextMenus();
browser.contextMenus.onClicked.addListener((info: browser.menus.OnClickData, tab?: browser.tabs.Tab) =>
browser.contextMenus.onClicked.addListener((info: browser.contextMenus.OnClickData, tab?: browser.tabs.Tab) =>
handleContextMenuClick(info, tab)
);
@@ -25,6 +25,7 @@ export default defineBackground({
onMessage('GET_CREDENTIALS', () => handleGetCredentials());
onMessage('CREATE_IDENTITY', ({ data }) => handleCreateIdentity(data));
onMessage('GET_DEFAULT_EMAIL_DOMAIN', () => handleGetDefaultEmailDomain());
onMessage('GET_PASSWORD_SETTINGS', () => handleGetPasswordSettings());
onMessage('GET_DERIVED_KEY', () => handleGetDerivedKey());
onMessage('OPEN_POPUP', () => handleOpenPopup());
onMessage('OPEN_POPUP_WITH_CREDENTIAL', ({ data }) => handlePopupWithCredential(data));

View File

@@ -10,6 +10,7 @@ import { BoolResponse as messageBoolResponse } from '../../utils/types/messaging
import { VaultResponse as messageVaultResponse } from '../../utils/types/messaging/VaultResponse';
import { CredentialsResponse as messageCredentialsResponse } from '../../utils/types/messaging/CredentialsResponse';
import { DefaultEmailDomainResponse as messageDefaultEmailDomainResponse } from '../../utils/types/messaging/DefaultEmailDomainResponse';
import { PasswordSettingsResponse as messagePasswordSettingsResponse } from '../../utils/types/messaging/PasswordSettingsResponse';
/**
* Check if the user is logged in and if the vault is locked.
@@ -258,6 +259,22 @@ export function handleGetDefaultEmailDomain(
})();
}
/**
* Get the password settings.
*/
export async function handleGetPasswordSettings(
) : Promise<messagePasswordSettingsResponse> {
try {
const sqliteClient = await createVaultSqliteClient();
const passwordSettings = sqliteClient.getPasswordSettings();
return { success: true, settings: passwordSettings };
} catch (error) {
console.error('Error getting password settings:', error);
return { success: false, error: 'Failed to get password settings' };
}
}
/**
* Get the derived key for the encrypted vault.
*/

View File

@@ -7,6 +7,7 @@ import { storage } from "wxt/storage";
import { sendMessage } from "webext-bridge/content-script";
import { CredentialsResponse } from '@/utils/types/messaging/CredentialsResponse';
import { CombinedStopWords } from '../../utils/formDetector/FieldPatterns';
import { PasswordSettingsResponse } from '@/utils/types/messaging/PasswordSettingsResponse';
/**
* WeakMap to store event listeners for popup containers
@@ -212,7 +213,11 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
const identityGenerator = new IdentityGeneratorEn();
const identity = await identityGenerator.generateRandomIdentity();
const passwordGenerator = new PasswordGenerator();
// Get password settings from background
const passwordSettingsResponse = await sendMessage('GET_PASSWORD_SETTINGS', {}, 'background') as PasswordSettingsResponse;
// Initialize password generator with the retrieved settings
const passwordGenerator = new PasswordGenerator(passwordSettingsResponse.settings);
const password = passwordGenerator.generateRandomPassword();
// Extract favicon from page and get the bytes

View File

@@ -2,6 +2,7 @@ import initSqlJs, { Database } from 'sql.js';
import { Credential } from './types/Credential';
import { EncryptionKey } from './types/EncryptionKey';
import { TotpCode } from './types/TotpCode';
import { PasswordSettings } from './types/PasswordSettings';
/**
* Client for interacting with the SQLite database.
@@ -280,6 +281,33 @@ class SqliteClient {
return this.getSetting('DefaultEmailDomain');
}
/**
* Get the password settings from the database.
*/
public getPasswordSettings(): PasswordSettings {
const settingsJson = this.getSetting('PasswordGenerationSettings');
// Default settings if none found or parsing fails
const defaultSettings: PasswordSettings = {
Length: 18,
UseLowercase: true,
UseUppercase: true,
UseNumbers: true,
UseSpecialChars: true,
UseNonAmbiguousChars: false
};
try {
if (settingsJson) {
return { ...defaultSettings, ...JSON.parse(settingsJson) };
}
} catch (error) {
console.warn('Failed to parse password settings:', error);
}
return defaultSettings;
}
/**
* Create a new credential with associated entities
* @param credential The credential object to insert

View File

@@ -1,3 +1,5 @@
import { PasswordSettings } from '../../types/PasswordSettings';
/**
* Generate a random password.
*/
@@ -6,12 +8,37 @@ export class PasswordGenerator {
private readonly uppercaseChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
private readonly numberChars = '0123456789';
private readonly specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?';
private readonly ambiguousChars = 'Il1O0';
private length: number = 18;
private useLowercase: boolean = true;
private useUppercase: boolean = true;
private useNumbers: boolean = true;
private useSpecial: boolean = true;
private useNonAmbiguous: boolean = false;
/**
* Create a new instance of PasswordGenerator
* @param settings Optional password settings to initialize with
*/
public constructor(settings?: PasswordSettings) {
if (settings) {
this.applySettings(settings);
}
}
/**
* Apply password settings to this generator
*/
public applySettings(settings: PasswordSettings): this {
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.
@@ -53,6 +80,14 @@ export class PasswordGenerator {
return this;
}
/**
* Set if only non-ambiguous characters should be used.
*/
public useNonAmbiguousCharacters(use: boolean): this {
this.useNonAmbiguous = use;
return this;
}
/**
* Get a random index from the crypto module.
*/
@@ -98,6 +133,13 @@ export class PasswordGenerator {
chars = this.lowercaseChars;
}
// Remove ambiguous characters if needed
if (this.useNonAmbiguous) {
for (const ambChar of this.ambiguousChars) {
chars = chars.replace(ambChar, '');
}
}
// Generate password
for (let i = 0; i < this.length; i++) {
password += chars[this.getUnbiasedRandomIndex(chars.length)];
@@ -105,21 +147,41 @@ export class PasswordGenerator {
// Ensure password contains at least one character from each selected set
if (this.useLowercase && !/[a-z]/.exec(password)) {
let lowercaseCharsToUse = this.lowercaseChars;
if (this.useNonAmbiguous) {
for (const ambChar of this.ambiguousChars) {
lowercaseCharsToUse = lowercaseCharsToUse.replace(ambChar.toLowerCase(), '');
}
}
const pos = this.getUnbiasedRandomIndex(this.length);
password = password.substring(0, pos) +
this.lowercaseChars[this.getUnbiasedRandomIndex(this.lowercaseChars.length)] +
lowercaseCharsToUse[this.getUnbiasedRandomIndex(lowercaseCharsToUse.length)] +
password.substring(pos + 1);
}
if (this.useUppercase && !/[A-Z]/.exec(password)) {
let uppercaseCharsToUse = this.uppercaseChars;
if (this.useNonAmbiguous) {
for (const ambChar of this.ambiguousChars) {
uppercaseCharsToUse = uppercaseCharsToUse.replace(ambChar.toUpperCase(), '');
}
}
const pos = this.getUnbiasedRandomIndex(this.length);
password = password.substring(0, pos) +
this.uppercaseChars[this.getUnbiasedRandomIndex(this.uppercaseChars.length)] +
uppercaseCharsToUse[this.getUnbiasedRandomIndex(uppercaseCharsToUse.length)] +
password.substring(pos + 1);
}
if (this.useNumbers && !/\d/.exec(password)) {
let numberCharsToUse = this.numberChars;
if (this.useNonAmbiguous) {
for (const ambChar of this.ambiguousChars) {
if (/\d/.test(ambChar)) {
numberCharsToUse = numberCharsToUse.replace(ambChar, '');
}
}
}
const pos = this.getUnbiasedRandomIndex(this.length);
password = password.substring(0, pos) +
this.numberChars[this.getUnbiasedRandomIndex(this.numberChars.length)] +
numberCharsToUse[this.getUnbiasedRandomIndex(numberCharsToUse.length)] +
password.substring(pos + 1);
}
if (this.useSpecial && !/[!@#$%^&*()_+\-=[\]{}|;:,.<>?]/.exec(password)) {

View File

@@ -0,0 +1,34 @@
/**
* Settings for password generation stored in SQLite database settings table as string.
*/
export 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;
}

View File

@@ -0,0 +1,7 @@
import { PasswordSettings } from "@/utils/types/PasswordSettings";
export type PasswordSettingsResponse = {
success: boolean,
error?: string,
settings?: PasswordSettings
};