Desktop Material

Surface locks / 版面鎖

A for-fun password or one-time-password speed bump on a tab, a tab group, or any appearance value. Off by default, opt-in per surface, and never described as security.

一個純粹好玩嘅路障:可以喺分頁、分頁群組或者任何外觀數值上面加個密碼/一次性密碼。 預設冇開,逐個版面自己揀,永遠唔會當佢係保安。


What it is, and what it deliberately is not

A lock makes a surface ask for a credential before it opens. That is all it does.

The design goal is the same as School mode's: a self-imposed speed bump the user chose, with an obvious and self-service way out.

Behaviour

Where a lock is created

From the surface's own context menu, beside Edit tab appearance…. The menu items are built by buildMd3LockMenuItems in app/src/ui/md3/md3-lock-menu-items.ts and spliced into the menu the surface already has, so they carry the same filter field and keyboard operation as every other entry in that menu. Each item shows the shortcut that actually works in that context (Shift+Cmd+L to lock, Shift+Cmd+K for the manager), and the keyboard route to the menu itself is the surface's existing one.

Selecting Lock this tab… opens Md3LockSetupDialog: an anchored, non-modal panel beside the control that asked for it, never a detached dialog.

One lock, one credential

Locks do not inherit and do not share.

Factors

Factor Answered by Stored where
Password A password the user chooses for that one lock A per-lock random salt and a SHA-256 digest, in the operating-system credential vault
One-time password The current code from the app's own authenticator Nothing extra — the lock records only the authenticator entry id, and the secret stays where the authenticator put it

The OTP factor consumes app/src/lib/authenticator/totp.ts through the adapter in lock-totp-authenticator.ts. There is exactly one RFC 6238 implementation in the app, and this feature does not add a second. When no authenticator is registered the OTP choice is disabled and states the unmet condition, rather than sitting greyed out with no explanation.

Unlocking

Activating a locked surface opens Md3LockUnlockPrompt: anchored beside the control that was clicked, non-modal, and returning focus to that control when it is cancelled or dismissed with Esc.

The prompt always shows, at every funny level and in every language mode:

  1. The lead sentence, which the funny level styles.
  2. The credential field for that lock's factor and nothing else.
  3. The unlock duration — this surface only, for N minutes, or until the app closes — defaulting to whatever the lock was configured with.
  4. The fixed honesty line saying this is just for fun and is not security. No funny level styles this sentence.
  5. The recovery sentence, naming the real application-data folder.
  6. A Forgotten your password? link to Support Tickets.

A successful unlock raises a non-blocking toast and grants an IMd3ActiveUnlock. Unlocks live in memory only: they never survive a restart, and a lock whose lockOnLaunch is set (the default) is locked again at start-up.

Every row in the lock manager offers Lock again while its unlock is live, and the same command appears in the surface's context menu.

A wrong answer

A wrong answer says so, states how many consecutive wrong answers there have been, and names the recovery route. It never wipes content, never escalates, and never claims a lockout is enforcement.

The first two wrong answers cost nothing, because the overwhelmingly common case is a typo. From the third, the next attempt waits — 5 s, then 15 s, then 30 s — with the remaining seconds counted down visibly on the prompt. The ledger is held in memory only: persisting it would turn a typo into a delay that survives a restart, which would be enforcement rather than a speed bump. A match clears it.

Recovery

Deleting the app's local application-data folder removes every lock on the machine. The exact path is shown on the setup dialog and on the unlock prompt — the two places a user will be looking for it — and when the path cannot be resolved the copy says so instead of inventing one. Forgetting a toy lock's credential is a normal outcome, so the route is documented and self-service; a lock is never the only thing standing between a user and their own content.

The Forgotten your password? link routes to Support Tickets, which opens the folder in the platform's file manager. The app never deletes the folder for the user.

The lock manager

Md3LocksView lists every lock in the app. It carries what this project asks of every list:

Each row shows what the lock covers, which factor answers it, when it was made, whether it is open right now, and whether it re-locks on launch. It shows nothing whatsoever about the credential.

A locked tab or property still appears in the tab searches, the settings search and the command palette, labelled as locked by md3LockedResultLabel. Selecting one prompts to unlock rather than teleporting past the lock or silently doing nothing.

excludeLockedFromBulkClose holds locked tabs back from a bulk close by default, exactly as pinned tabs are, and reports how many were held back. An explicit includeLocked closes them too — and still states how many locked tabs it swept up, so an inclusive close is never silently identical to an exclusive one.

Configuration

Value Default Where it lives
Lock exists on a surface none desktop-material-surface-locks-v1 in the profile's local storage
Factor Password Per lock
Unlock duration 10 minutes Per lock, overridable per unlock
Lock again on launch On Per lock

The setup dialog carries its explanation behind progressive disclosure and a truthful provenance line beside the duration: it says whether the current value is the shipped default (naming the real value) or something saved for this particular lock.

Security considerations

The word "security" appears here to describe what this feature is not, and to describe how its credential handling behaves anyway.

Failure modes

Situation Behaviour
The credential vault is unavailable The lock is not saved, the vault's own error is reported verbatim, and the lock record written a moment earlier is removed
The vault holds no credential for a lock The unlock prompt reports the lock cannot be checked and points at the recovery route. Nothing is held against the user
No authenticator is registered The OTP factor is disabled with the unmet condition named. An existing OTP lock reports unavailable rather than counting a failure
A stored OTP secret cannot be decoded Verification fails closed and says nothing about the value
The lock document is corrupt Reading yields an empty list rather than throwing; individual malformed entries are dropped without taking their siblings with them
A persisted lockOnLaunch is missing or malformed Fails closed: the surface locks again on launch
The application-data path cannot be resolved The recovery sentence says the exact path could not be read, and does not invent one
Support Tickets is not registered The link says plainly that it is not wired up in this build and points back at the folder. It never appears to work and silently do nothing

Accessibility

Verification

node script/test.mjs app/test/unit/md3-locks-test.ts app/test/unit/md3-locks-view-test.tsx

Six guard-shaped assertions were verified by breaking the thing they guard, watching the test go red, and restoring it:

Guard Broken by Result
Every declared appearance value type has a lockable property Deleting the only preset property red
Every surface that creates or answers a lock renders the honesty line Removing the line from the unlock prompt red
No md3.locks.* string claims a lock secures/protects/encrypts Adding "which protects the tab" to a menu label red
Every funny band exists in both catalogues Deleting one English band entry red
No export writes anything that could open a lock Adding a digest column red
Bulk removal is behind the two-key gate Enabling the slider without both keys red

Not yet wired

Two joins are left to the surrounding shell and are named here rather than left silent:

Until each is installed, the affected control says exactly what is missing rather than failing quietly.

Suggested articles