汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
146 行
7.2 KiB
JavaScript
146 行
7.2 KiB
JavaScript
const { randomBytes, createHash } = require("node:crypto");
|
|
const { spawnSync } = require("node:child_process");
|
|
const path = require("node:path");
|
|
const cryptoService = require("../../../src/services/crypto");
|
|
const {
|
|
checkSyntheticSample,
|
|
runSyntheticContract,
|
|
} = require("../../../../scripts/qa/check-sensitive-recovery-contract.cjs");
|
|
|
|
describe("K0 synthetic sensitive recovery contract", () => {
|
|
let config;
|
|
let plaintext;
|
|
let encrypted;
|
|
|
|
beforeEach(() => {
|
|
config = { fieldEncryptionKey: randomBytes(32).toString("base64") };
|
|
plaintext = `synthetic-only-${randomBytes(16).toString("hex")}`;
|
|
encrypted = cryptoService.encryptStringOrThrow(config, plaintext);
|
|
});
|
|
|
|
// Boolean assertions keep failed tests from printing fixture values/digests.
|
|
function expectNoFixture(output, additional = []) {
|
|
const key = Buffer.from(config.fieldEncryptionKey, "base64");
|
|
const forbidden = [
|
|
plaintext, encrypted, config.fieldEncryptionKey, key.toString("hex"),
|
|
createHash("sha256").update(key).digest("hex"),
|
|
createHash("sha256").update(config.fieldEncryptionKey).digest("hex"),
|
|
...encrypted.split(":").slice(1), ...additional,
|
|
];
|
|
expect(forbidden.some((value) => output.includes(value))).toBe(false);
|
|
}
|
|
|
|
it("round-trips with the actual production crypto exports and only reports status", () => {
|
|
const report = checkSyntheticSample(config, encrypted, plaintext);
|
|
expect(report).toEqual({ status: "PASS", reason: "SYNTHETIC_MATCH" });
|
|
expectNoFixture(JSON.stringify(report));
|
|
});
|
|
|
|
it.each([
|
|
["no config", undefined], ["no key", {}],
|
|
["null", { fieldEncryptionKey: null }], ["empty", { fieldEncryptionKey: "" }],
|
|
["whitespace", { fieldEncryptionKey: " " }],
|
|
["wrong type", { fieldEncryptionKey: 32 }],
|
|
["unparseable", { fieldEncryptionKey: "%%%" }],
|
|
["short", { fieldEncryptionKey: Buffer.alloc(31).toString("base64") }],
|
|
["long", { fieldEncryptionKey: Buffer.alloc(33).toString("base64") }],
|
|
])("stops for an unavailable or invalid key: %s", (_label, candidate) => {
|
|
expect(checkSyntheticSample(candidate, encrypted, plaintext)).toEqual({
|
|
status: "STOP", reason: "KEY_UNAVAILABLE_OR_INVALID",
|
|
});
|
|
});
|
|
|
|
it("stops for a different but valid 32-byte key", () => {
|
|
const wrongKey = Buffer.from(config.fieldEncryptionKey, "base64");
|
|
wrongKey[0] ^= 1;
|
|
const report = checkSyntheticSample({ fieldEncryptionKey: wrongKey.toString("base64") }, encrypted, plaintext);
|
|
expect(report).toEqual({ status: "STOP", reason: "DECRYPTION_FAILED" });
|
|
expectNoFixture(JSON.stringify(report), [wrongKey.toString("base64")]);
|
|
});
|
|
|
|
it.each([["iv", 1], ["tag", 2], ["ciphertext", 3]])("stops after tampering with %s", (_label, index) => {
|
|
const parts = encrypted.split(":");
|
|
const value = Buffer.from(parts[index], "base64url");
|
|
value[0] ^= 1;
|
|
parts[index] = value.toString("base64url");
|
|
const tampered = parts.join(":");
|
|
const report = checkSyntheticSample(config, tampered, plaintext);
|
|
expect(report).toEqual({ status: "STOP", reason: "DECRYPTION_FAILED" });
|
|
expectNoFixture(JSON.stringify(report), [tampered]);
|
|
});
|
|
|
|
it.each([["empty", ""], ["null", null], ["absent", undefined]])("marks %s fields NOT_TESTED", (_label, value) => {
|
|
expect(checkSyntheticSample(config, value, plaintext)).toEqual({ status: "NOT_TESTED", reason: "EMPTY_FIELD" });
|
|
});
|
|
|
|
it("does not let an empty field hide a missing key", () => {
|
|
expect(checkSyntheticSample({}, "", plaintext)).toEqual({ status: "STOP", reason: "KEY_UNAVAILABLE_OR_INVALID" });
|
|
});
|
|
|
|
it.each([
|
|
["legacy plaintext", "synthetic-unencrypted"], ["unknown version", "v2:a:b:c"],
|
|
["missing parts", "v1:a:b"], ["extra parts", "v1:a:b:c:d"],
|
|
["empty parts", "v1:::"], ["invalid encoding", "v1:%%:%%:%%"],
|
|
["wrong type", 123],
|
|
])("stops instead of treating a null decryption as success: %s", (_label, payload) => {
|
|
expect(checkSyntheticSample(config, payload, plaintext)).toEqual({ status: "STOP", reason: "INVALID_PAYLOAD" });
|
|
});
|
|
|
|
it("requires the synthetic expectation and detects a plaintext mismatch", () => {
|
|
expect(checkSyntheticSample(config, encrypted)).toEqual({ status: "STOP", reason: "SYNTHETIC_EXPECTATION_REQUIRED" });
|
|
const report = checkSyntheticSample(config, encrypted, "different-synthetic-value");
|
|
expect(report).toEqual({ status: "STOP", reason: "PLAINTEXT_MISMATCH" });
|
|
expectNoFixture(JSON.stringify(report));
|
|
});
|
|
|
|
it("drops raw exceptions, does not log, and never uses the logging decrypt branch", () => {
|
|
const marker = `synthetic-error-${randomBytes(8).toString("hex")}`;
|
|
const spies = ["log", "warn", "error", "info", "debug"].map((name) => vi.spyOn(console, name).mockImplementation(() => {}));
|
|
const safe = vi.spyOn(cryptoService, "decryptStringSafe").mockImplementation(() => { throw new Error(marker); });
|
|
try {
|
|
const throwingConfig = { get fieldEncryptionKey() { throw new Error(marker); } };
|
|
const report = checkSyntheticSample(throwingConfig, encrypted, plaintext);
|
|
expect(report).toEqual({ status: "STOP", reason: "DECRYPTION_FAILED" });
|
|
expectNoFixture(JSON.stringify(report), [marker]);
|
|
expect(runSyntheticContract().status).toBe("PASS");
|
|
expect(safe).not.toHaveBeenCalled();
|
|
expect(spies.every((spy) => spy.mock.calls.length === 0)).toBe(true);
|
|
} finally {
|
|
safe.mockRestore();
|
|
spies.forEach((spy) => spy.mockRestore());
|
|
}
|
|
});
|
|
|
|
it("keeps real custody and real-data decryption untested after the synthetic matrix passes", () => {
|
|
const report = runSyntheticContract();
|
|
expect(report.status).toBe("PASS");
|
|
expect(report.checks).toHaveLength(7);
|
|
expect(report.checks.every((check) => check.matched)).toBe(true);
|
|
expect(report).toMatchObject({ scope: "K0_SYNTHETIC_ONLY", originalKeySource: "UNKNOWN", custody: "NOT_TESTED", realDataDecryption: "NOT_TESTED" });
|
|
});
|
|
|
|
it("CLI ignores ambient key inputs and produces only the fixed aggregate contract", () => {
|
|
const script = path.resolve(__dirname, "../../../../scripts/qa/check-sensitive-recovery-contract.cjs");
|
|
const child = spawnSync(process.execPath, [script], {
|
|
encoding: "utf8",
|
|
env: { FIELD_ENCRYPTION_KEY: config.fieldEncryptionKey, DATABASE_URL: "synthetic-unused" },
|
|
});
|
|
expect(child.status).toBe(0);
|
|
expect(child.stderr.length).toBe(0);
|
|
expectNoFixture(child.stdout + child.stderr);
|
|
const report = JSON.parse(child.stdout);
|
|
expect(Object.keys(report).sort()).toEqual(["checks", "custody", "originalKeySource", "realDataDecryption", "scope", "status"]);
|
|
expect(report.status).toBe("PASS");
|
|
expect(report.checks.every((check) => Object.keys(check).sort().join(",") === "category,matched,reason,status")).toBe(true);
|
|
});
|
|
|
|
it("CLI rejects arbitrary arguments without echoing them", () => {
|
|
const script = path.resolve(__dirname, "../../../../scripts/qa/check-sensitive-recovery-contract.cjs");
|
|
const child = spawnSync(process.execPath, [script, plaintext], { encoding: "utf8", env: {} });
|
|
expect(child.status).toBe(1);
|
|
expect(child.stderr.length).toBe(0);
|
|
expectNoFixture(child.stdout + child.stderr);
|
|
expect(JSON.parse(child.stdout)).toEqual({ scope: "K0_SYNTHETIC_ONLY", status: "FAIL", reason: "ARGUMENTS_NOT_ALLOWED" });
|
|
});
|
|
});
|