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:
@@ -159,7 +159,7 @@ English is the default language for new profiles. Open **Settings > General** to
|
||||
|
||||
### Local data and credentials
|
||||
|
||||
Settings, pending queue state, and upload history are stored in Electron's user-data directory. Passwords and API keys are encrypted with Electron `safeStorage` before they are written when operating-system encryption is available and encryption succeeds. On Windows, `safeStorage` uses DPAPI and ties encrypted values to the current Windows user profile. If operating-system encryption is unavailable or encryption fails, passwords and API keys are stored as plaintext on disk.
|
||||
Settings, pending queue state, and upload history are stored in Electron's user-data directory. Passwords and API keys must be encrypted with Electron `safeStorage` before they are written. On Windows, `safeStorage` uses DPAPI and ties encrypted values to the current Windows user profile. If operating-system encryption is unavailable or encryption fails, the write is rejected and credentials are never written as plaintext. Legacy plaintext credentials remain readable for migration and are encrypted during the next successful save.
|
||||
|
||||
Credentials are decrypted when required for account validation or upload to the selected host. Do not share application data files, backup files, screenshots containing credentials, or generated backup keys.
|
||||
|
||||
|
||||
+6
-7
@@ -73,7 +73,6 @@ const DEFAULTS = {
|
||||
// load() sets logMode after the merge, looking at the saved-only data.
|
||||
resumeQueueOnLaunch: true,
|
||||
autoStartRestoredQueue: false,
|
||||
allowPlaintextCredentialStorage: false,
|
||||
parallelUploadCount: 0, // 0 = use per-hoster limits only
|
||||
scaleParallelUploads: false,
|
||||
lastBrowseDirectory: '',
|
||||
@@ -182,7 +181,7 @@ function applyHistoryRetention(history, retention, nowMs) {
|
||||
}
|
||||
|
||||
class ConfigStore {
|
||||
constructor(app, options = {}) {
|
||||
constructor(app) {
|
||||
const useUserDataDir = app && (
|
||||
app.isPackaged ||
|
||||
(app.commandLine && typeof app.commandLine.hasSwitch === 'function' && app.commandLine.hasSwitch('user-data-dir'))
|
||||
@@ -201,7 +200,6 @@ class ConfigStore {
|
||||
this._cacheKey = '';
|
||||
this._perfLog = null;
|
||||
this._wqDepth = 0;
|
||||
this._allowPlaintextCredentialStorage = options.allowPlaintextCredentialStorage === true;
|
||||
|
||||
// Migrate config from old location if current doesn't exist
|
||||
if (!fs.existsSync(this.filePath) && app && app.isPackaged) {
|
||||
@@ -448,6 +446,7 @@ class ConfigStore {
|
||||
...DEFAULTS.globalSettings,
|
||||
...savedGlobal
|
||||
};
|
||||
delete globalSettings.allowPlaintextCredentialStorage;
|
||||
// Deep-merge nested objects so new keys are always present
|
||||
for (const key of Object.keys(DEFAULTS.globalSettings)) {
|
||||
const def = DEFAULTS.globalSettings[key];
|
||||
@@ -487,10 +486,10 @@ class ConfigStore {
|
||||
// on every write was a primary long-running main-thread stall.
|
||||
_serializeForDisk(config) {
|
||||
const hosters = this._clone(config.hosters || {});
|
||||
secretStore.encryptCredentials({ hosters }, {
|
||||
allowPlaintext: this._allowPlaintextCredentialStorage || config.globalSettings?.allowPlaintextCredentialStorage === true
|
||||
});
|
||||
return JSON.stringify({ ...config, hosters }, null, 2);
|
||||
const globalSettings = this._clone(config.globalSettings || {});
|
||||
delete globalSettings.allowPlaintextCredentialStorage;
|
||||
secretStore.encryptCredentials({ hosters });
|
||||
return JSON.stringify({ ...config, globalSettings, hosters }, null, 2);
|
||||
}
|
||||
|
||||
_commit(config) {
|
||||
|
||||
+3
-10
@@ -38,23 +38,17 @@ function isEncrypted(value) {
|
||||
return typeof value === 'string' && value.startsWith(SENTINEL);
|
||||
}
|
||||
|
||||
function getAvailabilityStatus() {
|
||||
return getSafeStorage() ? 'available' : 'unavailable';
|
||||
}
|
||||
|
||||
function encryptField(value, options = {}) {
|
||||
function encryptField(value) {
|
||||
if (!value || typeof value !== 'string') return value;
|
||||
if (isEncrypted(value)) return value;
|
||||
const ss = getSafeStorage();
|
||||
if (!ss) {
|
||||
if (options.allowPlaintext === true) return value;
|
||||
throw new SecretStoreError('SECRET_STORE_UNAVAILABLE', 'Sicherer Zugangsdaten-Speicher ist nicht verfügbar');
|
||||
}
|
||||
try {
|
||||
const buf = ss.encryptString(value);
|
||||
return SENTINEL + buf.toString('base64');
|
||||
} catch (cause) {
|
||||
if (options.allowPlaintext === true) return value;
|
||||
throw new SecretStoreError('SECRET_STORE_ENCRYPT_FAILED', 'Zugangsdaten konnten nicht sicher verschlüsselt werden', cause);
|
||||
}
|
||||
}
|
||||
@@ -88,14 +82,13 @@ function mapHosterAccounts(config, fn) {
|
||||
return config;
|
||||
}
|
||||
|
||||
function encryptCredentials(config, options = {}) {
|
||||
return mapHosterAccounts(config, value => encryptField(value, options));
|
||||
function encryptCredentials(config) {
|
||||
return mapHosterAccounts(config, encryptField);
|
||||
}
|
||||
function decryptCredentials(config) { return mapHosterAccounts(config, decryptField); }
|
||||
|
||||
module.exports = {
|
||||
SecretStoreError,
|
||||
getAvailabilityStatus,
|
||||
encryptField,
|
||||
decryptField,
|
||||
encryptCredentials,
|
||||
|
||||
@@ -7,7 +7,6 @@ nativeTheme.themeSource = 'dark';
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ConfigStore = require('./lib/config-store');
|
||||
const secretStore = require('./lib/secret-store');
|
||||
const UploadManager = require('./lib/upload-manager');
|
||||
const { createSourceFileCleanup } = require('./lib/source-file-cleanup');
|
||||
const SourceDeleteJournal = require('./lib/source-delete-journal');
|
||||
@@ -1699,8 +1698,6 @@ ipcMain.handle('get-config', () => {
|
||||
return configStore.load();
|
||||
});
|
||||
|
||||
ipcMain.handle('secret-store:status', () => ({ status: secretStore.getAvailabilityStatus() }));
|
||||
|
||||
ipcMain.handle('save-config', async (_event, config) => {
|
||||
assertConfigWriteAllowed();
|
||||
await configStore.save(config);
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "2.1.12",
|
||||
"version": "2.1.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "2.1.12",
|
||||
"version": "2.1.13",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"undici": "^7.29.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-hoster-uploader",
|
||||
"version": "2.1.12",
|
||||
"version": "2.1.13",
|
||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -4,7 +4,6 @@ contextBridge.exposeInMainWorld('api', {
|
||||
// Config
|
||||
getConfig: () => ipcRenderer.invoke('get-config'),
|
||||
saveConfig: (config) => ipcRenderer.invoke('save-config', config),
|
||||
getSecretStoreStatus: () => ipcRenderer.invoke('secret-store:status'),
|
||||
getHistory: () => ipcRenderer.invoke('get-history'),
|
||||
clearHistory: () => ipcRenderer.invoke('clear-history'),
|
||||
pruneHistory: (retention, opts) => ipcRenderer.invoke('prune-history', { retention, dryRun: !!(opts && opts.dryRun) }),
|
||||
|
||||
@@ -4255,14 +4255,6 @@ function renderSettings() {
|
||||
<input type="checkbox" class="settings-autosave" id="showDropTargetInput" ${globalSettings.showDropTarget ? 'checked' : ''}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-option credential-fallback-option">
|
||||
<div class="settings-option-copy">
|
||||
<label for="allowPlaintextCredentialStorageInput">Unsichere Klartext-Speicherung erlauben</label>
|
||||
<span class="settings-option-description">Nur verwenden, wenn der sichere Betriebssystem-Speicher dauerhaft nicht verfügbar ist. Passwörter und API-Keys liegen dann lesbar in der Konfigurationsdatei.</span>
|
||||
<span class="panel-status" id="secretStoreStatus">Prüfe…</span>
|
||||
</div>
|
||||
<input type="checkbox" class="settings-autosave" id="allowPlaintextCredentialStorageInput" ${globalSettings.allowPlaintextCredentialStorage ? 'checked' : ''}>
|
||||
</div>
|
||||
<div class="settings-section-label">Programmupdate</div>
|
||||
<div class="settings-row program-update-row program-update-card">
|
||||
<div class="program-update-copy">
|
||||
@@ -4870,13 +4862,6 @@ function renderSettings() {
|
||||
document.getElementById('chooseLogFilePathBtn')?.addEventListener('click', chooseLogFilePath);
|
||||
document.getElementById('openLogFolderBtn')?.addEventListener('click', () => window.api.openLogFolder());
|
||||
document.getElementById('manualUpdateCheckBtn')?.addEventListener('click', requestUpdateCheck);
|
||||
window.api.getSecretStoreStatus().then(result => {
|
||||
const badge = document.getElementById('secretStoreStatus');
|
||||
if (!badge) return;
|
||||
const available = result?.status === 'available';
|
||||
badge.textContent = available ? 'Sicher verfügbar' : 'Nicht verfügbar';
|
||||
badge.classList.toggle('active', available);
|
||||
}).catch(() => {});
|
||||
_syncHeaderUpdateState();
|
||||
container.querySelectorAll('.settings-autosave').forEach((input) => {
|
||||
const eventName = input.type === 'checkbox' || input.tagName === 'SELECT' ? 'change' : 'input';
|
||||
@@ -4894,15 +4879,6 @@ function renderSettings() {
|
||||
});
|
||||
if (!confirmed) input.checked = false;
|
||||
}
|
||||
if (input.id === 'allowPlaintextCredentialStorageInput' && input.checked) {
|
||||
const confirmed = await showAppConfirm({
|
||||
title: 'Zugangsdaten unverschlüsselt speichern?',
|
||||
message: 'Passwörter und API-Keys werden dann im Klartext auf diesem PC gespeichert. Andere Benutzer oder Programme mit Dateizugriff können sie lesen.',
|
||||
confirmText: 'Klartext-Speicherung erlauben',
|
||||
danger: true
|
||||
});
|
||||
if (!confirmed) input.checked = false;
|
||||
}
|
||||
markSettingsDirty();
|
||||
});
|
||||
});
|
||||
@@ -5028,7 +5004,6 @@ async function performSaveSettings(options = {}) {
|
||||
})(),
|
||||
resumeQueueOnLaunch: elChk('resumeQueueOnLaunchInput', cur.resumeQueueOnLaunch !== false),
|
||||
autoStartRestoredQueue: elChk('autoStartRestoredQueueInput', !!cur.autoStartRestoredQueue),
|
||||
allowPlaintextCredentialStorage: elChk('allowPlaintextCredentialStorageInput', !!cur.allowPlaintextCredentialStorage),
|
||||
parallelUploadCount: elInt('parallelUploadCountInput', cur.parallelUploadCount ?? 0, 0, 0, 100),
|
||||
scaleParallelUploads: elChk('scaleParallelUploadsInput', !!cur.scaleParallelUploads),
|
||||
removeFromQueueOnDone: elChk('removeFromQueueOnDoneInput', !!cur.removeFromQueueOnDone),
|
||||
|
||||
@@ -474,13 +474,6 @@
|
||||
['Zugangsdaten konnten nicht sicher verschlüsselt werden', 'Credentials could not be encrypted securely'],
|
||||
['Gespeicherte Zugangsdaten konnten nicht entschlüsselt werden', 'Stored credentials could not be decrypted'],
|
||||
['Zugangsdaten gesperrt', 'Credentials locked'],
|
||||
['Unsichere Klartext-Speicherung erlauben', 'Allow insecure plaintext storage'],
|
||||
['Nur verwenden, wenn der sichere Betriebssystem-Speicher dauerhaft nicht verfügbar ist. Passwörter und API-Keys liegen dann lesbar in der Konfigurationsdatei.', 'Use only when secure operating system storage is permanently unavailable. Passwords and API keys will then be readable in the configuration file.'],
|
||||
['Sicher verfügbar', 'Securely available'],
|
||||
['Nicht verfügbar', 'Unavailable'],
|
||||
['Zugangsdaten unverschlüsselt speichern?', 'Store credentials without encryption?'],
|
||||
['Passwörter und API-Keys werden dann im Klartext auf diesem PC gespeichert. Andere Benutzer oder Programme mit Dateizugriff können sie lesen.', 'Passwords and API keys will then be stored as plaintext on this PC. Other users or programs with file access can read them.'],
|
||||
['Klartext-Speicherung erlauben', 'Allow plaintext storage'],
|
||||
['Die Verlaufsdatei ist beschädigt und wurde nicht verändert', 'The history file is damaged and was not changed'],
|
||||
['Verlauf und Aufbewahrung konnten nicht konsistent gespeichert werden', 'History and retention could not be saved consistently'],
|
||||
['Kein Ordnerpfad angegeben', 'No folder path was provided'],
|
||||
|
||||
@@ -3318,16 +3318,6 @@ input[type="checkbox"] {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.settings-option.credential-fallback-option {
|
||||
margin-top: 12px;
|
||||
border-color: color-mix(in srgb, var(--danger) 72%, var(--border));
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--danger) 10%, transparent);
|
||||
}
|
||||
|
||||
.settings-option.credential-fallback-option:focus-within {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.settings-option-description,
|
||||
.hint {
|
||||
color: var(--text-dim);
|
||||
|
||||
@@ -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