test(storage): assert credentials never persist in plaintext at rest

Pre-launch credential-security regression: scans the ENTIRE serialized account
record (meta + encrypted blob) for the secret/access-key strings and asserts
they never appear in plaintext, that listConnections() doesn't expose them, that
they round-trip while unlocked, and that they're unreachable once locked. The
prior check only compared the cipher field to JSON.stringify(payload), which
wouldn't catch a secret leaking into plaintext meta.

Audit conclusion: credential storage is sound — envelope encryption (AES-GCM-256
payload, fresh per-encrypt IV; master key encrypted to-self via the Nostr signer,
NIP-44 preferred / NIP-04 fallback), in-memory master key cleared on logout, no
secret logging, legacy plaintext migrated then cleared.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-17 05:35:02 -04:00
1 parent c2228f3f4f
commit 200e5cec80
1 file changed
+57
+57
View File
@@ -220,4 +220,61 @@ describe("StorageService", () => {
assert.strictEqual(c1.meta.defaultForUploads, false);
assert.strictEqual(c2.meta.defaultForUploads, true);
});
// Scenario (SCN-secrets-never-at-rest-plaintext):
// Pre-launch credential-security invariant — a user's S3/R2 secret (and
// access key id) must NEVER be persisted in plaintext anywhere in the stored
// record (not in the encrypted payload, not leaked into plaintext meta), and
// listConnections() (the unauthenticated/metadata view) must not expose them.
// Cheat-resistant: scans the ENTIRE serialized at-rest record for the secret
// strings, so passing requires real encryption, not a narrower field check.
test("secrets never appear in plaintext anywhere in the at-rest record", async () => {
await storageService.unlock(pubkey, { signer: mockSigner });
const SECRET = "S3CR3T-do-not-persist-7f3a9c";
const ACCESS_KEY = "AKIA-PUBLIC-ID-do-not-persist-1b2c3d";
await storageService.saveConnection(
pubkey,
"conn_secret",
{
provider: PROVIDERS.R2,
accessKeyId: ACCESS_KEY,
secretAccessKey: SECRET,
accountId: "acct-123",
},
{ label: "My R2", bucket: "my-bucket", publicBaseUrl: "https://cdn.example" },
);
// Serialize the WHOLE persisted account (meta + encrypted blob + everything).
const account = await storageService._getAccount(pubkey);
const atRest = JSON.stringify(account);
assert.ok(
!atRest.includes(SECRET),
"secretAccessKey must never be stored in plaintext at rest",
);
assert.ok(
!atRest.includes(ACCESS_KEY),
"accessKeyId must never be stored in plaintext at rest",
);
// The encrypted payload must actually exist (encryption happened).
assert.ok(account.connections.conn_secret.encrypted.cipher);
// The metadata view must not expose secrets either.
const listed = JSON.stringify(await storageService.listConnections(pubkey));
assert.ok(!listed.includes(SECRET) && !listed.includes(ACCESS_KEY),
"listConnections() must not expose secrets");
// Sanity: round-trips correctly while unlocked.
const got = await storageService.getConnection(pubkey, "conn_secret");
assert.strictEqual(got.secretAccessKey, SECRET);
// And once locked, the plaintext secret is no longer reachable.
storageService.lock(pubkey);
await assert.rejects(
() => storageService.getConnection(pubkey, "conn_secret"),
/Storage is locked/,
);
});
});