Managing a stored credential meant installing Seahorse. The keyring rows on Privacy could say whether it was locked and nothing about what was in it. Four rules, each pinned by a contract, because each is a way this could leak the thing it exists to protect: Listing never reads values. Enumerating reports labels and attributes; it does not ask the keyring to hand over what it is protecting. A secret never reaches a command line. /proc makes argv readable by every process on this machine, so a password passed as an argument is published to all of them. The helper reads the value in process and writes it to wl-copy on stdin. A secret never reaches an error message, a log, or a QML property. An exception raised while holding a password does not get to choose what text is printed, so the clipboard tool's stderr is discarded rather than echoed. Forgetting one is irreversible, so the first press asks and the second does it, and the confirming button is the only one wearing danger. The list is collapsed until asked for: opening Privacy should not enumerate someone's passwords as a side effect. A copied value clears itself about a minute later, but only if the clipboard still holds it -- the guard compares a SHA-256, so the waiting process never has the password. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
51 lines
1.6 KiB
QML
51 lines
1.6 KiB
QML
import QtQuick
|
|
import qs.config
|
|
|
|
Rectangle {
|
|
id: root
|
|
|
|
property string text: ""
|
|
// "normal", "accent", or "danger". Danger is for an action that cannot be
|
|
// undone -- it never initiates one on a single press, it marks the press
|
|
// that confirms it, so the confirming button cannot be mistaken for the
|
|
// Cancel sitting next to it.
|
|
property string tone: "normal"
|
|
property bool enabled: true
|
|
signal clicked
|
|
|
|
implicitWidth: label.implicitWidth + 22
|
|
implicitHeight: 31
|
|
radius: 9
|
|
opacity: enabled ? 1 : 0.45
|
|
color: {
|
|
if (tone === "accent")
|
|
return mouse.containsMouse ? Theme.mix(Theme.accent, Theme.fg, 0.12) : Theme.accent;
|
|
if (tone === "danger")
|
|
return mouse.containsMouse ? Theme.alpha(Theme.danger, 0.22) : Theme.alpha(Theme.danger, 0.12);
|
|
return mouse.containsMouse ? Theme.alpha(Theme.fg, 0.13) : Theme.alpha(Theme.fg, 0.075);
|
|
}
|
|
border.width: tone === "accent" ? 0 : 1
|
|
border.color: tone === "danger" ? Theme.alpha(Theme.danger, 0.45) : Theme.alpha(Theme.fg, 0.08)
|
|
|
|
Text {
|
|
id: label
|
|
anchors.centerIn: parent
|
|
text: root.text
|
|
color: root.tone === "accent"
|
|
? Theme.bgDark
|
|
: (root.tone === "danger" ? Theme.danger : Theme.fg)
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.Medium
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouse
|
|
anchors.fill: parent
|
|
enabled: root.enabled
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.clicked()
|
|
}
|
|
}
|