420542ce10
The relay control password (relay_pw) was read by the driver but had NO form field, so Test connection sent it as 0 → the device ignored the probe → a controller showed "offline" even though it pinged. Add a "Relay control password" config field (secret; blank keeps the stored value). Because relayPassword is redacted from the client, the edit form can't resend it — so the test endpoint now re-merges the stored secret by device id (mirroring save). It is re-merged ONLY when the submitted config addresses the SAME device: matching driverId and every connection-identity field it sets (host/port/binaryPort/httpPort/serial). A redirected host/port or mismatched driver yields NO secret, so a probe can't exfiltrate the password to an attacker host (the booth operator is the threat-model adversary). testDevice() now passes the device id; setup-secrets.test.ts covers the identity guard. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { randomUUID } from "node:crypto";
|
|
import { devices, type Db } from "@parking/db";
|
|
import { createTestDb } from "@parking/db/testing";
|
|
import { storedSecrets } from "./setup.js";
|
|
|
|
// storedSecrets re-merges a device's machine-only secrets (relayPassword/pushPassword)
|
|
// into a test/save — but ONLY when the submitted config addresses the SAME device at the
|
|
// SAME host/port. This guards against a redirected probe exfiltrating the secret to an
|
|
// attacker host (an admin keeps a real device id but swaps the host). The booth operator
|
|
// is the threat-model adversary, so an authenticated-admin redirect must NOT leak.
|
|
|
|
let db: Db;
|
|
const ID = "ctl-secret";
|
|
const HOST = "10.0.10.5";
|
|
|
|
beforeEach(() => {
|
|
({ db } = createTestDb());
|
|
db.insert(devices).values({
|
|
id: ID,
|
|
category: "access",
|
|
driverId: "dingtian",
|
|
config: { host: HOST, binaryPort: 60000, relayPassword: 1996, pushPassword: "p-secret" },
|
|
enabled: true,
|
|
}).run();
|
|
});
|
|
|
|
describe("storedSecrets identity guard", () => {
|
|
it("re-merges secrets when host/port/driver match the stored device", () => {
|
|
const out = storedSecrets(db, ID, "dingtian", { host: HOST, binaryPort: 60000 });
|
|
expect(out.relayPassword).toBe(1996);
|
|
expect(out.pushPassword).toBe("p-secret");
|
|
});
|
|
|
|
it("re-merges when identity fields are OMITTED (fall back to the stored device)", () => {
|
|
const out = storedSecrets(db, ID, "dingtian", {});
|
|
expect(out.relayPassword).toBe(1996);
|
|
});
|
|
|
|
it("REFUSES secrets when the host is redirected (exfiltration attempt)", () => {
|
|
const out = storedSecrets(db, ID, "dingtian", { host: "10.66.66.66", binaryPort: 60000 });
|
|
expect(out).toEqual({});
|
|
});
|
|
|
|
it("REFUSES secrets when a control port is changed", () => {
|
|
const out = storedSecrets(db, ID, "dingtian", { host: HOST, binaryPort: 9999 });
|
|
expect(out).toEqual({});
|
|
});
|
|
|
|
it("REFUSES secrets when the driver doesn't match the stored row", () => {
|
|
const out = storedSecrets(db, ID, "stub-access", { host: HOST });
|
|
expect(out).toEqual({});
|
|
});
|
|
|
|
it("returns nothing for an unknown device id", () => {
|
|
expect(storedSecrets(db, randomUUID(), "dingtian", { host: HOST })).toEqual({});
|
|
});
|
|
});
|