244 lines
11 KiB
Markdown
244 lines
11 KiB
Markdown
# logsdu
|
|
|
|
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.
|
|
|
|
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 the submit
|
|
button, so the normal case is zero clicks.
|
|
|
|
## Install
|
|
|
|
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 # bundle and package dist/logsdu-<version>-firefox.xpi
|
|
```
|
|
|
|
`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".
|
|
|
|
Dependencies are managed with **pnpm**, never npm. See "Why pnpm" below.
|
|
|
|
## Setting it up
|
|
|
|
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 |
|
|
| 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
|
|
input masks on these forms expect the values in exactly that shape.
|
|
|
|
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 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
|
|
redirect lands on the login page and you would be signed straight back in.
|
|
- When it fills without submitting, a small note at the bottom of the page says
|
|
why. Click it to dismiss.
|
|
- The toolbar popup has a **Preencher agora** button that fills without
|
|
submitting, for when you want to check the values before sending them.
|
|
- Automatic submission can be turned off entirely in the options.
|
|
|
|
## Where your data goes
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
The interface is in Portuguese, matching the portals it was written for.
|
|
|
|
## 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
|
|
|
|
```
|
|
make chrome # unpacked Chrome build in build/chrome/
|
|
make crx # package it as dist/logsdu-<version>-chrome.zip
|
|
```
|
|
|
|
Load `build/chrome/` via `chrome://extensions` -> Developer mode -> **Load
|
|
unpacked** (select the folder itself), or upload the zip at
|
|
[the Web Store dashboard](https://chrome.google.com/webstore/devconsole).
|
|
|
|
One codebase, two manifests. Chrome MV3 requires a background *service worker*
|
|
and rejects Firefox's event-page `background.scripts`; Firefox needs the gecko
|
|
block that Chrome has no use for. `esbuild.config.mjs` writes the right manifest
|
|
per target, so the port is a build flag rather than a fork. Everything else --
|
|
the `chrome.*` namespace, MV3, the permission model -- is shared.
|
|
|
|
Two things that differ in practice, both handled:
|
|
|
|
- **Icons must be raster.** Chrome does not accept SVG in `icons`, so the PNGs
|
|
in `icons/` are generated from `logsdu.svg` and both browsers use those.
|
|
- **Service workers have no `window` or `document`.** A stray reference through
|
|
a shared import would break Chrome only, silently, at runtime. `make smoke`
|
|
runs the built background bundle in a worker-shaped sandbox to catch that.
|
|
|
|
What has been verified: Chrome 151 loads the build without errors, and the
|
|
background bundle registers exactly one content script for exactly the
|
|
configured origin. What has **not** been verified is a real login against a live
|
|
portal in Chrome.
|
|
|
|
A caveat that applies to both browsers: the page-world filler is injected as a
|
|
`<script src>` tag, which a site's Content-Security-Policy can refuse. Portals
|
|
that send no CSP -- the common case for this kind of form -- are unaffected. A
|
|
portal that does would need the filler registered as a `MAIN` world content
|
|
script instead.
|
|
|
|
## Development
|
|
|
|
```
|
|
corepack pnpm install
|
|
make # everything: build and package both browsers
|
|
make firefox # build Firefox only -> build/firefox/
|
|
make chrome # build Chrome only -> build/chrome/
|
|
make test # unit tests
|
|
make smoke # background bundle under a service worker
|
|
corepack pnpm run dev # rebuild Firefox on change
|
|
make clean # remove build/, dist/ and stray packages
|
|
make distclean # clean, plus node_modules/
|
|
```
|
|
|
|
A bare `make` typechecks once, bundles for both browsers into `build/`, and
|
|
packages both into `dist/`. Packaging is only a zip of a directory that was
|
|
going to be built anyway, so it costs nothing and keeps `dist/` from drifting
|
|
out of step with the sources.
|
|
|
|
Each browser gets its own directory under `build/`, so the two can coexist and
|
|
neither is ever left stale by the other. `dist/` holds the packages meant to be
|
|
published, and is kept out of `build/` so that packaging never tries to include
|
|
its own output.
|
|
|
|
While iterating, load the unpacked directory rather than reinstalling an `.xpi`
|
|
each time: `about:debugging` -> **This Firefox** -> **Load Temporary Add-on** ->
|
|
`build/firefox/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.
|
|
|
|
### Layout
|
|
|
|
| Path | Role |
|
|
| --- | --- |
|
|
| `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` | 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 |
|
|
| `tools/sw-smoke.mjs` | Checks the background bundle survives a service worker |
|
|
|
|
### Why two scripts instead of one
|
|
|
|
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()`. 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 field selectors are configurable
|
|
|
|
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
|
|
|
|
**Do not run `npm install` or `npm ci` in this repo.** This project is developed
|
|
on **ZFS**, and npm's installer renames many directories in parallel while
|
|
hoisting and deduping, which ZFS intermittently fails with:
|
|
|
|
```
|
|
npm error code ENOTEMPTY
|
|
npm error syscall rename
|
|
```
|
|
|
|
pnpm hard-links packages from a global content-addressable store and does not
|
|
perform that rename dance, so it is unaffected. Only `npm install` / `npm ci`
|
|
are the problem -- running *scripts* through npm is fine, since that just spawns
|
|
tsc and esbuild. The lockfile is `pnpm-lock.yaml`; there is no
|
|
`package-lock.json` and none should be created.
|
|
|
|
## Licence
|
|
|
|
GPL-3.0-or-later. See `LICENSE`.
|