From 84d5e9648723d79f45a4d31ebd039d9b5bfaf250 Mon Sep 17 00:00:00 2001 From: pdmarf <135653545+pdmarf@users.noreply.github.com> Date: Fri, 10 Apr 2026 20:20:10 +0100 Subject: [PATCH] Add Ed25519 signature verification to auto-updater (v1.0.4) --- com.pdma.notion-timer.sdPlugin/bin/plugin.js | 39 +++++++++++++++---- .../bin/plugin.js.sig | 1 + package.json | 3 +- scripts/sign.js | 23 +++++++++++ src/notion.ts | 8 ++-- src/plugin.ts | 39 +++++++++++++++---- version.json | 2 +- 7 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig create mode 100644 scripts/sign.js diff --git a/com.pdma.notion-timer.sdPlugin/bin/plugin.js b/com.pdma.notion-timer.sdPlugin/bin/plugin.js index 1c81326..a57725a 100644 --- a/com.pdma.notion-timer.sdPlugin/bin/plugin.js +++ b/com.pdma.notion-timer.sdPlugin/bin/plugin.js @@ -6383,7 +6383,7 @@ async function fetchProjects(token, dbId) { sorts: [{ property: "Project name", direction: "ascending" }] }) }); - if (!resp.ok) throw new Error(`Notion error ${resp.status}: ${await resp.text()}`); + if (!resp.ok) throw new Error(`Notion error ${resp.status}`); const data = await resp.json(); return data.results.map((page) => { const titleArr = page.properties?.["Project name"]?.title ?? []; @@ -6413,7 +6413,7 @@ async function startTimer(token, timingDbId, projectId, projectName, userId) { } }) }); - if (!resp.ok) throw new Error(`Failed to start timer: ${await resp.text()}`); + if (!resp.ok) throw new Error(`Failed to start timer: ${resp.status}`); const data = await resp.json(); return data.id; } @@ -6429,26 +6429,49 @@ async function stopTimer(token, entryId) { } }) }); - if (!resp.ok) throw new Error(`Failed to stop timer: ${await resp.text()}`); + if (!resp.ok) throw new Error(`Failed to stop timer: ${resp.status}`); } // src/plugin.ts -var CURRENT_VERSION = "1.0.3"; +var CURRENT_VERSION = "1.0.4"; var GITEA_BASE = "http://100.120.125.113:3000/pdm/stream_deck_notion_timer/raw/branch/master"; +var SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAN7ko8TUpuPzPAJuKAZCRjV0c4ZSlou5d9pUAF6o12b4= +-----END PUBLIC KEY-----`; +function isNewerVersion(remote, current) { + const parse = (v) => v.split(".").map(Number); + const [rMaj, rMin, rPat] = parse(remote); + const [cMaj, cMin, cPat] = parse(current); + if (rMaj !== cMaj) return rMaj > cMaj; + if (rMin !== cMin) return rMin > cMin; + return rPat > cPat; +} async function checkForUpdates() { try { const resp = await fetch(`${GITEA_BASE}/version.json`); if (!resp.ok) return; const { version } = await resp.json(); - if (version === CURRENT_VERSION) return; - const pluginResp = await fetch(`${GITEA_BASE}/com.pdma.notion-timer.sdPlugin/bin/plugin.js`); - if (!pluginResp.ok) return; + if (!/^\d+\.\d+\.\d+$/.test(version)) return; + if (!isNewerVersion(version, CURRENT_VERSION)) return; + const [pluginResp, sigResp] = await Promise.all([ + fetch(`${GITEA_BASE}/com.pdma.notion-timer.sdPlugin/bin/plugin.js`), + fetch(`${GITEA_BASE}/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig`) + ]); + if (!pluginResp.ok || !sigResp.ok) return; const newCode = await pluginResp.text(); + const sigBytes = Buffer.from(await sigResp.arrayBuffer()); + const { verify } = await import("node:crypto"); + const valid = verify(null, Buffer.from(newCode), SIGNING_PUBLIC_KEY, sigBytes); + if (!valid) { + plugin_default.logger.error("Update rejected: signature verification failed"); + return; + } const fs3 = await import("fs"); fs3.writeFileSync(__filename, newCode); plugin_default.logger.info(`Updated to ${version}, restarting\u2026`); process.exit(0); - } catch { + } catch (err) { + plugin_default.logger.error(`Update check failed: ${err instanceof Error ? err.message : String(err)}`); } } var HARDCODED = { diff --git a/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig b/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig new file mode 100644 index 0000000..37b8ae4 --- /dev/null +++ b/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig @@ -0,0 +1 @@ +MsRE¸I æ6Tİßşã<æİE]&Z¸pŒĞݼÓĞ^ßlt!°BDuÖs屟ê1)ë€6Ç_/ø`9 \ No newline at end of file diff --git a/package.json b/package.json index d00ab15..3aad0d7 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "version": "1.0.0", "description": "Notion time tracking toggle for Stream Deck", "scripts": { - "build": "esbuild src/plugin.ts --bundle --platform=node --target=node20 --outfile=com.pdma.notion-timer.sdPlugin/bin/plugin.js --external:electron", + "build": "esbuild src/plugin.ts --bundle --platform=node --target=node20 --outfile=com.pdma.notion-timer.sdPlugin/bin/plugin.js --external:electron && node scripts/sign.js", "dev": "esbuild src/plugin.ts --bundle --platform=node --target=node20 --outfile=com.pdma.notion-timer.sdPlugin/bin/plugin.js --external:electron --watch", + "sign": "node scripts/sign.js", "link": "ln -sf \"$(pwd)/com.pdma.notion-timer.sdPlugin\" \"$HOME/Library/Application Support/com.elgato.StreamDeck/Plugins/com.pdma.notion-timer.sdPlugin\"", "unlink": "rm -f \"$HOME/Library/Application Support/com.elgato.StreamDeck/Plugins/com.pdma.notion-timer.sdPlugin\"" }, diff --git a/scripts/sign.js b/scripts/sign.js new file mode 100644 index 0000000..2a574e0 --- /dev/null +++ b/scripts/sign.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +// Signs plugin.js with the Ed25519 private key, producing plugin.js.sig +// The private key lives at ~/.notion-timer-signing-key.pem and is never committed. + +const { sign } = require("crypto"); +const fs = require("fs"); +const path = require("path"); + +const PLUGIN_JS = path.join(__dirname, "../com.pdma.notion-timer.sdPlugin/bin/plugin.js"); +const SIG_FILE = PLUGIN_JS + ".sig"; +const KEY_FILE = path.join(process.env.HOME, ".notion-timer-signing-key.pem"); + +if (!fs.existsSync(KEY_FILE)) { + console.error(`Signing key not found at ${KEY_FILE}`); + process.exit(1); +} + +const privateKey = fs.readFileSync(KEY_FILE, "utf8"); +const payload = fs.readFileSync(PLUGIN_JS); +const signature = sign(null, payload, privateKey); + +fs.writeFileSync(SIG_FILE, signature); +console.log(`Signed: ${path.basename(SIG_FILE)} (${signature.length} bytes)`); diff --git a/src/notion.ts b/src/notion.ts index ac9d409..6714a56 100644 --- a/src/notion.ts +++ b/src/notion.ts @@ -55,7 +55,7 @@ export async function fetchProjects(token: string, dbId: string): Promise { @@ -74,7 +74,7 @@ export async function startTimer( timingDbId: string, projectId: string, projectName: string, - userId: string, + userId: string ): Promise { const now = new Date().toISOString(); const date = new Date().toLocaleDateString("en-GB"); @@ -95,7 +95,7 @@ export async function startTimer( }), }); - if (!resp.ok) throw new Error(`Failed to start timer: ${await resp.text()}`); + if (!resp.ok) throw new Error(`Failed to start timer: ${resp.status}`); const data = (await resp.json()) as { id: string }; return data.id; @@ -115,5 +115,5 @@ export async function stopTimer(token: string, entryId: string): Promise { }), }); - if (!resp.ok) throw new Error(`Failed to stop timer: ${await resp.text()}`); + if (!resp.ok) throw new Error(`Failed to stop timer: ${resp.status}`); } diff --git a/src/plugin.ts b/src/plugin.ts index 8d33b18..e5cfbc9 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,23 +1,48 @@ -const CURRENT_VERSION = "1.0.3"; +const CURRENT_VERSION = "1.0.4"; const GITEA_BASE = "http://100.120.125.113:3000/pdm/stream_deck_notion_timer/raw/branch/master"; +const SIGNING_PUBLIC_KEY = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAN7ko8TUpuPzPAJuKAZCRjV0c4ZSlou5d9pUAF6o12b4= +-----END PUBLIC KEY-----`; + +function isNewerVersion(remote: string, current: string): boolean { + const parse = (v: string) => v.split(".").map(Number); + const [rMaj, rMin, rPat] = parse(remote); + const [cMaj, cMin, cPat] = parse(current); + if (rMaj !== cMaj) return rMaj > cMaj; + if (rMin !== cMin) return rMin > cMin; + return rPat > cPat; +} async function checkForUpdates(): Promise { try { const resp = await fetch(`${GITEA_BASE}/version.json`); if (!resp.ok) return; const { version } = await resp.json() as { version: string }; - if (version === CURRENT_VERSION) return; + if (!/^\d+\.\d+\.\d+$/.test(version)) return; + if (!isNewerVersion(version, CURRENT_VERSION)) return; - const pluginResp = await fetch(`${GITEA_BASE}/com.pdma.notion-timer.sdPlugin/bin/plugin.js`); - if (!pluginResp.ok) return; - const newCode = await pluginResp.text(); + const [pluginResp, sigResp] = await Promise.all([ + fetch(`${GITEA_BASE}/com.pdma.notion-timer.sdPlugin/bin/plugin.js`), + fetch(`${GITEA_BASE}/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig`), + ]); + if (!pluginResp.ok || !sigResp.ok) return; + + const newCode = await pluginResp.text(); + const sigBytes = Buffer.from(await sigResp.arrayBuffer()); + + const { verify } = await import("node:crypto"); + const valid = verify(null, Buffer.from(newCode), SIGNING_PUBLIC_KEY, sigBytes); + if (!valid) { + streamDeck.logger.error("Update rejected: signature verification failed"); + return; + } const fs = await import("fs"); fs.writeFileSync(__filename, newCode); streamDeck.logger.info(`Updated to ${version}, restarting…`); process.exit(0); - } catch { - // Silently ignore — don't disrupt normal operation if update check fails + } catch (err) { + streamDeck.logger.error(`Update check failed: ${err instanceof Error ? err.message : String(err)}`); } } diff --git a/version.json b/version.json index bcd6d09..5db88d9 100644 --- a/version.json +++ b/version.json @@ -1 +1 @@ -{ "version": "1.0.3" } +{ "version": "1.0.4" }