1export type LinuxPasswordStorePreference =
2 | "auto"
3 | "gnome-libsecret"
4 | "kwallet"
5 | "kwallet5"
6 | "kwallet6";
7export type LinuxPasswordStoreSwitch = Exclude<LinuxPasswordStorePreference, "auto">;
8
9export const DEFAULT_LINUX_PASSWORD_STORE: LinuxPasswordStorePreference = "auto";
10
11// Chromium matches XDG_CURRENT_DESKTOP values case-sensitively and returns on the first value it
12// recognizes, so these stay exact literals and are scanned in order. Omitting a real desktop fails
13// safe: we force gnome-libsecret, which is the backend Chromium selects for all of these anyway.
14const ELECTRON_LIBSECRET_DESKTOPS = new Set([
15 "Deepin",
16 "GNOME",
17 "Pantheon",
18 "UKUI",
19 "Unity",
20 "X-Cinnamon",
21 "XFCE",
22]);
23// Chromium selects a KWallet generation for KDE from KDE_SESSION_VERSION, so it needs no help.
24const ELECTRON_KDE_DESKTOP = "KDE";
25// Chromium recognizes LXQt and still selects basic text for it, so it does need a forced backend.
26const ELECTRON_UNPROTECTED_DESKTOPS = new Set(["LXQt"]);
27
28export function normalizeLinuxPasswordStorePreference(
29 value: unknown,
30): LinuxPasswordStorePreference {
31 return value === "gnome-libsecret" ||
32 value === "kwallet" ||
33 value === "kwallet5" ||
34 value === "kwallet6"
35 ? value
36 : DEFAULT_LINUX_PASSWORD_STORE;
37}
38
39// Auto mode asks one question: will Electron select a real keyring on its own? If so, stay out of
40// the way, which is how canonical KDE sessions keep the KWallet generation Chromium picks for them.
41// Otherwise force gnome-libsecret, because the alternative is basic text, which is barely
42// encryption at all. Forcing never guesses a KWallet generation; a KDE session that needs a
43// specific one sets linuxPasswordStore explicitly.
44export function resolveLinuxPasswordStoreSwitch(input: {
45 readonly preference: LinuxPasswordStorePreference;
46 readonly env: NodeJS.ProcessEnv;
47}): LinuxPasswordStoreSwitch | null {
48 if (input.preference !== "auto") {
49 return input.preference;
50 }
51
52 return electronSelectsProtectedBackend(input.env) ? null : "gnome-libsecret";
53}
54
55// Only an exact XDG_CURRENT_DESKTOP literal proves Electron will protect the session. Chromium can
56// also reach a real backend through DESKTOP_SESSION and the legacy KDE markers, but those are the
57// variables a previous session leaves behind, and trusting them is what let stale hints suppress
58// the forced backend before. Forcing where Chromium would have chosen libsecret is harmless, since
59// it lands on the same backend.
60function electronSelectsProtectedBackend(env: NodeJS.ProcessEnv): boolean {
61 for (const name of splitDesktopNameList(env.XDG_CURRENT_DESKTOP)) {
62 const trimmed = name.trim();
63 if (trimmed.length === 0) {
64 continue;
65 }
66 if (trimmed === ELECTRON_KDE_DESKTOP || ELECTRON_LIBSECRET_DESKTOPS.has(trimmed)) {
67 return true;
68 }
69 if (ELECTRON_UNPROTECTED_DESKTOPS.has(trimmed)) {
70 return false;
71 }
72 }
73
74 return false;
75}
76
77function splitDesktopNameList(value: string | undefined): string[] {
78 return value?.split(":") ?? [];
79}