diff --git a/apps/browser-extension/src/utils/formDetector/FieldPatterns.ts b/apps/browser-extension/src/utils/formDetector/FieldPatterns.ts index 52f570df5..6ee06e11e 100644 --- a/apps/browser-extension/src/utils/formDetector/FieldPatterns.ts +++ b/apps/browser-extension/src/utils/formDetector/FieldPatterns.ts @@ -14,6 +14,7 @@ export type FieldPatterns = { birthDateDay: string[]; birthDateMonth: string[]; birthDateYear: string[]; + totp: string[]; } /** @@ -48,7 +49,8 @@ export const EnglishFieldPatterns: FieldPatterns = { gender: ['gender', 'sex'], birthDateDay: ['-day', 'birthdate_d', 'birthdayday', '_day', 'day'], birthDateMonth: ['-month', 'birthdate_m', 'birthdaymonth', '_month', 'month'], - birthDateYear: ['-year', 'birthdate_y', 'birthdayyear', '_year', 'year'] + birthDateYear: ['-year', 'birthdate_y', 'birthdayyear', '_year', 'year'], + totp: ['totp', 'otp', 'one-time', 'onetime', 'token', 'authenticator', '2fa', 'twofa', 'two-factor', 'mfa', 'security-code', 'auth-code', 'passcode', 'pin-code', 'pincode'] }; /** @@ -123,7 +125,8 @@ export const DutchFieldPatterns: FieldPatterns = { gender: ['geslacht', 'aanhef'], birthDateDay: ['dag'], birthDateMonth: ['maand'], - birthDateYear: ['jaar'] + birthDateYear: ['jaar'], + totp: ['verificatiecode', 'eenmalig', 'authenticatie', 'tweefactor', 'beveiligingscode'] }; /** @@ -204,7 +207,8 @@ export const CombinedFieldPatterns: FieldPatterns = { gender: [...new Set([...EnglishFieldPatterns.gender, ...DutchFieldPatterns.gender])], birthDateDay: [...new Set([...EnglishFieldPatterns.birthDateDay, ...DutchFieldPatterns.birthDateDay])], birthDateMonth: [...new Set([...EnglishFieldPatterns.birthDateMonth, ...DutchFieldPatterns.birthDateMonth])], - birthDateYear: [...new Set([...EnglishFieldPatterns.birthDateYear, ...DutchFieldPatterns.birthDateYear])] + birthDateYear: [...new Set([...EnglishFieldPatterns.birthDateYear, ...DutchFieldPatterns.birthDateYear])], + totp: [...new Set([...EnglishFieldPatterns.totp, ...DutchFieldPatterns.totp])] }; /** diff --git a/apps/browser-extension/src/utils/formDetector/FormDetector.ts b/apps/browser-extension/src/utils/formDetector/FormDetector.ts index 73f03d022..73c2c6a34 100644 --- a/apps/browser-extension/src/utils/formDetector/FormDetector.ts +++ b/apps/browser-extension/src/utils/formDetector/FormDetector.ts @@ -42,8 +42,8 @@ export class FormDetector { return false; } - // Check if the wrapper contains a password or likely username field before processing. - if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper)) { + // Check if the wrapper contains a password, likely username field, or TOTP field before processing. + if (this.containsPasswordField(formWrapper) || this.containsLikelyUsernameOrEmailField(formWrapper) || this.containsTotpField(formWrapper)) { return true; } @@ -871,6 +871,66 @@ export class FormDetector { return false; } + /** + * Check if a form contains a TOTP/2FA field. + */ + private containsTotpField(wrapper: HTMLElement): boolean { + const totpField = this.findTotpField(wrapper as HTMLFormElement | null); + return totpField !== null && this.isElementVisible(totpField); + } + + /** + * Find a TOTP/2FA input field in the form. + * Uses pattern matching and heuristics specific to TOTP fields. + */ + private findTotpField(form: HTMLFormElement | null): HTMLInputElement | null { + // First try pattern-based detection + const candidates = this.findAllInputFields( + form, + CombinedFieldPatterns.totp, + ['text', 'number'] + ); + + // Filter out parent-child duplicates + const filteredCandidates = this.filterOutNestedDuplicates(candidates); + + if (filteredCandidates.length > 0) { + return filteredCandidates[0]; + } + + // Additional heuristics for TOTP fields that may not match patterns + const allInputs = form + ? Array.from(form.querySelectorAll('input')) + : Array.from(this.document.querySelectorAll('input')); + + for (const input of allInputs) { + if (!this.isElementVisible(input)) { + continue; + } + + // Check for autocomplete="one-time-code" + const autocomplete = input.getAttribute('autocomplete')?.toLowerCase() ?? ''; + if (autocomplete === 'one-time-code') { + return input; + } + + // Check for maxLength=6 combined with inputmode="numeric" + const maxLength = input.maxLength; + const inputMode = input.getAttribute('inputmode'); + if (maxLength === 6 && inputMode === 'numeric') { + return input; + } + + // Check for numeric pattern attribute with length constraint + const pattern = input.getAttribute('pattern'); + if (pattern && /^\[0-9\]/.test(pattern) && maxLength === 6) { + return input; + } + } + + return null; + } + /** * Check if a form contains a likely username or email field. */ @@ -964,6 +1024,12 @@ export class FormDetector { return 'email'; } + // Check if any of the elements is a TOTP field + const totpField = this.findTotpField(formWrapper as HTMLFormElement | null); + if (totpField && elementsToCheck.includes(totpField)) { + return 'totp'; + } + return null; } @@ -1030,6 +1096,11 @@ export class FormDetector { detectedFields.push(genderField.field as HTMLInputElement); } + const totpField = this.findTotpField(wrapper as HTMLFormElement | null); + if (totpField) { + detectedFields.push(totpField); + } + return { form: wrapper as HTMLFormElement, emailField: emailFields.primary, @@ -1041,7 +1112,8 @@ export class FormDetector { firstNameField, lastNameField, birthdateField, - genderField + genderField, + totpField }; } } diff --git a/apps/browser-extension/src/utils/formDetector/__tests__/FormDetector.totp.test.ts b/apps/browser-extension/src/utils/formDetector/__tests__/FormDetector.totp.test.ts new file mode 100644 index 000000000..5fbbbee2a --- /dev/null +++ b/apps/browser-extension/src/utils/formDetector/__tests__/FormDetector.totp.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; + +import { FormField, testField } from './TestUtils'; + +describe('FormDetector TOTP tests', () => { + it('contains tests for TOTP field detection', () => { + /** + * This test suite uses testField() helper function + * to test TOTP/2FA field detection for various forms. + * The actual test implementations are in the helper functions. + * This test is just to ensure the test suite is working and to satisfy the linter. + */ + expect(true).toBe(true); + }); + + describe('English TOTP form 1 detection', () => { + const htmlFile = 'en-totp-form1.html'; + + testField(FormField.Totp, 'otp', htmlFile); + }); +}); diff --git a/apps/browser-extension/src/utils/formDetector/__tests__/TestUtils.ts b/apps/browser-extension/src/utils/formDetector/__tests__/TestUtils.ts index ee1c990d8..5a950f368 100644 --- a/apps/browser-extension/src/utils/formDetector/__tests__/TestUtils.ts +++ b/apps/browser-extension/src/utils/formDetector/__tests__/TestUtils.ts @@ -26,7 +26,8 @@ export enum FormField { Gender = 'gender', GenderMale = 'genderMale', GenderFemale = 'genderFemale', - GenderOther = 'genderOther' + GenderOther = 'genderOther', + Totp = 'totp' } /** @@ -199,7 +200,8 @@ export const createMockFormFields = (document: Document): FormFields => ({ genderField: { type: 'select', field: document.createElement('select') - } + }, + totpField: null }); /** diff --git a/apps/browser-extension/src/utils/formDetector/__tests__/test-forms/en-totp-form1.html b/apps/browser-extension/src/utils/formDetector/__tests__/test-forms/en-totp-form1.html new file mode 100644 index 000000000..1b1697b7c --- /dev/null +++ b/apps/browser-extension/src/utils/formDetector/__tests__/test-forms/en-totp-form1.html @@ -0,0 +1,9 @@ + + + + TOTP Form 1 + + +

+ + diff --git a/apps/browser-extension/src/utils/formDetector/types/FormFields.ts b/apps/browser-extension/src/utils/formDetector/types/FormFields.ts index bcb170715..7f8301b27 100644 --- a/apps/browser-extension/src/utils/formDetector/types/FormFields.ts +++ b/apps/browser-extension/src/utils/formDetector/types/FormFields.ts @@ -30,4 +30,7 @@ export type FormFields = { other: HTMLInputElement | null; }; }; + + // TOTP/2FA field for one-time codes + totpField: HTMLInputElement | null; }