version 0.2 ready for upload
This commit is contained in:
193
README.md
193
README.md
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user