- 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
478 lines
24 KiB
JavaScript
478 lines
24 KiB
JavaScript
#!/usr/bin/env node
|
|
// One `bw list items` read feeds both the panel and the companion. These tests
|
|
// run the real shell pipeline against a fake `bw` and a real FIFO, because the
|
|
// properties that matter are process-boundary properties: what reaches QML's
|
|
// stdout, what reaches the FIFO, and -- above all -- that the optional agent
|
|
// branch can never take the ordinary item list down with it.
|
|
//
|
|
// node tests/ssh-agent-pipeline.test.js
|
|
|
|
const fs = require("fs")
|
|
const os = require("os")
|
|
const path = require("path")
|
|
const { spawnSync, execFileSync } = require("child_process")
|
|
|
|
const repoRoot = path.join(__dirname, "..")
|
|
const Model = {}
|
|
new Function("exports", fs.readFileSync(path.join(repoRoot, "BitwardenModel.js"), "utf8")
|
|
.replace(/^\.pragma library\s*$/m, "") + `
|
|
exports.sanitizedListCommand = sanitizedListCommand
|
|
exports.sshAgentFifoPath = sshAgentFifoPath
|
|
exports.loadIdEnvVar = loadIdEnvVar
|
|
exports.isValidLoadId = isValidLoadId
|
|
exports.loadIdCommand = loadIdCommand
|
|
exports.sshAgentLoadBeginLine = sshAgentLoadBeginLine
|
|
exports.sshAgentLoadEndLine = sshAgentLoadEndLine
|
|
exports.sshAgentVaultLockedLine = sshAgentVaultLockedLine
|
|
exports.sshAgentLoggedOutLine = sshAgentLoggedOutLine
|
|
exports.sshAgentHelloLine = sshAgentHelloLine
|
|
`)(Model)
|
|
|
|
let pass = 0
|
|
const failures = []
|
|
const check = (label, ok, detail) => ok ? pass++ : failures.push(`${label}\n ${detail}`)
|
|
const eq = (label, actual, expected) =>
|
|
check(label, actual === expected, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
|
|
|
|
const PRIVATE_MARKER = "SSH_PRIVATE_MARKER_must_not_reach_QML"
|
|
const REPROMPT_MARKER = "REPROMPT_PRIVATE_MARKER_must_not_reach_the_agent"
|
|
const LOAD_ID = "0123456789abcdef0123456789abcdef"
|
|
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-pipeline-"))
|
|
const fixturePath = path.join(tempDir, "items.json")
|
|
const runtimeDir = path.join(tempDir, "run")
|
|
fs.mkdirSync(runtimeDir, { mode: 0o700 })
|
|
fs.mkdirSync(path.join(runtimeDir, "qs-bitwarden-cli"), { mode: 0o700 })
|
|
const fifoPath = Model.sshAgentFifoPath(runtimeDir)
|
|
|
|
fs.writeFileSync(path.join(tempDir, "bw"), [
|
|
"#!/usr/bin/env bash",
|
|
'if [ "$1" = "--version" ]; then printf "%s\\n" "${QSBW_BW_VERSION:-2026.2.0}"; exit 0; fi',
|
|
'cat -- "$QSBW_FIXTURE"',
|
|
'exit "${QSBW_BW_EXIT:-0}"',
|
|
""
|
|
].join("\n"), { mode: 0o755 })
|
|
|
|
const baseEnv = () => Object.assign({}, process.env, {
|
|
PATH: tempDir + path.delimiter + process.env.PATH,
|
|
QSBW_FIXTURE: fixturePath,
|
|
XDG_RUNTIME_DIR: runtimeDir
|
|
})
|
|
|
|
const privatePem = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + PRIVATE_MARKER + "\n-----END OPENSSH PRIVATE KEY-----"
|
|
const repromptPem = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + REPROMPT_MARKER + "\n-----END OPENSSH PRIVATE KEY-----"
|
|
|
|
const fixture = [
|
|
{ object: "item", id: "login-1", type: 1, name: "Login", login: { username: "u", password: "p" } },
|
|
{ object: "item", id: "ssh-1", type: 5, name: "Work", favorite: true, reprompt: 0,
|
|
sshKey: { privateKey: privatePem, publicKey: "ssh-ed25519 AAAAWORK", fingerprint: "SHA256:work" } },
|
|
{ object: "item", id: "ssh-2", type: 5, name: "Guarded", reprompt: 1,
|
|
sshKey: { privateKey: repromptPem, publicKey: "ssh-ed25519 AAAAGUARD", fingerprint: "SHA256:guard" } },
|
|
{ object: "item", id: "bank-1", type: 6, name: "Bank", bankAccount: { number: "UNKNOWN_TYPE_MARKER" } }
|
|
]
|
|
|
|
// A real reader on the FIFO, held open the way the companion holds it (O_RDWR),
|
|
// so the writer never blocks on open and never sees the reader disappear.
|
|
function withFifoReader(fn) {
|
|
try { fs.unlinkSync(fifoPath) } catch (e) {}
|
|
execFileSync("mkfifo", ["-m", "600", fifoPath])
|
|
const fd = fs.openSync(fifoPath, fs.constants.O_RDWR | fs.constants.O_NONBLOCK)
|
|
try {
|
|
const result = fn()
|
|
// Drain whatever the branch wrote, without blocking when it wrote nothing.
|
|
let out = Buffer.alloc(0)
|
|
const buf = Buffer.alloc(1 << 20)
|
|
for (;;) {
|
|
let n = 0
|
|
try { n = fs.readSync(fd, buf, 0, buf.length, null) } catch (e) { break }
|
|
if (n <= 0) break
|
|
out = Buffer.concat([out, buf.slice(0, n)])
|
|
}
|
|
return { result, fifo: out.toString("utf8") }
|
|
} finally {
|
|
fs.closeSync(fd)
|
|
try { fs.unlinkSync(fifoPath) } catch (e) {}
|
|
}
|
|
}
|
|
|
|
function runPipeline(opts, envOverrides, contents) {
|
|
fs.writeFileSync(fixturePath, contents === undefined ? JSON.stringify(fixture) : contents)
|
|
const command = Model.sanitizedListCommand(opts)
|
|
return spawnSync(command[0], command.slice(1), {
|
|
env: Object.assign(baseEnv(), envOverrides || {}),
|
|
encoding: "utf8", maxBuffer: 20 * 1024 * 1024
|
|
})
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// The load nonce
|
|
// -------------------------------------------------------------------------
|
|
|
|
eq("the load id env var is named", Model.loadIdEnvVar(), "QSBW_LOAD_ID")
|
|
eq("a 128-bit lowercase hex nonce is valid", Model.isValidLoadId(LOAD_ID), true)
|
|
eq("a short nonce is refused", Model.isValidLoadId("abc"), false)
|
|
eq("an uppercase nonce is refused", Model.isValidLoadId(LOAD_ID.toUpperCase()), false)
|
|
eq("a non-hex nonce is refused", Model.isValidLoadId("z".repeat(32)), false)
|
|
eq("an empty nonce is refused", Model.isValidLoadId(""), false)
|
|
eq("a non-string nonce is refused", Model.isValidLoadId(null), false)
|
|
|
|
const nonceRun = spawnSync("bash", Model.loadIdCommand().slice(1), { encoding: "utf8" })
|
|
const generated = String(nonceRun.stdout || "").trim()
|
|
eq("the generator produces a usable nonce", Model.isValidLoadId(generated), true)
|
|
const second = String(spawnSync("bash", Model.loadIdCommand().slice(1), { encoding: "utf8" }).stdout || "").trim()
|
|
check("two nonces differ", generated !== second, generated + " == " + second)
|
|
|
|
// The nonce is what stops another same-UID process writing its own key set
|
|
// into an open FIFO window. /proc/<pid>/cmdline is world-readable, so it must
|
|
// never be an argument.
|
|
const agentCommandText = Model.sanitizedListCommand({ agentBranch: true, runtimeDir: runtimeDir }).join(" ")
|
|
// The fstat below is the check that decides; this one only keeps a dead
|
|
// companion from costing a full decrypt-and-filter pass whose output has
|
|
// nowhere to go.
|
|
check("a missing FIFO skips the filter rather than running it for nobody",
|
|
/if \[ -p "\$__qsbw_fifo" \]; then/.test(agentCommandText),
|
|
agentCommandText.slice(0, 500))
|
|
check("the FIFO is opened without following a swapped symlink",
|
|
agentCommandText.indexOf("O_NOFOLLOW") >= 0, agentCommandText.slice(0, 500))
|
|
check("the opened descriptor, not the pathname, is checked as a FIFO",
|
|
agentCommandText.indexOf("fstatSync") >= 0 && agentCommandText.indexOf("S_IFIFO") >= 0,
|
|
agentCommandText.slice(0, 500))
|
|
check("the nonce is read from the environment, never passed in argv",
|
|
agentCommandText.indexOf(Model.loadIdEnvVar()) >= 0 && agentCommandText.indexOf(LOAD_ID) < 0,
|
|
"nonce appears literally in the command")
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Disabled mode has no private branch at all
|
|
// -------------------------------------------------------------------------
|
|
|
|
const plainText = Model.sanitizedListCommand().join(" ")
|
|
check("the default command has no tee branch", plainText.indexOf("tee") < 0, plainText.slice(0, 400))
|
|
check("the default command never names the key FIFO",
|
|
plainText.indexOf("ssh-keys.fifo") < 0, plainText.slice(0, 400))
|
|
check("an explicitly disabled branch is identical to the default",
|
|
Model.sanitizedListCommand({ agentBranch: false, runtimeDir: runtimeDir }).join(" ")
|
|
=== Model.sanitizedListCommand().join(" "), "disabled form differs from the default")
|
|
|
|
const disabledRun = withFifoReader(() =>
|
|
runPipeline({ agentBranch: false, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID }))
|
|
eq("the disabled pipeline succeeds", disabledRun.result.status, 0)
|
|
eq("the disabled pipeline writes nothing to the FIFO", disabledRun.fifo, "")
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Enabled mode: one read, two consumers
|
|
// -------------------------------------------------------------------------
|
|
|
|
const enabled = withFifoReader(() =>
|
|
runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID }))
|
|
|
|
eq("the fan-out pipeline succeeds", enabled.result.status, 0)
|
|
|
|
const panelOut = JSON.parse(enabled.result.stdout)
|
|
check("QML still receives the ordinary items", panelOut.items.length === 1 && panelOut.items[0].id === "login-1",
|
|
JSON.stringify(panelOut.items))
|
|
eq("QML still receives both public SSH keys", panelOut.sshKeys.length, 2)
|
|
check("no private key marker reaches QML", enabled.result.stdout.indexOf(PRIVATE_MARKER) < 0, "leaked")
|
|
check("no re-prompt private marker reaches QML", enabled.result.stdout.indexOf(REPROMPT_MARKER) < 0, "leaked")
|
|
check("no unknown cipher type reaches QML", enabled.result.stdout.indexOf("UNKNOWN_TYPE_MARKER") < 0, "leaked")
|
|
|
|
const payload = JSON.parse(enabled.fifo)
|
|
eq("the FIFO payload carries the matching nonce", payload.loadId, LOAD_ID)
|
|
eq("the FIFO payload carries only eligible keys", payload.items.length, 1)
|
|
eq("the eligible key is the non-reprompt one", payload.items[0].itemId, "ssh-1")
|
|
eq("the eligible key carries its private material", payload.items[0].privateKey, privatePem)
|
|
eq("the eligible key carries its public blob", payload.items[0].publicKey, "ssh-ed25519 AAAAWORK")
|
|
eq("the eligible key carries its fingerprint", payload.items[0].fingerprint, "SHA256:work")
|
|
eq("the eligible key is not marked re-prompt", payload.items[0].requiresReprompt, false)
|
|
|
|
// Re-prompt private keys never leave the jq stage, so the companion is never
|
|
// asked to hold one -- its own skip rule is the second line, not the first.
|
|
check("no re-prompt private key reaches the FIFO", enabled.fifo.indexOf(REPROMPT_MARKER) < 0, "leaked")
|
|
check("no ordinary item reaches the FIFO", enabled.fifo.indexOf("login-1") < 0, "leaked")
|
|
check("no unknown cipher type reaches the FIFO", enabled.fifo.indexOf("UNKNOWN_TYPE_MARKER") < 0, "leaked")
|
|
|
|
// The companion decodes with serde `deny_unknown_fields`, so the projection
|
|
// has to be exactly the agreed shape or every load fails closed.
|
|
eq("the envelope has exactly loadId and items",
|
|
Object.keys(payload).sort().join(","), "items,loadId")
|
|
eq("each item has exactly the agreed fields",
|
|
Object.keys(payload.items[0]).sort().join(","),
|
|
"fingerprint,itemId,name,privateKey,publicKey,requiresReprompt")
|
|
|
|
// -------------------------------------------------------------------------
|
|
// The optional branch can never break the ordinary list
|
|
// -------------------------------------------------------------------------
|
|
|
|
// No FIFO at all: the helper never started, or cleaned up on the way out.
|
|
{
|
|
try { fs.unlinkSync(fifoPath) } catch (e) {}
|
|
const run = runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID })
|
|
eq("a missing FIFO still loads the item list", run.status, 0)
|
|
check("a missing FIFO still produces the full envelope",
|
|
JSON.parse(run.stdout).sshKeys.length === 2, run.stdout.slice(0, 200))
|
|
check("a missing FIFO is not created as a regular file", !fs.existsSync(fifoPath),
|
|
"the branch created something at the FIFO path")
|
|
}
|
|
|
|
// A regular file squatting on the FIFO path is never written through.
|
|
{
|
|
fs.writeFileSync(fifoPath, "not a fifo\n")
|
|
const run = runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID })
|
|
eq("a squatted FIFO path still loads the item list", run.status, 0)
|
|
eq("the squatting file is untouched", fs.readFileSync(fifoPath, "utf8"), "not a fifo\n")
|
|
check("no private key was written to the squatting file",
|
|
fs.readFileSync(fifoPath, "utf8").indexOf(PRIVATE_MARKER) < 0, "leaked")
|
|
fs.unlinkSync(fifoPath)
|
|
}
|
|
|
|
// A FIFO nobody drains. The branch opens O_RDWR so it never blocks on open,
|
|
// and the payload here fits the pipe buffer, so the list is unaffected.
|
|
{
|
|
execFileSync("mkfifo", ["-m", "600", fifoPath])
|
|
const run = runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: LOAD_ID })
|
|
eq("an undrained FIFO still loads the item list", run.status, 0)
|
|
check("an undrained FIFO still produces the full envelope",
|
|
JSON.parse(run.stdout).sshKeys.length === 2, run.stdout.slice(0, 200))
|
|
fs.unlinkSync(fifoPath)
|
|
}
|
|
|
|
// A missing nonce must not produce a payload the companion would accept.
|
|
{
|
|
const run = withFifoReader(() =>
|
|
runPipeline({ agentBranch: true, runtimeDir: runtimeDir }, { [Model.loadIdEnvVar()]: "" }))
|
|
eq("a missing nonce still loads the item list", run.result.status, 0)
|
|
check("a missing nonce never yields a usable payload",
|
|
run.fifo === "" || !Model.isValidLoadId((JSON.parse(run.fifo || "{}").loadId) || ""),
|
|
run.fifo.slice(0, 200))
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Whole-pipeline failures publish no partial private set
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Input the filters reject: the branch cannot even construct a payload, so
|
|
// nothing usable reaches the FIFO in the first place.
|
|
for (const [label, contents] of [
|
|
["malformed JSON", "{ this is not json"],
|
|
["a truncated array", '[{"object":"item","id":"a","type":1,'],
|
|
["a non-array document", '{"items":[]}']
|
|
]) {
|
|
const run = withFifoReader(() =>
|
|
runPipeline({ agentBranch: true, runtimeDir: runtimeDir },
|
|
{ [Model.loadIdEnvVar()]: LOAD_ID }, contents))
|
|
check(`${label} fails the whole read`, run.result.status !== 0, `status ${run.result.status}`)
|
|
check(`${label} produces no item list`, run.result.stdout.trim() === "", run.result.stdout.slice(0, 200))
|
|
check(`${label} publishes no private key`, run.fifo.indexOf(PRIVATE_MARKER) < 0, "leaked")
|
|
check(`${label} publishes no complete payload`, (() => {
|
|
if (run.fifo.trim() === "") return true
|
|
try { JSON.parse(run.fifo); return false } catch (e) { return true }
|
|
})(), run.fifo.slice(0, 200))
|
|
}
|
|
|
|
// A `bw` that streams a complete, valid document and *then* exits nonzero is
|
|
// a different shape of failure: the branch has already forwarded well-formed
|
|
// bytes by the time the exit status exists, and no in-stream check could have
|
|
// known. The FIFO is deliberately not the boundary here -- `key_load_end` is.
|
|
// The companion holds every candidate unpublished until that line arrives, and
|
|
// discards it on `failed`, so what matters is that the panel reports failure.
|
|
{
|
|
const run = withFifoReader(() =>
|
|
runPipeline({ agentBranch: true, runtimeDir: runtimeDir },
|
|
{ [Model.loadIdEnvVar()]: LOAD_ID, QSBW_BW_EXIT: "1" }, JSON.stringify(fixture)))
|
|
check("a failing bw fails the whole read", run.result.status !== 0, `status ${run.result.status}`)
|
|
check("a failing bw produces no item list", run.result.stdout.trim() === "", run.result.stdout.slice(0, 200))
|
|
eq("a failed read is framed as a failed load", Model.sshAgentLoadEndLine(7, false),
|
|
JSON.stringify({ v: 1, type: "key_load_end", epoch: 7, status: "failed" }) + "\n")
|
|
const panelSrc = fs.readFileSync(path.join(repoRoot, "Panel.qml"), "utf8")
|
|
check("the panel closes every load window with the read's real outcome",
|
|
/endSshAgentLoad\(exitCode === 0\)/.test(panelSrc), "the exit handler does not close the load window")
|
|
check("the panel closes the window on paths that abandon a load",
|
|
/endSshAgentLoad\(false\)/.test(panelSrc), "no path reports a failed load")
|
|
check("closing the window writes the versioned key_load_end line",
|
|
/sshAgentProc\.write\(Model\.sshAgentLoadEndLine\(/.test(panelSrc),
|
|
"key_load_end is never sent to the helper")
|
|
// The cancel itself lives in the lock transition (see
|
|
// tests/ssh-agent-lifecycle.test.js); what matters here is that locking
|
|
// goes through it rather than leaving a fan-out read running.
|
|
check("a lock abandons the in-flight load",
|
|
/function lockVault\(\)[\s\S]{0,600}?applySshAgentLifecycle\("lock"\)/.test(panelSrc)
|
|
&& /function applySshAgentLifecycle\(event\)[\s\S]{0,900}?action\.cancelLoad\) cancelSshAgentLoad\(\)/.test(panelSrc),
|
|
"locking does not cancel the in-flight load")
|
|
check("a failed fan-out read is retried once without the branch",
|
|
/listRetriedWithoutAgent = true[\s\S]{0,200}?startVaultListRead\(true\)/.test(panelSrc),
|
|
"no retry without the agent branch")
|
|
}
|
|
|
|
// An ordinary item carrying an SSH subtree still rejects the whole read, with
|
|
// the agent branch present as well as without it.
|
|
{
|
|
const crossed = [{ object: "item", id: "x", type: 1, name: "Crossed",
|
|
login: { username: "u" }, sshKey: { privateKey: privatePem } }]
|
|
const run = withFifoReader(() =>
|
|
runPipeline({ agentBranch: true, runtimeDir: runtimeDir },
|
|
{ [Model.loadIdEnvVar()]: LOAD_ID }, JSON.stringify(crossed)))
|
|
check("a cross-typed item rejects the whole read", run.result.status !== 0, `status ${run.result.status}`)
|
|
check("a cross-typed item leaks no private key to QML",
|
|
run.result.stdout.indexOf(PRIVATE_MARKER) < 0, "leaked")
|
|
check("a cross-typed item leaks no private key to the FIFO",
|
|
run.fifo.indexOf(PRIVATE_MARKER) < 0, "leaked")
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Lock has to be able to stop the whole group
|
|
// -------------------------------------------------------------------------
|
|
|
|
check("the fan-out pipeline runs under process-group supervision",
|
|
agentCommandText.indexOf("set -m") >= 0 && agentCommandText.indexOf("kill -TERM") >= 0,
|
|
agentCommandText.slice(0, 300))
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Control lines that frame a load
|
|
// -------------------------------------------------------------------------
|
|
|
|
eq("key_load_begin is a versioned v1 line", Model.sshAgentLoadBeginLine(7, LOAD_ID),
|
|
JSON.stringify({ v: 1, type: "key_load_begin", epoch: 7, loadId: LOAD_ID }) + "\n")
|
|
eq("key_load_end reports success", Model.sshAgentLoadEndLine(7, true),
|
|
JSON.stringify({ v: 1, type: "key_load_end", epoch: 7, status: "ok" }) + "\n")
|
|
eq("key_load_end reports failure", Model.sshAgentLoadEndLine(7, false),
|
|
JSON.stringify({ v: 1, type: "key_load_end", epoch: 7, status: "failed" }) + "\n")
|
|
eq("vault_locked is a versioned v1 line", Model.sshAgentVaultLockedLine(7),
|
|
JSON.stringify({ v: 1, type: "vault_locked", epoch: 7 }) + "\n")
|
|
eq("vault_logged_out is a versioned v1 line", Model.sshAgentLoggedOutLine(),
|
|
JSON.stringify({ v: 1, type: "vault_logged_out" }) + "\n")
|
|
eq("an invalid nonce yields no begin line", Model.sshAgentLoadBeginLine(7, "nope"), "")
|
|
|
|
// No control line may ever carry key material or a session token.
|
|
for (const line of [Model.sshAgentLoadBeginLine(7, LOAD_ID), Model.sshAgentLoadEndLine(7, true),
|
|
Model.sshAgentVaultLockedLine(7), Model.sshAgentLoggedOutLine()]) {
|
|
check("no control line carries private material",
|
|
line.indexOf(PRIVATE_MARKER) < 0 && line.indexOf("BW_SESSION") < 0 && line.indexOf("privateKey") < 0, line)
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// The whole data plane, end to end
|
|
// -------------------------------------------------------------------------
|
|
//
|
|
// Everything above tests one boundary at a time. This runs the real thing:
|
|
// the real pipeline writes to the real companion's FIFO, the companion
|
|
// validates and publishes, and the real OpenSSH client lists the key back.
|
|
// It is the only test that would catch the projection and the decoder
|
|
// disagreeing about a field name, because both sides are real here.
|
|
const helperBin = path.join(repoRoot, "agent", "target", "debug", "qs-bitwarden-ssh-agent")
|
|
|
|
function endToEnd(done) {
|
|
if (!fs.existsSync(helperBin) || !fs.existsSync("/usr/bin/ssh-keygen")) {
|
|
failures.push("the end-to-end check needs the built helper and /usr/bin/ssh-keygen\n "
|
|
+ "build it with: cargo build --manifest-path agent/Cargo.toml --locked")
|
|
return done()
|
|
}
|
|
|
|
const e2eDir = fs.mkdtempSync(path.join(os.tmpdir(), "qsbw-e2e-"))
|
|
const e2eRuntime = path.join(e2eDir, "run")
|
|
fs.mkdirSync(e2eRuntime, { mode: 0o700 })
|
|
const e2eLoadId = "aaaabbbbccccddddeeeeffff00001111"
|
|
|
|
// A disposable key that exists only for the life of this test.
|
|
const keyPath = path.join(e2eDir, "id_ed25519")
|
|
execFileSync("/usr/bin/ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", "e2e", "-f", keyPath])
|
|
const priv = fs.readFileSync(keyPath, "utf8")
|
|
const pub = fs.readFileSync(keyPath + ".pub", "utf8").trim()
|
|
const fingerprint = execFileSync("/usr/bin/ssh-keygen", ["-lf", keyPath + ".pub"],
|
|
{ encoding: "utf8" }).split(" ")[1]
|
|
|
|
const e2eFixture = path.join(e2eDir, "items.json")
|
|
fs.writeFileSync(e2eFixture, JSON.stringify([
|
|
{ object: "item", id: "login-1", type: 1, name: "Login", login: { username: "u" } },
|
|
{ object: "item", id: "ssh-1", type: 5, name: "Disposable", reprompt: 0,
|
|
sshKey: { privateKey: priv, publicKey: pub, fingerprint: fingerprint } }
|
|
]))
|
|
fs.writeFileSync(path.join(e2eDir, "bw"), [
|
|
"#!/usr/bin/env bash",
|
|
'if [ "$1" = "--version" ]; then echo 2026.2.0; exit 0; fi',
|
|
'cat -- "$QSBW_FIXTURE"', ""
|
|
].join("\n"), { mode: 0o755 })
|
|
|
|
const { spawn } = require("child_process")
|
|
const helper = spawn(helperBin, [], { env: { XDG_RUNTIME_DIR: e2eRuntime }, stdio: ["pipe", "pipe", "pipe"] })
|
|
let buffered = ""
|
|
let socketPath = ""
|
|
let settled = false
|
|
const finish = () => {
|
|
if (settled) return
|
|
settled = true
|
|
clearTimeout(guard)
|
|
try { helper.kill("SIGKILL") } catch (e) {}
|
|
try { fs.rmSync(e2eDir, { recursive: true, force: true }) } catch (e) {}
|
|
done()
|
|
}
|
|
const guard = setTimeout(() => {
|
|
failures.push("the end-to-end load timed out\n the companion never reported keys_loaded")
|
|
finish()
|
|
}, 30000)
|
|
|
|
helper.stdin.write(Model.sshAgentHelloLine())
|
|
helper.stdout.on("data", chunk => {
|
|
buffered += chunk.toString("utf8")
|
|
let nl
|
|
while ((nl = buffered.indexOf("\n")) >= 0) {
|
|
const line = buffered.slice(0, nl)
|
|
buffered = buffered.slice(nl + 1)
|
|
let message
|
|
try { message = JSON.parse(line) } catch (e) {
|
|
failures.push("the companion emitted an unparseable line\n " + line)
|
|
return finish()
|
|
}
|
|
|
|
if (message.type === "ready") {
|
|
socketPath = message.socketPath
|
|
// Arm the window before anything can write to the FIFO.
|
|
helper.stdin.write(Model.sshAgentLoadBeginLine(1, e2eLoadId))
|
|
const command = Model.sanitizedListCommand({ agentBranch: true })
|
|
const run = spawnSync(command[0], command.slice(1), {
|
|
env: { PATH: e2eDir + path.delimiter + process.env.PATH, QSBW_FIXTURE: e2eFixture,
|
|
XDG_RUNTIME_DIR: e2eRuntime, [Model.loadIdEnvVar()]: e2eLoadId },
|
|
encoding: "utf8", maxBuffer: 20 * 1024 * 1024
|
|
})
|
|
eq("the end-to-end pipeline succeeds", run.status, 0)
|
|
check("the end-to-end read still renders the panel envelope",
|
|
run.status === 0 && JSON.parse(run.stdout).sshKeys.length === 1, String(run.stdout).slice(0, 200))
|
|
check("no private key reaches QML in the end-to-end read",
|
|
String(run.stdout).indexOf("PRIVATE KEY") < 0, "leaked")
|
|
if (run.status !== 0) return finish()
|
|
helper.stdin.write(Model.sshAgentLoadEndLine(1, true))
|
|
}
|
|
|
|
if (message.type === "keys_loaded") {
|
|
eq("the companion published exactly the eligible key", message.keyCount, 1)
|
|
eq("the companion published it for the load's epoch", message.epoch, 1)
|
|
const listed = spawnSync("/usr/bin/ssh-add", ["-L"], {
|
|
env: { SSH_AUTH_SOCK: socketPath, PATH: "/usr/bin", HOME: os.homedir() }, encoding: "utf8"
|
|
})
|
|
eq("a real OpenSSH client lists the loaded identity", listed.status, 0)
|
|
eq("the agent offers exactly the key the vault held",
|
|
String(listed.stdout).trim().split(" ").slice(0, 2).join(" "),
|
|
pub.split(" ").slice(0, 2).join(" "))
|
|
return finish()
|
|
}
|
|
}
|
|
})
|
|
helper.on("error", () => {
|
|
failures.push("the companion could not be started\n " + helperBin)
|
|
finish()
|
|
})
|
|
helper.on("exit", code => {
|
|
if (settled) return
|
|
failures.push("the companion exited before publishing\n exit " + code)
|
|
finish()
|
|
})
|
|
}
|
|
|
|
endToEnd(() => {
|
|
try { fs.rmSync(tempDir, { recursive: true, force: true }) } catch (e) {}
|
|
if (failures.length) {
|
|
console.error(`\n${failures.length} failed, ${pass} passed\n`)
|
|
failures.forEach(f => console.error(` FAIL ${f}`))
|
|
process.exit(1)
|
|
}
|
|
console.log(`ssh-agent-pipeline: ${pass} passed`)
|
|
})
|