CH-J Server ManagerSerververwaltung über SSH
Menü
Veröffentlichte Quellen

CH-J Server Manager

Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.

Quellen als ZIP herunterladen
CH-J Proprietary Software License 1.14

Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.

14,9 KB · 196 ZeilenDatei herunterladen
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const fs = require("node:fs");
6const os = require("node:os");
7const path = require("node:path");
8const vectors = require("./fixtures/hash-vectors.json");
9const { LocalHashService } = require("../src/main/hashing/localHashService");
10const { ALGORITHMS } = require("../src/main/hashing/hashAlgorithms");
12const SIP_KEY = "000102030405060708090a0b0c0d0e0f";
13const HIGHWAY_KEY = Array.from({ length: 32 }, (_unused, index) => index.toString(16).padStart(2, "0")).join("");
15function requests() {
16 return ALGORITHMS.map((algorithm) => ({
17 id: algorithm.id,
18 ...(algorithm.id === "siphash-2-4" ? { keyHex: SIP_KEY } : {}),
19 ...(algorithm.id.startsWith("highwayhash") ? { keyHex: HIGHWAY_KEY } : {})
20 }));
23function fixture(t, files) {
24 const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-local-hash-"));
25 t.after(() => fs.rmSync(root, { recursive: true, force: true }));
26 const paths = {};
27 for (const [name, value] of Object.entries(files)) { paths[name] = path.join(root, name); fs.mkdirSync(path.dirname(paths[name]), { recursive: true }); fs.writeFileSync(paths[name], value); }
28 return { root, paths };
31function serviceFor(filePaths, chunkSize = 1024 * 1024, extras = {}) {
32 return new LocalHashService({
33 chunkSize,
34 selectFilesDialog: async () => ({ canceled: false, paths: filePaths }),
35 selectDirectoryDialog: async () => ({ canceled: false, path: extras.directory || path.dirname(filePaths[0]) }),
36 selectManifestDialog: async () => ({ canceled: false, path: extras.manifest || filePaths[0] }),
37 selectSaveDialog: async () => ({ canceled: false, path: extras.destination || path.join(path.dirname(filePaths[0]), "output.txt") }),
38 writeClipboard: extras.writeClipboard || (() => {})
39 });
42async function completed(service, pluginId, start) {
43 let job = start;
44 for (let attempt = 0; attempt < 3000 && ["queued", "running"].includes(job.state); attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 5)); job = service.status(pluginId, job.jobId); }
45 assert.equal(job.state, "completed", JSON.stringify(job.error));
46 return job;
49async function hashFile(service, pluginId = "chj.hash-checksum") {
50 const selection = await service.selectFiles(pluginId, { multiple: false });
51 const job = await completed(service, pluginId, service.start(pluginId, { selectionId: selection.selectionId, algorithms: requests() }));
52 assert.equal(job.results[0].status, "COMPLETED", JSON.stringify(job.results[0].error));
53 return Object.fromEntries(job.results[0].hashes.map((hash) => [hash.id, hash.hex]));
56test("algorithm registry contains every specified exact variant and portable metadata", () => {
57 assert.equal(ALGORITHMS.length, 49);
58 assert.deepEqual(new Set(ALGORITHMS.map((algorithm) => algorithm.id)), new Set(Object.keys(vectors.vectors.abc)));
59 assert.equal(ALGORITHMS.find((item) => item.id === "crc16-ccitt-false").parameters, "poly=0x1021 init=0xffff refin=false refout=false xorout=0x0000");
60 assert.deepEqual(ALGORITHMS.find((item) => item.id === "shake128").xof, { defaultBytes: 32, minBytes: 16, maxBytes: 1024 });
61 assert.deepEqual(ALGORITHMS.find((item) => item.id === "shake256").xof, { defaultBytes: 64, minBytes: 16, maxBytes: 1024 });
62 assert.deepEqual(ALGORITHMS.find((item) => item.id === "kangaroo-twelve").xof, { defaultBytes: 32, minBytes: 16, maxBytes: 1024 });
63 for (const id of ["sha512-224", "sha3-224", "sha3-256", "sha3-384", "sha3-512", "shake128", "shake256", "blake2b-512", "blake2s-256", "kangaroo-twelve"]) assert.equal(ALGORITHMS.find((item) => item.id === id).backend, "noble-hashes", id);
64});
66for (const name of ["empty", "abc", "quick"]) {
67 test(`all algorithms match pinned reference vectors for ${name}`, async (t) => {
68 const { paths } = fixture(t, { vector: vectors.messages[name] });
69 assert.deepEqual(await hashFile(serviceFor([paths.vector], 7)), vectors.vectors[name]);
70 });
73test("all algorithms are invariant across 1 B, 7 B, 64 B, 4 KiB, 64 KiB, and 1 MiB chunks", async (t) => {
74 const data = Buffer.alloc(8193); for (let index = 0; index < data.length; index += 1) data[index] = (index * 131 + 17) & 0xff;
75 const { paths } = fixture(t, { "stream.bin": data });
76 const baseline = await hashFile(serviceFor([paths["stream.bin"]], 1024 * 1024));
77 for (const chunkSize of [1, 7, 64, 4096, 65536, 1024 * 1024]) assert.deepEqual(await hashFile(serviceFor([paths["stream.bin"]], chunkSize)), baseline, `chunk size ${chunkSize}`);
78});
80test("selection tokens are opaque, plugin-owned, and arbitrary renderer paths are ignored", async (t) => {
81 const { paths } = fixture(t, { "allowed.txt": "allowed", "secret.txt": "secret" });
82 const service = serviceFor([paths["allowed.txt"]]); const selection = await service.selectFiles("owner", { multiple: false });
83 assert.equal(JSON.stringify(selection).includes(paths["allowed.txt"]), false);
84 assert.throws(() => service.start("attacker", { selectionId: selection.selectionId, algorithms: ["sha256"] }), { code: "HASH_INVALID_SELECTION_TOKEN" });
85 assert.throws(() => service.start("owner", { selectionId: "missing", path: paths["secret.txt"], algorithms: ["sha256"] }), { code: "HASH_INVALID_SELECTION_TOKEN" });
86 const job = await completed(service, "owner", service.start("owner", { selectionId: selection.selectionId, path: paths["secret.txt"], algorithms: ["sha256"] }));
87 assert.equal(job.results[0].file.name, "allowed.txt");
88 assert.throws(() => service.status("attacker", job.jobId), { code: "HASH_INVALID_JOB" });
89 assert.throws(() => service.status("owner", "unknown-job"), { code: "HASH_INVALID_JOB" });
90});
92test("a file changed after selection is rejected by its stable file identity", async (t) => {
93 const { paths } = fixture(t, { file: "abc" }); const service = serviceFor([paths.file]); const selection = await service.selectFiles("p");
94 fs.writeFileSync(paths.file, "xyz"); fs.utimesSync(paths.file, new Date(), new Date(Date.now() + 2000));
95 const job = await completed(service, "p", service.start("p", { selectionId: selection.selectionId, algorithms: ["sha256"] }));
96 assert.equal(job.results[0].status, "ERROR"); assert.equal(job.results[0].error.code, "HASH_FILE_CHANGED");
97});
99test("verification reports MATCH and MISMATCH without accepting malformed expected hashes", async (t) => {
100 const { paths } = fixture(t, { file: "abc" }); const service = serviceFor([paths.file]); const selection = await service.selectFiles("p");
101 let job = await completed(service, "p", service.verify("p", { selectionId: selection.selectionId, algorithm: "sha256", expected: vectors.vectors.abc.sha256.toUpperCase() }));
102 assert.equal(job.results[0].status, "MATCH");
103 job = await completed(service, "p", service.verify("p", { selectionId: selection.selectionId, algorithm: "sha256", expected: "00".repeat(32) }));
104 assert.equal(job.results[0].status, "MISMATCH");
105 assert.throws(() => service.verify("p", { selectionId: selection.selectionId, algorithm: "sha256", expected: "not-a-hash" }), { code: "HASH_INVALID_EXPECTED" });
106});
108test("directory enumeration is bounded to the selected root and never follows symlinks", async (t) => {
109 const { root, paths } = fixture(t, { "tree/a.txt": "a", "tree/sub/b.txt": "b", "outside.txt": "outside" });
110 try { fs.symlinkSync(paths["outside.txt"], path.join(root, "tree", "escape.txt")); } catch (error) { if (process.platform !== "win32") throw error; }
111 const service = serviceFor([paths["tree/a.txt"]], 64, { directory: path.join(root, "tree") }); const directory = await service.selectDirectory("p");
112 const job = await completed(service, "p", service.start("p", { selectionId: directory.selectionId, algorithms: ["sha256"], recursive: true }));
113 assert.deepEqual(job.results.map((result) => result.file.relativePath), ["a.txt", "sub/b.txt"]);
114});
116test("manifest generation and verification support GNU and reject traversal and symlink escapes", async (t) => {
117 const { root, paths } = fixture(t, { "root/a file.txt": "abc", "root/sub/žluťoučký.txt": "unicode", "outside": "secret" });
118 const manifest = path.join(root, "checksums.sha256"); const service = serviceFor([paths["root/a file.txt"], paths["root/sub/žluťoučký.txt"]], 7, { directory: path.join(root, "root"), destination: manifest, manifest });
119 const source = await service.selectFiles("p", { multiple: true }); const destination = await service.selectManifestDestination("p", { suggestedName: "checksums.sha256" });
120 await completed(service, "p", service.generateManifest("p", { selectionId: source.selectionId, destinationSelectionId: destination.selectionId, algorithm: "sha256", format: "gnu" }));
121 const generated = fs.readFileSync(manifest, "utf8"); assert.match(generated, /# Algorithm: sha256/); assert.match(generated, /a file\.txt/); assert.match(generated, /žluťoučký\.txt/);
122 const manifestToken = await service.selectManifest("p"); const rootToken = await service.selectDirectory("p");
123 const verified = await completed(service, "p", service.verifyManifest("p", { manifestSelectionId: manifestToken.selectionId, rootSelectionId: rootToken.selectionId }));
124 assert.deepEqual(verified.results.map((result) => result.status), ["MATCH", "MATCH"]);
125 fs.writeFileSync(manifest, `${vectors.vectors.abc.sha256} ../outside\n`);
126 const traversalToken = await service.selectManifest("p"); const traversal = await completed(service, "p", service.verifyManifest("p", { manifestSelectionId: traversalToken.selectionId, rootSelectionId: rootToken.selectionId, algorithmId: "sha256" }));
127 assert.equal(traversal.results[0].status, "INVALID ENTRY"); assert.equal(traversal.results[0].error.code, "HASH_PATH_ESCAPE");
128});
130test("BSD and SFV manifests round-trip spaces and Unicode filenames", async (t) => {
131 const { root, paths } = fixture(t, { "root/a file.txt": "abc", "root/žluťoučký.txt": "unicode" });
132 for (const [format, algorithm, filename] of [["bsd", "fnv1a-64", "checksums.txt"], ["sfv", "crc32", "checksums.sfv"]]) {
133 const destinationPath = path.join(root, filename);
134 const service = serviceFor([paths["root/a file.txt"], paths["root/žluťoučký.txt"]], 7, { directory: path.join(root, "root"), destination: destinationPath, manifest: destinationPath });
135 const source = await service.selectFiles("p", { multiple: true }); const destination = await service.selectManifestDestination("p", { suggestedName: filename });
136 await completed(service, "p", service.generateManifest("p", { selectionId: source.selectionId, destinationSelectionId: destination.selectionId, algorithm, format }));
137 const manifest = await service.selectManifest("p"); const selectedRoot = await service.selectDirectory("p");
138 const verified = await completed(service, "p", service.verifyManifest("p", { manifestSelectionId: manifest.selectionId, rootSelectionId: selectedRoot.selectionId }));
139 assert.deepEqual(verified.results.map((result) => result.status), ["MATCH", "MATCH"], format);
140 }
141});
143test("manifest verification rejects absolute, drive, UNC, traversal, and symlink paths", async (t) => {
144 const { root, paths } = fixture(t, { "root/safe.txt": "safe", "outside/secret.txt": "secret", manifest: "" });
145 let symlinkCreated = false;
146 try { fs.symlinkSync(path.join(root, "outside"), path.join(root, "root", "link"), "dir"); symlinkCreated = true; } catch (error) { if (process.platform !== "win32") throw error; }
147 const digest = vectors.vectors.abc.sha256;
148 const entries = ["../outside/secret.txt", "/etc/passwd", "C:\\Windows\\system.ini", "\\\\server\\share\\file", ...(symlinkCreated ? ["link/secret.txt"] : [])];
149 fs.writeFileSync(paths.manifest, entries.map((entry) => `${digest} ${entry}`).join("\n"));
150 const service = serviceFor([paths.manifest], 7, { directory: path.join(root, "root"), manifest: paths.manifest });
151 const manifest = await service.selectManifest("p"); const selectedRoot = await service.selectDirectory("p");
152 const job = await completed(service, "p", service.verifyManifest("p", { manifestSelectionId: manifest.selectionId, rootSelectionId: selectedRoot.selectionId, algorithmId: "sha256" }));
153 assert.equal(job.results.length, entries.length); assert.ok(job.results.every((result) => result.status === "INVALID ENTRY"));
154 assert.ok(job.results.slice(0, 4).every((result) => result.error.code === "HASH_PATH_ESCAPE"));
155 if (symlinkCreated) assert.equal(job.results.at(-1).error.code, "HASH_SYMLINK_REJECTED");
156});
158test("cancel stops an active worker and cleanup removes plugin authorization", async (t) => {
159 const { paths } = fixture(t, { large: Buffer.alloc(16 * 1024 * 1024, 0x5a) }); const service = serviceFor([paths.large], 4096); const selection = await service.selectFiles("p");
160 const started = service.start("p", { selectionId: selection.selectionId, algorithms: requests() }); const cancelled = await service.cancel("p", started.jobId); assert.equal(cancelled.state, "cancelled");
161 service.cleanupPlugin("p");
162 assert.throws(() => service.start("p", { selectionId: selection.selectionId, algorithms: ["sha256"] }), { code: "HASH_INVALID_SELECTION_TOKEN" });
163 assert.throws(() => service.status("p", started.jobId), { code: "HASH_INVALID_JOB" });
165 const other = serviceFor([paths.large], 4096); const otherSelection = await other.selectFiles("p"); const active = other.start("p", { selectionId: otherSelection.selectionId, algorithms: requests() });
166 other.cleanupPlugin("p"); await new Promise((resolve) => setTimeout(resolve, 20));
167 assert.throws(() => other.status("p", active.jobId), { code: "HASH_INVALID_JOB" });
168});
170test("all 49 algorithms export uppercase HEX and Base64 without altering digest bytes", async (t) => {
171 const { paths } = fixture(t, { file: "abc" });
172 const service = serviceFor([paths.file], 7);
173 const selection = await service.selectFiles("p");
174 for (const output of ["hex-upper", "base64"]) {
175 const job = await completed(service, "p", service.start("p", { selectionId: selection.selectionId, algorithms: requests(), output }));
176 assert.equal(job.results[0].hashes.length, 49);
177 for (const hash of job.results[0].hashes) {
178 const reference = vectors.vectors.abc[hash.id];
179 assert.equal(hash.hex, reference, hash.id);
180 assert.equal(hash.value, output === "base64" ? Buffer.from(reference, "hex").toString("base64") : reference.toUpperCase(), hash.id);
181 }
182 }
183});
185test("comparison distinguishes matching and different files using all 49 algorithms", async (t) => {
186 const { paths } = fixture(t, { a: "abc", b: "abc", c: "different" });
187 const service = serviceFor([paths.a], 7);
188 const left = await service.selectFiles("p");
189 for (const [file, identical] of [[paths.b, true], [paths.c, false]]) {
190 service.selectFilesDialog = async () => ({ canceled: false, paths: [file] });
191 const right = await service.selectFiles("p");
192 const job = await completed(service, "p", service.compare("p", { leftSelectionId: left.selectionId, rightSelectionId: right.selectionId, algorithms: requests() }));
193 assert.equal(job.comparison.identical, identical);
194 assert.equal(job.results.every((result) => result.status === "COMPLETED" && result.hashes.length === 49), true);
195 }
196});

SHA-256: 655abac6a9f2f9a31b45ad2e1d34b101b2017c1db65f8ef270cc07029f75c711

SHA-256 des Archivs: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0