mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
* fix(secrets): stop telling Linux users to install a keyring they already run On a desktop Chromium does not recognise (Hyprland, sway, river, niri) the selected backend is basic_text and sealing is unavailable, so the at-rest protection report told the user to install and unlock gnome-keyring — which is usually already running and serving org.freedesktop.secrets. Chromium simply never looked, because it picks the backend from XDG_CURRENT_DESKTOP. Split the Linux unavailable path on the selected backend: an unrecognised desktop now says so, names XDG_CURRENT_DESKTOP, and points at --password-store. A backend that did resolve but cannot seal keeps the install-and-unlock text, which is correct there. The backend read stays after isEncryptionAvailable(), the call that performs the D-Bus probe, so nothing new blocks (STA-5765 timing unchanged). * fix(secrets): seal credentials on Linux desktops Chromium cannot detect Chromium picks its os_crypt backend from the desktop-environment env vars and recognises none of the tiling compositors (Hyprland, sway, river, niri). On those it selects basic_text, whose key Electron only exposes after an explicit setUsePlainTextEncryption() this app never calls — so isEncryptionAvailable() is false and every credential store takes its plaintext fallback, while gnome-keyring sits on the session bus unasked (#21827). Name gnome-libsecret ourselves, but only where it provably cannot hurt: - Never for a desktop Chromium does resolve. Overriding a working selection is the one change that could strand already-sealed credentials, and a KDE session identified only by KDE_FULL_SESSION is the case that matters. - Only when the secret service's default collection is present AND unlocked, measured out of process with a killable 1.5s deadline. A locked collection with no unlock prompter is what made isEncryptionAvailable() block for 76s to first window (STA-5765); selecting libsecret there would trade silent plaintext for a frozen app. The probe reads the Locked property, which cannot itself trigger a prompt. Anything unexpected — no session bus, no gdbus, no name owner, timeout — leaves Chromium's own choice alone, so the worst case is today's behaviour. The desktop-presence env vars come from a review finding by @Raajik on #21831, confirmed there against base/nix/xdg_util.cc.
133 lines
4.7 KiB
TypeScript
133 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
identifiesResolvedDesktop,
|
|
resolveLinuxPasswordStore,
|
|
type SecretServiceCollectionState
|
|
} from './linux-keyring-backend-selection'
|
|
|
|
const SESSION_BUS = { DBUS_SESSION_BUS_ADDRESS: 'unix:path=/run/user/1000/bus' }
|
|
|
|
function resolve(
|
|
env: NodeJS.ProcessEnv,
|
|
collection: SecretServiceCollectionState = 'unlocked',
|
|
passwordStoreAlreadySelected = false
|
|
) {
|
|
const probeCollection = vi.fn(() => collection)
|
|
const decision = resolveLinuxPasswordStore({
|
|
env: { ...SESSION_BUS, ...env },
|
|
passwordStoreAlreadySelected,
|
|
probeCollection
|
|
})
|
|
return { ...decision, probeCollection }
|
|
}
|
|
|
|
describe('identifiesResolvedDesktop', () => {
|
|
it.each([
|
|
'GNOME',
|
|
'gnome',
|
|
'GNOME:ubuntu',
|
|
'ubuntu:GNOME',
|
|
'X-Cinnamon',
|
|
'KDE',
|
|
'XFCE',
|
|
'Pantheon',
|
|
'UKUI',
|
|
'Deepin',
|
|
'unity',
|
|
// Chromium identifies LXQt and then picks basic_text for it. That is still a
|
|
// resolved desktop, and overriding a deliberate choice is not this fix's job.
|
|
'LXQt'
|
|
])('treats %s as a desktop Chromium resolves', (desktop) => {
|
|
expect(identifiesResolvedDesktop({ XDG_CURRENT_DESKTOP: desktop })).toBe(true)
|
|
})
|
|
|
|
it.each(['Hyprland', 'sway', 'river', 'niri', '', 'wlroots:custom'])(
|
|
'treats %s as unrecognised',
|
|
(desktop) => {
|
|
expect(identifiesResolvedDesktop({ XDG_CURRENT_DESKTOP: desktop })).toBe(false)
|
|
}
|
|
)
|
|
|
|
it('reads the DESKTOP_SESSION fallback Chromium also consults', () => {
|
|
expect(
|
|
identifiesResolvedDesktop({ XDG_CURRENT_DESKTOP: 'Hyprland', DESKTOP_SESSION: 'plasma' })
|
|
).toBe(true)
|
|
})
|
|
|
|
// Why this case has its own test: missing it is what would strand sealed credentials.
|
|
// A KDE session identified only by KDE_FULL_SESSION gets kwallet from Chromium, and
|
|
// overriding that with libsecret makes every existing secret unreadable.
|
|
it.each(['GNOME_DESKTOP_SESSION_ID', 'KDE_FULL_SESSION', 'KDE_SESSION_VERSION'])(
|
|
'treats a bare %s as identifying a desktop',
|
|
(variable) => {
|
|
expect(identifiesResolvedDesktop({ XDG_CURRENT_DESKTOP: 'Hyprland', [variable]: '1' })).toBe(
|
|
true
|
|
)
|
|
}
|
|
)
|
|
|
|
it('ignores a presence variable that is set but empty', () => {
|
|
expect(
|
|
identifiesResolvedDesktop({ XDG_CURRENT_DESKTOP: 'Hyprland', KDE_FULL_SESSION: ' ' })
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('resolveLinuxPasswordStore', () => {
|
|
it('selects libsecret on an unrecognised desktop with an unlocked secret service', () => {
|
|
expect(resolve({ XDG_CURRENT_DESKTOP: 'Hyprland' }).store).toBe('gnome-libsecret')
|
|
})
|
|
|
|
it('leaves a resolved desktop alone without probing at all', () => {
|
|
const { store, probeCollection } = resolve({ XDG_CURRENT_DESKTOP: 'GNOME' })
|
|
expect(store).toBeNull()
|
|
expect(probeCollection).not.toHaveBeenCalled()
|
|
})
|
|
|
|
// Why this is the whole point of the design: selecting libsecret against a locked
|
|
// collection with no prompter is what made isEncryptionAvailable() block for 76s to
|
|
// first window (STA-5765). Plaintext plus an accurate warning beats a frozen app.
|
|
it('declines to select a locked secret service rather than risk a startup stall', () => {
|
|
const { store, reason } = resolve({ XDG_CURRENT_DESKTOP: 'sway' }, 'locked')
|
|
expect(store).toBeNull()
|
|
expect(reason).toMatch(/locked/)
|
|
})
|
|
|
|
it('declines when no secret service answers', () => {
|
|
expect(resolve({ XDG_CURRENT_DESKTOP: 'river' }, 'unavailable').store).toBeNull()
|
|
})
|
|
|
|
it('defers to an explicit --password-store without probing', () => {
|
|
const { store, probeCollection } = resolve({ XDG_CURRENT_DESKTOP: 'niri' }, 'unlocked', true)
|
|
expect(store).toBeNull()
|
|
expect(probeCollection).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not probe when there is no session bus to probe', () => {
|
|
const probeCollection = vi.fn((): SecretServiceCollectionState => 'unlocked')
|
|
const decision = resolveLinuxPasswordStore({
|
|
env: { XDG_CURRENT_DESKTOP: 'Hyprland' },
|
|
passwordStoreAlreadySelected: false,
|
|
probeCollection
|
|
})
|
|
expect(decision.store).toBeNull()
|
|
expect(probeCollection).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('treats an empty session bus address as no session bus', () => {
|
|
const probeCollection = vi.fn((): SecretServiceCollectionState => 'unlocked')
|
|
resolveLinuxPasswordStore({
|
|
env: { XDG_CURRENT_DESKTOP: 'Hyprland', DBUS_SESSION_BUS_ADDRESS: ' ' },
|
|
passwordStoreAlreadySelected: false,
|
|
probeCollection
|
|
})
|
|
expect(probeCollection).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('gives a reason for every outcome, since the log line is the only trace', () => {
|
|
for (const collection of ['unlocked', 'locked', 'unavailable'] as const) {
|
|
expect(resolve({ XDG_CURRENT_DESKTOP: 'Hyprland' }, collection).reason).not.toBe('')
|
|
}
|
|
})
|
|
})
|