Use the code signing Subject Name as basis for Tray GUID on Windows

Fixes https://github.com/element-hq/element-web/issues/32907
This commit is contained in:
Michael Telatynski 2026-03-26 12:41:06 +00:00
parent cd429874db
commit ea9f72dd47
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
4 changed files with 23 additions and 19 deletions

View File

@ -1,7 +1,7 @@
import * as os from "node:os";
import * as fs from "node:fs";
import * as path from "node:path";
import { type Configuration as BaseConfiguration, type Protocol } from "electron-builder";
import { type Configuration as BaseConfiguration } from "electron-builder";
/**
* This script has different outputs depending on your os platform.
@ -38,6 +38,7 @@ interface Metadata {
interface ExtraMetadata extends Metadata {
electron_appId: string;
electron_protocol: string;
electron_windows_cert_sn?: string;
}
/**
@ -208,6 +209,7 @@ if (variant["linux.deb.name"]) {
if (process.env.ED_SIGNTOOL_SUBJECT_NAME && process.env.ED_SIGNTOOL_THUMBPRINT) {
config.win.signtoolOptions!.certificateSubjectName = process.env.ED_SIGNTOOL_SUBJECT_NAME;
config.win.signtoolOptions!.certificateSha1 = process.env.ED_SIGNTOOL_THUMBPRINT;
config.extraMetadata.electron_windows_cert_sn = config.win.signtoolOptions!.certificateSubjectName;
}
if (os.platform() === "linux") {

View File

@ -12,9 +12,14 @@ import { type JsonObject, loadJsonFile } from "./utils.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
interface BuildConfig {
export interface BuildConfig {
// Application User Model ID
appId: string;
// Protocol string used for OIDC callbacks
protocol: string;
// Subject name of the code signing cert used for Windows packages, if signed
// used as a basis for the Tray GUID which must be rolled if the certificate changes.
windowsCertSubjectName: string | undefined;
}
export function readBuildConfig(): BuildConfig {
@ -22,5 +27,6 @@ export function readBuildConfig(): BuildConfig {
return {
appId: (packageJson["electron_appId"] as string) || "im.riot.app",
protocol: (packageJson["electron_protocol"] as string) || "io.element.desktop",
windowsCertSubjectName: packageJson["electron_windows_cert_sn"] as string,
};
}

View File

@ -489,7 +489,7 @@ app.on("ready", async () => {
global.mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));
// Create trayIcon icon
if (store.get("minimizeToTray")) tray.create(global.trayConfig);
if (store.get("minimizeToTray")) tray.create(global.trayConfig, buildConfig);
global.mainWindow.once("ready-to-show", () => {
if (!global.mainWindow) return;

View File

@ -14,6 +14,10 @@ import pngToIco from "png-to-ico";
import path from "node:path";
import { _t } from "./language-helper.js";
import { BuildConfig } from "./build-config.js";
// This hardcoded uuid is an arbitrary v4 uuid generated on https://www.uuidgenerator.net/version4
const UUID_NAMESPACE = "9fc9c6a0-9ffe-45c9-9cd7-5639ae38b232";
let trayIcon: Tray | null = null;
@ -38,30 +42,22 @@ function toggleWin(): void {
}
}
function getUuid(): string {
// The uuid field is optional and only needed on unsigned Windows packages where the executable path changes
// The hardcoded uuid is an arbitrary v4 uuid generated on https://www.uuidgenerator.net/version4
return global.vectorConfig["uuid"] || "eba84003-e499-4563-8e9d-166e34b5cc25";
}
export function create(config: (typeof global)["trayConfig"]): void {
export function create(config: (typeof global)["trayConfig"], buildConfig: BuildConfig): void {
// no trays on darwin
if (process.platform === "darwin" || trayIcon) return;
const defaultIcon = nativeImage.createFromPath(config.icon_path);
let guid: string | undefined;
if (process.platform === "win32" && app.isPackaged) {
if (process.platform === "win32" && app.isPackaged && buildConfig.windowsCertSubjectName) {
// Providing a GUID lets Windows be smarter about maintaining user's tray preferences
// https://github.com/electron/electron/pull/21891
// Ideally we would only specify it for signed packages but determining whether the app is signed sufficiently
// is non-trivial. So instead we have an escape hatch that unsigned packages can iterate the `uuid` in
// config.json to prevent Windows refusing GUID-reuse if their executable path changes.
guid = uuidv5(`${app.getName()}-${app.getPath("userData")}`, getUuid());
// We generate the GUID in a custom arbitrary namespace and use the subject name & userData path
// to differentiate different app builds on the same system.
const guid = uuidv5(`${buildConfig.windowsCertSubjectName}:${app.getPath("userData")}`, UUID_NAMESPACE);
trayIcon = new Tray(defaultIcon, guid);
} else {
trayIcon = new Tray(defaultIcon);
}
// Passing guid=undefined on Windows will cause it to throw `Error: Invalid GUID format`
// The type here is wrong, the param must be omitted, never undefined.
trayIcon = guid ? new Tray(defaultIcon, guid) : new Tray(defaultIcon);
trayIcon.setToolTip(config.brand);
initApplicationMenu();
trayIcon.on("click", toggleWin);