Skip to content

fix: improve hostname parsing logic in getSiteName function#1417

Open
Hank076 wants to merge 119 commits into
Authenticator-Extension:devfrom
Hank076:dev
Open

fix: improve hostname parsing logic in getSiteName function#1417
Hank076 wants to merge 119 commits into
Authenticator-Extension:devfrom
Hank076:dev

Conversation

@Hank076

@Hank076 Hank076 commented May 6, 2025

Copy link
Copy Markdown

Fix IP address handling priority

Hank076 and others added 30 commits May 6, 2025 21:52
EntryStorage.import() ran parseInt() on the type and algorithm fields,
but both are persisted as their enum *names* ("hotp", "SHA256"), so
parseInt always returned NaN and fell back to the defaults. Every
imported entry therefore became TOTP/SHA1, losing HOTP/Steam/Battle/hex
types and SHA256/SHA512 digests.

Map the stored name back to the enum (case-insensitive for algorithm),
guarding with a typeof-number check so legacy numeric data still falls
back safely.

Refs Authenticator-Extension#1292, Authenticator-Extension#405, Authenticator-Extension#1442, Authenticator-Extension#1294, Authenticator-Extension#1089
second % 60 keeps the sign of the dividend in JS, so a negative clock
offset could leave state.second negative; the old "+ 60" only covered
offsets down to -60. Use a positive modulo so any offset stays in 0..59.

Refs Authenticator-Extension#1310
The otpauth importer rejected periods that were > 60 or not a divisor of
60, silently dropping valid values like 45, 90 or 120 and falling back to
30s, which produces wrong codes. Validate as a positive integer instead.

Refs Authenticator-Extension#1271, Authenticator-Extension#1508
Toggling Smart Filter off also fired the "smart filter loosely matches
the domain name" notification. Show it only when the toggle is turned on.

Refs Authenticator-Extension#1282
An unset autolock falls back to a 30-minute default in setAutolock(), but
the advisor treated "unset" the same as "disabled" and kept showing the
warning. Only warn when autolock is explicitly set to 0.

Refs Authenticator-Extension#1281
The algorithm and digits <select>s used v-model without the .number
modifier, so the in-memory entry held string values (e.g. "2"). The
generator compares the algorithm with === against the numeric enum, so
every non-SHA1 choice fell through to SHA1 until the entry was reloaded
from storage (which normalises the type). Add .number like the type
select already does.

Refs Authenticator-Extension#1184
The OneDrive OAuth code-for-token POST had no body at all, so Microsoft's
token endpoint always rejected it and sign-in never completed. Unlike
Google's endpoint (which accepts the params in the query string), Microsoft
requires them in the x-www-form-urlencoded body. Add client_id,
client_secret, code, redirect_uri and grant_type.

Refs Authenticator-Extension#1494, Authenticator-Extension#1369, Authenticator-Extension#1408, Authenticator-Extension#1202, Authenticator-Extension#1424
The search box was only revealed by the clearFilter action, so a large
account list never showed it on initial load or after unlocking with a
passphrase. Reveal it after entries load when there are >= 10 of them and
a smart filter isn't currently applied.

Refs Authenticator-Extension#1496, Authenticator-Extension#1400
andOTP exports a flat JSON array using a "label" field instead of the
keyed object with "account" that the importer expects, so its backups
imported as empty/garbled. Detect the array form and map each entry to
RawOTPStorage (label -> account with the redundant issuer prefix stripped,
type lower-cased; EntryStorage.import normalises type/algorithm names).

Refs Authenticator-Extension#1304
getQrUrl encoded the whole label, turning the issuer/account separator into
%3A. Several authenticators (including Google Authenticator) split the label
on a literal ":" and reject the %3A form, so the exported QR scanned as
invalid. Encode issuer and account separately and strip the internal
"::host" match suffix, matching the text backup and the otpauth spec.

Refs Authenticator-Extension#1302
Autofill collected every text/number/tel/password input regardless of
visibility, so the code could be written into a hidden honeypot or an
off-screen field instead of the real 2FA box. Filter candidates by
checkVisibility() (guarded for browsers without it).

Refs Authenticator-Extension#1273, Authenticator-Extension#1136
moveCode received from/to positions in the displayed (pinned-first) order
but spliced and re-indexed the raw entries array, so dragging scrambled the
list whenever any entry was pinned. Reorder the displayed view first, then
write back sequential indices. No change when nothing is pinned.

Refs Authenticator-Extension#1312, Authenticator-Extension#1428
The issuer/account edit inputs let keydown events bubble to the list's
arrow-key navigation handlers, so pressing left/right to move the text
cursor moved focus to another entry, blurred the field and committed the
edit (e.g. saving an emptied account). Stop keydown propagation on those
inputs.

Refs Authenticator-Extension#495
…ding

failedCount filtered Promise.allSettled results with `!res`, but those are
always-truthy {status,value} objects, so the count was always 0 and both
failure branches were dead code — a fully failed otpauth-migration import
still reported "migrationsuccess". Inspect status/value instead.

Refs code review §2.2
chrome.storage.sync.set was awaited with no error handling, so hitting the
sync quota (MAX_ITEMS / QUOTA_BYTES_PER_ITEM) rejected as a silent unhandled
rejection while the entry stayed in the Vuex view but was never persisted —
the long-standing "can't add more than N accounts" data loss. Wrap the write
with a clear error, and surface it when adding an account so a save that
failed no longer shows a phantom entry.

Refs code review §2.1
… codes

isMatchedEntry matched the bound host with an unanchored indexOf and matched
the page-controlled <title> as a substring, so a hostile page at
google.com.attacker.com (or just <title>Google</title>) could be the sole
match and receive the user's live OTP via the autofill command. Add a strict
mode (used by autofill) that anchors the host match to a real domain boundary
and drops the title hint; display filtering stays loose.

Refs code review §2.3
nextCode() guarded on this.$store.state.style.hotpDisabled, but the flag lives
one level deeper (state.style.style.hotpDisabled), so the guard was always
undefined and every click advanced and persisted the HOTP counter with no
rate limit. The disabled CSS class also read a misspelled `hotpDiabled` and
never applied. Fix both.

Refs code review §3.4
…ckup

getOneLineOtpBackupFile reassigned otpStorage.issuer/account in place, but
that object is the live Vuex export state, so generating a one-line backup
corrupted the stored issuer/account (colons stripped, values %-encoded) for
any subsequent JSON backup and double-encoded on a repeat download. Compute
sanitized values into locals instead.

Refs code review §3.3
…tion

The hand-rolled protobuf walker trusted every attacker-controlled length byte
and read past the buffer (subBytesArray returns undefined), and indexed the
algorithm/digits/type lookup tables with raw bytes — out-of-range values
produced otpauth://undefined/...&algorithm=undefined, silently importing
entries that generate wrong codes. decodeURIComponent on a missing/garbled
data= param could also throw and abort the whole import batch. Guard the
data= extraction and decode, bounds-check each field before reading, skip
entries with out-of-range algorithm/digits/type, and wrap the caller so one
bad payload can't abort a mixed import.

Refs code review §3.2
The confirm action added an anonymous window "confirm" listener on every
dispatch and never removed it, so listeners accumulated and a single confirm
event re-fired every stale one, resolving old promises unexpectedly. Use a
named handler that removes itself when it fires.

Refs code review §3.5
applyPassphrase switches to LoadingPage before decrypting/migrating, which
can throw (e.g. "argon2 did not return a hash!"). The caller awaited without
catching, so the error became an unhandled rejection and the UI was stuck on
LoadingPage with no way back. Catch it, return to EnterPasswordPage and alert.

Refs code review §3.6
Each argon2 call added an anonymous window "message" listener that was never
removed and resolved on the first message to arrive, with no link between
request and reply — overlapping hash/verify calls (e.g. the v3 multi-key
unlock loop) could resolve with the wrong response, and listeners piled up.
Route every call through one helper that tags the request with a unique id,
only accepts the reply from the sandbox iframe matching that id, removes its
listener, and times out instead of hanging forever. Collapse the six copied
inline postMessage blocks (Accounts, import) onto argonHash/argonVerify.

Refs code review §3.1, §3.6 (sandbox timeout)
crypto-js is imported and run at runtime (OTP secret AES encrypt/decrypt in
key-utilities, encryption, migration, etc.) but was declared under
devDependencies, so an SBOM or `npm install --omit=dev` would drop the actual
crypto library. @types/lodash is type-only and was in dependencies. Swap them
to the correct sections.

Refs code review §5, §6
The base webpack config sets devtool: "source-map" for development; the prod
config merged it through, so release artifacts shipped full source maps (and
build.sh copies the .map files into the package). Override devtool: false for
production.

Refs code review §5
The fork notice ended with `read -n1` (Press any key to continue), which
hangs any non-interactive/automated build on a fork remote. Keep the notice
but drop the blocking read.

Refs code review §5
…mport

decryptBackupData JSON.parsed the AES-decrypted v2/v3 payload with no guard,
so one entry that decrypts to invalid UTF-8/JSON threw and aborted the whole
backup import. Wrap it and continue past the bad entry.

Refs code review §6
new Date(header).getTime() returns NaN for an unparseable Date header, which
made the offset NaN and fell through to "clock_too_far_off" — a misleading
state. Return "updateFailure" instead.

Refs code review §6
The Dropbox upload handler only special-cased 401 and otherwise JSON-parsed
the body, so a 5xx or HTML error page would throw or be misread. Reject
non-2xx responses with a clear error.

Refs code review §6
Hank076 added 30 commits June 23, 2026 10:07
New keys introduced by the redesign: theme_auto, settings_appearance/
settings_general, onboarding_*, vault_locked/unlock, qr_transfer_*,
backup_on_device/cloud_sync/connected/not_connected, edit_accounts,
show_qr, pin_to_top/unpin, showing_codes_for/other_accounts/filter_to_site.

Added to en (default_locale) and zh_TW; other locales fall back to en.
…stem

Replace the SCSS themify mixin and the 7 named themes with CSS
custom-property design tokens (OKLCH accents). Themes are now
light / dark / auto (prefers-color-scheme) / accessibility (high
contrast) / compact; the redundant simple and flat themes are dropped
and mapped to light. Switch to a system font stack (no bundled webfonts)
and widen the popup to 360px.

Restyle every surface to the new visual language (per the Claude Design
spec): rounded cards, monogram avatars, a depleting bottom countdown bar
with a seconds badge, a copy toast, an onboarding first-run view, and
redesigned unlock / loading / manage-password / backup / add-account /
preferences / permissions / options / import screens. Settings use
sectioned card rows with pill toggles.

Move the pin and Show-QR actions to a right-click context menu; the QR
now opens in a bottom sheet showing the account. The smart-filter view
groups matched (accent-bordered) vs. other (dimmed) accounts under a
toggleable "showing codes for <site>" banner that only appears when it
actually segregates the list.

Also fix: opening search with "/" no longer breaks the search box layout.
Redesign the extension UI onto a CSS custom-property design system (light/dark/auto/accessibility/compact), restyling every screen and moving pin/QR actions into a right-click menu.
Plan to bind each OTP entry to a host and require a host match before
autofilling into a page, closing the popup copyCode path that injected
live codes without any origin check.
Bind each OTP entry to a host (new `host` field) and only autofill a
live code into a page whose real host matches it. This closes the popup
copyCode path that injected codes into any active tab with no origin
check, and unifies both autofill paths (popup click and keyboard
command) on a strict "no bound host, no injection" rule.

- add `host` to OTPEntry / RawOTPStorage; encryption covers it via the
  serialized JSON, and legacy "issuer::host" encodings migrate on read
- isMatchedEntry strict matching now trusts only the bound host
- manual add page gains a host field; QR scans pre-bind the scanned tab
  host; entry edit mode can set or fix the host
Host-bound autofill: bind each OTP entry to a host and require a host
match before injecting a live code into a page. Adds a host field to the
manual add page, pre-binds the scanned tab host on QR import, and lets
entry edit mode set the host.
Block all cloud uploads (Dropbox / Drive / OneDrive) until a master
password is set, so plaintext OTP secrets can never leave the device.
Without a password the export is unencrypted, and both the scheduled and
manual upload paths previously sent it to the cloud as-is.

- cloudBackupAllowed() gates every provider's upload()
- runScheduledBackup() skips entirely when no password is set
- the backup page hides the cloud providers and explains why
Require a master password before any cloud backup so plaintext OTP
secrets never leave the device. Blocks every provider's upload(), skips
scheduled backups, and hides the cloud providers in the UI until a
password is set.
Match the redesign spec: a wide card with a header, underline tabs
(Backup file / QR images / Setup codes) that switch panels, and a file
panel with a dashed dropzone plus an encrypted-file passphrase state
(file card with name + "encrypted" badge, passphrase field, decrypt
button). Backing out of the picker cancels the pending read so a re-pick
doesn't double-process.
The Backup page's "import" row was a plain in-page link, so clicking it
navigated the popup to the full-width import page and broke its layout.
Open it in a new tab instead, matching the other import entry points.
Redesign the import page (interactive tabs + file dropzone/passphrase) and fix the Backup page opening import inside the popup.
EntryStorage.get() rebuilt each OTPEntry from an explicit field list that
omitted the new host field, so an unencrypted entry's bound host was
dropped on every reload (e.g. after exporting a backup, then editing it
again). Carry host through, matching import(). Adds a storage round-trip
regression test.
Preserve an entry's bound host across a storage reload: EntryStorage.get()
dropped the new host field when rebuilding entries, so an unencrypted
entry's host was cleared after exporting a backup. Adds a round-trip test.
Rename from the upstream "Authenticator" to a distinct brand so the fork can
ship to the Chrome Web Store without tripping the impersonation policy:

- extName / extShortName / extDesc in the en locale
- new countdown-ring "076 721" app icon (svg master + 16/19/38/48/128 png)
- cobalt accent (hue 258) replacing the indigo design token
Fill the previously empty credentials stub with the fork's own Dropbox app
key so cloud backup can authenticate. This is a public OAuth client id (it
appears in the auth URL and the shipped bundle), not a secret.
uploadBackup runs in the background service worker, where XMLHttpRequest does
not exist, so every Dropbox upload threw a ReferenceError before reaching the
network. Port the upload to fetch (available in both popup and worker) and
surface the Dropbox response body on a non-2xx so scope/path errors are
diagnosable instead of a bare HTTP status. Also rebrand the user-facing 401
strings to OTPilot.
The cloud backup pages shared three bugs, all surfaced while bringing Dropbox
online; they touch the same components so they land together:

- pass the active Encryption instance (encryption.get(defaultEncryption)) to
  upload() instead of the whole Vuex Map, fixing "getEncryptionStatus is not a
  function" on Dropbox / Drive / OneDrive
- notify an open popup when Dropbox auth finishes or is cancelled, and add a
  connecting indicator so the page leaves the sign-in view automatically
- default cloud backup to encrypted (matching backup.ts upload behaviour) and
  only show the unencrypted warning after connecting, when the user has
  explicitly opted out; drop the dead === null lazy-init from the getters
- redesign the Dropbox page UI
The rebrand only renamed the en locale, so non-English users still saw the old
name (sometimes translated, e.g. de "Authentifizierung"). A brand name must not
be translated: set extName/extShortName to "OTPilot Authenticator" / "OTPilot"
in the remaining 43 locales. If the locale generator is re-run, it must keep the
brand name verbatim.
Replace the upstream Google client_id in the Chrome manifest with the fork's
own OAuth client, and add the Web Store public "key" so the unpacked dev build
and the published build share one extension id. getAuthToken binds the OAuth
client to the extension id, so a stable id is required for Drive to work in
both dev and production.
The dropzone was a bare <label> with no drag handlers, so dropping a file did
nothing — only click-to-browse worked. Add @dragover.prevent (required for the
drop event to fire) and a dropFile handler that reads dataTransfer.files and
reuses importFile through a shimmed event.
- fill the zh_TW gap (host, backup_requires_password)
- add zh_CN and ja translations for the 35 fork-added strings
- replace every standalone "Authenticator" (the app) with "OTPilot" across all
  locales, keeping "OTPilot Authenticator" and "Google Authenticator" intact
Two UI papercuts found while testing cloud backup:
- argon2.verify() rejects on a wrong passphrase (the common case) as well as
  on a malformed hash; stop console.error-ing every mistyped password.
- widen SelectInput's modelValue to include Boolean. DrivePage/OneDrivePage
  bind a boolean isEncrypted to the encrypted selector, which tripped a Vue
  prop-type warning.
Google blocks custom-URI-scheme (Chrome App) OAuth clients for new apps, so
getAuthToken can no longer be used. Switch Drive to launchWebAuthFlow with a
Web-application client and the extension's own chromiumapp.org redirect, and
rewrite getToken/refreshToken/getFolder/upload from XMLHttpRequest to fetch —
they run in the MV3 background service worker, where XHR is undefined. The
background now broadcasts "driveauthdone" so DrivePage leaves the sign-in view
automatically, mirroring the Dropbox flow.
Match the redesign spec: a "Sync with Google Drive" connect prompt with
the Drive logo, description and encrypted-before-upload note, and a
connected state showing the account, an encrypted-backup selector, a risk
note, and back-up-now / log-out actions. Reuses the shared cloud-page
styles with a Drive brand variant.
- grey out and disable the OneDrive cloud backup row (not implemented yet)
  with a "not available yet" label (backup_unavailable; translated for
  en/zh_TW/zh_CN/ja, the rest fall back to en)
- use the official multi-colour Google Drive logo in the backup list so it
  matches the Drive page header
- replace the popup header logo with the OTPilot countdown ring to match the
  app icon
Re-apply the Traditional Chinese wording that was accidentally reverted when committing the privacy policy (0c6c152).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant