Files
cinny/src/app/utils/securityNudge.ts
T

95 lines
3.7 KiB
TypeScript
Raw Normal View History

/**
* [Gitea #110, #123] One "security" nudge for this device, in priority order:
*
* - verify-device: the account has cross-signing but THIS device isn't signed.
* It can't unlock backed-up history and shows as untrusted to others.
* Verifying with the recovery key also connects the backup, so it comes first.
* - connect-backup: a key backup exists on the server but this device isn't
* backing up to it (`getActiveSessionBackupVersion()` null).
* - setup-backup: no key backup at all (includes accounts that never set up
* cross-signing: the setup flow does both).
*
* Anything not yet known (crypto still loading) → no nudge. Timing: not in
* the first 24 h on a device (onboarding), "Not now" snoozes 7 days, three
* dismissals of a nudge stop it for good; a healthy device never sees one.
*/
export type NudgeKind = 'verify-device' | 'connect-backup' | 'setup-backup';
export type SecurityState = {
/** m.cross_signing.master account data present. */
crossSigning: boolean;
/** This device cross-signing-verified; undefined while crypto isn't ready. */
deviceVerified: boolean | undefined;
/** A key backup version on the server; undefined while loading. */
backupOnServer: boolean | undefined;
/** This device is backing up to it; undefined while loading. */
backupActive: boolean | undefined;
};
export const nudgeFor = (s: SecurityState): NudgeKind | undefined => {
if (s.crossSigning) {
if (s.deviceVerified === undefined) return undefined;
if (!s.deviceVerified) return 'verify-device';
}
if (s.backupOnServer === undefined) return undefined;
if (!s.backupOnServer) return 'setup-backup';
if (s.backupActive === undefined) return undefined;
return s.backupActive ? undefined : 'connect-backup';
};
export const NUDGE_GRACE_MS = 24 * 60 * 60 * 1000;
export const NUDGE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000;
export const NUDGE_MAX_DISMISSALS = 3;
export type NudgeRecord = {
/** When this device was first seen by the nudge (ms). */
firstSeen: number;
dismissals: Partial<Record<NudgeKind, { count: number; last: number }>>;
};
export const shouldShowNudge = (kind: NudgeKind, record: NudgeRecord, now: number): boolean => {
if (now - record.firstSeen < NUDGE_GRACE_MS) return false;
const d = record.dismissals[kind];
if (!d) return true;
return d.count < NUDGE_MAX_DISMISSALS && now - d.last >= NUDGE_SNOOZE_MS;
};
export const dismissNudge = (kind: NudgeKind, record: NudgeRecord, now: number): NudgeRecord => ({
...record,
dismissals: {
...record.dismissals,
[kind]: { count: (record.dismissals[kind]?.count ?? 0) + 1, last: now },
},
});
export const NUDGE_KEY_PREFIX = 'lotus-security-nudge-';
/** Parse a stored record; anything unexpected starts a fresh one (grace period from now). */
export const parseNudgeRecord = (raw: string | null, now: number): NudgeRecord => {
try {
const v = JSON.parse(raw ?? '');
if (v && typeof v.firstSeen === 'number' && v.dismissals && typeof v.dismissals === 'object') {
return { firstSeen: v.firstSeen, dismissals: v.dismissals };
}
} catch {
/* fresh record below */
}
return { firstSeen: now, dismissals: {} };
};
export const NUDGE_COPY: Record<NudgeKind, { text: string; action: string }> = {
'verify-device': {
text: 'Verify this device so you can read your older encrypted messages and others see it as trusted.',
action: 'Verify',
},
'connect-backup': {
text: "This device isn't using your key backup yet. Connect it so your encrypted messages stay readable if you lose it.",
action: 'Connect',
},
'setup-backup': {
text: "Your encryption keys aren't backed up. Set up key backup so you don't lose your encrypted messages if you log out or lose this device.",
action: 'Set up',
},
};