mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
The review found the store key readable from a copy of the disk. The desktop app only added its OS layer with the lock on, the record from before a lock stayed in LevelDB and opened the mail with no PIN, any frame could unwrap the PIN record and guess at it, a lock left the key in the renderer's memory, and a store waiting after a refused login went to whoever signed in next under the same username@host. - The desktop app wraps the store key with safeStorage, lock or no lock. A record without the layer gets it at its next open. A refusal is said out loud, and only a computer with no key store goes without, which settings say. So do they for a browser, which has no such layer. - A record that gained layers, and every record after a changed PIN, asks for a new key: `sqlite3_backup` into a new file under a new key while the store keeps working, one write to the record, then the old file goes. A changed PIN makes a new lock key, since the old PIN's record stays on disk too. The passkey and OS unlock go with the old key. - `wrap` and `unwrap` come through `claim()`, and the key under the PIN takes a secret from main, one counted trip per guess. - A lock on the desktop ends the renderer rather than reload in it. - A sign-in compares the waiting store's JMAP account and server first, and wipes on a mismatch. The account id keeps its form: every store, setting and server record of an account is keyed by it. - The 30 days also run in an app that stays open. Changes an existing assertion: turning the lock off now keeps the safeStorage layer, by decision, so that test expects it to stay.
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
/**
|
|
* The calls only the page itself may make to the desktop app's main process. A same-origin frame
|
|
* reaches `bulwarkDesktop` through `parent`, and main can't tell its calls from the page's. So the
|
|
* preload hands these out once per document, and the page takes them in its first render, before
|
|
* any frame exists. Imports next to nothing, for lib/app-lock/vault.ts and controller.ts alike.
|
|
*/
|
|
|
|
import { getDesktopBridge } from '@/lib/desktop/bridge';
|
|
import type { DesktopAppLockBridge, DesktopAppLockControls } from '@/lib/desktop/types';
|
|
|
|
let claimedFrom: DesktopAppLockBridge | null = null;
|
|
let claimed: DesktopAppLockControls | null = null;
|
|
|
|
export function shellControls(): DesktopAppLockControls | null {
|
|
const member = getDesktopBridge()?.appLock ?? null;
|
|
if (member !== claimedFrom) {
|
|
claimedFrom = member;
|
|
claimed = member?.claim?.() ?? null;
|
|
}
|
|
return claimed;
|
|
}
|
|
|
|
/** A shell older than the webmail it loads has these on the bridge. */
|
|
function olderShell(): Pick<DesktopAppLockControls, 'wrap' | 'unwrap'> {
|
|
return (getDesktopBridge()?.appLock ?? {}) as unknown as Pick<DesktopAppLockControls, 'wrap' | 'unwrap'>;
|
|
}
|
|
|
|
export function shellWrapCall(): DesktopAppLockControls['wrap'] {
|
|
return shellControls()?.wrap ?? olderShell().wrap;
|
|
}
|
|
|
|
export function shellUnwrapCall(): DesktopAppLockControls['unwrap'] {
|
|
return shellControls()?.unwrap ?? olderShell().unwrap;
|
|
}
|
|
|
|
/**
|
|
* Whether this computer has a real key store the app can use. No: a browser, or Linux with no secret
|
|
* service, and settings say so. A shell that doesn't answer isn't a no: this rejects, so nobody
|
|
* writes a key without the layer because of it.
|
|
*/
|
|
export async function shellHasWrap(): Promise<boolean> {
|
|
const member = getDesktopBridge()?.appLock;
|
|
if (!member || !shellWrapCall()) return false;
|
|
return Boolean((await member.status()).wrap);
|
|
}
|
|
|
|
/** Tests only. */
|
|
export function resetShellForTests(): void {
|
|
claimedFrom = null;
|
|
claimed = null;
|
|
}
|