Skip to content

fix(linux): detect Secret Service via D-Bus to prevent basic_text fallback on unsupported desktops#1907

Closed
krit22 wants to merge 1 commit intogeneralaction:mainfrom
krit22:fix/linux-safe-storage
Closed

fix(linux): detect Secret Service via D-Bus to prevent basic_text fallback on unsupported desktops#1907
krit22 wants to merge 1 commit intogeneralaction:mainfrom
krit22:fix/linux-safe-storage

Conversation

@krit22
Copy link
Copy Markdown

@krit22 krit22 commented May 6, 2026

Problem

Fixes #1875.

On Linux, Emdash correctly refuses to store credentials when Chromium's safeStorage falls back to the insecure basic_text backend. However, this fallback is triggered even when a fully functional Secret Service (org.freedesktop.secrets) is running on the session bus.

Root cause: Chromium picks its keyring backend by inspecting XDG_CURRENT_DESKTOP. It only recognises a small hardcoded set of values (GNOME, XFCE, unity, etc.). Any other value — including every modern tiling compositor (Hyprland, sway, i3, dwm, Omarchy's Hyprland preset, etc.) — causes Chromium to silently fall back to basic_text, even when gnome-keyring is fully operational on the bus.

Emdash's assertSecureStorageAvailable() then correctly detects basic_text and throws, making the app unusable on these environments:

Failed to store session token: Error: Secure secret storage is unavailable on this system.
    at EncryptedAppSecretsStore.assertSecureStorageAvailable (...)
    at EncryptedAppSecretsStore.setSecret (...)
    at AccountCredentialStore.set (...)
    at EmdashAccountService.signIn (...)

Fix

Rather than trusting XDG_CURRENT_DESKTOP, this fix probes the session bus directly before Chromium initialises, using dbus-send to call org.freedesktop.DBus.NameHasOwner for org.freedesktop.secrets.

If the Secret Service is confirmed to be available, we append --password-store=gnome-libsecret to Chromium's command line — the documented, upstream-supported way to guide Chromium's backend selection — before app.whenReady() runs.

Three guards are in place to avoid over-reaching:

Condition Behaviour
No Secret Service on D-Bus Skip override (fail safe — do not force a backend with nothing behind it)
XDG_CURRENT_DESKTOP contains kde Skip override (Chromium already handles KDE/KWallet natively)
User passed --password-store=... manually Skip override (respect the user's explicit preference)

Code change (src/main/index.ts):

function secretServiceAvailable(): boolean {
  try {
    const output = execFileSync('dbus-send', [
      '--session',
      '--print-reply=literal',
      '--dest=org.freedesktop.DBus',
      '/org/freedesktop/DBus',
      'org.freedesktop.DBus.NameHasOwner',
      'string:org.freedesktop.secrets',
    ])
      .toString()
      .trim();

    return output.includes('true');
  } catch {
    return false;
  }
}

if (process.platform === 'linux') {
  app.commandLine.appendSwitch('ozone-platform-hint', 'auto');

  const isKDE = process.env.XDG_CURRENT_DESKTOP?.toLowerCase().includes('kde');
  const userOverrode = process.argv.some((a) => a.startsWith('--password-store='));

  if (!isKDE && !userOverrode && secretServiceAvailable()) {
    app.commandLine.appendSwitch('password-store', 'gnome-libsecret');
  }
}

Note on output parsing: dbus-send --print-reply=literal outputs boolean true (with the type prefix), not a bare true. The check uses .includes('true') after .trim() to handle this correctly.


Testing

Environment: Kali Linux (VirtualBox VM), XFCE desktop, XDG_CURRENT_DESKTOP overridden to Hyprland to simulate the failing environment. gnome-keyring confirmed operational.

Step 1 — Verified Secret Service pre-condition

$ dbus-send --session --print-reply=literal \
    --dest=org.freedesktop.DBus /org/freedesktop/DBus \
    org.freedesktop.DBus.NameHasOwner \
    string:org.freedesktop.secrets
   boolean true

Step 2 — Reproduced the bug on main

$ git checkout main
$ XDG_CURRENT_DESKTOP=Hyprland pnpm run dev

Attempted sign-in via Settings → Sign in. Observed in terminal:

Failed to store session token: Error: Secure secret storage is unavailable on this system.
    at EncryptedAppSecretsStore.assertSecureStorageAvailable (...)
    at AccountCredentialStore.set (...)
    at EmdashAccountService.signIn (...)
Account sign-in failed: Error: Secure secret storage is unavailable on this system.

✅ Bug confirmed reproduced.

Step 3 — Verified the fix on fix/linux-safe-storage

$ git checkout fix/linux-safe-storage
$ XDG_CURRENT_DESKTOP=Hyprland pnpm run dev

Attempted sign-in via Settings → Sign in.

Terminal output: No assertSecureStorageAvailable errors. Sign-in completed successfully and session token persisted across app restart.

✅ Bug resolved.

Step 4 — Verified fail-safe (no Secret Service)

$ pkill gnome-keyring-daemon
$ XDG_CURRENT_DESKTOP=Hyprland pnpm run dev

Our code detected no Secret Service on D-Bus, did not force gnome-libsecret, and Emdash's existing guardrail correctly blocked the login.

✅ Fail-safe confirmed — the fix never silently enables insecure storage.


Checklist

  • pnpm run format — passed
  • pnpm run lint — passed
  • pnpm run typecheck — passed
  • pnpm run test — 486 passed (3 pre-existing failures in legacy-port DB tests due to a better-sqlite3 native module ABI mismatch unrelated to this change)

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 6, 2026

Greptile Summary

This PR adds a secretServiceAvailable() helper that probes D-Bus at startup to detect a running Secret Service, and uses that result to conditionally append --password-store=gnome-libsecret to Chromium's command line — working around the bug where Chromium falls back to basic_text on desktops whose XDG_CURRENT_DESKTOP value it doesn't recognise.

  • Syntax error blocks launch: The secretServiceAvailable function at line 30 is never closed — its closing } is absent. Everything from if (import.meta.env.DEV) onward (the entire app initialisation) is parsed as unreachable code inside the function body, making the app unlaunchable on every platform.
  • Logic guards: Three guards (no Secret Service on D-Bus, KDE desktop, user-supplied --password-store) are in place to avoid forcing the libsecret backend when it shouldn't be used — the design is sound but cannot ship until the syntax is fixed.

Confidence Score: 1/5

Do not merge — the function added in this PR is missing its closing brace, which is a syntax error that prevents the app from starting on any platform.

The secretServiceAvailable function at line 30 is never closed: every line of application initialisation code from if (import.meta.env.DEV) onward is trapped inside the function body. The Electron main process would fail to load this file entirely, making the app completely unlaunchable regardless of desktop environment.

src/main/index.ts — missing closing } for secretServiceAvailable must be resolved before any further review.

Important Files Changed

Filename Overview
src/main/index.ts Adds secretServiceAvailable() and the --password-store=gnome-libsecret logic, but the function is missing its closing }, producing a fatal syntax error that prevents the app from launching on any platform.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[App startup — Linux] --> B{isKDE?}
    B -- Yes --> E[Skip override\nChromium handles KDE/KWallet]
    B -- No --> C{userOverrode\n--password-store?}
    C -- Yes --> F[Skip override\nRespect user preference]
    C -- No --> D{secretServiceAvailable?\ndbus-send probe}
    D -- false\ndbus-send fails / no service --> G[Skip override\nFail safe]
    D -- true\nboolean true on bus --> H[appendSwitch\npassword-store=gnome-libsecret]
    H --> I[Chromium uses libsecret\nSecret Service backend]
    E & F & G --> J[Chromium uses default\nbackend selection]
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
src/main/index.ts:30-62
**Missing closing brace — syntax error, app cannot start**

`secretServiceAvailable` opens with `{` at line 30, the `try/catch` block closes its own brace at line 46 (`  }`), but there is no matching `}` at column 0 to close the function itself. Every line from 47 onward (`if (import.meta.env.DEV)`, `registerAppScheme()`, the `app.whenReady()` callback, etc.) is parsed as dead code inside the function body. TypeScript and V8 would reject this file at load time, making the application completely unlaunchable.

Reviews (2): Last reviewed commit: "Merge branch 'main' into fix/linux-safe-..." | Re-trigger Greptile

Comment thread src/main/index.ts
Comment on lines +30 to +37
const output = execFileSync('dbus-send', [
'--session',
'--print-reply=literal',
'--dest=org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus.NameHasOwner',
'string:org.freedesktop.secrets',
])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 execFileSync with no timeout option defaults to an unlimited wait. If dbus-send hangs — e.g. the D-Bus daemon is slow, stalled, or the socket is unresponsive — the entire app startup blocks forever with no way to recover. This runs synchronously at module load time before app.whenReady(), so there is no watchdog or timeout mechanism that can interrupt it.

Suggested change
const output = execFileSync('dbus-send', [
'--session',
'--print-reply=literal',
'--dest=org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus.NameHasOwner',
'string:org.freedesktop.secrets',
])
const output = execFileSync('dbus-send', [
'--session',
'--print-reply=literal',
'--dest=org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus.NameHasOwner',
'string:org.freedesktop.secrets',
], { timeout: 2000 })
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/index.ts
Line: 30-37

Comment:
`execFileSync` with no `timeout` option defaults to an unlimited wait. If `dbus-send` hangs — e.g. the D-Bus daemon is slow, stalled, or the socket is unresponsive — the entire app startup blocks forever with no way to recover. This runs synchronously at module load time before `app.whenReady()`, so there is no watchdog or timeout mechanism that can interrupt it.

```suggestion
    const output = execFileSync('dbus-send', [
      '--session',
      '--print-reply=literal',
      '--dest=org.freedesktop.DBus',
      '/org/freedesktop/DBus',
      'org.freedesktop.DBus.NameHasOwner',
      'string:org.freedesktop.secrets',
    ], { timeout: 2000 })
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/main/index.ts Outdated
.toString()
.trim();

return output.includes('true');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 .includes('true') will match any output containing the substring true. The --print-reply=literal output for NameHasOwner is always boolean true or boolean false, so testing for the more specific string 'boolean true' removes any ambiguity and makes the intent clearer.

Suggested change
return output.includes('true');
return output.includes('boolean true');
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/index.ts
Line: 41

Comment:
`.includes('true')` will match any output containing the substring `true`. The `--print-reply=literal` output for `NameHasOwner` is always `boolean true` or `boolean false`, so testing for the more specific string `'boolean true'` removes any ambiguity and makes the intent clearer.

```suggestion
    return output.includes('boolean true');
```

How can I resolve this? If you propose a fix, please make it concise.

@krit22 krit22 closed this May 6, 2026
@krit22 krit22 reopened this May 6, 2026
Comment thread src/main/index.ts
Comment on lines +30 to 62
function secretServiceAvailable(): boolean {
try {
const output = execFileSync('dbus-send', [
'--session',
'--print-reply=literal',
'--dest=org.freedesktop.DBus',
'/org/freedesktop/DBus',
'org.freedesktop.DBus.NameHasOwner',
'string:org.freedesktop.secrets',
])
.toString()
.trim();

return output.includes('true');
} catch {
return false;
}
if (import.meta.env.DEV) {
dotenvConfig({ path: '.env.local', override: false });
}

if (process.platform === 'linux') {
app.commandLine.appendSwitch('ozone-platform-hint', 'auto');

// Work around Chromium falling back to 'basic_text' on some Linux desktops.
// If Secret Service is available, force libsecret unless overridden or on KDE.
const isKDE = process.env.XDG_CURRENT_DESKTOP?.toLowerCase().includes('kde');
const userOverrode = process.argv.some((a) => a.startsWith('--password-store='));

if (!isKDE && !userOverrode && secretServiceAvailable()) {
app.commandLine.appendSwitch('password-store', 'gnome-libsecret');
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Missing closing brace — syntax error, app cannot start

secretServiceAvailable opens with { at line 30, the try/catch block closes its own brace at line 46 ( }), but there is no matching } at column 0 to close the function itself. Every line from 47 onward (if (import.meta.env.DEV), registerAppScheme(), the app.whenReady() callback, etc.) is parsed as dead code inside the function body. TypeScript and V8 would reject this file at load time, making the application completely unlaunchable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/index.ts
Line: 30-62

Comment:
**Missing closing brace — syntax error, app cannot start**

`secretServiceAvailable` opens with `{` at line 30, the `try/catch` block closes its own brace at line 46 (`  }`), but there is no matching `}` at column 0 to close the function itself. Every line from 47 onward (`if (import.meta.env.DEV)`, `registerAppScheme()`, the `app.whenReady()` callback, etc.) is parsed as dead code inside the function body. TypeScript and V8 would reject this file at load time, making the application completely unlaunchable.

How can I resolve this? If you propose a fix, please make it concise.

@krit22 krit22 force-pushed the fix/linux-safe-storage branch from 6e06541 to 13506b1 Compare May 6, 2026 20:55
@krit22 krit22 closed this May 6, 2026
@krit22 krit22 deleted the fix/linux-safe-storage branch May 7, 2026 06:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linux: safeStorage falls back to basic_text on non-GNOME/KDE desktops, breaking all credential storage

1 participant