version 0.2 ready for upload

This commit is contained in:
2026-08-10 15:23:17 -03:00
parent 9826dc1263
commit 17ad302a6c
15 changed files with 402 additions and 143 deletions

193
README.md
View File

@@ -1,67 +1,81 @@
# logsdu
A Firefox extension that logs you into an academic portal whose sign-in form is
three fields -- registration number, birth date and national ID -- rather than
the usual user and password.
A Firefox extension for logins that password managers cannot save: the ones
made of a registration number, a date of birth and a document number, instead
of a username and a password.
That shape defeats password managers. The form is marked `autocomplete="off"`,
none of the three inputs is a `password` field, and Bitwarden, Firefox and
Chrome all decline to remember it. Since the values never change and the
institution does not let you pick a different login method, the only option
left is copying three values by hand, every time.
Academic portals do this a lot. The form is usually marked `autocomplete="off"`,
none of the fields is a `password` field, and Bitwarden, Firefox and Chrome all
decline to remember it. When the values never change and the institution offers
no other way in, the only option left is copying three values by hand, every
single time -- including the registration number nobody has memorised.
logsdu stores them once and fills them in. By default it also presses "Entrar",
so the normal case is zero clicks.
logsdu stores them once and fills them in. By default it also presses the submit
button, so the normal case is zero clicks.
## Install
Dependencies are managed with **pnpm**, never npm (see "Why pnpm" below).
Node 22+ ships `corepack`, so pnpm does not have to be installed globally.
Once it is published, from addons.mozilla.org. Until then, build it yourself:
```
corepack pnpm install # first time, or after a dependency change
make xpi # typecheck, bundle, and package logsdu-<version>.xpi
```
Then install it permanently. Firefox will not load an unsigned add-on unless
you tell it to, and only the ESR, Developer Edition and Nightly builds accept
being told:
`make xpi` prints the full path of the file and how to install it. The package
it produces is unsigned, and Firefox only accepts unsigned add-ons on the ESR,
Developer Edition and Nightly builds, after setting
`xpinstall.signatures.required` to `false` in `about:config`. On release Firefox
the file has to be signed by Mozilla first -- see "Publishing".
1. Open `about:config`, accept the warning
2. Set `xpinstall.signatures.required` to **false**
3. Open `about:addons`, click the **gear** icon, choose **Install Add-on From
File**, and pick `logsdu-<version>.xpi`
Dependencies are managed with **pnpm**, never npm. See "Why pnpm" below.
It survives restarts. On **release** Firefox that pref is ignored -- see
"Release Firefox" below.
## Setting it up
Finally, open the extension's options page, fill in the four values, and save.
## The four values
Open the extension's options page and fill in four values:
| Field | Example | Notes |
| --- | --- | --- |
| Portal address | `https://portal.example.br/` | Only the origin matters; the path is ignored |
| Registration number | `2000101010` | Digits only |
| Birth date | `01/01/2000` | Reformatted as you type |
| National ID | `000.000.000-00` | Reformatted as you type |
| Date of birth | `01/01/2000` | Reformatted as you type |
| Document number | `000.000.000-00` | Reformatted as you type |
Paste raw digits if you like -- the options page inserts the separators, because
the portal's input masks expect the values in exactly that shape.
input masks on these forms expect the values in exactly that shape.
**The portal address is configuration, not code.** No institution is named
anywhere in the extension: not in the manifest, not in the source, not in the
build output. Be clear about what that does and does not buy you. It stops
someone who reads the extension from learning which portal it is for. It does
not hide anything from someone who can read the extension's storage -- and that
is the same access that would expose your national ID and birth date anyway.
When you press Save, Firefox asks whether logsdu may access the address you
entered. That prompt names one site. Accept it and the extension starts working
there; decline and nothing is stored as usable.
If the portal's markup differs from the common shape, open **Ajustes avançados**
in the options page and adjust the CSS selectors. Invalid selectors are rejected
on save rather than failing silently later.
## What it can access
Nothing, until you say so.
The manifest requests **no host permissions at all**. There is no content script
declared against any site. When you save an address, the extension asks for that
single origin through `permissions.request()`, and a background script then
registers the content script for that one origin and no other.
That means a fresh install can read no pages, the permission prompt names one
site, and you can revoke it whenever you like in `about:addons` -> Permissions.
Clearing your data in the options page hands the permission back automatically.
The `optional_host_permissions` entry in the manifest is `*://*/*`, because the
address is not known until you type it. It is the set the extension may *ask*
from, not what it holds -- nothing is granted without your click, and what is
granted is one origin.
## How it behaves
- **Fills and submits** on the login page, with no interaction.
- **At most one automatic submit per hour.** After an attempt, the extension
drops back to filling only, so a wrong value cannot resubmit itself on every
page load and lock you out. Correct the values and save; saving clears the
- **At most one automatic submit per hour.** After an attempt it drops back to
filling only, so a wrong value cannot resubmit itself on every page load and
lock you out of your account. Correct the values and save; saving clears the
timer, so the next visit tries again immediately.
- **Logging out keeps you logged out.** Clicking the portal's logout control
suppresses the automatic submit for five minutes -- otherwise the logout
@@ -72,32 +86,44 @@ is the same access that would expose your national ID and birth date anyway.
submitting, for when you want to check the values before sending them.
- Automatic submission can be turned off entirely in the options.
## How the values are stored
## Where your data goes
In `storage.local`: private to this browser profile, never synced, never sent
anywhere. That is the same protection a browser-saved password gets, and it has
the same limit -- anyone with your unlocked account can read it. If that is not
good enough for your threat model, this extension is the wrong tool.
Nowhere. It is written to `storage.local`: private to your browser profile,
never synced, never transmitted. The extension makes no network requests of its
own and contains no analytics.
## Release Firefox
Be clear about the limit, though. This is the same protection a browser-saved
password gets, and it has the same weakness -- anyone with your unlocked
computer can read it. If you need protection at rest, this is the wrong tool.
`xpinstall.signatures.required` only works on ESR, Developer Edition and
Nightly. Release Firefox ignores it and refuses unsigned add-ons outright, so
there the same `.xpi` has to be signed first: upload it to
[addons.mozilla.org](https://addons.mozilla.org) as an **unlisted** add-on.
Signing is automated -- nothing is published publicly or reviewed by hand -- and
you install the signed file it hands back.
The interface is in Portuguese, matching the portals it was written for.
The `browser_specific_settings.gecko.id` in the manifest is what gives the
add-on a stable identity across both routes, so settings survive an upgrade
from one to the other.
## Publishing
`make xpi` produces the file to upload at
[addons.mozilla.org](https://addons.mozilla.org/developers/addon/submit/).
Two distribution choices:
- **Listed** -- public on addons.mozilla.org, searchable, installable by anyone,
and updates are delivered by Mozilla automatically.
- **Unlisted** -- signed but not published. You distribute the signed file
yourself. Updates need a self-hosted update manifest, or resending the file.
Builds are never minified, which is deliberate: AMO requires a separate
source-code submission for any add-on whose uploaded code is machine-generated,
and that obligation would apply to every future release. The whole extension is
about 33 KB, so the saving would not pay for the process, and readable code is
easier for a reviewer -- or anyone auditing what handles their credentials -- to
check.
## Chrome
Not yet. The source deliberately avoids anything Firefox-specific: it uses the
`chrome.*` namespace, Manifest V3, and no APIs Chrome lacks, so a Chrome build
should be a manifest question rather than a rewrite. It has not been tried, so
do not assume it works.
`make chrome` builds it. The only difference is the background key: Firefox MV3
uses an event page, Chrome MV3 requires a service worker, so the manifest is
generated per target rather than duplicated. Everything else -- `chrome.*`
namespace, MV3, no Firefox-only APIs -- is already shared.
It has not been tested against Chrome. Do not assume it works.
## Development
@@ -109,54 +135,49 @@ corepack pnpm run dev # rebuild on change
make clean
```
While iterating, reinstalling an `.xpi` for every edit is tedious. Load the
unpacked directory instead: `about:debugging` -> **This Firefox** -> **Load
Temporary Add-on** -> `build/manifest.json`, then press **Reload** there after
each rebuild. That copy disappears on restart, which is the point -- it is for
development, not for daily use.
While iterating, load the unpacked directory rather than reinstalling an `.xpi`
each time: `about:debugging` -> **This Firefox** -> **Load Temporary Add-on** ->
`build/manifest.json`, then press **Reload** there after each rebuild. That copy
disappears on restart, which is the point -- it is for development, not daily
use.
`corepack pnpm run dev` watches and rebuilds, static files included, but
Firefox still needs the Reload click to pick anything up.
`corepack pnpm run dev` watches and rebuilds, static files included, but Firefox
still needs the Reload click to pick anything up.
### Layout
| Path | Role |
| --- | --- |
| `src/manifest.json` | MV3 manifest. Names no site |
| `src/manifest.json` | MV3 manifest. Requests no host access |
| `src/background.ts` | Registers the content script for the granted origin |
| `src/content.ts` | Isolated world. Decides whether to act, then delegates |
| `src/injected.ts` | Page world. Does the actual filling and clicking |
| `src/portal.ts` | Every selector the extension knows about the form |
| `src/portal.ts` | Default selectors and the shape of a fill request |
| `src/config.ts` | Stored values, the rate limit and the logout cooldown |
| `src/format.ts` | Input normalisers for the three masked fields |
| `src/options.*`, `src/popup.*` | The two bits of UI |
### Why two scripts instead of one
The portal drives its inputs with Inputmask, which replaces each element's
`value` property with its own accessor. A content script assigning
`input.value` from the isolated world writes through Xrays to the *native*
setter and skips that accessor: the field looks right on screen, but the mask's
buffer is unchanged, and the page's submit handler reads the stale buffer back
out through jQuery. So the filling happens inside the page, through the page's
own jQuery and Inputmask.
These portals drive their inputs with Inputmask, which replaces each element's
`value` property with its own accessor. A content script assigning `input.value`
from the isolated world writes through Xrays to the *native* setter and skips
that accessor: the field looks right on screen, but the mask's buffer is
unchanged, and the page's submit handler reads the stale buffer back out through
jQuery. So the filling happens inside the page, through the page's own jQuery
and Inputmask, in `injected.ts`.
Submitting is a real click on the button, never `form.submit()`. The portal
intercepts the submit event, cancels it, and posts by AJAX with a CSRF token
taken from a meta tag. `form.submit()` would bypass that handler and lose the
token; a click reproduces exactly what a person pressing "Entrar" does.
Submitting is a real click on the button, never `form.submit()`. These portals
intercept the submit event, cancel it, and post by AJAX with a CSRF token taken
from a meta tag. `form.submit()` would bypass that handler and lose the token; a
click reproduces exactly what a person pressing the button does.
### Why the content script matches every URL
### Why the field selectors are configurable
The address is configured at runtime, so it cannot also be a manifest match
pattern -- putting it there is exactly what would name the institution in the
shipped code. The script therefore loads everywhere and stops immediately
unless the page's origin equals the configured one. Origins are compared whole,
so `portal.example.br.evil.tld` does not match.
The honest cost: the extension holds read access to every page you visit. The
alternative -- registering the content script at runtime with
`scripting.registerContentScripts` and an optional host permission -- avoids
that at the price of a background script and a permission prompt.
Hardcoding them would tie the extension to one institution while pretending to
be general. They live in `portal.ts` as defaults and can be overridden per
installation, so the same build works for any portal of this shape -- and no
institution is named anywhere in the source, the manifest or the build output.
### Why pnpm, not npm