- Profiles are independent: each profile has its own settings, filters, and optional PIN.
- If a profile is PIN-protected, its UI must stay locked (no restricted views rendered) until the correct PIN is entered.
- If a profile has no PIN, it must be fully accessible.
- A hard refresh of the New Tab UI must re-lock any PIN-protected profile (no persisted unlock).
- Popup UI and New Tab UI may behave independently for unlock state unless we explicitly implement a secure shared session.
- Profile: An entry inside
ftProfilesV4.profiles[profileId]. - Default (Master): The
profileId === "default"profile. - Account profile:
type: "account"(or inferred account when notypeand noparentProfileId). - Child profile:
type: "child"(or inferred child whenparentProfileIdis present). - Locked profile: A profile is considered locked if it has a PIN verifier.
- Master:
profiles.default.security.masterPinVerifier - Non-default:
profiles[profileId].security.profilePinVerifier
- Master:
- Unlocked session state (UI-local):
- Stored in-memory in the UI context (
unlockedProfilesSet). - Not persisted to
storage. - Reset on hard refresh / tab close.
- Stored in-memory in the UI context (
- PIN crypto artifacts:
- Verifier: PBKDF2-SHA256 with 150k iterations + random salt → stored as
{kdf, hashAlg, iterations, salt, hash}. - Encryption container: PBKDF2-SHA256 (150k) → AES-GCM (random IV) with
{kdf, cipher, data}payload.
- Verifier: PBKDF2-SHA256 with 150k iterations + random salt → stored as
{
"schemaVersion": 4,
"activeProfileId": "default",
"profiles": {
"default": {
"type": "account",
"parentProfileId": null,
"name": "Default",
"settings": {
"enabled": true,
"syncKidsToMain": false,
"autoBackupEnabled": true,
"autoBackupMode": "latest",
"autoBackupFormat": "auto",
"...": "other per-profile settings (hideShorts, hideComments, etc.)"
},
"main": {
"channels": [],
"keywords": []
},
"kids": {
"mode": "blocklist",
"strictMode": true,
"blockedChannels": [],
"blockedKeywords": []
},
"security": {
"masterPinVerifier": { "kdf": "pbkdf2-sha256", "iterations": 150000, "salt": "...", "hash": "..." }
}
},
"child1": {
"type": "child",
"parentProfileId": "default",
"name": "Kiddo",
"settings": { "...": "same shape as above" },
"main": { "...": "profile-specific main settings" },
"kids": { "...": "kids profile data" },
"security": {
"profilePinVerifier": { "kdf": "pbkdf2-sha256", "iterations": 150000, "salt": "...", "hash": "..." }
}
}
}
}Key points:
- Settings are per-profile; feature flags (e.g., hideShorts) live under the active profile’s
settings. - Auto-backup flags live per profile (
autoBackupEnabled,autoBackupMode,autoBackupFormat). - PIN verifiers live under
security(masterPinVerifierfor Default;profilePinVerifierfor others). activeProfileIdcontrols UI scoping and export scope.
-
If active profile is locked and not unlocked in this UI session:
- Allowed views:
help,whatsnew,support
- Blocked views:
- Everything else (Dashboard / Filters / Kids / Semantic / Settings / etc.)
- Expected behavior:
- Blocked views do not render.
- Navigation to a blocked view is redirected to
help.
- Allowed views:
-
If active profile is unlocked (correct PIN entered in this UI session):
- All views for that profile are accessible.
-
If active profile has no PIN:
- All views are accessible.
- Switching to a profile requires:
- Only the target profile’s PIN, if the target profile is PIN-protected.
- No Master PIN requirement for switching across profiles.
- Type is treated as a grouping/metadata concept for UI (and possible future policy), not an access restriction by itself.
- If
profile.typeis missing:- If
parentProfileIdexists => inferred aschild - Else => inferred as
account
- If
- When the UI is locked:
- Mutating actions (adding/removing channels/keywords, toggling settings) are blocked.
- StateManager also no-ops mutating operations while locked (defense-in-depth).
- This includes the popup header
Enabled / Disabledbrand toggle; it must not be able to pause filtering while a PIN-protected profile is still locked.
- Unlock state is currently local-only to each UI context.
- Popup unlock does not unlock New Tab automatically.
- New Tab unlock does not unlock popup automatically.
- The top-bar profile selector in New Tab stays available while locked so the user can switch to a different profile, but the switch still requires the target profile PIN only when that target is protected.
- If a PIN prompt is denied/cancelled during profile switching, the surface must refresh its selector/badge/lock UI back to the true active profile state instead of leaving stale selection UI behind.
- Background session PIN cache:
- UIs send
FilterTube_SessionPinAuthwith the PIN; background verifies against stored verifier and keeps a memory-only cache per profile. - UIs can clear it with
FilterTube_ClearSessionPin(on lock/Logout). - Auto-backup encryption depends on this cache (see below); if missing, encrypted auto-backup is skipped.
- UIs send
- Import/restore is Master-only (active profile must be
default; Master PIN must be unlocked if set). - Full export is Master-only; non-default exports are forced to “active profile only”.
- Account policy controls (allow account creation, max accounts) are Master-only and disabled elsewhere.
- When a profile is locked, StateManager and UI both block mutations (defense-in-depth).
- Create/Update PIN:
- UI collects PIN →
FilterTubeSecurity.createPinVerifier(pin)→ PBKDF2-SHA256 (150k, random salt) → verifier stored inprofiles[*].security.
- UI collects PIN →
- Verify PIN:
- UI sends PIN to background via
FilterTube_SessionPinAuth. - Background loads
ftProfilesV4, picks the correct verifier (master vs profile), runsFilterTubeSecurity.verifyPin, and only then caches the PIN insessionPinCache(memory only, per-profile).
- UI sends PIN to background via
- Encrypt export/backup:
- If format is
encryptedorauto+profile has PIN, background requires a cached session PIN. - Payload is wrapped as
{ meta: { encrypted: true, ... }, encrypted: { kdf: PBKDF2-SHA256 150k + salt, cipher: AES-GCM + iv, data: b64 } }.
- If format is
- Decrypt import:
- UI (tab view) prompts for password/PIN, calls
FilterTubeSecurity.decryptJson(encrypted, password), then imports the decrypted payload through the normal import path.
- UI (tab view) prompts for password/PIN, calls
sequenceDiagram
participant UI
participant BG as Background
participant Sec as FilterTubeSecurity
UI->>Sec: createPinVerifier(pin)
Sec-->>UI: verifier (PBKDF2-SHA256 150k)
UI->>BG: save verifier in profilesV4.security
UI->>BG: FilterTube_SessionPinAuth(pin, profileId)
BG->>Sec: verifyPin(pin, verifier)
Sec-->>BG: ok?
BG-->>BG: sessionPinCache[profileId] = pin (mem-only)
BG->>Sec: encryptJson(payload, pin) (when backup/export encrypts)
Sec-->>BG: {kdf, cipher, data}
BG-->>UI: encrypted backup file
Some actions are intentionally limited to the Default (Master) profile UI context.
-
Account policy controls (e.g. allow account creation / max accounts):
- Only editable while
activeProfileId === "default". - If Master has a PIN, Master must be unlocked first.
- Only editable while
-
Creating independent accounts:
- Initiated from Default (subject to account policy).
Note: This does not affect a parallel account’s ability to access its own Filters/Kids/Settings when it has no PIN or after it is unlocked.
- Import/restore UI is gated to Default (Master) context. If Default has a PIN, it must be unlocked first.
- Non-default profiles cannot import/restore (prevents overwrite by children).
- If the active profile is a locked child profile, Nanah behaves as a replica-only surface for that UI session.
- If the active profile is an unlocked child profile, it may send that child profile's own scoped settings (
active,main,kids) to another device, but it still cannot send afullaccount backup from that surface. - Locked child profiles cannot save arbitrary peer/source trust.
- Locked child profiles can only save managed
source -> replicalinks. - Core backup/import/export/account-admin controls are disabled for child profiles in the desktop UI.
- Unlocked child surfaces also block profile rename/delete/PIN mutation and trusted parent-link edit/remove from Accounts & Sync.
- Saved Nanah trust is still per device link, not per entire machine.
- Managed reconnect can require local approval per session on the child/replica side through the link's
reconnectMode. - Live sessions now exchange the receiver's profile inventory, and the sender can choose a specific Remote target profile during the session.
- Managed links can also pin the receiver-side target to one fixed local profile for later sessions.
- First managed parent -> child connection may require one local parent approval on the child device.
- After that, the child does not always need to press allow.
- If the saved link uses easier parent-control settings, later matching updates can apply more directly.
- If the saved link uses stricter child protection, the child device must stop for approval more often.
Short version:
standard parent control= easier later updatesstrict child protection= more approval steps on the child device
- Auto-backup reads settings from the active profile’s
settingsobject whenftProfilesV4is present. - Backup output behavior:
- If active profile is
default: export type isfulland includesprofilesV4. - If active profile is not
default: export type isprofileand includes only the active profile insideprofilesV4.
- If active profile is
- Destination:
Downloads/FilterTube Backup/<ProfileName>/FilterTube-Backup-<timestamp>.json
- Rotation:
- Rotates per profile folder.
- Enablement:
profilesV4.profiles[activeId].settings.autoBackupEnabled === true
- Modes:
latest: overwritesFilterTube-Backup-Latest.json|encrypted.jsonhistory: writes timestamped files and rotates within the profile folder
- Format:
auto: encrypt if profile has a PIN verifierplain: always plaintext JSONencrypted: always encrypted container
- Encryption gating:
- Requires session PIN cached in background; if absent and encryption is needed, backup is skipped (safe default).
flowchart TD
UI[UI (Popup/Tab)] -->|PIN| BGAuth[FilterTube_SessionPinAuth\n(verifies PIN, caches in memory)]
BGAuth --> Cache[Session PIN cache (mem only)]
Auto[Auto-backup scheduler] --> Build[buildAutoBackupPayload\n(scope = active profile)]
Build --> DecideFmt{Format?}
DecideFmt -->|plain| SavePlain[write JSON]
DecideFmt -->|encrypted| NeedPIN{Session PIN?}
NeedPIN -->|no| Skip[skip backup (safe default)]
NeedPIN -->|yes| Encrypt[PBKDF2-SHA256 150k + AES-GCM]
Encrypt --> SaveEnc[write encrypted JSON container]
SavePlain --> Rotate[rotate per-profile folder (if history mode)]
SaveEnc --> Rotate
- Per-profile enablement:
profilesV4.profiles[activeId].settings.autoBackupEnabled. - Mode (
profilesV4.profiles[activeId].settings.autoBackupMode):latest: overwrites a single file per profile (FilterTube-Backup-Latest.*).history: keeps timestamped backups (rotates per profile folder).
- Format (
profilesV4.profiles[activeId].settings.autoBackupFormat):auto: encrypt if the active profile has a PIN, otherwise plain JSON.plain: always write plain JSON.encrypted: always write encrypted backup containers.
- If auto-backups are encrypted, the background uses an in-memory, session-only PIN cache.
- The cache is populated only when an extension UI page sends the PIN and the background verifies it against the stored verifier.
- If the profile is PIN-protected but no session PIN is available, auto-backup is skipped (best-effort).
- Creation surfaces: Tab view “Profiles” card (
ftCreateAccountBtn/ftCreateChildBtn); Default (Master) must allow account creation and stay withinmaxAccounts. - Types:
account(or inferred when noparentProfileId).child(explicit type or inferred whenparentProfileIdis set).
- Parent linkage: Children carry
parentProfileId; policies can later use this to gate cross-account switches. - Lock gate: Any profile with a PIN stays locked until correct PIN entered; locked state blocks restricted views and all mutations (StateManager defense-in-depth).
- Import/export scope: Default can export/import full; non-default exports/imports active-only; encrypted imports require the correct password/PIN.
- Session security: Unlock is UI-local; background only trusts a PIN once verified; cache is memory-only (cleared on profile lock/clear action).
- Tamper resistance:
This is the stricter product interpretation we should follow when parent-control behavior matters more than single-user convenience.
MASTER-CENTERED EXAMPLE
[Default / Master]
|
+-- [Independent Account A]
| |
| +-- [Child A1]
| +-- [Child A2]
|
+-- [Independent Account B]
| |
| +-- [Child B1]
|
+-- [Master-owned Child M1]
LOCAL RULE
The active profile and its lock state in the current UI session decide what Nanah is allowed to do.
[Active = account] -> peer or source or replica allowed
[Active = locked child] -> replica-only for Nanah
[Active = unlocked child] -> may send that child profile's own scoped snapshot
- Owns full import/export and restore
- Owns account policy
- Can create independent accounts
- Can still own its own child profiles
- Is the only place where broad system-level recovery and migration should happen
- Owns its own settings, filters, and optional PIN
- Can create and manage its own child profiles
- Does not automatically gain authority over sibling accounts
- Can become a Nanah
sourcefor its own managed child devices
- Should be treated as a managed surface, not a free admin surface
- Should not be a broad Nanah sender
- Should not own full import/export or account-management actions
- Should be the profile type most likely to receive parent/source updates through trusted managed links
For Nanah, child profiles should default to replica behavior while locked:
- a child profile may receive managed updates from a trusted parent/source
- a locked child profile should not behave like a normal
source - a child profile should not be used as an equal
peerauthority surface for persistent trust
That keeps the child device useful for parent control without turning it into its own admin endpoint.
Important scope note:
- this rule is currently based on the active profile and local lock state in the Accounts & Sync UI session
- if the active profile is a locked child profile, Nanah behaves replica-only for that session
- if the active profile is an unlocked child profile, Nanah may send that child profile's own scoped snapshot
- this is not yet a broader device-wide “child mode” outside explicit profile context
+----------------------+---------------------------+-------------------------------+
| Active profile type | Nanah send | Nanah trust |
+----------------------+---------------------------+-------------------------------+
| Default / Master | yes | peer or managed |
| Independent account | yes | peer or managed |
| Locked child | no | managed replica only |
| Unlocked child | scoped send only | managed replica only |
+----------------------+---------------------------+-------------------------------+
+----------------------+-----------------------------------------------+
| Active profile type | Settings/admin actions |
+----------------------+-----------------------------------------------+
| Default / Master | full backup/import/export + account policy |
| Independent account | local settings + child management |
| Child | no broad backup/import/export/admin actions |
+----------------------+-----------------------------------------------+
Two rules must stay true at the same time:
- Kids must not learn or enter the parent PIN for ordinary managed updates.
- The parent must still be able to keep the child device aligned.
- PIN values stay local
- locked profiles block restricted views and mutations
- Nanah send/apply paths can require local unlock
- trusted managed child links may optionally save a local
Allow trusted updates while lockedrule on the child device
Trusted managed source -> replica links can now save one extra local child-side rule:
Require local unlockAllow trusted updates while locked
That rule is:
- local to the child device
- created only after local approval on the child device
- limited to trusted
source -> replicamanaged links - limited to the saved managed scopes/policy
- limited to child-safe scopes (
active,main,kids), notfull - not equivalent to sharing the child PIN with the parent device
Default remains conservative:
- child profile stays locked
- parent update still requires the child profile to be locally unlocked
Only if the child device explicitly saves Allow trusted updates while locked may matching managed updates apply without unlocking that child profile each time.
CURRENT SAFE RULE
[Parent / Source] ---> [Child / Replica]
|
+-- PIN never sent
+-- allowed scope only
+-- policy must be saved locally on child device
CURRENT CHILD-LINK RULE
[Child locked]
|
+-- default: require local unlock
|
+-- optional saved child-side rule:
allow trusted managed updates while locked
Current behavior:
- normal FilterTube backup/export restores profile data
- Nanah trusted-link records are stored separately in local extension storage
- uninstall/reinstall currently loses trusted-link state unless the link is rebuilt manually
- saved Nanah trust does not keep a live background connection open; it only remembers policy for later live sessions
Recommended rule:
- do not put trusted links into casual plaintext backup/export
- if trusted-link restore is added later, prefer an explicit encrypted admin-grade restore flow
- PIN verification happens in background (UI cannot “claim” unlock).
- No persisted unlock tokens; reload resets lock state.
- Encrypted backups require the verified PIN at creation time; imports require the correct password/PIN to decrypt.
[Normal backup/export]
|
+-- profiles
+-- lists
+-- settings
+-- encrypted or plain
|
+-- NOT trusted-link graph
[Trusted-link storage]
|
+-- local extension storage only
+-- lost on uninstall today
+-- future restore, if any, should be encrypted and admin-only
DEVICE 1: [A] [B*]
DEVICE 2: [A*] [B] [C]
Send from Device 1 while B is active:
active -> reads B, writes into active profile on Device 2 = A
main -> reads B.main, writes into A.main
kids -> reads B.kids, writes into A.kids
full -> whole-account import path
So today:
B -> A can happen
B -> B does NOT happen automatically
Trusted managed source -> replica links can now pin one receiver-side local profile.
[Parent / Source]
|
+-- managed link policy -->
target on child device:
- current active profile
- OR fixed local profile
Example:
Parent sends from profile B
Child device currently has A active
Managed link target is fixed to local profile B
-> incoming update lands in child-device profile B
-> child-device active profile does not need to switch to B
Rules:
- This target mapping is explicit and local to the receiver-side trusted link.
- It is not guessed automatically from profile names.
- Child managed links should prefer a fixed local target so parent control stays predictable.
- Default can export full or active-only.
- Non-default profiles are forced to export active-only (current UI behavior).
- Manual import is allowed only when the active profile is Default (and unlocked if PIN-protected).
- Option A (current): strict local-only unlock.
- Option B (future): shared unlock within the extension session, but only if background verifies PIN (do not trust “unlocked” claims from UIs).
Currently: child is mostly UI grouping.
Possible future policies (not implemented):
- Disable export / disable backup in child profiles.
- Always restrict certain tabs in child profiles unless a parent/admin action occurs.
- Align New Tab profile selector UX with popup dropdown.
- Grey/disable admin-only actions consistently based on lock state and active profile.
- [Done] Strict lock gate in New Tab UI (no restricted views rendered while locked).
- [Done] Profile switching prompts only for the target profile PIN (no Master PIN required just to switch).
- [Done] Mutations blocked while locked (UI + StateManager defense-in-depth).
- [Done] Auto-backup per-profile enable/mode/format + encrypted auto-backup using background-verified session PIN cache.
- [Done] New Tab UI exposes auto-backup mode/format controls.
- [Done] Dashboard keyword/channel counts update on profile switch.
- [Done] Popup enabled/disabled header toggle now respects the locked-profile mutation gate.
- [Done] New Tab profile badge/dropdown now stays aligned with popup’s guarded switch contract, including denied/cancelled PIN refresh behavior.
- [In progress] Standardize dropdown styling across all UIs (Sort/Date/Settings/selects).
- [In progress] Standardize profile selector styling/colors across Popup + New Tab (shared accent colors).
- [Done] Gate import/restore behind Default (Master) UI context (best-effort safety).
- Encrypted backups intentionally keep a small plaintext
metasection (version/app/exportType/profile label +encrypted: true). - This metadata does not enable decryption.
- Decryption security comes from:
- PBKDF2-SHA256 (150k iterations, per-backup random salt)
- AES-GCM (random IV)
- The biggest real-world risk is weak PINs/passwords (e.g. 4-digit PINs) because backups can be brute-forced offline.
-
Profile has no PIN:
- Can open Dashboard/Filters/Kids/Semantic/Settings.
- Can edit settings/lists.
-
Profile has PIN, not yet unlocked in this session:
- Cannot access Dashboard/Filters/Kids/Semantic/Settings.
- Can access Help/What’s New/Support.
- Hard refresh keeps it locked.
-
Profile has PIN, unlocked in this session:
- Can access all views.
- Can edit settings/lists.
-
Switching profiles:
- Only prompts for the target profile’s PIN (if any).
- Does not require Master PIN just to switch.