Files
webmail/lib/plugin-secrets/wipe-proof.ts
T
Linus Rath a9e33ec2d6 fix: stop a page wiping another account's plugin secrets by naming it in the desktop app
A wipe by name took any username and server with no session behind it. It
now needs a sealed cookie the account got while it was signed in, since a
dropped account has no session cookie left. The old wipe test asserted that
a name alone was enough, which is the behaviour being removed.
2026-11-04 03:15:35 +01:00

43 lines
1.6 KiB
TypeScript

/**
* Proof that this browser had a session for an account, for the desktop app.
*
* An account the app dropped has no session cookie left, and its plugin secrets
* still sit on this device until a wipe. So when a signed-in account stores or lists
* them, it also gets this sealed cookie, and a wipe by name needs the one for that
* name. Nobody can then clear an account's secrets by knowing its address.
*
* It opens nothing else, and only the desktop build writes or reads it.
*/
import type { NextRequest, NextResponse } from 'next/server';
import { decryptPayload, encryptPayload } from '@/lib/auth/crypto';
import { getCookieOptions } from '@/lib/oauth/cookie-config';
import { userKey, type VaultUser } from './vault';
/** Chromium's upper limit. A dropped account can stay dropped for a long time. */
const MAX_AGE_SECONDS = 400 * 24 * 60 * 60;
function cookieName(key: string): string {
return `jmap_psp_${key.slice(0, 16)}`;
}
export function setWipeProof(response: NextResponse, user: VaultUser): void {
const key = userKey(user);
response.cookies.set(cookieName(key), encryptPayload({ u: key }, 'plugin-wipe-proof'), { ...getCookieOptions(), maxAge: MAX_AGE_SECONDS });
}
export function clearWipeProof(response: NextResponse, user: VaultUser): void {
response.cookies.set(cookieName(userKey(user)), '', { ...getCookieOptions(), maxAge: 0 });
}
export function hasWipeProof(request: NextRequest, user: VaultUser): boolean {
const key = userKey(user);
const value = request.cookies.get(cookieName(key))?.value;
if (!value) return false;
try {
return decryptPayload(value, 'plugin-wipe-proof')?.u === key;
} catch {
return false;
}
}