feat(fuzz): add fuzzing suite for schemas, dms, and magnets

- Add `scripts/agent/fuzz-nostr-schemas.mjs` to fuzz event builders and validation
- Add `scripts/agent/fuzz-dm-decryptor.mjs` to fuzz DM unwrapping
- Add `scripts/agent/fuzz-magnet-utils.mjs` to fuzz magnet normalization
- Utilize `scripts/agent/fuzz-lib.mjs` for randomization and reporting
- Verify that `scripts/agent/setup-test-env.js` is imported to polyfill globals
- Output reports to `artifacts/`

No crashes were found during the initial fuzzing run (5000 iterations per target).
This commit is contained in:
google-labs-jules[bot] committed 2026-01-21 16:34:50 +00:00
1 parent a99a9f3231
commit 3e38916f35
3 files changed
+172 -180

No files matched your search

+76 -64
View File
@@ -1,72 +1,84 @@
import "./setup-test-env.js";
import { Fuzzer } from "./fuzz-lib.mjs";
import { decryptDM } from "../../js/dmDecryptor.js";
// Mock global dependencies
if (typeof global.window === "undefined") {
global.window = {
location: { protocol: "https:" },
crypto: {
getRandomValues: (arr) => crypto.randomFillSync(arr),
subtle: {
digest: async (algo, data) => {
const hash = crypto.createHash("sha256").update(data).digest();
return hash.buffer;
}
}
}
};
}
if (typeof global.localStorage === "undefined") {
global.localStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {}
};
}
const fuzzer = new Fuzzer("dm-decryptor");
async function test(fuzzer, state) {
// Generate random event
const event = {
kind: fuzzer.randBool() ? fuzzer.pick([4, 1059]) : fuzzer.randInt(0, 20000), // 4=DM, 1059=GiftWrap
created_at: Math.floor(Date.now() / 1000),
tags: fuzzer.randArray(() => {
const tagType = fuzzer.pick(["p", "e", "encrypted"]);
if (tagType === "p") return ["p", fuzzer.randString(64, "0123456789abcdef")];
if (tagType === "encrypted") return ["encrypted", fuzzer.pick(["nip04", "nip44", "nip44_v2"])];
return [fuzzer.randString(5), fuzzer.randString(10)];
}, 0, 5),
content: fuzzer.randUnicodeString(fuzzer.randInt(0, 200)),
pubkey: fuzzer.randString(64, "0123456789abcdef"),
id: fuzzer.randString(64, "0123456789abcdef")
};
class MockDecryptor {
constructor(scheme, shouldFail = false) {
this.scheme = scheme;
this.shouldFail = shouldFail;
this.supportsGiftWrap = scheme === "nip44" || scheme === "nip44_v2";
}
if (fuzzer.randBool()) event.content = fuzzer.randJSON(); // Garbage content
// Mock decryptors
const decryptors = fuzzer.randArray(() => {
return {
decrypt: async (pubkey, ciphertext) => {
if (fuzzer.randBool()) throw new Error("Decrypt failed");
// Return random valid JSON or garbage
if (fuzzer.randBool()) return JSON.stringify(fuzzer.randJSON());
return fuzzer.randUnicodeString(50);
},
scheme: fuzzer.pick(["nip04", "nip44", "nip44_v2", "unknown"]),
priority: fuzzer.randInt(0, 10),
supportsGiftWrap: fuzzer.randBool()
};
}, 0, 3);
const context = {
actorPubkey: fuzzer.randString(64, "0123456789abcdef"),
decryptors: decryptors
};
state.input = { event, context };
await decryptDM(event, context);
async decrypt(pubkey, ciphertext) {
if (this.shouldFail) {
throw new Error("Decryption failed (mock)");
}
// Return something that might be JSON or might be garbage
if (fuzzer.randBool()) {
// Return valid JSON event string (inner rumor)
return JSON.stringify({
kind: 1,
content: "Hello world " + fuzzer.randString(10),
pubkey: fuzzer.randString(64, "0123456789abcdef"),
created_at: Math.floor(Date.now() / 1000),
tags: []
});
} else {
// Return garbage string
return fuzzer.randString(50);
}
}
}
fuzzer.runFuzzLoop(2000, test);
async function fuzzTest(fuzzer, state) {
const schemes = ["nip04", "nip44", "nip44_v2", "unknown"];
// Create decryptors
const decryptors = [];
const numDecryptors = fuzzer.randInt(0, 3);
for(let i=0; i<numDecryptors; i++) {
decryptors.push(new MockDecryptor(fuzzer.pick(schemes), fuzzer.randBool()));
}
// Create event
// kinds: 4 (legacy), 1059 (gift wrap), random
const kinds = [4, 1059, fuzzer.randInt(0, 20000)];
const kind = fuzzer.pick(kinds);
const event = {
kind: kind,
pubkey: fuzzer.randString(64, "0123456789abcdef"),
content: fuzzer.randString(100), // ciphertext
created_at: Math.floor(Date.now() / 1000),
tags: []
};
// Add random tags
const numTags = fuzzer.randInt(0, 5);
for(let i=0; i<numTags; i++) {
event.tags.push(fuzzer.randArray(() => fuzzer.randString(10), 1, 4));
}
// Context
const context = {
actorPubkey: fuzzer.randString(64, "0123456789abcdef"),
decryptors: decryptors
};
state.input = {
event,
context
};
try {
await decryptDM(event, context);
} catch (err) {
// We expect decryptDM to handle errors gracefully and return { ok: false, errors: ... }
// If it throws, it's a bug (except maybe argument validation if we pass null event)
throw err;
}
}
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);