diff --git a/jsconfig.json b/jsconfig.json index 7f3328e..5f8b5a2 100755 --- a/jsconfig.json +++ b/jsconfig.json @@ -6,4 +6,4 @@ "strictNullChecks": true, "strictFunctionTypes": true } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 96025bd..cbd9cfd 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "HugoAura", - "version": "0.1.1-pre-II", + "version": "0.1.1-pre-III", "description": "Aura for SeewoHugo", "main": "app.asar/main.js", "dependencies": {}, diff --git a/src/aura/init/main/ipcHandler.js b/src/aura/init/main/ipcHandler.js index 5d4f69b..759aea1 100755 --- a/src/aura/init/main/ipcHandler.js +++ b/src/aura/init/main/ipcHandler.js @@ -11,7 +11,7 @@ const buildIpcMain = (electron) => { /** * @type {import("../../types/main/electron").AuraIPCMain} */ - // @ts-ignore + // @ts-expect-error const ipcMain = electron.ipcMain; /** @@ -28,11 +28,33 @@ const buildIpcMain = (electron) => { * @param {string} chan * @param {any} targetData */ + if (!global.__HUGO_AURA__.hookedWindows) { + return { + success: false, + }; + } + const sendDataToWebContents = (key, chan, targetData) => { const webContents = - global.__HUGO_AURA__.hookedWindows.get(key).webContents; + // @ts-expect-error + global.__HUGO_AURA__.hookedWindows.get(key)?.webContents; - if (grep !== webContents) webContents.send(chan, targetData); + if (!webContents) { + console.error( + `[HugoAura / Main / IPC / ERROR] Failed sending data to ${key}: WebContents not found` + ); + return { + success: false, + }; + } + + if (grep !== webContents) { + webContents.send(chan, targetData); + } + + return { + success: true, + }; }; if (windowKey === "*") { @@ -51,6 +73,7 @@ const buildIpcMain = (electron) => { } }; + const { applyConfigIpcHandler } = require("./ipcModules/configIpcHandler"); const { applyPlsIpcHandler } = require("./ipcModules/plsIpcHandler"); ipcMain.handle("$aura.base.restartApplication", async () => { @@ -58,6 +81,7 @@ const buildIpcMain = (electron) => { app.exit(0); }); + applyConfigIpcHandler(ipcMain); applyPlsIpcHandler(ipcMain); }; diff --git a/src/aura/init/main/ipcModules/configIpcHandler.js b/src/aura/init/main/ipcModules/configIpcHandler.js new file mode 100755 index 0000000..8443cdb --- /dev/null +++ b/src/aura/init/main/ipcModules/configIpcHandler.js @@ -0,0 +1,79 @@ +// @ts-check + +const __SCOPE = "main"; + +/** + * + * @param {import("../../../types/main/electron").AuraIPCMain} ipcMain + */ +const applyConfigIpcHandler = (ipcMain) => { + const methodBase = "$aura.config"; + + const mainEventBus = global.__HUGO_AURA_EVENT_BUS__; + + const ConfigManager = require("../../shared/configManager"); + const configManager = global.__HUGO_AURA_CONFIG_MGR__ + ? global.__HUGO_AURA_CONFIG_MGR__ + : new ConfigManager(); + + ipcMain.on(`${methodBase}.refreshMainConfig`, (_event) => { + mainEventBus.emit("$aura.config.refreshConfig"); + }); + + ipcMain.handle( + `${methodBase}.setConfigEncSettings`, + ( + /** @type {import("electron").IpcMainInvokeEvent} */ _event, + /** @type {{ target: boolean }} */ arg + ) => { + mainEventBus.emit("$aura.config.updateConfigEncSettings", arg.target); + return { + success: true, + }; + } + ); + + ipcMain.on(`${methodBase}.getConfigFromMainSync`, (event, _arg) => { + if ( + global.__HUGO_AURA_CONFIG__ && + Object.keys(global.__HUGO_AURA_CONFIG__).length !== 0 + ) { + event.returnValue = { + success: true, + data: global.__HUGO_AURA_CONFIG__, + }; + } else { + console.warn( + "[HugoAura / Main / IPC / Config / WARN] Global config var not found!" + ); + event.returnValue = { + success: false, + data: {}, + }; + } + }); + + ipcMain.handle( + `${methodBase}.dispatchConfigFromRenderer`, + (_event, /** @type {{data: string, writeConfig?: boolean}} */ arg) => { + const parsedData = JSON.parse(arg.data); + + global.__HUGO_AURA_CONFIG__ = parsedData; + + if (arg.writeConfig) { + const result = configManager.writeConfig(parsedData); + if (!result) { + return { + success: false, + }; + } + } + + return { + success: true, + }; + } + ); +}; + +module.exports = { applyConfigIpcHandler }; diff --git a/src/aura/init/main/ipcModules/plsIpcHandler.js b/src/aura/init/main/ipcModules/plsIpcHandler.js index cb27fff..bbbc5dd 100755 --- a/src/aura/init/main/ipcModules/plsIpcHandler.js +++ b/src/aura/init/main/ipcModules/plsIpcHandler.js @@ -52,7 +52,7 @@ const applyPlsIpcHandler = (ipcMain) => { `${methodBase}.getPlsStats`, /** * - * @returns {{ success: boolean; data: PLSStatus; }} + * @returns {{ success: boolean; data: PLSStatus | null | undefined; }} */ (_event, _arg) => { return { @@ -83,7 +83,7 @@ const applyPlsIpcHandler = (ipcMain) => { `${methodBase}.getPlsSettings`, /** * - * @returns {{ success: boolean; data: Record }} + * @returns {{ success: boolean; data: Record | null | undefined }} */ (_event, _arg) => { return { @@ -113,7 +113,7 @@ const applyPlsIpcHandler = (ipcMain) => { `${methodBase}.getPlsRules`, /** * - * @returns {{ success: boolean; data: Record }} + * @returns {{ success: boolean; data: Record | null | undefined }} */ (_event, _arg) => { return { diff --git a/src/aura/init/preload/webpackHook.js b/src/aura/init/preload/webpackHook.js index 1ec41b5..7656d06 100755 --- a/src/aura/init/preload/webpackHook.js +++ b/src/aura/init/preload/webpackHook.js @@ -1,4 +1,4 @@ -const path = require('path'); +const path = require("path"); class WebpackHook { #ruleCache = new Map(); @@ -19,7 +19,7 @@ class WebpackHook { if (!rule) { rule = require(path.join( __dirname, - '../../../aura/jsRewrite/', + "../../../aura/jsRewrite/", rulePath )); this.#ruleCache.set(rulePath, rule); @@ -49,7 +49,7 @@ class WebpackHook { patchModules(modules, rewrites) { modules.forEach((mod, index) => { - if (typeof mod !== 'function') return; + if (typeof mod !== "function") return; const stringifyFunc = mod.toString(); rewrites.forEach((rewrite) => { @@ -67,14 +67,14 @@ class WebpackHook { let rewrittenFunction = mod; switch (method) { - case 'reactComponent': + case "reactComponent": window.__HUGO_AURA_HOOK__[ruleId] = { feature: rewrite.feature, - newFunction: rewrite.newFunction - } + newFunction: rewrite.newFunction, + }; rewrittenFunction = rewrite.preHook(mod); break; - case 'legacy': + case "legacy": default: rewrittenFunction = rewrite.newFunction; break; @@ -95,7 +95,7 @@ class WebpackHook { }); }); - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { window.__HUGO_AURA_DEBUG__ = { getRuleCache: () => Array.from(this.#ruleCache.keys()), }; @@ -105,7 +105,7 @@ class WebpackHook { installHook(window, config) { let realWebpackJsonp = window.webpackJsonp; - Object.defineProperty(window, 'webpackJsonp', { + Object.defineProperty(window, "webpackJsonp", { get: () => realWebpackJsonp, set: (value) => { console.log( @@ -119,7 +119,7 @@ class WebpackHook { if (args[0] && Array.isArray(args[0][1])) { const [chunkIds, modules] = args[0]; console.log( - `[HugoAura / AppHook] Intercepting chunk ${chunkIds.join(', ')}` + `[HugoAura / AppHook] Intercepting chunk ${chunkIds.join(", ")}` ); const rewrites = this.loadRewriteRules(config); diff --git a/src/aura/init/rendererHook/uiHooksManager.js b/src/aura/init/rendererHook/uiHooksManager.js index 3e8e462..463bf31 100755 --- a/src/aura/init/rendererHook/uiHooksManager.js +++ b/src/aura/init/rendererHook/uiHooksManager.js @@ -87,7 +87,9 @@ class RendererHooksManager { return; } - console.log(`[HugoAura / Init / RDH] UI Hook is initializing for ${windowKey}...`); + console.log( + `[HugoAura / Init / RDH] UI Hook is initializing for ${windowKey}...` + ); console.log( `[HugoAura / Init / RDH] UI Hook loaded at: ${new Date().toISOString()}` ); diff --git a/src/aura/init/shared/configManager.js b/src/aura/init/shared/configManager.js index 73c1d2e..5ba180a 100755 --- a/src/aura/init/shared/configManager.js +++ b/src/aura/init/shared/configManager.js @@ -6,6 +6,9 @@ const os = require("os"); const crypto = require("crypto"); const childProc = require("child_process"); +const RegistryManagerClass = require("./registryManager"); +const registryManager = new RegistryManagerClass(); + // Constants const CRYPTO_SETTINGS_AES = { @@ -18,6 +21,7 @@ const CRYPTO_SETTINGS_AES = { obfuscateStr: "eCybsseK", hash: "sha256", }; +const LMAK_SETTINGS_BASE = "EncSettings\\LMAK"; /** * @@ -70,14 +74,35 @@ class ConfigManager { this.encConfigPath = path.join(this.configDir, ".cache_2eafc8d0.dat"); // (雾 /* ↑ 不使用 .tmp 扩展名, 不然容易真被清理了 */ this.defaultConfigPath = path.join(__dirname, "default.json"); + this.useEncConfig = false; + this.isConfigReadFailed = false; + this.side = "unknown"; + + if (fs.existsSync(this.configPath)) { + this.useEncConfig = false; + } else { + this.useEncConfig = true; + } + + if (global.__HUGO_AURA_EVENT_BUS__) { + // Expect always true + global.__HUGO_AURA_EVENT_BUS__.on( + "$aura.config.updateConfigEncSettings", + (/** @type {boolean} */ newVal) => { + this.useEncConfig = newVal; + } + ); + } } getHugoAuraConfigPath() { - return path.dirname(this.configPath); + return path.dirname( + this.useEncConfig ? this.encConfigPath : this.configPath + ); } getConfigPath() { - return this.configPath; + return this.useEncConfig ? this.encConfigPath : this.configPath; } getDefaultConfig() { @@ -99,7 +124,7 @@ class ConfigManager { fs.mkdirSync(hugoAuraPath, { recursive: true }); } - if (!fs.existsSync(this.configPath)) { + if (!fs.existsSync(this.configPath) && !fs.existsSync(this.encConfigPath)) { console.log("[HugoAura / Config] Creating default config file"); const defaultConfig = this.getDefaultConfig(); this.writeConfig(defaultConfig); @@ -108,11 +133,30 @@ class ConfigManager { readConfig() { try { - const config = JSON.parse(fs.readFileSync(this.configPath, "utf8")); - console.log("[HugoAura / Config] Successfully loaded config:", config); + let config = {}; + if (this.useEncConfig) { + const hashedPasswdResultObj = this.retrieveEncPassword(); + if (hashedPasswdResultObj.success && hashedPasswdResultObj.data) { + config = this.decryptConfig(hashedPasswdResultObj.data).data; + + if (!config) { + this.isConfigReadFailed = true; + return this.getDefaultConfig(); // should be changed, too + } + } else { + console.error("[HugoAura / Config / ERROR] Failed to decrypt config"); + this.isConfigReadFailed = true; + return this.getDefaultConfig(); // This behaviour should be changed later + } + } else { + config = JSON.parse(fs.readFileSync(this.configPath, "utf8")); + } + // console.log("[HugoAura / Config] Successfully loaded config:", config); + if (this.isConfigReadFailed) this.isConfigReadFailed = false; return config; } catch (err) { console.error("[HugoAura / Config] Failed to read config:", err); + this.isConfigReadFailed = true; return this.getDefaultConfig(); } } @@ -120,15 +164,32 @@ class ConfigManager { /** * * @param {Record} config - * @returns + * @returns {boolean} */ writeConfig(config) { try { - fs.writeFileSync( - this.configPath, - JSON.stringify(config, null, 2), - "utf8" - ); + if (this.useEncConfig) { + const hashedPasswdResultObj = this.retrieveEncPassword(); + if (hashedPasswdResultObj.success && hashedPasswdResultObj.data) { + this.encryptConfig(config, hashedPasswdResultObj.data); + } else { + console.error( + "[HugoAura / Config / Write / ERROR] Failed to write config: Retrieve enc password failed" + ); + return false; + } + } else { + fs.writeFileSync( + this.configPath, + JSON.stringify(config, null, 2), + "utf8" + ); + } + + if (this.side === "renderer") { + global.ipcRenderer.send("$aura.config.refreshMainConfig"); + } + return true; } catch (err) { console.error("[HugoAura / Config] Failed to write config:", err); @@ -140,8 +201,8 @@ class ConfigManager { let defaultConfig = this.getDefaultConfig(); let config = {}; try { - if (fs.existsSync(this.configPath)) { - const userConfig = JSON.parse(fs.readFileSync(this.configPath, "utf8")); + if (fs.existsSync(this.configPath) || fs.existsSync(this.encConfigPath)) { + const userConfig = this.readConfig(); if (global.__HUGO_AURA__.configInit) { config = userConfig; return userConfig; @@ -153,18 +214,236 @@ class ConfigManager { } } catch (err) { console.error("[HugoAura / Config] Failed to load user config:", err); + this.isConfigReadFailed = true; config = defaultConfig; } return config; } + priv_getMacAddr() { + const netInf = os.networkInterfaces(); + const realInfs = Object.keys(netInf).filter( + (key) => + !key.includes("Pseudo") && + !key.includes("Loopback") && + !key.includes("Virtual") && + !key.includes("Tunnel") && + !key.includes("Cisco") && + !key.includes("VPN") + ); + + /** + * + * @param {string[]} infNames + * @param {Record} infObj + * @returns + */ + const getValidInfMac = (infNames, infObj) => { + for (const name of infNames) { + const target = infObj[name][0]; + const isValid = !target.internal && target.mac !== "00:00:00:00:00:00"; + if (isValid) { + return target.mac; + } + } + return null; + }; + + const rawInfMac = getValidInfMac(realInfs, netInf); + const macAddr = rawInfMac + ? rawInfMac.replace(/:/g, "").toUpperCase() + : null; + return macAddr; + } + + /** + * + * @param {SHA256EncryptedPassword} password + */ + saveEncPassword(password) { + let macAddr = this.priv_getMacAddr(); + let fallbackToStaticKey = false; + + if (!macAddr) { + console.warn( + "[HugoAura / Config / LMK] No valid network inf found, fallback to static key." + ); + macAddr = Buffer.from(crypto.randomBytes(6)) + .toString("hex") + .toUpperCase(); + } + + const randomSalt = crypto.randomBytes(CRYPTO_SETTINGS_AES.saltLength); + const key = crypto.scryptSync(macAddr, randomSalt, 32); + const iv = crypto.randomBytes(CRYPTO_SETTINGS_AES.ivLength); + + const cipherIns = crypto.createCipheriv(CRYPTO_SETTINGS_AES.mode, key, iv, { + // @ts-expect-error + authTagLength: CRYPTO_SETTINGS_AES.tagLength, + }); + + let encryptedPassword = cipherIns.update(password, "utf-8", "hex"); + encryptedPassword += cipherIns.final("hex"); + + const authTagHex = cipherIns.getAuthTag().toString("hex"); + const ivHex = iv.toString("hex"); + const saltHex = randomSalt.toString("hex"); + + registryManager.createOrUpdateRegKey( + LMAK_SETTINGS_BASE, + "LMAK_Value", + encryptedPassword, + true + ); + registryManager.createOrUpdateRegKey( + LMAK_SETTINGS_BASE, + "LMAK_IV", + ivHex, + true + ); + registryManager.createOrUpdateRegKey( + LMAK_SETTINGS_BASE, + "LMAK_Salt", + saltHex, + true + ); + registryManager.createOrUpdateRegKey( + LMAK_SETTINGS_BASE, + "LMAK_AuthTag", + authTagHex, + true + ); + + if (fallbackToStaticKey) { + registryManager.createOrUpdateRegKey( + LMAK_SETTINGS_BASE, + "LMAK_FakeMac", + macAddr, + true + ); + } + + return true; + } + + retrieveEncPassword() { + try { + const authTagHex = registryManager.readRegKey( + LMAK_SETTINGS_BASE, + "LMAK_AuthTag", + true + )?.data; + const ivHex = registryManager.readRegKey( + LMAK_SETTINGS_BASE, + "LMAK_IV", + true + )?.data; + const saltHex = registryManager.readRegKey( + LMAK_SETTINGS_BASE, + "LMAK_Salt", + true + )?.data; + const encPasswdHex = registryManager.readRegKey( + LMAK_SETTINGS_BASE, + "LMAK_Value", + true + )?.data; + let isStaticKey = false; + let macAddr = null; + + try { + macAddr = registryManager.readRegKey( + LMAK_SETTINGS_BASE, + "LMAK_FakeMac", + true + )?.data; + if (!macAddr) { + isStaticKey = false; + } else { + isStaticKey = true; + } + } catch { + isStaticKey = false; + } + + if (!isStaticKey) { + macAddr = this.priv_getMacAddr(); + + if (!macAddr) { + console.error( + "[HugoAura / Config / ERROR] Failed to retrieve password from reg: MAC Address invalid." + ); + return { + success: false, + data: null, + error: new Error("Mac is null or undefined"), + }; + } + } + + if (!saltHex || !ivHex || !authTagHex || !encPasswdHex) { + console.error( + "[HugoAura / Config / ERROR] Failed to retrieve password from reg: Reg keys invalid." + ); + return { + success: false, + data: null, + error: new Error("Reg key invalid"), + }; + } + const salt = Buffer.from(saltHex, "hex"); + const iv = Buffer.from(ivHex, "hex"); + const authTag = Buffer.from(authTagHex, "hex"); + const encPasswd = Buffer.from(encPasswdHex, "utf-8").toString(); + + const key = crypto.scryptSync(macAddr, salt, 32); + const decipherIns = crypto.createDecipheriv( + CRYPTO_SETTINGS_AES.mode, + key, + iv, + { + // @ts-expect-error + authTagLength: CRYPTO_SETTINGS_AES.tagLength, + } + ); + + decipherIns.setAuthTag(authTag); + + const result = Buffer.concat([ + decipherIns.update(encPasswd, "hex"), + decipherIns.final(), + ]).toString(); + + return { + success: true, + data: result, + error: null, + }; + } catch (e) { + console.error( + "[HugoAura / Config / ERROR] Unexpected error occurred while retrieving password from reg, error:", + e + ); + return { + success: false, + data: null, + error: e, + }; + } + } + + clearEncPasswdRegKey() { + registryManager.delRegKey(LMAK_SETTINGS_BASE, null); + } + /** * * @param {Record} configData - * @param {string} passwd + * @param {SHA256EncryptedPassword} passwd */ encryptConfig(configData, passwd) { + registryManager.initRegistry(); const salt = crypto.randomBytes(CRYPTO_SETTINGS_AES.saltLength); const key = crypto.pbkdf2Sync( passwd, @@ -198,15 +477,13 @@ class ConfigManager { try { fs.writeFileSync(this.encConfigPath, base64EncConfig, "utf-8"); - // fs.rmSync(this.configPath); + try { + fs.unlinkSync(this.configPath); + } catch { + console.debug("[HugoAura / Config] Dec config not exists, skipping..."); + } - const _hideFileProc = childProc.spawnSync( - "cmd.exe", - ["/c", "attrib", "+h", this.encConfigPath], - { - stdio: "inherit", - } - ); + if (!this.useEncConfig) this.useEncConfig = true; return true; } catch (err) { console.error( @@ -223,95 +500,169 @@ class ConfigManager { /** * - * @param {string} passwd - * @returns + * @param {SHA256EncryptedPassword} passwd + * @returns {{success: boolean, data: AuraConfig}} */ decryptConfig(passwd) { - const FAILED_RET = { - success: false, - data: {}, - }; - - let base64EncConfig = null; - try { - if (!fs.existsSync(this.encConfigPath)) { - return FAILED_RET; - } - base64EncConfig = fs.readFileSync(this.encConfigPath, "utf-8"); - } catch (err) { - console.error( - "[HugoAura / Config] Failed to read encrypted config:", - err - ); - return FAILED_RET; - } + const FAILED_RET = { + success: false, + data: {}, + }; + + let base64EncConfig = null; - if (base64EncConfig) { - const strip64EncCfg = base64EncConfig.split( - CRYPTO_SETTINGS_AES.obfuscateStr - )[1]; - const encryptCfg = Buffer.from(strip64EncCfg, "base64").toString("utf-8"); - /** @type {null | EncryptedConfig} */ - let parsedEncCfg = null; try { - parsedEncCfg = JSON.parse(encryptCfg); - } catch (err) { - console.error( - "[HugoAura / Config] Failed to parse encrypted config:", - err - ); - console.error("[HugoAura / Config] Pending data:", encryptCfg); - } - if (parsedEncCfg === null) return FAILED_RET; - - const salt = Buffer.from(parsedEncCfg.salt, "base64"); - const iv = Buffer.from(parsedEncCfg.iv, "base64"); - - const authTag = Buffer.from(parsedEncCfg.authTag, "base64"); - - const key = crypto.pbkdf2Sync( - passwd, - salt, - CRYPTO_SETTINGS_AES.keyIter, - CRYPTO_SETTINGS_AES.keyLength, - CRYPTO_SETTINGS_AES.hash - ); - - const decipherIns = crypto.createDecipheriv( - CRYPTO_SETTINGS_AES.mode, - key, - iv, - { - // @ts-expect-error - authTagLength: CRYPTO_SETTINGS_AES.tagLength, + if (!fs.existsSync(this.encConfigPath)) { + return FAILED_RET; } - ); - - decipherIns.setAuthTag(authTag); - - let stringifyDecCfg = Buffer.concat([ - decipherIns.update(parsedEncCfg.content, "hex"), - decipherIns.final(), - ]).toString(); - - /** @type {null | Record} */ - let decConfig = null; - try { - decConfig = JSON.parse(stringifyDecCfg); + base64EncConfig = fs.readFileSync(this.encConfigPath, "utf-8"); } catch (err) { console.error( - "[HugoAura / Config] Failed to parse decrypted config:", + "[HugoAura / Config] Failed to read encrypted config:", err ); - console.error("[HugoAura / Config] Pending data:", decConfig); return FAILED_RET; } - if (decConfig === null) return FAILED_RET; - console.debug(decConfig); + if (base64EncConfig) { + const strip64EncCfg = base64EncConfig.split( + CRYPTO_SETTINGS_AES.obfuscateStr + )[1]; + const encryptCfg = Buffer.from(strip64EncCfg, "base64").toString( + "utf-8" + ); + /** @type {null | EncryptedConfig} */ + let parsedEncCfg = null; + try { + parsedEncCfg = JSON.parse(encryptCfg); + } catch (err) { + console.error( + "[HugoAura / Config] Failed to parse encrypted config:", + err + ); + console.error("[HugoAura / Config] Pending data:", encryptCfg); + } + if (parsedEncCfg === null) return FAILED_RET; + + const salt = Buffer.from(parsedEncCfg.salt, "base64"); + const iv = Buffer.from(parsedEncCfg.iv, "base64"); + + const authTag = Buffer.from(parsedEncCfg.authTag, "base64"); + + const key = crypto.pbkdf2Sync( + passwd, + salt, + CRYPTO_SETTINGS_AES.keyIter, + CRYPTO_SETTINGS_AES.keyLength, + CRYPTO_SETTINGS_AES.hash + ); + + const decipherIns = crypto.createDecipheriv( + CRYPTO_SETTINGS_AES.mode, + key, + iv, + { + // @ts-expect-error + authTagLength: CRYPTO_SETTINGS_AES.tagLength, + } + ); + + decipherIns.setAuthTag(authTag); + + const stringifyDecCfg = Buffer.concat([ + decipherIns.update(parsedEncCfg.content, "hex"), + decipherIns.final(), + ]).toString(); + + /** @type {null | Record} */ + let decConfig = null; + try { + decConfig = JSON.parse(stringifyDecCfg); + } catch (err) { + console.error( + "[HugoAura / Config] Failed to parse decrypted config:", + err + ); + console.error("[HugoAura / Config] Pending data:", decConfig); + return FAILED_RET; + } + if (decConfig === null) return FAILED_RET; + + // console.debug(decConfig); + + return { + success: true, + data: decConfig, + }; + } else { + console.error( + "[HugoAura / Config] Unexpected error occurred while decrypting config: base64EncConfig is undefined" + ); + return FAILED_RET; + } + } catch (e) { + console.error( + "[HugoAura / Config] Unexpected error occurred while decrypting config:", + e + ); + return { + success: false, + data: {}, + }; } } + + /** + * + * @param {Record | null} curConfig + * @param {SHA256EncryptedPassword | undefined | null} passwd + * @returns {{success: boolean}} + */ + switchToDecConfig(curConfig, passwd = null) { + let decConfig = null; + if (!curConfig && passwd) { + const getDecConfigResult = this.decryptConfig(passwd); + if ( + !getDecConfigResult?.success || + !getDecConfigResult.data || + Object.keys(getDecConfigResult.data).length === 0 + ) { + console.error( + "[HugoAura / Config] Failed to switch to decrypted config: Error decrypting config" + ); + return { + success: false, + }; + } + decConfig = getDecConfigResult.data; + } + + this.useEncConfig = false; + this.clearEncPasswdRegKey(); + // @ts-expect-error + this.writeConfig(curConfig ? curConfig : decConfig); + try { + fs.unlinkSync(this.encConfigPath); + } catch { + console.debug("[HugoAura / Config] Enc config not exists, skipping..."); + } + global.__HUGO_AURA_EVENT_BUS__.emit( + "$aura.config.updateConfigEncSettings", + false + ); + if (this.side === "renderer") { + global.ipcRenderer.invoke("$aura.config.setConfigEncSettings", { + target: false, + }); + global.ipcRenderer.invoke("$aura.config.dispatchConfigFromRenderer", { + data: JSON.stringify(curConfig), + }); + } + return { + success: true, + }; + } } -module.exports = new ConfigManager(); +module.exports = ConfigManager; diff --git a/src/aura/init/shared/default.json b/src/aura/init/shared/default.json index 6bb4f9c..740256a 100755 --- a/src/aura/init/shared/default.json +++ b/src/aura/init/shared/default.json @@ -7,7 +7,7 @@ "passwordWithSalt": "89f6c4d57d0202a05c32d37cc6a2c6a0", "salt": "aura" }, - "authModeRewrite": "none" + "authModeRewrite": "default" }, "vendor/screenLock": { "enabled": true, @@ -30,9 +30,8 @@ "auraSettings": { "settingsPasswordEnabled": false, "settingsPasswordWithSalt": "32703D292460CC9A3B867494D6AD9A8E4A3ADF0FAA4D6867BC4D412CC3927D02E47C6D0B1763BB53E57B2241C6193433561CDA09D7C48CA03983072B876F0965", - "appearance": { - "enablePasswdDialogBlur": true - } + "encryptConfig": false, + "appearance": {} }, "devTools": false } diff --git a/src/aura/init/shared/registryManager.js b/src/aura/init/shared/registryManager.js new file mode 100755 index 0000000..42f5e92 --- /dev/null +++ b/src/aura/init/shared/registryManager.js @@ -0,0 +1,224 @@ +// @ts-check + +const childProc = require("child_process"); + +// Constants + +const LOG_PREFIX = "[HugoAura / Init / Reg"; +const LOG_PREFIX_FUNC = "[HugoAura / Reg"; +const AURA_REGISTRY_PATH = ["HKCU", "SOFTWARE", "HugoAura"].join("\\"); + +class RegistryManager { + /** + * @param {string} [path] + */ + handleCreateReg(path) { + try { + const createResult = childProc.execSync(["reg", "add", path].join(" "), { + encoding: "utf8", + }); + + if (createResult) { + console.log( + `${LOG_PREFIX} / SUCCESS] Registry path ${path} successfully created.` + ); + console.debug( + `${LOG_PREFIX} / DEBUG] Reg add command stdout:`, + createResult + ); + return true; + } + } catch (e) { + console.error( + `${LOG_PREFIX} / ERROR] Failed creating registry path, error:`, + e + ); + return false; + } + } + + initRegistry() { + try { + const queryResult = childProc.execSync( + ["reg", "query", AURA_REGISTRY_PATH].join(" "), + { encoding: "utf8" } + ); + + if (queryResult) { + console.log(`${LOG_PREFIX}] Registry check up success.`); + console.debug(`${LOG_PREFIX}] Command stdout:`, queryResult); + return true; + } + } catch (e) { + console.warn(`${LOG_PREFIX} / WARN] Failed to query registry, error:`, e); + return this.handleCreateReg(AURA_REGISTRY_PATH); + } + } + + /** + * + * @param {string} relativePath + * @param {string} keyName + * @param {string} keyVal + * @param {boolean | undefined} silent + */ + createOrUpdateRegKey(relativePath, keyName, keyVal, silent = false) { + try { + const result = childProc.execSync( + [ + "reg", + "add", + [AURA_REGISTRY_PATH, relativePath].join("\\"), + "/v", + keyName, + "/t", + "REG_SZ", + "/d", + `\"${keyVal}\"`, + "/f", + ].join(" "), + { encoding: "utf8" } + ); + + if (result) { + if (!silent) { + console.debug( + `${LOG_PREFIX_FUNC} / SUCCESS] Successfully created / updated reg key ${relativePath}/${keyName} with data: ${keyVal}` + ); + console.debug( + `${LOG_PREFIX_FUNC} / SUCCESS] Add key command stdout:`, + result + ); + } + return { + success: true, + error: null, + }; + } + } catch (e) { + console.error( + `${LOG_PREFIX_FUNC} / ERROR] Failed to create / update reg key, error:`, + silent ? "" : e + ); + return { + success: false, + error: e, + }; + } + } + + /*>>> BUC <<< + keyName === null --> delete the whole entry + >>> EUC <<<*/ + /** + * + * @param {string} relativePath + * @param {string | null} keyName + * @param {boolean | undefined} silent + */ + delRegKey(relativePath, keyName, silent = false) { + if (keyName === undefined) { + throw new Error( + `${LOG_PREFIX_FUNC} / CRITICAL] Arg \"keyName\" for function \"delRegKey\" cannot be undefined. Only null or string accepted.` + ); + } + + try { + const result = childProc.execSync( + [ + "reg", + "delete", + [AURA_REGISTRY_PATH, relativePath].join("\\"), + keyName ? "/v" : "", + keyName ? keyName : "", + "/f", + ].join(" "), + { encoding: "utf8" } + ); + + if (result) { + if (!silent) { + console.debug( + `${LOG_PREFIX_FUNC} / SUCCESS] Successfully deleted reg key ${relativePath}/${keyName}` + ); + console.debug( + `${LOG_PREFIX_FUNC} / SUCCESS] Delete key command stdout:`, + result + ); + } + return { + success: true, + error: null, + }; + } + } catch (e) { + console.error( + `${LOG_PREFIX_FUNC} / ERROR] Failed to delete reg key, error:`, + silent ? "" : e + ); + return { + success: false, + error: e, + }; + } + } + + /** + * + * @param {string} relativePath + * @param {string} keyName + * @param {boolean | undefined} silent + */ + readRegKey(relativePath, keyName, silent = false) { + try { + const readResult = childProc.execSync( + [ + "reg", + "query", + [AURA_REGISTRY_PATH, relativePath].join("\\"), + "/v", + `\"${keyName}\"`, + ].join(" "), + { encoding: "utf8" } + ); + + if (readResult) { + if (!silent) { + console.debug( + `${LOG_PREFIX}] Successfully read reg key ${relativePath}/${keyName}, stdout:`, + readResult + ); + } + const match = readResult.match(/REG_SZ\s+(.+)/); + + if (!match) { + console.warn(`${LOG_PREFIX_FUNC} / WARN] Data not found in stdout`); + return { + success: false, + data: null, + error: new Error("Data not found"), + }; + } + + const data = match[1].trim(); + return { + success: true, + data, + error: null, + }; + } + } catch (e) { + console.error( + `${LOG_PREFIX_FUNC} / ERROR] Failed to read reg key, error:`, + silent ? "" : e + ); + return { + success: false, + data: null, + error: e, + }; + } + } +} + +module.exports = RegistryManager; diff --git a/src/aura/types/main/core.d.ts b/src/aura/types/main/core.d.ts index 6940586..e019aaa 100755 --- a/src/aura/types/main/core.d.ts +++ b/src/aura/types/main/core.d.ts @@ -21,13 +21,6 @@ type HookedWindowsMap = Map; type HookRequire = any; -type HooksMap = Map; +type UIHooksMap = Map; -interface MainProcessGlobal { - hookedWindows: HookedWindowsMap; - hooks: HooksMap; - configInit: boolean; - plsStats: PLSStatus | null; - plsSettings: Record | null; - plsRules: Record | null; -} +type WindowHooksMap = Map; diff --git a/src/aura/types/render/global.d.ts b/src/aura/types/render/global.d.ts index 14d9161..49a011a 100755 --- a/src/aura/types/render/global.d.ts +++ b/src/aura/types/render/global.d.ts @@ -12,3 +12,6 @@ interface DesktopAssistantHugoAuraGlobal extends HugoAuraGlobal { plsWs: WebSocket | null; plsStats: PLSStatus; } + +type UIFunctionsObject = Record; +type UIReactivesObject = Record; diff --git a/src/aura/types/render/uiHook.d.ts b/src/aura/types/render/uiHook.d.ts index 198b026..97635ce 100755 --- a/src/aura/types/render/uiHook.d.ts +++ b/src/aura/types/render/uiHook.d.ts @@ -23,3 +23,5 @@ interface UIHookConfig { interface UIHookConfigFin extends UIHookConfig { windowName: WindowName; } + +type UIHooksObject = Record; diff --git a/src/aura/types/shared/config.d.ts b/src/aura/types/shared/config.d.ts index 2034ec0..4a61585 100755 --- a/src/aura/types/shared/config.d.ts +++ b/src/aura/types/shared/config.d.ts @@ -1,5 +1,6 @@ type AES256EncryptedConfig = string; type Base64String = string; +type SHA256EncryptedPassword = string; interface EncryptedConfig { content: AES256EncryptedConfig; @@ -7,3 +8,5 @@ interface EncryptedConfig { salt: Base64String; iv: Base64String; } + +type AuraConfig = Record; diff --git a/src/aura/types/shared/global.d.ts b/src/aura/types/shared/global.d.ts new file mode 100755 index 0000000..108ba2a --- /dev/null +++ b/src/aura/types/shared/global.d.ts @@ -0,0 +1,37 @@ +import { IpcRenderer } from "electron"; +import type EventBus from "../../utils/eventBus"; +import { HookedWindowsMap, UIHooksMap, WindowHooksMap } from "../main/core"; +import { UIHooksObject } from "../render/uiHook"; +import ConfigManager from "../../init/shared/configManager"; + +type MainProcessOnlyVal = T; +type RendererProcessOnlyVal = T; + +interface GlobalHugoAuraInfo { + central?: MainProcessOnlyVal<(...args: any) => any>; + configInit: boolean; + hookedWindows?: MainProcessOnlyVal; + ipcInit?: MainProcessOnlyVal; + plsRules?: Record | null; + plsSettings?: Record | null; + plsStats?: PLSStatus | null; + uiHooks?: MainProcessOnlyVal; + windowHooks?: MainProcessOnlyVal; + version: RendererProcessOnlyVal; +} + +type GlobalHugoAuraConfig = AuraConfig; + +declare global { + var ipcRenderer: RendererProcessOnlyVal; + var __HUGO_AURA__: GlobalHugoAuraInfo; + var __HUGO_AURA_CONFIG__: GlobalHugoAuraConfig; + var __HUGO_AURA_CONFIG_MGR__: ConfigManager; + var __HUGO_AURA_EVENT_BUS__: EventBus; + var __HUGO_AURA_DEBUG__: RendererProcessOnlyVal>; + var __HUGO_AURA_GLOBAL__: RendererProcessOnlyVal>; + var __HUGO_AURA_HOOK__: RendererProcessOnlyVal>; + var __HUGO_AURA_LOADER__: RendererProcessOnlyVal; + var __HUGO_AURA_UI_FUNCTIONS__: RendererProcessOnlyVal; + var __HUGO_AURA_UI_REACTIVES__: RendererProcessOnlyVal; +} diff --git a/src/aura/ui/composables/settingsRenderer.js b/src/aura/ui/composables/settingsRenderer.js index 5119c87..a606972 100755 --- a/src/aura/ui/composables/settingsRenderer.js +++ b/src/aura/ui/composables/settingsRenderer.js @@ -141,9 +141,9 @@ const settingsRenderer = (pendingEl, settingsObj, isPls = false) => { const elValue = entry.valueGetter(); switchEl.value = elValue; switchEl.checked = elValue; - switchEl.addEventListener("change", (event) => { + switchEl.addEventListener("change", async (event) => { showToast(entry); - entry.callbackFn(event.target.checked); + await entry.callbackFn(event.target.checked); }); entryOperationArea.classList.add("form-check", "form-switch"); entryOperationArea.appendChild(switchEl); @@ -167,10 +167,10 @@ const settingsRenderer = (pendingEl, settingsObj, isPls = false) => { template )}`; radioEl.checked = template === elValue ? true : false; - radioEl.addEventListener("change", (event) => { + radioEl.addEventListener("change", async (event) => { if (event.target.checked) { showToast(entry); - entry.callbackFn(event.target.value); + await entry.callbackFn(event.target.value); } }); inlineContainerEl.appendChild(radioEl); @@ -192,8 +192,8 @@ const settingsRenderer = (pendingEl, settingsObj, isPls = false) => { inputEl.value = entry.valueGetter(); inputEl.placeholder = entry.placeHolder; inputEl.id = entry.id; - inputEl.addEventListener("change", (event) => { - const result = entry.callbackFn(event.target.value); + inputEl.addEventListener("change", async (event) => { + const result = await entry.callbackFn(event.target.value); const success = result.valid; if (success) { showToast(entry); diff --git a/src/aura/ui/pages/config/config.css b/src/aura/ui/pages/config/config.css index 2c4daed..2f968ba 100755 --- a/src/aura/ui/pages/config/config.css +++ b/src/aura/ui/pages/config/config.css @@ -1,486 +1,27 @@ /* General */ -#aura-container-Aura-UI-Assistant-Config { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - z-index: 1000; -} - -.aura-config-page-root { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - background: url("../../../../app.asar/public/ae247697b4639c92bd008d0ea7d13b53.png"); - /* 这里不用 background-size: cover; 的效果反而更舒服一些... */ - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - opacity: 1; - transform: scale(1); - - transition: all 0.5s; -} - -.aura-config-page-root-inactive { - opacity: 0; - transform: scale(1.5); -} +@import url("./css/general.css"); /* Header */ -.aura-config-page-header-area { - flex: 1; - display: flex; - flex-direction: row; - justify-content: flex-start; - width: 100%; - padding-left: 8px; - padding-right: 8px; - color: white; - z-index: 12000; - - opacity: 1; - transform: translateY(0); - - transition: all 0.5s; -} - -.aura-config-page-header-area .iconfont { - font-size: 24px; - - transition: all 0.25s; -} - -.aura-config-page-header-area .iconfont:hover { - opacity: 0.75; - cursor: pointer; -} - -.aura-config-page-header-area .iconfont:active { - opacity: 0.375; -} - -.aura-config-page-header-area p { - margin-top: -2px; -} - -.aura-config-page-header-area.header-collapsed { - transform: translateY(-1rem); - opacity: 0; -} - -.aura-config-page-app-bar { - height: 40px; - display: flex; - justify-content: flex-start; - align-items: center; - width: 100%; -} +@import url("./css/header.css"); /* Status */ -.aura-config-page-status-container { - flex: 1; - width: 100%; - align-self: center; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - opacity: 1; - - transition: all 0.5s; -} - -.aura-config-page-status-container-hidden { - position: absolute; - opacity: 0; -} - -.aura-config-page-status-main, -.aura-config-page-status-description { - width: 100%; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; -} - -.aura-config-page-status-description { - margin-top: 0.5rem; - transform: translateY(0); - opacity: 1; - - transition: all 0.5s; -} - -.aura-config-page-status-description.status-description-hidden { - transform: translateY(-2rem); - opacity: 0; -} - -.aura-config-page-status-description p { - font-size: 18px; - margin-left: 15px; - margin-top: -2px; - color: white; - font-family: "Consolas", "Microsoft YaHei", sans-serif; -} - -.aura-config-page-status-description i { - color: white; -} - -.aura-config-page-central-aura-logo { - margin: 0.5rem 3rem; - width: 17.5%; -} - -.aura-config-hr-vertical { - height: 3.75rem; - width: 1px; - background-color: rgba(255, 255, 255, 0.3); - margin-left: 30px; - margin-right: 30px; - border: none; -} - -.aura-config-page-status-el { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-family: "Consolas", monospace; - color: white; - - /* - .version-type { - content: "I want to use scss plz 😇" - } - */ -} - -.aura-config-page-status-side { - height: 30%; - display: flex; - flex-direction: row; - align-items: center; - flex: 1; - - transform: translateX(0); - opacity: 1; - - transition: transform 0.5s, opacity 0.5s; -} - -.aura-config-page-status-side.left-side { - justify-content: flex-end; -} - -.aura-config-page-status-side.left-side.status-side-hidden { - transform: translateX(5rem); - opacity: 0; -} - -.aura-config-page-status-side.right-side { - justify-content: flex-start; -} - -.aura-config-page-status-side.right-side.status-side-hidden { - transform: translateX(-5rem); - opacity: 0; -} - -.aura-config-page-status-el .version-type { - font-size: 20px; - font-weight: 500; -} - -.aura-config-page-status-el .version-content { - font-size: 16px; - margin-top: 5px; - opacity: 0.625; -} +@import url("./css/status.css"); /* Operation */ -.aura-config-page-operation-area { - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-end; - flex: 1; - width: 100%; - overflow-y: auto; -} +@import url("./css/operation.css"); -.aura-config-page-operation-area::-webkit-scrollbar { - display: none; -} +/* Config Status Notify */ -.aura-config-page-operation-area.subpage-expanded { - flex: 15; -} - -.aura-config-page-subpage-container { - width: 100%; - height: 0; - background-color: rgba(255, 255, 255, 0.825); - z-index: 6000; - overflow-y: scroll; - - opacity: 0; - - transition: all 0.5s; -} - -.aura-config-page-subpage-container::-webkit-scrollbar { - display: none; -} - -.aura-config-page-operation-area.subpage-expanded - .aura-config-page-subpage-container { - height: calc(100% - 40px - 4rem); - opacity: 1; -} - -.aura-config-page-operation-container { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - background-color: rgba(255, 255, 255, 0.1); - padding-left: 1rem; - padding-right: 1rem; -} - -.aura-config-page-operation-container.hide-other-operations - .aura-config-page-operation-el:not(.preserve-operation) { - max-width: 0; - opacity: 0; -} - -.aura-config-page-operation-container.hide-other-operations - .aura-config-page-operation-el.preserve-operation { - flex: 0.25; -} - -.aura-config-page-operation-el { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - flex: 1; - padding-top: 2rem; - padding-bottom: 2rem; - overflow: hidden; - white-space: nowrap; - - max-width: 25%; - opacity: 1; - transform: translateY(0); - - transition: opacity 0.5s, transform 0.5s, - max-width cubic-bezier(0, 0.42, 0.18, 1) 0.5s; -} - -.aura-config-page-operation-el.operation-el-show:hover { - cursor: pointer; -} - -.aura-config-page-operation-el.operation-el-show[aura-disabled="true"]:hover { - cursor: not-allowed; -} - -.aura-config-page-operation-el.operation-el-hidden { - transform: translateY(2rem); - opacity: 0; -} - -.aura-config-page-operation-el.operation-el-show - .aura-config-page-operation-body { - opacity: 1; - transition: opacity 0.25s; -} - -.aura-config-page-operation-el.operation-el-show[aura-disabled="true"] - .aura-config-page-operation-body { - transition: opacity 0.5s; -} - -.aura-config-page-operation-el.operation-el-show:not(.preserve-operation):hover - .aura-config-page-operation-body { - opacity: 0.625; -} - -.aura-config-page-operation-el.operation-el-show[aura-disabled="true"]:hover - .aura-config-page-operation-body { - opacity: 0.25; -} - -.aura-config-page-operation-el.operation-el-show:not(.preserve-operation):active - .aura-config-page-operation-body { - opacity: 0.25; -} - -.aura-config-page-operation-el.operation-el-show[aura-disabled="true"]::after { - content: "别急嘛, 还在开发呢..."; - font-size: 16px; - opacity: 0; - color: white; - position: absolute; - - transition: all 0.5s; -} - -.aura-config-page-operation-el.operation-el-show[aura-disabled="true"]:hover::after, -.aura-config-page-operation-el.operation-el-show[aura-disabled="true"]:active::after { - opacity: 1; -} - -.aura-config-page-operation-body { - display: flex; - align-items: center; - justify-content: center; -} - -.aura-config-page-operation-el img { - max-width: 40px; - margin-right: 20px; -} - -.aura-config-page-operation-el .config-operation-title { - color: white; - font-size: large; -} - -.aura-config-page-operation-el .config-operation-description { - color: white; - opacity: 0.75; - font-size: small; -} +@import url("./css/configStatusNotify.css"); /* Auth Dialog */ -.aura-config-page-auth-dialog-area { - position: absolute; - height: calc(100% - 40px); - width: 100%; - top: 40px; - left: 0; - display: flex; - justify-content: center; - align-items: center; - z-index: 10000; - background-color: rgba(255, 255, 255, 0.35); - - opacity: 1; - transition: all 0.5s; -} - -.aura-config-page-auth-dialog-area.blur-enabled { - height: 100%; - top: 0; - background-color: rgba(255, 255, 255, 0.15); - backdrop-filter: blur(5px); - filter: blur(0.1px); - /* ↑ 似乎会导致性能问题 */ -} - -.acp-ada-hidden { - opacity: 0; -} - -.acp-ada-hidden.blur-enabled { - backdrop-filter: blur(0.1px); - filter: unset; -} - -.aura-config-page-auth-dialog { - height: 40%; - width: 100%; - background-color: rgba(255, 255, 255, 0.625); - display: flex; - flex-direction: column; - text-align: center; - align-items: center; - padding-top: 2rem; - padding-bottom: 2rem; -} - -.acp-auth-dialog-title { - font-size: x-large; - margin-bottom: 1.5rem; -} - -#acp-auth-user-input { - max-width: 50%; - /* background-color: rgba(255, 255, 255, 0.5); */ - border-radius: 35px; - margin-bottom: 2rem; -} - -#acp-auth-user-input.invalid { - animation: invalidShake 0.6s linear; -} - -.acp-auth-confirm-btn { - background-color: transparent; - border-radius: 35px; - border: 1px solid rgba(0, 0, 0, 0.3); - padding: 0.5rem; -} - -.acp-auth-confirm-btn .layui-icon { - font-size: 24px; - margin-left: 2px; -} +@import url("./css/authDialog.css"); /* Toast */ -.aura-config-page-toast-area { - z-index: 9000; -} - -.aura-config-page-toast-area .toast { - --bs-toast-border-width: 0 !important; - --bs-toast-bg: #fff !important; -} - -.aura-config-page-toast-area .toast-header { - background-color: rgb(255, 234, 202); - - border-top-left-radius: var(--bs-toast-border-radius); - border-top-right-radius: var(--bs-toast-border-radius); -} - -.aura-config-page-toast-area .toast.acp-toast-emerg .toast-header { - background-color: rgb(255, 202, 202); -} - -.aura-config-page-toast-area .toast-header * { - color: rgba(234, 126, 14, 0.85); -} - -.aura-config-page-toast-area .toast.acp-toast-emerg .toast-header * { - color: rgba(234, 65, 14, 0.85); -} - -.aura-config-page-toast-area .toast-body p { - margin-bottom: var(--bs-toast-padding-x); -} - -.aura-config-page-toast-area .toast-header .layui-icon { - font-weight: bolder; - margin-right: 0.5rem; - font-size: 18px; -} +@import url("./css/toast.css"); diff --git a/src/aura/ui/pages/config/config.html b/src/aura/ui/pages/config/config.html index b0f8b9b..4a7f52e 100755 --- a/src/aura/ui/pages/config/config.html +++ b/src/aura/ui/pages/config/config.html @@ -1,4 +1,8 @@ -
+