Skip to content

Commit a9cf5d3

Browse files
NathanFlurryclaude
andcommitted
feat: US-026 - Fix pg auth method discrepancy between local and CI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7292809 commit a9cf5d3

6 files changed

Lines changed: 57 additions & 6 deletions

File tree

packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,23 @@
10511051
});
10521052
};
10531053

1054+
SandboxSubtle.deriveBits = function deriveBits(algorithm, baseKey, length) {
1055+
return Promise.resolve().then(function() {
1056+
var algo = normalizeAlgo(algorithm);
1057+
var reqAlgo = Object.assign({}, algo);
1058+
if (reqAlgo.hash) reqAlgo.hash = normalizeAlgo(reqAlgo.hash);
1059+
if (reqAlgo.salt) reqAlgo.salt = toBase64(reqAlgo.salt);
1060+
var result2 = JSON.parse(subtleCall({
1061+
op: 'deriveBits',
1062+
algorithm: reqAlgo,
1063+
key: baseKey._keyData,
1064+
length: length,
1065+
}));
1066+
var buf = Buffer.from(result2.data, 'base64');
1067+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
1068+
});
1069+
};
1070+
10541071
SandboxSubtle.sign = function sign(algorithm, key, data) {
10551072
return Promise.resolve().then(function() {
10561073
var result2 = JSON.parse(subtleCall({

packages/secure-exec-core/src/generated/isolate-runtime.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/secure-exec-node/src/bridge-setup.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,27 @@ export async function setupRequire(
889889
}
890890
throw new Error(`Unsupported decrypt algorithm: ${algoName}`);
891891
}
892+
case "deriveBits": {
893+
const { algorithm, key, length } = req;
894+
const algoName = algorithm.name;
895+
if (algoName === "PBKDF2") {
896+
const password = Buffer.from(key._raw, "base64");
897+
const salt = Buffer.from(algorithm.salt, "base64");
898+
const iterations = algorithm.iterations;
899+
const hashAlgo = normalizeHash(algorithm.hash);
900+
const keylen = length / 8;
901+
return JSON.stringify({
902+
data: pbkdf2Sync(
903+
password,
904+
salt,
905+
iterations,
906+
keylen,
907+
hashAlgo,
908+
).toString("base64"),
909+
});
910+
}
911+
throw new Error(`Unsupported deriveBits algorithm: ${algoName}`);
912+
}
892913
case "sign": {
893914
const { key, data } = req;
894915
const dataBytes = Buffer.from(data, "base64");

packages/secure-exec/tests/e2e-docker.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ describe.skipIf(skipReason)("e2e-docker", () => {
172172
POSTGRES_USER: "testuser",
173173
POSTGRES_PASSWORD: "testpass",
174174
POSTGRES_DB: "testdb",
175-
POSTGRES_HOST_AUTH_METHOD: "trust",
176175
},
177176
healthCheck: ["pg_isready", "-U", "testuser", "-d", "testdb"],
178177
healthCheckTimeout: 30_000,

scripts/ralph/prd.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,8 @@
464464
"Typecheck passes"
465465
],
466466
"priority": 26,
467-
"passes": false,
468-
"notes": "Local uses POSTGRES_HOST_AUTH_METHOD: trust (password bypassed), CI defaults to scram-sha-256. This means the SCRAM crypto path is only tested in CI and never locally. If a developer breaks crypto support needed for SCRAM, they won't catch it locally."
467+
"passes": true,
468+
"notes": "Fixed: Removed POSTGRES_HOST_AUTH_METHOD: trust from local Docker setup. Implemented subtle.deriveBits (PBKDF2) in both guest-side SandboxSubtle and host-side bridge dispatcher. Local and CI now both use scram-sha-256 authentication. All pg fixtures pass with SCRAM auth."
469469
},
470470
{
471471
"id": "US-027",

scripts/ralph/progress.txt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ PRD: ralph/cli-tool-sandbox-tests (7 stories)
1919
- `createHostCommandExecutor()` wraps `node:child_process.spawn` as a `CommandExecutor` for sandbox bash tool tests
2020
- TCP socket bridge: guest calls `_netSocketConnectRaw.applySync()` → host creates real `net.Socket` → events dispatched back via `_netSocketDispatch` applySync callback
2121
- `wrapNetworkAdapter` in `permissions.ts` must forward ALL adapter methods — new methods added to NetworkAdapter interface need corresponding forwarding in the wrapper
22-
- pg library uses Web Crypto `subtle.deriveBits` for SCRAM-SHA-256 auth — this is NOT implemented in sandbox; use `POSTGRES_HOST_AUTH_METHOD: "trust"` to bypass
22+
- pg library uses Web Crypto `subtle.deriveBits` for SCRAM-SHA-256 auth — implemented in sandbox crypto.subtle bridge (guest: SandboxSubtle.deriveBits in require-setup.ts, host: deriveBits case in bridge-setup.ts using pbkdf2Sync)
2323

2424
---
2525

@@ -60,7 +60,21 @@ PRD: ralph/cli-tool-sandbox-tests (7 stories)
6060
- CRITICAL: `wrapNetworkAdapter` in `permissions.ts` must forward ALL new adapter methods — this was the root cause of a silent failure where the adapter's `netSocketConnect` was never called
6161
- The net module was previously a "deferred core module" stub (Proxy that throws on any method call) — must remove from `_deferredCoreModules` set when implementing
6262
- `loadPolyfillRef` in `bridge-setup.ts` must also return null for `net` to prevent node-stdlib-browser's stub from being loaded
63-
- Postgres 16 defaults to SCRAM-SHA-256 auth which needs `subtle.deriveBits` — not available in sandbox yet; `trust` auth is the workaround
63+
- Postgres 16 defaults to SCRAM-SHA-256 auth which needs `subtle.deriveBits` — now implemented in sandbox crypto.subtle bridge
6464
- The adapter's socketId counter is reused from `nextUpgradeSocketId` — bridge-setup must use the adapter's returned socketId, not its own counter
6565
---
6666

67+
## 2026-03-19T20:46 - US-026
68+
- Implemented `subtle.deriveBits` in sandbox crypto bridge to support SCRAM-SHA-256 Postgres authentication
69+
- Removed `POSTGRES_HOST_AUTH_METHOD: "trust"` from local Docker setup so local and CI both use scram-sha-256
70+
- Files changed:
71+
- `packages/secure-exec-node/src/bridge-setup.ts` — added `deriveBits` case to host-side crypto.subtle dispatcher (uses Node's `pbkdf2Sync`)
72+
- `packages/secure-exec-core/isolate-runtime/src/inject/require-setup.ts` — added `SandboxSubtle.deriveBits` guest-side implementation
73+
- `packages/secure-exec/tests/e2e-docker.test.ts` — removed `POSTGRES_HOST_AUTH_METHOD: "trust"` from Postgres container env
74+
- `scripts/ralph/progress.txt` — updated codebase patterns
75+
- **Learnings for future iterations:**
76+
- `subtle.deriveBits` for PBKDF2 maps directly to Node's `pbkdf2Sync(password, salt, iterations, keylen, digest)` — the algorithm object carries salt/iterations/hash, the key carries the password in `_raw`
77+
- Guest-side `SandboxSubtle` methods must serialize algorithm-specific fields (salt as base64, hash as normalized name) before crossing the bridge
78+
- After adding a new subtle operation, the bridge/isolate-runtime must be rebuilt (`pnpm turbo build`) and fixture caches cleared to pick up the regenerated code
79+
---
80+

0 commit comments

Comments
 (0)