Compare commits
No commits in common. "7dc68c76152f03e760d7aecc5ccff5977e6b8051" and "b80ca7238d6599df041fd4977b53da7305692311" have entirely different histories.
7dc68c7615
...
b80ca7238d
@ -1,6 +1,5 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const secretStore = require('./secret-store');
|
|
||||||
|
|
||||||
const HOSTER_SETTINGS_DEFAULTS = {
|
const HOSTER_SETTINGS_DEFAULTS = {
|
||||||
retries: 3,
|
retries: 3,
|
||||||
@ -206,24 +205,12 @@ class ConfigStore {
|
|||||||
globalSettings[key] = { ...def, ...(savedGlobal[key] || {}) };
|
globalSettings[key] = { ...def, ...(savedGlobal[key] || {}) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const result = { hosters, hosterSettings, globalSettings, history: data.history || [] };
|
return { hosters, hosterSettings, globalSettings, history: data.history || [] };
|
||||||
// Decrypt credentials stored with safeStorage so the rest of the app
|
|
||||||
// keeps working with plaintext in memory.
|
|
||||||
secretStore.decryptCredentials(result);
|
|
||||||
return result;
|
|
||||||
} catch {
|
} catch {
|
||||||
return JSON.parse(JSON.stringify(DEFAULTS));
|
return JSON.parse(JSON.stringify(DEFAULTS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deep-clone a config and encrypt its credential fields. Never mutate the
|
|
||||||
// caller's object — the rest of the app holds plaintext references.
|
|
||||||
_serializeForDisk(config) {
|
|
||||||
const clone = JSON.parse(JSON.stringify(config));
|
|
||||||
secretStore.encryptCredentials(clone);
|
|
||||||
return JSON.stringify(clone, null, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
_enqueueWrite(fn) {
|
_enqueueWrite(fn) {
|
||||||
this._writeQueue = this._writeQueue.then(fn, fn);
|
this._writeQueue = this._writeQueue.then(fn, fn);
|
||||||
return this._writeQueue;
|
return this._writeQueue;
|
||||||
@ -235,7 +222,7 @@ class ConfigStore {
|
|||||||
if (config.hosters) current.hosters = config.hosters;
|
if (config.hosters) current.hosters = config.hosters;
|
||||||
if (config.hosterSettings) current.hosterSettings = config.hosterSettings;
|
if (config.hosterSettings) current.hosterSettings = config.hosterSettings;
|
||||||
if (config.globalSettings) current.globalSettings = config.globalSettings;
|
if (config.globalSettings) current.globalSettings = config.globalSettings;
|
||||||
return this._atomicWrite(this._serializeForDisk(current));
|
return this._atomicWrite(JSON.stringify(current, null, 2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,7 +255,7 @@ class ConfigStore {
|
|||||||
return this._enqueueWrite(() => {
|
return this._enqueueWrite(() => {
|
||||||
const config = this.load();
|
const config = this.load();
|
||||||
config.history.push(entry);
|
config.history.push(entry);
|
||||||
return this._atomicWrite(this._serializeForDisk(config));
|
return this._atomicWrite(JSON.stringify(config, null, 2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,7 +263,7 @@ class ConfigStore {
|
|||||||
return this._enqueueWrite(() => {
|
return this._enqueueWrite(() => {
|
||||||
const config = this.load();
|
const config = this.load();
|
||||||
config.history = [];
|
config.history = [];
|
||||||
return this._atomicWrite(this._serializeForDisk(config));
|
return this._atomicWrite(JSON.stringify(config, null, 2));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,75 +0,0 @@
|
|||||||
// Wraps Electron's safeStorage (OS-level credential encryption: DPAPI on
|
|
||||||
// Windows, Keychain on macOS, libsecret on Linux) to keep hoster passwords and
|
|
||||||
// API keys out of the plaintext electron-config.json.
|
|
||||||
//
|
|
||||||
// On Windows the DPAPI key is tied to the current user profile, so credentials
|
|
||||||
// encrypted here are only readable by the same Windows user. For backups we
|
|
||||||
// export to plaintext (the .mhu envelope has its own AES-GCM layer) so moving
|
|
||||||
// between machines/users works transparently.
|
|
||||||
|
|
||||||
const SENTINEL = 'enc:v1:';
|
|
||||||
const CRED_FIELDS = ['password', 'apiKey'];
|
|
||||||
|
|
||||||
let _safeStorageCache = undefined;
|
|
||||||
function getSafeStorage() {
|
|
||||||
if (_safeStorageCache !== undefined) return _safeStorageCache;
|
|
||||||
try {
|
|
||||||
const { safeStorage } = require('electron');
|
|
||||||
if (safeStorage && typeof safeStorage.isEncryptionAvailable === 'function'
|
|
||||||
&& safeStorage.isEncryptionAvailable()) {
|
|
||||||
_safeStorageCache = safeStorage;
|
|
||||||
return _safeStorageCache;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
_safeStorageCache = null;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEncrypted(value) {
|
|
||||||
return typeof value === 'string' && value.startsWith(SENTINEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
function encryptField(value) {
|
|
||||||
if (!value || typeof value !== 'string') return value;
|
|
||||||
if (isEncrypted(value)) return value;
|
|
||||||
const ss = getSafeStorage();
|
|
||||||
if (!ss) return value;
|
|
||||||
try {
|
|
||||||
const buf = ss.encryptString(value);
|
|
||||||
return SENTINEL + buf.toString('base64');
|
|
||||||
} catch {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptField(value) {
|
|
||||||
if (!value || typeof value !== 'string') return value;
|
|
||||||
if (!isEncrypted(value)) return value;
|
|
||||||
const ss = getSafeStorage();
|
|
||||||
if (!ss) return '';
|
|
||||||
try {
|
|
||||||
const buf = Buffer.from(value.slice(SENTINEL.length), 'base64');
|
|
||||||
return ss.decryptString(buf);
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapHosterAccounts(config, fn) {
|
|
||||||
if (!config || !config.hosters || typeof config.hosters !== 'object') return config;
|
|
||||||
for (const accounts of Object.values(config.hosters)) {
|
|
||||||
if (!Array.isArray(accounts)) continue;
|
|
||||||
for (const acc of accounts) {
|
|
||||||
if (!acc || typeof acc !== 'object') continue;
|
|
||||||
for (const f of CRED_FIELDS) {
|
|
||||||
if (acc[f]) acc[f] = fn(acc[f]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
function encryptCredentials(config) { return mapHosterAccounts(config, encryptField); }
|
|
||||||
function decryptCredentials(config) { return mapHosterAccounts(config, decryptField); }
|
|
||||||
|
|
||||||
module.exports = { encryptField, decryptField, encryptCredentials, decryptCredentials, isEncrypted };
|
|
||||||
11
main.js
11
main.js
@ -219,10 +219,9 @@ function flattenHistoryForExport(history) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
// Only accept real URLs. file_code alone is just an opaque ID and
|
const link = result && (result.download_url || result.embed_url || result.file_code)
|
||||||
// ends up looking like "nur sone Nummerierung" in the CSV.
|
? String(result.download_url || result.embed_url || result.file_code)
|
||||||
const rawLink = result && (result.download_url || result.embed_url) || '';
|
: '';
|
||||||
const link = /^https?:\/\//i.test(String(rawLink)) ? String(rawLink) : '';
|
|
||||||
rows.push({
|
rows.push({
|
||||||
batchId,
|
batchId,
|
||||||
batchTimestamp,
|
batchTimestamp,
|
||||||
@ -1120,7 +1119,7 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => {
|
|||||||
globalSettings: importedGlobal,
|
globalSettings: importedGlobal,
|
||||||
history: imported.history || []
|
history: imported.history || []
|
||||||
};
|
};
|
||||||
await configStore._atomicWrite(configStore._serializeForDisk(merged));
|
await configStore._atomicWrite(JSON.stringify(merged, null, 2));
|
||||||
return { ok: true, config: configStore.load() };
|
return { ok: true, config: configStore.load() };
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1254,7 +1253,7 @@ ipcMain.on('save-global-settings-sync', (event, globalSettings) => {
|
|||||||
try {
|
try {
|
||||||
const current = configStore.load();
|
const current = configStore.load();
|
||||||
current.globalSettings = globalSettings;
|
current.globalSettings = globalSettings;
|
||||||
const data = configStore._serializeForDisk(current);
|
const data = JSON.stringify(current, null, 2);
|
||||||
const tmpPath = configStore.filePath + '.tmp';
|
const tmpPath = configStore.filePath + '.tmp';
|
||||||
const backupPath = configStore.filePath + '.bak';
|
const backupPath = configStore.filePath + '.bak';
|
||||||
fs.writeFileSync(tmpPath, data, 'utf-8');
|
fs.writeFileSync(tmpPath, data, 'utf-8');
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "2.8.3",
|
"version": "2.8.2",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user