mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 16:16:49 +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.
50 lines
1.9 KiB
TypeScript
50 lines
1.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { getDesktopBridge } from '@/lib/desktop/bridge';
|
|
import { useOfflineStore } from '@/lib/offline/state';
|
|
|
|
/**
|
|
* What protects the key to the mail on this device, where that's less than one would think, and
|
|
* how a re-encryption is getting on. `browser`: also says what a browser can't do. The app lock's
|
|
* own settings have said that already.
|
|
*/
|
|
export function KeyProtectionNotes({ browser = false }: { browser?: boolean }) {
|
|
const t = useTranslations('keyProtection');
|
|
const keyUnprotected = useOfflineStore((state) => state.keyUnprotected);
|
|
const rekey = useOfflineStore((state) => state.rekey);
|
|
const [bridge, setBridge] = useState<'none' | 'wraps' | 'obfuscates'>('wraps');
|
|
|
|
useEffect(() => {
|
|
const member = getDesktopBridge()?.appLock;
|
|
if (!member) {
|
|
setBridge('none');
|
|
return;
|
|
}
|
|
let live = true;
|
|
void member.status().then((status) => {
|
|
if (live) setBridge(status.wrap ? 'wraps' : 'obfuscates');
|
|
}).catch(() => {});
|
|
return () => {
|
|
live = false;
|
|
};
|
|
}, []);
|
|
|
|
const states = Object.values(rekey);
|
|
const running = states.filter((state): state is number => typeof state === 'number');
|
|
const lines = [
|
|
bridge === 'obfuscates' ? t('no_secure_storage') : null,
|
|
bridge === 'none' && browser ? t('browser_note') : null,
|
|
keyUnprotected ? t('wrap_failed') : null,
|
|
states.includes('failed') ? t('rekey_failed') : null,
|
|
running.length > 0 ? t('rekey_progress', { percent: Math.min(...running) }) : null,
|
|
].filter((line): line is string => line !== null);
|
|
if (lines.length === 0) return null;
|
|
return (
|
|
<div className="space-y-1 rounded-md border border-border bg-muted px-3 py-2 text-xs text-foreground" data-testid="key-protection-notes">
|
|
{lines.map((line) => <p key={line}>{line}</p>)}
|
|
</div>
|
|
);
|
|
}
|