[WIP] Security work While on holiday

This commit is contained in:
Alex 2026-08-12 22:45:28 +02:00
commit 7f0231e3f1
Signed by: alex
SSH key fingerprint: SHA256:D1+Ub8o0v4K5y1JNivW8IxEOelqLSvPmUzBbDIoZkRQ
109 changed files with 19694 additions and 5210 deletions

File diff suppressed because it is too large Load diff

95
test/encrypted-secret.mjs Normal file
View file

@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
import { InMemoryEncryptedSecretProvider } from "../dist/sdk/encrypted-secret.js";
function record(id, bytes, updatedAt = 2) {
return {
id,
encryptedSecret: new Uint8Array(bytes),
formatVersion: 1,
wrappingScheme: "test-wrap-v1",
wrappingKeyId: "wrapping-key-1",
createdAt: 1,
updatedAt,
};
}
test("encrypted secret provider stores, reads, replaces, and deletes", async () => {
const provider = new InMemoryEncryptedSecretProvider();
const initial = record("session-secret:alpha", [1, 2, 3]);
await provider.set(initial);
initial.encryptedSecret[0] = 99;
assert.deepEqual(await provider.get("session-secret:alpha"), record(
"session-secret:alpha",
[1, 2, 3],
));
const replacement = record("session-secret:alpha", [4, 5], 3);
await provider.set(replacement);
assert.deepEqual(
await provider.get("session-secret:alpha"),
replacement,
);
const fetched = await provider.get("session-secret:alpha");
fetched.encryptedSecret[0] = 88;
assert.deepEqual(
await provider.get("session-secret:alpha"),
replacement,
);
await provider.delete("session-secret:alpha");
assert.equal(await provider.get("session-secret:alpha"), null);
await provider.delete("session-secret:missing");
});
test("encrypted secret provider isolates unrelated opaque IDs", async () => {
const provider = new InMemoryEncryptedSecretProvider();
const session = record("session-secret:one", [1]);
const pipe = record("pipe-secret:one", [2]);
await provider.set(session);
await provider.set(pipe);
assert.deepEqual(await provider.get(session.id), session);
assert.deepEqual(await provider.get(pipe.id), pipe);
assert.equal(await provider.get("identity-secret:one"), null);
});
test("encrypted secret provider has no application identity hierarchy", async () => {
const source = await readFile(
new URL("../src/sdk/encrypted-secret.ts", import.meta.url),
"utf8",
);
const forbiddenFields = [
["user", "Id"],
["device", "Id"],
["chat", "Id"],
["conversation", "Id"],
].map(([prefix, suffix]) => `${prefix}${suffix}`);
for (const field of forbiddenFields) {
assert.doesNotMatch(source, new RegExp(`\\b${field}\\b`));
}
});
test("encrypted secret provider validates IDs and records", async () => {
const provider = new InMemoryEncryptedSecretProvider();
await assert.rejects(() => provider.get(""), /non-empty string/);
await assert.rejects(() => provider.delete(""), /non-empty string/);
await assert.rejects(
() => provider.set(record("", [1])),
/non-empty string/,
);
await assert.rejects(
() => provider.set({
...record("invalid", [1]),
encryptedSecret: new Uint8Array(),
}),
/non-empty encryptedSecret bytes/,
);
});

View file

@ -0,0 +1,78 @@
import type {
MTPDataValueInput,
MTPClientOptions,
MTPOpenRelayMetadataOptions,
MTPOpenRelayContentOptions,
MTPClient,
MTPSendProtectedOptions,
MTPSendSealedRelayOptions,
} from "../src/sdk/index.js";
const recipients = [new Uint8Array([1])];
const clientOptions: MTPClientOptions = {
url: "https://example.invalid",
credentials: {
clientId: 1,
keyring: recipients[0],
// @ts-expect-error keyringBytes was removed from the stable credential API
keyringBytes: recipients[0],
},
};
const protectedOptions: MTPSendProtectedOptions = {
receiverId: 2,
recipients,
signaturePurpose: 32,
encryptionPurpose: 33,
// @ts-expect-error sendProtected has only receiverId
receiver: 3,
};
const relayOptions: MTPSendSealedRelayOptions = {
finalRecipientId: 2,
nextHopId: 3,
metadataRecipients: recipients,
contentRecipients: recipients,
// @ts-expect-error sealed relay frames never expose an outer sender
sender: 1,
};
const metadataValues: MTPDataValueInput[] = [
null,
true,
42,
42n,
"metadata",
new Uint8Array([1, 2]),
["nested", false],
{ Metadata: "typed container" },
];
const relayReceiveOptions: MTPOpenRelayMetadataOptions = {
resolveSignerPublicKeys: () => recipients,
};
const contentReceiveOptions: MTPOpenRelayContentOptions = {
// @ts-expect-error replayGuard belongs to metadata opening or subscriptions
replayGuard: { accept: () => true },
};
declare const client: MTPClient;
void client.sendProtected("ProtectedMessage", "scalar protected content", protectedOptions);
void client.sendSealedRelay("ProtectedMessage", new Uint8Array([1, 2, 3]), relayOptions);
// @ts-expect-error sealed relay subscriptions accept application type names, not numeric IDs
client.subscribeSealedRelay(32, () => {});
// @ts-expect-error relay receive uses the key-history resolver API
relayReceiveOptions.senderPublicKey = recipients[0];
// @ts-expect-error relay receive selects a verification policy, not a signature suite
relayReceiveOptions.signatureSuite = "dual";
void protectedOptions;
void relayOptions;
void metadataValues;
void relayReceiveOptions;
void contentReceiveOptions;
void clientOptions;

243
test/vite-type-map.mjs Normal file
View file

@ -0,0 +1,243 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import {
chmod,
mkdir,
mkdtemp,
readdir,
readFile,
rename,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import {
copyWasmBuildInputs,
generateTypeMapModule,
hashPackageInputs,
parseTypeMapYaml,
} from "../dist/vite/index.js";
const run = promisify(execFile);
const repositoryRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
const repeatedNames = `
protocol_version: "2.0"
type_maps:
"1.0":
CommunicationTypes:
Message: 32
LegacyMessage: 33
DataTypes:
Payload: 32
LegacyPayload: 33
"2.0":
CommunicationTypes:
Message: 35
CurrentMessage: 36
DataTypes:
Payload: 35
CurrentPayload: 36
`;
const selected = parseTypeMapYaml(repeatedNames, "repeated.yaml");
assert.ok(selected.communicationTypes.includes("Message"));
assert.ok(selected.communicationTypes.includes("CurrentMessage"));
assert.ok(!selected.communicationTypes.includes("LegacyMessage"));
assert.ok(selected.dataTypes.includes("Payload"));
assert.ok(selected.dataTypes.includes("CurrentPayload"));
assert.ok(!selected.dataTypes.includes("LegacyPayload"));
assert.ok(selected.communicationTypes.includes("Ping"));
assert.ok(selected.dataTypes.includes("Version"));
const generated = generateTypeMapModule(selected);
assert.match(generated.dts, /"Message"/);
assert.match(generated.dts, /"CurrentMessage"/);
assert.doesNotMatch(generated.dts, /LegacyMessage/);
assert.doesNotMatch(generated.dts, /LegacyPayload/);
assert.throws(
() =>
parseTypeMapYaml(
`protocol_version: "1.0"\ntype_maps:\n "1.0":\n CommunicationTypes:\n Ping: 32\n`,
"reserved.yaml",
),
/uses a reserved type name/,
);
assert.throws(
() =>
parseTypeMapYaml(
`protocol_version: "2.0"\ntype_maps:\n "1.0": {}\n`,
"missing-version.yaml",
),
/is not defined in type_maps/,
);
assert.throws(
() => parseTypeMapYaml(`protocol_version: "1.0"\ntype_maps: {}`, "empty.yaml"),
/is not defined in type_maps/,
);
assert.throws(
() =>
parseTypeMapYaml(
`protocol_version: "latest"\ntype_maps:\n "latest": {}\n`,
"invalid-version.yaml",
),
/protocol_version must be a string/,
);
const temporaryBuildRoot = await mkdtemp(path.join(os.tmpdir(), "mtp-vite-inputs-"));
try {
await copyWasmBuildInputs(temporaryBuildRoot);
for (const requiredInput of [
"Cargo.lock",
"wasm/Cargo.toml",
"wasm/src/lib.rs",
"common/Cargo.toml",
"codec/Cargo.toml",
"crypto/Cargo.toml",
"type-map/Cargo.toml",
"type-map/build.rs",
"type-map/reserved.json",
]) {
assert.equal(
existsSync(path.join(temporaryBuildRoot, requiredInput)),
true,
`temporary WASM build input is missing: ${requiredInput}`,
);
}
assert.equal(
await readFile(path.join(temporaryBuildRoot, "type-map/reserved.json"), "utf8"),
await readFile(path.join(repositoryRoot, "type-map/reserved.json"), "utf8"),
);
assert.match(
await readFile(path.join(temporaryBuildRoot, "type-map/build.rs"), "utf8"),
/include_str!\("reserved\.json"\)/,
);
} finally {
await rm(temporaryBuildRoot, { recursive: true, force: true });
}
const hashTestRoot = await mkdtemp(path.join(os.tmpdir(), "mtp-vite-hash-"));
try {
await mkdir(path.join(hashTestRoot, "type-map"), { recursive: true });
const manifestPath = path.join(hashTestRoot, "type-map/reserved.json");
await writeFile(manifestPath, "{\"version\":1}");
const firstHash = await hashPackageInputs(hashTestRoot);
await writeFile(manifestPath, "{\"version\":2}");
const secondHash = await hashPackageInputs(hashTestRoot);
assert.notEqual(firstHash, secondHash);
} finally {
await rm(hashTestRoot, { recursive: true, force: true });
}
const viteCandidates = [
path.join(repositoryRoot, "example/web-client/node_modules/.bin/vite"),
path.join(repositoryRoot, "node_modules/.bin/vite"),
];
const viteBin = viteCandidates.find((candidate) => existsSync(candidate));
if (!viteBin) {
console.warn("Skipping packed Vite smoke test: Vite is not installed");
} else {
const temporaryPackageRoot = await mkdtemp(path.join(os.tmpdir(), "mtp-vite-package-"));
try {
await run(
"npm",
["pack", "--pack-destination", temporaryPackageRoot],
{
cwd: repositoryRoot,
env: {
...process.env,
npm_config_cache: path.join(temporaryPackageRoot, "npm-cache"),
},
},
);
const packageName = (await readdir(temporaryPackageRoot)).find((entry) =>
entry.endsWith(".tgz"),
);
assert.ok(packageName, "npm pack did not report a tarball");
const extractedRoot = path.join(temporaryPackageRoot, "extracted");
const appRoot = path.join(temporaryPackageRoot, "app");
await mkdir(extractedRoot, { recursive: true });
await run("tar", ["-xzf", path.join(temporaryPackageRoot, packageName), "-C", extractedRoot]);
await mkdir(path.join(appRoot, "node_modules"), { recursive: true });
await rename(path.join(extractedRoot, "package"), path.join(appRoot, "node_modules/mtp"));
await symlink(
path.join(repositoryRoot, "node_modules/yaml"),
path.join(appRoot, "node_modules/yaml"),
"dir",
);
await symlink(
path.join(path.dirname(path.dirname(viteBin)), "vite"),
path.join(appRoot, "node_modules/vite"),
"dir",
);
await writeFile(
path.join(appRoot, "package.json"),
JSON.stringify({ type: "module", private: true }),
);
await writeFile(
path.join(appRoot, "index.html"),
'<script type="module" src="/main.js"></script>',
);
await writeFile(
path.join(appRoot, "main.js"),
'import { communicationTypes } from "mtp/type-map";\n' +
'if (!communicationTypes.includes("CurrentMessage") || communicationTypes.includes("LegacyMessage")) throw new Error("wrong browser type-map selection");\n',
);
await writeFile(
path.join(appRoot, "type-maps.yaml"),
repeatedNames,
);
await writeFile(
path.join(appRoot, "vite.config.mjs"),
'import { defineConfig } from "vite";\n' +
'import { mtp } from "mtp/vite";\n' +
'export default defineConfig({ plugins: [mtp({ typeMaps: "./type-maps.yaml", release: false })] });\n',
);
const fakeBin = path.join(temporaryPackageRoot, "bin");
const fakeWasmPack = path.join(fakeBin, "wasm-pack");
await mkdir(fakeBin, { recursive: true });
await writeFile(
fakeWasmPack,
`#!/usr/bin/env node
const fs = require("node:fs");
const path = require("node:path");
const args = process.argv.slice(2);
const outIndex = args.indexOf("--out-dir");
if (outIndex < 0) throw new Error("fake wasm-pack did not receive --out-dir");
if (!fs.existsSync(path.join(process.cwd(), "type-map", "reserved.json"))) {
throw new Error("temporary wasm build is missing type-map/reserved.json");
}
const outDir = args[outIndex + 1];
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, "mtp_wasm.js"), "export default function init() {}\\n");
fs.writeFileSync(path.join(outDir, "mtp_wasm_bg.wasm"), Buffer.from([0, 97, 115, 109, 1, 0, 0, 0]));
`,
);
await chmod(fakeWasmPack, 0o755);
await run(viteBin, ["build", "--config", "vite.config.mjs"], {
cwd: appRoot,
env: {
...process.env,
PATH: `${fakeBin}${path.delimiter}${process.env.PATH ?? ""}`,
},
});
assert.equal(existsSync(path.join(appRoot, "dist/index.html")), true);
} finally {
await rm(temporaryPackageRoot, { recursive: true, force: true });
}
}
console.log("Vite type-map tests passed");