-
Notifications
You must be signed in to change notification settings - Fork 0
chore: deterministic fuzz, DRY chunk verify, DRY KDF metadata #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /** | ||
| * Builds the KDF metadata object for vault/manifest encryption metadata. | ||
| * | ||
| * @param {Buffer} salt - KDF salt. | ||
| * @param {{ algorithm: string, keyLength: number, iterations?: number, cost?: number, blockSize?: number, parallelization?: number }} params - KDF parameters. | ||
| * @returns {{ algorithm: string, salt: string, keyLength: number, iterations?: number, cost?: number, blockSize?: number, parallelization?: number }} | ||
| */ | ||
| export default function buildKdfMetadata(salt, params) { | ||
| return { | ||
| algorithm: params.algorithm, | ||
| salt: salt.toString('base64'), | ||
| ...('iterations' in params && { iterations: params.iterations }), | ||
| ...('cost' in params && { cost: params.cost }), | ||
| ...('blockSize' in params && { blockSize: params.blockSize }), | ||
| ...('parallelization' in params && { parallelization: params.parallelization }), | ||
| keyLength: params.keyLength, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import buildKdfMetadata from '../../../../src/domain/helpers/buildKdfMetadata.js'; | ||
|
|
||
| describe('buildKdfMetadata', () => { | ||
| it('builds PBKDF2 metadata with iterations', () => { | ||
| const salt = Buffer.from('test-salt'); | ||
| const params = { algorithm: 'pbkdf2', iterations: 100000, keyLength: 32 }; | ||
|
|
||
| const result = buildKdfMetadata(salt, params); | ||
|
|
||
| expect(result).toEqual({ | ||
| algorithm: 'pbkdf2', | ||
| salt: salt.toString('base64'), | ||
| iterations: 100000, | ||
| keyLength: 32, | ||
| }); | ||
| }); | ||
|
|
||
| it('builds scrypt metadata with cost/blockSize/parallelization', () => { | ||
| const salt = Buffer.from('scrypt-salt'); | ||
| const params = { algorithm: 'scrypt', cost: 16384, blockSize: 8, parallelization: 1, keyLength: 32 }; | ||
|
|
||
| const result = buildKdfMetadata(salt, params); | ||
|
|
||
| expect(result).toEqual({ | ||
| algorithm: 'scrypt', | ||
| salt: salt.toString('base64'), | ||
| cost: 16384, | ||
| blockSize: 8, | ||
| parallelization: 1, | ||
| keyLength: 32, | ||
| }); | ||
| }); | ||
|
|
||
| it('omits optional fields when absent', () => { | ||
| const salt = Buffer.from('minimal'); | ||
| const params = { algorithm: 'pbkdf2', keyLength: 32 }; | ||
|
|
||
| const result = buildKdfMetadata(salt, params); | ||
|
|
||
| expect(result).toEqual({ | ||
| algorithm: 'pbkdf2', | ||
| salt: salt.toString('base64'), | ||
| keyLength: 32, | ||
| }); | ||
| expect(result).not.toHaveProperty('iterations'); | ||
| expect(result).not.toHaveProperty('cost'); | ||
| expect(result).not.toHaveProperty('blockSize'); | ||
| expect(result).not.toHaveProperty('parallelization'); | ||
| }); | ||
| }); | ||
|
Comment on lines
+1
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ Verification inconclusive🏁 Script executed: #!/bin/bash
set -e
# Check repository structure
echo "=== Repository Structure ==="
ls -la
echo ""
echo "=== Checking for package.json and vitest config ==="
if [ -f package.json ]; then
cat package.json | head -30
fi
echo ""
echo "=== Checking test directory ==="
find test -type f -name "*.test.js" -o -name "*.test.ts" | head -10Repository: git-stunts/git-cas Length of output: 3534 🏁 Script executed: #!/bin/bash
set -e
# Try Node.js 22.x test run
echo "=== Running npm test (Node.js) ==="
npm test 2>&1 | tail -50 || echo "Node test exit code: $?"Repository: git-stunts/git-cas Length of output: 179 🏁 Script executed: #!/bin/bash
# Try Bun test run
echo "=== Running bunx vitest run test/unit (Bun) ==="
bunx vitest run test/unit 2>&1 | tail -50 || echo "Bun test exit code: $?"Repository: git-stunts/git-cas Length of output: 152 🏁 Script executed: #!/bin/bash
# Try Deno test run
echo "=== Running deno run -A npm:vitest run test/unit (Deno) ==="
deno run -A npm:vitest run test/unit 2>&1 | tail -50 || echo "Deno test exit code: $?"Repository: git-stunts/git-cas Length of output: 164 Bun and Deno unit tests must pass before merge. Per the test matrix requirements,
This change cannot merge until verified on all three runtimes. 🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. False positive — CI already passed on all three runtimes (Node, Bun, Deno). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query: 💡 Result: Yes. In the Bun runtime, 🌐 Web query: 💡 Result:
Recommendation: use Sources: [1] [2] [3]
The web search confirms that Buffer is globally available in Bun, and when Deno runs tests via Thank you for the correction—I've noted this for future reviews. 🧠 Learnings used |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,19 @@ import CasError from '../../../../src/domain/errors/CasError.js'; | |
|
|
||
| const testCrypto = await getTestCryptoAdapter(); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Deterministic PRNG (xorshift32) — keeps fuzz tests reproducible | ||
| // --------------------------------------------------------------------------- | ||
| function createSeededRng(seed = 42) { | ||
| let s = seed >>> 0 || 1; | ||
| return (max) => { | ||
| s ^= s << 13; | ||
| s ^= s >>> 17; | ||
| s ^= s << 5; | ||
| return (s >>> 0) % max; | ||
| }; | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Helpers | ||
| // --------------------------------------------------------------------------- | ||
|
|
@@ -310,8 +323,9 @@ describe('CasService – envelope encryption (fuzz round-trips)', () => { | |
| beforeEach(() => { ({ service } = setup()); }); | ||
|
|
||
| it('50 random plaintexts × 3 random KEKs all round-trip', async () => { | ||
| const rng = createSeededRng(12345); | ||
| for (let i = 0; i < 50; i++) { | ||
| const size = Math.floor(Math.random() * 4096); | ||
| const size = rng(4096); | ||
| const original = randomBytes(size); | ||
| const keys = [randomBytes(32), randomBytes(32), randomBytes(32)]; | ||
|
Comment on lines
+326
to
330
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fuzz round-trip is still partially non-deterministic because test data/keys use If a failure depends on byte content, it won’t be exactly reproducible across runs. 💡 Suggested fix to make fuzz inputs fully deterministic function createSeededRng(seed = 42) {
let s = seed >>> 0 || 1;
return (max) => {
s ^= s << 13;
s ^= s >>> 17;
s ^= s << 5;
return (s >>> 0) % max;
};
}
+
+function seededBytes(rng, size) {
+ const buf = Buffer.alloc(size);
+ for (let i = 0; i < size; i++) {
+ buf[i] = rng(256);
+ }
+ return buf;
+}
@@
it('50 random plaintexts × 3 random KEKs all round-trip', async () => {
const rng = createSeededRng(12345);
for (let i = 0; i < 50; i++) {
const size = rng(4096);
- const original = randomBytes(size);
- const keys = [randomBytes(32), randomBytes(32), randomBytes(32)];
+ const original = seededBytes(rng, size);
+ const keys = [seededBytes(rng, 32), seededBytes(rng, 32), seededBytes(rng, 32)];
@@
const idx = rng(3);
const { buffer } = await service.restore({ manifest, encryptionKey: keys[idx] });
expect(buffer.equals(original)).toBe(true);
}
});Also applies to: 339-340 🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intentional design — The seeded PRNG targets only the control flow variables (plaintext size selection and recipient index selection) — these are the values that determine which code path executes, and making them deterministic ensures failures in those paths are reproducible. The actual cryptographic operations remain properly random. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐰✨ ✏️ Learnings added
|
||
|
|
||
|
|
@@ -322,7 +336,7 @@ describe('CasService – envelope encryption (fuzz round-trips)', () => { | |
| recipients: keys.map((key, j) => ({ label: `k${j}`, key })), | ||
| }); | ||
|
|
||
| const idx = Math.floor(Math.random() * 3); | ||
| const idx = rng(3); | ||
| const { buffer } = await service.restore({ manifest, encryptionKey: keys[idx] }); | ||
| expect(buffer.equals(original)).toBe(true); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.