Store Nexus API key securely with safeStorage and fix bugs related to invalid Silksong path

This commit is contained in:
2026-02-23 21:24:06 +01:00
parent 9a65857f81
commit 8924e1f882
6 changed files with 112 additions and 46 deletions

74
main.js
View File

@@ -1,4 +1,4 @@
import { app, BrowserWindow, ipcMain, dialog, shell, Menu } from "electron"; import { app, BrowserWindow, ipcMain, dialog, shell, Menu, safeStorage } from "electron";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import Store from "electron-store"; import Store from "electron-store";
@@ -10,26 +10,26 @@ import { gql, GraphQLClient } from "graphql-request";
import { path7za } from "7zip-bin"; import { path7za } from "7zip-bin";
import node7z from "node-7z"; import node7z from "node-7z";
const { extractFull } = node7z; const { extractFull } = node7z;
import packageJson from "./package.json" with { type: "json" };
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
const isDev = !app.isPackaged; const isDev = !app.isPackaged;
const VERSION = "1.0.0"; const VERSION = packageJson.version;
const store = new Store(); const store = new Store();
const bepinexStore = new Store({ cwd: "bepinex-version" }); const bepinexStore = new Store({ name: "bepinex-version" });
const installedModsStore = new Store({ cwd: "installed-mods-list" }); const installedModsStore = new Store({ name: "installed-mods-list" });
const userSavePath = app.getPath("userData"); const userSavePath = app.getPath("userData");
const modSavePath = `${userSavePath}\\mods`; const modSavePath = `${userSavePath}\\mods`;
const dataPath = `${userSavePath}\\config.json`; const dataPath = `${userSavePath}\\config.json`;
let silksongPath = store.get("silksong-path"); let silksongPath = store.get("silksong-path");
const NexusAPIStore = new Store({ name: "nexus-api", encryptionKey: packageJson["AES-key-nexus-api"], fileExtension: "encrypted", clearInvalidConfig: true });
const Nexus = NexusModule.default; const Nexus = NexusModule.default;
let nexusAPI = store.get("nexus-api");
let nexus = undefined; let nexus = undefined;
createNexus();
let installedCachedModList = undefined; let installedCachedModList = undefined;
let onlineCachedModList = undefined; let onlineCachedModList = undefined;
@@ -90,6 +90,7 @@ app.whenReady().then(() => {
} }
if (gotTheLock) { if (gotTheLock) {
createNexus(loadNexusApi());
checkInstalledMods(); checkInstalledMods();
createWindow(); createWindow();
} }
@@ -160,22 +161,28 @@ ipcMain.handle("load-bepinex-backup-version", () => {
return bepinexBackupVersion; return bepinexBackupVersion;
}); });
ipcMain.handle("save-nexus-api", (event, api) => { ipcMain.handle("save-nexus-api", async (event, api) => {
nexusAPI = api; if (api) {
createNexus(); const encryptedAPI = safeStorage.encryptString(api);
store.set("nexus-api", nexusAPI); NexusAPIStore.set("nexus-api", encryptedAPI.toString("base64"));
} else {
NexusAPIStore.delete("nexus-api");
}
await createNexus(api);
}); });
function loadNexusApi() { function loadNexusApi() {
nexusAPI = store.get("nexus-api"); const encryptedAPI = NexusAPIStore.get("nexus-api");
if (nexusAPI == undefined) { if (encryptedAPI) {
return ""; return safeStorage.decryptString(Buffer.from(encryptedAPI, "base64"));
} }
return nexusAPI;
} }
ipcMain.handle("load-nexus-api", () => { ipcMain.handle("load-nexus-api", () => {
return loadNexusApi(); if (loadNexusApi()) {
return true;
}
return false;
}); });
ipcMain.handle("save-theme", (event, theme, lacePinState) => { ipcMain.handle("save-theme", (event, theme, lacePinState) => {
@@ -255,6 +262,11 @@ ipcMain.handle("import-data", async () => {
////////////////////// BEPINEX /////////////////////// ////////////////////// BEPINEX ///////////////////////
async function installBepinex() { async function installBepinex() {
if (!(await fileExists(silksongPath))) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
return;
}
if (await fileExists(bepinexBackupPath)) { if (await fileExists(bepinexBackupPath)) {
if (await fileExists(`${bepinexBackupPath}/BepInEx`)) { if (await fileExists(`${bepinexBackupPath}/BepInEx`)) {
await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, { recursive: true }); await fs.cp(`${bepinexBackupPath}/BepInEx`, bepinexFolderPath, { recursive: true });
@@ -282,6 +294,9 @@ async function installBepinex() {
}); });
if (!res.ok) { if (!res.ok) {
if (res.status == 403) {
mainWindow.webContents.send("showToast", "Github has blocked the application. Please try again later.", "error");
}
throw new Error(`GitHub API error: ${res.status}`); throw new Error(`GitHub API error: ${res.status}`);
} }
@@ -304,6 +319,11 @@ ipcMain.handle("install-bepinex", async () => {
}); });
async function uninstallBepinex() { async function uninstallBepinex() {
if (!(await fileExists(silksongPath))) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
return;
}
if (await fileExists(bepinexFolderPath)) { if (await fileExists(bepinexFolderPath)) {
await fs.rm(bepinexFolderPath, { recursive: true }); await fs.rm(bepinexFolderPath, { recursive: true });
} }
@@ -322,6 +342,11 @@ ipcMain.handle("uninstall-bepinex", async () => {
}); });
async function backupBepinex() { async function backupBepinex() {
if (!(await fileExists(silksongPath))) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
return;
}
if ((await fileExists(bepinexBackupPath)) == false) { if ((await fileExists(bepinexBackupPath)) == false) {
await fs.mkdir(bepinexBackupPath); await fs.mkdir(bepinexBackupPath);
} }
@@ -352,6 +377,11 @@ ipcMain.handle("backup-bepinex", async () => {
}); });
ipcMain.handle("delete-bepinex-backup", async () => { ipcMain.handle("delete-bepinex-backup", async () => {
if (!(await fileExists(silksongPath))) {
mainWindow.webContents.send("showToast", "Path to the game invalid", "warning");
return;
}
if (await fileExists(bepinexBackupPath)) { if (await fileExists(bepinexBackupPath)) {
await fs.rm(bepinexBackupPath, { recursive: true }); await fs.rm(bepinexBackupPath, { recursive: true });
saveBepinexBackupVersion(undefined); saveBepinexBackupVersion(undefined);
@@ -361,15 +391,21 @@ ipcMain.handle("delete-bepinex-backup", async () => {
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
/////////////////////// NEXUS //////////////////////// /////////////////////// NEXUS ////////////////////////
async function createNexus() { async function createNexus(api) {
if (nexusAPI == undefined) { if (api == undefined) {
nexus = undefined;
return; return;
} }
try { try {
nexus = await Nexus.create(nexusAPI, "silk-fly-launcher", VERSION, "hollowknightsilksong"); nexus = await Nexus.create(api, "silk-fly-launcher", VERSION, "hollowknightsilksong");
} catch (error) { } catch (error) {
console.log(error); if (error.mStatusCode == 401) {
mainWindow.webContents.send("showToast", "Invalid Nexus API key", "error");
}
if (error.code == "ENOTFOUND") {
mainWindow.webContents.send("showToast", "Unable to communicate with Nexus servers", "error");
}
nexus = undefined; nexus = undefined;
} }
} }

View File

@@ -1,12 +1,11 @@
{ {
"name": "silkflylauncher", "name": "silkflylauncher",
"productName": "Silk Fly Launcher", "productName": "Silk Fly Launcher",
"version": "1.0.0", "version": "1.0.0-dev",
"description": "Silk Fly Launcher is a launcher and mod manager for Silksong mods from Nexus, built with Electron.", "description": "Silk Fly Launcher is a launcher and mod manager for Silksong mods from Nexus, built with Electron.",
"main": "main.js", "main": "main.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "electron .",
"start": "electron-forge start", "start": "electron-forge start",
"package": "electron-forge package", "package": "electron-forge package",
"make": "electron-forge make", "make": "electron-forge make",
@@ -33,5 +32,6 @@
"graphql": "^16.12.0", "graphql": "^16.12.0",
"graphql-request": "^7.4.0", "graphql-request": "^7.4.0",
"node-7z": "^3.0.0" "node-7z": "^3.0.0"
} },
"AES-key-nexus-api": "__AES_KEY__"
} }

View File

@@ -92,7 +92,7 @@
<template id="installed-mods-template"> <template id="installed-mods-template">
<h2>List Of Installed Mods</h2> <h2>List Of Installed Mods</h2>
<div class="horizontal-div"> <div class="horizontal-div">
<form class="horizontal-div search-form" id="search-form"> <form class="horizontal-div input-form" id="search-form">
<input class="input" id="search-input" type="text" placeholder="Search For Mods..." /> <input class="input" id="search-input" type="text" placeholder="Search For Mods..." />
<button class="default-button" onclick="searchInstalledMods()">Search</button> <button class="default-button" onclick="searchInstalledMods()">Search</button>
</form> </form>
@@ -115,7 +115,7 @@
<template id="online-mods-template"> <template id="online-mods-template">
<h2>List Of Nexus Mods</h2> <h2>List Of Nexus Mods</h2>
<div class="horizontal-div"> <div class="horizontal-div">
<form class="horizontal-div search-form" id="search-form"> <form class="horizontal-div input-form" id="search-form">
<input class="input" id="search-input" type="text" placeholder="Search For Mods..." /> <input class="input" id="search-input" type="text" placeholder="Search For Mods..." />
<button class="default-button" onclick="searchNexusMods()">Search</button> <button class="default-button" onclick="searchNexusMods()">Search</button>
</form> </form>
@@ -221,19 +221,24 @@
<h2>Nexus</h2> <h2>Nexus</h2>
<p class="transparent-text" id="bepinex-version-text"></p> <p class="transparent-text" id="bepinex-version-text"></p>
<div class="horizontal-div"> <div class="horizontal-div">
<label for="nexus-api-label">Enter your nexus api: </label> <label>Nexus API key: </label>
<input type="text" class="input" id="nexus-api-input" name="nexus-api-input" />
<img class="nexus-check-image" id="nexus-check-image" src="assets/icons/cross.svg" /> <img class="nexus-check-image" id="nexus-check-image" src="assets/icons/cross.svg" />
<button class="default-button" onclick="verifyNexusAPI()">Verify</button> <form class="horizontal-div input-form" id="nexus-api-form">
<input class="input" id="nexus-api-input" type="text" placeholder="Enter your nexus api" />
<button class="default-button" onclick="setNexusAPI()">Set</button>
</form>
<button class="default-button" onclick="resetNexusAPI()">Reset</button>
</div> </div>
<br /> <br />
<h2>Import/Export</h2> <h2>Import/Export</h2>
<div class="horizontal-div"> <div class="horizontal-div">
<button class="default-button" onclick="importData()">Import Data</button> <button class="default-button" onclick="importData()">Import Data</button>
<button class="default-button" onclick="exportData()">Export Data</button> <button class="default-button" onclick="exportData()">Export Data</button>
<button class="important-button" onclick="deleteData()">Delete All Data</button> <button class="important-button" onclick="deleteData()">Delete Data</button>
</div> </div>
<br /> <br />
<p class="transparent-text">This data does not include your Nexus API key or the list of your installed mods.</p>
<br />
<h2>Debugging</h2> <h2>Debugging</h2>
<div class="horizontal-div"> <div class="horizontal-div">
<h3>Versions:</h3> <h3>Versions:</h3>

View File

@@ -78,7 +78,7 @@ async function navigate(page) {
const searchFormInstalled = installedModsTemplateCopy.getElementById("search-form"); const searchFormInstalled = installedModsTemplateCopy.getElementById("search-form");
const searchInputInstalled = installedModsTemplateCopy.getElementById("search-input"); const searchInputInstalled = installedModsTemplateCopy.getElementById("search-input");
searchFormInstalled.addEventListener("submit", async function (event) { searchFormInstalled.addEventListener("submit", function (event) {
event.preventDefault(); event.preventDefault();
}); });
searchInputInstalled.value = searchValueInstalled; searchInputInstalled.value = searchValueInstalled;
@@ -241,7 +241,7 @@ async function navigate(page) {
title.innerText = "Settings"; title.innerText = "Settings";
const settingsTemplateCopy = settingsTemplate.content.cloneNode(true); const settingsTemplateCopy = settingsTemplate.content.cloneNode(true);
const silksongPathInput = settingsTemplateCopy.getElementById("silksong-path-input"); const silksongPathInput = settingsTemplateCopy.getElementById("silksong-path-input");
const nexusAPIInput = settingsTemplateCopy.getElementById("nexus-api-input"); const nexusAPIForm = settingsTemplateCopy.getElementById("nexus-api-form");
const versionsList = settingsTemplateCopy.getElementById("versions-list"); const versionsList = settingsTemplateCopy.getElementById("versions-list");
const versionsDictionnary = { const versionsDictionnary = {
"Silk-Fly-Launcher": `Silk Fly Launcher: v${await versions.silkFlyLauncher()}`, "Silk-Fly-Launcher": `Silk Fly Launcher: v${await versions.silkFlyLauncher()}`,
@@ -257,10 +257,8 @@ async function navigate(page) {
files.saveSilksongPath(silksongPath); files.saveSilksongPath(silksongPath);
}); });
nexusAPIInput.value = await files.loadNexusAPI(); nexusAPIForm.addEventListener("submit", function (event) {
nexusAPIInput.addEventListener("input", async function (event) { event.preventDefault();
let nexusAPI = nexusAPIInput.value;
files.saveNexusAPI(nexusAPI);
}); });
for (const element of versionsList.children) { for (const element of versionsList.children) {
@@ -286,7 +284,7 @@ async function navigate(page) {
setBepinexVersion(); setBepinexVersion();
setThemeButton(); setThemeButton();
toggleSelectedListButton("themes-menu", actualTheme[0]); toggleSelectedListButton("themes-menu", actualTheme[0]);
verifyNexusAPI(); setNexusAPI();
break; break;
} }
} }
@@ -321,18 +319,18 @@ async function welcomeNavigate() {
case 2: case 2:
pageDiv.appendChild(nexusTemplate.content.cloneNode(true)); pageDiv.appendChild(nexusTemplate.content.cloneNode(true));
const nexusLink = document.getElementById("external-link"); const nexusLink = document.getElementById("external-link");
const nexusAPIForm = document.getElementById("nexus-api-form");
nexusLink.addEventListener("click", function (event) { nexusLink.addEventListener("click", function (event) {
event.preventDefault(); event.preventDefault();
const url = nexusLink.href; const url = nexusLink.href;
electronAPI.openExternalLink(url); electronAPI.openExternalLink(url);
}); });
const nexusAPIInput = document.getElementById("nexus-api-input"); nexusAPIForm.addEventListener("submit", function (event) {
nexusAPIInput.value = await files.loadNexusAPI(); event.preventDefault();
nexusAPIInput.addEventListener("input", async function (event) {
let nexusAPI = nexusAPIInput.value;
await files.saveNexusAPI(nexusAPI);
}); });
setNexusAPI();
break; break;
case 3: case 3:
@@ -372,7 +370,6 @@ async function initialImportData() {
async function importData() { async function importData() {
await files.import(); await files.import();
document.getElementById("silksong-path-input").value = await files.loadSilksongPath(); document.getElementById("silksong-path-input").value = await files.loadSilksongPath();
document.getElementById("nexus-api-input").value = await files.loadNexusAPI();
const lacePinCheckbox = document.getElementById("lace-pin"); const lacePinCheckbox = document.getElementById("lace-pin");
const theme = await files.loadTheme(); const theme = await files.loadTheme();
lacePinCheckbox.checked = theme[1]; lacePinCheckbox.checked = theme[1];
@@ -391,7 +388,6 @@ async function deleteData() {
toggleThemesMenu(); toggleThemesMenu();
await files.delete(); await files.delete();
document.getElementById("silksong-path-input").value = await files.loadSilksongPath(); document.getElementById("silksong-path-input").value = await files.loadSilksongPath();
document.getElementById("nexus-api-input").value = await files.loadNexusAPI();
} }
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
@@ -470,6 +466,32 @@ async function searchNexusMods() {
searchInput.value = searchValueNexus; searchInput.value = searchValueNexus;
} }
async function setNexusAPI() {
const nexusAPIInput = document.getElementById("nexus-api-input");
const secretString = "●".repeat(1000);
if (!(await files.loadNexusAPI())) {
if (nexusAPIInput.value && nexusAPIInput.value != secretString) {
await files.saveNexusAPI(nexusAPIInput.value);
nexusAPIInput.value = secretString;
nexusAPIInput.readOnly = true;
} else {
nexusAPIInput.value = "";
nexusAPIInput.readOnly = false;
}
} else {
nexusAPIInput.value = secretString;
nexusAPIInput.readOnly = true;
}
verifyNexusAPI();
}
async function resetNexusAPI() {
if (await files.loadNexusAPI()) {
await files.saveNexusAPI(undefined);
setNexusAPI();
}
}
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
/////////////////////// THEMES /////////////////////// /////////////////////// THEMES ///////////////////////

View File

@@ -436,7 +436,7 @@ body {
color: #ff9800; color: #ff9800;
} }
.search-form { .input-form {
display: flex; display: flex;
flex: 1; flex: 1;
} }

View File

@@ -16,6 +16,7 @@
<div id="page"></div> <div id="page"></div>
<div class="button-div" id="button-div"></div> <div class="button-div" id="button-div"></div>
</div> </div>
<div class="toast-div" id="toast-div"></div>
</div> </div>
<template id="welcome-template"> <template id="welcome-template">
@@ -53,10 +54,12 @@
<a href="https://www.nexusmods.com/settings/api-keys" class="link" id="external-link">https://www.nexusmods.com/settings/api-keys</a> and click on new personnal API key <a href="https://www.nexusmods.com/settings/api-keys" class="link" id="external-link">https://www.nexusmods.com/settings/api-keys</a> and click on new personnal API key
</p> </p>
<div class="horizontal-div"> <div class="horizontal-div">
<label for="nexus-api-label">Enter your nexus api: </label>
<input type="text" class="input" id="nexus-api-input" name="nexus-api-input" />
<img class="nexus-check-image" id="nexus-check-image" src="assets/icons/cross.svg" /> <img class="nexus-check-image" id="nexus-check-image" src="assets/icons/cross.svg" />
<button class="default-button" onclick="verifyNexusAPI()">Verify</button> <form class="horizontal-div input-form" id="nexus-api-form">
<input class="input" id="nexus-api-input" type="text" placeholder="Enter your nexus api" />
<button class="default-button" onclick="setNexusAPI()">Set</button>
</form>
<button class="default-button" onclick="resetNexusAPI()">Reset</button>
</div> </div>
</template> </template>