- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
332 lines
15 KiB
JavaScript
332 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
// Regression coverage for password-FIFO auth prewarming. The expensive bw
|
|
// process must be alive before the password is submitted, while the password
|
|
// itself stays out of argv and is written only after the user submits.
|
|
//
|
|
// node tests/auth-prewarm.test.js
|
|
|
|
const fs = require("fs")
|
|
const os = require("os")
|
|
const path = require("path")
|
|
const { execFileSync } = require("child_process")
|
|
|
|
const Model = {}
|
|
new Function("exports", fs.readFileSync(path.join(__dirname, "..", "BitwardenModel.js"), "utf8")
|
|
.replace(/^\.pragma library\s*$/m, "") + `
|
|
exports.unlockPrewarmCommand = typeof unlockPrewarmCommand === "function" ? unlockPrewarmCommand : null
|
|
exports.emailLoginPrewarmCommand = typeof emailLoginPrewarmCommand === "function" ? emailLoginPrewarmCommand : null
|
|
exports.apiKeyLoginCommand = typeof apiKeyLoginCommand === "function" ? apiKeyLoginCommand : null
|
|
exports.authPasswordWriteCommand = typeof authPasswordWriteCommand === "function" ? authPasswordWriteCommand : null
|
|
exports.passwordEnvVar = passwordEnvVar
|
|
`)(Model)
|
|
|
|
let pass = 0
|
|
const failures = []
|
|
const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`)
|
|
const flat = command => (command || []).join(" ")
|
|
|
|
check("the model exposes the three prewarming commands",
|
|
!!Model.unlockPrewarmCommand && !!Model.emailLoginPrewarmCommand && !!Model.authPasswordWriteCommand,
|
|
"unlock, email login and writer commands are required")
|
|
|
|
if (Model.unlockPrewarmCommand && Model.emailLoginPrewarmCommand && Model.authPasswordWriteCommand) {
|
|
const unlock = Model.unlockPrewarmCommand()
|
|
const email = Model.emailLoginPrewarmCommand("person@example.com", false, "")
|
|
const email2fa = Model.emailLoginPrewarmCommand("person@example.com", true, "https://vault.example.com")
|
|
const writer = Model.authPasswordWriteCommand("unlock")
|
|
const distinctive = " exact master password with spaces "
|
|
|
|
check("prewarmed unlock makes bw itself wait on a password FIFO",
|
|
/bw unlock .*--passwordfile/.test(flat(unlock)) && !flat(unlock).includes("--passwordenv"), flat(unlock))
|
|
check("prewarmed email login makes bw itself wait on a password FIFO",
|
|
/bw login .*--passwordfile/.test(flat(email)) && !flat(email).includes("--passwordenv"), flat(email))
|
|
check("email and server inputs remain shell-quoted",
|
|
flat(email2fa).includes("'person@example.com'")
|
|
&& flat(email2fa).includes("'https://vault.example.com'"), flat(email2fa))
|
|
check("2FA still expands from its environment binding",
|
|
flat(email2fa).includes('--code "$QSBW_CODE"'), flat(email2fa))
|
|
check("the writer reads the password from the existing protected environment binding",
|
|
flat(writer).includes('"$' + Model.passwordEnvVar() + '"'), flat(writer))
|
|
check("neither half embeds a password in its command",
|
|
!flat(unlock).includes(distinctive) && !flat(writer).includes(distinctive), flat(unlock) + "\n" + flat(writer))
|
|
check("an unknown FIFO name is rejected instead of becoming a path",
|
|
Model.authPasswordWriteCommand("../elsewhere").length === 0,
|
|
flat(Model.authPasswordWriteCommand("../elsewhere")))
|
|
check("a normally completed child is disarmed before the EXIT cleanup trap runs",
|
|
/wait "\$__auth_job"; __auth_rc=\$\?; __auth_job=''; exit "\$__auth_rc"/.test(flat(unlock)),
|
|
flat(unlock))
|
|
|
|
// Exercise the real command pair against a fake bw. The fake announces that
|
|
// it has started, then blocks while reading the FIFO. Only after observing
|
|
// that announcement do we launch the writer. Leading and trailing spaces
|
|
// prove the panel cannot normalize a real master password along the way.
|
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-prewarm-"))
|
|
const bin = path.join(temp, "bin")
|
|
const runtime = path.join(temp, "runtime")
|
|
fs.mkdirSync(bin)
|
|
fs.mkdirSync(runtime, { mode: 0o700 })
|
|
const started = path.join(temp, "started")
|
|
const received = path.join(temp, "received")
|
|
const output = path.join(temp, "output")
|
|
const error = path.join(temp, "error")
|
|
const token = "Zm9vYmFyYmF6cXV1eDEyMzQ1Njc4OTBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODk9PQ=="
|
|
|
|
fs.writeFileSync(path.join(bin, "bw"), `#!/usr/bin/env bash
|
|
set -u
|
|
password_file=""
|
|
while [ "$#" -gt 0 ]; do
|
|
if [ "$1" = "--passwordfile" ]; then password_file="$2"; shift 2; else shift; fi
|
|
done
|
|
printf started > "$QSBW_STUB_STARTED"
|
|
[ -n "$password_file" ] || exit 9
|
|
IFS= read -r password < "$password_file" || true
|
|
printf '%s' "$password" > "$QSBW_STUB_RECEIVED"
|
|
printf '%s' "$QSBW_STUB_TOKEN"
|
|
`)
|
|
fs.chmodSync(path.join(bin, "bw"), 0o755)
|
|
|
|
try {
|
|
execFileSync("bash", ["-c", `
|
|
set -euo pipefail
|
|
bash -c "$QSBW_PREWARM_SCRIPT" >"$QSBW_OUTPUT" 2>"$QSBW_ERROR" &
|
|
auth_pid=$!
|
|
started=false
|
|
for unused in {1..200}; do
|
|
if [ -s "$QSBW_STUB_STARTED" ]; then started=true; break; fi
|
|
sleep 0.01
|
|
done
|
|
[ "$started" = true ]
|
|
bash -c "$QSBW_WRITER_SCRIPT"
|
|
wait "$auth_pid"
|
|
`], {
|
|
env: Object.assign({}, process.env, {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
XDG_RUNTIME_DIR: runtime,
|
|
BW_PASSWORD: distinctive,
|
|
QSBW_PREWARM_SCRIPT: unlock[2],
|
|
QSBW_WRITER_SCRIPT: writer[2],
|
|
QSBW_STUB_STARTED: started,
|
|
QSBW_STUB_RECEIVED: received,
|
|
QSBW_STUB_TOKEN: token,
|
|
QSBW_OUTPUT: output,
|
|
QSBW_ERROR: error,
|
|
}),
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
})
|
|
check("bw starts before the writer supplies the password", fs.readFileSync(started, "utf8") === "started", "no start marker")
|
|
check("the FIFO preserves the exact password bytes", fs.readFileSync(received, "utf8") === distinctive,
|
|
JSON.stringify(fs.readFileSync(received, "utf8")))
|
|
check("the prewarmed command still returns the session token", fs.readFileSync(output, "utf8") === token,
|
|
fs.readFileSync(output, "utf8"))
|
|
const fifo = path.join(runtime, "qs-bitwarden-cli", "unlock-password.fifo")
|
|
check("the password FIFO is removed when auth finishes", !fs.existsSync(fifo), fifo)
|
|
} catch (errorCaught) {
|
|
failures.push(`the prewarm command pair completes successfully\n ${errorCaught.stderr || errorCaught.message}`)
|
|
} finally {
|
|
fs.rmSync(temp, { recursive: true, force: true })
|
|
}
|
|
|
|
// QML stops a Process by sending SIGTERM to its immediate child. A shell
|
|
// waiting for a foreground FIFO reader can defer that signal until the
|
|
// reader exits, which would strand both after the panel closes. Exercise
|
|
// cancellation separately and require the wrapper, its bw child and the
|
|
// FIFO to disappear promptly.
|
|
const cancelTemp = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-prewarm-cancel-"))
|
|
const cancelBin = path.join(cancelTemp, "bin")
|
|
const cancelRuntime = path.join(cancelTemp, "runtime")
|
|
fs.mkdirSync(cancelBin)
|
|
fs.mkdirSync(cancelRuntime, { mode: 0o700 })
|
|
const childPid = path.join(cancelTemp, "child-pid")
|
|
fs.writeFileSync(path.join(cancelBin, "bw"), `#!/usr/bin/env bash
|
|
printf '%s' "$$" > "$QSBW_STUB_CHILD_PID"
|
|
password_file=""
|
|
while [ "$#" -gt 0 ]; do
|
|
if [ "$1" = "--passwordfile" ]; then password_file="$2"; shift 2; else shift; fi
|
|
done
|
|
[ -n "$password_file" ] || exit 9
|
|
IFS= read -r unused < "$password_file" || true
|
|
`)
|
|
fs.chmodSync(path.join(cancelBin, "bw"), 0o755)
|
|
|
|
try {
|
|
const result = execFileSync("bash", ["-c", `
|
|
set -u
|
|
bash -c "$QSBW_PREWARM_SCRIPT" >"$QSBW_CANCEL_OUTPUT" 2>"$QSBW_CANCEL_ERROR" &
|
|
auth_pid=$!
|
|
for unused in {1..200}; do [ -s "$QSBW_STUB_CHILD_PID" ] && break; sleep 0.01; done
|
|
[ -s "$QSBW_STUB_CHILD_PID" ] || exit 8
|
|
bw_pid=$(cat "$QSBW_STUB_CHILD_PID")
|
|
kill -TERM "$auth_pid"
|
|
parent_stopped=no
|
|
for unused in {1..200}; do
|
|
if ! kill -0 "$auth_pid" 2>/dev/null; then parent_stopped=yes; break; fi
|
|
sleep 0.01
|
|
done
|
|
if [ "$parent_stopped" = no ]; then kill -KILL "$auth_pid" 2>/dev/null || true; fi
|
|
wait "$auth_pid" 2>/dev/null || true
|
|
child_stopped=no
|
|
for unused in {1..200}; do
|
|
if ! kill -0 "$bw_pid" 2>/dev/null; then child_stopped=yes; break; fi
|
|
sleep 0.01
|
|
done
|
|
if [ "$child_stopped" = no ]; then kill -KILL "$bw_pid" 2>/dev/null || true; fi
|
|
fifo_gone=no
|
|
[ ! -e "$XDG_RUNTIME_DIR/qs-bitwarden-cli/unlock-password.fifo" ] && fifo_gone=yes
|
|
printf '%s %s %s' "$parent_stopped" "$child_stopped" "$fifo_gone"
|
|
`], {
|
|
env: Object.assign({}, process.env, {
|
|
PATH: `${cancelBin}:${process.env.PATH}`,
|
|
XDG_RUNTIME_DIR: cancelRuntime,
|
|
QSBW_PREWARM_SCRIPT: unlock[2],
|
|
QSBW_STUB_CHILD_PID: childPid,
|
|
QSBW_CANCEL_OUTPUT: path.join(cancelTemp, "output"),
|
|
QSBW_CANCEL_ERROR: path.join(cancelTemp, "error"),
|
|
}),
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
})
|
|
check("cancelling prewarm stops the wrapper and bw child and removes the FIFO",
|
|
result === "yes yes yes", result)
|
|
} catch (errorCaught) {
|
|
failures.push(`cancelling prewarm completes cleanly\n ${errorCaught.stderr || errorCaught.message}`)
|
|
} finally {
|
|
fs.rmSync(cancelTemp, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
// API-key login does not use a password FIFO, but it still runs through the
|
|
// same cancellable Process. Stopping that Process must terminate the active bw
|
|
// child rather than orphaning an authentication attempt behind the panel.
|
|
if (Model.apiKeyLoginCommand) {
|
|
const cancelTemp = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-apikey-cancel-"))
|
|
const cancelBin = path.join(cancelTemp, "bin")
|
|
fs.mkdirSync(cancelBin)
|
|
const childPid = path.join(cancelTemp, "child-pid")
|
|
fs.writeFileSync(path.join(cancelBin, "bw"), `#!/usr/bin/env bash
|
|
printf '%s' "$$" > "$QSBW_STUB_CHILD_PID"
|
|
sleep 30
|
|
`)
|
|
fs.chmodSync(path.join(cancelBin, "bw"), 0o755)
|
|
|
|
try {
|
|
const result = execFileSync("bash", ["-c", `
|
|
set -u
|
|
bash -c "$QSBW_APIKEY_SCRIPT" >/dev/null 2>&1 &
|
|
auth_pid=$!
|
|
for unused in {1..200}; do [ -s "$QSBW_STUB_CHILD_PID" ] && break; sleep 0.01; done
|
|
[ -s "$QSBW_STUB_CHILD_PID" ] || exit 8
|
|
bw_pid=$(cat "$QSBW_STUB_CHILD_PID")
|
|
kill -TERM "$auth_pid"
|
|
parent_stopped=no
|
|
for unused in {1..200}; do
|
|
if ! kill -0 "$auth_pid" 2>/dev/null; then parent_stopped=yes; break; fi
|
|
sleep 0.01
|
|
done
|
|
if [ "$parent_stopped" = no ]; then kill -KILL "$auth_pid" 2>/dev/null || true; fi
|
|
wait "$auth_pid" 2>/dev/null || true
|
|
child_stopped=no
|
|
for unused in {1..200}; do
|
|
if ! kill -0 "$bw_pid" 2>/dev/null; then child_stopped=yes; break; fi
|
|
sleep 0.01
|
|
done
|
|
if [ "$child_stopped" = no ]; then kill -KILL "$bw_pid" 2>/dev/null || true; fi
|
|
printf '%s %s' "$parent_stopped" "$child_stopped"
|
|
`], {
|
|
env: Object.assign({}, process.env, {
|
|
PATH: `${cancelBin}:${process.env.PATH}`,
|
|
QSBW_APIKEY_SCRIPT: Model.apiKeyLoginCommand("")[2],
|
|
QSBW_STUB_CHILD_PID: childPid,
|
|
}),
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
})
|
|
check("cancelling API-key login stops the wrapper and active bw child",
|
|
result === "yes yes", result)
|
|
} catch (errorCaught) {
|
|
failures.push(`cancelling API-key login completes cleanly\n ${errorCaught.stderr || errorCaught.message}`)
|
|
} finally {
|
|
fs.rmSync(cancelTemp, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
// The QML lifecycle is part of the security boundary: start early, write only
|
|
// on submit, and stop a waiting process when the panel closes.
|
|
const panelSrc = fs.readFileSync(path.join(__dirname, "..", "Panel.qml"), "utf8")
|
|
const bodyOf = name => {
|
|
const start = panelSrc.indexOf(`function ${name}(`)
|
|
if (start === -1) return ""
|
|
let depth = 0
|
|
for (let i = panelSrc.indexOf("{", start); i < panelSrc.length; i++) {
|
|
if (panelSrc[i] === "{") depth++
|
|
else if (panelSrc[i] === "}" && --depth === 0) return panelSrc.slice(start, i + 1)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
check("opening an already-locked panel starts unlock prewarming",
|
|
/prepareUnlock\(\)/.test(bodyOf("onPanelOpened")), bodyOf("onPanelOpened"))
|
|
check("submitting unlock writes to the prepared FIFO",
|
|
/writeAuthPassword\("unlock",\s*p\)/.test(bodyOf("unlockVaultWithPassword")), bodyOf("unlockVaultWithPassword"))
|
|
check("closing the panel cancels auth prewarming",
|
|
/cancelAuthPrewarm/.test(bodyOf("close")), bodyOf("close"))
|
|
check("an unreadable status result cancels a prewarm that can no longer be used",
|
|
/if\s*\(!st\)\s*\{\s*cancelAuthPrewarm\(\)/.test(bodyOf("onStatusFinished")),
|
|
bodyOf("onStatusFinished"))
|
|
check("an externally unlocked status cancels the obsolete locked-state prewarm",
|
|
/if\s*\(st\.unlocked\)\s*\{\s*cancelAuthPrewarm\(\)/.test(bodyOf("onStatusFinished")),
|
|
bodyOf("onStatusFinished"))
|
|
check("master-password validation preserves whitespace",
|
|
/var p\s*=\s*String\(/.test(bodyOf("unlockVaultWithPassword"))
|
|
&& !/var p\s*=\s*String\([^\n]+\.trim\(\)/.test(bodyOf("unlockVaultWithPassword")),
|
|
bodyOf("unlockVaultWithPassword"))
|
|
check("focusing the email-login password field prepares login",
|
|
/id:\s*loginPassField[\s\S]{0,700}onActiveFocusChanged:[\s\S]{0,160}prepareEmailLogin/.test(panelSrc),
|
|
"loginPassField has no prewarm focus handler")
|
|
const prepareEmail = bodyOf("prepareEmailLogin")
|
|
check("a custom server is not configured by focus-only prewarming",
|
|
/var serverUrl\s*=\s*resolvedLoginServerUrl\(\)/.test(prepareEmail)
|
|
&& /if\s*\(serverUrl\)\s*return/.test(prepareEmail)
|
|
&& prepareEmail.indexOf("resolvedLoginServerUrl") < prepareEmail.indexOf("loginProc.command"),
|
|
prepareEmail)
|
|
check("submitting while an obsolete login prewarm stops queues a clean restart",
|
|
/loginSubmitAfterPrewarmStop\s*=\s*true/.test(bodyOf("submitLogin"))
|
|
&& /loginProc\.running\s*=\s*false/.test(bodyOf("submitLogin")),
|
|
bodyOf("submitLogin"))
|
|
const loginProcBlock = panelSrc.slice(panelSrc.indexOf("id: loginProc"), panelSrc.indexOf("id: authPasswordWriterProc"))
|
|
// The dispatch lives in resumeDeferredLogin() so both ways the process can end
|
|
// -- its own exit, and the buffer scrub that may follow it -- go through it.
|
|
// A scrub that returned early used to drop the queued login silently.
|
|
const resumeDeferredBlock = bodyOf("resumeDeferredLogin")
|
|
check("the obsolete prewarm exit starts the queued login instead of consuming its result",
|
|
/loginSubmitAfterPrewarmStop[\s\S]*submitLogin/.test(resumeDeferredBlock)
|
|
&& /resumeDeferredLogin\(true\)/.test(loginProcBlock),
|
|
resumeDeferredBlock)
|
|
check("a queued login survives the buffer scrub taking the process first",
|
|
/finishScrubRun\(loginProc\)\)\s*\{[\s\S]{0,140}resumeDeferredLogin\(false\)/.test(loginProcBlock),
|
|
loginProcBlock)
|
|
check("focusing during prewarm shutdown queues another prewarm",
|
|
/loginPrepareAfterPrewarmStop\s*=\s*true/.test(bodyOf("prepareEmailLogin")),
|
|
bodyOf("prepareEmailLogin"))
|
|
check("the stopped process services a queued prewarm when no submit is waiting",
|
|
/loginPrepareAfterPrewarmStop[\s\S]*prepareEmailLogin/.test(resumeDeferredBlock),
|
|
resumeDeferredBlock)
|
|
check("a cancelled login with no queued restart scrubs any token that won the exit race",
|
|
/else if \(mayScrub\) \{\s*\n\s*clearProcessCollectorSoon\(loginProc\)/.test(resumeDeferredBlock),
|
|
resumeDeferredBlock)
|
|
// The scrub is the fallback, so a queued restart must be preferred to it --
|
|
// otherwise the restart is what gets dropped.
|
|
check("a queued restart is dispatched in preference to the scrub",
|
|
resumeDeferredBlock.indexOf("loginSubmitAfterPrewarmStop")
|
|
< resumeDeferredBlock.indexOf("clearProcessCollectorSoon"),
|
|
resumeDeferredBlock)
|
|
const unlockProcBlock = panelSrc.slice(panelSrc.indexOf("id: unlockProc"), panelSrc.indexOf("id: logoutProc"))
|
|
check("a cancelled unlock scrubs any token that won the exit race",
|
|
/!root\.unlockSubmitted[\s\S]{0,120}clearProcessCollectorSoon\(unlockProc\)/.test(unlockProcBlock),
|
|
unlockProcBlock)
|
|
|
|
console.log(`${pass} passed, ${failures.length} failed`)
|
|
if (failures.length) {
|
|
console.error("\nFAILURES:\n " + failures.join("\n "))
|
|
process.exit(1)
|
|
}
|