// Privacy & Security. // // GNOME's Privacy panel covers screen lock, camera and microphone access, file // history, trash, and device security. Panama covers the parts it genuinely // owns and is explicit about the parts it does not. // // The file-history and trash settings are the notable omission, and the reason // is worth stating: those are GNOME preferences enforced by gsd-housekeeping, // which is not running in a Hyprland session. Offering switches for them would // store a preference, change nothing, and give no sign of it -- the exact // failure this codebase keeps designing against. So they are delegated by name // rather than reimplemented as controls that lie. import QtQuick import qs.config import qs.services SettingsPage { id: root title: "Privacy & Security" // The stored-secret list is collapsed until asked for, and one item at a // time can be waiting on a confirmed Forget. property bool showingSecrets: false property string confirmingPath: "" // What a stored secret is FOR, from its attributes. Never its value. function describe(item: var): string { const attributes = item?.attributes ?? {}; const parts = []; for (const key of ["user", "username", "account", "server", "host", "domain", "service", "application"]) { if (attributes[key]) parts.push(String(attributes[key])); } if (parts.length > 0) return parts.join(" · "); const schema = String(item?.schema ?? ""); return schema !== "" ? schema : "No further detail stored"; } lede: DeviceSecurity.scanned && DeviceSecurity.attentionCount === 0 ? "Screen lock, device access, and a machine whose security settings all check out." : "Screen lock, which applications can see you, and how this machine is protected." Component.onCompleted: { if (!DeviceSecurity.scanned) DeviceSecurity.refresh(); if (!Keyring.scanned) Keyring.refresh(); } SettingsCard { title: "Screen lock" subtitle: "The same settings as Power & Lock, which is where the idle timings live." SliderRow { setting: "lockMinutes"; zeroLabel: "Never" } ToggleRow { setting: "lockOnSleep"; divider: false } } // The login keyring, which nothing else surfaces. // // It is unlocked at sign-in by PAM, so this card normally just confirms // that. It earns its place on the rare occasion it is not: a locked keyring // breaks saved passwords everywhere at once, and does it without ever // saying the word "keyring" -- you get a mail account that will not // authenticate and a git push that cannot find its key. SettingsCard { visible: Keyring.scanned title: "Saved passwords" subtitle: !Keyring.available ? "No secret service is answering, so saved passwords are unavailable." : Keyring.locked ? "The login keyring is locked. Saved passwords cannot be read until it is unlocked, and applications that need one will appear to fail for unrelated reasons." : "The login keyring is unlocked, as it is after every normal sign-in." // Two rows rather than one with a conditional button: a locked keyring // needs an action, an unlocked one is a statement of fact, and ActionRow // and TextRow already say exactly those two things. ActionRow { visible: Keyring.available && Keyring.locked label: "Login keyring" detail: "Unlock to restore access to stored passwords and keys" action: Keyring.unlocking ? "Waiting…" : "Unlock" enabled: !Keyring.unlocking divider: Keyring.replacementDaemon || Keyring.lastError !== "" onTriggered: Keyring.unlock() } TextRow { visible: !(Keyring.available && Keyring.locked) label: "Login keyring" detail: Keyring.available ? "Unlocked at sign-in by PAM, the same way GNOME does it" : "No secret service is answering on this session" value: Keyring.available ? "Unlocked" : "Unavailable" divider: Keyring.replacementDaemon || Keyring.lastError !== "" } // Only shown when it is true, because it is a diagnostic rather than a // setting: it means the daemon holding your secrets is not the one PAM // started, so whatever unlocked it will not survive a restart. SettingRow { visible: Keyring.replacementDaemon label: "Keyring service" detail: "The original keyring service was replaced during this session, usually after it crashed. Signing out and back in restores the one PAM unlocks." value: "Replaced" divider: Keyring.lastError !== "" } SettingRow { visible: Keyring.lastError !== "" label: "Keyring problem" detail: Keyring.lastError divider: false } } // ── What is actually stored ────────────────────────────────────────────── // Collapsed until asked. Opening the Privacy page should not enumerate // someone's saved passwords as a side effect, and the list is long enough // that it would bury every other setting on the page. SettingsCard { visible: Keyring.scanned && Keyring.available && !Keyring.locked title: "Stored secrets" subtitle: Keyring.listed ? "Passwords and tokens applications have saved. The values are never shown here." : "Passwords and tokens applications have saved, listed only when you ask." ActionRow { label: "Saved items" detail: Keyring.listed ? Keyring.storedCount + " stored across " + Keyring.collections.length + " keyring" + (Keyring.collections.length === 1 ? "" : "s") : "Read the keyring and list what is in it" action: root.showingSecrets ? "Hide" : (Keyring.listing ? "Reading…" : "Show") enabled: !Keyring.listing divider: root.showingSecrets onTriggered: { if (root.showingSecrets) { root.showingSecrets = false; root.confirmingPath = ""; return; } root.showingSecrets = true; if (!Keyring.listed) Keyring.list(); } } TextRow { visible: root.showingSecrets && Keyring.copiedPath !== "" label: "Copied to the clipboard" detail: "It clears itself in about a minute, unless you copy something else first." value: "" divider: true } Repeater { model: root.showingSecrets && Keyring.listed ? Keyring.collections : [] delegate: Column { id: collectionBlock required property var modelData width: parent.width TextRow { width: collectionBlock.width label: String(collectionBlock.modelData.label ?? "") detail: collectionBlock.modelData.locked ? "Locked, so its contents cannot be listed" : (collectionBlock.modelData.items ?? []).length + " stored" value: "" divider: false } Repeater { model: collectionBlock.modelData.items ?? [] delegate: SettingRow { id: secretRow required property var modelData required property int index readonly property string itemPath: String(secretRow.modelData.path ?? "") readonly property bool confirming: root.confirmingPath === secretRow.itemPath width: collectionBlock.width label: String(secretRow.modelData.label ?? "") // Attributes, never the value: what the secret is FOR is // the part that identifies it. detail: root.describe(secretRow.modelData) controlWidth: 200 divider: secretRow.index < (collectionBlock.modelData.items ?? []).length - 1 Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 8 SettingsButton { text: secretRow.confirming ? "Cancel" : "Copy" enabled: !Keyring.working onClicked: { if (secretRow.confirming) { root.confirmingPath = ""; return; } Keyring.copy(secretRow.itemPath); } } SettingsButton { // Two presses, always. Forgetting a stored // password cannot be undone, and the button // sits next to Copy where a misclick is cheap. text: secretRow.confirming ? "Forget it" : "Forget" tone: secretRow.confirming ? "danger" : "normal" enabled: !Keyring.working onClicked: { if (!secretRow.confirming) { root.confirmingPath = secretRow.itemPath; return; } root.confirmingPath = ""; Keyring.forget(secretRow.itemPath); } } } } } Item { width: 1; height: 6 } } } TextRow { visible: root.showingSecrets && Keyring.listed && Keyring.storedCount === 0 label: "Nothing stored yet" detail: "Applications that save a password or token will appear here." value: "" divider: false } } SettingsCard { title: "Camera & microphone" subtitle: PrivacyState.anyActive ? "In use right now — the bar shows an indicator whenever this is true." : "Nothing is using your camera or microphone." TextRow { label: "Camera" detail: PrivacyState.cameraActive ? "In use by " + (PrivacyState.cameraApp || "an application") : "Not in use" value: PrivacyState.cameraActive ? "Active" : "Idle" } TextRow { label: "Microphone" detail: PrivacyState.microphoneActive ? "In use by " + (PrivacyState.microphoneApp || "an application") : "Not in use" value: PrivacyState.microphoneActive ? "Active" : "Idle" } TextRow { label: "Screen sharing" detail: PrivacyState.screenSharingActive ? "Being shared by " + (PrivacyState.screenSharingApp || "an application") : "Not being shared" value: PrivacyState.screenSharingActive ? "Active" : "Idle" divider: false } } SettingsCard { title: "Device security" subtitle: DeviceSecurity.attentionCount === 0 ? "Everything below is in its recommended state." : DeviceSecurity.attentionCount + " item" + (DeviceSecurity.attentionCount === 1 ? "" : "s") + " below may deserve attention." Repeater { model: DeviceSecurity.facts TextRow { id: factRow required property var modelData required property int index label: factRow.modelData.label detail: factRow.modelData.detail value: factRow.modelData.value divider: factRow.index < DeviceSecurity.facts.length - 1 } } } SettingsCard { title: "Owned by Fedora" subtitle: "File history and trash retention are GNOME preferences, applied by a housekeeping service that does not run in a Hyprland session. They are not offered as switches here, because storing that preference would change nothing." ActionRow { label: "File history & trash" detail: "Opens GNOME Settings, which owns these" action: "Open privacy" divider: false onTriggered: SystemSettings.openGnomePanel("privacy") } } }