Credential writes now require Electron safeStorage encryption. Legacy plaintext values remain readable for migration and are encrypted during the next successful save. The UI override and status IPC are removed.
This commit is contained in:
@@ -3,7 +3,20 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const Module = require('node:module');
|
||||
const originalLoad = Module._load;
|
||||
const safeStorage = {
|
||||
isEncryptionAvailable: () => true,
|
||||
encryptString: value => Buffer.from(`test-protected:${value}`),
|
||||
decryptString: value => value.toString().replace(/^test-protected:/, '')
|
||||
};
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === 'electron') return { safeStorage };
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
const ConfigStore = require('../lib/config-store');
|
||||
require('../lib/secret-store').encryptField('test-initialization');
|
||||
Module._load = originalLoad;
|
||||
|
||||
let tmpDir;
|
||||
let store;
|
||||
@@ -15,7 +28,7 @@ function createStore() {
|
||||
};
|
||||
// ConfigStore uses path.join(__dirname, '..') for non-packaged
|
||||
// We override by setting filePath directly
|
||||
store = new ConfigStore(fakeApp, { allowPlaintextCredentialStorage: true });
|
||||
store = new ConfigStore(fakeApp);
|
||||
store.filePath = path.join(tmpDir, 'electron-config.json');
|
||||
store.historyPath = path.join(tmpDir, 'electron-history.json');
|
||||
return store;
|
||||
@@ -46,7 +59,7 @@ describe('ConfigStore', () => {
|
||||
if (name === 'exe') return path.join(isolatedDir, 'Multi-Hoster-Upload.exe');
|
||||
throw new Error(`Unexpected app path: ${name}`);
|
||||
}
|
||||
}, { allowPlaintextCredentialStorage: true });
|
||||
});
|
||||
|
||||
try {
|
||||
assert.equal(explicitStore.filePath, path.join(isolatedDir, 'electron-config.json'));
|
||||
@@ -76,6 +89,7 @@ describe('ConfigStore', () => {
|
||||
assert.equal(config.globalSettings.shutdownAfterFinish, 'nothing');
|
||||
assert.equal(config.globalSettings.logFilePath, '');
|
||||
assert.equal(config.globalSettings.resumeQueueOnLaunch, true);
|
||||
assert.equal(Object.hasOwn(config.globalSettings, 'allowPlaintextCredentialStorage'), false);
|
||||
assert.equal(config.globalSettings.parallelUploadCount, 0);
|
||||
assert.equal(config.globalSettings.scaleParallelUploads, false);
|
||||
assert.equal(config.globalSettings.lastBrowseDirectory, '');
|
||||
@@ -83,6 +97,28 @@ describe('ConfigStore', () => {
|
||||
assert.deepEqual(config.history, []);
|
||||
});
|
||||
|
||||
it('drops the retired plaintext credential setting from legacy configurations', () => {
|
||||
fs.writeFileSync(store.filePath, JSON.stringify({
|
||||
hosters: {},
|
||||
hosterSettings: {},
|
||||
globalSettings: { allowPlaintextCredentialStorage: true }
|
||||
}), 'utf-8');
|
||||
|
||||
const config = store.load();
|
||||
|
||||
assert.equal(Object.hasOwn(config.globalSettings, 'allowPlaintextCredentialStorage'), false);
|
||||
});
|
||||
|
||||
it('never writes the retired plaintext credential setting back to disk', async () => {
|
||||
await store.save({
|
||||
globalSettings: { allowPlaintextCredentialStorage: true }
|
||||
});
|
||||
|
||||
const saved = JSON.parse(fs.readFileSync(store.filePath, 'utf-8'));
|
||||
|
||||
assert.equal(Object.hasOwn(saved.globalSettings, 'allowPlaintextCredentialStorage'), false);
|
||||
});
|
||||
|
||||
it('keeps permanent source deletion disabled by default', () => {
|
||||
const config = store.load();
|
||||
assert.equal(config.globalSettings.deleteSourceAfterSuccessfulUpload, false);
|
||||
@@ -579,7 +615,7 @@ describe('ConfigStore history split (electron-history.json)', () => {
|
||||
let s;
|
||||
|
||||
function makeStore() {
|
||||
const st = new ConfigStore({ isPackaged: false, getPath: () => dir }, { allowPlaintextCredentialStorage: true });
|
||||
const st = new ConfigStore({ isPackaged: false, getPath: () => dir });
|
||||
st.filePath = path.join(dir, 'electron-config.json');
|
||||
st.historyPath = path.join(dir, 'electron-history.json');
|
||||
return st;
|
||||
|
||||
@@ -3,6 +3,25 @@ const assert = require('node:assert');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const Module = require('node:module');
|
||||
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === 'electron') {
|
||||
return {
|
||||
safeStorage: {
|
||||
isEncryptionAvailable: () => true,
|
||||
encryptString: value => Buffer.from(`test-protected:${value}`),
|
||||
decryptString: value => value.toString().replace(/^test-protected:/, '')
|
||||
}
|
||||
};
|
||||
}
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
|
||||
const ConfigStore = require('../lib/config-store');
|
||||
require('../lib/secret-store').encryptField('test-initialization');
|
||||
Module._load = originalLoad;
|
||||
|
||||
// Minimal app mock for ConfigStore
|
||||
function createTestConfigStore() {
|
||||
@@ -12,8 +31,7 @@ function createTestConfigStore() {
|
||||
getPath: (name) => tmpDir,
|
||||
getPath: () => tmpDir
|
||||
};
|
||||
const ConfigStore = require('../lib/config-store');
|
||||
const store = new ConfigStore(mockApp, { allowPlaintextCredentialStorage: true });
|
||||
const store = new ConfigStore(mockApp);
|
||||
store.filePath = path.join(tmpDir, 'test-config.json');
|
||||
return { store, tmpDir };
|
||||
}
|
||||
|
||||
+16
-15
@@ -28,15 +28,6 @@ function availableSafeStorage(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
test('reports whether secure credential storage is available', () => {
|
||||
withSecretStore(availableSafeStorage(), secretStore => {
|
||||
assert.equal(secretStore.getAvailabilityStatus(), 'available');
|
||||
});
|
||||
withSecretStore(null, secretStore => {
|
||||
assert.equal(secretStore.getAvailabilityStatus(), 'unavailable');
|
||||
});
|
||||
});
|
||||
|
||||
test('encrypts and decrypts fields when secure storage is available', () => {
|
||||
withSecretStore(availableSafeStorage(), secretStore => {
|
||||
const encrypted = secretStore.encryptField('secret');
|
||||
@@ -55,13 +46,18 @@ test('refuses plaintext storage by default when secure storage is unavailable',
|
||||
});
|
||||
});
|
||||
|
||||
test('allows plaintext storage only through an explicit opt-in', () => {
|
||||
test('never allows plaintext storage when secure storage is unavailable', () => {
|
||||
withSecretStore(null, secretStore => {
|
||||
assert.equal(secretStore.encryptField('secret', { allowPlaintext: true }), 'secret');
|
||||
assert.throws(
|
||||
() => secretStore.encryptField('secret', { allowPlaintext: true }),
|
||||
error => error instanceof secretStore.SecretStoreError
|
||||
&& error.code === 'SECRET_STORE_UNAVAILABLE'
|
||||
);
|
||||
const config = { hosters: { example: [{ password: 'secret' }] } };
|
||||
assert.equal(
|
||||
secretStore.encryptCredentials(config, { allowPlaintext: true }).hosters.example[0].password,
|
||||
'secret'
|
||||
assert.throws(
|
||||
() => secretStore.encryptCredentials(config, { allowPlaintext: true }),
|
||||
error => error instanceof secretStore.SecretStoreError
|
||||
&& error.code === 'SECRET_STORE_UNAVAILABLE'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -75,7 +71,12 @@ test('refuses plaintext storage by default when encryption fails', () => {
|
||||
&& error.code === 'SECRET_STORE_ENCRYPT_FAILED'
|
||||
&& error.cause === failure
|
||||
);
|
||||
assert.equal(secretStore.encryptField('secret', { allowPlaintext: true }), 'secret');
|
||||
assert.throws(
|
||||
() => secretStore.encryptField('secret', { allowPlaintext: true }),
|
||||
error => error instanceof secretStore.SecretStoreError
|
||||
&& error.code === 'SECRET_STORE_ENCRYPT_FAILED'
|
||||
&& error.cause === failure
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1031,6 +1031,8 @@ setTimeout(async () => {
|
||||
await wc.executeJavaScript('document.querySelector("[data-settings-page=\\'uploads\\']")?.click()');
|
||||
const uploadSettingsState = await wc.executeJavaScript('(() => { const activePage = document.querySelector(".settings-subpage.active"); return [activePage?.dataset.subpage, activePage?.querySelector("h3")?.textContent.trim(), document.querySelector("label[for=removeFromQueueOnDoneInput]")?.textContent.trim(), document.getElementById("removeFromQueueOnDoneInput")?.closest(".settings-option")?.querySelector(".settings-option-description")?.textContent.trim()].join("|"); })()');
|
||||
check('Upload completion behavior is immediately findable', uploadSettingsState === 'uploads|Upload-Verhalten|Nach Abschluss aus der Liste entfernen|Erfolgreich hochgeladene Dateien verschwinden automatisch aus der Upload-Liste.');
|
||||
const plaintextCredentialOverride = await wc.executeJavaScript('(() => ({ control: document.getElementById("allowPlaintextCredentialStorageInput"), copy: document.body.textContent.includes("Unsichere Klartext-Speicherung"), bridge: typeof window.api.getSecretStoreStatus }))()');
|
||||
check('Settings expose no plaintext credential storage override', plaintextCredentialOverride.control === null && plaintextCredentialOverride.copy === false && plaintextCredentialOverride.bridge === 'undefined');
|
||||
const settingsTypography = await wc.executeJavaScript('(() => { const size = selector => parseFloat(getComputedStyle(document.querySelector(selector)).fontSize); return { heading: size(".settings-subpage.active .settings-page-header h3"), intro: size(".settings-subpage.active .settings-page-header p"), section: size(".settings-subpage.active .settings-section-label"), rowLabel: size(".settings-subpage.active .settings-row > label"), hint: size(".settings-subpage.active .hint"), optionLabel: size(".settings-subpage.active .settings-option-copy label"), optionDescription: size(".settings-subpage.active .settings-option-description"), navigation: size(".settings-nav-button"), search: size("#settingsSearchInput") }; })()');
|
||||
check('Settings use the enlarged readable typography scale', settingsTypography.heading >= 22 && settingsTypography.intro >= 14 && settingsTypography.section >= 12 && settingsTypography.rowLabel >= 14 && settingsTypography.hint >= 12 && settingsTypography.optionLabel >= 14 && settingsTypography.optionDescription >= 12 && settingsTypography.navigation >= 13 && settingsTypography.search >= 13);
|
||||
const settingsSelection = await wc.executeJavaScript('(() => ({ heading: getComputedStyle(document.querySelector(".settings-subpage.active .settings-page-header h3")).userSelect, hint: getComputedStyle(document.querySelector(".settings-subpage.active .hint")).userSelect, input: getComputedStyle(document.getElementById("globalMaxSpeedMbsInput")).userSelect }))()');
|
||||
|
||||
Reference in New Issue
Block a user