From 79950ab9fc88fa203d0fdf4910bcd7f02522cde6 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 18 Mar 2025 16:30:41 +0100 Subject: [PATCH] Add password generator settings awareness to browser extension (#167) --- .../src/entrypoints/background.ts | 5 +- .../background/VaultMessageHandler.ts | 17 +++++ .../src/entrypoints/contentScript/Popup.ts | 7 +- browser-extension/src/utils/SqliteClient.tsx | 28 ++++++++ .../generators/Password/PasswordGenerator.ts | 68 ++++++++++++++++++- .../src/utils/types/PasswordSettings.ts | 34 ++++++++++ .../messaging/PasswordSettingsResponse.ts | 7 ++ 7 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 browser-extension/src/utils/types/PasswordSettings.ts create mode 100644 browser-extension/src/utils/types/messaging/PasswordSettingsResponse.ts diff --git a/browser-extension/src/entrypoints/background.ts b/browser-extension/src/entrypoints/background.ts index f5adfcbca..9b518f215 100644 --- a/browser-extension/src/entrypoints/background.ts +++ b/browser-extension/src/entrypoints/background.ts @@ -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)); diff --git a/browser-extension/src/entrypoints/background/VaultMessageHandler.ts b/browser-extension/src/entrypoints/background/VaultMessageHandler.ts index 198015ce2..89c9a2e8c 100644 --- a/browser-extension/src/entrypoints/background/VaultMessageHandler.ts +++ b/browser-extension/src/entrypoints/background/VaultMessageHandler.ts @@ -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 { + 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. */ diff --git a/browser-extension/src/entrypoints/contentScript/Popup.ts b/browser-extension/src/entrypoints/contentScript/Popup.ts index 455b3af55..15c7bcdd0 100644 --- a/browser-extension/src/entrypoints/contentScript/Popup.ts +++ b/browser-extension/src/entrypoints/contentScript/Popup.ts @@ -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 diff --git a/browser-extension/src/utils/SqliteClient.tsx b/browser-extension/src/utils/SqliteClient.tsx index 49bf56a59..0e86ef57d 100644 --- a/browser-extension/src/utils/SqliteClient.tsx +++ b/browser-extension/src/utils/SqliteClient.tsx @@ -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 diff --git a/browser-extension/src/utils/generators/Password/PasswordGenerator.ts b/browser-extension/src/utils/generators/Password/PasswordGenerator.ts index c026d06ff..7d10df81f 100644 --- a/browser-extension/src/utils/generators/Password/PasswordGenerator.ts +++ b/browser-extension/src/utils/generators/Password/PasswordGenerator.ts @@ -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)) { diff --git a/browser-extension/src/utils/types/PasswordSettings.ts b/browser-extension/src/utils/types/PasswordSettings.ts new file mode 100644 index 000000000..50719e895 --- /dev/null +++ b/browser-extension/src/utils/types/PasswordSettings.ts @@ -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; +} \ No newline at end of file diff --git a/browser-extension/src/utils/types/messaging/PasswordSettingsResponse.ts b/browser-extension/src/utils/types/messaging/PasswordSettingsResponse.ts new file mode 100644 index 000000000..56a10991f --- /dev/null +++ b/browser-extension/src/utils/types/messaging/PasswordSettingsResponse.ts @@ -0,0 +1,7 @@ +import { PasswordSettings } from "@/utils/types/PasswordSettings"; + +export type PasswordSettingsResponse = { + success: boolean, + error?: string, + settings?: PasswordSettings +};