Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
// Quick-unlock PIN for the password vault — the one PIN in Theseus. Settings,
// the unlock prompt, Pithos (requestUnlock) and Aegis's PIN pads
// (api.vault.pin) all check it here, against one strike counter.
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
//
// The PIN is an alias for the master password, never a replacement: it
// encrypts the master password (PBKDF2-SHA256 -> AES-256-GCM), and the
// result is sealed again with Electron safeStorage (DPAPI on Windows,
2026-10-04 03:39:14 +02:00
// Keychain on macOS, libsecret on Linux), so a copied vault-pin.json is
// useless on another machine or OS account.
//
// The OS seal does not stop anything that runs as this OS user, nor a disk
Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.
- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
right number of dots and submit on the last digit; a PIN of the wrong
length is refused without a strike, so an older Aegis pad cannot burn the
count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
(6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
offers to save (or update) the login, with an optional "ask for my PIN or
password before filling it". Focusing a login form offers the saved
logins under it; on a locked vault it offers to unlock first. A failed
login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
cookies and site storage of sites with a saved login. Their hostnames are
kept sealed with the OS keystore so the list is readable at quit while
the vault is locked. Verified end to end on a scratch profile: signed in,
restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
// image plus the Windows password; for those a 6-8 digit PIN falls to an
2026-10-04 03:39:14 +02:00
// offline search in minutes. Where a TPM is available the PIN is therefore
// also the authorization value of a TPM key (lib/tpm-pin.cjs) whose secret is
// mixed into the AES key, and the chip's own lockout limits guesses to about
// 144 a day however the file was obtained. Without a TPM the PIN is
// software-only, and status().hardware says so.
//
// Nothing is stored without a real OS keystore: set() refuses, and an
// unsealed record from an older build is deleted. On Linux the basic_text
// backend (a constant key compiled into Chromium) counts as no keystore.
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
//
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
// Five wrong PINs lock the PIN for 15 minutes, and every further wrong PIN
// locks it again (the policy Aegis's PIN screens have always described). The
// master password works throughout, and a correct PIN or a master-password
// unlock clears the count. The counter lives in this file, so a restart does
// not reset it — but anyone who can write the file can, which is why the
// TPM lockout, not this counter, is the limit that matters against an
// attacker on the machine.
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
//
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
// File: { v: 1, sealed: true, data: <b64 safeStorage blob>, fails, last }
Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.
- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
right number of dots and submit on the last digit; a PIN of the wrong
length is refused without a strike, so an older Aegis pad cannot burn the
count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
(6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
offers to save (or update) the login, with an optional "ask for my PIN or
password before filling it". Focusing a login form offers the saved
logins under it; on a locked vault it offers to unlock first. A failed
login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
cookies and site storage of sites with a saved login. Their hostnames are
kept sealed with the OS keystore so the list is readable at quit while
the vault is locked. Verified end to end on a scratch profile: signed in,
restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
// blob = { salt, iv, ct, iters, len?, hw? } (all b64 except iters; len is
// the PIN's digit count, absent = 6 from builds that took only 6)
2026-10-04 03:39:14 +02:00
// hw = { kind: "tpm", key: <TPM key name>, wrapped: <b64> }
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
"use strict" ;
const fs = require ( "node:fs" ) ;
const crypto = require ( "node:crypto" ) ;
2026-10-04 03:39:14 +02:00
const tpmPin = require ( "./tpm-pin.cjs" ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
const MAX _FAILS = 5 ;
const LOCKOUT _MS = 15 * 60 * 1000 ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
const ITERATIONS = 600_000 ;
Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.
- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
right number of dots and submit on the last digit; a PIN of the wrong
length is refused without a strike, so an older Aegis pad cannot burn the
count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
(6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
offers to save (or update) the login, with an optional "ask for my PIN or
password before filling it". Focusing a login form offers the saved
logins under it; on a locked vault it offers to unlock first. A failed
login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
cookies and site storage of sites with a saved login. Their hostnames are
kept sealed with the OS keystore so the list is readable at quit while
the vault is locked. Verified end to end on a scratch profile: signed in,
restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
const PIN _MIN = 6 ;
const PIN _MAX = 8 ;
const PIN _RE = /^\d{6,8}$/ ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
function createVaultPin ( { file , safeStorage , tpm = tpmPin , log = ( ) => { } , now = ( ) => Date . now ( ) } ) {
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
const sealAvailable = ( ) => {
2026-10-04 03:39:14 +02:00
try {
if ( ! safeStorage || ! safeStorage . isEncryptionAvailable ( ) ) return false ;
if ( process . platform === "linux" ) {
const backend = typeof safeStorage . getSelectedStorageBackend === "function" ? safeStorage . getSelectedStorageBackend ( ) : "unknown" ;
if ( backend === "basic_text" || backend === "unknown" ) return false ;
}
return true ;
} catch { return false ; }
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
} ;
2026-10-04 03:39:14 +02:00
let tpmUnavailable = false ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
function read ( ) {
2026-10-04 03:39:14 +02:00
let rec ;
try { rec = JSON . parse ( fs . readFileSync ( file , "utf8" ) ) ; } catch { return null ; }
if ( rec && ! rec . sealed ) {
// Written by a build that stored the blob in the clear when the OS
// keystore was missing. Never use it; drop it.
try { fs . unlinkSync ( file ) ; } catch { }
return null ;
}
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
// Records from the 3-strike build: "master password required" becomes
// one lockout period.
if ( rec && rec . requireMaster ) { rec . fails = Math . max ( rec . fails || 0 , MAX _FAILS ) ; rec . last = rec . last || now ( ) ; delete rec . requireMaster ; }
2026-10-04 03:39:14 +02:00
return rec ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
}
function write ( rec ) {
const tmp = file + ".tmp" ;
fs . writeFileSync ( tmp , JSON . stringify ( rec ) , { mode : 0o600 } ) ;
fs . renameSync ( tmp , file ) ;
}
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
const lockedMsOf = ( rec ) => ( rec && ( rec . fails || 0 ) >= MAX _FAILS ? Math . max ( 0 , LOCKOUT _MS - ( now ( ) - ( rec . last || 0 ) ) ) : 0 ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
function blobOf ( rec ) {
if ( ! rec ) return null ;
if ( ! sealAvailable ( ) ) throw new Error ( "this PIN was sealed by the system keystore, which is not available now" ) ;
return JSON . parse ( safeStorage . decryptString ( Buffer . from ( rec . data , "base64" ) ) ) ;
}
2026-10-04 03:39:14 +02:00
function blobOrNull ( rec ) { try { return blobOf ( rec ) ; } catch { return null ; } }
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
2026-10-04 03:39:14 +02:00
const keyFor = ( pin , salt , iters ) => new Promise ( ( resolve , reject ) =>
crypto . pbkdf2 ( String ( pin ) , salt , iters , 32 , "sha256" , ( e , k ) => ( e ? reject ( e ) : resolve ( k ) ) ) ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
const self = {
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
MAX _FAILS ,
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
LOCKOUT _MS ,
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
status ( ) {
const rec = read ( ) ;
2026-10-04 03:39:14 +02:00
const b = rec ? blobOrNull ( rec ) : null ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
return {
pinSet : ! ! rec ,
fails : rec ? rec . fails || 0 : 0 ,
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
last : rec ? rec . last || 0 : 0 ,
lockedMs : lockedMsOf ( rec ) ,
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
sealed : ! ! ( rec && rec . sealed ) ,
2026-10-04 03:39:14 +02:00
hardware : b ? ( b . hw ? "tpm" : "none" ) : null ,
Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.
- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
right number of dots and submit on the last digit; a PIN of the wrong
length is refused without a strike, so an older Aegis pad cannot burn the
count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
(6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
offers to save (or update) the login, with an optional "ask for my PIN or
password before filling it". Focusing a login form offers the saved
logins under it; on a locked vault it offers to unlock first. A failed
login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
cookies and site storage of sites with a saved login. Their hostnames are
kept sealed with the OS keystore so the list is readable at quit while
the vault is locked. Verified end to end on a scratch profile: signed in,
restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
// So PIN pads can draw the right number of dots and submit on the
// last digit. Knowing it saves an attacker under 12% of the search.
length : b ? b . len || PIN _MIN : null ,
2026-10-04 03:39:14 +02:00
storable : sealAvailable ( ) ,
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
} ;
} ,
// Caller must have verified masterPassword against the vault first.
2026-10-04 03:39:14 +02:00
async set ( pin , masterPassword ) {
Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.
- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
right number of dots and submit on the last digit; a PIN of the wrong
length is refused without a strike, so an older Aegis pad cannot burn the
count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
(6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
offers to save (or update) the login, with an optional "ask for my PIN or
password before filling it". Focusing a login form offers the saved
logins under it; on a locked vault it offers to unlock first. A failed
login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
cookies and site storage of sites with a saved login. Their hostnames are
kept sealed with the OS keystore so the list is readable at quit while
the vault is locked. Verified end to end on a scratch profile: signed in,
restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
if ( ! PIN _RE . test ( String ( pin || "" ) ) ) throw new Error ( ` the PIN must be ${ PIN _MIN } to ${ PIN _MAX } digits ` ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
if ( ! masterPassword ) throw new Error ( "master password required" ) ;
2026-10-04 03:39:14 +02:00
if ( ! sealAvailable ( ) ) throw new Error ( "this system has no protected keystore, so a PIN cannot be stored safely" ) ;
const old = blobOrNull ( read ( ) ) ;
let hw = null ;
if ( tpm . supported ( ) && ! tpmUnavailable ) {
try { hw = await tpm . create ( pin , "Theseus-PIN" ) ; }
catch ( e ) { tpmUnavailable = true ; log ( "vault PIN: no TPM key:" , e ? . message || e ) ; }
}
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
const salt = crypto . randomBytes ( 16 ) ;
const iv = crypto . randomBytes ( 12 ) ;
2026-10-04 03:39:14 +02:00
let key = await keyFor ( pin , salt , ITERATIONS ) ;
if ( hw ) key = tpm . mixKey ( hw . secret , key ) ;
const cipher = crypto . createCipheriv ( "aes-256-gcm" , key , iv ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
const ct = Buffer . concat ( [ cipher . update ( String ( masterPassword ) , "utf8" ) , cipher . final ( ) , cipher . getAuthTag ( ) ] ) ;
Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.
- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
right number of dots and submit on the last digit; a PIN of the wrong
length is refused without a strike, so an older Aegis pad cannot burn the
count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
(6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
offers to save (or update) the login, with an optional "ask for my PIN or
password before filling it". Focusing a login form offers the saved
logins under it; on a locked vault it offers to unlock first. A failed
login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
cookies and site storage of sites with a saved login. Their hostnames are
kept sealed with the OS keystore so the list is readable at quit while
the vault is locked. Verified end to end on a scratch profile: signed in,
restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
const blob = { salt : salt . toString ( "base64" ) , iv : iv . toString ( "base64" ) , ct : ct . toString ( "base64" ) , iters : ITERATIONS , len : String ( pin ) . length } ;
2026-10-04 03:39:14 +02:00
if ( hw ) blob . hw = { kind : "tpm" , key : hw . keyName , wrapped : hw . wrapped } ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
write ( {
v : 1 ,
2026-10-04 03:39:14 +02:00
sealed : true ,
data : safeStorage . encryptString ( JSON . stringify ( blob ) ) . toString ( "base64" ) ,
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
fails : 0 ,
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
last : 0 ,
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
} ) ;
2026-10-04 03:39:14 +02:00
if ( old ? . hw ? . key && old . hw . key !== hw ? . keyName ) tpm . remove ( old . hw . key ) . catch ( ( ) => { } ) ;
return { hardware : hw ? "tpm" : "none" } ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
} ,
clear ( ) {
2026-10-04 03:39:14 +02:00
const old = blobOrNull ( read ( ) ) ;
if ( old ? . hw ? . key ) tpm . remove ( old . hw . key ) . catch ( ( ) => { } ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
try { fs . unlinkSync ( file ) ; } catch { }
} ,
// Returns the master password, or throws:
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
// { code: "no-pin" | "locked" | "wrong-pin" | "tpm-locked" | "pin-gone", remaining, lockedMs }
2026-10-04 03:39:14 +02:00
async open ( pin ) {
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
const rec = read ( ) ;
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
if ( ! rec ) throw Object . assign ( new Error ( "no PIN is set" ) , { code : "no-pin" , remaining : 0 , lockedMs : 0 } ) ;
const locked = lockedMsOf ( rec ) ;
if ( locked > 0 ) throw Object . assign ( new Error ( "too many wrong PINs" ) , { code : "locked" , remaining : 0 , lockedMs : locked } ) ;
Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.
- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
right number of dots and submit on the last digit; a PIN of the wrong
length is refused without a strike, so an older Aegis pad cannot burn the
count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
(6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
offers to save (or update) the login, with an optional "ask for my PIN or
password before filling it". Focusing a login form offers the saved
logins under it; on a locked vault it offers to unlock first. A failed
login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
cookies and site storage of sites with a saved login. Their hostnames are
kept sealed with the OS keystore so the list is readable at quit while
the vault is locked. Verified end to end on a scratch profile: signed in,
restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
// A PIN of the wrong length cannot be right. Refuse it without a
// strike or a TPM attempt: an older Aegis pad that submits at six
// digits would otherwise burn the count against an 8-digit PIN.
const want = ( blobOrNull ( rec ) || { } ) . len || PIN _MIN ;
if ( String ( pin || "" ) . length !== want ) {
throw Object . assign ( new Error ( ` your PIN has ${ want } digits ` ) ,
{ code : "wrong-length" , length : want , remaining : Math . max ( 0 , MAX _FAILS - ( rec . fails || 0 ) ) , lockedMs : 0 } ) ;
}
2026-10-04 03:39:14 +02:00
// Count the guess before trying it, so a crash mid-check still costs one.
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
rec . fails = ( rec . fails || 0 ) + 1 ;
rec . last = now ( ) ;
2026-10-04 03:39:14 +02:00
write ( rec ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
let masterPassword = null ;
if ( PIN _RE . test ( String ( pin || "" ) ) ) {
try {
const b = blobOf ( rec ) ;
2026-10-04 03:39:14 +02:00
let secret = null ;
if ( b . hw ) {
const r = await tpm . open ( b . hw . key , b . hw . wrapped , pin ) ;
if ( r . ok ) secret = r . secret ;
else if ( r . code === "locked" || r . code === "error" ) {
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
// Not a verdict on the PIN: give this one attempt back.
const cur = read ( ) ;
if ( cur ) { cur . fails = Math . max ( 0 , ( cur . fails || 0 ) - 1 ) ; write ( cur ) ; }
2026-10-04 03:39:14 +02:00
throw Object . assign ( new Error ( r . code === "locked"
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
? "The security chip is refusing PINs for a few minutes after too many wrong ones. Use the master password, or wait."
: "The security chip did not answer. Use the master password." ) , { code : "tpm-locked" , remaining : MAX _FAILS - ( rec . fails - 1 ) , lockedMs : 0 } ) ;
2026-10-04 03:39:14 +02:00
} else if ( r . code === "missing" ) {
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
self . clear ( ) ;
throw Object . assign ( new Error ( "This PIN was tied to a security chip that no longer has its key. Enter the master password, then set the PIN again." ) , { code : "pin-gone" , remaining : 0 , lockedMs : 0 } ) ;
2026-10-04 03:39:14 +02:00
}
}
if ( ! b . hw || secret ) {
const ct = Buffer . from ( b . ct , "base64" ) ;
let key = await keyFor ( pin , Buffer . from ( b . salt , "base64" ) , b . iters ) ;
if ( b . hw ) key = tpm . mixKey ( secret , key ) ;
const decipher = crypto . createDecipheriv ( "aes-256-gcm" , key , Buffer . from ( b . iv , "base64" ) ) ;
decipher . setAuthTag ( ct . subarray ( ct . length - 16 ) ) ;
masterPassword = Buffer . concat ( [ decipher . update ( ct . subarray ( 0 , ct . length - 16 ) ) , decipher . final ( ) ] ) . toString ( "utf8" ) ;
}
} catch ( e ) {
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
if ( e && ( e . code === "tpm-locked" || e . code === "pin-gone" ) ) throw e ;
2026-10-04 03:39:14 +02:00
masterPassword = null ;
}
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
}
if ( masterPassword == null ) {
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
const cur = read ( ) || rec ;
const remaining = Math . max ( 0 , MAX _FAILS - ( cur . fails || 0 ) ) ;
throw Object . assign ( new Error ( remaining ? "wrong PIN" : "too many wrong PINs" ) ,
{ code : remaining ? "wrong-pin" : "locked" , remaining , lockedMs : lockedMsOf ( cur ) } ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
}
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
const cur = read ( ) || rec ;
cur . fails = 0 ; cur . last = 0 ; write ( cur ) ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
return masterPassword ;
} ,
// A successful master-password unlock clears the strikes.
resetFails ( ) {
const rec = read ( ) ;
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
if ( rec && ( rec . fails || rec . last ) ) { rec . fails = 0 ; rec . last = 0 ; write ( rec ) ; }
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
} ,
} ;
Theseus: one PIN — the vault PIN, offered to Aegis through api.vault.pin
Theseus and Aegis each wrapped the same master password under their
own PIN: two offline targets, two guess budgets, and two PINs to keep
in step. The vault PIN is now the only one. Built-in add-ons get
api.vault.pin {status, unlock, set, clear} (advertised by
features.vaultPin); unlock(pin) opens the vault in main and answers
only { ok } or why not, so the master password stays in main.
The policy is the one Aegis's PIN screens describe: five wrong PINs
lock the PIN for 15 minutes, every further wrong one locks it again,
and the master password always works. The unlock prompt uses the same
PIN pad and the same wording as Aegis, and Settings says so.
2026-10-04 04:15:15 +02:00
return self ;
Theseus: quick-unlock PIN for the vault, shared with extensions
The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
2026-10-03 20:33:26 +02:00
}
Vault: PIN setup steps, 6-8 digit PINs, save and offer logins, keep sign-ins
The PIN could only be six digits and was set from three bare inputs; the
unlock prompt sat at the top of the page; and the password manager only
filled when you found the key chip, never offered to save, and "Clear
cookies on quit" signed you out of every site, including the ones whose
login the vault already holds.
- PINs are 6 to 8 digits. The PIN record stores its length so pads draw the
right number of dots and submit on the last digit; a PIN of the wrong
length is refused without a strike, so an older Aegis pad cannot burn the
count against an 8-digit PIN.
- Settings sets a PIN in steps: master password, choose the PIN on a pad
(6/7/8), repeat it, done. The locked vault opens Theseus's own prompt,
which is now centred, with the PIN pad or the master password field.
- After a sign-in or sign-up form is sent and the page moves on, Theseus
offers to save (or update) the login, with an optional "ask for my PIN or
password before filling it". Focusing a login form offers the saved
logins under it; on a locked vault it offers to unlock first. A failed
login (the password field still showing) gets no offer.
- "Keep sign-ins for sites in your vault" (on): the quit clear spares the
cookies and site storage of sites with a saved login. Their hostnames are
kept sealed with the OS keystore so the list is readable at quit while
the vault is locked. Verified end to end on a scratch profile: signed in,
restarted, still signed in; another site's cookie was cleared.
2026-10-04 20:23:43 +02:00
module . exports = { createVaultPin , MAX _FAILS , LOCKOUT _MS , PIN _MIN , PIN _MAX } ;