mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-09 19:57:20 -05:00
feat: add signed Windows launcher self-updates with SHA-256 verification
This commit is contained in:
23
main.js
23
main.js
@@ -217,6 +217,7 @@ async function start() {
|
||||
next[name] = names.includes(name) ? release.components[name].id : current[name];
|
||||
}
|
||||
patcher.saveCurrentVersion(release.version, release.sha256, next);
|
||||
patcher.writeIntegrity(names);
|
||||
} catch (err) {
|
||||
try { patcher.rollback(); } catch (x) { debugLog(`Rollback failed: ${x.message}`); }
|
||||
throw err;
|
||||
@@ -264,6 +265,27 @@ async function start() {
|
||||
return false;
|
||||
};
|
||||
|
||||
if (release && release.installer && process.platform === 'win32' && process.env.VYLA_DEV !== '1') {
|
||||
const appVersion = process.env.VYLA_APP_VERSION || '0.0.0';
|
||||
const tried = updateClient.readCache().selfUpdate || {};
|
||||
const recentlyTried = tried.version === release.installer.version && Date.now() - (tried.at || 0) < 24 * 60 * 60 * 1000;
|
||||
if (isNewer(release.installer.version, appVersion) && !recentlyTried) {
|
||||
try {
|
||||
const dest = path.join(APPDATA_DIR, `vyla-installer-${normalizeVer(release.installer.version)}.exe`);
|
||||
sendToTauri('status', { message: 'Downloading launcher update...' });
|
||||
await downloadVerified('launcher', release.installer, dest);
|
||||
updateClient.writeCache({ selfUpdate: { version: release.installer.version, at: Date.now() } });
|
||||
sendToTauri('status', { message: 'Installing launcher update...' });
|
||||
sendToTauri('self-update', { path: dest });
|
||||
await sleep(30000);
|
||||
sendToTauri('notice', { message: 'Launcher update did not start. Continuing on the current version.' });
|
||||
} catch (err) {
|
||||
debugLog(`Launcher update failed: ${err.message}`);
|
||||
sendToTauri('notice', { message: `Launcher update failed: ${err.message}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (release) {
|
||||
const stale = patcher.staleComponents(release);
|
||||
const broken = stale.some((name) => !patcher.componentIntact(name));
|
||||
@@ -325,6 +347,7 @@ async function start() {
|
||||
} catch (e) { }
|
||||
|
||||
patcher.saveCurrentVersion(ver, manSha, ids);
|
||||
patcher.writeIntegrity(['frontend', 'player']);
|
||||
} catch (e) {
|
||||
try { patcher.rollback(); } catch (x) { }
|
||||
throw e;
|
||||
|
||||
50
patcher.js
50
patcher.js
@@ -91,14 +91,62 @@ class Patcher {
|
||||
}
|
||||
}
|
||||
|
||||
integrityPath() {
|
||||
return path.join(this.appDataPath, 'integrity.json');
|
||||
}
|
||||
|
||||
readIntegrity() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(this.integrityPath(), 'utf8'));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
writeIntegrity(names) {
|
||||
const all = this.readIntegrity();
|
||||
for (const name of names) {
|
||||
if (name !== 'frontend' && name !== 'player') continue;
|
||||
const files = {};
|
||||
const walk = (dir, rel) => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
const r = `${rel}/${entry.name}`;
|
||||
if (r.includes('/node_modules/@vyla-entertainment/sdk')) continue;
|
||||
if (entry.isDirectory()) walk(full, r);
|
||||
else if (entry.isFile()) files[r] = fs.statSync(full).size;
|
||||
}
|
||||
};
|
||||
walk(path.join(this.appDataPath, name), name);
|
||||
all[name] = files;
|
||||
}
|
||||
const tmp = `${this.integrityPath()}.tmp`;
|
||||
fs.writeFileSync(tmp, JSON.stringify(all), 'utf8');
|
||||
renameSafe(tmp, this.integrityPath());
|
||||
}
|
||||
|
||||
integrityOk(name) {
|
||||
const files = this.readIntegrity()[name];
|
||||
if (!files) return true;
|
||||
for (const [rel, size] of Object.entries(files)) {
|
||||
try {
|
||||
if (fs.statSync(path.join(this.appDataPath, rel)).size !== size) return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
componentIntact(name) {
|
||||
const probe = COMPONENT_PROBES[name];
|
||||
if (!probe || name === 'secrets') return true;
|
||||
try {
|
||||
return fs.statSync(path.join(this.appDataPath, probe)).size > 0;
|
||||
if (fs.statSync(path.join(this.appDataPath, probe)).size <= 0) return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return this.integrityOk(name);
|
||||
}
|
||||
|
||||
staleComponents(release) {
|
||||
|
||||
20
release.js
20
release.js
@@ -2,6 +2,8 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const { execSync } = require('child_process');
|
||||
const crypto = require('crypto');
|
||||
const UpdateClient = require('./update-client');
|
||||
|
||||
const GITLAB_PROJECT_PATH = 'vyla-entertainment%2Fhome';
|
||||
const PROJECT_ID = process.env.CI_PROJECT_ID || GITLAB_PROJECT_PATH;
|
||||
@@ -217,6 +219,24 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
if (installerPath && installerUploadName && process.platform === 'win32') {
|
||||
const signingKey = process.env.VYLA_UPDATE_SIGNING_KEY;
|
||||
if (!signingKey) {
|
||||
throw new Error('VYLA_UPDATE_SIGNING_KEY is required to sign the installer entry.');
|
||||
}
|
||||
const installerBuf = fs.readFileSync(installerPath);
|
||||
const signedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
signedManifest.installer = {
|
||||
file: installerUploadName,
|
||||
sha256: crypto.createHash('sha256').update(installerBuf).digest('hex'),
|
||||
size: installerBuf.length,
|
||||
version: version.replace(/^v/, '')
|
||||
};
|
||||
delete signedManifest.signature;
|
||||
signedManifest.signature = crypto.sign(null, Buffer.from(UpdateClient.canonicalManifest(signedManifest)), signingKey.replace(/\\n/g, '\n')).toString('base64');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(signedManifest, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
console.log('[2/4] Uploading artifacts to GitLab Package Registry...');
|
||||
const timestamp = Date.now();
|
||||
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -4073,7 +4073,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vyla-home"
|
||||
version = "1.1.54"
|
||||
version = "1.1.55"
|
||||
dependencies = [
|
||||
"objc2 0.5.2",
|
||||
"serde",
|
||||
|
||||
@@ -733,6 +733,8 @@ fn main() {
|
||||
.current_dir(&root_dir)
|
||||
.env("APPDATA_PATH", &vyla_files_str)
|
||||
.env("NODE_EXE", &node_bin)
|
||||
.env("VYLA_APP_VERSION", env!("CARGO_PKG_VERSION"))
|
||||
.env("VYLA_DEV", if cfg!(debug_assertions) { "1" } else { "0" })
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
@@ -796,6 +798,40 @@ fn main() {
|
||||
};
|
||||
|
||||
match msg.event.as_str() {
|
||||
"self-update" => {
|
||||
let target = msg
|
||||
.data
|
||||
.get("path")
|
||||
.and_then(|p| p.as_str())
|
||||
.map(PathBuf::from);
|
||||
if let Some(p) = target {
|
||||
let name = p
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
if name.starts_with("vyla-installer-")
|
||||
&& name.ends_with(".exe")
|
||||
&& p.exists()
|
||||
{
|
||||
let state: State<NodeServices> = w.state();
|
||||
if let Ok(mut lock) = state.0.lock() {
|
||||
if let Some(mut child) = lock.take() {
|
||||
kill_child_process(&mut child);
|
||||
}
|
||||
};
|
||||
let mut installer = Command::new(&p);
|
||||
installer.args(["/S", "/UPDATE"]);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
installer.creation_flags(0x00000008 | 0x00000200);
|
||||
}
|
||||
let _ = installer.spawn();
|
||||
w.app_handle().exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
"ready" => {
|
||||
if let Some(u) = msg.data.get("url").and_then(|u| u.as_str()) {
|
||||
if let Ok(mut g) = READY_URL.lock() {
|
||||
|
||||
@@ -43,7 +43,12 @@ function canonicalManifest(manifest) {
|
||||
const c = manifest.components[name];
|
||||
return `${name}:${c.file}:${c.sha256}:${c.id}:${c.size}`;
|
||||
});
|
||||
return ['vyla-manifest-v2', normalizeTag(manifest.version), manifest.sha256, ...parts].join('\n');
|
||||
const lines = ['vyla-manifest-v2', normalizeTag(manifest.version), manifest.sha256, ...parts];
|
||||
if (manifest.installer) {
|
||||
const i = manifest.installer;
|
||||
lines.push(`installer:${i.file}:${i.sha256}:${i.size}:${normalizeTag(i.version)}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function verifyManifest(manifest) {
|
||||
@@ -381,11 +386,25 @@ class UpdateClient {
|
||||
components[name] = { url: link.url, sha256: c.sha256, id: c.id, size: c.size };
|
||||
}
|
||||
|
||||
let installer = null;
|
||||
if (manifest.installer) {
|
||||
const installerLink = links.find((a) => a.name === manifest.installer.file);
|
||||
if (installerLink) {
|
||||
installer = {
|
||||
url: installerLink.url,
|
||||
sha256: manifest.installer.sha256,
|
||||
size: manifest.installer.size,
|
||||
version: manifest.installer.version
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const out = {
|
||||
version: release.tag_name,
|
||||
sha256: manifest.sha256 || null,
|
||||
manifest,
|
||||
components
|
||||
components,
|
||||
installer
|
||||
};
|
||||
this.writeCache({ checkedAt: now, etag: result.etag || null, release: out });
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user