Desktop Material

The built-in authenticator and QR pairing / 內置驗證器同 QR 配對

Desktop Material ships its own TOTP authenticator: a place to register and keep second factors for whatever accounts the user likes, and to read live codes without reaching for a phone. Registering a factor draws a scannable QR from a secret generated on this machine, and nothing is stored until the user has typed one working code back.

Everything here is local. There is no account, no cloud sync, no telemetry, and no network request of any kind — including for drawing the QR, which is encoded and painted in-process rather than fetched from a chart service.

Desktop Material 自己有個 TOTP 驗證器:可以幫任何帳戶登記同保存第二重驗證,唔使攞手機 都睇到即時密碼。登記嗰陣會用喺呢部機度整嘅密鑰畫個掃得到嘅 QR,而且要你打返一個正確 密碼,先至會真係存低。

呢度所有嘢都係本機做。冇帳戶、冇雲端同步、冇遙測,亦都完全冇任何網絡請求——連個 QR 都係喺程序入面編碼同畫出嚟,唔係去圖表服務攞返嚟。

Where it lives / 相關檔案

Concern Module
RFC 4226 HOTP and RFC 6238 TOTP, clock assessment app/src/lib/authenticator/totp.ts
RFC 4648 base32 app/src/lib/authenticator/base32.ts
otpauth://totp/ build and parse app/src/lib/authenticator/otpauth-uri.ts
GF(256) and Reed–Solomon app/src/lib/authenticator/galois.ts
ISO/IEC 18004 tables (versions 1–20) app/src/lib/authenticator/qr-tables.ts
QR encoder app/src/lib/authenticator/qr-encode.ts
QR decoder and image sampling app/src/lib/authenticator/qr-decode.ts
Entry records, groups, ordering app/src/lib/authenticator/entries.ts
Credential-vault boundary app/src/lib/authenticator/secret-vault.ts
Git-backed mutation history app/src/lib/stores/authenticator-store.ts
The list surface app/src/ui/md3/md3-authenticator-view.tsx
The registration dialog app/src/ui/md3/md3-authenticator-registration.tsx
The QR renderer app/src/ui/md3/md3-authenticator-qr.tsx
Image, clipboard and camera capture app/src/ui/md3/md3-authenticator-capture.ts
Export serializers app/src/ui/md3/md3-authenticator-export.ts
Styles app/styles/ui/_md3-authenticator.scss
Tests app/test/unit/authenticator-totp-test.ts, app/test/unit/authenticator-qr-test.ts, app/test/unit/authenticator-entries-test.ts, app/test/unit/md3-authenticator-view-test.tsx

Behaviour / 行為

Registration / 登記

The registration dialog accepts six sources, selected from one radio group:

  1. Generate here — a 160-bit secret is generated locally with crypto.randomBytes, drawn as a QR, and shown beside its grouped base32.
  2. Paste a link — an otpauth://totp/ URI the issuer supplied.
  3. Type the secret — base32 by hand, with the algorithm, digits and period set beside it.
  4. Read an image — a saved QR image, decoded on this machine.
  5. Read the clipboard — a QR image, or an otpauth:// link, from the clipboard.
  6. Scan with a camera — where the platform exposes one.

All six converge on the same descriptor and then on the same pairing confirmation. The user types one current code back, and the factor is committed only when it matches (with a one-step tolerance either side, per RFC 6238 §5.2). Without that step a mis-scanned secret is stored perfectly, produces beautifully formatted digits, and is refused by every server on earth — with the first symptom appearing at a login screen and no error anywhere to read.

Editing an existing factor is the same dialog without the sources, without the QR, and without the confirmation: the secret is never re-read or re-shown, only the issuer, account, group and parameters can change.

登記對話框接受六種來源:喺本機整、貼 otpauth:// 連結、自己打 base32、讀圖檔、讀剪貼 簿、用鏡頭掃。六條路最後都行同一個確認:打返一個而家嘅密碼,夾到先至存低。冇呢一步嘅 話,掃錯咗嘅密鑰一樣會靚靚哋存住、出到靚靚哋嘅數字,但全世界都唔收——而你要到登入嗰 陣先發現,仲要冇任何錯誤訊息可以睇。

The QR / 個 QR

The symbol is encoded by encodeQr (byte mode, error-correction level M by default, versions 1–20) and painted as SVG rectangles. Three properties are load-bearing rather than decorative:

The text alternative names the account, the issuer and the parameters, so a screen-reader user is told the QR and the grouped base32 beside it are the same thing.

The list / 個清單

Each row shows the current code in large grouped digits (a read-only text box, so it can be selected by click rather than by a drag-and-hope), a peek at the next code, and a countdown. The countdown always has a text equivalent in seconds and is never colour-only or motion-only. The code region is an aria-live="polite" region carrying the code and nothing else, so it speaks once per time step rather than reading a number at the user every second.

A factor whose secret is not in the credential vault — a restored record whose key is gone, for instance — renders as unable to produce a code and says so, rather than showing a blank cell that reads as a bug.

The list carries what this project asks of every list: a search bar wired to the full regex builder, group chips composed with the query, multi-select by click, shift-click and keyboard, an honestly-scoped select-all, an inverse selection, reordering, grouping through a Move… into group… picker, and bulk delete and export scoped to the selection or the filter.

Clock skew / 時鐘偏差

Codes come from the system clock. When the app can compare that clock against a reference it trusts, assessTotpClock reports the offset and whether it exceeds half a time step — the point past which the server's own ±1-step window stops overlapping. The surface states the exact offset in seconds and says to fix the system time. When nothing has been compared, it says the clock is unverified rather than fine: a check nobody ran is not a check that passed.

There is deliberately no network time lookup. This app makes no runtime network requests, and a silent one to a time server would be exactly that.

密碼係跟系統時鐘計。有得同可信參考對嗰陣,App 會講清楚差咗幾多秒、超唔超出容忍範圍; 未對過就直接講「未驗證」,唔會扮冇事。呢度特登冇去對時伺服器攞時間——因為呢個 App 唔 會喺執行期發任何網絡請求。

Configuration / 設定

Setting Default Provenance
Algorithm SHA1 Shipped default; overridden per factor by an otpauth:// link
Digits 6 Shipped default; 6–8 accepted
Period 30 seconds Shipped default; 1–86 400 accepted
Error-correction level for the pairing QR M Shipped default
Quiet zone 4 modules ISO/IEC 18004 requirement, not a preference

Both the list and the registration dialog carry their explanation behind progressive disclosure and a truthful default-provenance line beside it. The registration dialog distinguishes Default in use from Set by the issuer, so a user can tell whether a parameter came from the app's own defaults or from the link they pasted.

Security considerations / 保安考慮

Failure modes / 失敗情況

Situation What happens
A pasted link is not otpauth://totp/ The dialog names the reason — wrong scheme, counter-based factor, invalid base32, no account name
An image contains no QR "No QR was found in that image."
A QR is found but the grid cannot be read "Hold the code square to the lens and try again." The decoder's geometry is affine: rotation and mild shear are handled, a steep photographic angle is not
A QR is found but too damaged for Reed–Solomon Reported as too damaged, never mis-corrected into a plausible-looking wrong secret
A QR carries Kanji or a structured-append header Reported as unsupported content rather than returning half a URI
The machine has no camera The camera control is disabled and says so, pointing at the image-file route
Camera access is refused Stated plainly, pointing at the image-file route
The pairing code does not match The factor is not stored; the message names both likely causes, the code and the clock
A factor's vault key is gone The row says the factor cannot produce a code and to register it again
The credential vault refuses a delete The record still goes, and the toast names how many secrets are still on the machine
The pairing link is too long to encode The QR is not drawn and the encoder's own message is shown verbatim

Verification / 驗證

The RFC vectors are the acceptance gate, because a subtly wrong authenticator produces confidently formatted digits that every server refuses with no error to read.

Run them with node script/test.mjs app/test/unit/authenticator-totp-test.ts app/test/unit/authenticator-qr-test.ts app/test/unit/authenticator-entries-test.ts app/test/unit/md3-authenticator-view-test.tsx.

Suggested articles / 建議閱讀