| 1 | export type LinuxPasswordStorePreference = |
| 2 | | "auto" |
| 3 | | "gnome-libsecret" |
| 4 | | "kwallet" |
| 5 | | "kwallet5" |
| 6 | | "kwallet6"; |
| 7 | export type LinuxPasswordStoreSwitch = Exclude<LinuxPasswordStorePreference, "auto">; |
| 8 | |
| 9 | export 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. |
| 14 | const 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. |
| 24 | const ELECTRON_KDE_DESKTOP = "KDE"; |
| 25 | // Chromium recognizes LXQt and still selects basic text for it, so it does need a forced backend. |
| 26 | const ELECTRON_UNPROTECTED_DESKTOPS = new Set(["LXQt"]); |
| 27 | |
| 28 | export 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. |
| 44 | export 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. |
| 60 | function 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 | |
| 77 | function splitDesktopNameList(value: string | undefined): string[] { |
| 78 | return value?.split(":") ?? []; |
| 79 | } |